├── 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 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 46 | 48 | 50 | 53 | 54 | 56 | 59 | 60 | 62 | 65 | 66 | 68 | 71 | 72 | 74 | 77 | 78 | 79 | 88 | 97 | 101 | 108 | 112 | 116 | 123 | 133 | SMA Sunny Island 144 | 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 | --------------------------------------------------------------------------------