Skip to content

Commit d07ab3f

Browse files
committed
more flexible universal BMS interface
1 parent 72ebeba commit d07ab3f

File tree

9 files changed

+370
-77
lines changed

9 files changed

+370
-77
lines changed

app.py

Lines changed: 34 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
from multiplus2 import MultiPlus2
1212
from timer import Timer
1313
from trace import Trace
14-
from us2000 import US2000
14+
from bms_us2000 import US2000
1515
from utils import *
1616
from web import AppWeb
1717

18+
from bms_dummy import BMS_DUMMY
19+
1820
"""
1921
ESS Application
2022
"""
@@ -29,11 +31,15 @@ def __init__(self):
2931
self.trace = Trace()
3032
self.config = config
3133
self.meterhub = ApiRequest(config['meterhub_address'], timeout=0.5, lifetime=10, log_name='meterhub')
32-
self.bms = US2000(port=config['pylontech_bms_port'],
33-
pack_number=config['us2000_pack_number'],
34-
baudrate=config['us2000_baudrate'],
35-
lifetime=30,
36-
log_name='bms')
34+
35+
if 'bms_us2000' in config:
36+
self.bms = US2000(**self.config['bms_us2000']) # pass config to BMS class
37+
# elif 'bms_seplos' in config:
38+
# self.bms = SEPLOS(**self.config['bms_seplos']) # pass config to BMS class
39+
else:
40+
self.log.exception("undefined BMS")
41+
self.bms = None
42+
3743
self.multiplus = MultiPlus2(config['victron_mk3_port'])
3844
self.blackbox = Blackbox(size=config['blackbox_size'],
3945
path=config['log_path'],
@@ -52,16 +58,13 @@ def __init__(self):
5258
self.home_all_p = 0
5359
self.car_p = 0
5460
self.pv_p = 0
55-
self.soc = None
56-
self.soc_low = None # lowest SOC
57-
self.soc_high = None # highest SOC
58-
self.ubat = None
5961

6062
self.charge_start_timer = Timer()
6163
self.feed_start_timer = Timer()
6264
self.feed_throttle_timer = Timer()
6365
self.state_timer = Timer()
6466

67+
6568
def get_setting(self, name):
6669
"""
6770
Give a setting depending on the option used or default
@@ -85,12 +88,13 @@ def start(self):
8588
# === meterhub =================================================== ~ 15ms
8689

8790
self.meterhub.read(
88-
post={'bat_info': self.get_info_text(), 'bat_soc': dictget(self.bms.data, 'soc')})
91+
post={'bat_info': self.get_info_text(), 'bat_soc': self.bms.soc})
8992
self.log.debug("meterhub {}".format(self.meterhub.data))
9093

9194
# === bms =================================================== ~ 0ms (Thread)
9295

93-
self.log.debug("bms {}".format(self.bms.data))
96+
self.bms.update()
97+
self.log.debug("bms {}".format(self.bms.get_state()))
9498

9599
# ================================================================
96100

@@ -137,38 +141,23 @@ def update_in(self):
137141
else:
138142
self.home_p = None
139143

140-
self.soc = dictget(self.bms.data, 'soc')
141-
142-
try:
143-
self.soc_low = min(self.bms.data['soc_pack'])
144-
self.soc_high = max(self.bms.data['soc_pack'])
145-
except:
146-
self.soc_low = None
147-
self.soc_high = None
148144

149-
self.ubat = dictget(self.bms.data, 'u')
150145

151146
def fsm_switch(self):
152147
"""
153148
Auto state change by events
154149
"""
155-
if dictget(self.bms.data, 'u', 0) > self.config['udc_max']:
156-
self.log.error("error max voltage at bms {}".format(self.bms.data))
150+
if self.bms.voltage and self.bms.voltage > self.config['udc_max']:
151+
self.log.error("error max voltage at bms {}".format(self.bms.get_state()))
157152
self.set_fsm_state('error')
158-
# elif dictget(self.multiplus.data, 'bat_u', 0) > self.config['udc_max']:
159-
# self.log.error("error max voltage at multiplus {}".format(self.multiplus.data))
160-
# self.set_fsm_state('error')
161-
elif dictget(self.bms.data, 't', 0) > self.config['t_max']:
162-
self.log.error("error max bms temperature {}".format(self.bms.data))
153+
elif self.bms.temperature and self.bms.temperature > self.config['t_max']:
154+
self.log.error("error max bms temperature {}".format(self.bms.get_state()))
163155
self.set_fsm_state('error')
164156
elif not self.is_meterhub_ready():
165157
self.log.error("meterhub error {}".format(self.meterhub.data))
166158
self.set_fsm_state('error')
167-
elif not self.is_bms_ready():
168-
self.log.error("bms not ready {}".format(self.bms.data))
169-
self.set_fsm_state('error')
170-
elif self.is_bms_error():
171-
self.log.error("bms error {}".format(self.bms.data))
159+
elif self.bms.error:
160+
self.log.error("bms error {}".format(self.bms.get_state()))
172161
self.set_fsm_state('error')
173162
elif not self.is_multiplus_ready():
174163
self.log.error("multiplus error {}".format(self.multiplus.data))
@@ -192,7 +181,7 @@ def is_charge_start(self):
192181
except:
193182
p = 0
194183

195-
if p < self.get_setting('charge_min_power') or self.soc_high > (
184+
if p < self.get_setting('charge_min_power') or self.bms.soc_high is None or self.bms.soc_high > (
196185
self.get_setting('charge_end_soc') - self.get_setting('charge_hysteresis_soc')):
197186
self.charge_start_timer.stop()
198187
else:
@@ -215,7 +204,7 @@ def is_feed_start(self):
215204
except:
216205
p = 0
217206

218-
if p < self.get_setting('feed_min_power') or self.soc_low < (
207+
if p < self.get_setting('feed_min_power') or self.bms.soc_low is None or self.bms.soc_low < (
219208
self.get_setting('feed_end_soc') + self.get_setting('feed_hysteresis_soc')):
220209
self.feed_start_timer.stop()
221210
else:
@@ -226,12 +215,6 @@ def is_feed_start(self):
226215
return True
227216
return False
228217

229-
def is_bms_error(self):
230-
return True if self.bms.data['error'] else False
231-
232-
def is_bms_ready(self):
233-
return self.bms.data['ready']
234-
235218
def is_meterhub_ready(self):
236219
return True if self.meterhub.data and 'error' not in self.meterhub.data else False
237220

@@ -247,16 +230,14 @@ def fsm_init(self, entry):
247230
self.set_p = 0
248231
self.state_timer.start(10)
249232

250-
if self.is_meterhub_ready() and self.is_bms_ready() and self.is_multiplus_ready():
233+
if self.is_meterhub_ready() and not self.bms.error and self.is_multiplus_ready():
251234
self.fsm_switch()
252235

253236
if self.state_timer.is_expired():
254237
if not self.is_meterhub_ready():
255238
self.log.error("meterhub error {}".format(self.meterhub.data))
256-
if not self.is_bms_ready():
257-
self.log.error("bms not ready {}".format(self.bms.data))
258-
if self.is_bms_error():
259-
self.log.error("bms error {}".format(self.bms.data))
239+
if self.bms.error():
240+
self.log.error("bms error {}".format(self.bms.get_state()))
260241
if not self.is_multiplus_ready():
261242
self.log.error("multiplus error={}".format(self.multiplus.data))
262243
self.set_fsm_state('error')
@@ -323,10 +304,10 @@ def fsm_auto_charge(self, entry):
323304
# ToDo Filter schnell runter, langsam hoch
324305

325306
charge_set_p = limit(p, 0, self.get_setting('charge_max_power')) # limit to 0..max
326-
if self.soc_high >= self.get_setting('charge_end_soc'): # end by SOC
307+
if self.bms.soc_high and self.bms.soc_high >= self.get_setting('charge_end_soc'): # end by SOC
327308
self.log.info("charge end by soc (config.charge_end_soc)")
328309
self.set_fsm_state('auto_idle')
329-
elif self.ubat >= self.get_setting('charge_end_voltage'): # end by UDC
310+
elif self.bms.voltage and self.bms.voltage >= self.get_setting('charge_end_voltage'): # end by UDC
330311
self.log.info("charge end by voltage (config.charge_end_voltage)")
331312
self.set_fsm_state('auto_idle')
332313
else:
@@ -356,12 +337,11 @@ def fsm_auto_feed(self, entry):
356337
try:
357338
p = self.home_p - self.pv_p - self.get_setting('feed_reserve_power')
358339

359-
if self.soc_low <= 25:
340+
if self.bms.soc_low and self.bms.soc_low <= 25:
360341
max_p = self.get_setting('feed_soc25_max_power')
361342
else:
362343
max_p = self.get_setting('feed_max_power')
363344

364-
365345
feed_set_p = limit(p, 0, max_p) # limit to 0..max
366346

367347
# ------ throttle --------------
@@ -387,10 +367,10 @@ def fsm_auto_feed(self, entry):
387367
feed_set_p = limit(p, 0, self.get_setting('feed_throttle_power'))
388368
# --------------------------------------------------------------------------------
389369

390-
if self.soc_low <= self.get_setting('feed_end_soc'): # end by SOC
370+
if self.bms.soc_low and self.bms.soc_low <= self.get_setting('feed_end_soc'): # end by SOC
391371
self.log.info("feed end by soc (config.feed_end_soc)")
392372
self.set_fsm_state('auto_idle')
393-
elif self.ubat <= self.get_setting('feed_end_voltage'): # end by UDC
373+
elif self.bms.voltage and self.bms.voltage <= self.get_setting('feed_end_voltage'): # end by UDC
394374
self.log.info("feed end by voltage (config.feed_end_voltage)")
395375
self.set_fsm_state('auto_idle')
396376
else:
@@ -457,11 +437,11 @@ def get_state(self, bms_detail=False):
457437
'info': self.get_info_text()
458438
},
459439
'meterhub': self.meterhub.data,
460-
'bms': self.bms.data,
440+
'bms': self.bms.get_state(),
461441
'multiplus': self.multiplus.data,
462442
}
463443
if bms_detail:
464-
d['bms_detail'] = self.bms.data_detail
444+
d['bms_detail'] = self.bms.get_detail()
465445
return d
466446

467447
def get_info_text(self):

bms.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from abc import ABC, abstractmethod
2+
3+
"""
4+
Interface class description of the BMS interface
5+
"""
6+
7+
8+
class BMS(ABC):
9+
10+
def __init__(self):
11+
self._error = None # None or string in case of an error
12+
self._voltage = None # voltage [V]
13+
self._current = None # current [A]
14+
self._temperature = None # temperature [°C]
15+
self._soc = None # State of charge [%]
16+
self._soc_low = None # lowest soc with multiple packs (set to soc for single pack)
17+
self._soc_high = None # highest soc with multiple packs (set to soc for single pack)
18+
19+
@property
20+
def voltage(self):
21+
return self._voltage
22+
23+
@property
24+
def current(self):
25+
return self._current
26+
27+
@property
28+
def temperature(self):
29+
return self._temperature
30+
31+
@property
32+
def soc(self):
33+
return self._soc
34+
35+
@property
36+
def soc_low(self):
37+
return self._soc_low
38+
39+
@property
40+
def soc_high(self):
41+
return self._soc_high
42+
43+
@property
44+
def error(self):
45+
return self._error
46+
47+
@abstractmethod
48+
def update(self):
49+
"""
50+
Run update / read cycle. If implemented as thread, update() must be a Dummy
51+
:return:
52+
"""
53+
pass
54+
55+
@abstractmethod
56+
def get_state(self):
57+
"""
58+
Get actual state
59+
60+
:return: Dictionary
61+
"""
62+
pass
63+
64+
@abstractmethod
65+
def get_detail(self):
66+
"""
67+
Get actual state with detailed Information
68+
69+
:return: Dictionary
70+
"""
71+
pass

bms_dummy.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from bms import BMS
2+
3+
4+
5+
class BMS_DUMMY(BMS):
6+
def __init__(self, port, timeout):
7+
super().__init__()
8+
# optional (showed in ui but not used in control loop)
9+
self._pack_u = []
10+
self._pack_i = []
11+
self._pack_t = []
12+
self._pack_soc = []
13+
self._pack_cycle = []
14+
15+
def update(self):
16+
# read data from BMS and set values
17+
# self._voltage = ...
18+
pass
19+
20+
21+
def get_state(self):
22+
return {
23+
'u': self._voltage,
24+
'i': self._current,
25+
't': self._temperature,
26+
'soc': self._soc,
27+
'soc_low': self._soc_low,
28+
'soc_high': self._soc_high,
29+
30+
'pack_u': self._pack_u,
31+
'pack_i': self._pack_i,
32+
'pack_t': self._pack_t,
33+
'pack_soc': self._pack_soc,
34+
'pack_cycle': self._pack_cycle,
35+
# ...
36+
}
37+
38+
def get_detail(self):
39+
return {
40+
}

0 commit comments

Comments
 (0)