├── LICENSE
├── NOTES_recvd_sma_can_msgs
├── NOTES_sendbms_sma_can_msgs
├── README.md
├── assets
└── overview-inverter.svg
├── bin
└── canable_fw_d2cb159.bin
├── dbus-sma
├── bms_state_machine.py
├── bms_test.py
├── dbus-sma.py
├── dbus-sma.yaml
└── service
│ ├── log
│ └── run
│ └── run
├── install
├── 99-candlelight.rules
└── install.sh
└── test
└── sma_keep_alive.py
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 madsci1016, jaedog
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/NOTES_recvd_sma_can_msgs:
--------------------------------------------------------------------------------
1 | 0x300
2 | First 2 bytes Master total external grid power 0.1s of kW
3 | Second 2 bytes, Slave total external grid power 0.1s of kW
4 |
5 | 0x301
6 | First 2 bytes, Master total inverter power 0.1s of kW
7 | Second 2 bytes, Slave total inverter power 0.1s of kW
8 |
9 | 0x302
10 | First two bytes, External tenths of kvar master
11 | Next two bytes, External tenths of kvar slave
12 |
13 | 0x303
14 | First two bytes, Inverter tenths of kvar master
15 | Next two bytes, Inverter tenths of kvar slave
16 |
17 | 0x304
18 | First two bytes output voltage master
19 | Next two bytes output voltage slave 1?
20 | Next two bytes output voltage slave 2?
21 | Last two bytes output frequency
22 |
23 | 0x305
24 | First two bytes, DC voltage
25 | Second 2 bytes, DC current combined system, signed 16bit 0.1A
26 | Last 4 bytes unknown, always seem to be E6,00,DE,03
27 |
28 | 0x306
29 |
30 | 0x307
31 | Byte 2, high two bits are useful. Bit 7 is 1 for AC2 relay closed. Bit 6 is 1 for valid voltage present on AC2.
32 |
33 | 0x308
34 | (Guess) first two bytes is total Load power.
35 |
36 | 0x309
37 | First two bytes input voltage
38 | Next two bytes input voltage slave 1?
39 | Next two bytes input voltage slave 2?
40 | last two bytes grid frequency LSB 0.01Hz
41 |
42 | 0x351
43 | Bytes 1,2 Charge voltage goal. 0.1V Not sure the Sunny Island does anything with this. I suspect it errors out if battery goes above this
44 | Bytes 3,4 Requested charge current, 0.1A
45 | Bytes 5,6 Requested discharge current. 0.1A. Don’t think SunnyIsland does anything with this.
46 | Bytes 7,8 Discharge voltage. 0.1V. Not sure the Sunny Island does anything with this. I suspect it errors out if battery goes below this.
47 |
48 | 0x355
49 | Byte 1,2 SoC 1%
50 | Byte 3,4 SoH 1%
51 | Bytes 5,6 SoC 0.01%
52 |
53 |
--------------------------------------------------------------------------------
/NOTES_sendbms_sma_can_msgs:
--------------------------------------------------------------------------------
1 | BMS CAN Messages sent to inverter
2 |
3 | Data extracted from: http://www.rec-bms.com/datasheet/UserManual9R_SMA.pdf
4 |
5 | CAN messages are sent each measuring cycle with 100 ms interval between.
6 |
7 | CAN message 0x351:
8 | Byte Description Type Property
9 | 1 Charge voltage low byte Unsigned integer LSB = 0.1V
10 | 2 Charge voltage high byte
11 | 3 Max charging current low byte Signed integer LSB = 0.1A
12 | 4 Max charging current high byte
13 | 5 Max charging current low byte Signed integer LSB = 0.1A
14 | 6 Max charging current high byte
15 | 7 Discharge voltage low byte Unsigned integer LSB = 0.1V
16 | 8 Discharge voltage high byte
17 |
18 | CAN message 0x355:
19 | Byte Description Type Property
20 | 1 SOC low byte Unsigned integer LSB = 1%
21 | 2 SOC high byte
22 | 3 SOH low byte Unsigned integer LSB = 1%
23 | 4 SOH high byte
24 | 5 SOC high definition low byte Unsigned integer LSB = 0.01%
25 | 6 SOC high definition high byte
26 |
27 | CAN message 0x356:
28 | Byte Description Type Property
29 | 1 Battery voltage low byte Signed integer LSB = 0.01V
30 | 2 Battery voltage high byte
31 | 3 Battery current low byte Signed integer LSB = 0.1A
32 | 4 Battery current high byte
33 | 5 Battery temperature low byte Signed integer LSB = 0.1°C
34 | 6 Battery temperature high byte
35 |
36 | CAN message 0x35A:
37 | Byte Description Type Property
38 | 1 Alarm byte 1 Unsigned char Bit orientated Alarm structure
39 | 2 Alarm byte 2 Unsigned char
40 | 3 Alarm byte 3 Unsigned char
41 | 4 Alarm byte 4 Unsigned char
42 | 5 Warning byte 1 Unsigned char Bit orientated Warning structure
43 | 6 Warning byte 2 Unsigned char
44 | 7 Warning byte 3 Unsigned char
45 | 8 Warning byte 4 Unsigned char
46 |
47 | CAN message 0x35E:
48 | Byte Description Type Property
49 | 1 Byte 1 ASCII BMS OEM description: ABCDEFG
50 | 2 Byte 2 ASCII
51 | 3 Byte 3 ASCII
52 | 4 Byte 4 ASCII
53 | 5 Byte 5 ASCII
54 | 6 Byte 6 ASCII
55 | 7 Byte 7 ASCII
56 | 8 Byte 8 ASCII
57 |
58 | CAN message 0x35F:
59 | Byte Description Type Property
60 | 1 Cell chemistry low byte Unsigned integer
61 | 2 Cell chemistry high byte
62 | 3 Hardware version low byte Byte
63 | 4 Hardware version high byte Byte
64 | 5 Capacity low byte Unsigned integer LSB = 1 Ah
65 | 6 Capacity high byte
66 | 7 Software version low byte Byte Version: 01
67 | 8 Software version high byte Byte
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### THIS IS A WORK IN PROGRESS -- YMMV, NO WARRANTY EXPRESSED OR IMPLIED. YOU CAN ENDANGER YOURSELF AND OTHERS.
2 |
3 | # SMA Venus Driver
4 | This project integrates the SMA Sunny Island inverters with Victron Venus OS. It supports SMA LiIon_Ext-BMS mode by providing BMS data to the Sunny Island via the CAN bus (directly supported by socketcan devices). The software runs on the Venus OS device as a Venus driver and uses the Venus dbus to read/write data for use with the Venus device.
5 |
6 | ### Kudos to Victron Energy
7 | Victron Engergy has provided much of the Venus OS architecture under Open Source copyright. This framework allows independent projects like this to exist. Even though we are using non-Victron hardware, we can include it into the Victron ecosystem with other Victron equipment. Victron stated that although they would not assist, they would not shut it down. Send support to Victron Energy by buying their products!
8 |
9 | Tested with RPi 3B - v2.60 Build 20200906135923
10 |
11 | ### Install
12 |
13 | The provided install.sh script will copy files download dependencies and should provide a running configuration. It will not setup valid configuration values so don't expect this to be plug and play:
14 |
15 | 1. flash the included "candlelight" firmware image onto your CAN adapter using your computer (see CAN adapter below)
16 | 2. enable root access on your Venus device (see useful reading below)
17 | 3. from root login on the venus root home directory
18 | 4. connect the CAN adapter to your Venus / Raspberry Pi now.
19 | 5. run wget https://github.com/madsci1016/SMAVenusDriver/raw/master/install/install.sh
20 | 6. run chmod +x install.sh
21 | 7. run ./install.sh
22 | 8. answer Y to the install of the driver
23 | 9. answer Y to the dependencies (unless they are already installed)
24 |
25 | ## Victron VenusOS Notes
26 |
27 | This project implements a com.victronenergy.vebus inverter/charger (like Multis, Quattros, Inverters) device so that the rest of the eco-system (web-ui, VRM portal, etc) grabs the data and displays/logs it.
28 |
29 | Victron is amazing at letting Venus OS be open source AND documenting it VERY well so that hackers can have at it. Yeah, they are understandably not thrilled I'm using a third-party device with their free stuff, but aren't against me doing it and specifically didn't ask me to stop (I offered). So, go buy Victron stuff, even if you already have an SMA inverter. I have 4 of their solar charge controllers and love them!
30 |
31 | That said, I'm not sure if I've emulated the Multiplus very well or in every way that I could. I did manage to reverse engineer the energy counting architecture so usage data should appear in the portal. But there are some quarks trying to do a 1-1 map SMA to Victron. For example, the SMA reports inverter power flow and AC-2 (external) power flow, but not output power flow implicitly per line. So I had to do math (inverter power, External power, and output power should always sum to 0, right) to get that value. There can be artifacts in the real time data because of that. SMA also doesn’t report energy at all, so my code is calculating that from power data to the best of it’s ability.
32 |
33 | So the Victron system (or whatever you use this code with) will needs its own battery monitor or device to measure/calculate the SoC at a minimum, plus you have to hard code some voltage limits to makeup the minimum BMS messages the SunnyIsland requires. I recommend the Smart Shunt (https://www.victronenergy.com/battery-monitors/smart-battery-shunt) or the BMV-712 (https://www.victronenergy.com/battery-monitors/bmv-712-smart) connected to the Raspberry Pi with the VE.Direct USB cable. Note: The VE.Direct interface on these devices are 3.3V
34 |
35 | ### Useful Reading
36 |
37 | Venus Developmental Info see https://github.com/victronenergy/venus/wiki/howto-add-a-driver-to-Venus .
38 |
39 | Enable Root / SSH Access see https://www.victronenergy.com/live/ccgx:root_access#:~:text=Go%20to%20Settings%2C%20General,Access%20Level%20change%20to%20Superuser.
40 |
41 | Add additional modules (now handles by install script) sww https://github.com/victronenergy/venus/wiki/installing-additional-python-modules.
42 |
43 | ## SMA Sunny Island
44 |
45 | The Sunny Island was originally designed to use Lead Acid batteries, only. Lithium-ion support was added as a firmware update and does not contain any BMS logic. It requires an external BMS to provide details of the battery SoC, SoH, charge current need, etc. If it does not receive valid BMS data within a period of time, it will shutdown.
46 |
47 | ### CAN Bus
48 | The Controller Area Network (CAN bus) is used at a rate of 500 kbs.
49 |
50 | NOTE: The SMA SI will go into hard shutdown mode if it hasn't received a good BMS message after several minutes. If this happens you will need to power off the DC side of the inverter and wait for 15-30 min capacitors to drain. If the cover is off, you can monitor the red LED located left and down of the center control panel. When it goes off it can be powered on.
51 |
52 | SMA SI Manual: https://files.sma.de/downloads/SI4548-6048-US-BE-en-21W.pdf
53 |
54 | Page 53, Section 6.4.2 Connecting the Data Cable of the Lithium-Ion Batteries details where to connect the RJ45 CAN cable
55 |
56 | #### CAN Adapter
57 | The SMA SI use the CAN bus to communicate between master/slave and other devices. In order to participate on the CAN bus, you must have a CAN adapter. The tested CAN adapter is the open source USB CANable device (https://canable.io/). Either version from https://store.protofusion.org/ will work. The firmware installed from ProtoFusion store is slcan, which emmulates a tty serial device. This project supports the "candlelight" FW by default, which will require a FW flash to the canable device. To flash your adapter, follow the directions here: https://canable.io/getting-started.html#flashing-new-firmware Use the ST DFU tool if you are on Windows. For more info, see: https://github.com/jaedog/SMAVenusDriver/wiki/Canable-Firmware.
58 |
59 | ##### CAN Pinouts
60 | The SMA SI uses an RJ45 connector for its CAN Bus interface.
61 |
62 | For a T-568B RJ45 pinout, the pins and colors are:
63 | 1. White Orange - Sync1 (reserved)
64 | 2. Orange - CAN_GND
65 | 3. White Green - SYNC_H
66 | 4. Blue - CAN_H
67 | 5. White Blue - CAN_L
68 | 6. Green - SYNC_L
69 | 7. White Brown - Sync7 (reserved)
70 | 8. Brown - Sync8 (reserved)
71 |
72 | The pins of interest are:
73 |
74 | * CAN_GND - Pin 2
75 | * CAN_H - Pin 4
76 | * CAN_L - Pin 5
77 |
78 | It is worth noting that there is a terminating resistor on both the CAN and SYNC lines as part of the SMA RJ-45 terminator dongle. However, in my experience terminating the CAN bus alone has not caused any issues with Master/Slave comms.
79 |
80 | ## Final Words
81 | There are still things hard-coded for specific applications. Although, configurability is improving. It supports the SMA as an off-grid (with grid available during low battery) with DC tied solar setup and the begining of support for AC coupled configurations. (See related project: https://github.com/jaedog/EnvoyVenusDriver for Enphase support). Note: The BMS logic is still **very crude** and may not work well depending on battery capacity or settings used.
82 |
83 | In case it wasn’t obvious, one fall back with this hack is if the Raspberry pi crashes or shuts off, the inverters will shut off as well. I recommend you have an offline back-up raspberry pi setup and ready to go to swap out in that event.
84 |
85 | ## Todo List
86 |
87 | 1) Proper charge controller state machine <-- IN WORK
88 | 2) Move configuration values (charge current, voltage thresholds, etc) to the Victron settings structure.
89 | 3) Create GUI in WEB_UI to change settings or trigger actions.
90 | 4) Convert polling CAN adapter to proper callback when new CAN message arrives.
91 | 5) Get logging working correctly.
92 |
93 | ## Tidbits
94 |
95 | ###### To determine if the driver is running execute:
96 | > ps | grep dbus-sma
97 | ```
98 | supervise dbus-sma
99 | multilog t s25000 n4 /var/log/dbus-sma <-- this will show up if logging is enabled
100 | python /data/etc/dbus-sma/dbus-sma.py
101 | grep dbus-sma
102 | ```
103 |
104 | ###### For debugging the script
105 | 1. Make sure the service auto start is disabled. Go to the /data/etc/dbus-sma directory.
106 | 2. Add the "down" file in ./service directory
107 | ```
108 | touch ./service/down <-- creates an empty file named "down"
109 | ```
110 | 3. Stop the service if is running by:
111 | ```
112 | svc -d /service/dbus-sma
113 | svstat /service/dbus-sma <-- checks if it is running, you can also do the ps cmd above
114 | ```
115 | 4. If you are using ssh to remote to the shell, you might want to be able to connect/disconnect the shell without disturbing the process. For that use "screen", a terminal multiplexer.
116 | 1. screen <-- starts a new screen
117 | 2. CTRL+A,D <-- disconnects from running screen
118 | 3. screen -r <-- reattaches to running screen
119 | 5. Now run the script: python dbus-sma.py
120 | 6. TBD logging...
121 |
122 | ###### Venus Service
123 |
124 | Venus uses daemontools (https://cr.yp.to/daemontools.html) to supervise and start the driver aka service.
125 |
126 | ## History
127 |
128 | ### Hacking the Sunny Island Notes
129 |
130 | The SMA SunnyIsland 6048 has two potential communications buses. One is a CAN bus “ComSync” and the other is a RS-485 bus "ComSma" that requires an adapter card to be installed. The CAN bus is used by the SMA’s to communicate from the master to the slaves in a cluster, and to a Battery Management System (BMS) when configure in Lithium Ion mode. The RS-485 bus is required to connect to SMA grid tie inverters and to the WebBox.
131 |
132 | It's clear the RS-485 was always the intended bus to connect to logging and telemetry systems such as the discontinued Sunny WebBox that allows you to see system telemetry on the SMA portal. But as they are very expensive now that they are discontinued, it isn’t a good option.
133 |
134 | I started dumping the CAN bus to see what was there. There is of course a lot of high frequency messages I’m sure are used to sync up master and slave units, as well as the BMS traffic which is documented on page 10 of this BMS manual: http://www.rec-bms.com/datasheet/UserManual9R_SMA.pdf
135 |
136 | BUT I also noticed there were some bytes in some messages moving in ways that appeared to correlate to system metrics. And indeed, they did. However, resolution is rather low, all power metrics are reported in 100s of watts. I realized this matched what is shown on the inverter screen, and then a light went off. You can buy the SMA “SunnyRemote” box which also connects by CAN bus. So these messages must be the system data meant for the “SunnyRemote” which has the same screen and menu as the local screen on the inverters.
137 |
138 | SO what this codes is doing is broken down into to big parts. First, it needs to pretend to be a BMS so the SunnyIslands will ingest battery SoC and charge current commands. Second, it is listening for the traffic intended for the “SunnyRemote” box so we can use it to extract ang log system metrics. All this is done through the CAN bus, so no additional parts need to be ordered.
139 |
--------------------------------------------------------------------------------
/assets/overview-inverter.svg:
--------------------------------------------------------------------------------
1 |
2 |
145 |
--------------------------------------------------------------------------------
/bin/canable_fw_d2cb159.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madsci1016/SMAVenusDriver/499880ed0582ee2b8831f7d76c45724d550fae51/bin/canable_fw_d2cb159.bin
--------------------------------------------------------------------------------
/dbus-sma/bms_state_machine.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """bms_state_machine.py: Statemachine to control the charge profile of the
5 | SMA SunnyIsland inverters with Victron Venus OS. """
6 |
7 | __author__ = "github usernames: jaedog"
8 | __copyright__ = "Copyright 2020"
9 | __license__ = "MIT"
10 | __version__ = "0.1"
11 |
12 | import logging
13 | from statemachine import StateMachine, State
14 | from datetime import datetime, timedelta
15 |
16 | # State machine class, handles state changes as uses
17 | # https://github.com/rschrader/python-statemachine
18 | # install with pip:
19 | # $ pip install python-statemachine
20 | #
21 |
22 | logger = logging.getLogger(__name__)
23 | #logger.setLevel(logging.INFO)
24 |
25 | class BMSChargeStateMachine(StateMachine):
26 | idle = State("Idle", initial=True)#, value=1)
27 | bulk_chg = State("ConstCurChg")#, value=2)
28 | absorb_chg = State("ConstVoltChg")#, value=3)
29 | float_chg = State("FloatChg")#, value=4)
30 | canceled = State("CancelChg")#, value=5)
31 |
32 | bulk = idle.to(bulk_chg)
33 | absorb = bulk_chg.to(absorb_chg)
34 | floating = absorb_chg.to(float_chg)
35 | rebulk = bulk_chg.from_(absorb_chg, float_chg)
36 | cancel = canceled.from_(bulk_chg, absorb_chg, float_chg)
37 |
38 | # canceled is the final state and cannot be cycled back to idle
39 | # create a new state machine to restart the charge cycle
40 | cycle = bulk | absorb | floating
41 |
42 | def on_enter_idle(self):
43 | if (getattr(self.model, "on_enter_idle", None) != None):
44 | self.model.on_enter_idle()
45 |
46 | def on_enter_bulk_chg(self):
47 | if (getattr(self.model, "on_enter_bulk_chg", None) != None):
48 | self.model.on_enter_bulk_chg()
49 |
50 | def on_enter_absorb_chg(self):
51 | if (getattr(self.model, "on_enter_absorb_chg", None) != None):
52 | self.model.on_enter_absorb_chg()
53 |
54 | def on_enter_float_chg(self):
55 | if (getattr(self.model, "on_enter_float_chg", None) != None):
56 | self.model.on_enter_float_chg()
57 |
58 | # Charge Model, contains the model of the bms charger
59 | class BMSChargeModel(object):
60 | def __init__(self, charge_bulk_current, charge_absorb_voltage, \
61 | charge_float_voltage, time_min_absorb, rebulk_voltage):
62 | self.charge_absorb_voltage = charge_absorb_voltage
63 | self.charge_bulk_current = charge_bulk_current
64 | self.original_bulk_current = charge_bulk_current
65 | self.charge_float_voltage = charge_float_voltage
66 | self.time_min_absorb = time_min_absorb
67 | self.rebulk_voltage = rebulk_voltage
68 |
69 | self.actual_voltage = 0.0
70 | self.last_error = 0.0
71 | self.actual_current = 0.0
72 | self.set_current = 0.0
73 |
74 | # init callback
75 | self.state_changed = False
76 | self.check_state = self.check_idle_state
77 | self.last_voltage = 0.0
78 |
79 | # event callbacks when entering different states
80 | def on_enter_idle(self):
81 | self.check_state = self.check_idle_state
82 |
83 | def on_enter_bulk_chg(self):
84 | self.check_state = self.check_bulk_chg_state
85 |
86 | def on_enter_absorb_chg(self):
87 | self.check_state = self.check_absorb_chg_state
88 | self.start_of_absorb_chg = datetime.now()
89 |
90 | def on_enter_float_chg(self):
91 | self.check_state = self.check_float_chg_state
92 |
93 | # functions used for logic on various states
94 | def check_idle_state(self):
95 | pass
96 |
97 | def update_battery_data(self, voltage, current):
98 | # use rounded values in logic
99 | self.actual_voltage = round(voltage, 2)
100 | self.actual_current = round(current, 1)
101 |
102 | def check_bulk_chg_state(self):
103 | self.set_current = self.charge_bulk_current
104 | if (self.actual_voltage >= self.charge_absorb_voltage):
105 | #self.set_current = 0
106 | # move to next state
107 | self.last_voltage = self.actual_voltage
108 | return 1
109 | return 0
110 |
111 | # TODO: WIP, values for testing.................................
112 | def do_current_logic(self, set_voltage):
113 | # if the batt voltage is greater than the set_voltage, it overshot
114 | #if (self.actual_voltage > set_voltage):
115 | # if the set current greater than actual, start with actual
116 | if (self.set_current > self.actual_current):
117 | self.set_current = self.actual_current
118 |
119 | #if (self.actual_voltage > set_voltage):
120 | # lower set current
121 | # Simple PD Loop
122 | P = 100.0
123 | D = 20.0
124 | Error = set_voltage - self.actual_voltage
125 | change = P*Error + D*(Error - self.last_error)
126 | print( "Error: " + str(Error) + " Last Error: " + str(self.last_error) + " Change: " + str(change))
127 | self.set_current += change
128 | self.last_error = Error
129 | #self.set_current -= 0.2
130 | #else:
131 | # self.set_current = self.actual_current
132 |
133 | # if set current is below min, set to min
134 | if (self.set_current < 0.6):
135 | self.set_current = 0.6
136 |
137 | # if the batt voltage is less than the set_voltage, inc current
138 | #elif (self.actual_voltage < set_voltage):
139 | # if the set current is less than the actual current, start with actual
140 | # if (self.set_current < self.actual_current):
141 | # self.set_current = self.actual_current
142 |
143 | # inc set current
144 | # self.set_current += 0.1
145 |
146 | # if set current is greater than max, set it to max
147 | #if (self.set_current > 10.0):
148 | # self.set_current = 10.0
149 |
150 | # cap charge current to the bulk_chg state
151 | if (self.set_current > self.charge_bulk_current):
152 | self.set_current = self.charge_bulk_current
153 |
154 | self.set_current = round(self.set_current, 1)
155 |
156 | logger.info("Actual Current: {0:.1f}A, Set Current: {1:.1f}A, Last Voltage: {2:.2f}V, Actual Voltage: {3:.2f}V"\
157 | .format(self.actual_current, self.set_current, self.last_voltage, self.actual_voltage))
158 |
159 | self.last_voltage = self.actual_voltage
160 |
161 | def check_absorb_chg_state(self):
162 | # if we just transitioned from bulk
163 | if (self.state_changed):
164 | #self.set_current = 0
165 | return 0
166 |
167 | # if voltage falls below rebulk, go back to bulk
168 | if (self.actual_voltage timedelta(minutes=self.time_min_absorb)):
172 | return 1
173 |
174 | self.do_current_logic( self.charge_absorb_voltage)
175 | return 0
176 |
177 | def check_float_chg_state(self):
178 | # if voltage falls below rebulk voltage, go back to bulk
179 | if (self.actual_voltage < self.rebulk_voltage):
180 | return -1
181 |
182 | self.do_current_logic(self.charge_float_voltage)
183 | return 0
184 |
185 | # Charge controller, external interface to the bms state machine charger
186 | class BMSChargeController(object):
187 | def __init__(self, charge_bulk_current, charge_absorb_voltage, \
188 | charge_float_voltage, time_min_absorb, rebulk_voltage):
189 | self.model = BMSChargeModel(charge_bulk_current, charge_absorb_voltage, \
190 | charge_float_voltage, time_min_absorb, rebulk_voltage)
191 | self.state_machine = BMSChargeStateMachine(self.model)
192 |
193 | def __str__(self):
194 | return "BMS Charge Config, CC: {0}A, CV: {1}V, CV Time: {2} hrs, Float: {3}V" \
195 | .format(self.model.charge_bulk_current, self.model.charge_absorb_voltage, \
196 | self.model.time_min_absorb, self.model.charge_float_voltage)
197 |
198 | def update_battery_data(self, voltage, current):
199 | self.model.update_battery_data(voltage, current)
200 | return self.check_state()
201 |
202 | def update_req_bulk_current(self, current):
203 | if (current == None):
204 | self.model.charge_bulk_current = self.model.original_bulk_current
205 | else:
206 | self.model.charge_bulk_current = current
207 |
208 | def start_charging(self):
209 | if (self.state_machine.current_state == self.state_machine.idle):
210 | self.state_machine.cycle()
211 | return True
212 | return False
213 |
214 | def is_charging(self):
215 | if ((self.state_machine.current_state == self.state_machine.bulk_chg) or
216 | (self.state_machine.current_state == self.state_machine.absorb_chg) or
217 | (self.state_machine.current_state == self.state_machine.float_chg)):
218 | return True
219 | return False
220 |
221 | def stop_charging(self):
222 | print ("stop_charging")
223 | self.state_machine.cancel()
224 |
225 | def check_state(self):
226 | self.state_changed = True
227 |
228 | val = self.model.check_state()
229 | if (val == 0):
230 | self.state_changed = False
231 | elif (val == 1):
232 | self.state_machine.cycle()
233 | elif (val == -1):
234 | # rebulk
235 | self.state_machine.rebulk()
236 |
237 | return val
238 |
239 | def get_charge_current(self):
240 | return self.model.set_current
241 |
242 | def get_state(self):
243 | return self.state_machine.current_state.value
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
--------------------------------------------------------------------------------
/dbus-sma/bms_test.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import time
5 | from bms_state_machine import BMSChargeStateMachine, BMSChargeModel, BMSChargeController
6 |
7 | # 48V 16S LiFePO4 Battery
8 |
9 | # Absorption: 58V (56.4 for longer life)
10 | # Float: 54.4V
11 | # Restart bulk voltage: Float-0.8 (max of 54V)
12 | # Inverter Cut-off: 42.8V-48V (depending on size of load and voltage drop etc)
13 |
14 | bms_controller = BMSChargeController(charge_bulk_current=160, charge_absorb_voltage=58.4, \
15 | charge_float_voltage=54.4, time_min_absorb=0.5, rebulk_voltage=53.6) # or 30 seconds for the simulation
16 | ret = bms_controller.start_charging()
17 |
18 | print ("{0}, Start Charging: {1}".format(bms_controller, ret))
19 |
20 | # simulated battery voltage
21 | bat_voltage = 42.8
22 | counter = 0
23 |
24 | while (True):
25 |
26 | charge_current = 0.0
27 | is_state_changed = bms_controller.update_battery_data(bat_voltage, charge_current)
28 | state = bms_controller.get_state()
29 | charge_current = bms_controller.get_charge_current()
30 |
31 | print ("Battery Voltage: {0}, Charge Current: {1}, Charge State: {2}, State Changed: {3}".format(bat_voltage, charge_current, state, is_state_changed))
32 |
33 | time.sleep(1)
34 |
35 |
36 | # update simulated values
37 | if (is_state_changed):
38 | if (state == "absorb_chg"):
39 | bat_voltage = 58.2
40 | elif (state == "float_chg"):
41 | bat_voltage = 56.1
42 |
43 | if (state == "bulk_chg"):
44 | bat_voltage += 1.8
45 | elif (state == "absorb_chg"):
46 | if (charge_current > 0):
47 | bat_voltage += charge_current * 0.1
48 | elif (charge_current == 0):
49 | bat_voltage -= 0.01
50 | if (counter > 5):
51 | counter += 1
52 | if (counter > 15):
53 | bat_voltage = 54
54 | counter = 0
55 | elif (state == "float_chg"):
56 | counter += 1
57 | if (counter > 5) :
58 | bat_voltage = 53
59 |
60 | if (charge_current > 0):
61 | bat_voltage += charge_current * 0.1
62 | elif (charge_current == 0):
63 | bat_voltage -= 0.03
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/dbus-sma/dbus-sma.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """dbus-sma.py: Driver to integrate SMA SunnyIsland inverters
5 | with Victron Venus OS. """
6 |
7 | __author__ = "github usernames: madsci1016, jaedog"
8 | __copyright__ = "Copyright 2020"
9 | __license__ = "MIT"
10 | __version__ = "1.1"
11 |
12 | import os
13 | import signal
14 | import sys
15 | import argparse
16 | import serial
17 | import socket
18 | import logging
19 | import yaml
20 |
21 | from dbus.mainloop.glib import DBusGMainLoop
22 | import dbus
23 | import gobject
24 |
25 | import can
26 | from can.bus import BusState
27 | from timeit import default_timer as timer
28 | import time
29 | from datetime import datetime, timedelta
30 |
31 | # Victron packages
32 | sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'ext', 'velib_python'))
33 | from vedbus import VeDbusService
34 | from ve_utils import get_vrm_portal_id, exit_on_error
35 | from dbusmonitor import DbusMonitor
36 | from settingsdevice import SettingsDevice # available in the velib_python repository
37 |
38 | from bms_state_machine import BMSChargeStateMachine, BMSChargeModel, BMSChargeController
39 |
40 |
41 | #from settingsdevice import SettingsDevice
42 | #from logger import setup_logging
43 | #import delegates
44 | #from sc_utils import safeadd as _safeadd, safemax as _safemax
45 |
46 | # ignore terminal resize signals (keeps exception from being thrown)
47 | signal.signal(signal.SIGWINCH, signal.SIG_IGN)
48 |
49 |
50 | softwareVersion = '1.1'
51 | #logger = logging.getLogger("dbus-sma")
52 | #logger = logging.getLogger(__name__)
53 |
54 | # global logger for all modules imported here
55 | logger = logging.getLogger()
56 |
57 | #logging.basicConfig(filename='/data/etc/dbus-sma/logging.log', encoding='utf-8', level=logging.INFO)
58 | #logger.setLevel(logging.DEBUG)
59 | logger.setLevel(logging.INFO)
60 |
61 | # The CANable (https://canable.io/) is a small open-source USB to CAN adapter. The CANable can show up as a virtual serial port (slcan): /dev/ttyACM0 or
62 | # as a socketcan: can0. In testing both methods work, however, I found the can0 to be much more robust.
63 | # Devices from http://protofusion.org store ship by default with the "slcan" firmware. It can be flashed with the "candlelight" firmware to
64 | # support socketcan.
65 |
66 | # When the adapter is a socketcan, bring up link first as root:
67 | # ip link set can0 up type can bitrate 500000
68 | #
69 | # To capture CAN msgs on the bus:
70 | # tcpdump -w capture.pcap -i can5
71 |
72 | # TODO: change to input param
73 | #canBusChannel = "/dev/ttyACM0"
74 | canBusChannel = "can5"
75 |
76 | #canBusType = "slcan"
77 | canBusType = "socketcan"
78 |
79 | # connect and register to dbus
80 | driver = {
81 | 'name' : "SMA SunnyIsland",
82 | 'servicename' : "smasunnyisland",
83 | 'instance' : 261,
84 | 'id' : 2754,
85 | 'version' : 0x476,
86 | 'serial' : "SMABillConnect",
87 | 'connection' : "com.victronenergy.vebus.smasunnyisland"
88 | }
89 |
90 | CAN_tx_msg = {"BatChg": 0x351, "BatSoC": 0x355, "BatVoltageCurrent" : 0x356, "AlarmWarning": 0x35a, "BMSOem": 0x35e, "BatData": 0x35f}
91 | CANFrames = {"ExtPwr": 0x300, "InvPwr": 0x301, "OutputVoltage": 0x304, "Battery": 0x305, "Relay": 0x306, "Bits": 0x307, "LoadPwr": 0x308, "ExtVoltage": 0x309}
92 | sma_line1 = {"OutputVoltage": 0, "ExtPwr": 0, "InvPwr": 0, "ExtVoltage": 0, "ExtFreq": 0.00, "OutputFreq": 0.00}
93 | sma_line2 = {"OutputVoltage": 0, "ExtPwr": 0, "InvPwr": 0, "ExtVoltage": 0}
94 | sma_battery = {"Voltage": 0, "Current": 0}
95 | sma_system = {"State": 0, "ExtRelay" : 0, "ExtOk" : 0, "Load" : 0}
96 |
97 | settings = 0
98 |
99 | #command packets to turn SMAs on or off
100 | SMA_ON_MSG = can.Message(arbitration_id = 0x35C, #on
101 | data=[0b00000001,0,0,0],
102 | is_extended_id=False)
103 |
104 | SMA_OFF_MSG = can.Message(arbitration_id = 0x35C, #off
105 | data=[0b00000010,0,0,0],
106 | is_extended_id=False)
107 |
108 |
109 |
110 | def getSignedNumber(number, bitLength):
111 | mask = (2 ** bitLength) - 1
112 | if number & (1 << (bitLength - 1)):
113 | return number | ~mask
114 | else:
115 | return number & mask
116 |
117 | def bytes(integer):
118 | return divmod(integer, 0x100)
119 |
120 | class BMSData:
121 | def __init__(self, max_battery_voltage, min_battery_voltage, low_battery_voltage, \
122 | charge_bulk_amps, max_discharge_amps, charge_absorb_voltage, charge_float_voltage, \
123 | time_min_absorb, rebulk_voltage):
124 |
125 | # settings for BMS
126 |
127 | # max and min battery voltage is used by the SMA as fault values
128 | # if the voltage goes above max or below min, the SMA will fault OFF
129 | # the inverter.
130 | self.max_battery_voltage = max_battery_voltage
131 | self.min_battery_voltage = min_battery_voltage
132 |
133 | # low battery voltage is used to trigger the SMA to connect to grid and
134 | # begin charging the batteries. Note, this value must be greater than
135 | # the min_battery_voltage
136 | self.low_battery_voltage = low_battery_voltage
137 |
138 | self.charge_bulk_amps = charge_bulk_amps
139 | self.max_discharge_amps = max_discharge_amps
140 | self.charge_absorb_voltage = charge_absorb_voltage
141 | self.charge_float_voltage = charge_float_voltage
142 | self.time_min_absorb = time_min_absorb
143 | self.rebulk_voltage = rebulk_voltage
144 |
145 | # state of BMS
146 | self.charging_state = "" # state of charge state machine
147 | self.state_of_charge = 42.0 # sane initial value
148 | self.actual_battery_voltage = 0.0
149 | self.req_discharge_amps = max_discharge_amps
150 | self.battery_current = 0.0
151 | self.pv_current = 0.0
152 |
153 | def __str__(self):
154 | return "BMS Data, MaxV: {0}V, MinV: {1}V, LowV: {2}V, BulkA: {3}A, AbsorbV: {4}V, FloatV: {5}V, MinuteAbsorb: {6}, RebulkV: {7}V" \
155 | .format(self.max_battery_voltage, self.min_battery_voltage, self.low_battery_voltage, self.charge_bulk_amps, \
156 | self.charge_absorb_voltage, self.charge_float_voltage, self.time_min_absorb, self.rebulk_voltage)
157 |
158 | # SMA Driver Class
159 | class SmaDriver:
160 |
161 | def __init__(self):
162 | self.driver_start_time = datetime.now()
163 |
164 | # data from yaml config file
165 | self._cfg = self.get_config_data()
166 | _cfg_bms = self._cfg['BMSData']
167 |
168 | # TODO: use venus settings to define these values
169 | #Initial BMS values eventually read from settings.
170 | self._bms_data = BMSData(max_battery_voltage=_cfg_bms['max_battery_voltage'], \
171 | min_battery_voltage=_cfg_bms['min_battery_voltage'], low_battery_voltage=_cfg_bms['low_battery_voltage'], \
172 | charge_bulk_amps=_cfg_bms['charge_bulk_amps'], max_discharge_amps=_cfg_bms['max_discharge_amps'], \
173 | charge_absorb_voltage=_cfg_bms['charge_absorb_voltage'], charge_float_voltage=_cfg_bms['charge_float_voltage'], \
174 | time_min_absorb=_cfg_bms['time_min_absorb'], rebulk_voltage=_cfg_bms['rebulk_voltage'])
175 |
176 | self.bms_controller = BMSChargeController(charge_bulk_current=self._bms_data.charge_bulk_amps, \
177 | charge_absorb_voltage=self._bms_data.charge_absorb_voltage, charge_float_voltage=self._bms_data.charge_float_voltage, \
178 | time_min_absorb=self._bms_data.time_min_absorb, rebulk_voltage=self._bms_data.rebulk_voltage)
179 | ret = self.bms_controller.start_charging()
180 |
181 | # Have a mainloop, so we can send/receive asynchronous calls to and from dbus
182 | DBusGMainLoop(set_as_default=True)
183 |
184 | self._can_bus = False
185 |
186 | self._safety_off = False #flag to see if we every shut the inverters off due to low batt.
187 |
188 | logger.debug("Can bus init")
189 | try :
190 | self._can_bus = can.interface.Bus(bustype=canBusType, channel=canBusChannel, bitrate=500000)
191 | except can.CanError as e:
192 | logger.error(e)
193 |
194 | logger.debug("Can bus init done")
195 |
196 | # Add the AcInput1 setting if it doesn't exist so that the grid data is reported
197 | # to the system by dbus-systemcalc-py service
198 | settings = SettingsDevice(
199 | bus=dbus.SystemBus(),# if (platform.machine() == 'armv7l') else dbus.SessionBus(),
200 | supportedSettings={
201 | 'acinput': ['/Settings/SystemSetup/AcInput1', 1, 0, 0],
202 | 'hub4mode': ['/Settings/CGwacs/Hub4Mode', 3, 0, 0],
203 | 'gridmeter': ['/Settings/CGwacs/RunWithoutGridMeter', 1, 0, 0],
204 | 'acsetpoint': ['/Settings/CGwacs/AcPowerSetPoint', 0, 0, 0],
205 | 'maxchargepwr': ['/Settings/CGwacs/MaxChargePower', 0, 0, 0],
206 | 'maxdischargepwr': ['/Settings/CGwacs/MaxDischargePower', 0, 0, 0],
207 | 'maxchargepercent': ['/Settings/CGwacs/MaxChargePercentage', 0, 0, 0],
208 | 'maxdischargepercent': ['/Settings/CGwacs/MaxDischargePercentage', 0, 0, 0],
209 | 'essMode': ['/Settings/CGwacs/BatteryLife/State', 0, 0, 0],
210 | },
211 | eventCallback=None)
212 |
213 |
214 | # Why this dummy? Because DbusMonitor expects these values to be there, even though we don't
215 | # need them. So just add some dummy data. This can go away when DbusMonitor is more generic.
216 | dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None}
217 | dbus_tree = {'com.victronenergy.system':
218 | {'/Dc/Battery/Soc': dummy, '/Dc/Battery/Current': dummy, '/Dc/Battery/Voltage': dummy, \
219 | '/Dc/Pv/Current': dummy, '/Ac/PvOnOutput/L1/Power': dummy, '/Ac/PvOnOutput/L2/Power': dummy, }}
220 |
221 | self._dbusmonitor = self._create_dbus_monitor(dbus_tree, valueChangedCallback=self._dbus_value_changed)
222 |
223 | self._dbusservice = self._create_dbus_service()
224 |
225 | self._dbusservice.add_path('/Serial', value=12345)
226 |
227 | # /SystemState/State -> 0: Off
228 | # -> 1: Low power
229 | # -> 2: VE.Bus Fault condition
230 | # -> 3: Bulk charging
231 | # -> 4: Absorption charging
232 | # -> 5: Float charging
233 | # -> 6: Storage mode
234 | # -> 7: Equalisation charging
235 | # -> 8: Passthru
236 | # -> 9: Inverting
237 | # -> 10: Assisting
238 | # -> 256: Discharging
239 | # -> 257: Sustain
240 | self._dbusservice.add_path('/State', 0)
241 | self._dbusservice.add_path('/Mode', 3)
242 | self._dbusservice.add_path('/Ac/PowerMeasurementType', 0)
243 | self._dbusservice.add_path('/Hub4/AssistantId', 5)
244 | self._dbusservice.add_path('/Hub4/DisableCharge', value=0, writeable=True)
245 | self._dbusservice.add_path('/Hub4/DisableFeedIn', value=0, writeable=True)
246 | self._dbusservice.add_path('/Hub4/DoNotFeedInOverVoltage', value=0, writeable=True)
247 | self._dbusservice.add_path('/Hub4/L1/AcPowerSetpoint', value=0, writeable=True)
248 | self._dbusservice.add_path('/Hub4/L2/AcPowerSetpoint', value=0, writeable=True)
249 | self._dbusservice.add_path('/Hub4/Sustain', value=0, writeable=True)
250 | self._dbusservice.add_path('/Hub4/L1/MaxFeedInPower', value=0, writeable=True)
251 | self._dbusservice.add_path('/Hub4/L2/MaxFeedInPower', value=0, writeable=True)
252 |
253 |
254 | # Create the inverter/charger paths
255 | self._dbusservice.add_path('/Ac/Out/L1/P', -1)
256 | self._dbusservice.add_path('/Ac/Out/L2/P', -1)
257 | self._dbusservice.add_path('/Ac/Out/L1/I', -1)
258 | self._dbusservice.add_path('/Ac/Out/L2/I', -1)
259 | self._dbusservice.add_path('/Ac/Out/L1/V', -1)
260 | self._dbusservice.add_path('/Ac/Out/L2/V', -1)
261 | self._dbusservice.add_path('/Ac/Out/L1/F', -1)
262 | self._dbusservice.add_path('/Ac/Out/L2/F', -1)
263 | self._dbusservice.add_path('/Ac/Out/P', -1)
264 | self._dbusservice.add_path('/Ac/ActiveIn/L1/P', -1)
265 | self._dbusservice.add_path('/Ac/ActiveIn/L2/P', -1)
266 | self._dbusservice.add_path('/Ac/ActiveIn/P', -1)
267 | self._dbusservice.add_path('/Ac/ActiveIn/L1/V', -1)
268 | self._dbusservice.add_path('/Ac/ActiveIn/L2/V', -1)
269 | self._dbusservice.add_path('/Ac/ActiveIn/L1/F', -1)
270 | self._dbusservice.add_path('/Ac/ActiveIn/L2/F', -1)
271 | self._dbusservice.add_path('/Ac/ActiveIn/L1/I', -1)
272 | self._dbusservice.add_path('/Ac/ActiveIn/L2/I', -1)
273 | self._dbusservice.add_path('/Ac/ActiveIn/Connected', 1)
274 | self._dbusservice.add_path('/Ac/ActiveIn/ActiveInput', 0)
275 | self._dbusservice.add_path('/VebusError', 0)
276 | self._dbusservice.add_path('/Dc/0/Voltage', -1)
277 | self._dbusservice.add_path('/Dc/0/Power', -1)
278 | self._dbusservice.add_path('/Dc/0/Current', -1)
279 | self._dbusservice.add_path('/Ac/NumberOfPhases', 2)
280 | self._dbusservice.add_path('/Alarms/GridLost', 0)
281 |
282 | # /VebusChargeState <- 1. Bulk
283 | # 2. Absorption
284 | # 3. Float
285 | # 4. Storage
286 | # 5. Repeat absorption
287 | # 6. Forced absorption
288 | # 7. Equalise
289 | # 8. Bulk stopped
290 | self._dbusservice.add_path('/VebusChargeState', 0)
291 |
292 | # Some attempts at logging consumption. Float of kwhr since driver start (i think)
293 | self._dbusservice.add_path('/Energy/GridToDc', 0)
294 | self._dbusservice.add_path('/Energy/GridToAcOut', 0)
295 | self._dbusservice.add_path('/Energy/DcToAcOut', 0)
296 | self._dbusservice.add_path('/Energy/AcIn1ToInverter', 0)
297 | self._dbusservice.add_path('/Energy/AcIn1ToAcOut', 0)
298 | self._dbusservice.add_path('/Energy/InverterToAcOut', 0)
299 | self._dbusservice.add_path('/Energy/Time', timer())
300 |
301 | self._changed = True
302 |
303 | # create timers (time in msec)
304 | gobject.timeout_add(2000, exit_on_error, self._can_bus_txmit_handler)
305 | gobject.timeout_add(2000, exit_on_error, self._energy_handler)
306 | gobject.timeout_add(20, exit_on_error, self._parse_can_data_handler)
307 |
308 | #----
309 | def __del__(self):
310 | if (self._can_bus):
311 | self._can_bus.shutdown()
312 | self._can_bus = False
313 | logger.debug("bus shutdown")
314 |
315 | #----
316 | def run(self):
317 | # Start and run the mainloop
318 | logger.info("Starting mainloop, responding only on events")
319 | self._mainloop = gobject.MainLoop()
320 |
321 | try:
322 | self._mainloop.run()
323 | except KeyboardInterrupt:
324 | self._mainloop.quit()
325 |
326 | #----
327 | def _create_dbus_monitor(self, *args, **kwargs):
328 | return DbusMonitor(*args, **kwargs)
329 |
330 | #----
331 | def _create_dbus_service(self):
332 | dbusservice = VeDbusService(driver['connection'])
333 | dbusservice.add_mandatory_paths(
334 | processname=__file__,
335 | processversion=softwareVersion,
336 | connection=driver['connection'],
337 | deviceinstance=driver['instance'],
338 | productid=driver['id'],
339 | productname=driver['name'],
340 | firmwareversion=driver['version'],
341 | hardwareversion=driver['version'],
342 | connected=1)
343 | return dbusservice
344 |
345 | #----
346 | # callback that gets called ever time a dbus value has changed
347 | def _dbus_value_changed(self, dbusServiceName, dbusPath, dict, changes, deviceInstance):
348 | self._changed = True
349 |
350 | #----
351 | # called by timer every 20 msec
352 | def _parse_can_data_handler(self):
353 |
354 | try:
355 | msg = None
356 | # read msgs until we get one we want
357 | while True:
358 | msg = self._can_bus.recv(1)
359 | if (msg is None) :
360 | sma_system["State"] = 0
361 | #self._dbusservice["/State"] = 0
362 | logger.info("No Message received from Sunny Island")
363 | return True
364 |
365 | if (msg.arbitration_id == CANFrames["ExtPwr"] or msg.arbitration_id == CANFrames["InvPwr"] or \
366 | msg.arbitration_id == CANFrames["LoadPwr"] or msg.arbitration_id == CANFrames["OutputVoltage"] or \
367 | msg.arbitration_id == CANFrames["ExtVoltage"] or msg.arbitration_id == CANFrames["Battery"] or \
368 | msg.arbitration_id == CANFrames["Relay"] or msg.arbitration_id == CANFrames["Bits"]):
369 | break
370 |
371 | if msg is not None:
372 | if msg.arbitration_id == CANFrames["ExtPwr"]:
373 | sma_line1["ExtPwr"] = (getSignedNumber(msg.data[0] + msg.data[1]*256, 16)*100)
374 | sma_line2["ExtPwr"] = (getSignedNumber(msg.data[2] + msg.data[3]*256, 16)*100)
375 | #self._updatedbus()
376 | #print ("Ex Power L1: " + str(sma_line1["ExtPwr"]) + " Power L2: " + str(sma_line2["ExtPwr"]))
377 | elif msg.arbitration_id == CANFrames["InvPwr"]:
378 | sma_line1["InvPwr"] = (getSignedNumber(msg.data[0] + msg.data[1]*256, 16)*100)
379 | sma_line2["InvPwr"] = (getSignedNumber(msg.data[2] + msg.data[3]*256, 16)*100)
380 | #calculate_pwr()
381 | #print ("Power L1: " + str(sma_line1["InvPwr"]) + " Power L2: " + str(sma_line2["InvPwr"]))
382 | self._updatedbus()
383 | elif msg.arbitration_id == CANFrames["LoadPwr"]:
384 | sma_system["Load"] = (getSignedNumber(msg.data[0] + msg.data[1]*256, 16)*100)
385 | self._updatedbus()
386 | elif msg.arbitration_id == CANFrames["OutputVoltage"]:
387 | sma_line1["OutputVoltage"] = (float(getSignedNumber(msg.data[0] + msg.data[1]*256, 16))/10)
388 | sma_line2["OutputVoltage"] = (float(getSignedNumber(msg.data[2] + msg.data[3]*256, 16))/10)
389 | sma_line1["OutputFreq"] = float(msg.data[6] + msg.data[7]*256) / 100
390 | self._updatedbus()
391 | elif msg.arbitration_id == CANFrames["ExtVoltage"]:
392 | sma_line1["ExtVoltage"] = (float(getSignedNumber(msg.data[0] + msg.data[1]*256, 16))/10)
393 | sma_line2["ExtVoltage"] = (float(getSignedNumber(msg.data[2] + msg.data[3]*256, 16))/10)
394 | sma_line1["ExtFreq"] = float(msg.data[6] + msg.data[7]*256) / 100
395 | self._updatedbus()
396 | elif msg.arbitration_id == CANFrames["Battery"]:
397 | sma_battery["Voltage"] = float(msg.data[0] + msg.data[1]*256) / 10
398 | sma_battery["Current"] = float(getSignedNumber(msg.data[2] + msg.data[3]*256, 16)) / 10
399 | self._updatedbus()
400 | elif msg.arbitration_id == CANFrames["Bits"]:
401 | if msg.data[2]&128:
402 | sma_system["ExtRelay"] = 1
403 | else:
404 | sma_system["ExtRelay"] = 0
405 | if msg.data[2]&64:
406 | sma_system["ExtOk"] = 0
407 | #print ("Grid OK")
408 | else:
409 | #it seems to always report grid down once during relay transfer, so lets wait for two messages to latch.
410 | if sma_system["ExtOk"] == 0:
411 | sma_system["ExtOk"] = 1
412 | elif sma_system["ExtOk"] == 1:
413 | sma_system["ExtOk"] = 2
414 | #print ("Grid Down")
415 |
416 | #print ("307 message" )
417 | #print(msg)
418 |
419 | except (KeyboardInterrupt) as e:
420 | self._mainloop.quit()
421 | except (can.CanError) as e:
422 | logger.error(e)
423 | pass
424 | except Exception as e:
425 | exception_type = type(e).__name__
426 | logger.error("Exception occured: {0}, {1}".format(exception_type, e))
427 |
428 | return True
429 |
430 | #----
431 | def _updatedbus(self):
432 | #self._dbusservice["/State"] = sma_system["State"]
433 | self._dbusservice["/Ac/ActiveIn/L1/P"] = sma_line1["ExtPwr"]
434 | self._dbusservice["/Ac/ActiveIn/L2/P"] = sma_line2["ExtPwr"]
435 | self._dbusservice["/Ac/ActiveIn/L1/V"] = sma_line1["ExtVoltage"]
436 | self._dbusservice["/Ac/ActiveIn/L2/V"] = sma_line2["ExtVoltage"]
437 | self._dbusservice["/Ac/ActiveIn/L1/F"] = sma_line1["ExtFreq"]
438 | self._dbusservice["/Ac/ActiveIn/L2/F"] = sma_line1["ExtFreq"]
439 | if sma_system["ExtOk"] == 0 or sma_system["ExtOk"] == 2:
440 | self._dbusservice["/Alarms/GridLost"] = sma_system["ExtOk"]
441 | if sma_line1["ExtVoltage"] != 0:
442 | self._dbusservice["/Ac/ActiveIn/L1/I"] = int(sma_line1["ExtPwr"] / sma_line1["ExtVoltage"])
443 | if sma_line2["ExtVoltage"] != 0:
444 | self._dbusservice["/Ac/ActiveIn/L2/I"] = int(sma_line2["ExtPwr"] / sma_line2["ExtVoltage"])
445 | self._dbusservice["/Ac/ActiveIn/P"] = sma_line1["ExtPwr"] + sma_line2["ExtPwr"]
446 | self._dbusservice["/Dc/0/Voltage"] = sma_battery["Voltage"]
447 | self._dbusservice["/Dc/0/Current"] = sma_battery["Current"] *-1
448 | self._dbusservice["/Dc/0/Power"] = sma_battery["Current"] * sma_battery["Voltage"] *-1
449 |
450 | line1_inv_outpwr = sma_line1["ExtPwr"] + sma_line1["InvPwr"]
451 | line2_inv_outpwr = sma_line2["ExtPwr"] + sma_line2["InvPwr"]
452 |
453 |
454 | #print ("After calc Power L1: " + str(line1_inv_outpwr) + " Power L2: " + str(line2_inv_outpwr))
455 |
456 | #we can gain back a little bit of resolution by compairing total reported load to sum of line loads reported to remove one source of rounding error.
457 | if (sma_system["Load"] == (line1_inv_outpwr + line2_inv_outpwr + 100)):
458 | line1_inv_outpwr+=50
459 | line2_inv_outpwr+=50
460 | elif (sma_system["Load"] == (line1_inv_outpwr + line2_inv_outpwr - 100)):
461 | line1_inv_outpwr-=50
462 | line2_inv_outpwr-=50
463 |
464 | self._dbusservice["/Ac/Out/L1/P"] = line1_inv_outpwr
465 | self._dbusservice["/Ac/Out/L2/P"] = line2_inv_outpwr
466 | self._dbusservice["/Ac/Out/P"] = sma_system["Load"]
467 | self._dbusservice["/Ac/Out/L1/F"] = sma_line1["OutputFreq"]
468 | self._dbusservice["/Ac/Out/L2/F"] = sma_line1["OutputFreq"]
469 | self._dbusservice["/Ac/Out/L1/V"] = sma_line1["OutputVoltage"]
470 | self._dbusservice["/Ac/Out/L2/V"] = sma_line2["OutputVoltage"]
471 |
472 | inverter_on = 0
473 | if sma_line1["OutputVoltage"] > 5:
474 | self._dbusservice["/Ac/Out/L1/I"] = int(line1_inv_outpwr / sma_line1["OutputVoltage"])
475 | inverter_on += 1
476 | if sma_line2["OutputVoltage"] > 5:
477 | self._dbusservice["/Ac/Out/L2/I"] = int(line2_inv_outpwr / sma_line2["OutputVoltage"])
478 | inverter_on += 1
479 |
480 | if sma_system["ExtRelay"]:
481 | self._dbusservice["/Ac/ActiveIn/Connected"] = 1
482 | self._dbusservice["/Ac/ActiveIn/ActiveInput"] = 0
483 | else:
484 | self._dbusservice["/Ac/ActiveIn/Connected"] = 0
485 | self._dbusservice["/Ac/ActiveIn/ActiveInput"] = 240
486 |
487 | # state = 3:Bulk, 4:Absorb, 5:Float, 6:Storage, 7:Equalize, 8:Passthrough 9:Inverting
488 | # push charging state to dbus
489 | vebusChargeState = 0
490 | sma_system["State"] = 0
491 |
492 | #logger.info("SysState: {0}, InvOn: {1}".format(systemState, inverter_on))
493 |
494 | if (inverter_on > 0):
495 | sma_system["State"] = 9
496 | # if current is going into the battery
497 | if (self._bms_data.battery_current > 0):
498 | if (self._bms_data.charging_state == "bulk_chg"):
499 | vebusChargeState = 1
500 | sma_system["State"] = 3
501 | elif (self._bms_data.charging_state == "absorb_chg"):
502 | vebusChargeState = 2
503 | sma_system["State"] = 4
504 | elif (self._bms_data.charging_state == "float_chg"):
505 | vebusChargeState = 3
506 | sma_system["State"] = 5
507 |
508 | self._dbusservice["/VebusChargeState"] = vebusChargeState
509 | self._dbusservice["/State"] = sma_system["State"]
510 |
511 | #----
512 | def _energy_handler(self):
513 | energy_sec = timer() - self._dbusservice["/Energy/Time"]
514 | self._dbusservice["/Energy/Time"] = timer()
515 |
516 | if self._dbusservice["/Dc/0/Power"] > 0:
517 | #Grid to battery
518 | self._dbusservice["/Energy/GridToAcOut"] = self._dbusservice["/Energy/GridToAcOut"] + \
519 | ((self._dbusservice["/Ac/Out/P"]) * energy_sec * 0.00000028)
520 |
521 | self._dbusservice["/Energy/GridToDc"] = self._dbusservice["/Energy/GridToDc"] + \
522 | (self._dbusservice["/Dc/0/Power"] * energy_sec * 0.00000028)
523 | else:
524 | #battery to out
525 | self._dbusservice["/Energy/DcToAcOut"] = self._dbusservice["/Energy/DcToAcOut"] + \
526 | ((self._dbusservice["/Ac/Out/P"]) * energy_sec * 0.00000028)
527 |
528 | #print(timer() - self._dbusservice["/Energy/Time"], ":", self._dbusservice["/Ac/Out/P"])
529 |
530 | self._dbusservice["/Energy/AcIn1ToAcOut"] = self._dbusservice["/Energy/GridToAcOut"]
531 | self._dbusservice["/Energy/AcIn1ToInverter"] = self._dbusservice["/Energy/GridToDc"]
532 | self._dbusservice["/Energy/InverterToAcOut"] = self._dbusservice["/Energy/DcToAcOut"]
533 | self._dbusservice["/Energy/Time"] = timer()
534 | return True
535 |
536 | #----
537 | # BMS charge logic since SMA is in dumb mode
538 | def _execute_grid_solar_charge_logic(self):
539 | charge_amps = None
540 |
541 | # time in UTC
542 | now = datetime.now()
543 |
544 | # SMA Sunny Island Feature:
545 | # Setting 232# Grid Control
546 | # Item 41 GdSocEna - Activate the grid request based on SOC (Default: Disable) = Enable
547 | #
548 | # By enabling this setting the SMA will activate grid to charge batteries. To set the ranges:
549 | # Setting 233# Grid Start
550 | #
551 | # Item 01 GdSocTm1Str - SOC limit for switching on utility grid for time 1 = 40%
552 | # Item 02 GdSocTm1Stp - SOC limit for switching off the utility grid for time 1 = 80%
553 | # Item 03 GdSocTm2Str - SOC limit for switching on utility grid for time 2 = 40%
554 | # Item 04 GdSocTm2Stp - SOC limit for switching off the utility grid for time 2 = 80%
555 |
556 | if (sma_system["ExtRelay"] == 1):
557 | #no point in running the math below to calculate a new target charge current unless we have an update from the inverters
558 | #which is slow. Like every 12 seconds.
559 | #global SMAupdate
560 | #if SMAupdate == True:
561 | # SMAupdate = False
562 |
563 | _cfg_grid = self._cfg["GridLogic"]
564 | _cfg_safety = self._cfg["SafetyLogic"]
565 | #requested charge current varies by time of day and SoC value
566 | #for now, some rules to change charge behavior hard coded for my application.
567 | #Gonna try making these charge current targets inlcuding solar, so we need to subtract solar current later.
568 | if now.hour >= _cfg_grid["start_hour"] and now.hour <= _cfg_grid["end_hour"]:
569 | if now.hour >= _cfg_grid["mid_hour"] and self._bms_data.state_of_charge < 49.0:
570 | charge_amps = _cfg_grid["mid_hour_current"]
571 | else:
572 | charge_amps = _cfg_grid["current"]
573 | else:
574 | charge_amps = _cfg_grid["offtime_current"]
575 |
576 | #TODO: can this use the same value as default bulk current?
577 | if self._bms_data.state_of_charge < _cfg_safety["after_blackout_min_soc"]: #recovering from blackout? Charge fast!
578 | charge_amps = _cfg_safety["after_blackout_charge_amps"]
579 |
580 | #subtract any active Solar current from the requested charge current
581 | charge_amps = charge_amps - self._bms_data.pv_current
582 |
583 | # if pv_current is greater than requested charge amps, don't go negative
584 | if (charge_amps < 0.0):
585 | charge_amps = 0.0
586 |
587 | logger.info("Grid Logic: Time: {0}, On Grid: {1} Charge amps: {2}" \
588 | .format(now, sma_system["ExtRelay"], charge_amps))
589 |
590 | return charge_amps
591 |
592 | #----
593 | # Called on a two second timer to send CAN messages
594 | def _can_bus_txmit_handler(self):
595 |
596 | # log data received from SMA on CAN bus (doing it here since this timer is slower!)
597 | out_load_msg = "SMA: System Load: {0}, Driver runtime: {1}".format(sma_system["Load"], datetime.now() - self.driver_start_time)
598 |
599 | out_ext_msg = "SMA: External, Line 1: {0}V, Line 2: {1}V, Line 1 Pwr: {2}W, Line 2 Pwr: {3}W, Freq: {4}" \
600 | .format(sma_line1["ExtVoltage"], sma_line2["ExtVoltage"], sma_line1["ExtPwr"], sma_line2["ExtPwr"], sma_line1["ExtFreq"])
601 |
602 | out_inv_msg = "SMA: Inverter, Line 1: {0}V, Line 2: {1}V, Line 1 Pwr: {2}W, Line 2 Pwr: {3}W, Freq: {4}" \
603 | .format(sma_line1["OutputVoltage"], sma_line2["OutputVoltage"], sma_line1["InvPwr"], sma_line2["InvPwr"], sma_line1["OutputFreq"])
604 |
605 | out_batt_msg = "SMA: Batt Voltage: {0}, Batt Current: {1}" \
606 | .format(sma_battery["Voltage"], sma_battery["Current"])
607 |
608 | logger.info(out_load_msg)
609 | logger.info(out_ext_msg)
610 | logger.info(out_inv_msg)
611 | logger.info(out_batt_msg)
612 |
613 | #get some data from the Victron BUS, invalid data returns NoneType
614 | soc = self._dbusmonitor.get_value('com.victronenergy.system', '/Dc/Battery/Soc')
615 | volt = self._dbusmonitor.get_value('com.victronenergy.system', '/Dc/Battery/Voltage')
616 | current = self._dbusmonitor.get_value('com.victronenergy.system', '/Dc/Battery/Current')
617 | pv_current = self._dbusmonitor.get_value('com.victronenergy.system', '/Dc/Pv/Current')
618 | if (pv_current == None):
619 | pv_current = 0.0
620 |
621 | # if we don't have these values, there is nothing to do!
622 | if (soc == None or volt == None):
623 | logger.error("DBusMonitor returning None for one or more: SOC: {0}, Volt: {1}, Current: {2}, PVCurrent: {3}" \
624 | .format(soc, volt, current, pv_current))
625 | return True
626 |
627 | # update bms state data
628 | self._bms_data.state_of_charge = soc
629 | self._bms_data.actual_battery_voltage = volt
630 | self._bms_data.battery_current = current
631 | self._bms_data.pv_current = pv_current
632 |
633 | # update the requested bulk current based on the grid solar charge logic
634 | self.bms_controller.update_req_bulk_current(self._execute_grid_solar_charge_logic())
635 |
636 | # update the battery voltage for the BMS to determine next state or charge current level
637 | # Note: Positive value for current means it is going INTO the battery. SMA will report as negative
638 | # so we change signs here
639 | is_state_changed = self.bms_controller.update_battery_data(self._bms_data.actual_battery_voltage, \
640 | -(sma_battery["Current"]))
641 |
642 | self._bms_data.charging_state = self.bms_controller.get_state()
643 | charge_current = self.bms_controller.get_charge_current()
644 |
645 | logger.info("BMS Send, SoC: {0:.1f}%, Batt Voltage: {1:.2f}V, Batt Current: {2:.2f}A, Charge State: {3}, Req Charge: {4}A, Req Discharge: {5}A, PV Cur: {6} ". \
646 | format(self._bms_data.state_of_charge, self._bms_data.actual_battery_voltage, \
647 | self._bms_data.battery_current, self._bms_data.charging_state, charge_current,
648 | self._bms_data.req_discharge_amps, self._bms_data.pv_current))
649 |
650 | #**************Low battery safety****************#
651 |
652 | _cfg_safety = self._cfg["SafetyLogic"]
653 |
654 | #if grid is up but battery low voltage, issue with shunt calibration or SMA setting, pre-empt SoC with minimum value to force grid transfer
655 | if (sma_system["ExtOk"] == 0 and self._bms_data.actual_battery_voltage < self._bms_data.low_battery_voltage):
656 | self._bms_data.state_of_charge = 1.0
657 |
658 | #if no grid and Soc is low, we are in blackout with dead batteries and need to shut off inverters
659 | if(self._safety_off == False):
660 | #normal running, check for grid not ok AND low Soc, send off message till inverters respond
661 | if(sma_system["ExtOk"] == 2 and soc < _cfg_safety["min_soc_inv_off"]):
662 | self._can_bus.send(SMA_OFF_MSG)
663 | if(sma_system["State"] == 0):
664 | self._safety_off = True
665 | #print("Shut off due to low SoC")
666 | else:
667 | #if we saftey shutdown, keep checking for grid restore OR SoC increase, send on message till inverters respond
668 | if(sma_system["ExtOk"] == 0 or soc >= _cfg_safety["min_soc_inv_off"]):
669 | self._can_bus.send(SMA_ON_MSG)
670 | if(sma_system["State"] != 0):
671 | self._safety_off = False
672 | #print("Start SMA due to grid restore or SoC increase")
673 |
674 | #breakup some of the values for CAN packing
675 | SoC_HD = int(self._bms_data.state_of_charge*100)
676 | SoC_HD_H, SoC_HD_L = bytes(SoC_HD)
677 |
678 | Req_Charge_H, Req_Charge_L = bytes(int(charge_current*10))
679 |
680 | Req_Discharge_H, Req_Discharge_L = bytes(int(self._bms_data.req_discharge_amps*10))
681 | Max_V_H, Max_V_L = bytes(int(self._bms_data.max_battery_voltage*10))
682 | Min_V_H, Min_V_L = bytes(int(self._bms_data.min_battery_voltage*10))
683 |
684 |
685 | msg = can.Message(arbitration_id = CAN_tx_msg["BatChg"],
686 | data=[Max_V_L, Max_V_H, Req_Charge_L, Req_Charge_H, Req_Discharge_L, Req_Discharge_H, Min_V_L, Min_V_H],
687 | is_extended_id=False)
688 |
689 | msg2 = can.Message(arbitration_id = CAN_tx_msg["BatSoC"],
690 | data=[int(self._bms_data.state_of_charge), 0x00, 0x64, 0x0, SoC_HD_L, SoC_HD_H],
691 | is_extended_id=False)
692 |
693 | msg3 = can.Message(arbitration_id = CAN_tx_msg["BatVoltageCurrent"],
694 | data=[0x00, 0x00, 0x00, 0x0, 0xf0, 0x00],
695 | is_extended_id=False)
696 |
697 | msg4 = can.Message(arbitration_id = CAN_tx_msg["AlarmWarning"],
698 | data=[0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00],
699 | is_extended_id=False)
700 |
701 | msg5 = can.Message(arbitration_id = CAN_tx_msg["BMSOem"],
702 | data=[0x42, 0x41, 0x54, 0x52, 0x49, 0x55, 0x4d, 0x20],
703 | is_extended_id=False)
704 |
705 | msg6 = can.Message(arbitration_id = CAN_tx_msg["BatData"],
706 | data=[0x03, 0x04, 0x0a, 0x04, 0x76, 0x02, 0x00, 0x00],
707 | is_extended_id=False)
708 |
709 | #logger.debug(self._can_bus)
710 |
711 | try :
712 | self._can_bus.send(msg)
713 | #logger.debug("Message sent on {}".format(self._can_bus.channel_info))
714 | time.sleep(.100)
715 |
716 | self._can_bus.send(msg2)
717 | #logger.debug("Message sent on {}".format(self._can_bus.channel_info))
718 | time.sleep(.100)
719 |
720 | self._can_bus.send(msg3)
721 | #logger.debug("Message sent on {}".format(self._can_bus.channel_info))
722 |
723 | time.sleep(.100)
724 |
725 | self._can_bus.send(msg4)
726 | #logger.debug("Message sent on {}".format(self._can_bus.channel_info))
727 |
728 | time.sleep(.100)
729 |
730 | self._can_bus.send(msg5)
731 | #logger.debug("Message sent on {}".format(self._can_bus.channel_info))
732 |
733 | time.sleep(.100)
734 |
735 | self._can_bus.send(msg6)
736 | #logger.debug("Message sent on {}".format(self._can_bus.channel_info))
737 |
738 | #logger.info("Sent to SI: {0}, {1}, {2}, {3}, {4}". \
739 | # format(self._bms_data.req_discharge_amps, self._bms_data.state_of_charge, \
740 | # self._bms_data.actual_battery_voltage, self._bms_data.battery_current, \
741 | # self._bms_data.pv_current))
742 |
743 | #logger.info("Sent 6 messages on {}".format(self._can_bus.channel_info))
744 | except (can.CanError) as e:
745 | logger.error("CAN BUS Transmit error (is controller missing?): %s" % e.message)
746 | except KeyboardInterrupt:
747 | pass
748 |
749 | return True # keep timer running
750 |
751 | #----
752 | def get_config_data(self):
753 | try :
754 | dir_path = os.path.dirname(os.path.realpath(__file__))
755 | with open(dir_path + "/dbus-sma.yaml", "r") as yamlfile:
756 | config = yaml.load(yamlfile, Loader=yaml.FullLoader)
757 | return config
758 | except :
759 | logger.info("dbus-sma.yaml file not found or correct.")
760 | sys.exit()
761 |
762 | if __name__ == "__main__":
763 | # Argument parsing
764 | parser = argparse.ArgumentParser(description='Converts readings from AC-Sensors connected to a VE.Bus device in a pvinverter ' + 'D-Bus service.')
765 | parser.add_argument('-s', '--serial', help='tty')
766 | parser.add_argument("-d", "--debug", help="set logging level to debug",action="store_true")
767 |
768 | args = parser.parse_args()
769 |
770 | print("-------- dbus_SMADriver, v" + softwareVersion + " is starting up --------")
771 | #logger = setup_logging(args.debug)
772 |
773 | # create SMA Driver
774 | smadriver = SmaDriver()
775 |
776 | # run driver (starts mainloop and hangs until CTRL+C/SIGINT received)
777 | smadriver.run()
778 |
779 | # force clean up resources
780 | smadriver.__del__()
781 |
782 | print("-------- dbus_SMADriver, v" + softwareVersion + " is shuting down --------")
783 |
784 | sys.exit(1)
785 |
786 |
--------------------------------------------------------------------------------
/dbus-sma/dbus-sma.yaml:
--------------------------------------------------------------------------------
1 | BMSData:
2 | max_battery_voltage: 60.0
3 | min_battery_voltage: 46.0
4 | hi_battery_voltage: 49.6
5 | low_battery_voltage: 49.6
6 | charge_bulk_amps: 164.0
7 | max_discharge_amps: 200.0
8 | charge_absorb_voltage: 56.2 #58.4
9 | charge_float_voltage: 54.4
10 | time_min_absorb: 120
11 | rebulk_voltage: 54.0
12 |
13 | GridLogic:
14 | start_hour: 14
15 | end_hour: 22
16 | current: 100.0
17 |
18 | mid_hour: 17
19 | mid_hour_current: 175.0
20 |
21 | offtime_current: 4.0
22 |
23 | SafetyLogic:
24 | after_blackout_charge_amps: 250.0
25 | after_blackout_min_soc: 15
26 | min_soc_inv_off: 5
27 |
28 |
--------------------------------------------------------------------------------
/dbus-sma/service/log/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | exec 2>&1
3 | exec multilog t s25000 n4 /var/log/dbus-sma
4 |
--------------------------------------------------------------------------------
/dbus-sma/service/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | exec 2>&1
3 | exec softlimit -d 100000000 -s 1000000 -a 100000000 /data/etc/dbus-sma/dbus-sma.py
4 |
--------------------------------------------------------------------------------
/install/99-candlelight.rules:
--------------------------------------------------------------------------------
1 | # IMPORTANT:
2 | # Replace the serial with all zeros with the one for your adapter.
3 | # Get the adapter SerialNumber by running: usb-devices | grep -A2 canable.io
4 |
5 | SUBSYSTEM=="net", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="606f", ATTRS{serial}=="000000000000000000000000", NAME="can5", RUN+="/sbin/ip link set '%k' up type can bitrate 500000"
6 |
--------------------------------------------------------------------------------
/install/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # for testing purposes
4 | #ROOT_DIR="/tmp/venus"
5 | #mkdir -p ${ROOT_DIR}/data/etc
6 | #mkdir -p ${ROOT_DIR}/var/log
7 | #mkdir -p ${ROOT_DIR}/service
8 | #mkdir -p ${ROOT_DIR}/etc/udev/rules.d
9 | ROOT_DIR=""
10 |
11 | # download this script:
12 | # wget https://github.com/madsci1016/SMAVenusDriver/raw/master/install/install.sh
13 |
14 | echo
15 | echo "Please ensure your socketcan enable canable USB adapter is plugged into the Venus"
16 | echo "If your canable.io adapter is using factory default slcan firmware, exit this"
17 | echo "script and install the \"candlelight\" firmware"
18 | echo
19 | echo "This script requires internet access to install dependencies and software."
20 | echo
21 | echo "Install SMA Sunny Island driver (w/ virtual BMS) on Venus OS at your own risk?"
22 | read -p "[Y to proceed] " -n 1 -r
23 |
24 | echo # (optional) move to a new line
25 | if [[ $REPLY =~ ^[Yy]$ ]]
26 | then
27 | echo "Install dependencies (pip and python libs)?"
28 | read -p "[Y to proceed] " -n 1 -r
29 | echo # (optional) move to a new line
30 | if [[ $REPLY =~ ^[Yy]$ ]]
31 | then
32 |
33 | echo "==== Download and install dependencies ===="
34 | opkg update
35 | opkg install python-misc python-distutils python-numbers python-html python-ctypes python-pkgutil
36 | opkg install python-unittest python-difflib python-compile gcc binutils python-dev python-unixadmin python-xmlrpc
37 |
38 | wget https://bootstrap.pypa.io/2.7/get-pip.py
39 | python get-pip.py
40 | rm get-pip.py
41 |
42 | pip install python-can
43 | pip install python-statemachine
44 | pip install pyyaml
45 | fi
46 |
47 | echo "==== Download driver and library ===="
48 |
49 | wget https://github.com/madsci1016/SMAVenusDriver/archive/master.zip
50 | unzip -qo master.zip
51 | rm master.zip
52 |
53 | echo "==== Add canable device to udev ===="
54 |
55 | error_msg="WARNING: Was unable to modify 99-candlelight.rules with device serial number automatically"
56 |
57 | template_udev_file="SMAVenusDriver-master/install/99-candlelight.rules"
58 | udev_file="${ROOT_DIR}/etc/udev/rules.d/99-candlelight.rules"
59 | cp $template_udev_file $udev_file
60 |
61 | # grab the details of the canable usb adapter
62 | value=`usb-devices | grep -A2 canable.io`
63 |
64 | # get the serial number of the canable usb adapter
65 | var="$(cut -d'=' -f 2 <<< $value)"
66 | set -- $var
67 | serial=$4
68 |
69 | # the serial number is 24 digits long, if the parse doesn't meet that
70 | # requirement toss the results and bail on task.
71 | if [ ${#serial} -eq 24 ]; then
72 |
73 | echo "Found canable.io serial number: $serial"
74 | # replace the template serial number with the actual device serial number
75 | sed -i "s/000000000000000000000000/$serial/g" "$udev_file"
76 |
77 | diff $template_udev_file $udev_file > /dev/null 2>&1
78 | error=$?
79 |
80 | if [ $error -eq 0 ]; then
81 | echo $error_msg
82 | fi
83 | else
84 | echo $error_msg
85 | fi
86 |
87 | echo "==== Install SMA SI driver ===="
88 | DBUS_NAME="dbus-sma"
89 | DBUS_SMA_DIR="${ROOT_DIR}/data/etc/${DBUS_NAME}"
90 |
91 | mkdir -p ${ROOT_DIR}/var/log/${DBUS_NAME}
92 | mkdir -p ${DBUS_SMA_DIR}
93 | cp -R SMAVenusDriver-master/dbus-sma/* ${ROOT_DIR}/data/etc/${DBUS_NAME}
94 |
95 | # replace inverter svg with custom yellow sunny island svg
96 | cp SMAVenusDriver-master/assets/overview-inverter.svg ${ROOT_DIR}/opt/victronenergy/themes/ccgx/images
97 |
98 | chmod +x ${ROOT_DIR}/data/etc/${DBUS_NAME}/dbus-sma.py
99 | chmod +x ${ROOT_DIR}/data/etc/${DBUS_NAME}/service/run
100 | chmod +x ${ROOT_DIR}/data/etc/${DBUS_NAME}//service/log/run
101 | ln -s ${ROOT_DIR}/opt/victronenergy/vrmlogger/ext/ ${DBUS_SMA_DIR}/ext
102 | ln -s ${DBUS_SMA_DIR}/service ${ROOT_DIR}/service/${DBUS_NAME}
103 |
104 | # remove archive files
105 | rm -rf SMAVenusDriver-master/
106 |
107 | echo
108 | echo "To finish, reboot the Venus OS device"
109 | fi
110 |
--------------------------------------------------------------------------------
/test/sma_keep_alive.py:
--------------------------------------------------------------------------------
1 | from signal import signal, SIGINT
2 | from sys import exit
3 | import can
4 | import serial
5 | import time
6 |
7 | #
8 | # when board configured as socketcan, bring up link first:
9 | # sudo ip link set can0 up type can bitrate 500000
10 | #
11 | #
12 |
13 | def getSignedNumber(number, bitLength):
14 | mask = (2 ** bitLength) - 1
15 | if number & (1 << (bitLength - 1)):
16 | return number | ~mask
17 | else:
18 | return number & mask
19 |
20 | def getbytes(integer):
21 | return divmod(integer, 0x100)
22 |
23 | def handler(signal_received, frame):
24 | # Handle any cleanup here
25 | print('SIGINT or CTRL-C detected. Exiting gracefully')
26 | exit(0)
27 |
28 | if __name__ == "__main__":
29 | # Tell Python to run the handler() function when SIGINT is recieved
30 | signal(SIGINT, handler)
31 |
32 | print('Running. Press CTRL-C to exit.')
33 |
34 | print ("Can bus init")
35 | canBusTTY = "/dev/ttyACM0"
36 | canBus = can.interface.Bus(bustype='socketcan', channel="can5", bitrate=500000)
37 |
38 | print ("Can bus init done")
39 |
40 | while True:
41 | # Do nothing and hog CPU forever until SIGINT received.
42 |
43 | SoC_HD = 99.5
44 | Soc = int(SoC_HD)
45 | Req_Charge_HD = 100
46 | Req_Charge_A = 100.0
47 | Req_Discharge_HD = 30
48 | Req_Discharge_A = 200.0
49 | Max_V_HD = 56
50 | Max_V = 60.0
51 | Min_V = 46.0
52 |
53 |
54 | #breakup some of the values for CAN packing
55 | SoC_HD = int(SoC_HD*100)
56 | SoC_HD_H, SoC_HD_L = getbytes(SoC_HD)
57 | Req_Charge_HD = int(Req_Charge_A*10)
58 | Req_Charge_H, Req_Charge_L = getbytes(Req_Charge_HD)
59 | Req_Discharge_HD = int(Req_Discharge_A*10)
60 | Req_Discharge_H, Req_Discharge_L = getbytes(Req_Discharge_HD)
61 | Max_V_HD = int(Max_V*10)
62 | Max_V_H, Max_V_L = getbytes(Max_V_HD)
63 | Min_V_HD = int(Min_V*10)
64 | Min_V_H, Min_V_L = getbytes(Min_V_HD)
65 |
66 |
67 | msg = can.Message(arbitration_id=0x351,
68 | data=[Max_V_L, Max_V_H, Req_Charge_L, Req_Charge_H, Req_Discharge_L, Req_Discharge_H, Min_V_L, Min_V_H],
69 | is_extended_id=False)
70 | msg2 = can.Message(arbitration_id=0x355,
71 | data=[Soc, 0x00, 0x64, 0x0, SoC_HD_L, SoC_HD_H],
72 | is_extended_id=False)
73 | msg3 = can.Message(arbitration_id=0x356,
74 | data=[0x00, 0x00, 0x00, 0x0, 0xf0, 0x00],
75 | is_extended_id=False)
76 | msg4 = can.Message(arbitration_id=0x35a,
77 | data=[0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00],
78 | is_extended_id=False)
79 | msg5 = can.Message(arbitration_id=0x35e,
80 | data=[0x42, 0x41, 0x54, 0x52, 0x49, 0x55, 0x4d, 0x20],
81 | is_extended_id=False)
82 | msg6 = can.Message(arbitration_id=0x35f,
83 | data=[0x03, 0x04, 0x0a, 0x04, 0x76, 0x02, 0x00, 0x00],
84 | is_extended_id=False)
85 |
86 | try:
87 | canBus.send(msg)
88 | #print("Message 0x351 sent on {}".format(canBus.channel_info))
89 | time.sleep(.100)
90 |
91 | canBus.send(msg2)
92 | #print("Message 0x355 sent on {}".format(canBus.channel_info))
93 | time.sleep(.100)
94 |
95 | canBus.send(msg3)
96 | #print("Message 0x356 sent on {}".format(canBus.channel_info))
97 | time.sleep(.100)
98 |
99 | canBus.send(msg4)
100 | #print("Message 0x35a sent on {}".format(canBus.channel_info))
101 | time.sleep(.100)
102 |
103 | canBus.send(msg5)
104 | #print("Message 0x35e sent on {}".format(canBus.channel_info))
105 | time.sleep(.100)
106 |
107 | canBus.send(msg6)
108 | #print("Message 0x35f sent on {}".format(canBus.channel_info))
109 |
110 | print("Sent 6 frames on {}".format(canBus.channel_info))
111 |
112 | except (can.CanError) as e:
113 | print("CAN BUS Transmit error (is controller missing?): %s" % e.message)
114 |
115 | time.sleep(2)
116 | pass
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------