tsmppt60-driver 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tsmppt60_driver/__init__.py +106 -0
- tsmppt60_driver/base.py +266 -0
- tsmppt60_driver/status.py +319 -0
- tsmppt60_driver-0.2.2.dist-info/METADATA +105 -0
- tsmppt60_driver-0.2.2.dist-info/RECORD +6 -0
- tsmppt60_driver-0.2.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from tsmppt60_driver.base import ManagementBase
|
|
2
|
+
from tsmppt60_driver.status import (
|
|
3
|
+
BatteryStatus,
|
|
4
|
+
CountersStatus,
|
|
5
|
+
OperatingConditions,
|
|
6
|
+
SolarArrayStatus,
|
|
7
|
+
TemperaturesStatus,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
"""TS-MPPT-60 driver library to get all devices status data."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SystemStatus(object):
|
|
15
|
+
"""This is class to get the system status of TS-MPPT-60. Use this like below with dict object.
|
|
16
|
+
|
|
17
|
+
print(SystemStatus("192.168.1.20").get())
|
|
18
|
+
|
|
19
|
+
{'Amp Hours': {'group': 'Counter', 'unit': 'Ah', 'value': 18097.9},
|
|
20
|
+
'Array Current': {'group': 'Array', 'unit': 'A', 'value': 1.4},
|
|
21
|
+
'Array Voltage': {'group': 'Array', 'unit': 'V', 'value': 53.41},
|
|
22
|
+
'Battery Voltage': {'group': 'Battery', 'unit': 'V', 'value': 23.93},
|
|
23
|
+
'Charge Current': {'group': 'Battery', 'unit': 'A', 'value': 3.2},
|
|
24
|
+
'Heat Sink Temperature': {'group': 'Temperature', 'unit': 'C', ...},
|
|
25
|
+
'Kilowatt Hours': {'group': 'Counter', 'unit': 'kWh', 'value': 237.0},
|
|
26
|
+
'Target Voltage': {'group': 'Battery', 'unit': 'V', 'value': 28.6}}
|
|
27
|
+
|
|
28
|
+
The above data is limited information. You can disable the limitter
|
|
29
|
+
like below.
|
|
30
|
+
|
|
31
|
+
print(SystemStatus("192.168.1.20").get(False))
|
|
32
|
+
|
|
33
|
+
{'Amp Hours': {'group': 'Counter', 'unit': 'Ah', 'value': 18097.8},
|
|
34
|
+
'Array Current': {'group': 'Array', 'unit': 'A', 'value': 1.3},
|
|
35
|
+
'Array Voltage': {'group': 'Array', 'unit': 'V', 'value': 53.41},
|
|
36
|
+
'Battery Temperature': {'group': 'Temperature', 'unit': 'C', ...},
|
|
37
|
+
'Battery Voltage': {'group': 'Battery', 'unit': 'V', 'value': 24.01},
|
|
38
|
+
'Charge Current': {'group': 'Battery', 'unit': 'A', 'value': 3.2},
|
|
39
|
+
'Heat Sink Temperature': {'group': 'Temperature', 'unit': 'C', ...},
|
|
40
|
+
'Kilowatt Hours': {'group': 'Counter', 'unit': 'kWh', 'value': 237.0},
|
|
41
|
+
'Output Power': {'group': 'Battery', 'unit': 'W', 'value': 76.0},
|
|
42
|
+
'Sweep Pmax': {'group': 'Array', 'unit': 'W', 'value': 73.0},
|
|
43
|
+
'Sweep Vmp': {'group': 'Array', 'unit': 'V', 'value': 53.41},
|
|
44
|
+
'Sweep Voc': {'group': 'Array', 'unit': 'V', 'value': 60.05},
|
|
45
|
+
'Target Voltage': {'group': 'Battery', 'unit': 'V', 'value': 28.6},
|
|
46
|
+
'LED State': {'group': 'Condition', 'value': 11, 'unit': ''},
|
|
47
|
+
'Charge State': {'group': 'Condition', 'value': 3, 'unit': ''}}
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, host):
|
|
51
|
+
"""Initialize class object.
|
|
52
|
+
|
|
53
|
+
Keyword arguments:
|
|
54
|
+
host -- TS-MPPT-60 host address like "192.168.1.20"
|
|
55
|
+
"""
|
|
56
|
+
_mb = ManagementBase(host)
|
|
57
|
+
|
|
58
|
+
self._devices = (
|
|
59
|
+
BatteryStatus(_mb),
|
|
60
|
+
SolarArrayStatus(_mb),
|
|
61
|
+
TemperaturesStatus(_mb),
|
|
62
|
+
CountersStatus(_mb),
|
|
63
|
+
OperatingConditions(_mb),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def get(self, is_limit=True):
|
|
67
|
+
"""Get and return all status of devices like the below dict.
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
"Battery Voltage":{
|
|
71
|
+
"group": "Battery",
|
|
72
|
+
"value": 12.1,
|
|
73
|
+
"unit": "V"},
|
|
74
|
+
"Charge Current":{
|
|
75
|
+
"group": "Battery",
|
|
76
|
+
"value": 8.4,
|
|
77
|
+
"unit": "A"}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Keyword arguments:
|
|
81
|
+
is_limit -- limit the number of getting status
|
|
82
|
+
"""
|
|
83
|
+
status_dict = {}
|
|
84
|
+
|
|
85
|
+
for device in self._devices:
|
|
86
|
+
for status in device.get_status_all(is_limit):
|
|
87
|
+
label = status.pop("label")
|
|
88
|
+
status_dict[label] = status
|
|
89
|
+
|
|
90
|
+
return status_dict
|
|
91
|
+
|
|
92
|
+
def __len__(self):
|
|
93
|
+
return len(self._devices)
|
|
94
|
+
|
|
95
|
+
def __iter__(self):
|
|
96
|
+
self._index = 0
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def __next__(self):
|
|
100
|
+
if self._index >= len(self._devices):
|
|
101
|
+
raise StopIteration
|
|
102
|
+
|
|
103
|
+
stat_obj = self._devices[self._index]
|
|
104
|
+
self._index += 1
|
|
105
|
+
|
|
106
|
+
return stat_obj
|
tsmppt60_driver/base.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
"""TS-MPPT-60 driver's base modules."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Logger(object):
|
|
10
|
+
"""Logger base class for this module."""
|
|
11
|
+
|
|
12
|
+
_FORMAT_LOG_MSG = "%(asctime)s %(name)s %(levelname)s: %(message)s"
|
|
13
|
+
_FORMAT_LOG_DATE = "%Y/%m/%d %p %l:%M:%S"
|
|
14
|
+
|
|
15
|
+
def __init__(self, log_file_path=None, debug=False):
|
|
16
|
+
"""Initialize Logger class object.
|
|
17
|
+
|
|
18
|
+
Keyword arguments:
|
|
19
|
+
log_file_path -- Path to record log file.
|
|
20
|
+
debug -- If True, logging is enabled.
|
|
21
|
+
"""
|
|
22
|
+
self.logger = logging.getLogger(type(self).__name__)
|
|
23
|
+
|
|
24
|
+
handler = logging.StreamHandler()
|
|
25
|
+
formatter = logging.Formatter(fmt=self._FORMAT_LOG_MSG, datefmt=self._FORMAT_LOG_DATE)
|
|
26
|
+
handler.setFormatter(formatter)
|
|
27
|
+
self.logger.addHandler(handler)
|
|
28
|
+
|
|
29
|
+
if log_file_path:
|
|
30
|
+
handler = logging.FileHandler(log_file_path, mode="a")
|
|
31
|
+
handler.setFormatter(formatter)
|
|
32
|
+
self.logger.addHandler(handler)
|
|
33
|
+
|
|
34
|
+
if debug:
|
|
35
|
+
self.logger.setLevel(logging.DEBUG)
|
|
36
|
+
else:
|
|
37
|
+
self.logger.setLevel(logging.INFO)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ModbusRegisterTable(object):
|
|
41
|
+
"""MODBUS register settings table."""
|
|
42
|
+
|
|
43
|
+
VOLTAGE_SCALING_HIGH = (0x0000, "", "Voltage Scaling High", 1)
|
|
44
|
+
VOLTAGE_SCALING_LOW = (0x0001, "", "Voltage Scaling Low", 1)
|
|
45
|
+
CURRENT_SCALING_HIGH = (0x0002, "", "Current Scaling High", 1)
|
|
46
|
+
CURRENT_SCALING_LOW = (0x0003, "", "Current Scaling Low", 1)
|
|
47
|
+
VOLTAGE_SCALING = (0x0000, "", "Voltage Scaling", 2)
|
|
48
|
+
CURRENT_SCALING = (0x0002, "", "Current Scaling", 2)
|
|
49
|
+
SOFTWARE_VERSION = (0x0004, "Numbers", "Software Version", 1)
|
|
50
|
+
|
|
51
|
+
BATTERY_VOLTAGE = (0x0026, "V", "Battery Voltage", 1)
|
|
52
|
+
CHARGING_CURRENT = (0x0027, "A", "Charge Current", 1)
|
|
53
|
+
TARGET_REGULATION_VOLTAGE = (0x0033, "V", "Target Voltage", 1)
|
|
54
|
+
OUTPUT_POWER = (0x003A, "W", "Output Power", 1)
|
|
55
|
+
ARRAY_VOLTAGE = (0x001B, "V", "Array Voltage", 1)
|
|
56
|
+
ARRAY_CURRENT = (0x001D, "A", "Array Current", 1)
|
|
57
|
+
VMP_LAST_SWEEP = (0x003D, "V", "Sweep Vmp", 1)
|
|
58
|
+
VOC_LAST_SWEEP = (0x003E, "V", "Sweep Voc", 1)
|
|
59
|
+
POWER_LAST_SWEEP = (0x003C, "W", "Sweep Pmax", 1)
|
|
60
|
+
HEATSINK_TEMP = (0x0023, "C", "Heat Sink Temperature", 1)
|
|
61
|
+
BATTERY_TEMP = (0x0025, "C", "Battery Temperature", 1)
|
|
62
|
+
AH_CHARGE_RESETABLE = (0x0034, "Ah", "Amp Hours", 2)
|
|
63
|
+
KWH_CHARGE_RESETABLE = (0x0038, "kWh", "Kilowatt Hours", 1)
|
|
64
|
+
|
|
65
|
+
LED_STATE = (0x0031, "Numbers", "LED State", 1)
|
|
66
|
+
CHARGE_STATE = (0x0032, "Numbers", "Charge State", 1)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ManagementBase(object):
|
|
70
|
+
"""Class to get raw data from TS-MPPT-60. MODBUS ID is fixed to 1 as written on data sheet TSMPPT.APP_.Modbus.EN_.10.2.pdf.
|
|
71
|
+
|
|
72
|
+
Keyword arguments:
|
|
73
|
+
host -- host name like dummy.co.jp
|
|
74
|
+
cgi -- cgi script file name
|
|
75
|
+
debug -- debug message output is enabled if True
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
_ID_MODBUS = 0x01
|
|
79
|
+
|
|
80
|
+
def __init__(self, host, cgi="MBCSV.cgi", debug=False):
|
|
81
|
+
"""Initialize class object.
|
|
82
|
+
|
|
83
|
+
Keyword arguments:
|
|
84
|
+
host -- Host address like "192.168.1.20" of TS-MPPT-60 live view
|
|
85
|
+
cgi -- CGI file name to get the information
|
|
86
|
+
debug -- If True, logging is enabled.
|
|
87
|
+
"""
|
|
88
|
+
self._logger = logging.getLogger(type(self).__name__)
|
|
89
|
+
self._logger.addHandler(logging.StreamHandler())
|
|
90
|
+
|
|
91
|
+
if debug:
|
|
92
|
+
self._logger.setLevel(logging.DEBUG)
|
|
93
|
+
|
|
94
|
+
self._url = "http://" + host + "/" + cgi
|
|
95
|
+
self._vscale = self._compute_scaler(ModbusRegisterTable.VOLTAGE_SCALING)
|
|
96
|
+
self._iscale = self._compute_scaler(ModbusRegisterTable.CURRENT_SCALING)
|
|
97
|
+
|
|
98
|
+
def _get(self, addr, reg, mbid=_ID_MODBUS, field=4):
|
|
99
|
+
"""Get and return raw data string like "1,4,1,1,1" against MBID, Address, Register, and Field.
|
|
100
|
+
|
|
101
|
+
Keyword arguments:
|
|
102
|
+
addr -- Address to get information
|
|
103
|
+
reg -- Register to get information
|
|
104
|
+
mbid -- MBID
|
|
105
|
+
field -- Field to get information
|
|
106
|
+
|
|
107
|
+
>>> mb._get(addr=0x0000, reg=1)
|
|
108
|
+
'1,4,2,0,0'
|
|
109
|
+
>>> mb._get(addr=0x0001, reg=1)
|
|
110
|
+
'1,4,2,0,0'
|
|
111
|
+
"""
|
|
112
|
+
params = []
|
|
113
|
+
params.append("ID=" + str(mbid))
|
|
114
|
+
params.append("F=" + str(field))
|
|
115
|
+
params.append("AHI=" + str(addr >> 8))
|
|
116
|
+
params.append("ALO=" + str(addr & 255))
|
|
117
|
+
params.append("RHI=" + str(reg >> 8))
|
|
118
|
+
params.append("RLO=" + str(reg & 255))
|
|
119
|
+
|
|
120
|
+
res = requests.get("{0}?{1}".format(self._url, "&".join(params)), timeout=(5, 15))
|
|
121
|
+
|
|
122
|
+
return res.text
|
|
123
|
+
|
|
124
|
+
def _read_modbus(self, address, register, mbid=_ID_MODBUS):
|
|
125
|
+
"""Read and return the value string with short integer (ex. 16bit value) against MBID, Address, and Register.
|
|
126
|
+
|
|
127
|
+
Keyword arguments:
|
|
128
|
+
address -- Address to get information
|
|
129
|
+
register -- Register to get information
|
|
130
|
+
mbid -- MBID
|
|
131
|
+
|
|
132
|
+
>>> mb._read_modbus(0x0000, 1)
|
|
133
|
+
[0]
|
|
134
|
+
>>> mb._read_modbus(0x0001, 1)
|
|
135
|
+
[0]
|
|
136
|
+
"""
|
|
137
|
+
raw_value_str = self._get(address, register, mbid)
|
|
138
|
+
raw_values = [int(v) for v in raw_value_str.split(",")]
|
|
139
|
+
idx_max = raw_values[2]
|
|
140
|
+
raw_values = raw_values[3:]
|
|
141
|
+
idx = 0
|
|
142
|
+
ret = []
|
|
143
|
+
|
|
144
|
+
while idx < idx_max:
|
|
145
|
+
ret_short = raw_values[idx] << 8
|
|
146
|
+
idx += 1
|
|
147
|
+
ret_short += raw_values[idx]
|
|
148
|
+
idx += 1
|
|
149
|
+
ret.append(ret_short)
|
|
150
|
+
|
|
151
|
+
return ret
|
|
152
|
+
|
|
153
|
+
def _compute_scaler(self, param):
|
|
154
|
+
"""Compute and return the voltage/current scaler as written on data sheet page 8 or 25.
|
|
155
|
+
This will be called only once when initializing this object.
|
|
156
|
+
|
|
157
|
+
Vscaling = whole.fraction = [V_PU hi].[V_PU lo]
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
Address:Value(hex):Variable Name
|
|
161
|
+
V_PU HI byte:0x004E = 78
|
|
162
|
+
V_PU LO byte:0x03A6 = 934
|
|
163
|
+
|
|
164
|
+
V_PU lo must be shifted by 16 (divided by 2^16)
|
|
165
|
+
and then added to V_PU hi Vscaling = 78 + 934/65536 = 78.01425
|
|
166
|
+
|
|
167
|
+
Keyword arguments:
|
|
168
|
+
param -- register to get a value
|
|
169
|
+
>>> mb._compute_scaler(ModbusRegisterTable.VOLTAGE_SCALING)
|
|
170
|
+
0.0
|
|
171
|
+
>>> mb._compute_scaler(ModbusRegisterTable.CURRENT_SCALING)
|
|
172
|
+
0.0
|
|
173
|
+
"""
|
|
174
|
+
values = self._read_modbus(param[0], param[3])
|
|
175
|
+
return float(values[0]) + (float(values[1]) / pow(2, 16))
|
|
176
|
+
|
|
177
|
+
def get_raw_value(self, address, register):
|
|
178
|
+
"""Return a raw value against address got from TS-MPPT-60.
|
|
179
|
+
|
|
180
|
+
Keyword arguments:
|
|
181
|
+
address -- address to get a value
|
|
182
|
+
register -- register to get a value
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Raw value as integer type.
|
|
186
|
+
|
|
187
|
+
>>> mb.get_raw_value(0x0026, 1)
|
|
188
|
+
0
|
|
189
|
+
>>> mb.get_raw_value(0x0027, 1)
|
|
190
|
+
0
|
|
191
|
+
"""
|
|
192
|
+
values = self._read_modbus(address, register)
|
|
193
|
+
|
|
194
|
+
if register > 1:
|
|
195
|
+
raw_value = (values[0] << 16) | (values[1] & 0xFFFF)
|
|
196
|
+
else:
|
|
197
|
+
raw_value = values[0]
|
|
198
|
+
raw_value &= 0xFFFF
|
|
199
|
+
if raw_value & 0x8000:
|
|
200
|
+
raw_value ^= 0xFFFF
|
|
201
|
+
raw_value = -1 * (raw_value + 1)
|
|
202
|
+
|
|
203
|
+
return raw_value
|
|
204
|
+
|
|
205
|
+
def get_scaled_value(self, address, scale_factor, register):
|
|
206
|
+
"""Calculate and return a scaled status value against address got from TS-MPPT-60.
|
|
207
|
+
|
|
208
|
+
Keyword arguments:
|
|
209
|
+
address -- address to get a value
|
|
210
|
+
scale_factor -- unit string
|
|
211
|
+
register -- register to get a value
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Scaled value like 12.4 against your expecting.
|
|
215
|
+
|
|
216
|
+
>>> mb.get_scaled_value(0x0026, "V", 1)
|
|
217
|
+
0.0
|
|
218
|
+
>>> mb.get_scaled_value(0x0027, "A", 1)
|
|
219
|
+
0.0
|
|
220
|
+
"""
|
|
221
|
+
raw_value = self.get_raw_value(address, register)
|
|
222
|
+
|
|
223
|
+
if scale_factor == "V":
|
|
224
|
+
scaled_value = raw_value * self._vscale / pow(2, 15)
|
|
225
|
+
elif scale_factor == "A":
|
|
226
|
+
scaled_value = raw_value * self._iscale / pow(2, 15)
|
|
227
|
+
elif scale_factor == "W":
|
|
228
|
+
wscale = self._iscale * self._vscale
|
|
229
|
+
scaled_value = raw_value * wscale / pow(2, 17)
|
|
230
|
+
elif scale_factor == "Ah":
|
|
231
|
+
scaled_value = raw_value / 10.0
|
|
232
|
+
else:
|
|
233
|
+
scaled_value = raw_value
|
|
234
|
+
|
|
235
|
+
return round(scaled_value, 2)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
import doctest
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
from unittest.mock import patch
|
|
243
|
+
except ImportError:
|
|
244
|
+
from mock import patch
|
|
245
|
+
|
|
246
|
+
class DummyRequest(object):
|
|
247
|
+
"""Dummy response class against requests.Response."""
|
|
248
|
+
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
class DummyResponse(object):
|
|
252
|
+
"""Dummy response class against requests.Response."""
|
|
253
|
+
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
dummy_host = "dummy.co.jp"
|
|
257
|
+
|
|
258
|
+
req = DummyRequest()
|
|
259
|
+
req.url = "http://" + dummy_host + "/dummy.cgi"
|
|
260
|
+
res = DummyResponse()
|
|
261
|
+
res.request = req
|
|
262
|
+
res.text = "1,4,2,0,0"
|
|
263
|
+
|
|
264
|
+
with patch("requests.get", returns=res) as _m:
|
|
265
|
+
_m.return_value = res
|
|
266
|
+
doctest.testmod(verbose=True, extraglobs={"mb": ManagementBase(host=dummy_host)})
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from tsmppt60_driver.base import ModbusRegisterTable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
"""TS-MPPT-60 driver's internal modules inherites base modules."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ChargeControllerStatus(object):
|
|
10
|
+
"""Abstract class to get data about charge controller status."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, mb, group, debug=False):
|
|
13
|
+
"""Initialize class object.
|
|
14
|
+
|
|
15
|
+
Keyword arguments:
|
|
16
|
+
mb -- instance of ManagementBase class.
|
|
17
|
+
group -- string to indicate this instance name.
|
|
18
|
+
debug -- If True, logging is enabled.
|
|
19
|
+
"""
|
|
20
|
+
self._mb = mb
|
|
21
|
+
self._group = group
|
|
22
|
+
|
|
23
|
+
handler = logging.StreamHandler()
|
|
24
|
+
handler.setFormatter(
|
|
25
|
+
logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s", "%Y/%m/%d %p %l:%M:%S")
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
self._logger = logging.getLogger(type(self).__name__)
|
|
29
|
+
self._logger.addHandler(handler)
|
|
30
|
+
|
|
31
|
+
if debug:
|
|
32
|
+
self._logger.setLevel(logging.DEBUG)
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return self._group
|
|
36
|
+
|
|
37
|
+
def __str__(self):
|
|
38
|
+
return self._group
|
|
39
|
+
|
|
40
|
+
def get_status(self, address, scale_factor, label, register):
|
|
41
|
+
"""
|
|
42
|
+
Get and return a data against the specified address, register, etc. like below.
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
"group": "battery",
|
|
46
|
+
"label": "Battery Voltage",
|
|
47
|
+
"value": 12.1,
|
|
48
|
+
"unit": "V"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Keyword arguments:
|
|
52
|
+
address -- address to get a value
|
|
53
|
+
scale_factor -- unit string
|
|
54
|
+
label -- label string of got value
|
|
55
|
+
register -- register to get a value
|
|
56
|
+
"""
|
|
57
|
+
ret_values = {}
|
|
58
|
+
ret_values["group"] = self._group
|
|
59
|
+
ret_values["label"] = label
|
|
60
|
+
ret_values["value"] = self._mb.get_scaled_value(address, scale_factor, register)
|
|
61
|
+
ret_values["unit"] = scale_factor
|
|
62
|
+
|
|
63
|
+
return ret_values
|
|
64
|
+
|
|
65
|
+
def get_status_all(self, is_limit=True):
|
|
66
|
+
"""
|
|
67
|
+
Get and return all data against the inherited class's paramter list.
|
|
68
|
+
|
|
69
|
+
{ "group": "Battery",
|
|
70
|
+
"label": "Battery Voltage",
|
|
71
|
+
"value": 12.1,
|
|
72
|
+
"unit": "V"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"group": "Battery",
|
|
76
|
+
"label": "Charge Current",
|
|
77
|
+
"value": 8.4,
|
|
78
|
+
"unit": "A"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Keyword arguments:
|
|
82
|
+
is_limit -- limit the number of getting status
|
|
83
|
+
"""
|
|
84
|
+
return [self.get_status(*param) for param in self.get_params(is_limit)]
|
|
85
|
+
|
|
86
|
+
def get_params(self, is_limit=True):
|
|
87
|
+
"""Get and return a list of all params of the inherited class's group.
|
|
88
|
+
|
|
89
|
+
Keyword arguments:
|
|
90
|
+
is_limit -- limit the number of getting status
|
|
91
|
+
((61, "V", "Sweep Vmp", 1),
|
|
92
|
+
(62, "V", "Sweep Voc", 1),
|
|
93
|
+
(60, "W", "Sweep Pmax", 1))
|
|
94
|
+
"""
|
|
95
|
+
raise NotImplementedError
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class BatteryStatus(ChargeControllerStatus):
|
|
99
|
+
"""
|
|
100
|
+
This class gives the following battery status.
|
|
101
|
+
|
|
102
|
+
* battery voltage
|
|
103
|
+
* target voltage
|
|
104
|
+
* charge current
|
|
105
|
+
* output power
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, mb):
|
|
109
|
+
"""Initialize BatteryStatus class object.
|
|
110
|
+
|
|
111
|
+
Keyword arguments:
|
|
112
|
+
mb -- instance of ManagementBase class
|
|
113
|
+
"""
|
|
114
|
+
ChargeControllerStatus.__init__(self, mb, "Battery")
|
|
115
|
+
|
|
116
|
+
def get_params(self, is_limit=True):
|
|
117
|
+
"""Get and return a list of all params to get the battery status. The param is consisted by (address, scale_factor, label, register).
|
|
118
|
+
|
|
119
|
+
Keyword arguments:
|
|
120
|
+
is_limit -- limit the number of getting status
|
|
121
|
+
|
|
122
|
+
>>> bat.get_params() == [
|
|
123
|
+
... (38, "V", "Battery Voltage", 1),
|
|
124
|
+
... (51, "V", "Target Voltage", 1),
|
|
125
|
+
... (39, "A", "Charge Current", 1),
|
|
126
|
+
... ]
|
|
127
|
+
True
|
|
128
|
+
>>> bat.get_params(True) == [
|
|
129
|
+
... (38, "V", "Battery Voltage", 1),
|
|
130
|
+
... (51, "V", "Target Voltage", 1),
|
|
131
|
+
... (39, "A", "Charge Current", 1),
|
|
132
|
+
... ]
|
|
133
|
+
True
|
|
134
|
+
>>> bat.get_params(False) == [
|
|
135
|
+
... (38, "V", "Battery Voltage", 1),
|
|
136
|
+
... (51, "V", "Target Voltage", 1),
|
|
137
|
+
... (39, "A", "Charge Current", 1),
|
|
138
|
+
... (58, "W", "Output Power", 1),
|
|
139
|
+
... ]
|
|
140
|
+
True
|
|
141
|
+
"""
|
|
142
|
+
params = [
|
|
143
|
+
ModbusRegisterTable.BATTERY_VOLTAGE,
|
|
144
|
+
ModbusRegisterTable.TARGET_REGULATION_VOLTAGE,
|
|
145
|
+
ModbusRegisterTable.CHARGING_CURRENT,
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
if not is_limit:
|
|
149
|
+
params.append(ModbusRegisterTable.OUTPUT_POWER)
|
|
150
|
+
|
|
151
|
+
return params
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class SolarArrayStatus(ChargeControllerStatus):
|
|
155
|
+
"""Class to get data about solar array.
|
|
156
|
+
|
|
157
|
+
* array voltage
|
|
158
|
+
* array current
|
|
159
|
+
* sweep vmp
|
|
160
|
+
* sweep voc
|
|
161
|
+
* sweep pmax
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(self, mb):
|
|
165
|
+
"""Initialize SolarArrayStatus class object.
|
|
166
|
+
|
|
167
|
+
Keyword arguments:
|
|
168
|
+
mb -- instance of ManagementBase class
|
|
169
|
+
"""
|
|
170
|
+
ChargeControllerStatus.__init__(self, mb, "Array")
|
|
171
|
+
|
|
172
|
+
def get_params(self, is_limit=True):
|
|
173
|
+
"""Get and return a list of all params to get the solar array status. The param is consisted by (address, scale_factor, label, register).
|
|
174
|
+
|
|
175
|
+
Keyword arguments:
|
|
176
|
+
is_limit -- limit the number of getting status
|
|
177
|
+
|
|
178
|
+
>>> array.get_params() == [(27, "V", "Array Voltage", 1), (29, "A", "Array Current", 1)]
|
|
179
|
+
True
|
|
180
|
+
>>> array.get_params(True) == [(27, "V", "Array Voltage", 1), (29, "A", "Array Current", 1)]
|
|
181
|
+
True
|
|
182
|
+
>>> array.get_params(False) == [
|
|
183
|
+
... (27, "V", "Array Voltage", 1),
|
|
184
|
+
... (29, "A", "Array Current", 1),
|
|
185
|
+
... (61, "V", "Sweep Vmp", 1),
|
|
186
|
+
... (62, "V", "Sweep Voc", 1),
|
|
187
|
+
... (60, "W", "Sweep Pmax", 1),
|
|
188
|
+
... ]
|
|
189
|
+
True
|
|
190
|
+
"""
|
|
191
|
+
params = [ModbusRegisterTable.ARRAY_VOLTAGE, ModbusRegisterTable.ARRAY_CURRENT]
|
|
192
|
+
|
|
193
|
+
if not is_limit:
|
|
194
|
+
params.extend(
|
|
195
|
+
[
|
|
196
|
+
ModbusRegisterTable.VMP_LAST_SWEEP,
|
|
197
|
+
ModbusRegisterTable.VOC_LAST_SWEEP,
|
|
198
|
+
ModbusRegisterTable.POWER_LAST_SWEEP,
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return params
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TemperaturesStatus(ChargeControllerStatus):
|
|
206
|
+
"""Class to get data about temperatures sensors.
|
|
207
|
+
|
|
208
|
+
* heat sink temperature
|
|
209
|
+
* battery temperature
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def __init__(self, mb):
|
|
213
|
+
"""Initialize SolarArrayStatus class object.
|
|
214
|
+
|
|
215
|
+
Keyword arguments:
|
|
216
|
+
mb -- instance of ManagementBase class
|
|
217
|
+
"""
|
|
218
|
+
ChargeControllerStatus.__init__(self, mb, "Temperature")
|
|
219
|
+
|
|
220
|
+
def get_params(self, is_limit=True):
|
|
221
|
+
"""Get and return a list of all params to get the temperatures. The param is consisted by (address, scale_factor, label, register).
|
|
222
|
+
|
|
223
|
+
Keyword arguments:
|
|
224
|
+
is_limit -- limit the number of getting status
|
|
225
|
+
|
|
226
|
+
>>> temp.get_params() == [(35, "C", "Heat Sink Temperature", 1)]
|
|
227
|
+
True
|
|
228
|
+
>>> temp.get_params(True) == [(35, "C", "Heat Sink Temperature", 1)]
|
|
229
|
+
True
|
|
230
|
+
>>> temp.get_params(False) == [(35, "C", "Heat Sink Temperature", 1), (37, "C", "Battery Temperature", 1)]
|
|
231
|
+
True
|
|
232
|
+
"""
|
|
233
|
+
params = [ModbusRegisterTable.HEATSINK_TEMP]
|
|
234
|
+
|
|
235
|
+
if not is_limit:
|
|
236
|
+
params.append(ModbusRegisterTable.BATTERY_TEMP)
|
|
237
|
+
|
|
238
|
+
return params
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class CountersStatus(ChargeControllerStatus):
|
|
242
|
+
"""
|
|
243
|
+
Class to get data about resettable counters.
|
|
244
|
+
|
|
245
|
+
* amp hours
|
|
246
|
+
* kilowatt hours
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def __init__(self, mb):
|
|
250
|
+
"""
|
|
251
|
+
Initialize CountersStaus class object.
|
|
252
|
+
|
|
253
|
+
Keyword arguments:
|
|
254
|
+
mb -- instance of ManagementBase class
|
|
255
|
+
"""
|
|
256
|
+
ChargeControllerStatus.__init__(self, mb, "Counter")
|
|
257
|
+
|
|
258
|
+
def get_params(self, is_limit=True):
|
|
259
|
+
"""Get and return a list of all params to get the counters. The param is consisted by (address, scale_factor, label, register).
|
|
260
|
+
|
|
261
|
+
Keyword arguments:
|
|
262
|
+
is_limit -- limit the number of getting status
|
|
263
|
+
|
|
264
|
+
>>> count.get_params() == ((52, "Ah", "Amp Hours", 2), (56, "kWh", "Kilowatt Hours", 1))
|
|
265
|
+
True
|
|
266
|
+
>>> count.get_params(True) == ((52, "Ah", "Amp Hours", 2), (56, "kWh", "Kilowatt Hours", 1))
|
|
267
|
+
True
|
|
268
|
+
>>> count.get_params(False) == ((52, "Ah", "Amp Hours", 2), (56, "kWh", "Kilowatt Hours", 1))
|
|
269
|
+
True
|
|
270
|
+
"""
|
|
271
|
+
return (ModbusRegisterTable.AH_CHARGE_RESETABLE, ModbusRegisterTable.KWH_CHARGE_RESETABLE)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class OperatingConditions(ChargeControllerStatus):
|
|
275
|
+
"""
|
|
276
|
+
Class to get data about controller's operating conditions.
|
|
277
|
+
|
|
278
|
+
* LED state
|
|
279
|
+
* Charge state
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def __init__(self, mb):
|
|
283
|
+
"""
|
|
284
|
+
Initialize OperatingConditions class object.
|
|
285
|
+
|
|
286
|
+
Keyword arguments:
|
|
287
|
+
mb -- instance of ManagementBase class
|
|
288
|
+
"""
|
|
289
|
+
ChargeControllerStatus.__init__(self, mb, "Condition")
|
|
290
|
+
|
|
291
|
+
def get_params(self, is_limit=True):
|
|
292
|
+
"""Get and return a list of all params to get the conditions. The param is consisted by (address, scale_factor, label, register).
|
|
293
|
+
|
|
294
|
+
Keyword arguments:
|
|
295
|
+
is_limit -- limit the number of getting status
|
|
296
|
+
|
|
297
|
+
>>> condition.get_params() == ((49, "Numbers", "LED State", 1), (50, "Numbers", "Charge State", 1))
|
|
298
|
+
True
|
|
299
|
+
>>> condition.get_params(True) == ((49, "Numbers", "LED State", 1), (50, "Numbers", "Charge State", 1))
|
|
300
|
+
True
|
|
301
|
+
>>> condition.get_params(False) == ((49, "Numbers", "LED State", 1), (50, "Numbers", "Charge State", 1))
|
|
302
|
+
True
|
|
303
|
+
"""
|
|
304
|
+
return (ModbusRegisterTable.LED_STATE, ModbusRegisterTable.CHARGE_STATE)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
import doctest
|
|
309
|
+
|
|
310
|
+
doctest.testmod(
|
|
311
|
+
verbose=True,
|
|
312
|
+
extraglobs={
|
|
313
|
+
"bat": BatteryStatus(None),
|
|
314
|
+
"array": SolarArrayStatus(None),
|
|
315
|
+
"temp": TemperaturesStatus(None),
|
|
316
|
+
"count": CountersStatus(None),
|
|
317
|
+
"condition": OperatingConditions(None),
|
|
318
|
+
},
|
|
319
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tsmppt60-driver
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Python module to get status of your solar charge controller TS-MPPT-60.
|
|
5
|
+
License: GPLv2
|
|
6
|
+
Author: Takashi Ando
|
|
7
|
+
Author-email: odo5522@gmail.com
|
|
8
|
+
Requires-Python: >=3.11,<3.15
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
18
|
+
Requires-Dist: requests (>=2.32,<3.0)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# TS-MPPT-60 driver module
|
|
22
|
+
|
|
23
|
+
This is python driver module to get the following status of TS-MPPT-60.
|
|
24
|
+
|
|
25
|
+
* Amp Hours
|
|
26
|
+
* Array Current
|
|
27
|
+
* Array Voltage
|
|
28
|
+
* Battery Temperature
|
|
29
|
+
* Battery Voltage
|
|
30
|
+
* Charge Current
|
|
31
|
+
* Heat Sink Temperature
|
|
32
|
+
* Kilowatt Hours
|
|
33
|
+
* Output Power
|
|
34
|
+
* Sweep Pmax
|
|
35
|
+
* Sweep Vmp
|
|
36
|
+
* Sweep Voc
|
|
37
|
+
* Target Voltage
|
|
38
|
+
|
|
39
|
+
# Requirement
|
|
40
|
+
|
|
41
|
+
* requests
|
|
42
|
+
|
|
43
|
+
# How to install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install tsmppt60-driver
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
# How to use
|
|
50
|
+
|
|
51
|
+
SystemStatus class object is iterator containing all live status data of TS-MPPT-60. Try the following line.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
print(SystemStatus("192.168.1.20").get())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The result is like following.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
{'Amp Hours': {'group': 'Counter', 'unit': 'Ah', 'value': 18097.9},
|
|
61
|
+
'Array Current': {'group': 'Array', 'unit': 'A', 'value': 1.4},
|
|
62
|
+
'Array Voltage': {'group': 'Array', 'unit': 'V', 'value': 53.41},
|
|
63
|
+
'Battery Voltage': {'group': 'Battery', 'unit': 'V', 'value': 23.93},
|
|
64
|
+
'Charge Current': {'group': 'Battery', 'unit': 'A', 'value': 3.2},
|
|
65
|
+
'Heat Sink Temperature': {'group': 'Temperature', 'unit': 'C', ...},
|
|
66
|
+
'Kilowatt Hours': {'group': 'Counter', 'unit': 'kWh', 'value': 237.0},
|
|
67
|
+
'Target Voltage': {'group': 'Battery', 'unit': 'V', 'value': 28.6}}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The above data is limited information. You can disable the limitter by setting False to the second argument as SystemStatus() class.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
print(SystemStatus("192.168.1.20").get(False))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The result is like following.
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
{'Amp Hours': {'group': 'Counter', 'unit': 'Ah', 'value': 18097.8},
|
|
80
|
+
'Array Current': {'group': 'Array', 'unit': 'A', 'value': 1.3},
|
|
81
|
+
'Array Voltage': {'group': 'Array', 'unit': 'V', 'value': 53.41},
|
|
82
|
+
'Battery Temperature': {'group': 'Temperature', 'unit': 'C', ...},
|
|
83
|
+
'Battery Voltage': {'group': 'Battery', 'unit': 'V', 'value': 24.01},
|
|
84
|
+
'Charge Current': {'group': 'Battery', 'unit': 'A', 'value': 3.2},
|
|
85
|
+
'Heat Sink Temperature': {'group': 'Temperature', 'unit': 'C', ...},
|
|
86
|
+
'Kilowatt Hours': {'group': 'Counter', 'unit': 'kWh', 'value': 237.0},
|
|
87
|
+
'Output Power': {'group': 'Battery', 'unit': 'W', 'value': 76.0},
|
|
88
|
+
'Sweep Pmax': {'group': 'Array', 'unit': 'W', 'value': 73.0},
|
|
89
|
+
'Sweep Vmp': {'group': 'Array', 'unit': 'V', 'value': 53.41},
|
|
90
|
+
'Sweep Voc': {'group': 'Array', 'unit': 'V', 'value': 60.05},
|
|
91
|
+
'Target Voltage': {'group': 'Battery', 'unit': 'V', 'value': 28.6}}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
JSON string can be got with following code.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
$ python -c "from tsmppt60_driver import *; import json; print(json.dumps(SystemStatus('192.168.1.20').get()))" | jq '."Battery Voltage"'
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
"group": "Battery",
|
|
101
|
+
"value": 28.6,
|
|
102
|
+
"unit": "V"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
tsmppt60_driver/__init__.py,sha256=rQVEYSNs57k8gBg6_X9WLkQ9aNc3-Kx_37G0xy4eiYM,3828
|
|
2
|
+
tsmppt60_driver/base.py,sha256=IHQ6syynGfhlVV_bHfTYKUW0CKv1r4bYfB9T5IRu2pE,8726
|
|
3
|
+
tsmppt60_driver/status.py,sha256=nrz7gb0KaD0bLZ5g6mjOxOgkMYPq6vCr2jeoZjZdVTc,9854
|
|
4
|
+
tsmppt60_driver-0.2.2.dist-info/METADATA,sha256=arGVJKWP5Jvgfa_2VyK1bvsO3NF5qHKMOo3S3GOm2VU,3310
|
|
5
|
+
tsmppt60_driver-0.2.2.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
|
|
6
|
+
tsmppt60_driver-0.2.2.dist-info/RECORD,,
|