├── .gitignore ├── LICENSE ├── README.md ├── docs ├── communication.md ├── data_packets.md └── mqtt_bridge.md └── mqtt-bridge ├── config.ini ├── modules ├── configuration.py ├── constants.py ├── mqtt_handler.py ├── packet_decoder.py └── promisc.py ├── mqtt-energi.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **!! Note - Unknown if this works with V5 firmware. I am not updating at the moment due to the many issues reported. !!** 2 | 3 | # mqtt-energi 4 | Protocol documentation myenergi device to device communications and mqtt bridge for local data access 5 | 6 | ## Introduction 7 | This repository is meant to document reverse engineering attempts on the device-to-device communications used between myenergi devices for the purposes of intercepting them and posting the data to a local MQTT server. 8 | 9 | ## Disclaimer 10 | This is a hobby project that is not affiliated with MyEnergi ltd in any way. Use at your own risk. All company and product names are copyright of their respective owners. 11 | 12 | ## Current Capabilities 13 | The MQTT Bridge can currently extract the following information from A V2.1 Eddi. It might be able to recieve packets from other myenergi devices, but cant do anything with them as it wont understand the format of the data. 14 | 15 | * Serial Number 16 | * Supply Voltage 17 | * Supply Frequency 18 | * Grid Power 19 | * Generation Power 20 | * Diverting Power 21 | * Total Diverted Energy per day. 22 | * Active heater channel 23 | * Boost State 24 | 25 | V1 devices (without an ethernet port or wifi) wont work at all as this relies on intercepting the device to device communications that happen over the network. 26 | 27 | ## Index 28 | * [Communication Overview] 29 | * [Data Packets Format] 30 | * [MQTT Bridge] 31 | 32 | 33 | [Data Packets Format]: docs/data_packets.md 34 | [MQTT Bridge]: docs/mqtt_bridge.md 35 | [Communication Overview]: docs/communication.md 36 | -------------------------------------------------------------------------------- /docs/communication.md: -------------------------------------------------------------------------------- 1 | # Communication Overview 2 | The devices appear to communicate with each other using a [layer 2](https://en.wikipedia.org/wiki/Data_link_layer) MAC multicast to the address '71:b3:d5:3a:6f:00'. 3 | 4 | The ethernet frame transmitted has an ethertype set of '0x88b5' which wireshark identifies as 'Local Experimental Ethertype 1' 5 | 6 | The Payload of each frame appears to contain a common header containing a value that appears to signify the type of data within the packet, and then various lengths of data frame that contain information from the device in question (current understanding of these documented in [Data Packets Format]) 7 | 8 | As most networking stacks drop frames that are not addressed to them, to recieve and propcess the frames the NIC needs to be in [promiscuous mode](https://en.wikipedia.org/wiki/Promiscuous_mode) to allow user level processes to recieve and process the frames. There may be a more elegant way to signal the kernel that you want to recieve these frames but at this moment I am unsure how. 9 | 10 | ## Packet Capture setup 11 | I have used [Wireshark](https://www.wireshark.org/) to capture the data from the device. You can set it up to only look for the communication packets by using the filter below 12 | 13 | > eth.type == 0x88b5 && eth.dst == 71:b3:d5:3a:6f:00 14 | 15 | Once packets are captured, the payloads can be exported as bin files and analysed. 16 | 17 | 18 | 19 | [Data Packets Format]: ../docs/data_packets.md 20 | -------------------------------------------------------------------------------- /docs/data_packets.md: -------------------------------------------------------------------------------- 1 | # Packet Format 2 | All packets are recieved as raw ethernet frames with a protocol set as type 0x88b5 which wireshark identifies as 'Local Experimental Ethertype 1' 3 | 4 | Following the ethernet header, various types of data packets can be seen from coming from the device. 5 | 6 | ## Header 7 | All packets start with a 30 byte long header formatted as below 8 | 9 | | Start Address | Size (Bytes) | Description | Contents | 10 | |---------------|--------------|--------------------|---------------------| 11 | | 0x00 | 4 | Packet Header? | CB DA E9 F8 | 12 | | 0x04 | 16 | Unknown (Padding?) | 0x00 | 13 | | 0x14 | 1 | Unknown | 0x01 | 14 | | 0x15 | 3 | Unknown (Padding) | 0x00 | 15 | | 0x18 | 1 | Message Length | Various (See Below) | 16 | | 0x19 | 1 | Device Type ? | Always 0x53 | 17 | | 0x1A | 2 | Message Type 1 ? | | 18 | | 0x1B | 2 | Message Type 2 ? | | 19 | 20 | Various Types of packets have been observed so far. 21 | ## Packet Types 22 | 23 | | Packet Len | Data Length (Bytes) | Contents | Transmit Rate (Seconds) | Notes | 24 | |-------------|---------------------|---------------------------------|-------------------------|------------------------------| 25 | | 0x1F | 55 | Eddi Data (Packet Type 1) | 24 | | 26 | | 0x20 | 56 | Eddi Data (Packet Type 2) | 12 | | 27 | | 0x2B | 67 | Eddi Data (Packet Type 3) | 12 | data not consistant | 28 | | 0x2C | 68 | ?? | 12 | | 29 | | 0x2D | 69 | ?? | 12 | | 30 | | 0x22 | 58 | CT Readings? | 2 | | 31 | | 0x27 | 63 | Unknown | 4 | | 32 | | 0x36 | 78 | Unknown | 2 | | 33 | | 0x37 | 79 | Unknown | tbc | | 34 | | 0x38 | 80 | Unknown | tbc | | 35 | | 0x39 | 81 | Unknown | tbc | | 36 | | 0x3a | 82 | Unknown | tbc | | 37 | 38 | The following Packets have been decoded (or partially decoded) so far 39 | 40 | ### 0x1F - Eddi Data (Packet Type 1) 41 | 42 | Transmitted Approx Every 12 seconds 43 | 44 | | Start Address | Size (Bytes) | Data Type | Description | Post Processing | 45 | |---------------|--------------|-----------|----------------|-----------------| 46 | | 0x1E | 4 | int32 | Serial Number | N/A | 47 | | 0x22 | 2 | int16 | Grid Frequency | Divide by 100 | 48 | | 0x2C | 2 | int16 | Diverted kWh | / 100 | 49 | | 0x32 | 2 | int16 | Voltage | Divide by 10 | 50 | 51 | ### 0x20 - Eddi Data (Packet Type 2) 52 | 53 | Transmitted Approx Every 24 seconds 54 | 55 | | Start Address | Size (Bytes) | Data Type | Description | Post Processing | 56 | |---------------|--------------|-----------|----------------------|-----------------| 57 | | 0x1E | 4 | int32 | Serial Number | N/A | 58 | | 0x22 | 2 | int16 | Grid Frequency | Divide by 100 | 59 | | 0x2C | 2 | int16 | Diverted kWh | / 100 | 60 | | 0x24 | 2 | bool | Status Bit? | | 61 | | 0x26 | 2 | int16 | Max Heater Power (*1)| | 62 | | 0x34 | 2 | int16 | Divert Power | / 100 | 63 | | 0x36 | 2 | int16 | Divert Current? | / 100 | 64 | | 0x2E.0 | 1 | bool | Boosting | N/A | 65 | | 0x2F.7 | 1 | bool | Heater 1 Active | N/A | 66 | | 0x2F.6 | 1 | bool | Heater 2 Active | N/A | 67 | | 0x2F.5 | 1 | bool | unknown | N/A | 68 | | 0x2F.4 | 1 | bool | unknown | N/A | 69 | | 0x2F.3 | 1 | bool | unknown | N/A | 70 | | 0x2F.2 | 1 | bool | unknown | N/A | 71 | | 0x2F.1 | 1 | bool | unknown | N/A | 72 | | 0x2F.0 | 1 | bool | unknown | N/A | 73 | | 0x25.7 | 1 | bool | Stopped Mode | N/A | 74 | 75 | 76 | 1. Appears to be the maximum heater power that is connected to the active channel. For example, if you had a 3kW heater then this would read 3000. Sits at maximum rated power (3600) when nothing is connected or channel is off. 77 | 78 | ### 0x2B - Eddi Data (Packet Type 3) 79 | 80 | Transmitted approx ever 12 seconds. Occasionally sends odd vales for generation & grid power 81 | 82 | | Start Address | Size (Bytes) | Data Type | Description | Post Processing | 83 | |---------------|--------------|-----------|---------------------------------------------------------|-----------------| 84 | | 0x1E | 4 | int32 | Serial Number | N/A | 85 | | 0x22 | 2 | int16 | Frequency | Divide by 100 | 86 | | 0x2C | 2 | int16 | Diverted kwH | Divide by 100 | 87 | | 0x3C | 2 | int16 | Generation Power | | 88 | | 0x34 | 2 | int16 | Grid Power | | 89 | 90 | 91 | ### 0x22 - CT Readings? 92 | 93 | Transmitted Approx ever 2 seconds 94 | 95 | | Start Address | Size (Bytes) | Data Type | Description | Post Processing | 96 | |---------------|--------------|-----------|------------------|-----------------| 97 | | 0x1E | 2 | int16 | Grid Power | N/A | 98 | | 0x22 | 2 | int16 | Generation Power | N/A | 99 | | 0x32 | 2 | int16 | Diverting Power | N/A | 100 | | 0x2E | 4 | int32 | Serial Number | N/A | 101 | 102 | 103 | ### 0x27 - Unknown 104 | 105 | Transmitted approx every 4 seconds 106 | 107 | | Start Address | Size (Bytes) | Data Type | Description | Post Processing | 108 | |---------------|--------------|-----------|---------------------------------------------------------|-----------------| 109 | | 0x1E | 4 | int32 | Serial Number | N/A | 110 | | 0x22 | 2 | int16 | Grid Frequency | Divide by 100 | 111 | | 0x2C | 2 | int16 | Diverted kWh | / 100 | 112 | | 0x36 | 2 | int16 | Firmware Version (But appears to occasionally go blank) | | 113 | | 0x38 | 2 | int16 | Firmware Version2(But appears to occasionally go blank) | | 114 | | 0x3A | 2 | int16 | Unknown | | 115 | | 0x3D | 2 | int16 | Unknown. Seems to track diverter power but not exactly | | 116 | 117 | 118 | ### 0x36 - Unknown 119 | 120 | Transmitted approx every 2 seconds 121 | Occassionally drops all readings to zero. 122 | 123 | | Start Address | Size (Bytes) | Data Type | Description | Post Processing | 124 | |---------------|--------------|-----------|---------------------------------------------------------|-----------------| 125 | | 0x4A | 4 | int32 | Serial Number | N/A | 126 | | 0x3A | 2 | int16 | Frequency (Seems to update faster than 0x1F) | | 127 | | 0x3C | 2 | int16 | Possibly Heatsink Temperature? | | 128 | | 0x38 | 2 | int16 | Voltage (Seems to update faster than 0x1F) | | 129 | 130 | ### 0x37 - Unknown 131 | 132 | 133 | | Start Address | Size (Bytes) | Data Type | Description | Post Processing | 134 | |---------------|--------------|-----------|---------------------------------------------------------|-----------------| 135 | | 0x1E | 4 | int32 | Serial Number | N/A | 136 | 137 | -------------------------------------------------------------------------------- /docs/mqtt_bridge.md: -------------------------------------------------------------------------------- 1 | # MQTT Bridge 2 | This is a quick and dirty script that captures the packets matching the ethertype and data header of the myenergi packets. This works by putting the PC's NIC into promiscuous mode and looking for packets that match the profile that we are looking for. Once it has found a packet, its payload is processed by the decoder. If the decoder understands the packet type, it will pull out the data and post it to a MQTT broker 3 | 4 | ## Compatibility 5 | In its current form, this probably only works on linux based systems due to having to set the NIC mode to allow it to recieve all packets. Windows may work in the future if I get around to working out how. 6 | 7 | This has only been tested recieving the following data from a Eddi V2.1 unit. Other capabilities may follow if the protocol is better understood 8 | * Current Supply Voltage / Frequency 9 | * Current Grid Power / Generation Power / Divert Power 10 | * Total Diverted Energy 11 | * Active Heater Channel 12 | * Boost & stopped status 13 | 14 | 15 | 16 | ## Setup 17 | Your python installation needs paho.mqtt installed 18 | 19 | in file config.ini, you need to set 20 | * your broker IP and port 21 | * the topic you want the data posted to 22 | * a unique client ID 23 | * the user/password for ypur mqtt broker (if needed) 24 | 25 | The data will be posted to 26 | 27 | > {topic}/{serialnumber}/{data} 28 | 29 | You also need to set the name of the NIC that you are using to allow it to be bound and set to promisc mode. The script needs to be run with root permissions for the moment as it manipulates the NIC to set it to promiscuous mode. 30 | -------------------------------------------------------------------------------- /mqtt-bridge/config.ini: -------------------------------------------------------------------------------- 1 | [NETWORK] 2 | nic = eno1 3 | 4 | [MQTT] 5 | broker = 10.87.90.6 6 | port = 1883 7 | topic = myenergi-test 8 | id = Myenergi-Bridge-1 9 | user = 10 | pass = 11 | 12 | [DEBUG] 13 | enabled = False 14 | 15 | -------------------------------------------------------------------------------- /mqtt-bridge/modules/configuration.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | 3 | CFG_NAME = 'config.ini' 4 | 5 | config_file = configparser.ConfigParser() 6 | 7 | def read(): 8 | config = configparser.ConfigParser() 9 | config.read(CFG_NAME) 10 | return config -------------------------------------------------------------------------------- /mqtt-bridge/modules/constants.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DATA_HEADER = (0xcb,0xda,0xe9,0xf8) # Myenergi packet header 6 | ETH_FRAME_LEN = 1514 # Ethernet frame len 7 | ETH_HLEN = 14 # Ethernet header len 8 | PKT_TYPE = 0x88b5 # Experimental Ethertype 1 9 | -------------------------------------------------------------------------------- /mqtt-bridge/modules/mqtt_handler.py: -------------------------------------------------------------------------------- 1 | from paho.mqtt import client as mqtt_client 2 | 3 | def connect(broker,port, id,user,passw): 4 | def on_connect(client, userdata, flags, rc): 5 | if rc == 0: 6 | print("Connected to MQTT Broker!") 7 | else: 8 | print("Failed to connect, return code %d\n", rc) 9 | 10 | mqtt = mqtt_client.Client(id) 11 | 12 | mqtt.username_pw_set(user,passw) 13 | mqtt.on_connect = on_connect 14 | mqtt.connect(broker, port) 15 | return mqtt 16 | -------------------------------------------------------------------------------- /mqtt-bridge/modules/packet_decoder.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | def status_decode(inbyte): 4 | return [1 if inbyte & (1 << (7-n)) else 0 for n in range(8)] 5 | 6 | 7 | def decode_packet(eth_pkt,debug): 8 | packet_len = len(eth_pkt) 9 | packet_type = eth_pkt[0x18] 10 | data = {} 11 | 12 | if packet_type == 0x1F: # Frequency Message 13 | serialno = int.from_bytes(eth_pkt[0x1e:0x22], "little") 14 | freq = int.from_bytes(eth_pkt[0x22:0x24], "little") 15 | div_wh = int.from_bytes(eth_pkt[0x2c:0x2e], "little") 16 | voltage = int.from_bytes(eth_pkt[0x32:0x34], "little") 17 | data = { 18 | 'serial':serialno, 19 | 'frequency':freq/100, 20 | 'voltage':voltage/10, 21 | 'diverted_kWh':div_wh/100 22 | } 23 | 24 | elif packet_type == 0x20: # Diverter Data? 25 | serialno = int.from_bytes(eth_pkt[0x1e:0x22], "little") 26 | div_wh = int.from_bytes(eth_pkt[0x2c:0x2e], "little") 27 | divert_pwr = int.from_bytes(eth_pkt[0x34:0x36], "little") 28 | divert_cur = int.from_bytes(eth_pkt[0x36:0x38], "little") # not sure about this 29 | max_power = int.from_bytes(eth_pkt[0x26:0x28], "little") # not sure about this 30 | status = status_decode(int(eth_pkt[0x2F])) 31 | status2 = status_decode(int(eth_pkt[0x25])) 32 | status3 = status_decode(int(eth_pkt[0x2E])) 33 | heater1 = status[7] 34 | heater2 = status[6] 35 | boost = status3[3] 36 | stopped = status2[0] 37 | 38 | data = { 39 | 'serial':serialno, 40 | 'diverted_kWh':div_wh/100, 41 | 'divert_pwr':divert_pwr, 42 | #'divert_cur':divert_cur, 43 | 'heater1':heater1, 44 | 'heater2':heater2, 45 | 'stopped':stopped, 46 | 'boost':boost, 47 | #'max_power':max_power 48 | } 49 | elif packet_type == 0x2B: #Data ? 50 | serialno = int.from_bytes(eth_pkt[0x1e:0x22], "little") 51 | freq = int.from_bytes(eth_pkt[0x22:0x24], "little") 52 | div_wh = int.from_bytes(eth_pkt[0x2c:0x2e], "little") 53 | gen_pow = int.from_bytes(eth_pkt[0x3c:0x3e], "little",signed=True) # occasionally goes zero 54 | grid_pow = int.from_bytes(eth_pkt[0x34:0x36], "little", signed=True) #occasionally goes to weird value 55 | div_pow = int.from_bytes(eth_pkt[0x41:0x43], "little", signed=True) 56 | v1 = int.from_bytes(eth_pkt[0x41:0x43], "little",signed=True) # proportional to generation 57 | if debug: 58 | data = { 59 | 'serial':serialno, 60 | 'frequency':freq/100, 61 | 'diverted_kWh':div_wh/100, 62 | #'gen_pwr':gen_pow, 63 | #'grid_pwr':grid_pow, 64 | 'divert_pwr':div_pow, 65 | } 66 | 67 | elif packet_type == 0x22: # CT Readings 68 | gen_pow = int.from_bytes(eth_pkt[0x22:0x24], "little",signed=True) # Generation Power 69 | grid_pow = int.from_bytes(eth_pkt[0x1e:0x20], "little", signed=True) # Grid Power 70 | div_pow = int.from_bytes(eth_pkt[0x32:0x34], "little", signed=True) # Diverter Power 71 | serialno = int.from_bytes(eth_pkt[0x2e:0x32], "little") # Serial No 72 | data = { 73 | 'serial':serialno, 74 | 'gen_pwr':gen_pow, 75 | 'grid_pwr':grid_pow, 76 | 'divert_pwr':div_pow, 77 | } 78 | 79 | 80 | 81 | 82 | 83 | elif packet_type == 0x27: #Data ? 84 | serialno = int.from_bytes(eth_pkt[0x1e:0x22], "little") 85 | freq = int.from_bytes(eth_pkt[0x22:0x24], "little") 86 | div_wh = int.from_bytes(eth_pkt[0x2c:0x2e], "little") 87 | fw1 = int.from_bytes(eth_pkt[0x38:0x3A], "little",signed=True) 88 | fw2 = int.from_bytes(eth_pkt[0x36:0x38], "little",signed=True) 89 | v1 = int.from_bytes(eth_pkt[0x3d:0x3e], "little",signed=True) # seems to track diverter power but not exactly. possibly output voltage? 90 | v2 = int.from_bytes(eth_pkt[0x3c:0x3d], "little",signed=True) 91 | v3 = int.from_bytes(eth_pkt[0x3A:0x3C], "little",signed=True) 92 | v4 = int.from_bytes(eth_pkt[0x34:0x36], "little",signed=True) 93 | v5 = int.from_bytes(eth_pkt[0x32:0x34], "little",signed=True) 94 | v6 = int.from_bytes(eth_pkt[0x30:0x32], "little",signed=True) 95 | v7 = int.from_bytes(eth_pkt[0x2E:0x30], "little",signed=True) 96 | if debug: 97 | data = { 98 | 'serial':serialno, 99 | 'frequency':freq/100, 100 | 'diverted_kWh':div_wh/100, 101 | '0x27/fw1':fw1, 102 | '0x27/fw2':fw2, 103 | '0x27/v1':v1, 104 | '0x27/v2':v2, 105 | '0x27/v3':v3, 106 | '0x27/v4':v4, 107 | '0x27/V5':v5, 108 | '0x27/v6':v6, 109 | '0x27/v7':v7 110 | } 111 | 112 | 113 | elif packet_type == 0x37: # Divert Power 114 | serialno = int.from_bytes(eth_pkt[0x1e:0x22], "little") 115 | v1 = int.from_bytes(eth_pkt[0x4D:0x4F], "little",signed=True) # diverter power? 116 | v2 = int.from_bytes(eth_pkt[0x3c:0x3d], "little",signed=True) 117 | v3 = int.from_bytes(eth_pkt[0x3A:0x3C], "little",signed=True) 118 | v4 = int.from_bytes(eth_pkt[0x34:0x36], "little",signed=True) 119 | v5 = int.from_bytes(eth_pkt[0x32:0x34], "little",signed=True) 120 | v6 = int.from_bytes(eth_pkt[0x30:0x32], "little",signed=True) 121 | v7 = int.from_bytes(eth_pkt[0x2E:0x30], "little",signed=True) 122 | if debug: 123 | data = { 124 | 'serial':serialno, 125 | '0x27/v1':v1, 126 | '0x27/v2':v2, 127 | '0x27/v3':v3, 128 | '0x27/v4':v4, 129 | '0x27/V5':v5, 130 | '0x27/v6':v6, 131 | '0x27/v7':v7 132 | } 133 | 134 | elif packet_type == 0x36: # ? 135 | serialno = int.from_bytes(eth_pkt[0x4A:0x4E], "little") 136 | 137 | v1 = int.from_bytes(eth_pkt[0x1C:0x1E], "little",signed=True) 138 | v2 = int.from_bytes(eth_pkt[0x1E:0x20], "little",signed=True) 139 | v3 = int.from_bytes(eth_pkt[0x20:0x22], "little",signed=True) 140 | v4 = int.from_bytes(eth_pkt[0x22:0x24], "little",signed=True) 141 | v5 = int.from_bytes(eth_pkt[0x24:0x26], "little",signed=True) 142 | v6 = int.from_bytes(eth_pkt[0x30:0x32], "little",signed=True) 143 | v7 = int.from_bytes(eth_pkt[0x34:0x36], "little",signed=True) 144 | v8 = int.from_bytes(eth_pkt[0x36:0x38], "little",signed=True) 145 | v9 = int.from_bytes(eth_pkt[0x38:0x3A], "little",signed=True) # Voltage? 146 | v10 = int.from_bytes(eth_pkt[0x3A:0x3C], "little",signed=True) # Frequency 147 | v11 = int.from_bytes(eth_pkt[0x3C:0x3E], "little",signed=True) # Heatsink Temp 148 | v12 = int.from_bytes(eth_pkt[0x3E:0x40], "little",signed=True) 149 | v13 = int.from_bytes(eth_pkt[0x40:0x42], "little",signed=True) 150 | v14 = int.from_bytes(eth_pkt[0x42:0x44], "little",signed=True) 151 | v15 = int.from_bytes(eth_pkt[0x44:0x46], "little",signed=True) 152 | v16 = int.from_bytes(eth_pkt[0x46:0x48], "little",signed=True) 153 | v17 = int.from_bytes(eth_pkt[0x48:0x4A], "little",signed=True) 154 | if debug: 155 | data = { 156 | 'serial':serialno, 157 | '0x36/v1':v1, 158 | '0x36/v2':v2, 159 | '0x36/v3':v3, 160 | '0x36/v4':v4, 161 | '0x36/v5':v5, 162 | '0x36/v6':v6, 163 | '0x36/v7':v7, 164 | '0x36/v8':v8, 165 | '0x36/v9':v9, 166 | '0x36/v10':v10, 167 | '0x36/v11':v11, 168 | '0x36/v12':v12, 169 | '0x36/v13':v13, 170 | '0x36/v14':v14, 171 | '0x36/v15':v15, 172 | '0x36/v16':v16, 173 | '0x36/v17':v17 174 | 175 | } 176 | elif packet_type == 0x38: # ? 177 | serialno = int.from_bytes(eth_pkt[0x1E:0x22], "little") 178 | v1 = int.from_bytes(eth_pkt[0x20:0x22], "little",signed=True) 179 | v2 = int.from_bytes(eth_pkt[0x22:0x24], "little",signed=True) 180 | v3 = int.from_bytes(eth_pkt[0x24:0x26], "little",signed=True) # Frequency 181 | v4 = int.from_bytes(eth_pkt[0x26:0x28], "little",signed=True) # Voltage? 182 | v5 = int.from_bytes(eth_pkt[0x28:0x2A], "little",signed=True) 183 | v6 = int.from_bytes(eth_pkt[0x2A:0x2C], "little",signed=True) 184 | v7 = int.from_bytes(eth_pkt[0x2C:0x2E], "little",signed=True) 185 | v8 = int.from_bytes(eth_pkt[0x2E:0x30], "little",signed=True) 186 | v9 = int.from_bytes(eth_pkt[0x30:0x32], "little",signed=True) 187 | v10 = int.from_bytes(eth_pkt[0x32:0x34], "little",signed=True) 188 | v11 = int.from_bytes(eth_pkt[0x34:0x36], "little",signed=True) 189 | v12 = int.from_bytes(eth_pkt[0x36:0x38], "little",signed=True) 190 | v13 = int.from_bytes(eth_pkt[0x38:0x3A], "little",signed=True) 191 | v14 = int.from_bytes(eth_pkt[0x3A:0x3C], "little",signed=True) 192 | v15 = int.from_bytes(eth_pkt[0x3C:0x3E], "little",signed=True) 193 | v16 = int.from_bytes(eth_pkt[0x3E:0x40], "little",signed=True) 194 | if debug: 195 | data = { 196 | 'serial':serialno, 197 | '0x38/v1':v1, 198 | '0x38/v2':v2, 199 | '0x38/v3':v3, 200 | '0x38/v4':v4, 201 | '0x38/v5':v5, 202 | '0x38/v6':v6, 203 | '0x38/v7':v7, 204 | '0x38/v8':v8, 205 | '0x38/v9':v9, 206 | '0x38/v10':v10, 207 | '0x38/v11':v11, 208 | '0x38/v12':v12, 209 | '0x38/v13':v13, 210 | '0x38/v14':v14, 211 | '0x38/v15':v15, 212 | '0x38/v16':v16 213 | 214 | } 215 | 216 | elif packet_type == 0x3A: # ? 217 | serialno = int.from_bytes(eth_pkt[0x1E:0x22], "little") 218 | v2 = int.from_bytes(eth_pkt[0x22:0x24], "little",signed=True) 219 | v3 = int.from_bytes(eth_pkt[0x24:0x26], "little",signed=True) 220 | v4 = int.from_bytes(eth_pkt[0x26:0x28], "little",signed=True) 221 | v5 = int.from_bytes(eth_pkt[0x28:0x2A], "little",signed=True) 222 | v6 = int.from_bytes(eth_pkt[0x2A:0x2C], "little",signed=True) 223 | v7 = int.from_bytes(eth_pkt[0x2C:0x2E], "little",signed=True) 224 | v8 = int.from_bytes(eth_pkt[0x2E:0x30], "little",signed=True) 225 | v9 = int.from_bytes(eth_pkt[0x30:0x32], "little",signed=True) 226 | v10 = int.from_bytes(eth_pkt[0x32:0x34], "little",signed=True) 227 | v11 = int.from_bytes(eth_pkt[0x34:0x36], "little",signed=True) 228 | v12 = int.from_bytes(eth_pkt[0x36:0x38], "little",signed=True) 229 | v13 = int.from_bytes(eth_pkt[0x38:0x3A], "little",signed=True) 230 | v14 = int.from_bytes(eth_pkt[0x44:0x46], "little",signed=True) 231 | v15 = int.from_bytes(eth_pkt[0x42:0x44], "little",signed=True) 232 | if debug: 233 | data = { 234 | 'serial':serialno, 235 | '0x3A/v2':v2, 236 | '0x3A/v3':v3, 237 | '0x3A/v4':v4, 238 | '0x3A/v5':v5, 239 | '0x3A/v6':v6, 240 | '0x3A/v7':v7, 241 | '0x3A/v8':v8, 242 | '0x3A/v9':v9, 243 | '0x3A/v10':v10, 244 | '0x3A/v11':v11, 245 | '0x3A/v12':v12, 246 | '0x3A/v13':v13, 247 | '0x3A/v14':v14, 248 | '0x3A/v15':v15, 249 | 250 | } 251 | 252 | 253 | else: 254 | dummy = True 255 | #print(f'type:{hex(packet_type)}, Packet Length: {packet_len} [UNIMPLEMENTED]') 256 | return data 257 | -------------------------------------------------------------------------------- /mqtt-bridge/modules/promisc.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import fcntl 3 | from socket import AF_NETLINK, SOCK_DGRAM, socket 4 | 5 | IFF_PROMISC = 0x100 6 | SIOCGIFFLAGS = 0x8913 7 | SIOCSIFFLAGS = 0x8914 8 | 9 | 10 | class ifreq(ctypes.Structure): 11 | _fields_ = [("ifr_ifrn", ctypes.c_char * 16), 12 | ("ifr_flags", ctypes.c_short)] 13 | 14 | 15 | 16 | 17 | def set(nic,state): 18 | ifr = ifreq() 19 | ifr.ifr_ifrn = str.encode(nic) 20 | s = socket(AF_NETLINK, SOCK_DGRAM) 21 | fcntl.ioctl(s.fileno(), SIOCGIFFLAGS, ifr) # G for Get 22 | if state: 23 | ifr.ifr_flags |= IFF_PROMISC 24 | else: 25 | ifr.ifr_flags &= ~IFF_PROMISC 26 | fcntl.ioctl(s.fileno(), SIOCSIFFLAGS, ifr) # S for Set -------------------------------------------------------------------------------- /mqtt-bridge/mqtt-energi.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import selectors 3 | import struct 4 | from modules import constants 5 | from modules import promisc 6 | from modules.packet_decoder import decode_packet 7 | from modules import mqtt_handler 8 | from modules import configuration 9 | 10 | import atexit 11 | 12 | settings = configuration.read() 13 | 14 | def exit_handler(): 15 | promisc.set(settings['NETWORK']['NIC'],False) # Kick NIC back out of promisc mode on exit 16 | 17 | 18 | atexit.register(exit_handler) 19 | 20 | def main(): 21 | mqtt = mqtt_handler.connect(settings['MQTT']['BROKER'], int(settings['MQTT']['PORT']),settings['MQTT']['ID'],settings['MQTT']['User'],settings['MQTT']['pass']) 22 | mqtt.loop_start() 23 | debug = settings.getboolean('DEBUG','Enabled') 24 | print(f'Debug: {debug}') 25 | with socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(constants.PKT_TYPE)) as server_socket: #Recieve Raw Frames 26 | server_socket.bind((settings['NETWORK']['NIC'], 0)) 27 | promisc.set(settings['NETWORK']['NIC'],True) # set NIC to promiscuous mode 28 | with selectors.DefaultSelector() as selector: 29 | selector.register(server_socket.fileno(), selectors.EVENT_READ) 30 | while True: # Loop Forever 31 | ready = selector.select() 32 | if ready: 33 | frame = server_socket.recv(constants.ETH_FRAME_LEN) # get ethernet frame 34 | header = frame[:constants.ETH_HLEN] # split header from frame 35 | dest, source, protocol = struct.unpack('!6s6sH', header) #unpack header 36 | payload = frame[constants.ETH_HLEN:] # unpack payload 37 | if protocol == constants.PKT_TYPE: 38 | start = struct.unpack('4B',payload[:4]) 39 | if start == constants.DATA_HEADER: 40 | data = decode_packet(payload, debug) 41 | if data: 42 | print(data) 43 | for key in data: 44 | mqtt.publish(settings['MQTT']['TOPIC']+"/"+str(data['serial'])+"/"+key, data[key]) 45 | 46 | 47 | 48 | 49 | 50 | if __name__ == '__main__': 51 | main() -------------------------------------------------------------------------------- /mqtt-bridge/requirements.txt: -------------------------------------------------------------------------------- 1 | paho-mqtt==1.6.1 2 | --------------------------------------------------------------------------------