Skip to content

Commit

Permalink
Add support for X1-Smart (#52)
Browse files Browse the repository at this point in the history
* Add support for X1-Smart

* Add support for X1-Smart

* Add fixtures for X1-Smart tests

* test for presence of headers

Co-authored-by: JJ Macalinao <jessie.macalinao@cai.io>
Co-authored-by: Robin Wohlers-Reichel <robin.wohlersreichel@gmail.com>
  • Loading branch information
3 people authored Dec 21, 2021
1 parent 0c3151f commit 883cdd9
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 68 deletions.
191 changes: 129 additions & 62 deletions solax/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async def get_data(self):
return data

@classmethod
async def make_request(cls, host, port, pwd=''):
async def make_request(cls, host, port, pwd='', headers=None):
"""
Return instance of 'InverterResponse'
Raise exception if unable to get data
Expand Down Expand Up @@ -171,7 +171,7 @@ class XHybrid(Inverter):
}

@classmethod
async def make_request(cls, host, port=80, pwd=''):
async def make_request(cls, host, port=80, pwd='', headers=None):
base = 'http://{}:{}/api/realTimeData.htm'
url = base.format(host, port)
async with aiohttp.ClientSession() as session:
Expand Down Expand Up @@ -200,17 +200,17 @@ def schema(cls):
class InverterPost(Inverter):
# This is an intermediate abstract class,
# so we can disable the pylint warning
# pylint: disable=W0223
# pylint: disable=W0223,R0914
@classmethod
async def make_request(cls, host, port=80, pwd=''):
async def make_request(cls, host, port=80, pwd='', headers=None):
if not pwd:
base = 'http://{}:{}/?optType=ReadRealTimeData'
url = base.format(host, port)
else:
base = 'http://{}:{}/?optType=ReadRealTimeData&pwd={}&'
url = base.format(host, port, pwd)
async with aiohttp.ClientSession() as session:
async with session.post(url) as req:
async with session.post(url, headers=headers) as req:
resp = await req.read()
raw_json = resp.decode("utf-8")
json_response = json.loads(raw_json)
Expand All @@ -219,15 +219,10 @@ async def make_request(cls, host, port=80, pwd=''):
response = cls.schema()(json_response)
except (Invalid, MultipleInvalid) as ex:
_ = humanize_error(json_response, ex)
# print(_)
raise
if 'SN' in response:
serial_number = response['SN']
else:
serial_number = response['sn']
return InverterResponse(
data=cls.map_response(response['Data']),
serial_number=serial_number,
serial_number=response.get('SN', response.get('sn')),
version=response['ver'],
type=response['type']
)
Expand Down Expand Up @@ -332,57 +327,57 @@ class X3V34(InverterPost):
}, 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),

'PV1 Voltage': (9, 'V', div10),
'PV2 Voltage': (10, 'V', div10),
'PV1 Current': (11, 'A', div10),
'PV2 Current': (12, 'A', div10),
'PV1 Power': (13, 'W'),
'PV2 Power': (14, 'W'),

'Total PV Energy': (89, 'kWh', pv_energy),
'Total PV Energy Resets': (90, ''),
'Today\'s PV Energy': (112, 'kWh', div10),

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

'Total Energy': (19, 'kWh', total_energy),
'Total Energy Resets': (20, ''),
'Today\'s Energy': (21, 'kWh', div10),

'Battery Voltage': (24, 'V', div100),
'Battery Current': (25, 'A', twoway_div100),
'Battery Power': (26, 'W', to_signed),
'Battery Temperature': (27, 'C'),
'Battery Remaining Capacity': (28, '%'),

'Total Battery Discharge Energy': (30, 'kWh',
'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),

'PV1 Voltage': (9, 'V', div10),
'PV2 Voltage': (10, 'V', div10),
'PV1 Current': (11, 'A', div10),
'PV2 Current': (12, 'A', div10),
'PV1 Power': (13, 'W'),
'PV2 Power': (14, 'W'),

'Total PV Energy': (89, 'kWh', pv_energy),
'Total PV Energy Resets': (90, ''),
'Today\'s PV Energy': (112, 'kWh', div10),

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

'Total Energy': (19, 'kWh', total_energy),
'Total Energy Resets': (20, ''),
'Today\'s Energy': (21, 'kWh', div10),

'Battery Voltage': (24, 'V', div100),
'Battery Current': (25, 'A', twoway_div100),
'Battery Power': (26, 'W', to_signed),
'Battery Temperature': (27, 'C'),
'Battery Remaining Capacity': (28, '%'),

'Total Battery Discharge Energy': (30, 'kWh',
discharge_energy),
'Total Battery Discharge Energy Resets': (31, ''),
'Today\'s Battery Discharge Energy': (113, 'kWh', div10),
'Battery Remaining Energy': (32, 'kWh', div10),
'Total Battery Charge Energy': (87, 'kWh', charge_energy),
'Total Battery Charge Energy Resets': (88, ''),
'Today\'s Battery Charge Energy': (114, 'kWh', div10),

'Exported Power': (65, 'W', to_signed),
'Total Feed-in Energy': (67, 'kWh', feedin_energy),
'Total Feed-in Energy Resets': (68, ''),
'Total Consumption': (69, 'kWh', consumption),
'Total Consumption Resets': (70, ''),
'Total Battery Discharge Energy Resets': (31, ''),
'Today\'s Battery Discharge Energy': (113, 'kWh', div10),
'Battery Remaining Energy': (32, 'kWh', div10),
'Total Battery Charge Energy': (87, 'kWh', charge_energy),
'Total Battery Charge Energy Resets': (88, ''),
'Today\'s Battery Charge Energy': (114, 'kWh', div10),

'Exported Power': (65, 'W', to_signed),
'Total Feed-in Energy': (67, 'kWh', feedin_energy),
'Total Feed-in Energy Resets': (68, ''),
'Total Consumption': (69, 'kWh', consumption),
'Total Consumption Resets': (70, ''),

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

Expand Down Expand Up @@ -607,5 +602,77 @@ def schema(cls):
return cls.__schema


class X1Smart(InverterPost):
"""
X1-Smart with Pocket WiFi v2.033.20
Includes X-Forwarded-For for direct LAN API access
"""
__schema = vol.Schema({
vol.Required('type', 'type'): vol.All(int, 8),
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=8, max=8)
)
),
}, extra=vol.REMOVE_EXTRA)

__sensor_map = {
'Network Voltage': (0, 'V', div10),
'Output Current': (1, 'A', div10),
'AC Power': (2, 'W'),
'PV1 Voltage': (3, 'V', div10),
'PV2 Voltage': (4, 'V', div10),
'PV1 Current': (5, 'A', div10),
'PV2 Current': (6, 'A', div10),
'PV1 Power': (7, 'W'),
'PV2 Power': (8, 'W'),
'Grid Frequency': (9, 'Hz', div100),
'Total Energy': (11, 'kWh', div10),
'Today\'s Energy': (13, 'kWh', div10),
'Inverter Temperature': (39, 'C'),
'Exported Power': (48, 'W', to_signed),
'Total Feed-in Energy': (50, 'kWh', div100),
'Total Consumption': (52, 'kWh', div100),
}

@classmethod
async def make_request(cls, host, port=80, pwd='', headers=None):
headers = {'X-Forwarded-For': '5.8.8.8'}
return await super().make_request(host, port, pwd, headers=headers)

@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


# registry of inverters
REGISTRY = [XHybrid, X3, X3V34, X1, X1Mini, X1MiniV34]
REGISTRY = [XHybrid, X3, X3V34, X1, X1Mini, X1MiniV34, X1Smart]
Loading

0 comments on commit 883cdd9

Please sign in to comment.