Skip to content

Commit 8cb2048

Browse files
authored
Merge pull request #105 from HotNoob/v1.1.10
V1.1.10
2 parents 86b7e64 + ef6d13a commit 8cb2048

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2866
-169
lines changed

.github/workflows/python-3.10.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
name: Python 3.10
55

6+
permissions:
7+
contents: read
8+
69
on:
710
push:
811
branches: [ "main" ]

.github/workflows/python-3.11.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
name: Python 3.11
55

6+
permissions:
7+
contents: read
8+
69
on:
710
push:
811
branches: [ "main" ]

.github/workflows/python-3.12.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
name: Python 3.12
55

6+
permissions:
7+
contents: read
8+
69
on:
710
push:
811
branches: [ "main" ]

.github/workflows/python-3.13.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
name: Python 3.13
55

6+
permissions:
7+
contents: read
8+
69
on:
710
push:
811
branches: [ "main" ]

.github/workflows/python-3.9.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
name: Python 3.9
55

6+
permissions:
7+
contents: read
8+
69
on:
710
push:
811
branches: [ "main" ]

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,8 @@ classes/transports/*custom*
2828

2929
input_registry.json
3030
holding_registry.json
31+
32+
#ignore pypi / pyproject.toml output
33+
dist/*
34+
build/*
35+
python_protocol_gateway.egg-info/*

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.9-alpine as base
1+
FROM python:3.13-alpine as base
22
FROM base as builder
33
RUN mkdir /install
44
WORKDIR /install

README.md

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
![Python 3.12](https://github.com/HotNoob/PythonProtocolGateway/actions/workflows/python-3.12.yml/badge.svg)
66
![Python 3.13](https://github.com/HotNoob/PythonProtocolGateway/actions/workflows/python-3.13.yml/badge.svg)
77

8+
[![PyPI version](https://img.shields.io/pypi/v/python-protocol-gateway.svg)](https://pypi.org/project/python-protocol-gateway/)
89
[![CodeQL](https://github.com/HotNoob/PythonProtocolGateway/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/HotNoob/PythonProtocolGateway/actions/workflows/github-code-scanning/codeql)
910

1011
For advanced configuration help, please checkout the documentation :)
11-
https://github.com/HotNoob/PythonProtocolGateway/tree/main/documentation
12+
13+
[/documentation](/documentation)
1214

1315
# Python Protocol Gateway
1416

@@ -17,8 +19,9 @@ Configuration is handled via a small config files.
1719
In the long run, Python Protocol Gateway will become a general purpose protocol gateway to translate between more than just modbus and mqtt.
1820

1921
For specific device installation instructions please checkout the documentation:
20-
Growatt, EG4, Sigineer, SOK, PACE-BMS
21-
https://github.com/HotNoob/PythonProtocolGateway/tree/main/documentation
22+
Growatt, EG4, Sigineer, SOK, PACE-BMS, Sigineer, ect...
23+
24+
[/documentation/devices](/documentation/devices)
2225

2326
# General Installation
2427
Connect the USB port on the inverter into your computer / device. This port is essentially modbus usb adapter.
@@ -28,7 +31,7 @@ Alternatively, connect a usb adapter to your rs485 / can port with appropriate w
2831

2932
### install as homeassistant add-on
3033
checkout:
31-
https://github.com/felipecrs/python-protocol-gateway-hass-addon/tree/master
34+
[PPG HASS Addon](https://github.com/HotNoob/python-protocol-gateway-hass-addon/tree/master)
3235

3336
### install requirements
3437
```
@@ -49,21 +52,30 @@ nano config.cfg
4952
manually select protocol in .cfg
5053
protocol_version = {{version}}
5154
```
55+
eg4_v58 = eg4 inverters
56+
eg4_3000ehv_v1 = eg4 inverters
5257
v0.14 = growatt inverters 2020+
5358
sigineer_v0.11 = sigineer inverters
54-
growatt_2020_v1.24 = alt protocol for large growatt inverters - currently untested
55-
srne_v3.9 = SRNE inverters - confirmed working-ish
56-
victron_gx_3.3 = Victron GX Devices - Untested
57-
solark_v1.1 = SolarArk 8/12K Inverters - Untested
59+
srne_v3.9 = SRNE inverters
60+
5861
hdhk_16ch_ac_module = some chinese current monitoring device :P
59-
srne_2021_v1.96 = SRNE inverters 2021+ (tested at ASF48100S200-H, ok-ish for HF2430U60-100 )
62+
```
6063

61-
eg4_v58 = eg4 inverters ( EG4-6000XP ) - confirmed working
62-
eg4_3000ehv_v1 = eg4 inverters ( EG4_3000EHV )
64+
Untested Protocols
6365
```
66+
growatt_2020_v1.24 = alt protocol for large growatt inverters
67+
victron_gx_3.3 = Victron GX Devices
68+
solark_v1.1 = SolarArk 8/12K Inverters
69+
```
70+
71+
For a complete list of protocols, explore:
72+
[/Protocols](/protocols)
6473

65-
more details on these protocols can be found in the documentation:
66-
https://github.com/HotNoob/PythonProtocolGateway/tree/main/documentation
74+
For a more complete list of tested devices & protocols:
75+
[Tested Devices & Protocols](documentation/usage/devices_and_protocols.csv)
76+
77+
more advanced details can be found in the documentation:
78+
[/Documentation](/documentation)
6779

6880
### run as script
6981
```
@@ -108,8 +120,11 @@ once installed; the device should show up on home assistant under mqtt
108120

109121
```Settings -> Devices & Services -> MQTT ```
110122

111-
more docs on setting up mqtt here: https://www.home-assistant.io/integrations/mqtt
112-
i probably might have missed something. ha is new to me.
123+
more docs on setting up mqtt here:
124+
https://www.home-assistant.io/integrations/mqtt
125+
126+
#### connect mqtt on home assistant with external mqtt broker
127+
[HowTo Connect External MQTT Broker To HomeAssistant](https://www.youtube.com/watch?v=sP2gYLYQat8)
113128

114129
### general update procedure
115130
update files and restart script / service
@@ -118,8 +133,6 @@ git pull
118133
systemctl restart protocol_gateway.service
119134
```
120135

121-
**if you installed this when it was called growatt2mqtt-hotnoob or invertermodbustomqtt, you'll need to reinstall if you want to update. **
122-
123136
### Unknown Status MQTT Home Assistant
124137
If all values appear as "Unknown"
125138
This is a bug with home assistant's discovery that some times happens when adding for the first time. just restart the service / script and it will fix itself.
@@ -148,17 +161,18 @@ As i dive deeper into solar monitoring and general automation, i've come to the
148161

149162
So... don't mind me as i may add other devices such as battery bms' and... i have a home energy monitor on the way! so i'll be adding that when it arrives.
150163

151-
### Rebranding Again... last time.
152-
if you installed this when it was called growatt2mqtt-hotnoob or InverterModBusToMQTT, you'll need to reinstall if you want to update.
153-
154-
155164
### donate
156165
this took me a while to make; and i had to make it because there werent any working solutions.
157-
donations would be appreciated.
158-
![BitCoin Donation](https://github.com/HotNoob/growatt2mqtt-hotnoob/blob/main/images/donate_to_hotnoob.png?raw=true)
159-
160-
```(btc) bc1qh394vazcguedkw2rlklnuhapdq7qgpnnz9c3t0```
166+
donations / sponsoring this repo would be appreciated.
161167

162-
### Use Docker - untested
168+
### Use Docker
163169
- ```docker build . -t protocol_gateway ```
164170
- ```docker run --device=/dev/ttyUSB0 protocol_gateway```
171+
172+
### Use Docker Image
173+
- ``` docker pull hotn00b/pythonprotocolgateway ```
174+
- ```docker run -v $(pwd)/config.cfg:/app/config.cfg --device=/dev/ttyUSB0 hotn00b/pythonprotocolgateway```
175+
176+
See [config.cfg.example](/config.cfg.example)
177+
178+
[Docker Image Repo](https://hub.docker.com/r/hotn00b/pythonprotocolgateway)

RELEASE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
things todo to perform a release.
2+
3+
can try to automate some of these later.
4+
5+
GitHub - https://github.com/HotNoob/PythonProtocolGateway/releases
6+
PyPi Package - https://pypi.org/project/python-protocol-gateway/
7+
HomeAssistant repo - https://github.com/HotNoob/python-protocol-gateway-hass-addon
8+
Docker Image - https://hub.docker.com/r/hotn00b/pythonprotocolgateway

classes/protocol_settings.py

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class registry_map_entry:
188188
register_bit : int
189189
register_byte : int
190190
''' byte offset for canbus ect... '''
191+
191192
variable_name : str
192193
documented_name : str
193194
unit : str
@@ -208,6 +209,9 @@ class registry_map_entry:
208209
data_type_size : int = -1
209210
''' for non-fixed size types like ASCII'''
210211

212+
data_byteorder : str = ''
213+
''' entry specific byte order little | big | '' '''
214+
211215
read_command : bytes = None
212216
''' for transports/protocols that require sending a command ontop of "register" '''
213217

@@ -330,6 +334,19 @@ def get_registry_entry(self, name : str, registry_type : Registry_Type) -> regis
330334
return item
331335

332336
return None
337+
338+
def get_code_by_value(self, entry : registry_map_entry, value : str, fallback=None) -> str:
339+
''' case insensitive '''
340+
341+
value = value.strip().lower()
342+
343+
if entry.variable_name+"_codes" in self.codes:
344+
codes = self.codes[entry.variable_name+"_codes"]
345+
for code, val in codes.items():
346+
if value == val.lower():
347+
return code
348+
349+
return fallback
333350

334351
def load__json(self, file : str = "", settings_dir : str = ""):
335352
if not settings_dir:
@@ -512,16 +529,30 @@ def process_row(row):
512529

513530
#region data type
514531
data_type = Data_Type.USHORT
515-
516532
data_type_len : int = -1
533+
data_byteorder : str = ''
517534
#optional row, only needed for non-default data types
518535
if "data type" in row and row["data type"]:
536+
data_type_str : str = ''
537+
519538
matches = data_type_regex.search(row["data type"])
520539
if matches:
521540
data_type_len = int(matches.group("length"))
522-
data_type = Data_Type.fromString(matches.group("datatype"))
541+
data_type_str = matches.group("datatype")
523542
else:
524-
data_type = Data_Type.fromString(row["data type"])
543+
data_type_str = row["data type"]
544+
545+
#check if datatype specifies byteorder
546+
if data_type_str.upper().endswith("_LE"):
547+
data_byteorder = "little"
548+
data_type_str = data_type_str[:-3]
549+
elif data_type_str.upper().endswith("_BE"):
550+
data_byteorder = "big"
551+
data_type_str = data_type_str[:-3]
552+
553+
554+
data_type = Data_Type.fromString(data_type_str)
555+
525556

526557

527558
if "values" not in row:
@@ -658,6 +689,7 @@ def process_row(row):
658689
unit_mod= unit_multiplier,
659690
data_type= data_type,
660691
data_type_size = data_type_len,
692+
data_byteorder = data_byteorder,
661693
concatenate = concatenate,
662694
concatenate_registers = concatenate_registers,
663695
values=values,
@@ -857,6 +889,10 @@ def load_registry_map(self, registry_type : Registry_Type, file : str = "", sett
857889
def process_register_bytes(self, registry : dict[int,bytes], entry : registry_map_entry):
858890
''' process bytes into data'''
859891

892+
byte_order : str = self.byteorder
893+
if entry.data_byteorder: #allow map entry to override byteorder
894+
byte_order = entry.data_byteorder
895+
860896
if isinstance(registry[entry.register], tuple):
861897
register = registry[entry.register][0] #can bus uses tuple for timestamp
862898
else:
@@ -869,14 +905,15 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma
869905
register = register[:entry.data_type_size]
870906

871907
if entry.data_type == Data_Type.UINT:
872-
value = int.from_bytes(register[:4], byteorder=self.byteorder, signed=False)
908+
value = int.from_bytes(register[:4], byteorder=byte_order, signed=False)
873909
elif entry.data_type == Data_Type.INT:
874-
value = int.from_bytes(register[:4], byteorder=self.byteorder, signed=True)
910+
value = int.from_bytes(register[:4], byteorder=byte_order, signed=True)
875911
elif entry.data_type == Data_Type.USHORT:
876-
value = int.from_bytes(register[:2], byteorder=self.byteorder, signed=False)
912+
value = int.from_bytes(register[:2], byteorder=byte_order, signed=False)
877913
elif entry.data_type == Data_Type.SHORT:
878-
value = int.from_bytes(register[:2], byteorder=self.byteorder, signed=True)
914+
value = int.from_bytes(register[:2], byteorder=byte_order, signed=True)
879915
elif entry.data_type == Data_Type._16BIT_FLAGS or entry.data_type == Data_Type._8BIT_FLAGS or entry.data_type == Data_Type._32BIT_FLAGS:
916+
val = int.from_bytes(register, byteorder=byte_order, signed=False)
880917
#16 bit flags
881918
start_bit : int = 0
882919
end_bit : int = 16 #default 16 bit
@@ -952,11 +989,20 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma
952989
# If positive, simply extract the value using the bit mask
953990
value = (register >> bit_index) & bit_mask
954991

955-
elif entry.data_type.value > 200 or entry.data_type == Data_Type.BYTE: #bit types
992+
elif entry.data_type == Data_Type.BYTE: #bit types
993+
value = int.from_bytes(register[:1], byteorder=byte_order, signed=False)
994+
elif entry.data_type.value > 200: #bit types
956995
bit_size = Data_Type.getSize(entry.data_type)
957996
bit_mask = (1 << bit_size) - 1 # Create a mask for extracting X bits
958997
bit_index = entry.register_bit
998+
999+
1000+
if isinstance(register, bytes):
1001+
register = int.from_bytes(register, byteorder=byte_order)
1002+
9591003
value = (register >> bit_index) & bit_mask
1004+
1005+
9601006
elif entry.data_type == Data_Type.HEX:
9611007
value = register.hex() #convert bytes to hex
9621008
elif entry.data_type == Data_Type.ASCII:
@@ -986,6 +1032,11 @@ def process_register_bytes(self, registry : dict[int,bytes], entry : registry_ma
9861032

9871033
def process_register_ushort(self, registry : dict[int, int], entry : registry_map_entry ):
9881034
''' process ushort type registry into data'''
1035+
1036+
byte_order : str = self.byteorder
1037+
if entry.data_byteorder:
1038+
byte_order = entry.data_byteorder
1039+
9891040
if entry.data_type == Data_Type.UINT: #read uint
9901041
if entry.register + 1 not in registry:
9911042
return
@@ -1058,7 +1109,7 @@ def process_register_ushort(self, registry : dict[int, int], entry : registry_ma
10581109
else:
10591110
flags : list[str] = []
10601111
if end_bit > 0:
1061-
end : int = 16 if end_bit >= 16 else end_bit
1112+
end : int = 16 if end_bit >= 16 else end_bit
10621113
for i in range(start_bit, end): # Iterate over each bit position (0 to 15)
10631114
# Check if the i-th bit is set
10641115
if (val >> i) & 1:
@@ -1074,10 +1125,10 @@ def process_register_ushort(self, registry : dict[int, int], entry : registry_ma
10741125
bit_index = entry.register_bit
10751126
value = (registry[entry.register] >> bit_index) & bit_mask
10761127
elif entry.data_type == Data_Type.HEX:
1077-
value = registry[entry.register].to_bytes((16 + 7) // 8, byteorder=self.byteorder) #convert to ushort to bytes
1128+
value = registry[entry.register].to_bytes((16 + 7) // 8, byteorder=byte_order) #convert to ushort to bytes
10781129
value = value.hex() #convert bytes to hex
10791130
elif entry.data_type == Data_Type.ASCII:
1080-
value = registry[entry.register].to_bytes((16 + 7) // 8, byteorder=self.byteorder) #convert to ushort to bytes
1131+
value = registry[entry.register].to_bytes((16 + 7) // 8, byteorder=byte_order) #convert to ushort to bytes
10811132
try:
10821133
value = value.decode("utf-8") #convert bytes to ascii
10831134
except UnicodeDecodeError as e:

0 commit comments

Comments
 (0)