├── templates ├── PV_background.png ├── PV_background-blue.png ├── evcc.yaml ├── hass-dashboard.yaml └── hass-dashboard-blue.yaml ├── src ├── mtecmqtt │ ├── __init__.py │ ├── config-template.yaml │ ├── test.py │ ├── mtec_export.py │ ├── mqtt.py │ ├── hass_int.py │ ├── mtec_util.py │ ├── config.py │ ├── mtec_mqtt.py │ ├── MTECmodbusAPI.py │ └── registers.yaml └── install_systemd_service.sh ├── .gitignore ├── pyproject.toml ├── LICENSE └── README.md /templates/PV_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/croedel/MTECmqtt/HEAD/templates/PV_background.png -------------------------------------------------------------------------------- /templates/PV_background-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/croedel/MTECmqtt/HEAD/templates/PV_background-blue.png -------------------------------------------------------------------------------- /src/mtecmqtt/__init__.py: -------------------------------------------------------------------------------- 1 | # MTECmqtt 2 | __all__ = ["config", "mqtt", "hass_int", "MTECmodbusAPI"] 3 | __version__ = "2.1.0" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Private config file 7 | /config.yaml 8 | /evcc.yaml 9 | 10 | # others 11 | .vscode/launch.json 12 | .vscode 13 | *.code-workspace -------------------------------------------------------------------------------- /src/install_systemd_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DIR=$(dirname "$0") 4 | BASE_DIR=$(readlink -f $DIR) 5 | 6 | SVC_TXT=" 7 | [Unit] 8 | Description=M-TEC MQTT service 9 | After=multi-user.target 10 | 11 | [Service] 12 | Type=simple 13 | User=USER 14 | WorkingDirectory=BASE_DIR 15 | ExecStart=BASE_DIR/python3 mtec_mqtt 16 | Restart=always 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | " 21 | 22 | echo "MTECmqtt: Installing systemd service to auto-start mtec_mqtt" 23 | 24 | if [ $(id -u) != "0" ]; then 25 | echo "This script required root rights. Please restart using 'sudo'" 26 | else 27 | echo "$SVC_TXT" | sed "s!BASE_DIR!$BASE_DIR!g" | sed "s/USER/$SUDO_USER/g" > /tmp/mtec_mqtt.service 28 | chmod 666 /tmp/mtec_mqtt.service 29 | mv /tmp/mtec_mqtt.service /etc/systemd/system 30 | systemctl daemon-reload 31 | systemctl enable mtec_mqtt.service 32 | systemctl start mtec_mqtt.service 33 | echo "==> systemd service '/etc/systemd/system/mtec_mqtt.service' installed" 34 | fi 35 | 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "MTECmqtt" 7 | description = "Read data from a M-TEC Energybutler system and write them to a MQTT broker" 8 | dependencies = [ 9 | "pyyaml", 10 | "requests", 11 | "PyModbus > 3.8", 12 | "paho-mqtt >= 2.1", 13 | ] 14 | requires-python = ">=3.8" 15 | authors = [ 16 | {name = "Christian Rödel", email = "christian@roedel.info"}, 17 | ] 18 | readme = "README.md" 19 | license = {file = "LICENSE"} 20 | keywords = ["MTEC", "PV", "mqtt", "inverter"] 21 | classifiers = [ 22 | "Development Status :: 4 - Beta", 23 | "Programming Language :: Python :: 3.8" 24 | ] 25 | dynamic = ["version"] 26 | 27 | [project.urls] 28 | Repository = "https://github.com/croedel/MTECmqtt" 29 | 30 | [project.scripts] 31 | mtec_mqtt = "mtecmqtt.mtec_mqtt:main" 32 | mtec_export = "mtecmqtt.mtec_export:main" 33 | mtec_util = "mtecmqtt.mtec_util:main" 34 | 35 | [tool.setuptools] 36 | script-files = ["src/install_systemd_service.sh"] 37 | 38 | [tool.setuptools.dynamic] 39 | version = {attr = "mtecmqtt.__version__"} 40 | 41 | [tool.setuptools.package-data] 42 | mtecmqtt = ["*.yaml"] 43 | -------------------------------------------------------------------------------- /templates/evcc.yaml: -------------------------------------------------------------------------------- 1 | # evcc.yaml snippet 2 | # can be used as template for an integration of your MTEC inverter into evcc 3 | 4 | # Meters 5 | # TODO: replace with the actual serial no of your inverter 6 | meters: 7 | - name: MTEC-grid 8 | type: custom 9 | power: # Leistung (W) 10 | source: mqtt 11 | broker: localhost:1883 12 | topic: MTEC//now-base/grid_power 13 | scale: -1 14 | 15 | - name: MTEC-pv 16 | type: custom 17 | power: # Leistung (W) 18 | source: mqtt 19 | broker: localhost:1883 20 | topic: MTEC//now-base/pv 21 | scale: 1 22 | 23 | - name: MTEC-battery 24 | type: custom 25 | power: # Leistung (W) 26 | source: mqtt 27 | broker: localhost:1883 28 | topic: MTEC//now-base/battery 29 | scale: 1 30 | soc: # Battery SOC (%) 31 | source: mqtt 32 | broker: localhost:1883 33 | topic: MTEC//now-base/battery_soc 34 | scale: 1 35 | 36 | # site describes the EVU connection, PV and home battery 37 | site: 38 | title: Zuhause 39 | meters: 40 | grid: MTEC-grid # grid meter 41 | pv: 42 | - MTEC-pv # list of pv inverters/ meters 43 | battery: 44 | - MTEC-battery # list of battery meters 45 | 46 | # mqtt broker 47 | mqtt: 48 | broker: localhost:1883 49 | topic: evcc # root topic for publishing, set empty to disable publishing 50 | # clientid: foo 51 | # user: 52 | # password: 53 | -------------------------------------------------------------------------------- /src/mtecmqtt/config-template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # M-TEC espressif MODBUS server 3 | MODBUS_IP : espressif # IP address / hostname of "espressif" modbus server 4 | MODBUS_PORT : 5743 # Port for firmware < V27.52.4.0 5 | MODBUS_PORT2 : 502 # Port for firmware > V27.52.4.0 6 | MODBUS_SLAVE : 252 # Modbus slave id (usually no change required) 7 | MODBUS_TIMEOUT : 5 # Timeout for Modbus server (s) 8 | MODBUS_RETRIES : 3 # Retries 9 | MODBUS_FRAMER: rtu # Modbus Framer (usually no change required; options: 'ascii', 'binary', 'rtu', 'socket', 'tls') 10 | 11 | # MQTT settings 12 | MQTT_DISABLE : False 13 | MQTT_SERVER : localhost # MQTT server 14 | MQTT_PORT : 1883 # MQTT server port 15 | MQTT_LOGIN : " " # MQTT Login 16 | MQTT_PASSWORD : "" # MQTT Password 17 | MQTT_TOPIC : MTEC # MQTT topic name 18 | MQTT_FLOAT_FORMAT : "{:.3f}" # Defines how to format float values 19 | 20 | # Refresh interval 21 | REFRESH_NOW : 10 # Refresh "now" data every N seconds 22 | REFRESH_DAY : 300 # Refresh "day" statistic every N seconds 23 | REFRESH_TOTAL : 310 # Refresh "total" statistic every N seconds 24 | REFRESH_CONFIG : 3605 # Refresh "config" data every N seconds 25 | 26 | # Home Assistent support 27 | HASS_ENABLE : False # Enable home assistant 28 | HASS_BASE_TOPIC : homeassistant # Basis MQTT topic of home assistant 29 | HASS_BIRTH_GRACETIME : 15 # Give HASS some time to get ready after the birth message was received 30 | 31 | # General 32 | DEBUG : False # Set to True to get verbose debug messages 33 | -------------------------------------------------------------------------------- /src/mtecmqtt/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test connection to M-TEC Energybutler 4 | (c) 2024 by Christian Rödel 5 | """ 6 | from pymodbus.client import ModbusTcpClient 7 | from pymodbus.framer import Framer 8 | import logging 9 | 10 | #===================================================== 11 | class MTECmodbusAPI: 12 | #------------------------------------------------- 13 | def __init__( self ): 14 | self.modbus_client = None 15 | self.slave = 0 16 | self._cluster_cache = {} 17 | logging.debug("API initialized") 18 | 19 | def __del__(self): 20 | self.disconnect() 21 | 22 | #------------------------------------------------- 23 | # Connect to Modbus server 24 | def connect( self, ip_addr, port, slave ): 25 | self.slave = slave 26 | 27 | framer = "rtu" 28 | logging.debug("Connecting to server {}:{} (framer={})".format(ip_addr, port, framer)) 29 | self.modbus_client = ModbusTcpClient(ip_addr, port, framer=Framer(framer), timeout=5, retries=3, retry_on_empty=True ) 30 | 31 | if self.modbus_client.connect(): 32 | logging.debug("Successfully connected to server {}:{}".format(ip_addr, port)) 33 | return True 34 | else: 35 | logging.error("Couldn't connect to server {}:{}".format(ip_addr, port)) 36 | return False 37 | 38 | #------------------------------------------------- 39 | # Disconnect from Modbus server 40 | def disconnect( self ): 41 | if self.modbus_client and self.modbus_client.is_socket_open(): 42 | self.modbus_client.close() 43 | logging.debug("Successfully disconnected from server") 44 | 45 | 46 | #-------------------------------- 47 | # The main() function is just a demo code how to use the API 48 | def main(): 49 | logging.basicConfig() 50 | logging.getLogger().setLevel(logging.DEBUG) 51 | 52 | print( "Please enter" ) 53 | ip_addr = input("espressif server IP Adress: ") 54 | port = input("espressif Port (Standard is 5743): ") 55 | 56 | api = MTECmodbusAPI() 57 | api.connect(ip_addr=ip_addr, port=port, slave=252) 58 | api.disconnect() 59 | 60 | #-------------------------------------------- 61 | if __name__ == '__main__': 62 | main() 63 | -------------------------------------------------------------------------------- /src/mtecmqtt/mtec_export.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | This tool enables to query MTECmodbusapi and export the data in various ways. 4 | (c) 2024 by Christian Rödel 5 | """ 6 | from mtecmqtt.config import cfg, register_groups 7 | import argparse 8 | import sys 9 | from mtecmqtt.MTECmodbusAPI import MTECmodbusAPI 10 | 11 | #----------------------------- 12 | def parse_options(): 13 | groups = sorted(register_groups) 14 | groups.append("all") 15 | 16 | parser = argparse.ArgumentParser(description='MTEC Modbus data export tool. Allows to read and export Modbus registers from an MTEC inverter.', 17 | formatter_class=argparse.RawDescriptionHelpFormatter) 18 | parser.add_argument( '-g', '--group', choices=groups, default="all", help='Group of registers you want to export' ) 19 | parser.add_argument( '-r', '--registers', help='Comma separated list of registers which shall be retrieved' ) 20 | parser.add_argument( '-c', '--csv', action='store_true', help='Export as CSV') 21 | parser.add_argument( '-f', '--file', help='Write data to instead of stdout') 22 | parser.add_argument( '-a', '--append', action='store_true', help='Use as modifier in combination with --file argument to append data to file instead of replacing it') 23 | return parser.parse_args() 24 | 25 | #------------------------------- 26 | def main(): 27 | args = parse_options() 28 | api = MTECmodbusAPI() 29 | print( "Reading data..." ) 30 | 31 | # redirect stdout to file (if defined as command line parameter) 32 | if args.file: 33 | try: 34 | print( "Writing output to '{}'".format(args.file) ) 35 | if args.csv: 36 | print( "CSV format selected" ) 37 | if args.append: 38 | print( "Append mode selected" ) 39 | f_mode = 'a' 40 | else: 41 | f_mode = 'w' 42 | original_stdout = sys.stdout 43 | sys.stdout = open(args.file, f_mode) 44 | except: 45 | print( "ERROR - Unable to open output file '{}'".format(args.file) ) 46 | exit(1) 47 | 48 | registers = None 49 | if args.group and args.group != "all": 50 | registers = sorted(api.get_register_list(args.group)) 51 | 52 | if args.registers: 53 | registers = [] 54 | reg_str = args.registers.split(",") 55 | for addr in reg_str: 56 | registers.append(addr.strip()) 57 | 58 | # Do the export 59 | api.connect( ip_addr=cfg['MODBUS_IP'], port=cfg['MODBUS_PORT'], slave=cfg['MODBUS_SLAVE'] ) 60 | data = api.read_modbus_data( registers=registers ) 61 | api.disconnect() 62 | 63 | if data: 64 | for register, item in data.items(): 65 | if args.csv: 66 | line = "{};{};{};{}".format( register, item["name"], item["value"], item["unit"] ) 67 | else: 68 | line = "- {}: {:50s} {} {}".format( register, item["name"], item["value"], item["unit"] ) 69 | print( line ) 70 | 71 | # cleanup 72 | if args.file: 73 | sys.stdout.close() 74 | sys.stdout = original_stdout 75 | print( "Data completed" ) 76 | 77 | #------------------------------- 78 | if __name__ == '__main__': 79 | main() 80 | -------------------------------------------------------------------------------- /src/mtecmqtt/mqtt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | MQTT client base implemantation 4 | (c) 2024 by Christian Rödel 5 | """ 6 | import logging 7 | from mtecmqtt.config import cfg 8 | import time 9 | 10 | try: 11 | import paho.mqtt.client as mqttcl 12 | import paho.mqtt.publish as publish 13 | except Exception as e: 14 | logging.warning("MQTT not set up because of: {}".format(e)) 15 | 16 | # ============ MQTT ================ 17 | def on_mqtt_connect(mqttclient, userdata, flags, rc, prop): 18 | if rc == 0: 19 | logging.info("Connected to MQTT broker") 20 | else: 21 | logging.error("Error while connecting to MQTT broker: rc={}".format(rc)) 22 | 23 | def on_mqtt_disconnect(mqttclient, userdata, rc): 24 | logging.warning("MQTT broker disconnected: rc={}".format(rc)) 25 | 26 | def on_mqtt_subscribe(client, userdata, mid, reason_code_list, properties): 27 | logging.info("MQTT broker subscribed to mid {}".format(mid)) 28 | 29 | def on_mqtt_message(mqttclient, userdata, message): 30 | try: 31 | msg = message.payload.decode("utf-8") 32 | topic = message.topic.split("/") 33 | if msg == "online" and userdata: 34 | gracetime = cfg.get("HASS_BIRTH_GRACETIME", 15) 35 | logging.info("Received HASS online message. Sending discovery info in {} sec".format(gracetime)) 36 | time.sleep(gracetime) # dirty workaround: hass requires some grace period for being ready to receive discovery info 37 | userdata.send_discovery_info() 38 | except Exception as e: 39 | logging.warning("Error while handling MQTT message: {}".format(str(e))) 40 | 41 | def mqtt_start( hass=None ): 42 | try: 43 | client = mqttcl.Client(mqttcl.CallbackAPIVersion.VERSION2) 44 | client.user_data_set(hass) # register home automation instance 45 | if cfg['MQTT_LOGIN']: 46 | client.username_pw_set(cfg['MQTT_LOGIN'], cfg['MQTT_PASSWORD']) 47 | client.on_connect = on_mqtt_connect 48 | client.on_disconnect = on_mqtt_disconnect 49 | client.on_message = on_mqtt_message 50 | client.on_subscribe = on_mqtt_subscribe 51 | client.connect(cfg['MQTT_SERVER'], cfg['MQTT_PORT'], keepalive = 60) 52 | if hass: 53 | client.subscribe(cfg["HASS_BASE_TOPIC"]+"/status", qos=0) 54 | client.loop_start() 55 | logging.info('MQTT server started') 56 | return client 57 | except Exception as e: 58 | logging.warning("Couldn't start MQTT: {}".format(str(e))) 59 | return None 60 | 61 | def mqtt_stop(client): 62 | try: 63 | client.loop_stop() 64 | logging.info('MQTT server stopped') 65 | except Exception as e: 66 | logging.warning("Couldn't stop MQTT: {}".format(str(e))) 67 | 68 | def mqtt_publish( topic, payload ): 69 | if cfg['MQTT_DISABLE']: # Don't do anything - just logg 70 | logging.info("- {}: {}".format(topic, str(payload))) 71 | else: 72 | auth = None 73 | if cfg['MQTT_LOGIN']: 74 | auth = { 'username': cfg['MQTT_LOGIN'], 'password': cfg['MQTT_PASSWORD'] } 75 | logging.debug("- {}: {}".format(topic, str(payload))) 76 | try: 77 | publish.single(topic, payload=payload, hostname=cfg['MQTT_SERVER'], port=cfg['MQTT_PORT'], auth=auth) 78 | except Exception as e: 79 | logging.error("Could't send MQTT command: {}".format(str(e))) 80 | -------------------------------------------------------------------------------- /src/mtecmqtt/hass_int.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Auto discovery for home assistant 4 | (c) 2024 by Christian Rödel 5 | """ 6 | 7 | from mtecmqtt.config import cfg, register_map 8 | import logging 9 | import json 10 | from mtecmqtt.mqtt import mqtt_publish 11 | 12 | #--------------------------------------------------- 13 | class HassIntegration: 14 | # Custom automations 15 | buttons = [ 16 | # name unique_id payload_press 17 | # [ "Set general mode", "MTEC_load_battery_btn", "load_battery_from_grid" ], 18 | ] 19 | 20 | #------------------------------------------------- 21 | def __init__(self): 22 | self.serial_no = None 23 | self.is_initialized = False 24 | self.devices_array=[] 25 | 26 | #--------------------------------------------------- 27 | def initialize( self, serial_no ): 28 | self.serial_no = serial_no 29 | self.device_info = { 30 | "identifiers": [ self.serial_no ], 31 | "name": "MTEC Energybutler", 32 | "manufacturer": "MTEC", 33 | "model": "Energybutler", 34 | "via_device": "MTECmqtt" 35 | } 36 | self.devices_array.clear() 37 | self._build_devices_array() 38 | self._build_automation_array() 39 | self.send_discovery_info() 40 | self.is_initialized = True 41 | 42 | #--------------------------------------------------- 43 | def send_discovery_info( self ): 44 | logging.info('Sending home assistant discovery info') 45 | for device in self.devices_array: 46 | mqtt_publish( topic=device[0], payload=device[1] ) 47 | 48 | #--------------------------------------------------- 49 | def send_unregister_info( self ): 50 | logging.info('Sending info to unregister from home assistant') 51 | for device in self.devices_array: 52 | mqtt_publish( topic=device[0], payload="" ) 53 | 54 | #--------------------------------------------------- 55 | def _build_automation_array( self ): 56 | # Buttons 57 | for item in self.buttons: 58 | data_item = { 59 | "name": item[0], 60 | "unique_id": item[1], 61 | "payload_press": item[2], 62 | "command_topic": "MTEC/" + self.serial_no + "/automations/command", 63 | "device": self.device_info 64 | } 65 | topic = cfg["HASS_BASE_TOPIC"] + "/button/" + item[1] + "/config" 66 | self.devices_array.append( [topic, json.dumps(data_item)] ) 67 | 68 | #--------------------------------------------------- 69 | # build discovery data for devices 70 | def _build_devices_array( self ): 71 | for register, item in register_map.items(): 72 | # Do registration if there is a "hass_" config entry 73 | do_hass_registration = False 74 | for key in item.keys(): 75 | if "hass_" in key: 76 | do_hass_registration = True 77 | break 78 | 79 | if item["group"] and do_hass_registration: 80 | component_type = item.get("hass_component_type", "sensor") 81 | if component_type == "sensor": 82 | self._append_sensor(item) 83 | if component_type == "binary_sensor": 84 | self._append_binary_sensor(item) 85 | 86 | #--------------------------------------------------- 87 | def _append_sensor( self, item ): 88 | data_item = { 89 | "name": item["name"], 90 | "unique_id": "MTEC_" + item["mqtt"], 91 | "unit_of_measurement": item["unit"], 92 | "state_topic": "MTEC/" + self.serial_no + "/" + item["group"] + "/" + item["mqtt"], 93 | "device": self.device_info 94 | } 95 | if item.get("hass_device_class"): 96 | data_item["device_class"] = item["hass_device_class"] 97 | if item.get("hass_value_template"): 98 | data_item["value_template"] = item["hass_value_template"] 99 | if item.get("hass_state_class"): 100 | data_item["state_class"] = item["hass_state_class"] 101 | 102 | topic = cfg["HASS_BASE_TOPIC"] + "/sensor/" + "MTEC_" + item["mqtt"] + "/config" 103 | self.devices_array.append( [topic, json.dumps(data_item)] ) 104 | 105 | #--------------------------------------------------- 106 | def _append_binary_sensor( self, item ): 107 | data_item = { 108 | "name": item["name"], 109 | "unique_id": "MTEC_" + item["mqtt"], 110 | "state_topic": "MTEC/" + self.serial_no + "/" + item["group"] + "/" + item["mqtt"], 111 | "device": self.device_info 112 | } 113 | if item.get("hass_device_class"): 114 | data_item["device_class"] = item["hass_device_class"] 115 | if item.get("hass_payload_on"): 116 | data_item["payload_on"] = item["hass_payload_on"] 117 | if item.get("hass_payload_off"): 118 | data_item["payload_off"] = item["hass_payload_off"] 119 | 120 | topic = cfg["HASS_BASE_TOPIC"] + "/binary_sensor/" + "MTEC_" + item["mqtt"] + "/config" 121 | self.devices_array.append( [topic, json.dumps(data_item)] ) 122 | 123 | #--------------------------------------------------- 124 | # Testcode only 125 | def main(): 126 | hass = HassIntegration() 127 | hass.initialize( "my_serial_number" ) 128 | 129 | for i in hass.devices_array: 130 | topic = i[0] 131 | data = i[1] 132 | logging.info( "- {}: {}".format(topic, data) ) 133 | 134 | #--------------------------------------------------- 135 | if __name__ == '__main__': 136 | main() 137 | -------------------------------------------------------------------------------- /src/mtecmqtt/mtec_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | This is a test utility for MTECmodbusapi. 4 | (c) 2023 by Christian Rödel 5 | """ 6 | 7 | import logging 8 | FORMAT = '[%(levelname)s] %(message)s' 9 | logging.basicConfig(format=FORMAT, level=logging.INFO) 10 | 11 | from mtecmqtt.config import cfg, register_map, register_groups 12 | from mtecmqtt.MTECmodbusAPI import MTECmodbusAPI 13 | 14 | #------------------------------- 15 | def read_register(api): 16 | print( "-------------------------------------" ) 17 | register = input("Register: ") 18 | data = api.read_modbus_data( registers=[register] ) 19 | if data: 20 | item = data.get(register) 21 | print("Register {} ({}): {} {}".format(register, item["name"], item["value"], item["unit"])) 22 | 23 | #------------------------------- 24 | def read_register_group(api): 25 | print( "-------------------------------------" ) 26 | line = "Groups: " 27 | for g in sorted(register_groups): 28 | line += g + ", " 29 | print( line + "all" ) 30 | 31 | group = input("Register group (or RETURN for all): ") 32 | if group=="" or group=="all": 33 | registers = None 34 | else: 35 | registers = api.get_register_list(group) 36 | if not registers: 37 | return 38 | 39 | print( "Reading..." ) 40 | data = api.read_modbus_data( registers=registers ) 41 | if data: 42 | for register, item in data.items(): 43 | print("- {}: {:50s} {} {}".format( register, item["name"], item["value"], item["unit"])) 44 | 45 | #------------------------------- 46 | def write_register(api): 47 | print( "-------------------------------------" ) 48 | print( "Current settings of writable registers:" ) 49 | print( "Reg Name Value Unit" ) 50 | print( "----- ------------------------------ ------ ----" ) 51 | register_map_sorted = dict(sorted(register_map.items())) 52 | for register, item in register_map_sorted.items(): 53 | if item["writable"]: 54 | data = api.read_modbus_data( registers=[register] ) 55 | value = "" 56 | if data: 57 | value = data[register]["value"] 58 | unit = item["unit"] if item["unit"] else "" 59 | print("{:5s} {:30s} {:6s} {:4s} ".format(register, item["name"], str(value), unit )) 60 | 61 | print( "" ) 62 | register = input("Register: ") 63 | value = input("Value: ") 64 | 65 | print( "WARNING: Be careful when writing registers to your Inverter!" ) 66 | yn = input("Do you really want to set register {} to '{}'? (y/N)".format(register,value)) 67 | if yn == "y" or yn == "Y": 68 | ret = api.write_register( register=register, value=value) 69 | if ret == True: 70 | print("New value successfully set") 71 | else: 72 | print("Writing failed") 73 | else: 74 | print("Write aborted by user") 75 | 76 | #------------------------------- 77 | def list_register_config(api): 78 | print( "-------------------------------------" ) 79 | print( "Reg MQTT Parameter Unit Mode Group Name " ) 80 | print( "----- ------------------------------ ---- ---- --------------- -----------------------" ) 81 | register_map_sorted = dict(sorted(register_map.items())) 82 | for register, item in register_map_sorted.items(): 83 | if not register.isnumeric(): # non-numeric registers are deemed to be calculated pseudo-registers 84 | register = "" 85 | mqtt = item["mqtt"] if item["mqtt"] else "" 86 | unit = item["unit"] if item["unit"] else "" 87 | group = item["group"] if item["group"] else "" 88 | mode = "RW" if item["writable"] else "R" 89 | print("{:5s} {:30s} {:4s} {:4s} {:15s} {}".format(register, mqtt, unit, mode, group, item["name"])) 90 | 91 | #------------------------------- 92 | def list_register_config_by_groups(api): 93 | for group in register_groups: 94 | print( "-------------------------------------" ) 95 | print( "Group {}:".format(group) ) 96 | print( "" ) 97 | print( "Reg MQTT Parameter Unit Mode Name " ) 98 | print( "----- ------------------------------ ---- ---- -----------------------" ) 99 | register_map_sorted = dict(sorted(register_map.items())) 100 | for register, item in register_map_sorted.items(): 101 | if item["group"]==group: 102 | if not register.isnumeric(): # non-nu1meric registers are deemed to be calculated pseudo-registers 103 | register = "" 104 | mqtt = item["mqtt"] if item["mqtt"] else "" 105 | unit = item["unit"] if item["unit"] else "" 106 | mode = "RW" if item["writable"] else "R" 107 | print("{:5s} {:30s} {:4s} {:4s} {}".format(register, mqtt, unit, mode, item["name"])) 108 | print( "" ) 109 | 110 | #------------------------------- 111 | def main(): 112 | api = MTECmodbusAPI() 113 | api.connect( ip_addr=cfg['MODBUS_IP'], port=cfg['MODBUS_PORT'], slave=cfg['MODBUS_SLAVE'] ) 114 | 115 | while True: 116 | print( "=====================================" ) 117 | print( "Menu:") 118 | print( " 1: List all known registers" ) 119 | print( " 2: List register configuration by groups" ) 120 | print( " 3: Read register group from Inverter" ) 121 | print( " 4: Read single register from Inverter" ) 122 | print( " 5: Write register to Inverter" ) 123 | print( " x: Exit" ) 124 | opt = input("Please select: ") 125 | if opt == "1": 126 | list_register_config(api) 127 | elif opt == "2": 128 | list_register_config_by_groups(api) 129 | if opt == "3": 130 | read_register_group(api) 131 | elif opt == "4": 132 | read_register(api) 133 | elif opt == "5": 134 | write_register(api) 135 | elif opt == "x" or opt == "X": 136 | break 137 | 138 | api.disconnect() 139 | print( "Bye!") 140 | 141 | #------------------------------- 142 | if __name__ == '__main__': 143 | main() 144 | -------------------------------------------------------------------------------- /src/mtecmqtt/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Read YAML config files 3 | (c) 2024 by Christian Rödel 4 | """ 5 | import yaml 6 | import os 7 | import sys 8 | import logging 9 | import socket 10 | 11 | #---------------------------------------- 12 | # Create new config file 13 | def create_config_file(): 14 | print("Creating config.yaml") 15 | 16 | # Resolve hostname 17 | try: 18 | ip_addr=socket.gethostbyname('espressif') 19 | print("Found espressif server: {}".format(ip_addr)) 20 | except socket.error: 21 | print("Couldn't find espressif server") 22 | ip_addr=input("Please enter IP address of espressif server: ") 23 | 24 | opt=input("Enable HomeAssistant support? (y/N): ") 25 | if opt.lower()=='y': 26 | hass_cfg="HASS_ENABLE : True" 27 | else: 28 | hass_cfg="HASS_ENABLE : False" 29 | 30 | # Read template 31 | try: 32 | BASE_DIR = os.path.dirname(__file__) # Base installation directory 33 | templ_fname = os.path.join(BASE_DIR, "config-template.yaml") 34 | with open(templ_fname, "r") as file: 35 | data = file.read() 36 | except Exception as ex: 37 | print("ERROR - Couldn't read 'config-template.yaml': {}".format(ex)) 38 | return False 39 | 40 | # Customize 41 | data = data.replace('HASS_ENABLE : False', hass_cfg) 42 | data = data.replace('MODBUS_IP : espressif', 'MODBUS_IP : "' + ip_addr +'"') 43 | 44 | # Write customized config 45 | cfg_path = os.environ.get('XDG_CONFIG_HOME') or os.environ.get('APPDATA') 46 | if cfg_path: # Usually something like ~/.config/mtecmqtt/config.yaml resp. 'C:\\Users\\xxxx\\AppData\\Roaming' 47 | cfg_fname = os.path.join(cfg_path, "mtecmqtt", "config.yaml") 48 | else: 49 | cfg_fname = os.path.join(os.path.expanduser("~"), ".config", "mtecmqtt", "config.yaml") # ~/.config/mtecmqtt/config.yaml 50 | 51 | try: 52 | os.makedirs(os.path.dirname(cfg_fname), exist_ok=True) 53 | with open(cfg_fname, "w") as file: 54 | file.write(data) 55 | except Exception as ex: 56 | logging.error("ERROR - Couldn't write {}: {}".format(cfg_fname, ex)) 57 | return False 58 | 59 | logging.info("Successfully created {}".format(cfg_fname)) 60 | return True 61 | 62 | # Read configuration from YAML file 63 | def init_config(): 64 | # Look in different locations for config.yaml file 65 | conf_files = [] 66 | conf_files.append(os.path.join(os.getcwd(), "config.yaml")) # CWD/config.yaml 67 | cfg_path = os.environ.get('XDG_CONFIG_HOME') or os.environ.get('APPDATA') 68 | if cfg_path: # Usually something like ~/.config/mtecmqtt/config.yaml resp. 'C:\\Users\\xxxx\\AppData\\Roaming' 69 | conf_files.append(os.path.join(cfg_path, "mtecmqtt", "config.yaml")) 70 | else: 71 | conf_files.append(os.path.join(os.path.expanduser("~"), ".config", "mtecmqtt", "config.yaml")) # ~/.config/mtecmqtt/config.yaml 72 | 73 | cfg = False 74 | for fname_conf in conf_files: 75 | try: 76 | with open(fname_conf, 'r', encoding='utf-8') as f_conf: 77 | cfg = yaml.safe_load(f_conf) 78 | logging.info("Using config YAML file: {}".format(fname_conf) ) 79 | break 80 | except IOError as err: 81 | logging.debug("Couldn't open config YAML file: {}".format(str(err)) ) 82 | except yaml.YAMLError as err: 83 | logging.debug("Couldn't read config YAML file {}: {}".format(fname_conf, str(err)) ) 84 | 85 | return cfg 86 | 87 | #---------------------------------------- 88 | # Read inverter registers and their mapping from YAML file 89 | def init_register_map(): 90 | BASE_DIR = os.path.dirname(__file__) # Base installation directory 91 | try: 92 | fname_regs = os.path.join(BASE_DIR, "registers.yaml") 93 | with open(fname_regs, 'r', encoding='utf-8') as f_regs: 94 | r_map = yaml.safe_load(f_regs) 95 | except IOError as err: 96 | logging.fatal("Couldn't open registers YAML file: {}".format(str(err))) 97 | sys.exit(1) 98 | except yaml.YAMLError as err: 99 | logging.fatal("Couldn't read config YAML file {}: {}".format(fname_regs, str(err)) ) 100 | sys.exit(1) 101 | 102 | # Syntax checks 103 | register_map = {} 104 | p_mandatory = [ 105 | "name", 106 | ] 107 | p_optional = [ 108 | # param, default 109 | [ "length", None ], 110 | [ "type", None ], 111 | [ "unit", "" ], 112 | [ "scale", 1 ], 113 | [ "writable", False ], 114 | [ "mqtt", None ], 115 | [ "group", None ], 116 | ] 117 | register_groups = [] 118 | 119 | for key, val in r_map.items(): 120 | # Check for mandatory paramaters 121 | for p in p_mandatory: 122 | error = False 123 | if not val.get(p): 124 | logging.warning("Skipping invalid register config: {}. Missing mandatory parameter: {}.".format( key, p )) 125 | error = True 126 | break 127 | 128 | if not error: # All madatory parameters found 129 | item = val.copy() 130 | # Check optional parameters and add defaults, if not found 131 | for p in p_optional: 132 | if not item.get(p[0]): 133 | item[p[0]] = p[1] 134 | register_map[key] = item # Append to register_map 135 | if item["group"] and item["group"] not in register_groups: 136 | register_groups.append(item["group"]) # Append to group list 137 | return register_map, register_groups 138 | 139 | #---------------------------------------- 140 | logging.basicConfig( level=logging.INFO, format="[%(levelname)s] %(filename)s: %(message)s" ) 141 | cfg = init_config() 142 | if not cfg: 143 | logging.info("No config.yaml found - creating new one from template.") 144 | if create_config_file(): # Create a new config 145 | cfg = init_config() 146 | if not cfg: 147 | logging.fatal("Couldn't open fresh created config YAML file") 148 | sys.exit(1) 149 | else: 150 | logging.warning("Please edit and adapt freshly created config.yaml. Restart afterwards.") 151 | sys.exit(0) 152 | else: 153 | logging.fatal("Couldn't create config YAML file from templare") 154 | sys.exit(1) 155 | 156 | 157 | 158 | register_map, register_groups = init_register_map() 159 | 160 | #-------------------------------------- 161 | # Test code only 162 | if __name__ == "__main__": 163 | logging.info( "Config: {}".format( str(cfg)) ) 164 | logging.info( "Register_map: {}".format( str(register_map)) ) 165 | -------------------------------------------------------------------------------- /src/mtecmqtt/mtec_mqtt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | MQTT server for M-TEC Energybutler reading modbus data 4 | (c) 2024 by Christian Rödel 5 | """ 6 | 7 | import logging 8 | #FORMAT = '%(asctime)s [%(levelname)s] %(message)s' 9 | FORMAT = '[%(levelname)s] %(message)s' 10 | logging.basicConfig(format=FORMAT, level=logging.INFO) 11 | 12 | from mtecmqtt.config import cfg, register_map 13 | from datetime import datetime, timedelta 14 | import time 15 | import signal 16 | from mtecmqtt.mqtt import mqtt_start, mqtt_stop, mqtt_publish 17 | from mtecmqtt.MTECmodbusAPI import MTECmodbusAPI 18 | from mtecmqtt.hass_int import HassIntegration 19 | 20 | #---------------------------------- 21 | def signal_handler(signal_number, frame): 22 | global run_status 23 | logging.warning('Received Signal {}. Graceful shutdown initiated.'.format(signal_number)) 24 | run_status = False 25 | 26 | # ============================================= 27 | # read data from MTEC modbus 28 | def read_MTEC_data( api, group ): 29 | logging.info("Reading registers for group: {}".format(group)) 30 | registers = api.get_register_list( group ) 31 | now = datetime.now() 32 | data = api.read_modbus_data(registers=registers) 33 | pvdata = {} 34 | try: # assign all data 35 | for register in registers: 36 | item = register_map[register] 37 | if item["mqtt"]: 38 | if register.isnumeric(): 39 | pvdata[item["mqtt"]] = data[register] 40 | else: # non-numeric registers are deemed to be calculated pseudo-registers 41 | if register == "consumption": 42 | pvdata[item["mqtt"]] = data["11016"]["value"] - data["11000"]["value"] # power consumption 43 | elif register == "consumption-day": 44 | pvdata[item["mqtt"]] = data["31005"]["value"] + data["31001"]["value"] + data["31004"]["value"] - data["31000"]["value"] - data["31003"]["value"] # power consumption 45 | elif register == "autarky-day": 46 | pvdata[item["mqtt"]] = 100*(1 - (data["31001"]["value"] / pvdata["consumption_day"])) if pvdata["consumption_day"]>0 else 0 47 | elif register == "ownconsumption-day": 48 | pvdata[item["mqtt"]] = 100*(1-data["31000"]["value"] / data["31005"]["value"]) if data["31005"]["value"]>0 else 0 49 | elif register == "consumption-total": 50 | pvdata[item["mqtt"]] = data["31112"]["value"] + data["31104"]["value"] + data["31110"]["value"] - data["31102"]["value"] - data["31108"]["value"] # power consumption 51 | elif register == "autarky-total": 52 | pvdata[item["mqtt"]] = 100*(1 - (data["31104"]["value"] / pvdata["consumption_total"])) if pvdata["consumption_total"]>0 else 0 53 | elif register == "ownconsumption-total": 54 | pvdata[item["mqtt"]] = 100*(1-data["31102"]["value"] / data["31112"]["value"]) if data["31112"]["value"]>0 else 0 55 | elif register == "api-date": 56 | pvdata[item["mqtt"]] = now.strftime("%Y-%m-%d %H:%M:%S") # Local time of this server 57 | else: 58 | logging.warning("Unknown calculated pseudo-register: {}".format(register)) 59 | 60 | if isinstance(pvdata[item["mqtt"]], float) and pvdata[item["mqtt"]] < 0: # Avoid to report negative values, which might occur in some edge cases 61 | pvdata[item["mqtt"]] = 0 62 | 63 | except Exception as e: 64 | logging.warning("Retrieved Modbus data is incomplete: {}".format(str(e))) 65 | return None 66 | return pvdata 67 | 68 | #---------------------------------- 69 | # write data to MQTT 70 | def write_to_MQTT( pvdata, base_topic ): 71 | for param, data in pvdata.items(): 72 | topic = base_topic + param 73 | if isinstance(data, dict): 74 | if isinstance(data["value"], float): 75 | payload = cfg['MQTT_FLOAT_FORMAT'].format( data["value"] ) 76 | elif isinstance(data["value"], bool): 77 | payload = "{:d}".format( data["value"] ) 78 | else: 79 | payload = data["value"] 80 | else: 81 | if isinstance(data, float): 82 | payload = cfg['MQTT_FLOAT_FORMAT'].format( data ) 83 | elif isinstance(data, bool): 84 | payload = "{:d}".format( data ) 85 | else: 86 | payload = data 87 | mqtt_publish( topic, payload ) 88 | 89 | #========================================== 90 | def main(): 91 | global run_status 92 | run_status = True 93 | 94 | # Initialization 95 | signal.signal(signal.SIGTERM, signal_handler) 96 | signal.signal(signal.SIGINT, signal_handler) 97 | if cfg['DEBUG'] == True: 98 | logging.getLogger().setLevel(logging.DEBUG) 99 | logging.info("Starting") 100 | 101 | next_read_config = datetime.now() 102 | next_read_day = datetime.now() 103 | next_read_total = datetime.now() 104 | now_ext_idx = 0 105 | topic_base = None 106 | 107 | api = MTECmodbusAPI() 108 | if not api.connect(): 109 | logging.fatal("Can't connect to MODBUS server - exiting") 110 | return 111 | 112 | if cfg["HASS_ENABLE"]: 113 | hass = HassIntegration() 114 | else: 115 | hass = None 116 | mqttclient = mqtt_start( hass ) 117 | 118 | # Initialize 119 | pv_config = None 120 | while run_status and not pv_config: 121 | pv_config = read_MTEC_data( api, "config" ) 122 | if not pv_config: 123 | logging.warning("Cant retrieve initial config - retry in 10 s") 124 | time.sleep(10) 125 | 126 | if not pv_config: 127 | logging.fatal("Cant retrieve initial config.") 128 | return 129 | 130 | topic_base = cfg['MQTT_TOPIC'] + '/' + pv_config["serial_no"]["value"] + '/' 131 | if hass and not hass.is_initialized: 132 | hass.initialize( pv_config["serial_no"]["value"] ) 133 | 134 | # Main loop - exit on signal only 135 | while run_status: 136 | now = datetime.now() 137 | 138 | # Now base 139 | pvdata = read_MTEC_data( api, "now-base" ) 140 | if pvdata: 141 | write_to_MQTT( pvdata, topic_base + 'now-base/' ) 142 | 143 | # Now extended - read groups in a round robin - one per loop 144 | if now_ext_idx == 0: 145 | pvdata = read_MTEC_data( api, "now-grid" ) 146 | if pvdata: 147 | write_to_MQTT( pvdata, topic_base + 'now-grid/' ) 148 | elif now_ext_idx == 1: 149 | pvdata = read_MTEC_data( api, "now-inverter" ) 150 | if pvdata: 151 | write_to_MQTT( pvdata, topic_base + 'now-inverter/' ) 152 | elif now_ext_idx == 2: 153 | pvdata = read_MTEC_data( api, "now-backup" ) 154 | if pvdata: 155 | write_to_MQTT( pvdata, topic_base + 'now-backup/' ) 156 | elif now_ext_idx == 3: 157 | pvdata = read_MTEC_data( api, "now-battery" ) 158 | if pvdata: 159 | write_to_MQTT( pvdata, topic_base + 'now-battery/' ) 160 | elif now_ext_idx == 4: 161 | pvdata = read_MTEC_data( api, "now-pv" ) 162 | if pvdata: 163 | write_to_MQTT( pvdata, topic_base + 'now-pv/' ) 164 | 165 | if now_ext_idx >= 4: 166 | now_ext_idx = 0 167 | else: 168 | now_ext_idx += 1 169 | 170 | # Day 171 | if next_read_day <= now: 172 | pvdata = read_MTEC_data( api, "day" ) 173 | if pvdata: 174 | write_to_MQTT( pvdata, topic_base + 'day/' ) 175 | next_read_day = datetime.now() + timedelta(seconds=cfg['REFRESH_DAY']) 176 | 177 | # Total 178 | if next_read_total <= now: 179 | pvdata = read_MTEC_data( api, "total" ) 180 | if pvdata: 181 | write_to_MQTT( pvdata, topic_base + 'total/' ) 182 | next_read_total = datetime.now() + timedelta(seconds=cfg['REFRESH_TOTAL']) 183 | 184 | # Config 185 | if next_read_config <= now: 186 | pvdata = read_MTEC_data( api, "config" ) 187 | if pvdata: 188 | write_to_MQTT( pvdata, topic_base + 'config/' ) 189 | next_read_config = datetime.now() + timedelta(seconds=cfg['REFRESH_CONFIG']) 190 | 191 | logging.debug("Sleep {}s".format( cfg['REFRESH_NOW'] )) 192 | time.sleep(cfg['REFRESH_NOW']) 193 | 194 | # clean up 195 | if hass: 196 | hass.send_unregister_info() 197 | api.disconnect() 198 | mqtt_stop(mqttclient) 199 | logging.info("Exiting") 200 | 201 | #--------------------------------------------------- 202 | if __name__ == '__main__': 203 | main() 204 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Christian Rödel 2 | 3 | GNU LESSER GENERAL PUBLIC LICENSE 4 | Version 3, 29 June 2007 5 | 6 | Copyright (C) 2007 Free Software Foundation, Inc. 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | 11 | This version of the GNU Lesser General Public License incorporates 12 | the terms and conditions of version 3 of the GNU General Public 13 | License, supplemented by the additional permissions listed below. 14 | 15 | 0. Additional Definitions. 16 | 17 | As used herein, "this License" refers to version 3 of the GNU Lesser 18 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 19 | General Public License. 20 | 21 | "The Library" refers to a covered work governed by this License, 22 | other than an Application or a Combined Work as defined below. 23 | 24 | An "Application" is any work that makes use of an interface provided 25 | by the Library, but which is not otherwise based on the Library. 26 | Defining a subclass of a class defined by the Library is deemed a mode 27 | of using an interface provided by the Library. 28 | 29 | A "Combined Work" is a work produced by combining or linking an 30 | Application with the Library. The particular version of the Library 31 | with which the Combined Work was made is also called the "Linked 32 | Version". 33 | 34 | The "Minimal Corresponding Source" for a Combined Work means the 35 | Corresponding Source for the Combined Work, excluding any source code 36 | for portions of the Combined Work that, considered in isolation, are 37 | based on the Application, and not on the Linked Version. 38 | 39 | The "Corresponding Application Code" for a Combined Work means the 40 | object code and/or source code for the Application, including any data 41 | and utility programs needed for reproducing the Combined Work from the 42 | Application, but excluding the System Libraries of the Combined Work. 43 | 44 | 1. Exception to Section 3 of the GNU GPL. 45 | 46 | You may convey a covered work under sections 3 and 4 of this License 47 | without being bound by section 3 of the GNU GPL. 48 | 49 | 2. Conveying Modified Versions. 50 | 51 | If you modify a copy of the Library, and, in your modifications, a 52 | facility refers to a function or data to be supplied by an Application 53 | that uses the facility (other than as an argument passed when the 54 | facility is invoked), then you may convey a copy of the modified 55 | version: 56 | 57 | a) under this License, provided that you make a good faith effort to 58 | ensure that, in the event an Application does not supply the 59 | function or data, the facility still operates, and performs 60 | whatever part of its purpose remains meaningful, or 61 | 62 | b) under the GNU GPL, with none of the additional permissions of 63 | this License applicable to that copy. 64 | 65 | 3. Object Code Incorporating Material from Library Header Files. 66 | 67 | The object code form of an Application may incorporate material from 68 | a header file that is part of the Library. You may convey such object 69 | code under terms of your choice, provided that, if the incorporated 70 | material is not limited to numerical parameters, data structure 71 | layouts and accessors, or small macros, inline functions and templates 72 | (ten or fewer lines in length), you do both of the following: 73 | 74 | a) Give prominent notice with each copy of the object code that the 75 | Library is used in it and that the Library and its use are 76 | covered by this License. 77 | 78 | b) Accompany the object code with a copy of the GNU GPL and this license 79 | document. 80 | 81 | 4. Combined Works. 82 | 83 | You may convey a Combined Work under terms of your choice that, 84 | taken together, effectively do not restrict modification of the 85 | portions of the Library contained in the Combined Work and reverse 86 | engineering for debugging such modifications, if you also do each of 87 | the following: 88 | 89 | a) Give prominent notice with each copy of the Combined Work that 90 | the Library is used in it and that the Library and its use are 91 | covered by this License. 92 | 93 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 94 | document. 95 | 96 | c) For a Combined Work that displays copyright notices during 97 | execution, include the copyright notice for the Library among 98 | these notices, as well as a reference directing the user to the 99 | copies of the GNU GPL and this license document. 100 | 101 | d) Do one of the following: 102 | 103 | 0) Convey the Minimal Corresponding Source under the terms of this 104 | License, and the Corresponding Application Code in a form 105 | suitable for, and under terms that permit, the user to 106 | recombine or relink the Application with a modified version of 107 | the Linked Version to produce a modified Combined Work, in the 108 | manner specified by section 6 of the GNU GPL for conveying 109 | Corresponding Source. 110 | 111 | 1) Use a suitable shared library mechanism for linking with the 112 | Library. A suitable mechanism is one that (a) uses at run time 113 | a copy of the Library already present on the user's computer 114 | system, and (b) will operate properly with a modified version 115 | of the Library that is interface-compatible with the Linked 116 | Version. 117 | 118 | e) Provide Installation Information, but only if you would otherwise 119 | be required to provide such information under section 6 of the 120 | GNU GPL, and only to the extent that such information is 121 | necessary to install and execute a modified version of the 122 | Combined Work produced by recombining or relinking the 123 | Application with a modified version of the Linked Version. (If 124 | you use option 4d0, the Installation Information must accompany 125 | the Minimal Corresponding Source and Corresponding Application 126 | Code. If you use option 4d1, you must provide the Installation 127 | Information in the manner specified by section 6 of the GNU GPL 128 | for conveying Corresponding Source.) 129 | 130 | 5. Combined Libraries. 131 | 132 | You may place library facilities that are a work based on the 133 | Library side by side in a single library together with other library 134 | facilities that are not Applications and are not covered by this 135 | License, and convey such a combined library under terms of your 136 | choice, if you do both of the following: 137 | 138 | a) Accompany the combined library with a copy of the same work based 139 | on the Library, uncombined with any other library facilities, 140 | conveyed under the terms of this License. 141 | 142 | b) Give prominent notice with the combined library that part of it 143 | is a work based on the Library, and explaining where to find the 144 | accompanying uncombined form of the same work. 145 | 146 | 6. Revised Versions of the GNU Lesser General Public License. 147 | 148 | The Free Software Foundation may publish revised and/or new versions 149 | of the GNU Lesser General Public License from time to time. Such new 150 | versions will be similar in spirit to the present version, but may 151 | differ in detail to address new problems or concerns. 152 | 153 | Each version is given a distinguishing version number. If the 154 | Library as you received it specifies that a certain numbered version 155 | of the GNU Lesser General Public License "or any later version" 156 | applies to it, you have the option of following the terms and 157 | conditions either of that published version or of any later version 158 | published by the Free Software Foundation. If the Library as you 159 | received it does not specify a version number of the GNU Lesser 160 | General Public License, you may choose any version of the GNU Lesser 161 | General Public License ever published by the Free Software Foundation. 162 | 163 | If the Library as you received it specifies that a proxy can decide 164 | whether future versions of the GNU Lesser General Public License shall 165 | apply, that proxy's public statement of acceptance of any version is 166 | permanent authorization for you to choose that version for the 167 | Library. -------------------------------------------------------------------------------- /src/mtecmqtt/MTECmodbusAPI.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Modbus API for M-TEC Energybutler 4 | (c) 2023 by Christian Rödel 5 | """ 6 | from datetime import datetime, timedelta 7 | from mtecmqtt.config import cfg, register_map 8 | from pymodbus.client import ModbusTcpClient 9 | from pymodbus.constants import Endian 10 | import logging 11 | 12 | #===================================================== 13 | class MTECmodbusAPI: 14 | #------------------------------------------------- 15 | def __init__( self ): 16 | self.modbus_client = None 17 | self.last_reconnect = None 18 | self._cluster_cache = {} 19 | self.slave = cfg['MODBUS_SLAVE'] 20 | logging.debug("API initialized") 21 | 22 | def __del__(self): 23 | self.disconnect() 24 | 25 | #------------------------------------------------- 26 | # Connect to Modbus server 27 | def connect(self): 28 | # try with configured port 29 | connected = self._connect(ip_addr=cfg['MODBUS_IP'], port=cfg['MODBUS_PORT'], framer=cfg.get("MODBUS_FRAMER", "rtu"), 30 | timeout=cfg["MODBUS_TIMEOUT"], retries=cfg["MODBUS_RETRIES"]) 31 | if not connected: 32 | # try alternative port (defaults to 502) 33 | connected = self._connect(ip_addr=cfg['MODBUS_IP'], port=cfg.get('MODBUS_PORT2',"502"), framer=cfg.get("MODBUS_FRAMER", "rtu"), 34 | timeout=cfg["MODBUS_TIMEOUT"], retries=cfg["MODBUS_RETRIES"]) 35 | if not connected: 36 | logging.fatal( "Can't connect to MODBUS server: {}".format(cfg['MODBUS_IP']) ) 37 | 38 | return connected 39 | 40 | #------------------------------------------------- 41 | def _connect(self, ip_addr, port, framer, timeout, retries): 42 | logging.debug("Connecting to server {}:{} (framer={})".format(ip_addr, port, framer)) 43 | self.modbus_client = ModbusTcpClient(ip_addr, port=port, framer=framer, timeout=timeout, retries=retries) 44 | 45 | if self.modbus_client.connect(): 46 | logging.info("Successfully connected to server {}:{}".format(ip_addr, port)) 47 | return True 48 | else: 49 | logging.error("Couldn't connect to server {}:{}".format(ip_addr, port)) 50 | self.modbus_client = None 51 | return False 52 | 53 | #------------------------------------------------- 54 | # Disconnect from Modbus server 55 | def disconnect( self ): 56 | if self.modbus_client: 57 | logging.info("Disconnecting from Modbus server") 58 | try: 59 | if self.modbus_client.is_socket_open(): 60 | self.modbus_client.close() 61 | except Exception as ex: 62 | logging.debug("Exception while diconnecting: {}".format(ex)) 63 | self.modbus_client = None 64 | logging.debug("Successfully disconnected from Modbus server") 65 | 66 | #------------------------------------------------- 67 | # restart Modbus server connection 68 | def reconnect(self): 69 | now = datetime.now() 70 | if not self.last_reconnect or now > self.last_reconnect+timedelta(seconds=30): 71 | logging.info("Trying to re-connect to Modbus server") 72 | self.last_reconnect = now 73 | self.disconnect() 74 | if self.connect(): 75 | logging.info("Successfully re-connected to Modbus server") 76 | else: 77 | logging.error("Couldn't re-connect to Modbus server") 78 | 79 | #-------------------------------- 80 | # Get a list of all registers which belong to a given group 81 | def get_register_list( self, group ): 82 | registers = [] 83 | for register, item in register_map.items(): 84 | if item["group"] == group: 85 | registers.append(register) 86 | 87 | if len(registers)==0: 88 | logging.error("Unknown or empty register group: {}".format(group)) 89 | return None 90 | return registers 91 | 92 | #-------------------------------- 93 | # This is the main API function. It either fetches all registers or a list of given registers 94 | def read_modbus_data(self, registers=None): 95 | data = {} 96 | logging.debug("Retrieving data...") 97 | 98 | if registers == None: # Create liset of all (numeric) registers 99 | registers = [] 100 | for register in register_map: 101 | if register.isnumeric(): # non-numeric registers are deemed to be calculated pseudo-registers 102 | registers.append(register) 103 | 104 | cluster_list = self._get_register_clusters(registers) 105 | for reg_cluster in cluster_list: 106 | offset = 0 107 | logging.debug("Fetching data for cluster start {}, length {}, items {}".format(reg_cluster["start"], reg_cluster["length"], len(reg_cluster["items"]))) 108 | rawdata = self._read_registers(reg_cluster["start"], reg_cluster["length"]) 109 | if rawdata: 110 | for item in reg_cluster["items"]: 111 | if item.get("type"): # type==None means dummy 112 | data_decoded = self._decode_rawdata(rawdata, offset, item) 113 | if data_decoded: 114 | register = str(reg_cluster["start"] + offset) 115 | data.update( {register: data_decoded} ) 116 | else: 117 | logging.error("Decoding error while decoding register {}".format(register)) 118 | offset += item["length"] 119 | 120 | logging.debug("Data retrieval completed") 121 | return data 122 | 123 | #-------------------------------- 124 | # Write a value to a register 125 | def write_register(self, register, value): 126 | # Lookup register 127 | item = register_map.get(str(register), None) 128 | if not item: 129 | logging.error("Can't write unknown register: {}".format(register)) 130 | return False 131 | elif item.get("writable", False) == False: 132 | logging.error("Can't write register which is marked read-only: {}".format(register)) 133 | return False 134 | 135 | # check value 136 | try: 137 | if isinstance(value, str): 138 | if "." in value: 139 | value = float(value) 140 | else: 141 | value = int(value) 142 | except Exception as ex: 143 | logging.error("Invalid numeric value: {}".format(value)) 144 | return False 145 | 146 | # adjust scale 147 | if item["scale"] > 1: 148 | value *= item["scale"] 149 | 150 | try: 151 | result = self.modbus_client.write_register(address=int(register), value=int(value), slave=self.slave ) 152 | except Exception as ex: 153 | logging.error("Exception while writing register {} to pymodbus: {}".format(register, ex)) 154 | return False 155 | 156 | if result.isError(): 157 | logging.error("Error while writing register {} to pymodbus".format(register)) 158 | return False 159 | return True 160 | 161 | #-------------------------------- 162 | # Cluster registers in order to optimize modbus traffic 163 | def _get_register_clusters( self, registers ): 164 | # Cache clusters to avoid unnecessary overhead 165 | idx = str(registers) # use stringified version of list as index 166 | if idx not in self._cluster_cache: 167 | self._cluster_cache[idx] = self._create_register_clusters(registers) 168 | return self._cluster_cache[idx] 169 | 170 | # Create clusters 171 | def _create_register_clusters( self, registers ): 172 | cluster = { 173 | "start": 0, 174 | "length": 0, 175 | "items": [] 176 | } 177 | cluster_list = [] 178 | 179 | for register in sorted(registers): 180 | if register.isnumeric(): # ignore non-numeric pseudo registers 181 | item = register_map.get(register) 182 | if item: 183 | if int(register) > cluster["start"] + cluster["length"]: # there is a gap 184 | if cluster["start"] > 0: # except for first cluster 185 | cluster_list.append(cluster) 186 | cluster = { 187 | "start": int(register), 188 | "length": 0, 189 | "items": [] 190 | } 191 | cluster["length"] += item["length"] 192 | cluster["items"].append(item) 193 | else: 194 | logging.warning("Unknown register: {} - skipped.".format(register)) 195 | 196 | if cluster["start"] > 0: # append last cluster 197 | cluster_list.append(cluster) 198 | 199 | return cluster_list 200 | 201 | #-------------------------------- 202 | # Do the actual reading from modbus 203 | def _read_registers(self, register, length): 204 | try: 205 | result = self.modbus_client.read_holding_registers(address=int(register), count=length, slave=self.slave) 206 | except Exception as ex: 207 | logging.error("Exception while reading register {}, length {} from pymodbus: {}".format(register, length, ex)) 208 | self.reconnect() 209 | return None 210 | if result.isError(): 211 | logging.error("Error while reading register {}, length {} from pymodbus".format(register, length)) 212 | return None 213 | if len(result.registers) != length: 214 | logging.error("Error while reading register {} from pymodbus: Requested length {}, received {}".format(register, length, len(result.registers))) 215 | return None 216 | return result 217 | 218 | #-------------------------------- 219 | # Decode the result from rawdata, starting at offset 220 | def _decode_rawdata(self, rawdata, offset, item): 221 | try: 222 | val = None 223 | if item["type"] == 'U16': 224 | reg = rawdata.registers[offset:offset+1] 225 | val = self.modbus_client.convert_from_registers(registers=reg, data_type=self.modbus_client.DATATYPE.UINT16) 226 | elif item["type"] == 'I16': 227 | reg = rawdata.registers[offset:offset+1] 228 | val = self.modbus_client.convert_from_registers(registers=reg, data_type=self.modbus_client.DATATYPE.INT16) 229 | elif item["type"] == 'U32': 230 | reg = rawdata.registers[offset:offset+2] 231 | val = self.modbus_client.convert_from_registers(registers=reg, data_type=self.modbus_client.DATATYPE.UINT32) 232 | elif item["type"] == 'I32': 233 | reg = rawdata.registers[offset:offset+2] 234 | val = self.modbus_client.convert_from_registers(registers=reg, data_type=self.modbus_client.DATATYPE.INT32) 235 | elif item["type"] == 'BYTE': 236 | if item["length"] == 1: 237 | reg1 = int(rawdata.registers[offset]) 238 | val = "{:02d} {:02d}".format( reg1>>8, reg1&0xff ) 239 | elif item["length"] == 2: 240 | reg1 = int(rawdata.registers[offset]) 241 | reg2 = int(rawdata.registers[offset+1]) 242 | val = "{:02d} {:02d} {:02d} {:02d}".format( reg1>>8, reg1&0xff, reg2>>8, reg2&0xff ) 243 | elif item["length"] == 4: 244 | reg1 = int(rawdata.registers[offset]) 245 | reg2 = int(rawdata.registers[offset+1]) 246 | reg3 = int(rawdata.registers[offset+2]) 247 | reg4 = int(rawdata.registers[offset+3]) 248 | val = "{:02d} {:02d} {:02d} {:02d} {:02d} {:02d} {:02d} {:02d}".format( reg1>>8, reg1&0xff, reg2>>8, reg2&0xff, reg3>>8, reg3&0xff, reg4>>8, reg4&0xff ) 249 | elif item["type"] == 'BIT': 250 | if item["length"] == 1: 251 | reg1 = int(rawdata.registers[offset]) 252 | val = "{:08b}".format( reg1 ) 253 | if item["length"] == 2: 254 | reg1 = int(rawdata.registers[offset]) 255 | reg2 = int(rawdata.registers[offset+1]) 256 | val = "{:08b} {:08b}".format( reg1, reg2 ) 257 | elif item["type"] == 'DAT': 258 | reg1 = int(rawdata.registers[offset]) 259 | reg2 = int(rawdata.registers[offset+1]) 260 | reg3 = int(rawdata.registers[offset+2]) 261 | val = "{:02d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format( reg1>>8, reg1&0xff, reg2>>8, reg2&0xff, reg3>>8, reg3&0xff ) 262 | elif item["type"] == 'STR': 263 | reg = rawdata.registers[offset:offset+item["length"]*2+1] 264 | val = self.modbus_client.convert_from_registers(registers=reg, data_type=self.modbus_client.DATATYPE.STRING) 265 | else: 266 | logging.error("Unknown type {} to decode".format(item["type"])) 267 | return None 268 | 269 | if val and item["scale"] > 1: 270 | val /= item["scale"] 271 | data = { "name":item["name"], "value":val, "unit":item["unit"] } 272 | return data 273 | except Exception as ex: 274 | logging.error("Exception while decoding data: {}".format(ex)) 275 | return None 276 | 277 | #-------------------------------- 278 | # The main() function is just a demo code how to use the API 279 | def main(): 280 | logging.basicConfig() 281 | if cfg['DEBUG'] == True: 282 | logging.getLogger().setLevel(logging.DEBUG) 283 | 284 | api = MTECmodbusAPI() 285 | api.connect() 286 | 287 | # fetch all available data 288 | logging.info("Fetching all data") 289 | data = api.read_modbus_data() 290 | for param, val in data.items(): 291 | logging.info("- {} : {}".format(param, val)) 292 | 293 | api.disconnect() 294 | 295 | #-------------------------------------------- 296 | if __name__ == '__main__': 297 | main() 298 | -------------------------------------------------------------------------------- /templates/hass-dashboard.yaml: -------------------------------------------------------------------------------- 1 | title: MTEC Energybutler 2 | views: 3 | - title: Overview 4 | path: overview 5 | type: panel 6 | badges: [] 7 | cards: 8 | - type: picture-elements 9 | image: http://automation.fritz.box:8123/local/PV_background.png?v=1 10 | elements: 11 | - type: state-label 12 | entity: sensor.mtec_energybutler_pv_power 13 | style: 14 | top: 48% 15 | left: 33% 16 | font-size: calc(0.5em + 1.8vw) 17 | color: white 18 | - type: state-label 19 | entity: sensor.mtec_energybutler_grid_power 20 | style: 21 | top: 48% 22 | left: 72% 23 | font-size: calc(0.5em + 1.8vw) 24 | color: white 25 | - type: state-label 26 | entity: sensor.mtec_energybutler_battery_power 27 | style: 28 | top: 71% 29 | left: 57% 30 | font-size: calc(0.5em + 1.8vw) 31 | color: white 32 | - type: state-label 33 | entity: sensor.mtec_energybutler_household_consumption 34 | style: 35 | top: 35% 36 | left: 58% 37 | font-size: calc(0.5em + 1.8vw) 38 | color: white 39 | - type: state-label 40 | entity: sensor.mtec_energybutler_battery_soc 41 | style: 42 | top: 86% 43 | left: 51% 44 | font-size: calc(0.5em + 1.8vw) 45 | color: white 46 | - type: state-label 47 | entity: sensor.mtec_energybutler_household_consumption_day 48 | style: 49 | top: 9% 50 | left: 80% 51 | font-size: calc(0.5em + 1.8vw) 52 | color: white 53 | - type: state-label 54 | entity: sensor.mtec_energybutler_household_autarky_day 55 | style: 56 | top: 19% 57 | left: 80% 58 | font-size: calc(0.5em + 1.8vw) 59 | color: white 60 | - type: state-label 61 | entity: sensor.mtec_energybutler_pv_energy_generated_day 62 | style: 63 | top: 65% 64 | left: 17% 65 | font-size: calc(0.5em + 1.8vw) 66 | color: white 67 | - type: state-label 68 | entity: sensor.mtec_energybutler_pv_energy_generated_total 69 | style: 70 | top: 76% 71 | left: 18% 72 | font-size: calc(0.5em + 1.8vw) 73 | color: white 74 | - type: state-label 75 | entity: sensor.mtec_energybutler_grid_purchased_energy_day 76 | style: 77 | top: 74% 78 | left: 85% 79 | font-size: calc(0.5em + 1.8vw) 80 | color: white 81 | - type: state-label 82 | entity: sensor.mtec_energybutler_grid_injection_energy_day 83 | style: 84 | top: 82% 85 | left: 85% 86 | font-size: calc(0.5em + 1.8vw) 87 | color: white 88 | - type: state-label 89 | entity: sensor.mtec_energybutler_pv1_power 90 | style: 91 | top: 13% 92 | left: 10% 93 | font-size: calc(0.3em + 1.2vw) 94 | color: white 95 | - type: state-label 96 | entity: sensor.mtec_energybutler_pv2_power 97 | style: 98 | top: 13% 99 | left: 22% 100 | font-size: calc(0.3em + 1.2vw) 101 | color: white 102 | - type: state-label 103 | entity: sensor.mtec_energybutler_grid_frequency 104 | style: 105 | top: 58% 106 | left: 72% 107 | font-size: calc(0.3em + 1.2vw) 108 | color: white 109 | - path: statistics 110 | title: Statistics 111 | icon: '' 112 | type: sidebar 113 | badges: [] 114 | cards: 115 | - type: history-graph 116 | entities: 117 | - entity: sensor.mtec_energybutler_household_consumption 118 | name: Consumption 119 | - entity: sensor.mtec_energybutler_pv_power 120 | name: PV 121 | - entity: sensor.mtec_energybutler_grid_power 122 | name: Grid 123 | - entity: sensor.mtec_energybutler_battery_power 124 | name: Battery 125 | - type: horizontal-stack 126 | cards: 127 | - type: entities 128 | entities: 129 | - entity: sensor.mtec_energybutler_inverter_operation_mode 130 | name: Operation mode 131 | - entity: sensor.mtec_energybutler_inverter_status 132 | name: Inverter status 133 | - entity: binary_sensor.mtec_energybutler_grid_injection_limit_switch 134 | name: Grid injection switch 135 | - entity: sensor.mtec_energybutler_grid_injection_power_limit 136 | name: Grid injection limit 137 | - entity: binary_sensor.mtec_energybutler_on_grid_soc_limit_switch 138 | name: On-grid SOC switch 139 | - entity: sensor.mtec_energybutler_on_grid_soc_limit 140 | name: On-grid SOC limit 141 | - entity: binary_sensor.mtec_energybutler_off_grid_soc_limit_switch 142 | name: Off-grid SOC switch 143 | - entity: sensor.mtec_energybutler_off_grid_soc_limit 144 | name: Off-grid SOC limit 145 | title: Inverter status 146 | state_color: true 147 | show_header_toggle: false 148 | - type: entities 149 | entities: 150 | - entity: sensor.mtec_energybutler_battery_mode 151 | name: Battery mode 152 | - entity: sensor.mtec_energybutler_battery_soh 153 | name: Battery SOH 154 | - entity: sensor.mtec_energybutler_battery_temperature 155 | name: Battery temperature 156 | - entity: sensor.mtec_energybutler_battery_voltage 157 | name: Battery voltage 158 | - entity: sensor.mtec_energybutler_battery_current 159 | name: Battery current 160 | title: Battery status 161 | state_color: false 162 | show_header_toggle: false 163 | - type: horizontal-stack 164 | cards: 165 | - type: entities 166 | entities: 167 | - entity: sensor.mtec_energybutler_household_consumption 168 | name: Household 169 | - entity: sensor.mtec_energybutler_pv_power 170 | name: PV 171 | - entity: sensor.mtec_energybutler_grid_power 172 | name: Grid 173 | - entity: sensor.mtec_energybutler_battery_power 174 | name: Battery 175 | title: Current power consumption 176 | show_header_toggle: true 177 | - type: vertical-stack 178 | cards: 179 | - type: gauge 180 | entity: sensor.mtec_energybutler_battery_soc 181 | name: SOC 182 | needle: true 183 | segments: 184 | - from: 0 185 | color: '#db4437' 186 | - from: 30 187 | color: '#ffa600' 188 | - from: 70 189 | color: '#43a047' 190 | - type: history-graph 191 | name: SOC 192 | entities: 193 | - entity: sensor.mtec_energybutler_battery_soc 194 | - type: horizontal-stack 195 | cards: 196 | - type: entities 197 | entities: 198 | - entity: sensor.mtec_energybutler_household_consumption_day 199 | name: Houshold energy 200 | - entity: sensor.mtec_energybutler_backup_energy_day 201 | name: Backup energy 202 | - entity: sensor.mtec_energybutler_pv_energy_generated_day 203 | name: PV energy 204 | - entity: sensor.mtec_energybutler_grid_injection_energy_day 205 | name: Grid injection energy 206 | - entity: sensor.mtec_energybutler_grid_purchased_energy_day 207 | name: Grid purchased energy 208 | - entity: sensor.mtec_energybutler_battery_charge_energy_day 209 | name: Battery charge energy 210 | - entity: sensor.mtec_energybutler_battery_discharge_energy_day 211 | name: Battery discharge energy 212 | title: Today's energy 213 | - type: vertical-stack 214 | cards: 215 | - type: gauge 216 | entity: sensor.mtec_energybutler_household_autarky_day 217 | name: Autarky 218 | needle: true 219 | segments: 220 | - from: 0 221 | color: '#db4437' 222 | - from: 30 223 | color: '#ffa600' 224 | - from: 70 225 | color: '#43a047' 226 | - type: gauge 227 | entity: sensor.mtec_energybutler_own_consumption_rate_day 228 | name: Own consumption rate 229 | needle: true 230 | segments: 231 | - from: 0 232 | color: '#db4437' 233 | - from: 30 234 | color: '#ffa600' 235 | - from: 70 236 | color: '#43a047' 237 | - type: horizontal-stack 238 | cards: 239 | - type: entities 240 | entities: 241 | - entity: sensor.mtec_energybutler_household_consumption_total 242 | name: Houshold energy 243 | - entity: sensor.mtec_energybutler_backup_energy_total 244 | name: Backup energy 245 | - entity: sensor.mtec_energybutler_pv_energy_generated_total 246 | name: PV energy 247 | - entity: sensor.mtec_energybutler_grid_energy_injected_total 248 | name: Grid injection energy 249 | - entity: sensor.mtec_energybutler_grid_energy_purchased_total 250 | name: Grid purchased energy 251 | - entity: sensor.mtec_energybutler_battery_energy_charged_total 252 | name: Battery charge energy 253 | - entity: sensor.mtec_energybutler_battery_energy_discharged_total 254 | name: Battery discharge energy 255 | title: Total energy 256 | - type: vertical-stack 257 | cards: 258 | - type: gauge 259 | entity: sensor.mtec_energybutler_household_autarky_total 260 | name: Autarky 261 | needle: true 262 | segments: 263 | - from: 0 264 | color: '#db4437' 265 | - from: 30 266 | color: '#ffa600' 267 | - from: 70 268 | color: '#43a047' 269 | - type: gauge 270 | entity: sensor.mtec_energybutler_own_consumption_rate_total 271 | name: Own consumption rate 272 | needle: true 273 | segments: 274 | - from: 0 275 | color: '#db4437' 276 | - from: 30 277 | color: '#ffa600' 278 | - from: 70 279 | color: '#43a047' 280 | - title: Details 281 | path: details 282 | type: sidebar 283 | badges: [] 284 | cards: 285 | - type: horizontal-stack 286 | cards: 287 | - type: vertical-stack 288 | cards: 289 | - show_name: true 290 | show_icon: false 291 | show_state: true 292 | type: glance 293 | entities: 294 | - entity: sensor.mtec_energybutler_grid_power_phase_a 295 | name: L1 296 | - entity: sensor.mtec_energybutler_grid_power_phase_b 297 | name: L2 298 | - entity: sensor.mtec_energybutler_grid_power_phase_c 299 | name: L3 300 | title: Grid power 301 | columns: 3 302 | - show_name: false 303 | show_icon: false 304 | show_state: true 305 | type: glance 306 | entities: 307 | - entity: sensor.mtec_energybutler_inverter_ac_voltage_phase_a 308 | - entity: sensor.mtec_energybutler_inverter_ac_voltage_phase_b 309 | - entity: sensor.mtec_energybutler_inverter_ac_voltage_phase_c 310 | - entity: sensor.mtec_energybutler_inverter_ac_current_phase_a 311 | - entity: sensor.mtec_energybutler_inverter_ac_current_phase_b 312 | - entity: sensor.mtec_energybutler_inverter_ac_current_phase_c 313 | - entity: sensor.mtec_energybutler_inverter_power_phase_a 314 | - entity: sensor.mtec_energybutler_inverter_power_phase_b 315 | - entity: sensor.mtec_energybutler_inverter_power_phase_c 316 | columns: 3 317 | state_color: false 318 | title: Inverter AC 319 | - show_name: false 320 | show_icon: false 321 | show_state: true 322 | type: glance 323 | entities: 324 | - entity: sensor.mtec_energybutler_backup_voltage_phase_a 325 | - entity: sensor.mtec_energybutler_backup_voltage_phase_b 326 | - entity: sensor.mtec_energybutler_backup_voltage_phase_c 327 | - entity: sensor.mtec_energybutler_backup_current_phase_a 328 | - entity: sensor.mtec_energybutler_backup_current_phase_b 329 | - entity: sensor.mtec_energybutler_backup_current_phase_c 330 | - entity: sensor.mtec_energybutler_backup_power_phase_a 331 | - entity: sensor.mtec_energybutler_backup_power_phase_b 332 | - entity: sensor.mtec_energybutler_backup_power_phase_c 333 | - entity: sensor.mtec_energybutler_backup_frequency_phase_a 334 | - entity: sensor.mtec_energybutler_backup_frequency_phase_c 335 | - entity: sensor.mtec_energybutler_backup_frequency_phase_b 336 | title: Backup 337 | columns: 3 338 | - type: vertical-stack 339 | cards: 340 | - show_name: true 341 | show_icon: false 342 | show_state: true 343 | type: glance 344 | entities: 345 | - entity: sensor.mtec_energybutler_pv1_voltage 346 | name: PV1 (V) 347 | - entity: sensor.mtec_energybutler_pv1_current 348 | name: PV1 (A) 349 | - entity: sensor.mtec_energybutler_pv1_power 350 | name: PV1 (W) 351 | - entity: sensor.mtec_energybutler_pv2_voltage 352 | name: PV2 (V) 353 | - entity: sensor.mtec_energybutler_pv2_current 354 | name: PV2 (A) 355 | - entity: sensor.mtec_energybutler_pv2_power 356 | name: PV2 (W) 357 | title: PV 358 | columns: 3 359 | - show_name: true 360 | show_icon: false 361 | show_state: true 362 | type: glance 363 | entities: 364 | - entity: sensor.mtec_energybutler_battery_cell_temperature_min 365 | name: Temp min (°C) 366 | - entity: sensor.mtec_energybutler_battery_cell_temperature_max 367 | name: Temp max (°C) 368 | - entity: sensor.mtec_energybutler_battery_cell_voltage_min 369 | name: Volt min 370 | - entity: sensor.mtec_energybutler_battery_cell_voltage_max 371 | name: Volt max 372 | title: Battery cell details 373 | columns: 2 374 | - type: entities 375 | entities: 376 | - entity: sensor.mtec_energybutler_grid_frequency 377 | name: Grid frequency 378 | - entity: sensor.mtec_energybutler_inverter_temperature_sensor_1 379 | name: Inverter temp1 380 | - entity: sensor.mtec_energybutler_inverter_temperature_sensor_2 381 | name: Inverter temp2 382 | - entity: sensor.mtec_energybutler_inverter_temperature_sensor_3 383 | name: Inverter temp3 384 | - entity: sensor.mtec_energybutler_inverter_temperature_sensor_4 385 | name: Inverter temp4 386 | title: Inverter 387 | -------------------------------------------------------------------------------- /templates/hass-dashboard-blue.yaml: -------------------------------------------------------------------------------- 1 | title: MTEC Energybutler 2 | views: 3 | - title: Overview 4 | path: overview 5 | type: sidebar 6 | badges: [] 7 | cards: 8 | - type: picture-elements 9 | image: http://192.168.2.10:8123/local/s2.png 10 | elements: 11 | - type: state-label 12 | entity: sensor.mtec_energybutler_pv_power 13 | style: 14 | top: 47% 15 | left: 35% 16 | font-size: calc(0.5em + 1.7vw) 17 | font-weight: bold 18 | color: black 19 | - type: state-label 20 | entity: sensor.mtec_energybutler_grid_power 21 | style: 22 | top: 48% 23 | left: 72% 24 | font-size: calc(0.5em + 1.7vw) 25 | font-weight: bold 26 | color: black 27 | - type: state-label 28 | entity: sensor.mtec_energybutler_battery_power 29 | style: 30 | top: 69% 31 | left: 55% 32 | font-size: calc(0.5em + 1.8vw) 33 | font-weight: bold 34 | color: black 35 | - type: state-label 36 | entity: sensor.mtec_energybutler_household_consumption 37 | style: 38 | top: 37% 39 | left: 56% 40 | font-size: calc(0.5em + 1.8vw) 41 | font-weight: bold 42 | color: black 43 | - type: state-label 44 | entity: sensor.mtec_energybutler_battery_soc 45 | style: 46 | top: 90% 47 | left: 50% 48 | font-size: calc(0.4em + 1.6vw) 49 | font-weight: bold 50 | color: black 51 | - type: state-label 52 | entity: sensor.mtec_energybutler_household_consumption_day 53 | style: 54 | top: 8% 55 | left: 78% 56 | font-size: calc(0.5em + 1.8vw) 57 | color: black 58 | - type: state-label 59 | entity: sensor.mtec_energybutler_household_autarky_day 60 | style: 61 | top: 18% 62 | left: 77% 63 | font-size: calc(0.5em + 1.8vw) 64 | color: black 65 | - type: state-label 66 | entity: sensor.mtec_energybutler_pv_energy_generated_day 67 | style: 68 | top: 64% 69 | left: 17% 70 | font-size: calc(0.5em + 1.8vw) 71 | color: black 72 | - type: state-label 73 | entity: sensor.mtec_energybutler_pv_energy_generated_total 74 | style: 75 | top: 73% 76 | left: 17% 77 | font-size: calc(0.5em + 1.8vw) 78 | color: black 79 | - type: state-label 80 | entity: sensor.mtec_energybutler_grid_purchased_energy_day 81 | style: 82 | top: 74% 83 | left: 85% 84 | font-size: calc(0.5em + 1.8vw) 85 | color: black 86 | - type: state-label 87 | entity: sensor.mtec_energybutler_grid_injection_energy_day 88 | style: 89 | top: 82% 90 | left: 85% 91 | font-size: calc(0.5em + 1.8vw) 92 | color: black 93 | - type: state-label 94 | entity: sensor.mtec_energybutler_pv1_power 95 | style: 96 | top: 9% 97 | left: 11% 98 | font-size: calc(0.5em + 1.6vw) 99 | font-weight: bold 100 | color: black 101 | - type: state-label 102 | entity: sensor.mtec_energybutler_pv2_power 103 | style: 104 | top: 9% 105 | left: 25% 106 | font-size: calc(0.5em + 1.6vw) 107 | font-weight: bold 108 | color: black 109 | - type: state-label 110 | entity: sensor.mtec_energybutler_grid_frequency 111 | style: 112 | top: 54% 113 | left: 72% 114 | font-size: calc(0.3em + 1.2vw) 115 | font-weight: bold 116 | color: black 117 | - path: statistics 118 | title: Statistics 119 | icon: '' 120 | type: sidebar 121 | badges: [] 122 | cards: 123 | - type: history-graph 124 | entities: 125 | - entity: sensor.mtec_energybutler_household_consumption 126 | name: Consumption 127 | - entity: sensor.mtec_energybutler_pv_power 128 | name: PV 129 | - entity: sensor.mtec_energybutler_grid_power 130 | name: Grid 131 | - entity: sensor.mtec_energybutler_battery_power 132 | name: Battery 133 | - type: horizontal-stack 134 | cards: 135 | - type: entities 136 | entities: 137 | - entity: sensor.mtec_energybutler_inverter_operation_mode 138 | name: Operation mode 139 | - entity: sensor.mtec_energybutler_inverter_status 140 | name: Inverter status 141 | - entity: binary_sensor.mtec_energybutler_grid_injection_limit_switch 142 | name: Grid injection switch 143 | - entity: sensor.mtec_energybutler_grid_injection_power_limit 144 | name: Grid injection limit 145 | - entity: binary_sensor.mtec_energybutler_on_grid_soc_limit_switch 146 | name: On-grid SOC switch 147 | - entity: sensor.mtec_energybutler_on_grid_soc_limit 148 | name: On-grid SOC limit 149 | - entity: binary_sensor.mtec_energybutler_off_grid_soc_limit_switch 150 | name: Off-grid SOC switch 151 | - entity: sensor.mtec_energybutler_off_grid_soc_limit 152 | name: Off-grid SOC limit 153 | title: Inverter status 154 | state_color: true 155 | show_header_toggle: false 156 | - type: entities 157 | entities: 158 | - entity: sensor.mtec_energybutler_battery_mode 159 | name: Battery mode 160 | - entity: sensor.mtec_energybutler_battery_soh 161 | name: Battery SOH 162 | - entity: sensor.mtec_energybutler_battery_temperature 163 | name: Battery temperature 164 | - entity: sensor.mtec_energybutler_battery_voltage 165 | name: Battery voltage 166 | - entity: sensor.mtec_energybutler_battery_current 167 | name: Battery current 168 | title: Battery status 169 | state_color: false 170 | show_header_toggle: false 171 | - type: horizontal-stack 172 | cards: 173 | - type: entities 174 | entities: 175 | - entity: sensor.mtec_energybutler_household_consumption 176 | name: Household 177 | - entity: sensor.mtec_energybutler_pv_power 178 | name: PV 179 | - entity: sensor.mtec_energybutler_grid_power 180 | name: Grid 181 | - entity: sensor.mtec_energybutler_battery_power 182 | name: Battery 183 | title: Current power consumption 184 | show_header_toggle: true 185 | - type: vertical-stack 186 | cards: 187 | - type: gauge 188 | entity: sensor.mtec_energybutler_battery_soc 189 | name: SOC 190 | needle: true 191 | segments: 192 | - from: 0 193 | color: '#db4437' 194 | - from: 30 195 | color: '#ffa600' 196 | - from: 70 197 | color: '#43a047' 198 | - type: history-graph 199 | name: SOC 200 | entities: 201 | - entity: sensor.mtec_energybutler_battery_soc 202 | - type: horizontal-stack 203 | cards: 204 | - type: entities 205 | entities: 206 | - entity: sensor.mtec_energybutler_household_consumption_day 207 | name: Houshold energy 208 | - entity: sensor.mtec_energybutler_backup_energy_day 209 | name: Backup energy 210 | - entity: sensor.mtec_energybutler_pv_energy_generated_day 211 | name: PV energy 212 | - entity: sensor.mtec_energybutler_grid_injection_energy_day 213 | name: Grid injection energy 214 | - entity: sensor.mtec_energybutler_grid_purchased_energy_day 215 | name: Grid purchased energy 216 | - entity: sensor.mtec_energybutler_battery_charge_energy_day 217 | name: Battery charge energy 218 | - entity: sensor.mtec_energybutler_battery_discharge_energy_day 219 | name: Battery discharge energy 220 | title: Today's energy 221 | - type: vertical-stack 222 | cards: 223 | - type: gauge 224 | entity: sensor.mtec_energybutler_household_autarky_day 225 | name: Autarky 226 | needle: true 227 | segments: 228 | - from: 0 229 | color: '#db4437' 230 | - from: 30 231 | color: '#ffa600' 232 | - from: 70 233 | color: '#43a047' 234 | - type: gauge 235 | entity: sensor.mtec_energybutler_own_consumption_rate_day 236 | name: Own consumption rate 237 | needle: true 238 | segments: 239 | - from: 0 240 | color: '#db4437' 241 | - from: 30 242 | color: '#ffa600' 243 | - from: 70 244 | color: '#43a047' 245 | - type: horizontal-stack 246 | cards: 247 | - type: entities 248 | entities: 249 | - entity: sensor.mtec_energybutler_household_consumption_total 250 | name: Houshold energy 251 | - entity: sensor.mtec_energybutler_backup_energy_total 252 | name: Backup energy 253 | - entity: sensor.mtec_energybutler_pv_energy_generated_total 254 | name: PV energy 255 | - entity: sensor.mtec_energybutler_grid_energy_injected_total 256 | name: Grid injection energy 257 | - entity: sensor.mtec_energybutler_grid_energy_purchased_total 258 | name: Grid purchased energy 259 | - entity: sensor.mtec_energybutler_battery_energy_charged_total 260 | name: Battery charge energy 261 | - entity: sensor.mtec_energybutler_battery_energy_discharged_total 262 | name: Battery discharge energy 263 | title: Total energy 264 | - type: vertical-stack 265 | cards: 266 | - type: gauge 267 | entity: sensor.mtec_energybutler_household_autarky_total 268 | name: Autarky 269 | needle: true 270 | segments: 271 | - from: 0 272 | color: '#db4437' 273 | - from: 30 274 | color: '#ffa600' 275 | - from: 70 276 | color: '#43a047' 277 | - type: gauge 278 | entity: sensor.mtec_energybutler_own_consumption_rate_total 279 | name: Own consumption rate 280 | needle: true 281 | segments: 282 | - from: 0 283 | color: '#db4437' 284 | - from: 30 285 | color: '#ffa600' 286 | - from: 70 287 | color: '#43a047' 288 | - title: Details 289 | path: details 290 | type: sidebar 291 | badges: [] 292 | cards: 293 | - type: horizontal-stack 294 | cards: 295 | - type: vertical-stack 296 | cards: 297 | - show_name: true 298 | show_icon: false 299 | show_state: true 300 | type: glance 301 | entities: 302 | - entity: sensor.mtec_energybutler_grid_power_phase_a 303 | name: L1 304 | - entity: sensor.mtec_energybutler_grid_power_phase_b 305 | name: L2 306 | - entity: sensor.mtec_energybutler_grid_power_phase_c 307 | name: L3 308 | title: Grid power 309 | columns: 3 310 | - show_name: false 311 | show_icon: false 312 | show_state: true 313 | type: glance 314 | entities: 315 | - entity: sensor.mtec_energybutler_inverter_ac_voltage_phase_a 316 | - entity: sensor.mtec_energybutler_inverter_ac_voltage_phase_b 317 | - entity: sensor.mtec_energybutler_inverter_ac_voltage_phase_c 318 | - entity: sensor.mtec_energybutler_inverter_ac_current_phase_a 319 | - entity: sensor.mtec_energybutler_inverter_ac_current_phase_b 320 | - entity: sensor.mtec_energybutler_inverter_ac_current_phase_c 321 | - entity: sensor.mtec_energybutler_inverter_power_phase_a 322 | - entity: sensor.mtec_energybutler_inverter_power_phase_b 323 | - entity: sensor.mtec_energybutler_inverter_power_phase_c 324 | columns: 3 325 | state_color: false 326 | title: Inverter AC 327 | - show_name: false 328 | show_icon: false 329 | show_state: true 330 | type: glance 331 | entities: 332 | - entity: sensor.mtec_energybutler_backup_voltage_phase_a 333 | - entity: sensor.mtec_energybutler_backup_voltage_phase_b 334 | - entity: sensor.mtec_energybutler_backup_voltage_phase_c 335 | - entity: sensor.mtec_energybutler_backup_current_phase_a 336 | - entity: sensor.mtec_energybutler_backup_current_phase_b 337 | - entity: sensor.mtec_energybutler_backup_current_phase_c 338 | - entity: sensor.mtec_energybutler_backup_power_phase_a 339 | - entity: sensor.mtec_energybutler_backup_power_phase_b 340 | - entity: sensor.mtec_energybutler_backup_power_phase_c 341 | - entity: sensor.mtec_energybutler_backup_frequency_phase_a 342 | - entity: sensor.mtec_energybutler_backup_frequency_phase_c 343 | - entity: sensor.mtec_energybutler_backup_frequency_phase_b 344 | title: Backup 345 | columns: 3 346 | - type: vertical-stack 347 | cards: 348 | - show_name: true 349 | show_icon: false 350 | show_state: true 351 | type: glance 352 | entities: 353 | - entity: sensor.mtec_energybutler_pv1_voltage 354 | name: PV1 (V) 355 | - entity: sensor.mtec_energybutler_pv1_current 356 | name: PV1 (A) 357 | - entity: sensor.mtec_energybutler_pv1_power 358 | name: PV1 (W) 359 | - entity: sensor.mtec_energybutler_pv2_voltage 360 | name: PV2 (V) 361 | - entity: sensor.mtec_energybutler_pv2_current 362 | name: PV2 (A) 363 | - entity: sensor.mtec_energybutler_pv2_power 364 | name: PV2 (W) 365 | title: PV 366 | columns: 3 367 | - show_name: true 368 | show_icon: false 369 | show_state: true 370 | type: glance 371 | entities: 372 | - entity: sensor.mtec_energybutler_battery_cell_temperature_min 373 | name: Temp min (°C) 374 | - entity: sensor.mtec_energybutler_battery_cell_temperature_max 375 | name: Temp max (°C) 376 | - entity: sensor.mtec_energybutler_battery_cell_voltage_min 377 | name: Volt min 378 | - entity: sensor.mtec_energybutler_battery_cell_voltage_max 379 | name: Volt max 380 | title: Battery cell details 381 | columns: 2 382 | - type: entities 383 | entities: 384 | - entity: sensor.mtec_energybutler_grid_frequency 385 | name: Grid frequency 386 | - entity: sensor.mtec_energybutler_inverter_temperature_sensor_1 387 | name: Inverter temp1 388 | - entity: sensor.mtec_energybutler_inverter_temperature_sensor_2 389 | name: Inverter temp2 390 | - entity: sensor.mtec_energybutler_inverter_temperature_sensor_3 391 | name: Inverter temp3 392 | - entity: sensor.mtec_energybutler_inverter_temperature_sensor_4 393 | name: Inverter temp4 394 | title: Inverter 395 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # M-TEC MQTT 2 | 3 | ## Introduction 4 | Welcome to the `MTECmqtt` project! 5 | 6 | This project enables to read data from a M-TEC Energybutler (https://www.mtec-systems.com) system and write them to a MQTT broker. 7 | 8 | The highlights are: 9 | * No additional hardware or modifications of your Inverter required 10 | * Just install on any existing (micro-)server, e.g. Rasperry Pi or NAS server 11 | * Works within you LAN - no internet connection required 12 | * Supports more than 80 parameters 13 | * Clustered reading of sequential register to reduce modbus traffic and load 14 | * Enables frequent polling of data (e.g. every 10s) 15 | * MQTT enables an easy integration into almost any EMS or home automation tool 16 | * Home Assistant (https://www.home-assistant.io) auto discovery via MQTT 17 | * Home Assistant demo dashboard included 18 | * Easy and prepared integration into evcc (https://evcc.io), which enables PV surplus charging 19 | 20 | I hope you like it and it will help you with for your EMS or home automation project :-) ! 21 | 22 | ### Disclaimer 23 | This project is a pure hobby project which I created by reverse-engineering different internet sources and my M-TEC Energybutler. It is *not* related to or supported by M-TEC GmbH by any means. 24 | 25 | Usage is completely on you own risk. I don't take any responsibility on functionality or potential damage. 26 | 27 | ### Credits 28 | This project would not have been possible without the really valuable pre-work of other people, especially: 29 | * https://www.photovoltaikforum.com/thread/206243-erfahrungen-mit-m-tec-energy-butler-hybrid-wechselrichter 30 | * https://forum.iobroker.net/assets/uploads/files/1681811699113-20221125_mtec-energybutler_modbus_rtu_protkoll.pdf 31 | * https://smarthome.exposed/wattsonic-hybrid-inverter-gen3-modbus-rtu-protocol 32 | * The Home Assistant "blue theme" background was thankfully provided by Enrico from redK! Webdesign & Content Management 33 | 34 | ### Compatibility 35 | The project was developed using my `M-TEC Energybutler 8kW-3P-3G25`, but I assume that it will also work with other Energybutler GEN3 versions (https://www.mtec-systems.com/batteriespeicher/energy-butler-11-bis-30-kwh/). 36 | 37 | It seems that there are at least three more Inverter products on the market which share the same (or at least a very similar) Chinese firmware. It *might* be that this API also works with these products. But since I do not have access to any of them, this is just a guess and up to you and your own risk to try it. 38 | 39 | | Provider | Link 40 | |---------- | -------------------------------------- 41 | | Wattsonic | https://www.wattsonic.com/ | 42 | | Sunways | https://de.sunways-tech.com | 43 | | Daxtromn | https://daxtromn-power.com/products/ | 44 | 45 | 46 | ## Setup & configuration 47 | ### Prerequisites 48 | The MTECmqtt project connects to the espressif Modbus server of you M-TEC inverter, retrieves relevant data, and writes them to a MQTT broker (https://mqtt.org/) of your choice. MQTT provides a light-weight publish/subscribe model which is widely used for Internet of Things messaging. MQTT connectivity is implemented in many EMS or home automation tools. 49 | That means, you obviously require a MQTT server. 50 | If you don't have one yet, you might want to try https://mosquitto.org/. 51 | You can easily install it like this: 52 | 53 | ``` 54 | sudo apt install mosquitto mosquitto-clients 55 | ``` 56 | 57 | ### Installation 58 | The basic installation requires only following 4 steps: 59 | 60 | (1) Check your Python installation 61 | ``` 62 | python -V 63 | ``` 64 | If this returns something like `Python 3.8.x`, you can just continue with the next step. 65 | 66 | If it return `command not found`, or `Python 2.x`, try 67 | ``` 68 | python3 -V 69 | ``` 70 | If this returns something like `Python 3.8.x`, remember that you need to use `python3` and continue. 71 | 72 | If both command return `command not found`, you need to install python3 on you system before you can continue 73 | 74 | 75 | (2) Create a new directory for the installation (e.g. within your HOME directory) 76 | ``` 77 | mkdir mtecmqtt && cd mtecmqtt 78 | ``` 79 | 80 | (3) Create and activate a virtual python environment for the project 81 | ``` 82 | python -m venv . && source bin/activate 83 | ``` 84 | If you remembered you require `python3`, use this instead: 85 | ``` 86 | python3 -m venv . && source bin/activate 87 | ``` 88 | 89 | 90 | (4) Install the MTECmqtt project from github 91 | ``` 92 | pip install https://github.com/croedel/MTECmqtt/archive/refs/heads/main.zip 93 | ``` 94 | 95 | As a next step, we can try to start the MQTT server. 96 | ``` 97 | mtec_mqtt 98 | ``` 99 | Starting it for the first time will create your `config.yaml` file. Please review and (if necessary) adapt it you needs (see below for details). 100 | 101 | Subsequent invocations will run the actual server. It will print out some debug info, so you can see what it does. 102 | 103 | You can stop the service by pressing CTRL-C or sending a SIGHUB. This will initiate a graceful shutdown. Please be patient - this might take a few seconds. 104 | 105 | Starting the service in a shell - as we just did - will not create a permanent running service and is probably only useful for testing. If you want a permanently running service, you need to install a systemd autostart script for `mtec_mytt.py`. The following command does this job: 106 | ``` 107 | sudo bin/install_systemd_service.sh 108 | ``` 109 | 110 | To check if the service is running smoothly, you can execute: 111 | ``` 112 | sudo systemctl status mtec_mqtt 113 | ``` 114 | 115 | ### Advanced configuration 116 | This section give you more information about all configuration options. But don't be afraid - it should only be relevant for advanced use cases. 117 | 118 | The installer will create a `config.yaml` file in the default location of your OS. 119 | For a Linux system it's probably `~/.config/mtecmqtt/config.yaml`, on Windowns something like `C:\Users\xxxxx\AppData\Roaming\config.yaml` 120 | 121 | #### Connect your M-TEC Inverter 122 | In order to connect to your Inverter, you need the IP address or internal hostname of your `espressif` device. 123 | If you run a FRITZ!Box, the pre-configured internal hostname `espressif.fritz.box` will probably already work out-of-the-box. 124 | Else you can easily adjust it like this: 125 | 1. Login to your internet router 126 | 2. Look for the list of connected devices 127 | 3. You should find a devices called `espressif` 128 | 4. Copy the IPv4 address or internal hostname of this device to `config.yaml` file as value for `MODBUS_IP`. 129 | 130 | You probably don't need to change any of the other `MODBUS_` config values. 131 | 132 | _IMPORTANT:_ M-TEC changed their Modbus port with firmware V27.52.4.0. If you run that version or a newer one, you need to change the `MODBUS_PORT` in the `config.yaml` to 502 ! 133 | 134 | ``` 135 | # MODBUS Server 136 | MODBUS_IP : espressif.fritz.box # IP address / hostname of "espressif" modbus server 137 | MODBUS_PORT : 5743 # Port (IMPORTANT: you need to change this to 502 for firmware versions newer than 27.52.4.0) 138 | MODBUS_SLAVE : 252 # Modbus slave id (usually no change required) 139 | MODBUS_TIMEOUT : 5 # Timeout for Modbus server (s) 140 | MODBUS_FRAMER: rtu # Modbus Framer (usually no change required; options: 'ascii', 'binary', 'rtu', 'socket', 'tls') 141 | ``` 142 | 143 | Hint for advanced users: If you run an external modbus adapter, connected e.g. to the EMS bus of the MTEC inverter, you might require to change the `MODBUS_FRAMER`. 144 | 145 | #### Connect you MQTT broker 146 | The `MQTT_` parameters in `config.yaml` define the connection to your MQTT server. 147 | 148 | ``` 149 | MQTT_SERVER : localhost # MQTT server 150 | MQTT_PORT : 1883 # MQTT server port 151 | MQTT_LOGIN : " " # MQTT Login 152 | MQTT_PASSWORD : "" # MQTT Password 153 | MQTT_TOPIC : "MTEC" # MQTT topic name 154 | ``` 155 | 156 | The other values of the `config.yaml` you probably don't need to change as of now. 157 | 158 | That's already all you need to do and you are ready to go! 159 | 160 | #### More configuration options 161 | The `REFRESH_` parameters define how frequently the data gets fetched from your Inverter 162 | 163 | ``` 164 | REFRESH_NOW : 10 # Refresh current data every N seconds 165 | REFRESH_DAY : 300 # Refresh daily statistic every N seconds 166 | REFRESH_TOTAL : 300 # Refresh total statistic every N seconds 167 | REFRESH_CONFIG : 3600 # Refresh config data every N seconds 168 | ``` 169 | 170 | ### Home Assistant support 171 | `mtec_mqtt` provides Home Assistant (https://www.home-assistant.io) auto-discovery, which means that Home Assistant will automatically detect and configure your MTEC Inverter. 172 | 173 | If you want to enable Home Assistant support, set `HASS_ENABLE: True` in `config.yaml`. 174 | 175 | ``` 176 | # Home Assistent 177 | HASS_ENABLE : True # Enable home assistant support 178 | HASS_BASE_TOPIC : homeassistant # Basis MQTT topic of home assistant 179 | HASS_BIRTH_GRACETIME : 15 # Give HASS some time to get ready after the birth message was received 180 | ``` 181 | 182 | As next step, you need to enable and configure the MQTT integration within Home Assistant. After that, the auto discovery should do it's job and the Inverter sensors should appear on your dashboard. 183 | 184 | If you want, you can use and install one of the Home Assistant dashboards in `templates` for a nice data visualization. 185 | The map view requires to install a background image. To do so, create a sub-directory called `www` in the `config` directory of your Home Assistant installation (e.g. `/home/homeassistant/.homeassistant/www/`) and copy the image to this directory. 186 | 187 | There are two versions you can chose from: 188 | | Theme | Dashboard | Image 189 | |-------------|-------------------- | --------------------- 190 | | Dark theme | hass-dashboard.yaml | PV_background.png 191 | | Blue theme | hass-dashboard-blue.yaml | PV_background-blue.png 192 | 193 | ### evcc support 194 | If you want to integrate the data into evcc (https://evcc.io), you might want to have a look at the `evcc.yaml` snippet in the `templates` directory. It shows how to define and use the MTEC `meters`, provided in MQTT. 195 | Please don't forget to replace `` with the actual serial no of your Inverter. 196 | 197 | ## Data format written to MQTT 198 | The exported data will be written to several MQTT topics. The topic path includes the serial number of your Inverter. 199 | 200 | | Sub-topic | Refresh frequency | Description 201 | |---------------------------------- | -------------------------- | ---------------------------------------------- 202 | | MTEC//config | `REFRESH_CONFIG` seconds | Relatively static config values 203 | | MTEC//now-base | `REFRESH_NOW` seconds | Current base data 204 | | MTEC//now-grid | `REFRESH_NOWEXT` seconds | Current extended grid data 205 | | MTEC//now-inverter | `REFRESH_NOWEXT` seconds | Current extended inverter data 206 | | MTEC//now-backup | `REFRESH_NOWEXT` seconds | Current extended backup data 207 | | MTEC//now-battery | `REFRESH_NOWEXT` seconds | Current extended battery data 208 | | MTEC//now-pv | `REFRESH_NOWEXT` seconds | Current extended PV data 209 | | MTEC//day | `REFRESH_DAY` seconds | Daily statistics 210 | | MTEC//total | `REFRESH_TOTAL` seconds | Lifetime statistics 211 | 212 | All `float` values will be written according to the configured `MQTT_FLOAT_FORMAT`. The default is a format with 3 decimal digits. 213 | 214 | This diagram tries to visualize the power flow values and directions: (at least from my understanding) 215 |
216 |      + ->               + ->                    + -> 
217 | PV  -------  inverter  ------- power connector ------- grid                
218 |               |    |                |
219 |             ^ |    | +            + |
220 |             + |    | v            v |
221 | Battery -------    |                --------- house
222 |                    -------------------------- backup power
223 | 
224 | 225 | *Remark:* Some parameters - marked by `(*)` - are calculated values. 226 | 227 | ### config 228 | | Register | MQTT Parameter | Unit | Description 229 | | -------- | ---------------------- | ---- | ---------------------------------------------- 230 | | 10000 | serial_no | | Inverter serial number 231 | | 10011 | firmware_version | | Firmware version 232 | | 25100 | grid_inject_switch | | Grid injection limit switch 233 | | 25103 | grid_inject_limit | % | Grid injection power limit 234 | | 52502 | on_grid_soc_switch | | On-grid SOC limit switch 235 | | 52503 | on_grid_soc_limit | % | On-grid SOC limit 236 | | 52504 | off_grid_soc_switch | | Off-grid SOC limit switch 237 | | 52505 | off_grid_soc_limit | % | Off-grid SOC limit 238 | | | api_date | | Local date of MTECmqtt server 239 | 240 | ### now-base 241 | | Register | MQTT Parameter | Unit | Description 242 | | -------- | ---------------------- | ---- | ---------------------------------------------- 243 | | 10100 | inverter_date | | Inverter date 244 | | 10105 | inverter_status | | Inverter status (0=wait for on-grid, 1=self-check, 2=on-grid, 3=fault, 4=firmware update, 5=off grid) 245 | | 11000 | grid_power | W | Grid power 246 | | 11016 | inverter | W | Inverter AC power 247 | | 11028 | pv | W | PV power 248 | | 30230 | backup | W | Backup power total 249 | | 30254 | battery_voltage | V | Battery voltage 250 | | 30255 | battery_current | A | Battery current 251 | | 30256 | battery_mode | | Battery mode (0=Discharge, 1=Charge) 252 | | 30258 | battery | W | Battery power 253 | | 33000 | battery_soc | % | Battery SOC 254 | | 50000 | mode | | Inverter operation mode (257=General mode, 258=Economic mode, 259=UPS mode, 512=Off grid 771=Manual mode) 255 | | | consumption | W | Household consumption (*) 256 | 257 | ### now-backup 258 | | Register | MQTT Parameter | Unit | Description 259 | | -------- | ---------------------- | ---- | ---------------------------------------------- 260 | | 30200 | backup_voltage_a | V | Backup voltage phase A 261 | | 30201 | backup_current_a | A | Backup current phase A 262 | | 30202 | backup_frequency_a | Hz | Backup frequency phase A 263 | | 30204 | backup_a | W | Backup power phase A 264 | | 30210 | backup_voltage_b | V | Backup voltage phase B 265 | | 30211 | backup_current_b | A | Backup current phase B 266 | | 30212 | backup_frequency_b | Hz | Backup frequency phase B 267 | | 30214 | backup_b | W | Backup power phase B 268 | | 30220 | backup_voltage_c | V | Backup voltage phase C 269 | | 30221 | backup_current_c | A | Backup current phase C 270 | | 30222 | backup_frequency_c | Hz | Backup frequency phase C 271 | | 30224 | backup_c | W | Backup power phase C 272 | 273 | ### now-battery 274 | | Register | MQTT Parameter | Unit | Description 275 | | -------- | ---------------------- | ---- | ---------------------------------------------- 276 | | 33001 | battery_soh | % | Battery SOH 277 | | 33003 | battery_temp | ℃ | Battery temperature 278 | | 33009 | battery_cell_t_max | ℃ | Battery cell temperature max. 279 | | 33011 | battery_cell_t_min | ℃ | Battery cell temperature min. 280 | | 33013 | battery_cell_v_max | V | Battery cell voltage max. 281 | | 33015 | battery_cell_v_min | V | Battery cell voltage min. 282 | 283 | ### now-grid 284 | | Register | MQTT Parameter | Unit | Description 285 | | -------- | ---------------------- | ---- | ---------------------------------------------- 286 | | 10994 | grid_a | W | Grid power phase A 287 | | 10996 | grid_b | W | Grid power phase B 288 | | 10998 | grid_c | W | Grid power phase C 289 | | 11006 | ac_voltage_a_b | V | Inverter AC voltage lines A/B 290 | | 11007 | ac_voltage_b_c | V | Inverter AC voltage lines B/C 291 | | 11008 | ac_voltage_c_a | V | Inverter AC voltage lines C/A 292 | | 11009 | ac_voltage_a | V | Inverter AC voltage phase A 293 | | 11010 | ac_current_a | A | Inverter AC current phase A 294 | | 11011 | ac_voltage_b | V | Inverter AC voltage phase B 295 | | 11012 | ac_current_b | A | Inverter AC current phase B 296 | | 11013 | ac_voltage_c | V | Inverter AC voltage phase C 297 | | 11014 | ac_current_c | A | Inverter AC current phase C 298 | | 11015 | ac_fequency | Hz | Inverter AC frequency 299 | 300 | ### now-inverter 301 | | Register | MQTT Parameter | Unit | Description 302 | | -------- | ---------------------- | ---- | ---------------------------------------------- 303 | | 11032 | inverter_temp1 | ℃ | Temperature Sensor 1 304 | | 11033 | inverter_temp2 | ℃ | Temperature Sensor 2 305 | | 11034 | inverter_temp3 | ℃ | Temperature Sensor 3 306 | | 11035 | inverter_temp4 | ℃ | Temperature Sensor 4 307 | | 30236 | inverter_a | W | Inverter power phase A 308 | | 30242 | inverter_b | W | Inverter power phase B 309 | | 30248 | inverter_c | W | Inverter power phase C 310 | 311 | ### now-pv 312 | | Register | MQTT Parameter | Unit | Description 313 | | -------- | ---------------------- | ---- | ---------------------------------------------- 314 | | 11022 | pv_generation_duration | h | PV generation time total 315 | | 11038 | pv_voltage_1 | V | PV1 voltage 316 | | 11039 | pv_current_1 | A | PV1 current 317 | | 11040 | pv_voltage_2 | V | PV2 voltage 318 | | 11041 | pv_current_2 | A | PV2 current 319 | | 11062 | pv_1 | W | PV1 power 320 | | 11064 | pv_2 | W | PV2 power 321 | 322 | ### day 323 | | Register | MQTT Parameter | Unit | Description 324 | | -------- | ---------------------- | ---- | ---------------------------------------------- 325 | | 31000 | grid_feed_day | kWh | Grid injection energy (day) 326 | | 31001 | grid_purchase_day | kWh | Grid purchased energy (day) 327 | | 31002 | backup_day | kWh | Backup energy (day) 328 | | 31003 | battery_charge_day | kWh | Battery charge energy (day) 329 | | 31004 | battery_discharge_day | kWh | Battery discharge energy (day) 330 | | 31005 | pv_day | kWh | PV energy generated (day) 331 | | | autarky_rate_day | % | Household autarky (day) (*) 332 | | | consumption_day | kWh | Household energy consumed (day) (*) 333 | | | own_consumption_day | % | Own consumption rate (day) (*) 334 | 335 | ### total 336 | | Register | MQTT Parameter | Unit | Description 337 | | -------- | ---------------------- | ---- | ---------------------------------------------- 338 | | 31102 | grid_feed_total | kWh | Grid energy injected (total) 339 | | 31104 | grid_purchase_total | kWh | Grid energy purchased (total) 340 | | 31106 | backup_total | kWh | Backup energy (total) 341 | | 31108 | battery_charge_total | kWh | Battery energy charged (total) 342 | | 31110 | battery_discharge_total | kWh | Battery energy discharged (total) 343 | | 31112 | pv_total | kWh | PV energy generated (total) 344 | | | autarky_rate_total | % | Household autarky (total) (*) 345 | | | consumption_total | kWh | Household energy consumed (total) (*) 346 | | | own_consumption_total | % | Own consumption rate (total) (*) 347 | 348 | 349 | ## What else you can find in the project? 350 | 351 | ### Modbus Utility 352 | `mtec_util` is an small inteative tool which enable to list the supported parameters and read and write registers of your Inverter. 353 | You can choose between: 354 | 355 | * 1: List all known registers 356 | * 2: List register configuration by groups 357 | * 3: Read register group from Inverter 358 | * 4: Read single register from Inverter 359 | * 5: Write register to Inverter 360 | 361 | (1) lists all know registers. This includes the ones which are written to MQTT as listed above. You will find a few more registers, which are not mapped to MQTT (=no value in "mqtt") - mostly because I'm not sure if they are reliable or what they really mean. 362 | 363 | (2) will give you a list of all mapped registers, similar to the one listed above. 364 | 365 | (3) allows you to read the current values of all registers or of a certain group from your Inverter. 366 | 367 | (4) allows you to read a sinfle register from your Inverter 368 | 369 | (5) enables you to write a value to a register of your Inverter. WARNING: Be careful when writing data to your Inverter! This is definitively at your own risk! 370 | 371 | ### Commandline export tool 372 | The command-line tool `mtec_export` offers functionality to read data from your Inverter using Modbus and export it in various combinations and formats. 373 | 374 | As default, it will connect to your device and retrieve a list of all known Modbus registers in a human readable format. 375 | 376 | By specifying commandline parameters, you can: 377 | * Specify a register group (e.g. `-g config`) or "all" (`-g all`) to export of all registers 378 | * Provide a customize list of Modbus registers which you would like to retrieve, e.g. `-r "33000,10105,11000"` 379 | * Request to export CSV instead of human readable (`-c`) 380 | * Write output to a file (`-f FILENAME`) 381 | -------------------------------------------------------------------------------- /src/mtecmqtt/registers.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Template with all supported parameters 3 | #"10000": 4 | # name: Inverter serial number 5 | # length: 8 6 | # type: STR 7 | # unit: "%" 8 | # scale: 10 9 | # writable: True 10 | # mqtt: serial_no 11 | # group: config 12 | # hass_component_type: sensor 13 | # hass_device_class: battery 14 | # hass_value_template: "{{ value | round(1) }}" 15 | # hass_state_class: measurement 16 | 17 | 18 | #------------------------------------------------------------------ 19 | # Calculated pseudo-registers 20 | 21 | "consumption": 22 | name: Household consumption 23 | unit: W 24 | mqtt: consumption 25 | group: now-base 26 | hass_device_class: power 27 | hass_value_template: "{{ value | round(0) }}" 28 | hass_state_class: measurement 29 | 30 | "consumption-day": 31 | name: Household consumption (day) 32 | unit: kWh 33 | mqtt: consumption_day 34 | group: day 35 | hass_device_class: energy 36 | hass_value_template: "{{ value | round(1) }}" 37 | hass_state_class: total_increasing 38 | 39 | "autarky-day": 40 | name: Household autarky (day) 41 | unit: "%" 42 | mqtt: autarky_rate_day 43 | group: day 44 | hass_device_class: power_factor 45 | hass_value_template: "{{ value | round(1) }}" 46 | hass_state_class: measurement 47 | 48 | "ownconsumption-day": 49 | name: Own consumption rate (day) 50 | unit: "%" 51 | mqtt: own_consumption_day 52 | group: day 53 | hass_device_class: power_factor 54 | hass_value_template: "{{ value | round(1) }}" 55 | hass_state_class: measurement 56 | 57 | "consumption-total": 58 | name: Household consumption (total) 59 | unit: kWh 60 | mqtt: consumption_total 61 | group: total 62 | hass_device_class: energy 63 | hass_value_template: "{{ value | round(0) }}" 64 | hass_state_class: total_increasing 65 | 66 | "autarky-total": 67 | name: Household autarky (total) 68 | unit: "%" 69 | mqtt: autarky_rate_total 70 | group: total 71 | hass_device_class: power_factor 72 | hass_value_template: "{{ value | round(1) }}" 73 | hass_state_class: measurement 74 | 75 | "ownconsumption-total": 76 | name: Own consumption rate (total) 77 | unit: "%" 78 | mqtt: own_consumption_total 79 | group: total 80 | hass_device_class: power_factor 81 | hass_value_template: "{{ value | round(1) }}" 82 | hass_state_class: measurement 83 | 84 | "api-date": 85 | name: API date 86 | mqtt: api_date 87 | group: now-base 88 | 89 | #------------------------------------------------------------------ 90 | # Modbus registers 91 | "10000": 92 | name: Inverter serial number 93 | length: 8 94 | type: STR 95 | mqtt: serial_no 96 | group: config 97 | 98 | #"10008": 99 | # name: Equipment info 100 | # length: 1 101 | # type: BYTE 102 | # group: config 103 | 104 | "10011": 105 | name: Firmware version 106 | length: 4 107 | type: BYTE 108 | mqtt: firmware_version 109 | group: config 110 | 111 | "10100": 112 | name: Inverter date 113 | length: 3 114 | type: DAT 115 | mqtt: inverter_date 116 | group: now-base 117 | 118 | "10105": 119 | name: Inverter status 120 | length: 1 121 | type: U16 122 | mqtt: inverter_status 123 | group: now-base 124 | hass_device_class: enum 125 | hass_value_template: "{% if value=='0' %}wait for on-grid{% elif value=='1' %}self-check{% elif value=='2' %}on-grid{% elif value=='3' %}fault{% elif value=='4' %}firmware update{% elif value=='5' %}off grid{% else %}Unknown{% endif %}" 126 | 127 | #"10112": 128 | # name: Fault flag1 129 | # length: 2 130 | # type: BIT 131 | 132 | #"10114": 133 | # name: Fault flag2 134 | # length: 2 135 | # type: BIT 136 | 137 | #"10120": 138 | # name: Fault flag3 139 | # length: 2 140 | # type: BIT 141 | 142 | "10994": 143 | name: Grid power phase A 144 | length: 2 145 | type: I32 146 | unit: W 147 | mqtt: grid_a 148 | group: now-grid 149 | hass_device_class: power 150 | hass_value_template: "{{ value | round(0) }}" 151 | hass_state_class: measurement 152 | 153 | "10996": 154 | name: Grid power phase B 155 | length: 2 156 | type: I32 157 | unit: W 158 | mqtt: grid_b 159 | group: now-grid 160 | hass_device_class: power 161 | hass_value_template: "{{ value | round(0) }}" 162 | hass_state_class: measurement 163 | 164 | "10998": 165 | name: Grid power phase C 166 | length: 2 167 | type: I32 168 | unit: W 169 | mqtt: grid_c 170 | group: now-grid 171 | hass_device_class: power 172 | hass_value_template: "{{ value | round(0) }}" 173 | hass_state_class: measurement 174 | 175 | "11000": 176 | name: Grid power 177 | length: 2 178 | type: I32 179 | unit: W 180 | mqtt: grid_power 181 | group: now-base 182 | hass_device_class: power 183 | hass_value_template: "{{ value | round(0) }}" 184 | hass_state_class: measurement 185 | 186 | "11006": 187 | name: Inverter AC voltage lines A/B 188 | length: 1 189 | type: U16 190 | unit: V 191 | scale: 10 192 | mqtt: ac_voltage_a_b 193 | group: now-grid 194 | hass_device_class: voltage 195 | hass_value_template: "{{ value | round(1) }}" 196 | hass_state_class: measurement 197 | 198 | "11007": 199 | name: Inverter AC voltage lines B/C 200 | length: 1 201 | type: U16 202 | unit: V 203 | scale: 10 204 | mqtt: ac_voltage_b_c 205 | group: now-grid 206 | hass_device_class: voltage 207 | hass_value_template: "{{ value | round(1) }}" 208 | hass_state_class: measurement 209 | 210 | "11008": 211 | name: Inverter AC voltage lines C/A 212 | length: 1 213 | type: U16 214 | unit: V 215 | scale: 10 216 | mqtt: ac_voltage_c_a 217 | group: now-grid 218 | hass_device_class: voltage 219 | hass_value_template: "{{ value | round(1) }}" 220 | hass_state_class: measurement 221 | 222 | "11009": 223 | name: Inverter AC voltage phase A 224 | length: 1 225 | type: U16 226 | unit: V 227 | scale: 10 228 | mqtt: ac_voltage_a 229 | group: now-grid 230 | hass_device_class: voltage 231 | hass_value_template: "{{ value | round(1) }}" 232 | hass_state_class: measurement 233 | 234 | "11010": 235 | name: Inverter AC current phase A 236 | length: 1 237 | type: U16 238 | unit: A 239 | scale: 10 240 | mqtt: ac_current_a 241 | group: now-grid 242 | hass_device_class: current 243 | hass_value_template: "{{ value | round(1) }}" 244 | hass_state_class: measurement 245 | 246 | "11011": 247 | name: Inverter AC voltage phase B 248 | length: 1 249 | type: U16 250 | unit: V 251 | scale: 10 252 | mqtt: ac_voltage_b 253 | group: now-grid 254 | hass_device_class: voltage 255 | hass_value_template: "{{ value | round(1) }}" 256 | hass_state_class: measurement 257 | 258 | "11012": 259 | name: Inverter AC current phase B 260 | length: 1 261 | type: U16 262 | unit: A 263 | scale: 10 264 | mqtt: ac_current_b 265 | group: now-grid 266 | hass_device_class: current 267 | hass_value_template: "{{ value | round(1) }}" 268 | hass_state_class: measurement 269 | 270 | "11013": 271 | name: Inverter AC voltage phase C 272 | length: 1 273 | type: U16 274 | unit: V 275 | scale: 10 276 | mqtt: ac_voltage_c 277 | group: now-grid 278 | hass_device_class: voltage 279 | hass_value_template: "{{ value | round(1) }}" 280 | hass_state_class: measurement 281 | 282 | "11014": 283 | name: Inverter AC current phase C 284 | length: 1 285 | type: U16 286 | unit: A 287 | scale: 10 288 | mqtt: ac_current_c 289 | group: now-grid 290 | hass_device_class: current 291 | hass_value_template: "{{ value | round(1) }}" 292 | hass_state_class: measurement 293 | 294 | "11015": 295 | name: Grid frequency 296 | length: 1 297 | type: U16 298 | unit: Hz 299 | scale: 100 300 | mqtt: grid_fequency 301 | group: now-grid 302 | hass_device_class: frequency 303 | hass_value_template: "{{ value | round(2) }}" 304 | hass_state_class: measurement 305 | 306 | "11016": 307 | name: Inverter AC power 308 | length: 2 309 | type: I32 310 | unit: W 311 | mqtt: inverter 312 | group: now-base 313 | hass_device_class: power 314 | hass_value_template: "{{ value | round(0) }}" 315 | hass_state_class: measurement 316 | 317 | #"11018": 318 | # name: PV generation on that day 319 | # length: 2 320 | # type: U32 321 | # unit: kWh 322 | # scale: 10 323 | 324 | #"11020": 325 | # name: PV generation total 326 | # length: 2 327 | # type: U32 328 | # unit: kWh 329 | # scale: 10 330 | 331 | "11022": 332 | name: PV generation time total 333 | length: 2 334 | type: U32 335 | unit: h 336 | mqtt: pv_generation_duration 337 | group: now-pv 338 | hass_device_class: duration 339 | hass_value_template: "{{ value }}" 340 | hass_state_class: measurement 341 | 342 | "11028": 343 | name: PV power 344 | length: 2 345 | type: U32 346 | unit: W 347 | scale: 1 348 | mqtt: pv 349 | group: now-base 350 | hass_device_class: power 351 | hass_value_template: "{{ value | round(0) }}" 352 | hass_state_class: measurement 353 | 354 | "11032": 355 | name: Inverter temperature sensor 1 356 | length: 1 357 | type: I16 358 | unit: "°C" 359 | scale: 10 360 | mqtt: inverter_temp1 361 | group: now-inverter 362 | hass_device_class: temperature 363 | hass_value_template: "{{ value | round(1) }}" 364 | hass_state_class: measurement 365 | 366 | "11033": 367 | name: Inverter temperature sensor 2 368 | length: 1 369 | type: I16 370 | unit: "°C" 371 | scale: 10 372 | mqtt: inverter_temp2 373 | group: now-inverter 374 | hass_device_class: temperature 375 | hass_value_template: "{{ value | round(1) }}" 376 | hass_state_class: measurement 377 | 378 | "11034": 379 | name: Inverter temperature sensor 3 380 | length: 1 381 | type: I16 382 | unit: "°C" 383 | scale: 10 384 | mqtt: inverter_temp3 385 | group: now-inverter 386 | hass_device_class: temperature 387 | hass_value_template: "{{ value | round(1) }}" 388 | hass_state_class: measurement 389 | 390 | "11035": 391 | name: Inverter temperature sensor 4 392 | length: 1 393 | type: I16 394 | unit: "°C" 395 | scale: 10 396 | mqtt: inverter_temp4 397 | group: now-inverter 398 | hass_device_class: temperature 399 | hass_value_template: "{{ value | round(1) }}" 400 | hass_state_class: measurement 401 | 402 | "11038": 403 | name: PV1 voltage 404 | length: 1 405 | type: U16 406 | unit: V 407 | scale: 10 408 | mqtt: pv_voltage_1 409 | group: now-pv 410 | hass_device_class: voltage 411 | hass_value_template: "{{ value | round(1) }}" 412 | hass_state_class: measurement 413 | 414 | "11039": 415 | name: PV1 current 416 | length: 1 417 | type: U16 418 | unit: A 419 | scale: 10 420 | mqtt: pv_current_1 421 | group: now-pv 422 | hass_device_class: current 423 | hass_value_template: "{{ value | round(1) }}" 424 | hass_state_class: measurement 425 | 426 | "11040": 427 | name: PV2 voltage 428 | length: 1 429 | type: U16 430 | unit: V 431 | scale: 10 432 | mqtt: pv_voltage_2 433 | group: now-pv 434 | hass_device_class: voltage 435 | hass_value_template: "{{ value | round(1) }}" 436 | hass_state_class: measurement 437 | 438 | "11041": 439 | name: PV2 current 440 | length: 1 441 | type: U16 442 | unit: A 443 | scale: 10 444 | mqtt: pv_current_2 445 | group: now-pv 446 | hass_device_class: current 447 | hass_value_template: "{{ value | round(1) }}" 448 | hass_state_class: measurement 449 | 450 | "11062": 451 | name: PV1 power 452 | length: 2 453 | type: U32 454 | unit: W 455 | mqtt: pv_1 456 | group: now-pv 457 | hass_device_class: power 458 | hass_value_template: "{{ value | round(0) }}" 459 | hass_state_class: measurement 460 | 461 | "11064": 462 | name: PV2 power 463 | length: 2 464 | type: U32 465 | unit: W 466 | mqtt: pv_2 467 | group: now-pv 468 | hass_device_class: power 469 | hass_value_template: "{{ value | round(0) }}" 470 | hass_state_class: measurement 471 | 472 | "30200": 473 | name: Backup voltage phase A 474 | length: 1 475 | type: U16 476 | unit: V 477 | scale: 10 478 | mqtt: backup_voltage_a 479 | group: now-backup 480 | hass_device_class: voltage 481 | hass_value_template: "{{ value | round(1) }}" 482 | hass_state_class: measurement 483 | 484 | "30201": 485 | name: Backup current phase A 486 | length: 1 487 | type: U16 488 | unit: A 489 | scale: 10 490 | mqtt: backup_current_a 491 | group: now-backup 492 | hass_device_class: current 493 | hass_value_template: "{{ value | round(2) }}" 494 | hass_state_class: measurement 495 | 496 | "30202": 497 | name: Backup frequency phase A 498 | length: 1 499 | type: U16 500 | unit: Hz 501 | scale: 100 502 | mqtt: backup_frequency_a 503 | group: now-backup 504 | hass_device_class: frequency 505 | hass_value_template: "{{ value | round(2) }}" 506 | hass_state_class: measurement 507 | 508 | "30204": 509 | name: Backup power phase A 510 | length: 2 511 | type: I32 512 | unit: W 513 | mqtt: backup_a 514 | group: now-backup 515 | hass_device_class: power 516 | hass_value_template: "{{ value | round(0) }}" 517 | hass_state_class: measurement 518 | 519 | "30210": 520 | name: Backup voltage phase B 521 | length: 1 522 | type: U16 523 | unit: V 524 | scale: 10 525 | mqtt: backup_voltage_b 526 | group: now-backup 527 | hass_device_class: voltage 528 | hass_value_template: "{{ value | round(1) }}" 529 | hass_state_class: measurement 530 | 531 | "30211": 532 | name: Backup current phase B 533 | length: 1 534 | type: U16 535 | unit: A 536 | scale: 10 537 | mqtt: backup_current_b 538 | group: now-backup 539 | hass_device_class: current 540 | hass_value_template: "{{ value | round(2) }}" 541 | hass_state_class: measurement 542 | 543 | "30212": 544 | name: Backup frequency phase B 545 | length: 1 546 | type: U16 547 | unit: Hz 548 | scale: 100 549 | mqtt: backup_frequency_b 550 | group: now-backup 551 | hass_device_class: frequency 552 | hass_value_template: "{{ value | round(2) }}" 553 | hass_state_class: measurement 554 | 555 | "30214": 556 | name: Backup power phase B 557 | length: 2 558 | type: I32 559 | unit: W 560 | mqtt: backup_b 561 | group: now-backup 562 | hass_device_class: power 563 | hass_value_template: "{{ value | round(0) }}" 564 | hass_state_class: measurement 565 | 566 | "30220": 567 | name: Backup voltage phase C 568 | length: 1 569 | type: U16 570 | unit: V 571 | scale: 10 572 | mqtt: backup_voltage_c 573 | group: now-backup 574 | hass_device_class: voltage 575 | hass_value_template: "{{ value | round(1) }}" 576 | hass_state_class: measurement 577 | 578 | "30221": 579 | name: Backup current phase C 580 | length: 1 581 | type: U16 582 | unit: A 583 | scale: 10 584 | mqtt: backup_current_c 585 | group: now-backup 586 | hass_device_class: current 587 | hass_value_template: "{{ value | round(2) }}" 588 | hass_state_class: measurement 589 | 590 | "30222": 591 | name: Backup frequency phase C 592 | length: 1 593 | type: U16 594 | unit: Hz 595 | scale: 100 596 | mqtt: backup_frequency_c 597 | group: now-backup 598 | hass_device_class: frequency 599 | hass_value_template: "{{ value | round(2) }}" 600 | hass_state_class: measurement 601 | 602 | "30224": 603 | name: Backup power phase C 604 | length: 2 605 | type: I32 606 | unit: W 607 | mqtt: backup_c 608 | group: now-backup 609 | hass_device_class: power 610 | hass_value_template: "{{ value | round(0) }}" 611 | hass_state_class: measurement 612 | 613 | "30230": 614 | name: Backup power total 615 | length: 2 616 | type: I32 617 | unit: W 618 | mqtt: backup 619 | group: now-base 620 | hass_device_class: power 621 | hass_value_template: "{{ value | round(0) }}" 622 | hass_state_class: measurement 623 | 624 | "30236": 625 | name: Inverter power phase A 626 | length: 2 627 | type: I32 628 | unit: W 629 | mqtt: inverter_a 630 | group: now-inverter 631 | hass_device_class: power 632 | hass_value_template: "{{ value | round(0) }}" 633 | hass_state_class: measurement 634 | 635 | "30242": 636 | name: Inverter power phase B 637 | length: 2 638 | type: I32 639 | unit: W 640 | mqtt: inverter_b 641 | group: now-inverter 642 | hass_device_class: power 643 | hass_value_template: "{{ value | round(0) }}" 644 | hass_state_class: measurement 645 | 646 | "30248": 647 | name: Inverter power phase C 648 | length: 2 649 | type: I32 650 | unit: W 651 | mqtt: inverter_c 652 | group: now-inverter 653 | hass_device_class: power 654 | hass_value_template: "{{ value | round(0) }}" 655 | hass_state_class: measurement 656 | 657 | "30254": 658 | name: Battery voltage 659 | length: 1 660 | type: U16 661 | unit: V 662 | scale: 10 663 | mqtt: battery_voltage 664 | group: now-base 665 | hass_device_class: voltage 666 | hass_value_template: "{{ value | round(1) }}" 667 | hass_state_class: measurement 668 | 669 | "30255": 670 | name: Battery current 671 | length: 1 672 | type: I16 673 | unit: A 674 | scale: 10 675 | mqtt: battery_current 676 | group: now-base 677 | hass_device_class: current 678 | hass_value_template: "{{ value | round(1) }}" 679 | hass_state_class: measurement 680 | 681 | "30256": 682 | name: Battery mode 683 | length: 1 684 | type: U16 685 | mqtt: battery_mode 686 | group: now-base 687 | hass_device_class: enum 688 | hass_value_template: "{% if value=='0' %}Discharge{% elif value=='1' %}Charge{% else %}Unknown{% endif %}" 689 | 690 | "30258": 691 | name: Battery power 692 | length: 2 693 | type: I32 694 | unit: W 695 | mqtt: battery 696 | group: now-base 697 | hass_device_class: power 698 | hass_value_template: "{{ value | round(0) }}" 699 | hass_state_class: measurement 700 | 701 | "31000": 702 | name: Grid injection energy (day) 703 | length: 1 704 | type: U16 705 | unit: kWh 706 | scale: 10 707 | mqtt: grid_feed_day 708 | group: day 709 | hass_device_class: energy 710 | hass_value_template: "{{ value | round(1) }}" 711 | hass_state_class: total_increasing 712 | 713 | "31001": 714 | name: Grid purchased energy (day) 715 | length: 1 716 | type: U16 717 | unit: kWh 718 | scale: 10 719 | mqtt: grid_purchase_day 720 | group: day 721 | hass_device_class: energy 722 | hass_value_template: "{{ value | round(1) }}" 723 | hass_state_class: total_increasing 724 | 725 | "31002": 726 | name: Backup energy (day) 727 | length: 1 728 | type: U16 729 | unit: kWh 730 | scale: 10 731 | mqtt: backup_day 732 | group: day 733 | hass_device_class: energy 734 | hass_value_template: "{{ value | round(1) }}" 735 | hass_state_class: total_increasing 736 | 737 | "31003": 738 | name: Battery charge energy (day) 739 | length: 1 740 | type: U16 741 | unit: kWh 742 | scale: 10 743 | mqtt: battery_charge_day 744 | group: day 745 | hass_device_class: energy 746 | hass_value_template: "{{ value | round(1) }}" 747 | hass_state_class: total_increasing 748 | 749 | "31004": 750 | name: Battery discharge energy (day) 751 | length: 1 752 | type: U16 753 | unit: kWh 754 | scale: 10 755 | mqtt: battery_discharge_day 756 | group: day 757 | hass_device_class: energy 758 | hass_value_template: "{{ value | round(1) }}" 759 | hass_state_class: total_increasing 760 | 761 | "31005": 762 | name: PV energy generated (day) 763 | length: 1 764 | type: U16 765 | unit: kWh 766 | scale: 10 767 | mqtt: pv_day 768 | group: day 769 | hass_device_class: energy 770 | hass_value_template: "{{ value | round(1) }}" 771 | hass_state_class: total_increasing 772 | 773 | #"31006": 774 | # name: Loading energy (day) 775 | # length: 1 776 | # type: U16 777 | # unit: kWh 778 | # scale: 10 779 | 780 | #"31008": 781 | # name: Grid energy purchased (day) 782 | # length: 1 783 | # type: U16 784 | # unit: kWh 785 | # scale: 10 786 | 787 | "31102": 788 | name: Grid energy injected (total) 789 | length: 2 790 | type: U32 791 | unit: kWh 792 | scale: 10 793 | mqtt: grid_feed_total 794 | group: total 795 | hass_device_class: energy 796 | hass_value_template: "{{ value | round(0) }}" 797 | hass_state_class: total_increasing 798 | 799 | "31104": 800 | name: Grid energy purchased (total) 801 | length: 2 802 | type: U32 803 | unit: kWh 804 | scale: 10 805 | mqtt: grid_purchase_total 806 | group: total 807 | hass_device_class: energy 808 | hass_value_template: "{{ value | round(0) }}" 809 | hass_state_class: total_increasing 810 | 811 | "31106": 812 | name: Backup energy (total) 813 | length: 2 814 | type: U32 815 | unit: kWh 816 | scale: 10 817 | mqtt: backup_total 818 | group: total 819 | hass_device_class: energy 820 | hass_value_template: "{{ value | round(0) }}" 821 | hass_state_class: total_increasing 822 | 823 | "31108": 824 | name: Battery energy charged (total) 825 | length: 2 826 | type: U32 827 | unit: kWh 828 | scale: 10 829 | mqtt: battery_charge_total 830 | group: total 831 | hass_device_class: energy 832 | hass_value_template: "{{ value | round(0) }}" 833 | hass_state_class: total_increasing 834 | 835 | "31110": 836 | name: Battery energy discharged (total) 837 | length: 2 838 | type: U32 839 | unit: kWh 840 | scale: 10 841 | mqtt: battery_discharge_total 842 | group: total 843 | hass_device_class: energy 844 | hass_value_template: "{{ value | round(0) }}" 845 | hass_state_class: total_increasing 846 | 847 | "31112": 848 | name: PV energy generated (total) 849 | length: 2 850 | type: U32 851 | unit: kWh 852 | scale: 10 853 | mqtt: pv_total 854 | group: total 855 | hass_device_class: energy 856 | hass_value_template: "{{ value | round(0) }}" 857 | hass_state_class: total_increasing 858 | 859 | #"31114": 860 | # name: Total Loading Energy consumed at grid side 861 | # length: 2 862 | # type: U32 863 | # unit: kWh 864 | # scale: 10 865 | 866 | #"31118": 867 | # name: Total energy purchased from grid 868 | # length: 2 869 | # type: U32 870 | # unit: kWh 871 | # scale: 10 872 | 873 | "33000": 874 | name: Battery SOC 875 | length: 1 876 | type: U16 877 | unit: "%" 878 | scale: 100 879 | mqtt: battery_soc 880 | group: now-base 881 | hass_device_class: battery 882 | hass_value_template: "{{ value | round(1) }}" 883 | hass_state_class: measurement 884 | 885 | "33001": 886 | name: Battery SOH 887 | length: 1 888 | type: U16 889 | unit: "%" 890 | scale: 100 891 | mqtt: battery_soh 892 | group: now-battery 893 | hass_device_class: power_factor 894 | hass_value_template: "{{ value | round(2) }}" 895 | hass_state_class: measurement 896 | 897 | "33002": 898 | name: BMS Status 899 | length: 1 900 | type: U16 901 | 902 | "33003": 903 | name: Battery temperature 904 | length: 1 905 | type: U16 906 | unit: "°C" 907 | scale: 10 908 | mqtt: battery_temp 909 | group: now-battery 910 | hass_device_class: temperature 911 | hass_value_template: "{{ value | round(1) }}" 912 | hass_state_class: measurement 913 | 914 | "33009": 915 | name: Battery cell temperature max. 916 | length: 1 917 | type: U16 918 | unit: "°C" 919 | scale: 10 920 | mqtt: battery_cell_t_max 921 | group: now-battery 922 | hass_device_class: temperature 923 | hass_value_template: "{{ value | round(1) }}" 924 | hass_state_class: measurement 925 | 926 | "33011": 927 | name: Battery cell temperature min. 928 | length: 1 929 | type: U16 930 | unit: "°C" 931 | scale: 10 932 | mqtt: battery_cell_t_min 933 | group: now-battery 934 | hass_device_class: temperature 935 | hass_value_template: "{{ value | round(1) }}" 936 | hass_state_class: measurement 937 | 938 | "33013": 939 | name: Battery cell voltage max. 940 | length: 1 941 | type: U16 942 | unit: V 943 | scale: 1000 944 | mqtt: battery_cell_v_max 945 | group: now-battery 946 | hass_device_class: voltage 947 | hass_value_template: "{{ value | round(3) }}" 948 | hass_state_class: measurement 949 | 950 | "33015": 951 | name: Battery cell voltage min. 952 | length: 1 953 | type: U16 954 | unit: V 955 | scale: 1000 956 | mqtt: battery_cell_v_min 957 | group: now-battery 958 | hass_device_class: voltage 959 | hass_value_template: "{{ value | round(3) }}" 960 | hass_state_class: measurement 961 | 962 | "50000": 963 | name: Inverter operation mode 964 | length: 1 965 | type: U16 966 | writable: True 967 | mqtt: mode 968 | group: now-base 969 | hass_device_class: enum 970 | hass_value_template: "{% if value=='257' %}General mode{% elif value=='258' %}Economic mode{% elif value=='259' %}UPS mode{% elif value=='512' %}Off grid{% elif value=='771' %}Manual mode{% else %}Unknown{% endif %}" 971 | 972 | "25100": 973 | name: Grid injection limit switch 974 | length: 1 975 | type: U16 976 | writable: True 977 | mqtt: grid_inject_switch 978 | group: config 979 | hass_component_type: binary_sensor 980 | hass_payload_on: "1" 981 | hass_payload_off: "0" 982 | 983 | "25103": 984 | name: Grid injection power limit 985 | length: 1 986 | type: U16 987 | unit: "%" 988 | scale: 10 989 | writable: True 990 | mqtt: grid_inject_limit 991 | group: config 992 | hass_value_template: "{{ value | round(1) }}" 993 | hass_state_class: measurement 994 | 995 | "52502": 996 | name: On-grid SOC limit switch 997 | length: 1 998 | type: U16 999 | writable: True 1000 | mqtt: on_grid_soc_switch 1001 | group: config 1002 | hass_component_type: binary_sensor 1003 | hass_payload_on: "1" 1004 | hass_payload_off: "0" 1005 | 1006 | "52503": 1007 | name: On-grid SOC limit 1008 | length: 1 1009 | type: U16 1010 | unit: "%" 1011 | scale: 10 1012 | writable: True 1013 | mqtt: on_grid_soc_limit 1014 | group: config 1015 | hass_value_template: "{{ value | round(1) }}" 1016 | hass_state_class: measurement 1017 | 1018 | "52504": 1019 | name: Off-grid SOC limit switch 1020 | length: 1 1021 | type: U16 1022 | writable: True 1023 | mqtt: off_grid_soc_switch 1024 | group: config 1025 | hass_component_type: binary_sensor 1026 | hass_payload_on: "1" 1027 | hass_payload_off: "0" 1028 | 1029 | "52505": 1030 | name: Off-grid SOC limit 1031 | length: 1 1032 | type: U16 1033 | unit: "%" 1034 | scale: 10 1035 | writable: True 1036 | mqtt: off_grid_soc_limit 1037 | group: config 1038 | hass_value_template: "{{ value | round(1) }}" 1039 | hass_state_class: measurement 1040 | 1041 | --------------------------------------------------------------------------------