Skip to content

Commit

Permalink
Add Support for Q.VOLT HYB-G3-3P (#57)
Browse files Browse the repository at this point in the history
* Add QVOLTHYBG33P Inverter

* Use Post with body for QVOLTHYBG33P

* Fix kwh typo

* Fix linting issues

* Move inverter/battery modes to QVOLTHYBG33P

Co-authored-by: Kraus, Vadim <vadim.kraus@umlaut.com>
  • Loading branch information
VadimKraus and VadimKraus authored Feb 21, 2022
1 parent 883cdd9 commit eefc446
Show file tree
Hide file tree
Showing 2 changed files with 282 additions and 4 deletions.
207 changes: 206 additions & 1 deletion solax/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,210 @@ def schema(cls):
return cls.__schema


class QVOLTHYBG33P(InverterPost):
"""
QCells
Q.VOLT HYB-G3-3P
"""
class Processors:
"""
Postprocessors used only in the QVOLTHYBG33P inverter sensor_map.
"""
@staticmethod
def inverter_modes(value, *_args, **_kwargs):
return {
0: "Waiting",
1: "Checking",
2: "Normal",
3: "Off",
4: "Permanent Fault",
5: "Updating",
6: "EPS Check",
7: "EPS Mode",
8: "Self Test",
9: "Idle",
10: "Standby"
}.get(value, f"unmapped value '{value}'")

@staticmethod
def battery_modes(value, *_args, **_kwargs):
return {
0: "Self Use Mode",
1: "Force Time Use",
2: "Back Up Mode",
3: "Feed-in Priority",
}.get(value, f"unmapped value '{value}'")

__schema = vol.Schema({
vol.Required('type'): vol.All(int, 14),
vol.Required('sn'): str,
vol.Required('ver'): str,
vol.Required('Data'): vol.Schema(
vol.All(
[vol.Coerce(float)],
vol.Length(min=200, max=200),
)
),
vol.Required('Information'): vol.Schema(
vol.All(
vol.Length(min=10, max=10)
)
),
}, extra=vol.REMOVE_EXTRA)

__sensor_map = {

'Network Voltage Phase 1': (0, 'V', div10),
'Network Voltage Phase 2': (1, 'V', div10),
'Network Voltage Phase 3': (2, 'V', div10),

'Output Current Phase 1': (3, 'A', twoway_div10),
'Output Current Phase 2': (4, 'A', twoway_div10),
'Output Current Phase 3': (5, 'A', twoway_div10),

'Power Now Phase 1': (6, 'W', to_signed),
'Power Now Phase 2': (7, 'W', to_signed),
'Power Now Phase 3': (8, 'W', to_signed),

'AC Power': (9, 'W', to_signed),

'PV1 Voltage': (10, 'V', div10),
'PV2 Voltage': (11, 'V', div10),

'PV1 Current': (12, 'A', div10),
'PV2 Current': (13, 'A', div10),

'PV1 Power': (14, 'W'),
'PV2 Power': (15, 'W'),

'Grid Frequency Phase 1': (16, 'Hz', div100),
'Grid Frequency Phase 2': (17, 'Hz', div100),
'Grid Frequency Phase 3': (18, 'Hz', div100),

'Inverter Operation mode': (19, '',
Processors.inverter_modes),
# 20 - 32: always 0
# 33: always 1
# instead of to_signed this is actually 34 - 35,
# because 35 = if 34>32767: 0 else: 65535
'Exported Power': (34, 'W', to_signed),
# 35: if 34>32767: 0 else: 65535
# 36 - 38 : always 0
'Battery Voltage': (39, 'V', div100),
'Battery Current': (40, 'A', twoway_div100),
'Battery Power': (41, 'W', to_signed),
# 42: div10, almost identical to [39]
# 43: twoway_div10, almost the same as "40" (battery current)
# 44: twoway_div100, almost the same as "41" (battery power),
# 45: always 1
# 46: follows PV Output, idles around 44, peaks at 52,
'Power Now': (47, 'W', to_signed),
# 48: always 256
# 49,50: [49] + [50] * 15160 some increasing counter
# 51: always 5634
# 52: always 100
# 53: always 0
# 54: follows PV Output, idles around 35, peaks at 54,
# 55-67: always 0
'Total Energy': (68, 'kWh', total_energy),
'Total Energy Resets': (69, ''),
# 70: div10, today's energy including battery usage
# 71-73: 0
'Total Battery Discharge Energy': (74, 'kWh', discharge_energy),
'Total Battery Discharge Energy Resets': (75, ''),
'Total Battery Charge Energy': (76, 'kWh', charge_energy),
'Total Battery Charge Energy Resets': (77, ''),
'Today\'s Battery Discharge Energy': (78, 'kWh', div10),
'Today\'s Battery Charge Energy': (79, 'kWh', div10),
'Total PV Energy': (80, 'kWh', pv_energy),
'Total PV Energy Resets': (81, ''),
'Today\'s Energy': (82, 'kWh', div10),
# 83-85: always 0
'Total Feed-in Energy': (86, 'kWh', feedin_energy),
'Total Feed-in Energy Resets': (87, ''),
'Total Consumption': (88, 'kWh', consumption),
'Total Consumption Resets': (89, ''),
'Today\'s Feed-in Energy': (90, 'kWh', div100),
# 91: always 0
'Today\'s Consumption': (92, 'kWh', div100),
# 93-101: always 0
# 102: always 1
'Battery Remaining Capacity': (103, '%'),
# 104: always 1
'Battery Temperature': (105, 'C'),
'Battery Remaining Energy': (106, 'kWh', div10),
# 107: always 256 or 0
# 108: always 3504
# 109: always 2400
# 110: around rise to 300 if battery not full, 0 if battery is full
# 112, 113: range [250,350]; looks like 113 + offset = 112,
# peaks if battery is full
# 114, 115: something around 33; Some temperature?!
# 116: increases slowly [2,5]
# 117-121: 1620 773 12850 12850 12850
# 122-124: always 0
# 125,126: some curve, look very similar to "42"(Battery Power),
# with offset around 15
# 127,128 resetting counter /1000, around battery charge + discharge
# 164,165,166 some curves
'Battery Operation mode': (168, '',
Processors.battery_modes),
# 169: div100 same as [39]
# 170-199: always 0

}

@classmethod
def sensor_map(cls):
"""
Return sensor map
"""
sensors = {}
for name, (idx, unit, *_) in cls.__sensor_map.items():
sensors[name] = (idx, unit)
return sensors

@classmethod
def postprocess_map(cls):
"""
Return postprocessing map
"""
sensors = {}
for name, (_, _, *processor) in cls.__sensor_map.items():
if processor:
sensors[name] = processor[0]
return sensors

@classmethod
def schema(cls):
return cls.__schema

@classmethod
async def make_request(cls, host, port=80, pwd='', headers=None):

url = f'http://{host}:{port}/'
data = f'optType=ReadRealTimeData&pwd={pwd}'

async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, data=data) as req:
resp = await req.read()

raw_json = resp.decode("utf-8")
json_response = json.loads(raw_json)
response = {}
try:
response = cls.schema()(json_response)
except (Invalid, MultipleInvalid) as ex:
_ = humanize_error(json_response, ex)
raise
return InverterResponse(
data=cls.map_response(response['Data']),
serial_number=response.get('SN', response.get('sn')),
version=response['ver'],
type=response['type']
)


class X1(InverterPost):
__schema = vol.Schema({
vol.Required('type'): vol.All(
Expand Down Expand Up @@ -675,4 +879,5 @@ def schema(cls):


# registry of inverters
REGISTRY = [XHybrid, X3, X3V34, X1, X1Mini, X1MiniV34, X1Smart]
REGISTRY = [XHybrid, X3, X3V34, X1, X1Mini, X1MiniV34, X1Smart,
QVOLTHYBG33P]
79 changes: 76 additions & 3 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,28 @@
"Information": [8.000, 5, "XXXXXXXX", 1, 4.60, 0.00, 4.42, 1.05, 0.00, 1]
}

QVOLTHYBG33P_RESPONSE_V34 = {
"sn": "SWX***",
"ver": "2.034.06",
"type": 14,
"Data": [
2214, 2238, 2251, 11, 10, 12, 162, 136, 146, 444, 5662, 5682, 18, 17,
1050, 977, 5002, 5001, 5002, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 65529, 65535, 0, 0, 0, 32340, 500, 1616, 3236, 50, 1618, 1, 50, 451,
256, 3841, 4875, 5634, 100, 0, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 2190, 0, 36, 1, 0, 1, 738, 0, 904, 0, 0, 81, 2316, 0, 118, 0, 0, 0,
10794, 0, 14544, 0, 166, 0, 466, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 95, 1,
35, 88, 256, 3504, 2400, 115, 300, 352, 325, 34, 34, 8, 1620, 773,
12850, 12850, 12850, 0, 0, 0, 3389, 3384, 33876, 2, 20564, 12339,
18497, 12599, 18743, 12356, 14386, 20564, 12339, 18498, 12600, 18740,
12356, 12855, 20564, 12339, 18498, 12600, 18740, 12612, 14642, 20564,
12339, 18498, 12600, 18740, 12356, 13877, 0, 0, 0, 0, 0, 0, 0, 3843,
2306, 1282, 257, 0, 32340, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
],
"Information": [12.0, 14, "H34***", 1, 1.15, 0.0, 1.14, 1.07, 0.0, 1]
}

X3_VALUES = {
'PV1 Current': 0,
'PV2 Current': 1,
Expand Down Expand Up @@ -317,8 +339,6 @@
'Power Now Phase 2': 44,
'Power Now Phase 3': 45,



'EPS Voltage': 53,
'EPS Current': 54,
'EPS Power': 55,
Expand Down Expand Up @@ -641,6 +661,50 @@
'Total Consumption': 81.84,
}

QVOLTHYBG33P_VALUES = {
'Network Voltage Phase 1': 221.4,
'Network Voltage Phase 2': 223.8,
'Network Voltage Phase 3': 225.1,
'Output Current Phase 1': 1.1,
'Output Current Phase 2': 1.0,
'Output Current Phase 3': 1.2,
'Power Now Phase 1': 162.0,
'Power Now Phase 2': 136.0,
'Power Now Phase 3': 146.0,
'AC Power': 444.0,
'PV1 Voltage': 566.2, 'PV2 Voltage': 568.2,
'PV1 Current': 1.8, 'PV2 Current': 1.7,
'PV1 Power': 1050.0,
'PV2 Power': 977.0, 'Grid Frequency Phase 1': 50.02,
'Grid Frequency Phase 2': 50.01,
'Grid Frequency Phase 3': 50.02,
'Inverter Operation mode': 'Normal',
'Exported Power': -6.0, 'Battery Voltage': 323.4,
'Battery Current': 5.0,
'Battery Power': 1616.0, 'Power Now': 451.0,
'Total Energy': 219.0,
'Total Energy Resets': 0.0,
'Total Battery Discharge Energy': 73.8,
'Total Battery Discharge Energy Resets': 0.0,
'Total Battery Charge Energy': 90.4,
'Total Battery Charge Energy Resets': 0.0,
"Today's Battery Discharge Energy": 0.0,
"Today's Battery Charge Energy": 8.1,
'Total PV Energy': 231.6,
'Total PV Energy Resets': 0.0,
"Today's Energy": 11.8,
'Total Feed-in Energy': 107.94,
'Total Feed-in Energy Resets': 0.0,
'Total Consumption': 145.44,
'Total Consumption Resets': 0.0,
"Today's Feed-in Energy": 1.66,
"Today's Consumption": 4.66,
'Battery Remaining Capacity': 95.0,
'Battery Temperature': 35.0,
'Battery Remaining Energy': 8.8,
'Battery Operation mode': 'Self Use Mode'
}


@pytest.fixture()
def simple_http_fixture(httpserver):
Expand Down Expand Up @@ -774,7 +838,16 @@ def simple_http_fixture(httpserver):
inverter=inverter.X3V34,
values=X3V34_HYBRID_VALUES_EPS_MODE,
headers=None,
)
),
InverterUnderTest(
uri="/",
method='POST',
query_string='',
response=QVOLTHYBG33P_RESPONSE_V34,
inverter=inverter.QVOLTHYBG33P,
values=QVOLTHYBG33P_VALUES,
headers=None,
),
]


Expand Down

0 comments on commit eefc446

Please sign in to comment.