diff --git a/README.md b/README.md index 151fbf0..e33eee4 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,15 @@ rm main.zip ### Change config.ini Within the project there is a file `/data/dbus-solax-x1-pvinverter/config.ini` - just change the values +If you add MODBUS to your config the SOLAXCLOUD settings will be ignored. + +To use with only one phase (X1 devices) just remove Phase2 and Phase3 from the INVERTER.PHASES section. + | Section | Config vlaue | Explanation | | ------------- | ------------- | ------------- | | DEFAULT | SignOfLifeLog | Time in minutes how often a status is added to the log-file `current.log` with log-level INFO | +| MODBUS | port | The port of the modbus adapter to use ie. /dev/ttyUSB0 | +| MODBUS | unit | The modbus unit id, if multiple units share the same modbus, default = 1 | | SOLAXCLOUD | Endpoint | API endpoint - should always be the same | | SOLAXCLOUD | TokenId | TokenId from Solax Cloud portal | | SOLAXCLOUD | RegNo | RegNo of inverter WifiStick | @@ -80,7 +86,9 @@ Within the project there is a file `/data/dbus-solax-x1-pvinverter/config.ini` - | INVERTER | MaxPower | Inverter max AC power in watts | | INVERTER | GridVoltage | The voltage is not returned by RESTapi so we use this value to calculate the current based on power | | INVERTER | Phase | Phase your inverter is connected to | - +| INVERTER.PHASES | Phase1 | Name of Phase 1, ie. L1 | +| INVERTER.PHASES | Phase2 | Name of Phase 2, ie. L2 | +| INVERTER.PHASES | Phase3 | Name of Phase 3, ie. L3 | ## Used documentation - https://github.com/victronenergy/venus/wiki/dbus#grid DBus paths for Victron namespace diff --git a/config.ini b/config.ini index 0ea956d..cb0f79e 100644 --- a/config.ini +++ b/config.ini @@ -1,6 +1,10 @@ [DEFAULT] SignOfLifeLog=5 +[MODBUS] +#unit=1 +port=/dev/ttyUSB0 + [SOLAXCLOUD] Endpoint=https://www.eu.solaxcloud.com:9443/proxy/api/getRealtimeInfo.do TokenId=12345678901234567890 @@ -10,4 +14,9 @@ RegNo=COOLREGNO Position=0 MaxPower=5000 GridVoltage=230 -Phase=L1 \ No newline at end of file +Phase=L1 + +[INVERTER.PHASES] +Phase1=L1 +Phase2=L2 +Phase3=L3 \ No newline at end of file diff --git a/dbus-solax-x1-pvinverter.py b/dbus-solax-x1-pvinverter.py index 3438c67..6fe8a77 100644 --- a/dbus-solax-x1-pvinverter.py +++ b/dbus-solax-x1-pvinverter.py @@ -6,7 +6,7 @@ import sys import os import sys -if sys.version_info.major == 2: +if sys.version_info.major > 2: import gobject else: from gi.repository import GLib as gobject @@ -19,6 +19,7 @@ sys.path.insert(1, os.path.join(os.path.dirname(__file__), '/opt/victronenergy/dbus-systemcalc-py/ext/velib_python')) from vedbus import VeDbusService +import solaxx3rs485 class DbusSolaxX1Service: def __init__(self, servicename, deviceinstance, paths, productname='Solax X1', connection='192.168.2.111- 126 (sunspec)'): @@ -48,16 +49,38 @@ def __init__(self, servicename, deviceinstance, paths, productname='Solax X1', c self._dbusservice.add_path('/UpdateIndex', 0) # add path values to dbus - for path, settings in self._paths.items(): - self._dbusservice.add_path( - self._replacePhaseVar(path), settings['initial'], gettextcallback=settings['textformat'], writeable=True, onchangecallback=self._handlechangedvalue) + if (config['INVERTER.PHASES']): + for key in config['INVERTER.PHASES']: + phase = config['INVERTER.PHASES'][key] + for path, settings in self._paths.items(): + self._dbusservice.add_path( + self._replacePhaseVar(path, phase), settings['initial'], gettextcallback=settings['textformat'], writeable=True, onchangecallback=self._handlechangedvalue) + else: + for path, settings in self._paths.items(): + self._dbusservice.add_path( + self._replacePhaseVar(path), settings['initial'], gettextcallback=settings['textformat'], writeable=True, onchangecallback=self._handlechangedvalue) + + # modus + self._source = "cloud" + if (config['MODBUS']): + self._source = "modbus" + self._modbus = solaxx3rs485.SolaxX3RS485Client(config['MODBUS']['port']) # last update self._lastUpdate = 0 self._lastCloudUpdate = 0 - self._lastCloudCheck = 0 + self._lastCloudCheck = 0 self._lastCloudACPower = 0 self._lastCloudInverterStatus = 0 + self._lastPhase1Power = 0 + self._lastPhase2Power = 0 + self._lastPhase3Power = 0 + self._lastPhase1Voltage = 0 + self._lastPhase2Voltage = 0 + self._lastPhase3Voltage = 0 + self._lastPhase1Current = 0 + self._lastPhase2Current = 0 + self._lastPhase3Current = 0 # add _update function 'timer' gobject.timeout_add(500, self._update) # call update routine @@ -105,9 +128,9 @@ def _getPhaseFromConfig(self): return result - def _replacePhaseVar(self, input): + def _replacePhaseVar(self, input, phase=self._getPhaseFromConfig()): result = input - result = result.replace("[*Phase*]", self._getPhaseFromConfig()) + result = result.replace("[*Phase*]", phase) return result @@ -151,9 +174,60 @@ def _getSolaxCloudData(self): raise ValueError("Response (%s) is not ok - 'success'=%s 'exception'=%s" % ( URL, data['success'], data['exception'])) return data + + def _getInverterStatusRunMode(self, solaxRunMode: int): + # * Status as returned by the fronius inverter + # * - 0-6: Startup + # * - 7: Running + # * - 8: Standby + # * - 9: Boot loading + # * - 10: Error + status = 10 + + # Run Mode Codes from solax docs + if solaxRunMode == 0: + # Waiting + status = 8 + elif solaxRunMode == 1: + # Checking + status = 0 + elif solaxRunMode == 2: + # Normal + status = 7 + elif solaxRunMode == 3: + # Fault + status = 10 + elif solaxRunMode == 4: + # Permanent Fault + status = 10 + elif solaxRunMode == 5: + # Update + status = 10 + elif solaxRunMode == 6: + # Off-grid waiting + status = 8 + elif solaxRunMode == 7: + # Off-grid + status = 8 + elif solaxRunMode == 8: + # Self Testing + status = 1 + elif solaxRunMode == 9: + # Idle + status = 8 + elif solaxRunMode == 10: + # Standby + status = 8 + + return status - - def _getInverterStatus(self, solaxInverterStatusCode: int): + def _getInverterStatus(self, solaxInverterStatusCode: int): + # * Status as returned by the fronius inverter + # * - 0-6: Startup + # * - 7: Running + # * - 8: Standby + # * - 9: Boot loading + # * - 10: Error status = 10 if solaxInverterStatusCode in (100,101) : @@ -206,10 +280,10 @@ def _update(self): logging.debug("---"); # some general data - grid_voltage = self._getGridVoltage() + grid_voltage = self._getGridVoltage() - #send data to DBus - if self._lastCloudCheck == 0 or (time.time()-self._lastCloudCheck) >= 15: + #get data from solax cloud + if self._modus == "cloud" and (self._lastCloudCheck == 0 or (time.time()-self._lastCloudCheck) >= 15): #get data from Solax Cloud meter_data = self._getSolaxCloudData() self._lastCloudCheck = time.time() @@ -225,18 +299,75 @@ def _update(self): logging.debug("Cloud Update - Inverter status-code: %s" % (self._getInverterStatus(self._lastCloudInverterStatus))) logging.debug("Cloud Update - AC power: %s" % (self._lastCloudACPower)) logging.debug("Cloud Update - AC energy total: %s" % (self._lastCloudACEnergyTotal)) - + #get data from modbus + if self._modus == "modbus": + #we can query this in every loop, not just every 5minutes :) + meter_data = self._modbus.get_data() + self._lastModbusCheck = time.time() + + self._lastModbusUpdate = time.time() + # power + self.lastPhase1Power = meter_data.output_power_phase_1 + self.lastPhase2Power = meter_data.output_power_phase_2 + self.lastPhase3Power = meter_data.output_power_phase_3 + + # currents + self.lastPhase1Current = meter_data.output_current_phase_1 + self.lastPhase2Current = meter_data.output_current_phase_2 + self.lastPhase3Current = meter_data.output_current_phase_3 + + # voltage data + self.lastPhase1Voltage = meter_data.grid_voltage_phase_1 + self.lastPhase2Voltage = meter_data.grid_voltage_phase_2 + self.lastPhase3Voltage = meter_data.grid_voltage_phase_3 + + # yield data + self.lastTotalYield = meter_data.total_yield + self.lastTodayYield = meter_data.yield_today + + # data of each pv string + self.lastPv1Power = meter_data.pv1_dc_power + self.lastPv1Current = meter_data.pv1_input_current + self.lastPv1Voltage = meter_data.pv1_input_voltage + self.lastPv2Power = meter_data.pv2_dc_power + self.lastPv2Current = meter_data.pv2_input_current + self.lastPv2Voltage = meter_data.pv2_input_voltage + + self.lastStatus = self._getInverterStatusRunMode(meter_data.run_mode) - - # set normal values - self._dbusservice['/Ac/Power'] = self._lastCloudACPower - self._dbusservice['/StatusCode'] = self._getInverterStatus(self._lastCloudInverterStatus) - self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Voltage')] = grid_voltage - self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Current')] = round(self._dbusservice['/Ac/Power'] / self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Voltage')] , 2) - self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Power')] = self._dbusservice['/Ac/Power'] - self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Energy/Forward')] = self._lastCloudACEnergyTotal - self._dbusservice['/Ac/Energy/Forward'] = self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Energy/Forward')] # one phase pv-inverter # + self._dbusservice['/Ac/L2/Energy/Forward'] + self._dbusservice['/Ac/L3/Energy/Forward'] - self._dbusservice['/Ac/Current'] = self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Current')] # just copy value - 1phase inverter + # set status + if self.lastStatus: + self._dbusservice['/StatusCode'] = self.lastStatus + else: + self._dbusservice['/StatusCode'] = self._getInverterStatus(self._lastCloudInverterStatus) + + # set energy values + total_current = 0 + total_energy_forward = 0 + total_power = 0 + if (config['INVERTER.PHASES']): + total_energy_forward = self._lastCloudACEnergyTotal + for key in config['INVERTER.PHASES']: + phase = config['INVERTER.PHASES'][key] + phase_power = self['_last'+key+'Power'] + phase_voltage = self['_last'+key+'Voltage'] + phase_current = self['_last'+key+'Current'] + total_power = total_power + phase_power + total_curren = total_current + phase_current + self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Voltage', phase)] = phase_voltage + self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Current', phase)] = phase_current + self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Power', phase)] = phase_power + else: + total_power = self._lastCloudACPower + total_energy_forward = self._lastCloudACEnergyTotal + self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Voltage')] = grid_voltage + self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Current')] = round(self._lastCloudACPower / grid_voltage, 2) + self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Power')] = self._lastCloudACPower + self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Energy/Forward')] = self._lastCloudACEnergyTotal + total_current = self._dbusservice[self._replacePhaseVar('/Ac/[*Phase*]/Current')] + self._dbusservice['/Ac/Power'] = total_power + self._dbusservice['/Ac/Energy/Forward'] = total_energy_forward + self._dbusservice['/Ac/Current'] = total_current self._dbusservice['/Ac/Voltage'] = grid_voltage