Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Q.Volt HYB-G3-1P #95

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
entry_points={
"solax.inverter": [
"qvolt_hyb_g3_3p = solax.inverters.qvolt_hyb_g3_3p:QVOLTHYBG33P",
"qvolt_hyb_g3_1p = solax.inverters.qvolt_hyb_g3_1p:QVOLTHYBG31P",
"x1 = solax.inverters.x1:X1",
"x1_boost = solax.inverters.x1_boost:X1Boost",
"x1_hybrid_gen4 = solax.inverters.x1_hybrid_gen4:X1HybridGen4",
Expand Down
2 changes: 2 additions & 0 deletions solax/inverters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .qvolt_hyb_g3_1p import QVOLTHYBG31P
from .qvolt_hyb_g3_3p import QVOLTHYBG33P
from .x1 import X1
from .x1_boost import X1Boost
Expand All @@ -13,6 +14,7 @@

__all__ = [
"QVOLTHYBG33P",
"QVOLTHYBG31P",
"XHybrid",
"X1",
"X1Mini",
Expand Down
172 changes: 172 additions & 0 deletions solax/inverters/qvolt_hyb_g3_1p.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
from typing import Any, Dict, Optional

import voluptuous as vol

from solax.inverter import Inverter, InverterHttpClient
from solax.units import DailyTotal, Measurement, Total, Units
from solax.utils import (
div10,
div100,
div1000,
pack_u16,
to_signed,
twoway_div10,
twoway_div100,
)


class QVOLTHYBG31P(Inverter):
"""
QCells
Q.VOLT HYB-G3-1P
"""

class Processors:
"""
Postprocessors used only in the QVOLTHYBG31P inverter sensor_map.
"""

# pylint: disable=duplicate-code
@staticmethod
def inverter_modes(value):
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}'")

# pylint: disable=duplicate-code
@staticmethod
def battery_modes(value):
return {
0: "Self Use Mode",
1: "Force Time Use",
2: "Back Up Mode",
3: "Feed-in Priority",
}.get(value, f"unmapped value '{value}'")

def __init__(self, http_client: InverterHttpClient, *args, **kwargs):
super().__init__(http_client, *args, **kwargs)
self.manufacturer = "Qcells"

# pylint: disable=duplicate-code
_schema = vol.Schema(
{
vol.Required("type"): vol.All(int, 15),
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,
)

@classmethod
def response_decoder(cls):
return {
"Network Voltage": (0, Units.V, div10),
"Inverter output current": (1, Units.A, twoway_div10),
"Inverter output power": (2, Units.W, to_signed),
"Grid Frequency": (3, Units.HZ, div100),
"PV1 Voltage": (4, Units.V, div10),
"PV2 Voltage": (5, Units.V, div10),
"PV1 Current": (6, Units.A, div10),
"PV2 Current": (7, Units.A, div10),
"PV1 Power": (8, Units.W),
"PV2 Power": (9, Units.W),
"Inverter status": (10, Units.NONE, cls.Processors.inverter_modes),
"Total inverter yield": (pack_u16(11, 12), Total(Units.KWH), div10),
"Today's inverter yield": (13, DailyTotal(Units.KWH), div10),
"Battery Voltage": (14, Units.V, div100),
"Battery Current": (15, Units.A, twoway_div100),
"Battery Power": (16, Units.W, to_signed),
"Battery Temperature": (17, Units.C),
"Battery Remaining Capacity": (18, Units.PERCENT),
"Total battery discharged energy": (
pack_u16(19, 20),
Total(Units.KWH),
div10,
),
"Total battery charged energy": (pack_u16(21, 22), Total(Units.KWH), div10),
"Battery Remaining Energy": (
23,
Measurement(Units.KWH, storage=True),
div10,
),
# 24: always 100. probably 10.0%, minimum charge left before switching to grid
# 25: always 0
# 26-27: jumping around
# 28-31: always 0
"Current grid power": (32, Units.W, to_signed),
# 33: if [32] > 32767 : 65535 ? 0
"Total grid export": (pack_u16(34, 35), Total(Units.KWH), div100),
"Total grid import": (pack_u16(36, 37), Total(Units.KWH), div100),
"Current power usage": (38, Units.W, to_signed),
# 39: observed range 0-38. something about the inverter
# 40: 256 when inverter is running, 0 when it's idle
# 41-42: fixed values
# 43-49: no idea, varying values
# 50-51: always 1. possibly an inverter setting
# 52: idk, max 4, min 0
# 53: always 0
"Total self-used solar": (pack_u16(54, 55), Total(Units.KWH), div10),
# 56-57: 32 bit pack? mostly negative, went positive briefly
# 58-59: 32 bit pack? always negative, between -144 and -175
# 60-69: always 0
# 70: fluctuates quite wildly
# 72-73: 32 bit pack?
# 74-77: fixed values
"Today's grid export": (pack_u16(78, 79), DailyTotal(Units.KWH), div100),
"Today's grid import": (pack_u16(80, 81), DailyTotal(Units.KWH), div100),
# 82-84: always 0
"Today's solar yield": (85, DailyTotal(Units.KWH), div10),
# 86: something daily
# 87: something else daily
# 88-89: always 0
# 90: min 350, max 360
# 91: always 0
# 92: always 230
# 93-95: something daily
# 97-98: always 0
# 99-100: weird counter. rtc?
# 101-109: fixed values
# 110: battery type!?
# 111-115: mysterious stuff
"Total battery energy throughput": (
pack_u16(116, 117),
Measurement(Units.KWH, storage=True),
div1000,
),
# 118-124: BMS serial number, ASCII, little-endian, 2 chars per register
# 125-131: Battery 1 serial number, ASCII, little-endian, 2 chars per register
# 132-138: Battery 2 serial number, ASCII, little-endian, 2 chars per register
# 139-145: Battery 3 serial number, ASCII, little-endian, 2 chars per register
# 146-152: Battery 4 serial number, ASCII, little-endian, 2 chars per register
# 153-156: ???
"Battery Operation mode": (157, Units.NONE, cls.Processors.battery_modes),
# 158 - 199: always 0
}

@classmethod
def inverter_serial_number_getter(cls, response: Dict[str, Any]) -> Optional[str]:
return response["information"][2]

@classmethod
def build_all_variants(cls, host, port, pwd=""):
versions = [cls._build(host, port, pwd, False)]
return versions
3 changes: 3 additions & 0 deletions solax/inverters/qvolt_hyb_g3_3p.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Processors:
Postprocessors used only in the QVOLTHYBG33P inverter sensor_map.
"""

# pylint: disable=duplicate-code
@staticmethod
def inverter_modes(value):
return {
Expand All @@ -34,6 +35,7 @@ def inverter_modes(value):
10: "Standby",
}.get(value, f"unmapped value '{value}'")

# pylint: disable=duplicate-code
@staticmethod
def battery_modes(value):
return {
Expand All @@ -47,6 +49,7 @@ def __init__(self, http_client: InverterHttpClient, *args, **kwargs):
super().__init__(http_client, *args, **kwargs)
self.manufacturer = "Qcells"

# pylint: disable=duplicate-code
_schema = vol.Schema(
{
vol.Required("type"): vol.All(int, 14),
Expand Down
1 change: 1 addition & 0 deletions solax/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Units(Enum):

W = "W"
KWH = "kWh"
WH = "Wh"
A = "A"
V = "V"
C = "°C"
Expand Down
4 changes: 4 additions & 0 deletions solax/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ def div100(val):
return val / 100


def div1000(val):
return val / 1000


INT16_MAX = 0x7FFF
INT32_MAX = 0x7FFFFFFF

Expand Down
12 changes: 12 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import solax.inverters as inverter
from tests.samples.expected_values import (
QVOLTHYBG31P_VALUES,
QVOLTHYBG33P_VALUES,
X1_BOOST_VALUES,
X1_BOOST_VALUES_OVERFLOWN,
Expand All @@ -23,6 +24,7 @@
XHYBRID_VALUES,
)
from tests.samples.responses import (
QVOLTHYBG31P_RESPONSE,
QVOLTHYBG33P_RESPONSE_V34,
X1_BOOST_AIR_MINI_RESPONSE,
X1_BOOST_RESPONSE,
Expand Down Expand Up @@ -262,6 +264,16 @@ def simple_http_fixture(httpserver):
headers=None,
data="optType=ReadRealTimeData",
),
InverterUnderTest(
uri="/",
method="POST",
query_string="",
response=QVOLTHYBG31P_RESPONSE,
inverter=inverter.QVOLTHYBG31P,
values=QVOLTHYBG31P_VALUES,
headers=None,
data=None,
),
]


Expand Down
34 changes: 34 additions & 0 deletions tests/samples/expected_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,3 +521,37 @@
"Total feed-in energy": 286.7,
"Total consumption": 6.2,
}

QVOLTHYBG31P_VALUES = {
"Network Voltage": 238.1,
"Inverter output current": 10.0,
"Inverter output power": 2372,
"Grid Frequency": 50.04,
"PV1 Voltage": 241.5,
"PV2 Voltage": 240.3,
"PV1 Current": 7.6,
"PV2 Current": 7.7,
"PV1 Power": 1839,
"PV2 Power": 1862,
"Inverter status": "Normal",
"Total inverter yield": 1925.2,
"Today's inverter yield": 0.6,
"Battery Voltage": 322.80,
"Battery Current": 3.8,
"Battery Power": 1252,
"Battery Temperature": 25,
"Battery Remaining Capacity": 96,
"Total battery discharged energy": 331.6,
"Total battery charged energy": 444.6,
"Battery Remaining Energy": 8.9,
"Current grid power": -180,
"Total grid export": 1179.00,
"Total grid import": 16.83,
"Current power usage": 940,
"Total self-used solar": 2105.0,
"Today's grid export": 0.10,
"Today's grid import": 1.63,
"Today's solar yield": 8.3,
"Total battery energy throughput": 1398.396,
"Battery Operation mode": "Feed-in Priority",
}
Loading