├── requirements.txt ├── Dockerfile ├── config ├── config.py └── registers.py ├── examples ├── register_scan.py └── client_example.py ├── README.md └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | umodbus 2 | paho-mqtt 3 | prometheus-client 4 | pysolarmanV5 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | EXPOSE 18000 4 | 5 | LABEL MAINTAINER="Andrius Kozeniauskas" 6 | LABEL NAME=solismon3 7 | 8 | RUN mkdir /solismon3 9 | COPY *.py *.txt /solismon3/ 10 | 11 | WORKDIR /solismon3 12 | 13 | RUN pip install --upgrade pip \ 14 | && pip3 install -r requirements.txt 15 | 16 | CMD [ "python", "./main.py" ] -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | INVERTER_SERIAL = 123456789 # WiFi stick serial number 2 | INVERTER_IP = "192.168.1.55" # IP address of inverter 3 | INVERTER_PORT = 8899 # Port number 4 | MQTT_SERVER = "192.168.1.20" # IP address of MQTT server 5 | MQTT_PORT = 1883 # Port number of MQTT server 6 | MQTT_TOPIC = "solis/METRICS" # MQTT topic to use 7 | MQTT_USER = "foo" # MQTT auth user 8 | MQTT_PASS = "bar" # MQTT auth password 9 | CHECK_INTERVAL = 30 # How often to check(seconds), only applies when 'PROMETHEUS = False' otherwise uses Prometheus scrape interval 10 | MQTT_KEEPALIVE = 60 # MQTT keepalive 11 | PROMETHEUS = False # Enable Prometheus exporter 12 | PROMETHEUS_PORT = 18000 # Port to use for Prometheus exporter 13 | MODIFIED_METRICS = True # Enable modified metrics 14 | DEBUG = False # Enable debugging, helpfull to diagnose problems 15 | -------------------------------------------------------------------------------- /examples/register_scan.py: -------------------------------------------------------------------------------- 1 | """ Scan Modbus registers to find valid registers""" 2 | from pysolarmanv5.pysolarmanv5 import PySolarmanV5, V5FrameError 3 | import umodbus.exceptions 4 | 5 | 6 | def main(): 7 | modbus = PySolarmanV5("192.168.1.24", 123456789, port=8899, mb_slave_id=1, verbose=0) 8 | 9 | print("Scanning input registers") 10 | for x in range(30000, 39999): 11 | try: 12 | val = modbus.read_input_registers(register_addr=x, quantity=1)[0] 13 | print(f"Register: {x:05}\t\tValue: {val:05} ({val:#06x})") 14 | except (V5FrameError, umodbus.exceptions.IllegalDataAddressError): 15 | continue 16 | print("Finished scanning input registers") 17 | 18 | print("Scanning holding registers") 19 | for x in range(40000, 49999): 20 | try: 21 | val = modbus.read_holding_registers(register_addr=x, quantity=1)[0] 22 | print(f"Register: {x:05}\t\tValue: {val:05} ({val:#06x})") 23 | except (V5FrameError, umodbus.exceptions.IllegalDataAddressError): 24 | continue 25 | print("Finished scanning holding registers") 26 | 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /examples/client_example.py: -------------------------------------------------------------------------------- 1 | """ A basic client demonstrating how to use pysolarmanv5.""" 2 | from pysolarmanv5.pysolarmanv5 import PySolarmanV5 3 | 4 | 5 | def main(): 6 | """Create new PySolarman instance, using IP address and S/N of data logger 7 | 8 | Only IP address and S/N of data logger are mandatory parameters. If port, 9 | mb_slave_id, and verbose are omitted, they will default to 8899, 1 and 0 10 | respectively. 11 | """ 12 | modbus = PySolarmanV5( 13 | "192.168.1.24", 123456789, port=8899, mb_slave_id=1, verbose=1 14 | ) 15 | 16 | """Query six input registers, results as a list""" 17 | print(modbus.read_input_registers(register_addr=33022, quantity=6)) 18 | 19 | """Query six holding registers, results as list""" 20 | print(modbus.read_holding_registers(register_addr=43000, quantity=6)) 21 | 22 | """Query single input register, result as an int""" 23 | print(modbus.read_input_register_formatted(register_addr=33035, quantity=1)) 24 | 25 | """Query single input register, apply scaling, result as a float""" 26 | print( 27 | modbus.read_input_register_formatted(register_addr=33035, quantity=1, scale=0.1) 28 | ) 29 | 30 | """Query two input registers, shift first register up by 16 bits, result as a signed int, """ 31 | print( 32 | modbus.read_input_register_formatted(register_addr=33079, quantity=2, signed=1) 33 | ) 34 | 35 | """Query single holding register, apply bitmask and bitshift left (extract bit1 from register)""" 36 | print( 37 | modbus.read_holding_register_formatted( 38 | register_addr=43110, quantity=1, bitmask=0x2, bitshift=1 39 | ) 40 | ) 41 | 42 | 43 | if __name__ == "__main__": 44 | main() 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SolisMon3 2 | 3 | This is 3rd iteration of my Solis Inverter monitor. 4 | This is based on great work by [jmccrohan](https://github.com/jmccrohan/pysolarmanv5) 5 | 6 | The data is pulled directly from Solis WiFi stick. You need to provide serial number and IP address of the stick. 7 | Metrics are published to MQTT and Prometheus, or just MQTT. 8 | The registers to be polled and their meaning are stored in `registers.py` file. I have populated file already with registers that I use. The list is not final but shoudl be good enough for most cases. Note the registers are not the same on all models and firmware versions. They do tend to move with firmware upgrades. You may need to adjusts or add new ones for the inverter that you use. 9 | 10 | ## Configuration 11 | ### config.py 12 | Modify the values in [config.py](./config/config.py) to match your setup 13 | ``` 14 | INVERTER_SERIAL = 123456789 # WiFi stick serial number 15 | INVERTER_IP = "192.168.1.55" # IP address of inverter 16 | INVERTER_PORT = 8899 # Port number 17 | MQTT_SERVER = "192.168.1.20" # IP address of MQTT server 18 | MQTT_PORT = 1883 # Port number of MQTT server 19 | MQTT_TOPIC = "solis/METRICS" # MQTT topic to use 20 | MQTT_USER = "foo" # MQTT auth user (blank to disable auth) 21 | MQTT_PASS = "bar" # MQTT auth password 22 | CHECK_INTERVAL = 30 # How often to check(seconds), only applies when 'PROMETHEUS = False' otherwise uses Prometheus scrape interval 23 | MQTT_KEEPALIVE = 60 # MQTT keepalive 24 | PROMETHEUS = False # Enable Prometheus exporter 25 | PROMETHEUS_PORT = 18000 # Port to use for Prometheus exporter 26 | MODIFIED_METRICS = True # Enable modified metrics 27 | DEBUG = False # Enable debugging, helpfull to diagnose problems 28 | ``` 29 | 30 | MODIFIED_METRICS create additional metrics. It takes multiple existing metrics and creates single metric out of them. 31 | This is useful for me as I do not need to make modifications later in Home-assistant using templates and/or Grafana. 32 | In Home-assistant this allows me easy integration with custom cards like 33 | [tesla-style-solar-power-card](https://github.com/reptilex/tesla-style-solar-power-card) 34 | 35 | ``` 36 | battery_power_modified 37 | battery_power_in_modified 38 | battery_power_out_modified 39 | grid_to_battery_power_in_modified 40 | meter_power_in_modified 41 | meter_power_modified 42 | meter_power_out_modified 43 | total_load_power_modified 44 | solar_to_house_power_modified 45 | ``` 46 | 47 | ### registers.py 48 | I have registers predefined for single phase Solis RHI 4G hybrid inverter in [registers.py](./config/registers.py) file. 49 | The register mappings are not the same on all inverters. Non-hybrid inverters have different mapping, so you will need to adjust. 50 | I will try to add more later on. 51 | The registers are read in blocks as that is much faster than reading individual registers one by one. 52 | In registers.py you need to provide: 53 | Register number you want to start with(integer), register name(string, no space allowed, use '_'), register description(string) 54 | Add '*' in front of register name if you want it to be skipped. 55 | 56 | ## Running 57 | Run main.py 58 | 59 | ### Running in docker 60 | Docker images is provided. 61 | On your docker host create a folder solismon3/config and copy your modified config.py and registers.py files in there 62 | ``` 63 | docker run -it -d --restart unless-stopped --name solismon3 -v /solismon3/config:/solismon3/config -p 18000:18000 nosireland/solismon3 64 | ``` 65 | 66 | ### Docker Compose example 67 | ``` 68 | version: "3.4" 69 | 70 | services: 71 | solismon3: 72 | image: nosireland/solismon3:latest 73 | container_name: solismon3 74 | restart: always 75 | ports: 76 | - 18000:18000 77 | volumes: 78 | - /solismon3/config:/solismon3/config 79 | logging: 80 | options: 81 | max-size: 5m 82 | ``` 83 | 84 | ### Testing 85 | To see if it is running properly I would advise enabling debugging `DEBUG = False` and also `PROMETHEUS = True` 86 | (you do not need to have Prometheus installed) for testing. Once started check container logs `docker logs solismon3` and in 87 | browser enter url http://docker_host_ip:18000. Assuming all is ok, you should see metrics in browser. 88 | 89 | If program fails with default [registers.py](./config/registers.py) you can try scanning registers with 90 | [register_scan](./examples/register_scan.py) and see if you can get anything. For non-hybrid inverter start with 1000 and go up. 91 | 92 | ## Important 93 | This is a very early draft version and things might not work as expected. Feel free to ask questions. 94 | -------------------------------------------------------------------------------- /config/registers.py: -------------------------------------------------------------------------------- 1 | # MODBUS Registers, prometheus name and description 2 | # Add * in front of the register name if you do not want it to be presented to Prometheus or published to MQTT 3 | 4 | all_regs = ( 5 | (33022, ( 6 | ('system_year', 'System Year(0-99)'), 7 | ('system_month', 'System Month'), 8 | ('system_day', 'System Day'), 9 | ('system_hour', 'System Hour'), 10 | ('system_minute', 'System Minute'), 11 | ('system_second', 'System Second'), 12 | ('*not_used', 'Not Used'), 13 | ('total_generation_1', 'Total Generation 1(kWh)'), 14 | ('total_generation_2', 'Total Generation 2(kWh)'), 15 | ('this_month_generated_1', 'Generated This Month 1(kWh)'), 16 | ('this_month_generated_2', 'Generated This Month 2(kWh)'), 17 | ('last_month_generated_1', 'Generated Last Month 1(kWh)'), 18 | ('last_month_generated_2', 'Generated Last Month 2(kWh)'), 19 | ('today_generated', 'Generated Today(0.1kWh)'), 20 | ('yesterday_generated', 'Generated Yesterday(0.1kWh)'), 21 | ('this_year_generated_1', 'Generated This Year 1(kWh)'), 22 | ('this_year_generated_2', 'Generated This Year 2(kWh)'), 23 | ('last_year_generated_1', 'Generated Last Year 1(kWh)'), 24 | ('last_year_generated_2', 'Generated Last Year 2(kWh)'))), 25 | (33049, ( 26 | ('dc_voltage_1', 'DC Voltage 1(0.1V)'), 27 | ('dc_current_1', 'DC Current 1(0.1A)'), 28 | ('dc_voltage_2', 'DC Voltage 2(0.1V)'), 29 | ('dc_current_2', 'DC Current 2(0.1A)'), 30 | ('*dc_voltage_3', 'DC Voltage 3(0.1V)'), 31 | ('*dc_current_3', 'DC Current 3(0.1A)'), 32 | ('*dc_voltage_4', 'DC Voltage 4(0.1V)'), 33 | ('*dc_current_4', 'DC Current 4(0.1A)'), 34 | ('total_dc_input_power_1', 'Total DC Input Power 1(W)'), 35 | ('total_dc_input_power_2', 'Total DC Input Power 2(W)'))), 36 | (33071, ( 37 | ('dc_bus_voltage', 'DC bus Voltage(0.1V)'), 38 | ('dc_bus_half_voltage', 'DC bus half Voltage(0.1V)'), 39 | ('phase_a_voltage', 'Phase A Voltage(0.1V)'), 40 | ('*phase_b_voltage', 'Phase B Voltage(0.1V)'), 41 | ('*phase_c_voltage', 'Phase C Voltage(0.1V)'), 42 | ('phase_a_current', 'Phase A Current(0.1A)'), 43 | ('*phase_b_current', 'Phase B Current(0.1A)'), 44 | ('*phase_c_current', 'Phase C Current(0.1A)'), 45 | ('active_power_1', 'Active Power 1(W)'), 46 | ('active_power_2', 'Active Power 2(W)'), 47 | ('reactive_power_1', 'Reactive power 1(W)'), 48 | ('reactive_power_2', 'Reactive power 2(W)'), 49 | ('apparent_power_1', 'Apparent Power 1(VA)'), 50 | ('apparent_power_2', 'Apparent Power 2(VA)'))), 51 | (33091, ( 52 | ('standard_working_mode', 'Standard Working Mode'), 53 | ('national_standard', 'National Standard'), 54 | ('inverter_temperature', 'Inverter Temperature(0.1C)'), 55 | ('grid_frequency', 'Grid Frequency(0.01Hz'), 56 | ('current_state_of_inverter', 'Current State Of Inverter'))), 57 | (33100, ( 58 | ('limit_active_power_output_value_1', 'Limit Active Power Output 1(W)'), 59 | ('limit_active_power_output_value_2', 'Limit Active Power Output 2(W)'), 60 | ('limit_reactive_power_output_value_1', 'Limit Reactive Power Output 1(W)'), 61 | ('limit_reactive_power_output_value_2', 'Limit Reactive Power Output 2(W)'), 62 | ('actual_power_limited_power', 'Actual Power Limited Power(%)'), 63 | ('actual_adjustment_value', 'Actual Adjustment(0.001)'), 64 | ('limit_reactive_power_value', 'Limit Reactive Power(%)'))), 65 | (33126, ( 66 | ('meter_total_energy_1', 'Electricity Meter Total Energy 1(Wh)'), 67 | ('meter_total_energy_2', 'Electricity Meter Total Energy 2(Wh)'), 68 | ('meter_voltage', 'Meter Voltage(0.1V)'), 69 | ('meter_current', 'Meter Current(0.1A)'), 70 | ('meter_active_power_1', 'Meter Active Power 1(0.1W)'), 71 | ('meter_active_power_2', 'Meter Active Power 2(0.1W)'), 72 | ('energy_storage_mode', 'Energy Storage Mode'), 73 | ('battery_voltage', 'Battery Voltage(0.1V)'), 74 | ('battery_current', 'Battery Current(0.1A)'), 75 | ('battery_current_direction', 'Battery Current_Direction(0=Charging, 1=Discharging'), 76 | ('llcbus_voltage', 'LLCbus Voltage(0.1V)'), 77 | ('bypass_ac_voltage', 'Bypass AC Voltage(0.1V)'), 78 | ('bypass_ac_current', 'Bypass AC Current(0.1A)'), 79 | ('battery_capacity_soc', 'Battery Capacity SOC(%)'), 80 | ('battery_health_soh', 'Battery Health SOH(%)'), 81 | ('battery_voltage_bms', 'Battery Voltage BMS(0.01V)'), 82 | ('battery_current_bms', 'Battery Current BMS(0.01A)'), 83 | ('battery_charge_current_limit', 'Battery Charge Current Limit(0.1A)'), 84 | ('battery_discharge_current_limit', 'Battery Discharge Current Limit(0.1A)'), 85 | ('battery_failure_info_01', 'Battery Failure Information 01'), 86 | ('battery_failure_info_02', 'Battery Failure Information 02'), 87 | ('house_load_power', 'House Load Power(W)'), 88 | ('bypass_load_power', 'Bypass Load Power(W)'), 89 | ('battery_power_1', 'Battery Power 1(W)'), 90 | ('battery_power_2', 'Battery Power 2(W)'))), 91 | (33161, ( 92 | ('total_battery_charge_1', 'Total Battery Charge 1(kWh)'), 93 | ('total_battery_charge_2', 'Total Battery Charge 2(kWh)'), 94 | ('today_battery_charge', 'Today Battery Charge(0.1kWh)'), 95 | ('yesterday_battery_charge', 'Yesterday Battery Charge(0.1kWh)'), 96 | ('total_battery_discharge_1', 'Total Battery Discharge 1(kWh)'), 97 | ('total_battery_discharge_2', 'Total Battery Discharge 2(kWh)'), 98 | ('battery_discharge_capacity', 'Battery Discharge Capacity(0.1kWh)'), 99 | ('yesterday_battery_discharge', 'Yesterday Battery Discharge(0.1kWh)'), 100 | ('total_imported_energy_1', 'Total Imported Energy 1(kWh)'), 101 | ('total_imported_energy_2', 'Total Imported Energy 2(kWh)'), 102 | ('today_imported_energy', 'Today Imported Energy(0.1kWh)'), 103 | ('yesterday_imported_energy', 'Yesterday Imported Energy(0.1kWh)'), 104 | ('total_exported_energy_1', 'Total Exported Energy 1(kWh)'), 105 | ('total_exported_energy_2', 'Total Exported Energy 2(kWh)'), 106 | ('today_exported_energy', 'Today Exported Energy(0.1kWh)'), 107 | ('yesterday_exported_energy', 'Yesterday Exported Energy(0.1kWh)'), 108 | ('total_house_load_1', 'Total House Load 1(kWh)'), 109 | ('total_house_load_2', 'Total House Load 2(kWh)'), 110 | ('today_house_load', 'Today House Load(0.1kWh)'), 111 | ('yesterday_house_load', 'Yesterday House Load(0.1kWh)'))), 112 | (33251, ( 113 | ('meter_ac_voltage_a', 'Meter AC Voltage A(0.1V)'), 114 | ('meter_ac_current_a', 'Meter AC Current A(0.01A)'), 115 | ('*meter_ac_voltage_b', 'Meter AC Voltage B(0.1V)'), 116 | ('*meter_ac_current_b', 'Meter AC Current B(0.01A)'), 117 | ('*meter_ac_voltage_c', 'Meter AC Voltage C(0.1V)'), 118 | ('*meter_ac_current_c', 'Meter AC Current C(0.01A)'), 119 | ('meter_active_power_a_1', 'Meter Active Power A 1(0.001kW)'), 120 | ('meter_active_power_a_2', 'Meter Active Power A 2(0.001kW)'), 121 | ('*meter_active_power_b_1', 'Meter Active Power B 1(0.001kW)'), 122 | ('*meter_active_power_b_2', 'Meter Active Power B 2(0.001kW)'), 123 | ('*meter_active_power_c_1', 'Meter Active Power C 1(0.001kW)'), 124 | ('*meter_active_power_c_2', 'Meter Active Power C 2(0.001kW)'), 125 | ('meter_total_active_power_1', 'Meter Total active Power 1(0.001kW)'), 126 | ('meter_total_active_power_2', 'Meter Total active Power 2(0.001kW)'), 127 | ('meter_reactive_power_a_1', 'Meter Active Reactive Power A 1(VA)'), 128 | ('meter_reactive_power_a_2', 'Meter Active Reactive Power A 2(VA)'), 129 | ('*meter_reactive_power_b_1', 'Meter Active Reactive Power B 1(VA)'), 130 | ('*meter_reactive_power_b_2', 'Meter Active Reactive Power B 2(VA)'), 131 | ('*meter_reactive_power_c_1', 'Meter Active Reactive Power C 1(VA)'), 132 | ('*meter_reactive_power_c_2', 'Meter Active Reactive Power C 2(VA)'), 133 | ('meter_total_reactive_power_1', 'Meter Total Reactive Power 1(VA)'), 134 | ('meter_total_reactive_power_2', 'Meter Total Reactive Power 2(VA)'), 135 | ('meter_apparent_power_a_1', 'Meter Active Apparent Power A 1(VA)'), 136 | ('meter_apparent_power_a_2', 'Meter Active Apparent Power A 2(VA)'), 137 | ('*meter_apparent_power_b_1', 'Meter Active Apparent Power B 1(VA)'), 138 | ('*meter_apparent_power_b_2', 'Meter Active Apparent Power B 2(VA)'), 139 | ('*meter_apparent_power_c_1', 'Meter Active Apparent Power C 1(VA)'), 140 | ('*meter_apparent_power_c_2', 'Meter Active Apparent Power C 2(VA)'), 141 | ('meter_total_apparent_power_1', 'Meter Total Apparent Power 1(VA)'), 142 | ('meter_total_apparent_power_2', 'Meter Total Apparent Power 2(VA)'), 143 | ('meter_power_factor', 'Meter Power Factor'), 144 | ('meter_grid_frequency', 'Meter Grid Frequency(0.01Hz)'), 145 | ('meter_total_active_imported_1', 'Meter Total Active Imported 1(0.01kWh)'), 146 | ('meter_total_active_imported_2', 'Meter Total Active Imported 2(0.01kWh)'), 147 | ('meter_total_active_exported_1', 'Meter Total Active Exported 1(0.01kWh)'), 148 | ('meter_total_active_exported_2', 'Meter Total Active Exported 2(0.01kWh)'))) 149 | ) 150 | 151 | 152 | # For future 153 | ''' 154 | (33115, ( 155 | ('set_the_flag_bit', 'Set The Flag Bit'), 156 | ('fault_code_01', 'Fault Code 01'), 157 | ('fault_code_02', 'Fault Code 02'), 158 | ('fault_code_03', 'Fault Code 03'), 159 | ('fault_code_04', 'Fault Code 04'), 160 | ('fault_code_05', 'Fault Code 05'), 161 | ('working_status', 'Working Status'))), 162 | ''' -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # ver. 0.0.29 2 | import config.config as config 3 | import config.registers as registers 4 | import logging 5 | import paho.mqtt.client as mqtt 6 | from sys import exit 7 | from json import dumps 8 | from time import strptime, mktime, sleep 9 | from prometheus_client import start_http_server 10 | from prometheus_client.core import GaugeMetricFamily, REGISTRY 11 | from pysolarmanv5.pysolarmanv5 import PySolarmanV5 12 | 13 | metrics_dict = {} 14 | debug = 0 15 | 16 | 17 | def add_modified_metrics(custom_metrics_dict): 18 | met_pwr = custom_metrics_dict['meter_active_power_1'] - custom_metrics_dict['meter_active_power_2'] 19 | total_load = custom_metrics_dict['house_load_power'] + custom_metrics_dict['bypass_load_power'] 20 | 21 | # Present battery modified metrics 22 | if custom_metrics_dict['battery_current_direction'] == 0: 23 | metrics_dict['battery_power_modified'] = 'Battery Power(modified)', custom_metrics_dict['battery_power_2'] 24 | metrics_dict['battery_power_in_modified'] = 'Battery Power In(modified)', custom_metrics_dict['battery_power_2'] 25 | metrics_dict['battery_power_out_modified'] = 'Battery Power Out(modified)', 0 26 | metrics_dict['grid_to_battery_power_in_modified'] = 'Grid to Battery Power In(modified)', 0 27 | else: 28 | metrics_dict['battery_power_modified'] = 'Battery Power(modified)', custom_metrics_dict['battery_power_2'] * -1 # negative 29 | metrics_dict['battery_power_out_modified'] = 'Battery Power Out(modified)', custom_metrics_dict['battery_power_2'] 30 | metrics_dict['battery_power_in_modified'] = 'Battery Power In(modified)', 0 31 | metrics_dict['grid_to_battery_power_in_modified'] = 'Grid to Battery Power In(modified)', 0 32 | 33 | if total_load < met_pwr and custom_metrics_dict['battery_power_2'] > 0: 34 | metrics_dict['grid_to_battery_power_in_modified'] = 'Grid to Battery Power In(modified)', custom_metrics_dict['battery_power_2'] 35 | 36 | # Present meter modified metrics 37 | if met_pwr > 0: 38 | metrics_dict['meter_power_in_modified'] = 'Meter Power In(modified)', met_pwr 39 | metrics_dict['meter_power_modified'] = 'Meter Power(modified)', met_pwr 40 | metrics_dict['meter_power_out_modified'] = 'Meter Power Out(modified)', 0 41 | else: 42 | metrics_dict['meter_power_out_modified'] = 'Meter Power Out(modified)', met_pwr * - 1 # negative 43 | metrics_dict['meter_power_in_modified'] = 'Meter Power In(modified)', 0 44 | metrics_dict['meter_power_modified'] = 'Meter Power(modified)', met_pwr 45 | 46 | # Present load modified metrics 47 | metrics_dict['total_load_power_modified'] = 'Total Load Power(modified)', total_load 48 | 49 | if 0 < custom_metrics_dict['total_dc_input_power_2'] <= total_load: 50 | metrics_dict['solar_to_house_power_modified'] = 'Solar To House Power(modified)', custom_metrics_dict['total_dc_input_power_2'] 51 | elif custom_metrics_dict['total_dc_input_power_2'] == 0: 52 | metrics_dict['solar_to_house_power_modified'] = 'Solar To House Power(modified)', 0 53 | elif custom_metrics_dict['total_dc_input_power_2'] > total_load: 54 | metrics_dict['solar_to_house_power_modified'] = 'Solar To House Power(modified)', total_load 55 | 56 | logging.info('Added modified metrics') 57 | 58 | 59 | def scrape_solis(debug): 60 | custom_metrics_dict = {} 61 | global metrics_dict 62 | metrics_dict = {} 63 | regs_ignored = 0 64 | try: 65 | logging.info('Connecting to Solis Modbus') 66 | modbus = PySolarmanV5( 67 | config.INVERTER_IP, config.INVERTER_SERIAL, port=config.INVERTER_PORT, mb_slave_id=1, verbose=debug) 68 | except Exception as e: 69 | logging.error(f'{repr(e)}. Exiting') 70 | exit(1) 71 | 72 | logging.info('Scraping...') 73 | 74 | for r in registers.all_regs: 75 | reg = r[0] 76 | reg_len = len(r[1]) 77 | reg_des = r[1] 78 | 79 | # Sometimes the query fails this will retry 3 times before exiting 80 | c = 0 81 | while True: 82 | try: 83 | logging.debug(f'Scrapping registers {reg} length {reg_len}') 84 | # read registers at address , store result in regs list 85 | regs = modbus.read_input_registers(register_addr=reg, quantity=reg_len) 86 | logging.debug(regs) 87 | except Exception as e: 88 | if c == 3: 89 | logging.error(f'Cannot read registers {reg} length{reg_len}. Tried {c} times. Exiting {repr(e)}') 90 | exit(1) 91 | else: 92 | c += 1 93 | logging.error(f'Cannot read registers {reg} length {reg_len} {repr(e)}') 94 | logging.error(f'Retry {c} in 3s') 95 | sleep(3) # hold before retry 96 | continue 97 | break 98 | 99 | # Convert time to epoch 100 | if reg == 33022: 101 | inv_year = '20' + str(regs[0]) + '-' 102 | if regs[1] < 10: 103 | inv_month = '0' + str(regs[1]) + '-' 104 | else: 105 | inv_month = str(regs[1]) + '-' 106 | if regs[2] < 10: 107 | inv_day = '0' + str(regs[2]) + ' ' 108 | else: 109 | inv_day = str(regs[2]) + ' ' 110 | if regs[3] < 10: 111 | inv_hour = '0' + str(regs[3]) + ':' 112 | else: 113 | inv_hour = str(regs[3]) + ':' 114 | if regs[4] < 10: 115 | inv_min = '0' + str(regs[4]) + ':' 116 | else: 117 | inv_min = str(regs[4]) + ':' 118 | if regs[5] < 10: 119 | inv_sec = '0' + str(regs[5]) 120 | else: 121 | inv_sec = str(regs[5]) 122 | inv_time = inv_year + inv_month + inv_day + inv_hour + inv_min + inv_sec 123 | logging.info(f'Solis Inverter time: {inv_time}') 124 | time_tuple = strptime(inv_time, '%Y-%m-%d %H:%M:%S') 125 | time_epoch = mktime(time_tuple) 126 | metrics_dict['system_epoch'] = 'System Epoch Time', time_epoch 127 | 128 | # Add metric to list 129 | 130 | for (i, item) in enumerate(regs): 131 | if '*' not in reg_des[i][0]: 132 | metrics_dict[reg_des[i][0]] = reg_des[i][1], item 133 | 134 | # Add custom metrics to custom_metrics_dict 135 | # Get battery metric for modification 136 | if reg_des[i][0] == 'battery_power_2': 137 | custom_metrics_dict[reg_des[i][0]] = item 138 | elif reg_des[i][0] == 'battery_current_direction': 139 | custom_metrics_dict[reg_des[i][0]] = item 140 | 141 | # Get grid metric for modification 142 | elif reg_des[i][0] == 'meter_active_power_1': 143 | custom_metrics_dict[reg_des[i][0]] = item 144 | elif reg_des[i][0] == 'meter_active_power_2': 145 | custom_metrics_dict[reg_des[i][0]] = item 146 | 147 | # Get load metric for modification 148 | elif reg_des[i][0] == 'house_load_power': 149 | custom_metrics_dict[reg_des[i][0]] = item 150 | elif reg_des[i][0] == 'total_dc_input_power_2': 151 | custom_metrics_dict[reg_des[i][0]] = item 152 | elif reg_des[i][0] == 'bypass_load_power': 153 | custom_metrics_dict[reg_des[i][0]] = item 154 | 155 | else: 156 | regs_ignored += 1 157 | 158 | logging.info(f'Ignored registers: {regs_ignored}') 159 | 160 | # Create modified metrics 161 | if config.MODIFIED_METRICS: 162 | add_modified_metrics(custom_metrics_dict) 163 | logging.info('Scraped') 164 | 165 | 166 | def publish_mqtt(): 167 | mqtt_dict = {} 168 | try: 169 | if not config.PROMETHEUS: 170 | scrape_solis(debug) 171 | 172 | # Resize dictionary and convert to JSON 173 | for metric, value in metrics_dict.items(): 174 | mqtt_dict[metric] = value[1] 175 | mqtt_json = dumps(mqtt_dict) 176 | 177 | def on_message(mqttc, userdata, msg): 178 | print(f"Message received [{msg.topic}]: {msg.payload}") 179 | 180 | 181 | mqttc = mqtt.Client() 182 | if config.MQTT_USER != '': 183 | mqttc.username_pw_set(config.MQTT_USER, config.MQTT_PASS) 184 | mqttc.connect(config.MQTT_SERVER, config.MQTT_PORT, config.MQTT_KEEPALIVE) 185 | mqttc.on_connect = logging.info(f'Connected to MQTT {config.MQTT_SERVER}:{config.MQTT_PORT}') 186 | 187 | logging.info('Publishing MQTT') 188 | mqttc.publish(topic=config.MQTT_TOPIC, payload=mqtt_json) 189 | 190 | mqttc.disconnect() 191 | 192 | except Exception as e: 193 | logging.error(f'Could not connect to MQTT {repr(e)}') 194 | 195 | 196 | class CustomCollector(object): 197 | def __init__(self): 198 | pass 199 | 200 | def collect(self): 201 | scrape_solis(debug) 202 | publish_mqtt() 203 | 204 | for metric, value in metrics_dict.items(): 205 | yield GaugeMetricFamily(metric, value[0], value=value[1]) 206 | 207 | 208 | if __name__ == '__main__': 209 | try: 210 | if config.DEBUG: 211 | logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', level=logging.DEBUG, 212 | datefmt='%Y-%m-%d %H:%M:%S') 213 | debug = 1 214 | else: 215 | logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', level=logging.INFO, 216 | datefmt='%Y-%m-%d %H:%M:%S') 217 | debug = 0 218 | 219 | logging.info('Starting') 220 | 221 | if config.PROMETHEUS: 222 | logging.info(f'Starting Web Server for Prometheus on port: {config.PROMETHEUS_PORT}') 223 | start_http_server(config.PROMETHEUS_PORT) 224 | 225 | REGISTRY.register(CustomCollector()) 226 | while True: 227 | sleep(config.CHECK_INTERVAL) 228 | 229 | else: 230 | while True: 231 | publish_mqtt() 232 | sleep(config.CHECK_INTERVAL) 233 | 234 | except Exception as e: 235 | logging.error(f'Cannot start: {repr(e)}') 236 | exit(1) 237 | --------------------------------------------------------------------------------