Skip to content

Commit c8ba5c6

Browse files
authored
Merge branch 'main' into parallel_fix
2 parents 349615f + 8cb2048 commit c8ba5c6

File tree

8 files changed

+129
-61
lines changed

8 files changed

+129
-61
lines changed

classes/protocol_settings.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,19 @@ def get_registry_entry(self, name : str, registry_type : Registry_Type) -> regis
338338
return item
339339

340340
return None
341+
342+
def get_code_by_value(self, entry : registry_map_entry, value : str, fallback=None) -> str:
343+
''' case insensitive '''
344+
345+
value = value.strip().lower()
346+
347+
if entry.variable_name+"_codes" in self.codes:
348+
codes = self.codes[entry.variable_name+"_codes"]
349+
for code, val in codes.items():
350+
if value == val.lower():
351+
return code
352+
353+
return fallback
341354

342355
def load__json(self, file : str = "", settings_dir : str = ""):
343356
if not settings_dir:

classes/transports/influxdb_out.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import sys
1+
import logging
22
import os
3-
import json
43
import pickle
5-
from configparser import SectionProxy
6-
from typing import TextIO
74
import time
85
import logging
96
import threading
7+
from configparser import SectionProxy
8+
9+
from influxdb import InfluxDBClient
1010

1111
from defs.common import strtobool
1212

@@ -209,7 +209,6 @@ def connect(self):
209209
self._log.info("influxdb_out connect")
210210

211211
try:
212-
from influxdb import InfluxDBClient
213212

214213
# Create InfluxDB client with timeout settings
215214
self.client = InfluxDBClient(

classes/transports/modbus_base.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,7 @@ def evaluate_score(entry : registry_map_entry, val):
810810
def write_variable(self, entry : registry_map_entry, value : str, registry_type : Registry_Type = Registry_Type.HOLDING):
811811
""" writes a value to a ModBus register; todo: registry_type to handle other write functions"""
812812

813-
value = value.strip()
813+
value = value.strip().lower()
814814

815815
temp_map = [entry]
816816
ranges = self.protocolSettings.calculate_registry_ranges(temp_map, self.protocolSettings.registry_map_size[registry_type], init=True) #init=True to bypass timechecks
@@ -820,6 +820,11 @@ def write_variable(self, entry : registry_map_entry, value : str, registry_type
820820
#current_registers = self.read_modbus_registers(start=entry.register, end=entry.register, registry_type=registry_type)
821821
#current_value = current_registers[entry.register]
822822
current_value = info[entry.variable_name]
823+
824+
825+
#handle codes
826+
value = self.protocolSettings.get_code_by_value(entry, value, fallback=value)
827+
current_value = self.protocolSettings.get_code_by_value(entry, current_value, fallback=current_value)
823828

824829
if not self.write_mode == TransportWriteMode.UNSAFE:
825830
if not self.protocolSettings.validate_registry_entry(entry, current_value):
@@ -830,14 +835,6 @@ def write_variable(self, entry : registry_map_entry, value : str, registry_type
830835
if not self.protocolSettings.validate_registry_entry(entry, value):
831836
return self._log.error(f"WRITE_ERROR: Invalid new value, '{value}'. Unsafe to write")
832837

833-
#handle codes
834-
if entry.variable_name+"_codes" in self.protocolSettings.codes:
835-
codes = self.protocolSettings.codes[entry.variable_name+"_codes"]
836-
for key, val in codes.items():
837-
if val == value: #convert "string" to key value
838-
value = key
839-
break
840-
841838
#apply unit_mod before writing.
842839
if entry.unit_mod != 1:
843840
value = int(float(value) / entry.unit_mod) # say unitmod is 0.1. 105*0.1 = 10.5. 10.5 / 0.1 = 105.

classes/transports/transport_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def __init__(self, settings : "SectionProxy") -> None:
112112

113113
#load a protocol_settings class for every transport; required for adv features. ie, variable timing.
114114
#must load after settings
115-
self.protocol_version = settings.get("protocol_version")
115+
self.protocol_version = settings.get("protocol_version", fallback='')
116116
if self.protocol_version:
117117
# Create a deep copy of protocol settings to avoid shared state between transports
118118
original_protocol_settings = protocol_settings(self.protocol_version, transport_settings=settings)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Brand,Model,Protocol,Transport,ReadMe
2+
AOLithium,51.2V 100Ah,voltronic_bms_2020_03_25,ModBus,https://github.com/HotNoob/PythonProtocolGateway/blob/main/documentation/devices/AOLithium.md
3+
AOLithium,51.2V 100Ah,victron_gx_generic_canbus,CanBus,https://github.com/HotNoob/PythonProtocolGateway/blob/main/documentation/devices/AOLithium.md
4+
EG4,EG4 18kPV,eg4_v58,ModBus,
5+
EG4,EG4 3000EHV ,eg4_3000ehv_v1 ,ModBus,
6+
EG4,EG4 6000XP ,eg4_v58,ModBus,
7+
EG4,MPPT100-48HV – unconfirmed,eg4_3000ehv_v1 ,ModBus,
8+
Growatt,SPF 12000T DVM-US MPV,v0.14,ModBus,
9+
Growatt,SPF 5000 ,v0.14,ModBus,
10+
Growatt,SPF 6000 ,v0.14,ModBus,
11+
Selphos,v3,growatt_bms_canbus_v1.04 ,CanBus,https://github.com/HotNoob/PythonProtocolGateway/discussions/88
12+
Sigineer,M3000H-48LV-3KW ,sigineer_v0.11,ModBus,https://github.com/HotNoob/PythonProtocolGateway/blob/main/documentation/devices/Sigineer.md
13+
SOK,48v100ah,pace_bms_v1.3 ,ModBus,https://github.com/HotNoob/PythonProtocolGateway/blob/main/documentation/devices/SOK.md
14+
SolArk,Untested,solark_v1.1 ,ModBus,
15+
SRNE,ASF48100S200-H ,srne_v3.9,ModBus,
16+
SRNE,HF2430U60-100 ,srne_v3.9,ModBus,

protocol_gateway.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from classes.protocol_settings import protocol_settings, registry_map_entry
3030
from classes.transports.transport_base import transport_base
31+
from defs.common import strtobool
3132

3233
__logo = """
3334
@@ -50,13 +51,13 @@
5051

5152
class CustomConfigParser(ConfigParser):
5253
def get(self, section, option, *args, **kwargs):
53-
if isinstance(option, list):
54-
fallback = None
54+
fallback = None
5555

56-
if "fallback" in kwargs: #override kwargs fallback, for manually handling here
57-
fallback = kwargs["fallback"]
58-
kwargs["fallback"] = None
56+
if "fallback" in kwargs: #override kwargs fallback, for manually handling here
57+
fallback = kwargs["fallback"]
58+
kwargs["fallback"] = None
5959

60+
if isinstance(option, list):
6061
for name in option:
6162
try:
6263
value = super().get(section, name, *args, **kwargs)
@@ -65,14 +66,20 @@ def get(self, section, option, *args, **kwargs):
6566

6667
if value:
6768
break
69+
else:
70+
try:
71+
value = super().get(section, option, *args, **kwargs)
72+
except NoOptionError:
73+
value = None
6874

69-
if not value:
70-
value = fallback
75+
if not value: #apply fallback
76+
value = fallback
7177

72-
if value is None:
78+
if value is None:
79+
if isinstance(option, list):
7380
raise NoOptionError(option[0], section)
74-
else:
75-
value = super().get(section, option, *args, **kwargs)
81+
else:
82+
raise NoOptionError(option, section)
7683

7784
if isinstance(value, int):
7885
return value
@@ -95,6 +102,10 @@ def getint(self, section, option, *args, **kwargs): #bypass fallback bug
95102
def getfloat(self, section, option, *args, **kwargs): #bypass fallback bug
96103
value = self.get(section, option, *args, **kwargs)
97104
return float(value) if value is not None else None
105+
106+
def getboolean(self, section, option, *args, **kwargs): #bypass fallback bug
107+
value = self.get(section, option, *args, **kwargs)
108+
return strtobool(value)
98109

99110
def getboolean(self, section, option, *args, **kwargs): #bypass fallback bug and handle case-insensitive boolean values
100111
value = self.get(section, option, *args, **kwargs)

protocols/srne/srne_2021_v1.96.holding_registry_map.csv

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,44 @@ variable name,data type,register,documented name,description,writable,values,uni
4545
,,0x0233,Load Phase_C active power,,R,,W,
4646
,,0x0235,Load Phase_C apparent power,,R,,VA,
4747
,,0x0237,Load Phase_C ratio,,R,0~100,%,
48-
,BYTE,0xF000,Stats PVEnergyYesterday,,R,,0.1kWh,
49-
,BYTE,0xF001,Stats PVEnergy2Dayago,,R,,0.1kWh,
50-
,BYTE,0xF002,Stats PVEnergy3Dayago,,R,,0.1kWh,
51-
,BYTE,0xF003,Stats PVEnergy4Dayago,,R,,0.1kWh,
52-
,BYTE,0xF004,Stats PVEnergy5Dayago,,R,,0.1kWh,
53-
,BYTE,0xF005,Stats PVEnergy6Dayago,,R,,0.1kWh,
54-
,BYTE,0xF006,Stats PVEnergy7Dayago,,R,,0.1kWh,
55-
,BYTE,0xF02C,Stats GenerateEnergyToGridTday,,R,,0.1kWh,
56-
,BYTE,0xF02D,Stats BatChgTday,,R,,1AH,
57-
,BYTE,0xF02E,Stats BatDischgTday,,R,,1AH,
58-
,BYTE,0xF02F,Stats GenerateEnergyTday,,R,,0.1kWh,
59-
,BYTE,0xF030,Stats UsedEnergyTday,,R,,0.1kWh,
60-
,BYTE,0xF031,Stats WorkDaysTotal,,R,,1d,
61-
,BYTE,0xF038,Stats GeneratEnergyTotal,,R,,0.1kWh,
62-
,BYTE,0xF03C,Stats GridChgEnergyTday,,R,,1AH,
63-
,BYTE,0xF03D,Stats LoadConsumLineTday,,R,,0.1kWh,
64-
,BYTE,0xF03E,Stats InvWorkTimeTday,,R,,1min,
65-
,BYTE,0xF03F,Stats GridWorkTimeTday,,R,,1min,
66-
,BYTE,0xF04A,Stats InvWorkTimeTotal,,R,,1h,
67-
,BYTE,0xF04B,Stats GridWorkTimeTotal,,R,,1h,
48+
,,0xF000,Stats PVEnergyYesterday,,R,,0.1kWh,
49+
,,0xF001,Stats PVEnergy2Dayago,,R,,0.1kWh,
50+
,,0xF002,Stats PVEnergy3Dayago,,R,,0.1kWh,
51+
,,0xF003,Stats PVEnergy4Dayago,,R,,0.1kWh,
52+
,,0xF004,Stats PVEnergy5Dayago,,R,,0.1kWh,
53+
,,0xF005,Stats PVEnergy6Dayago,,R,,0.1kWh,
54+
,,0xF006,Stats PVEnergy7Dayago,,R,,0.1kWh,
55+
,,0xF007,Stats BatChgEnergyYesterday,,R,,1AH,
56+
,,0xF008,Stats BatChgEnergy2Dayago,,R,,1AH,
57+
,,0xF009,Stats BatChgEnergy3Dayago,,R,,1AH,
58+
,,0xF00A,Stats BatChgEnergy4Dayago,,R,,1AH,
59+
,,0xF00B,Stats BatChgEnergy5Dayago,,R,,1AH,
60+
,,0xF00C,Stats BatChgEnergy6Dayago,,R,,1AH,
61+
,,0xF00D,Stats BatChgEnergy7Dayago,,R,,1AH,
62+
,,0xF00E,Stats BatDischgEnergyYesterday,,R,,1AH,
63+
,,0xF00F,Stats BatDischgEnergy2Dayago,,R,,1AH,
64+
,,0xF010,Stats BatDischgEnergy3Dayago,,R,,1AH,
65+
,,0xF011,Stats BatDischgEnergy4Dayago,,R,,1AH,
66+
,,0xF012,Stats BatDischgEnergy5Dayago,,R,,1AH,
67+
,,0xF013,Stats BatDischgEnergy6Dayago,,R,,1AH,
68+
,,0xF014,Stats BatDischgEnergy7Dayago,,R,,1AH,
69+
,,0xF015,Stats GridChgEnergyYesterday,,R,,1AH,
70+
,,0xF016,Stats GridChgEnergy2Dayago,,R,,1AH,
71+
,,0xF017,Stats GridChgEnergy3Dayago,,R,,1AH,
72+
,,0xF018,Stats GridChgEnergy4Dayago,,R,,1AH,
73+
,,0xF019,Stats GridChgEnergy5Dayago,,R,,1AH,
74+
,,0xF01A,Stats GridChgEnergy6Dayago,,R,,1AH,
75+
,,0xF01B,Stats GridChgEnergy7Dayago,,R,,1AH,
76+
,,0xF02C,Stats GenerateEnergyToGridTday,,R,,0.1kWh,
77+
,,0xF02D,Stats BatChgTday,,R,,1AH,
78+
,,0xF02E,Stats BatDischgTday,,R,,1AH,
79+
,,0xF02F,Stats GenerateEnergyTday,,R,,0.1kWh,
80+
,,0xF030,Stats UsedEnergyTday,,R,,0.1kWh,
81+
,,0xF031,Stats WorkDaysTotal,,R,,1d,
82+
,,0xF038,Stats GeneratEnergyTotal,,R,,0.1kWh,
83+
,,0xF03C,Stats GridChgEnergyTday,,R,,1AH,
84+
,,0xF03D,Stats GridLoadConsumTday,,R,,0.1kWh,
85+
,,0xF03E,Stats InvWorkTimeTday,,R,,1min,
86+
,,0xF03F,Stats GridWorkTimeTday,,R,,1min,
87+
,,0xF04A,Stats InvWorkTimeTotal,,R,,1h,
88+
,,0xF04B,Stats GridWorkTimeTotal,,R,,1h,

pytests/test_influxdb_out.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
Test for InfluxDB output transport
44
"""
55

6+
import time
67
import unittest
7-
from unittest.mock import Mock, patch, MagicMock
8-
from configparser import ConfigParser
8+
from protocol_gateway import CustomConfigParser as ConfigParser
9+
from unittest.mock import MagicMock, Mock, patch
910

1011
from classes.transports.influxdb_out import influxdb_out
1112

@@ -22,24 +23,27 @@ def setUp(self):
2223
self.config.set('influxdb_output', 'port', '8086')
2324
self.config.set('influxdb_output', 'database', 'test_db')
2425

26+
#@patch('classes.transports.influxdb_out.InfluxDBClient')
27+
#@patch('classes.transports.influxdb_out.InfluxDBClient')
2528
@patch('classes.transports.influxdb_out.InfluxDBClient')
2629
def test_connect_success(self, mock_influxdb_client):
2730
"""Test successful connection to InfluxDB"""
2831
# Mock the InfluxDB client
2932
mock_client = Mock()
3033
mock_influxdb_client.return_value = mock_client
3134
mock_client.get_list_database.return_value = [{'name': 'test_db'}]
32-
35+
3336
transport = influxdb_out(self.config['influxdb_output'])
3437
transport.connect()
35-
38+
3639
self.assertTrue(transport.connected)
3740
mock_influxdb_client.assert_called_once_with(
3841
host='localhost',
3942
port=8086,
4043
username=None,
4144
password=None,
42-
database='test_db'
45+
database='test_db',
46+
timeout=10
4347
)
4448

4549
@patch('classes.transports.influxdb_out.InfluxDBClient')
@@ -49,10 +53,10 @@ def test_connect_database_creation(self, mock_influxdb_client):
4953
mock_client = Mock()
5054
mock_influxdb_client.return_value = mock_client
5155
mock_client.get_list_database.return_value = [{'name': 'other_db'}]
52-
56+
5357
transport = influxdb_out(self.config['influxdb_output'])
5458
transport.connect()
55-
59+
5660
self.assertTrue(transport.connected)
5761
mock_client.create_database.assert_called_once_with('test_db')
5862

@@ -63,10 +67,11 @@ def test_write_data_batching(self, mock_influxdb_client):
6367
mock_client = Mock()
6468
mock_influxdb_client.return_value = mock_client
6569
mock_client.get_list_database.return_value = [{'name': 'test_db'}]
66-
70+
6771
transport = influxdb_out(self.config['influxdb_output'])
6872
transport.connect()
69-
73+
74+
7075
# Mock source transport
7176
source_transport = Mock()
7277
source_transport.transport_name = 'test_source'
@@ -75,21 +80,27 @@ def test_write_data_batching(self, mock_influxdb_client):
7580
source_transport.device_manufacturer = 'Test Manufacturer'
7681
source_transport.device_model = 'Test Model'
7782
source_transport.device_serial_number = '123456'
78-
83+
84+
mock_protocol_settings = Mock()
85+
mock_protocol_settings.get_registry_map.return_value = [] # or list of entries if you want
86+
source_transport.protocolSettings = mock_protocol_settings
87+
7988
# Test data
8089
test_data = {'battery_voltage': '48.5', 'battery_current': '10.2'}
81-
90+
91+
transport.last_batch_time = time.time() #stop "flush" from happening and failing test
92+
transport.batch_timeout = 21
8293
transport.write_data(test_data, source_transport)
83-
94+
8495
# Check that data was added to batch
8596
self.assertEqual(len(transport.batch_points), 1)
8697
point = transport.batch_points[0]
87-
98+
8899
self.assertEqual(point['measurement'], 'device_data')
89100
self.assertIn('device_identifier', point['tags'])
90101
self.assertIn('battery_voltage', point['fields'])
91102
self.assertIn('battery_current', point['fields'])
92-
103+
93104
# Check data type conversion
94105
self.assertEqual(point['fields']['battery_voltage'], 48.5)
95106
self.assertEqual(point['fields']['battery_current'], 10.2)
@@ -102,9 +113,9 @@ def test_configuration_options(self):
102113
self.config.set('influxdb_output', 'measurement', 'custom_measurement')
103114
self.config.set('influxdb_output', 'batch_size', '50')
104115
self.config.set('influxdb_output', 'batch_timeout', '5.0')
105-
116+
106117
transport = influxdb_out(self.config['influxdb_output'])
107-
118+
108119
self.assertEqual(transport.username, 'admin')
109120
self.assertEqual(transport.password, 'secret')
110121
self.assertEqual(transport.measurement, 'custom_measurement')

0 commit comments

Comments
 (0)