Skip to content

Commit 720bcde

Browse files
committed
fix merge upstream/v1.1.9
2 parents 3f09800 + 1e997e7 commit 720bcde

14 files changed

+2004
-1872
lines changed

.github/workflows/python-3.13.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3+
4+
name: Python 3.13
5+
6+
on:
7+
push:
8+
branches: [ "main" ]
9+
pull_request:
10+
branches: [ "main" ]
11+
12+
jobs:
13+
build:
14+
15+
runs-on: ubuntu-latest
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
python-version: ["3.13"]
20+
21+
steps:
22+
- uses: actions/checkout@v4
23+
- name: Set up Python ${{ matrix.python-version }}
24+
uses: actions/setup-python@v3
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
- name: Install dependencies
28+
run: |
29+
python -m pip install --upgrade pip
30+
python -m pip install flake8 pytest
31+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
32+
- name: Lint with flake8
33+
run: |
34+
# stop the build if there are Python syntax errors or undefined names
35+
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
36+
37+
# don't care about coding standards
38+
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
39+
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
40+
- name: Test with pytest
41+
run: |
42+
pytest

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
![Python 3.10](https://github.com/HotNoob/PythonProtocolGateway/actions/workflows/python-3.10.yml/badge.svg)
44
![Python 3.11](https://github.com/HotNoob/PythonProtocolGateway/actions/workflows/python-3.11.yml/badge.svg)
55
![Python 3.12](https://github.com/HotNoob/PythonProtocolGateway/actions/workflows/python-3.12.yml/badge.svg)
6+
![Python 3.13](https://github.com/HotNoob/PythonProtocolGateway/actions/workflows/python-3.13.yml/badge.svg)
67

78
[![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)
89

classes/protocol_settings.py

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from enum import Enum
44
import glob
55
import logging
6+
import time
67
from typing import Union
78
from defs.common import strtoint
89
import itertools
@@ -11,6 +12,11 @@
1112
import os
1213
import ast
1314

15+
from typing import TYPE_CHECKING
16+
17+
if TYPE_CHECKING:
18+
from configparser import SectionProxy
19+
1420
class Data_Type(Enum):
1521
BYTE = 1
1622
'''8bit byte'''
@@ -206,6 +212,12 @@ class registry_map_entry:
206212
read_command : bytes = None
207213
''' for transports/protocols that require sending a command ontop of "register" '''
208214

215+
read_interval : int = 1000
216+
''' how often to read register in ms'''
217+
218+
next_read_timestamp : int = 0
219+
''' unix timestamp in ms '''
220+
209221
write_mode : WriteMode = WriteMode.READ
210222
''' enable disable reading/writing '''
211223

@@ -240,12 +252,14 @@ class protocol_settings:
240252
settings : dict[str, str]
241253
''' default settings provided by protocol json '''
242254

255+
transport_settings : 'SectionProxy' = None
256+
243257
byteorder : str = "big"
244258

245259
_log : logging.Logger = None
246260

247261

248-
def __init__(self, protocol : str, settings_dir : str = 'protocols'):
262+
def __init__(self, protocol : str, transport_settings : 'SectionProxy' = None, settings_dir : str = 'protocols'):
249263

250264
#apply log level to logger
251265
self._log_level = getattr(logging, logging.getLevelName(logging.getLogger().getEffectiveLevel()), logging.INFO)
@@ -254,6 +268,7 @@ def __init__(self, protocol : str, settings_dir : str = 'protocols'):
254268

255269
self.protocol = protocol
256270
self.settings_dir = settings_dir
271+
self.transport_settings = transport_settings
257272

258273
#load variable mask
259274
self.variable_mask = []
@@ -358,13 +373,22 @@ def load__registry(self, path, registry_type : Registry_Type = Registry_Type.INP
358373
registry_map : list[registry_map_entry] = []
359374
register_regex = re.compile(r'(?P<register>(?:0?x[\da-z]+|[\d]+))\.(b(?P<bit>x?\d{1,2})|(?P<byte>x?\d{1,2}))')
360375

376+
read_interval_regex = re.compile(r'(?P<value>[\.\d]+)(?P<unit>[xs]|ms)')
377+
378+
361379
data_type_regex = re.compile(r'(?P<datatype>\w+)\.(?P<length>\d+)')
362380

363381
range_regex = re.compile(r'(?P<reverse>r|)(?P<start>(?:0?x[\da-z]+|[\d]+))[\-~](?P<end>(?:0?x[\da-z]+|[\d]+))')
364382
ascii_value_regex = re.compile(r'(?P<regex>^\[.+\]$)')
365383
list_regex = re.compile(r'\s*(?:(?P<range_start>(?:0?x[\da-z]+|[\d]+))-(?P<range_end>(?:0?x[\da-z]+|[\d]+))|(?P<element>[^,\s][^,]*?))\s*(?:,|$)')
366384

367385

386+
#load read_interval from transport settings, for #x per register read intervals
387+
transport_read_interval : int = 1000
388+
if self.transport_settings is not None:
389+
transport_read_interval = self.transport_settings.getint("read_interval", transport_read_interval)
390+
391+
368392
if not os.path.exists(path): #return empty is file doesnt exist.
369393
return registry_map
370394

@@ -392,10 +416,38 @@ def process_row(row):
392416
# Initialize variables to hold numeric and character parts
393417
unit_multiplier : float = 1
394418
unit_symbol : str = ''
419+
read_interval : int = 0
420+
''' read interval in ms '''
395421

396422
#clean up doc name, for extra parsing
397423
row['documented name'] = row['documented name'].strip().lower().replace(' ', '_')
398424

425+
#region read_interval
426+
427+
428+
if 'read interval' in row:
429+
row['read interval'] = row['read interval'].lower() #ensure is all lower case
430+
match = read_interval_regex.search(row['read interval'])
431+
if match:
432+
unit = match.group('unit')
433+
value = match.group('value')
434+
if value:
435+
value = float(value)
436+
if unit == 'x':
437+
read_interval = int((transport_read_interval * 1000) * value)
438+
else: # seconds or ms
439+
read_interval = value
440+
if unit != 'ms':
441+
read_interval *= 1000
442+
443+
if read_interval == 0:
444+
read_interval = transport_read_interval * 1000
445+
if "read_interval" in self.settings:
446+
try:
447+
read_interval = int(self.settings['read_interval'])
448+
except ValueError:
449+
read_interval = transport_read_interval * 1000
450+
399451

400452
#region overrides
401453
if overrides is not None:
@@ -589,6 +641,9 @@ def process_row(row):
589641
if "writable" in row:
590642
writeMode = WriteMode.fromString(row['writable'])
591643

644+
if "write" in row:
645+
writeMode = WriteMode.fromString(row['write'])
646+
592647
for i in r:
593648
item = registry_map_entry(
594649
registry_type = registry_type,
@@ -608,6 +663,7 @@ def process_row(row):
608663
value_max=value_max,
609664
value_regex=value_regex,
610665
read_command = read_command,
666+
read_interval=read_interval,
611667
write_mode=writeMode
612668
)
613669
registry_map.append(item)
@@ -706,7 +762,8 @@ def process_row(row):
706762

707763
return registry_map
708764

709-
def calculate_registry_ranges(self, map : list[registry_map_entry], max_register : int) -> list[tuple]:
765+
def calculate_registry_ranges(self, map : list[registry_map_entry], max_register : int, init : bool = False) -> list[tuple]:
766+
710767
''' read optimization; calculate which ranges to read'''
711768
max_batch_size = 45 #see manual; says max batch is 45
712769

@@ -719,6 +776,10 @@ def calculate_registry_ranges(self, map : list[registry_map_entry], max_register
719776
start = -max_batch_size
720777
ranges : list[tuple] = []
721778

779+
timestamp_ms = int(time.time() * 1000)
780+
if init : #hack so that all registers are read initially without adding extra if in loop
781+
timestamp_ms = 0
782+
722783
while (start := start+max_batch_size) <= max_register:
723784

724785
registers : list[int] = [] #use a list, im too lazy to write logic
@@ -731,7 +792,10 @@ def calculate_registry_ranges(self, map : list[registry_map_entry], max_register
731792
if register.write_mode == WriteMode.WRITEONLY: ##Write Only; skip
732793
continue
733794

734-
registers.append(register.register)
795+
#we are assuming calc registry ranges is being called EVERY READ.
796+
if register.next_read_timestamp < timestamp_ms:
797+
register.next_read_timestamp = timestamp_ms + register.read_interval
798+
registers.append(register.register)
735799

736800
if registers: #not empty
737801
ranges.append((min(registers), max(registers)-min(registers)+1)) ## APPENDING A TUPLE!
@@ -781,7 +845,7 @@ def load_registry_map(self, registry_type : Registry_Type, file : str = '', sett
781845
size = item.register
782846

783847
self.registry_map_size[registry_type] = size
784-
self.registry_map_ranges[registry_type] = self.calculate_registry_ranges(self.registry_map[registry_type], self.registry_map_size[registry_type])
848+
self.registry_map_ranges[registry_type] = self.calculate_registry_ranges(self.registry_map[registry_type], self.registry_map_size[registry_type], init=True)
785849

786850
def process_register_bytes(self, registry : dict[int,bytes], entry : registry_map_entry):
787851
''' process bytes into data'''

classes/transports/modbus_base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class modbus_base(transport_base):
4343
send_input_register : bool = True
4444

4545
def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_settings' = None):
46-
super().__init__(settings, protocolSettings=protocolSettings)
46+
super().__init__(settings)
4747

4848
self.analyze_protocol_enabled = settings.getboolean('analyze_protocol', fallback=self.analyze_protocol_enabled)
4949
self.analyze_protocol_save_load = settings.getboolean('analyze_protocol_save_load', fallback=self.analyze_protocol_save_load)
@@ -155,7 +155,10 @@ def read_data(self) -> dict[str, str]:
155155
if registry_type == Registry_Type.HOLDING and not self.send_holding_register:
156156
continue
157157

158-
registry = self.read_modbus_registers(ranges=self.protocolSettings.get_registry_ranges(registry_type=registry_type), registry_type=registry_type)
158+
#calculate ranges dynamically -- for variable read timing
159+
ranges = self.protocolSettings.calculate_registry_ranges(self.protocolSettings.registry_map[registry_type], self.protocolSettings.registry_map_size[registry_type])
160+
161+
registry = self.read_modbus_registers(ranges=ranges, registry_type=registry_type)
159162
new_info = self.protocolSettings.process_registery(registry, self.protocolSettings.get_registry_map(registry_type))
160163

161164
if False:

classes/transports/transport_base.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class transport_base:
3232

3333
_log : logging.Logger = None
3434

35-
def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_settings' = None) -> None:
35+
def __init__(self, settings : 'SectionProxy') -> None:
3636

3737
self.transport_name = settings.name #section name
3838

@@ -45,17 +45,6 @@ def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_setti
4545

4646
self.type = self.__class__.__name__
4747

48-
self.protocolSettings = protocolSettings
49-
if not self.protocolSettings: #if not, attempt to load. lazy i know
50-
self.protocol_version = settings.get('protocol_version')
51-
if self.protocol_version:
52-
self.protocolSettings = protocol_settings(self.protocol_version)
53-
54-
if self.protocolSettings:
55-
self.protocol_version = self.protocolSettings.protocol
56-
57-
#todo, reimplement default settings from protocolsettings
58-
5948
if settings:
6049
self.device_serial_number = settings.get(["device_serial_number", "serial_number"], self.device_serial_number)
6150
self.device_manufacturer = settings.get(["device_manufacturer", "manufacturer"], self.device_manufacturer)
@@ -68,6 +57,17 @@ def __init__(self, settings : 'SectionProxy', protocolSettings : 'protocol_setti
6857
else:
6958
self.write_enabled = settings.getboolean("write", self.write_enabled)
7059

60+
#load a protocol_settings class for every transport; required for adv features. ie, variable timing.
61+
#must load after settings
62+
self.protocol_version = settings.get('protocol_version')
63+
if self.protocol_version:
64+
self.protocolSettings = protocol_settings(self.protocol_version, transport_settings=settings)
65+
66+
if self.protocolSettings:
67+
self.protocol_version = self.protocolSettings.protocol
68+
69+
#todo, reimplement default settings from protocolsettings
70+
7171
self.update_identifier()
7272

7373

documentation/usage/creating_and_editing_protocols.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ The .csv files hold the registry or address definitions.
1111
CSV = comma seperated values... spreadsheets.
1212
delimeter for csv can be , or ; ( not both )
1313

14-
| variable name | data type | register|documented name|description|writable|values |
15-
| -- | -- | -- | -- | -- | -- | -- |
14+
| variable name | data type | register| documented name|description|writable|values | read interval |
15+
| -- | -- | -- | -- | -- | -- | -- | -- |
1616

1717

1818
### variable name
@@ -79,6 +79,28 @@ for example:
7979
the format for these flags is json. these flags / codes can also be defined via the .json file by naming them as such:
8080
"{{document_name}}_codes"
8181

82+
### read interval
83+
provides a per register read interval; the minimum value is the configured transport read interval.
84+
85+
#x for read_interval from transport config * #
86+
#s for plainold seconds
87+
#ms for miliseconds
88+
89+
for example:
90+
```
91+
[transport.modbus]
92+
read_interval = 7
93+
```
94+
95+
```7x```
96+
would set the read interval for that register to 49 seconds.
97+
98+
```7s```
99+
would set the read interval to 7s
100+
101+
```1s```
102+
because the transport read interval is 7 seconds, the read interval would effectively be 7 seconds
103+
82104
##### Bit Flag Example
83105
```
84106
{"b0" : "StandBy", "b1" : "On"}

documentation/usage/protocols.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,29 +37,29 @@ protocol_version = eg4_v58.custom
3737
{protocol_name}.registry_map.csv contains configuration for generic "registers".
3838

3939
### csv format:
40-
https://github.com/HotNoob/PythonProtocolGateway/wiki/Creating-and-Editing-Protocols-%E2%80%90-JSON-%E2%80%90-CSV#csv
40+
[creating_and_editing_protocols.md](creating_and_editing_protocols.md) - Creating and editing protocolss
4141

4242
## egv_v58
4343
```
4444
protocol_version = eg4_v58
4545
```
46-
[Devices\EG4 to MQTT](https://github.com/HotNoob/PythonProtocolGateway/wiki/Devices%5CEG4-to-MQTT)
46+
[Devices\EG4 to MQTT](/documentation/devices/EG4.md)
4747

4848
## v0.14
4949
```
5050
protocol_version = v0.14
5151
```
52-
[Devices\Growatt To MQTT](https://github.com/HotNoob/PythonProtocolGateway/wiki/Devices%5CGrowatt-To-MQTT)
52+
[Devices\Growatt To MQTT](/documentation/devices/Growatt.md)
5353

5454
## sigineer_v0.11
5555

5656
```
5757
protocol_version = sigineer_v0.11
5858
```
59-
[Devices\Sigineer to MQTT](https://github.com/HotNoob/PythonProtocolGateway/wiki/Devices%5CSigineer-to-MQTT)
59+
[Devices\Sigineer to MQTT](/documentation/devices/Sigineer.md)
6060

6161
## pace_bms_v1.3
6262
```
6363
protocol_version = pace_bms_v1.3
6464
```
65-
[Devices\SOK to MQTT](https://github.com/HotNoob/PythonProtocolGateway/wiki/Devices%5CSOK-to-MQTT)
65+
[Devices\SOK to MQTT](/documentation/devices/SOK.md)

0 commit comments

Comments
 (0)