pyrenac 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyrenac-0.0.2/LICENSE +21 -0
- pyrenac-0.0.2/PKG-INFO +22 -0
- pyrenac-0.0.2/README.md +7 -0
- pyrenac-0.0.2/pyproject.toml +24 -0
- pyrenac-0.0.2/setup.cfg +4 -0
- pyrenac-0.0.2/src/pyrenac/pyrenac.py +450 -0
- pyrenac-0.0.2/src/pyrenac.egg-info/PKG-INFO +22 -0
- pyrenac-0.0.2/src/pyrenac.egg-info/SOURCES.txt +8 -0
- pyrenac-0.0.2/src/pyrenac.egg-info/dependency_links.txt +1 -0
- pyrenac-0.0.2/src/pyrenac.egg-info/top_level.txt +1 -0
pyrenac-0.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gastush
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyrenac-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrenac
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: A library to fetch data from Renac Inverter
|
|
5
|
+
Author-email: gastush <gastush@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gastush/pyrenac
|
|
8
|
+
Project-URL: Issues, https://github.com/gastush/pyrenac/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# pyrenac
|
|
17
|
+
Python library to fetch data from Renac Inverter
|
|
18
|
+
|
|
19
|
+
[](https://github.com/gastush/pyrenac/actions/workflows/publish-to-pypi.yml)
|
|
20
|
+
|
|
21
|
+
The only tested inverter so far is a R3-8K-DT (that's the one I own)
|
|
22
|
+
I tried to add basic support for qn Hybrid model based on some sample data I got, but could not test it live yet...
|
pyrenac-0.0.2/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# pyrenac
|
|
2
|
+
Python library to fetch data from Renac Inverter
|
|
3
|
+
|
|
4
|
+
[](https://github.com/gastush/pyrenac/actions/workflows/publish-to-pypi.yml)
|
|
5
|
+
|
|
6
|
+
The only tested inverter so far is a R3-8K-DT (that's the one I own)
|
|
7
|
+
I tried to add basic support for qn Hybrid model based on some sample data I got, but could not test it live yet...
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyrenac"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="gastush", email="gastush@gmail.com" },
|
|
6
|
+
]
|
|
7
|
+
description = "A library to fetch data from Renac Inverter"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICEN[CS]E*"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/gastush/pyrenac"
|
|
19
|
+
Issues = "https://github.com/gastush/pyrenac/issues"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["setuptools >= 77.0.3"]
|
|
23
|
+
build-backend = "setuptools.build_meta"
|
|
24
|
+
|
pyrenac-0.0.2/setup.cfg
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""LIbrary providing access to the Renac SEC APIs.
|
|
2
|
+
|
|
3
|
+
Beware that this library as only been tested against On-Grid Inverters
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from collections.abc import Coroutine
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum
|
|
11
|
+
import logging
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Any, TypeVar
|
|
14
|
+
|
|
15
|
+
import aiohttp
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"run_coroutine_sync",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_coroutine_sync(coroutine: Coroutine[Any, Any, T], timeout: float = 30) -> T:
|
|
26
|
+
def run_in_new_loop():
|
|
27
|
+
new_loop = asyncio.new_event_loop()
|
|
28
|
+
asyncio.set_event_loop(new_loop)
|
|
29
|
+
try:
|
|
30
|
+
return new_loop.run_until_complete(coroutine)
|
|
31
|
+
finally:
|
|
32
|
+
new_loop.close()
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
loop = asyncio.get_running_loop()
|
|
36
|
+
except RuntimeError:
|
|
37
|
+
return asyncio.run(coroutine)
|
|
38
|
+
|
|
39
|
+
if threading.current_thread() is threading.main_thread():
|
|
40
|
+
if not loop.is_running():
|
|
41
|
+
return loop.run_until_complete(coroutine)
|
|
42
|
+
else:
|
|
43
|
+
with ThreadPoolExecutor() as pool:
|
|
44
|
+
future = pool.submit(run_in_new_loop)
|
|
45
|
+
return future.result(timeout=timeout)
|
|
46
|
+
else:
|
|
47
|
+
return asyncio.run_coroutine_threadsafe(coroutine, loop).result()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
API_ROOT = "https://sec.bg.renacpower.cn:8084/api/"
|
|
51
|
+
RENAC_API_ROOT = "https://sec.bg.renacpower.cn:8084/renac/"
|
|
52
|
+
BG_API_ROOT = "https://sec.bg.renacpower.cn:8084/bg/"
|
|
53
|
+
|
|
54
|
+
_LOGGER = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
InverterType = Enum("InverterType", ["ONGRID", "HYBRID"])
|
|
57
|
+
|
|
58
|
+
class Inverter:
|
|
59
|
+
"""The base class that represent the Inverter."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, api_client) -> None:
|
|
62
|
+
"""Initialize the Inverter."""
|
|
63
|
+
self.api_client = api_client
|
|
64
|
+
self._manufacturer = None
|
|
65
|
+
self._model = None
|
|
66
|
+
self._version = None
|
|
67
|
+
self._type = None
|
|
68
|
+
|
|
69
|
+
def _get_base_data(self):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def type(self) -> Any:
|
|
74
|
+
"""The Inverter type."""
|
|
75
|
+
return self._type
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class OnGridInverter(Inverter):
|
|
79
|
+
"""An On-Grid ivnerter."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, api_client) -> None:
|
|
82
|
+
"""Initialize the Inverter."""
|
|
83
|
+
super().__init__(api_client)
|
|
84
|
+
self._type = InverterType.ONGRID
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class HybridInverter(Inverter):
|
|
88
|
+
"""An hybrid inverter."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, api_client) -> None:
|
|
91
|
+
"""Initialize the Inverter."""
|
|
92
|
+
super().__init__(api_client)
|
|
93
|
+
self._type = InverterType.HYBRID
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class RenacInverterData:
|
|
98
|
+
"""Renac inverter data class."""
|
|
99
|
+
|
|
100
|
+
name: str
|
|
101
|
+
version: str
|
|
102
|
+
fwversion: str
|
|
103
|
+
registration_time: str
|
|
104
|
+
equipment_type: str
|
|
105
|
+
equipment_serial: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PyRenac:
|
|
109
|
+
"""The API wrapper."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, username, password) -> None:
|
|
112
|
+
"""Initialize the librqry."""
|
|
113
|
+
_LOGGER.info("New PyRenac instance %s", username)
|
|
114
|
+
self.username = username
|
|
115
|
+
self.password = password
|
|
116
|
+
self.emailSn = None
|
|
117
|
+
self.equipSn = None
|
|
118
|
+
self.token = None
|
|
119
|
+
self.station_id = None
|
|
120
|
+
self.inverterData = None
|
|
121
|
+
inverterData = run_coroutine_sync(self.async_get_inverter_data())
|
|
122
|
+
self.equipSn = inverterData.equipment_serial
|
|
123
|
+
self.inverterData = inverterData
|
|
124
|
+
|
|
125
|
+
def getSerial(self):
|
|
126
|
+
"""Get the serial number of the inverter."""
|
|
127
|
+
return self.equipSn
|
|
128
|
+
|
|
129
|
+
def getUniqueId(self, field):
|
|
130
|
+
"""Get a unique id for q given field name."""
|
|
131
|
+
return "renac_" + field + "_" + self.equipSn
|
|
132
|
+
|
|
133
|
+
def _login_request(self):
|
|
134
|
+
return {"loginName": self.username, "password": self.password}
|
|
135
|
+
|
|
136
|
+
def _fetch_request(self):
|
|
137
|
+
return {"sn": self.equipSn, "email": self.emailSn}
|
|
138
|
+
|
|
139
|
+
async def async_login(self) -> None:
|
|
140
|
+
"""Login to the Renac SEC API backend."""
|
|
141
|
+
_LOGGER.info("Requesting authorization")
|
|
142
|
+
req_json = self._login_request()
|
|
143
|
+
async with (
|
|
144
|
+
aiohttp.ClientSession() as session,
|
|
145
|
+
session.post(API_ROOT + "login/", json=req_json) as resp,
|
|
146
|
+
):
|
|
147
|
+
_LOGGER.debug(resp.status)
|
|
148
|
+
loginResponse = await resp.json(content_type=None)
|
|
149
|
+
_LOGGER.debug("Got %s", loginResponse.get("email"))
|
|
150
|
+
self.emailSn = loginResponse.get("email")
|
|
151
|
+
self.token = loginResponse.get("Token")
|
|
152
|
+
|
|
153
|
+
def login(self) -> None:
|
|
154
|
+
"""Login to the Renac SEC API backend."""
|
|
155
|
+
_LOGGER.info("Requesting authorization")
|
|
156
|
+
req_json = self._login_request()
|
|
157
|
+
resp = requests.post(API_ROOT + "login", json=req_json, timeout=60)
|
|
158
|
+
if resp.status_code == 200:
|
|
159
|
+
loginResponse = resp.json()
|
|
160
|
+
_LOGGER.debug("Got %s", loginResponse.get("email"))
|
|
161
|
+
self.emailSn = loginResponse.get("email")
|
|
162
|
+
self.token = loginResponse.get("Token")
|
|
163
|
+
|
|
164
|
+
def fetch_field_value(self, data, field):
|
|
165
|
+
"""Fetch the given field from the data."""
|
|
166
|
+
_LOGGER.debug("Fetch field value %s", field)
|
|
167
|
+
if data is not None:
|
|
168
|
+
return data.get(field)
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
async def async_fetch(self, field):
|
|
172
|
+
"""Fetch the data identified by the field."""
|
|
173
|
+
data = await self.async_fetch_all()
|
|
174
|
+
if data is not None:
|
|
175
|
+
return data.get(field)
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
def fetch(self, field):
|
|
179
|
+
"""Fetch the data identified by the field."""
|
|
180
|
+
data = self.fetch_all()
|
|
181
|
+
if data is not None:
|
|
182
|
+
return data.get(field)
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
async def async_ensure_login(self):
|
|
186
|
+
"""Ensure that we have a valid Token to be used."""
|
|
187
|
+
if self.token is None:
|
|
188
|
+
_LOGGER.info("Token is null, new fresh login sequence required")
|
|
189
|
+
await self.async_login()
|
|
190
|
+
|
|
191
|
+
def ensure_login(self):
|
|
192
|
+
"""Ensure that we have a valid Token to be used."""
|
|
193
|
+
if self.token is None:
|
|
194
|
+
_LOGGER.info("Token is null, new fresh login sequence required")
|
|
195
|
+
self.login()
|
|
196
|
+
|
|
197
|
+
async def async_fetch_all(self):
|
|
198
|
+
"""Fetch all the data.
|
|
199
|
+
|
|
200
|
+
A login will be done if needed to retrieve the right token.
|
|
201
|
+
"""
|
|
202
|
+
_LOGGER.debug("Fetching all data")
|
|
203
|
+
data = None
|
|
204
|
+
await self.async_ensure_login()
|
|
205
|
+
req_json = {"sn": self.equipSn, "email": self.emailSn}
|
|
206
|
+
headers = {"Token": self.token}
|
|
207
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
208
|
+
async with (
|
|
209
|
+
aiohttp.ClientSession() as session,
|
|
210
|
+
session.post(
|
|
211
|
+
API_ROOT + "equipDetail/",
|
|
212
|
+
json=req_json,
|
|
213
|
+
headers=headers,
|
|
214
|
+
timeout=timeout,
|
|
215
|
+
) as resp,
|
|
216
|
+
):
|
|
217
|
+
if resp.status == 200:
|
|
218
|
+
response = await resp.json(content_type=None)
|
|
219
|
+
if "results" in response:
|
|
220
|
+
data = response.get("results")
|
|
221
|
+
else:
|
|
222
|
+
_LOGGER.info("Null results. assuming a new Token is required")
|
|
223
|
+
self.token = None
|
|
224
|
+
else:
|
|
225
|
+
raise ("Failed to read sensor " + str(resp.status))
|
|
226
|
+
|
|
227
|
+
return data
|
|
228
|
+
|
|
229
|
+
def fetch_all(self):
|
|
230
|
+
"""Fetch all the data.
|
|
231
|
+
|
|
232
|
+
A login will be done if needed to retrieve the right token.
|
|
233
|
+
"""
|
|
234
|
+
_LOGGER.debug("Fetching all data")
|
|
235
|
+
data = None
|
|
236
|
+
self.ensure_login()
|
|
237
|
+
req_json = {"sn": self.equipSn, "email": self.emailSn}
|
|
238
|
+
headers = {"Token": self.token}
|
|
239
|
+
resp = requests.post(
|
|
240
|
+
API_ROOT + "equipDetail/", headers=headers, json=req_json, timeout=60
|
|
241
|
+
)
|
|
242
|
+
if resp.status == 200:
|
|
243
|
+
response = resp.json()
|
|
244
|
+
if "results" in response:
|
|
245
|
+
data = response.get("results")
|
|
246
|
+
else:
|
|
247
|
+
_LOGGER.info("Null results. assuming a new Token is required")
|
|
248
|
+
self.token = None
|
|
249
|
+
else:
|
|
250
|
+
raise ("Failed to read sensor " + str(resp.status))
|
|
251
|
+
|
|
252
|
+
return data
|
|
253
|
+
|
|
254
|
+
def getType(self, data) -> InverterType:
|
|
255
|
+
"""Get the Inverter Type from the availqble fields."""
|
|
256
|
+
inverterType = None
|
|
257
|
+
try:
|
|
258
|
+
value = self.fetch_field_value(
|
|
259
|
+
data, "BATTERY_CAPACITY"
|
|
260
|
+
) # HYBRID inverter have a battery.
|
|
261
|
+
if value is None:
|
|
262
|
+
inverterType = InverterType.ONGRID
|
|
263
|
+
else:
|
|
264
|
+
inverterType = InverterType.HYBRID
|
|
265
|
+
except KeyError:
|
|
266
|
+
inverterType = InverterType.ONGRID # Assume this is an On-Grid inverter.
|
|
267
|
+
return inverterType
|
|
268
|
+
|
|
269
|
+
def _station_list_request(self):
|
|
270
|
+
return {
|
|
271
|
+
"export_type": 0,
|
|
272
|
+
"installer_name": "",
|
|
273
|
+
"offset": 0,
|
|
274
|
+
"rows": 10,
|
|
275
|
+
"station_name": "",
|
|
276
|
+
"station_type": None,
|
|
277
|
+
"status": None,
|
|
278
|
+
"user_id": self.emailSn,
|
|
279
|
+
"user_name": "",
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async def async_get_station_id(self):
|
|
283
|
+
"""Get the station id."""
|
|
284
|
+
if self.station_id is None:
|
|
285
|
+
await self.async_ensure_login()
|
|
286
|
+
req_json = self._station_list_request()
|
|
287
|
+
headers = {"Token": self.token}
|
|
288
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
289
|
+
async with (
|
|
290
|
+
aiohttp.ClientSession() as session,
|
|
291
|
+
session.post(
|
|
292
|
+
API_ROOT + "station/list",
|
|
293
|
+
json=req_json,
|
|
294
|
+
headers=headers,
|
|
295
|
+
timeout=timeout,
|
|
296
|
+
) as resp,
|
|
297
|
+
):
|
|
298
|
+
if resp.status == 200:
|
|
299
|
+
response = await resp.json(content_type=None)
|
|
300
|
+
if "data" in response:
|
|
301
|
+
_LOGGER.warning(response)
|
|
302
|
+
self.station_id = response["data"]["list"][0]["station_id"]
|
|
303
|
+
else:
|
|
304
|
+
_LOGGER.info("Null results. assuming a new Token is required")
|
|
305
|
+
self.token = None
|
|
306
|
+
else:
|
|
307
|
+
raise ("Failed to read sensor " + str(resp.status))
|
|
308
|
+
_LOGGER.info("Got station_id %s", self.station_id)
|
|
309
|
+
return self.station_id
|
|
310
|
+
|
|
311
|
+
def get_station_id(self):
|
|
312
|
+
"""Get the station id."""
|
|
313
|
+
if self.station_id is None:
|
|
314
|
+
self.ensure_login()
|
|
315
|
+
req_json = self._station_list_request()
|
|
316
|
+
headers = {"Token": self.token}
|
|
317
|
+
resp = requests.post(
|
|
318
|
+
API_ROOT + "station/list", headers=headers, json=req_json, timeout=60
|
|
319
|
+
)
|
|
320
|
+
if resp.status == 200:
|
|
321
|
+
response = resp.json()
|
|
322
|
+
if "data" in response:
|
|
323
|
+
self.station_id = response["data"]["list"][0]["station_id"]
|
|
324
|
+
else:
|
|
325
|
+
_LOGGER.info("Null results. assuming a new Token is required")
|
|
326
|
+
self.token = None
|
|
327
|
+
else:
|
|
328
|
+
raise ("Failed to read sensor " + str(resp.status))
|
|
329
|
+
_LOGGER.info("Got station_id %s", self.station_id)
|
|
330
|
+
return self.station_id
|
|
331
|
+
|
|
332
|
+
async def async_get_historical_data(self, date):
|
|
333
|
+
"""Get Historical production data for the given date time range."""
|
|
334
|
+
data = None
|
|
335
|
+
await self.ensure_login()
|
|
336
|
+
station_id = await self.get_station_id()
|
|
337
|
+
req_json = {"station_id": station_id, "time": str(date), "time_type": 1}
|
|
338
|
+
headers = {"Token": self.token}
|
|
339
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
340
|
+
async with (
|
|
341
|
+
aiohttp.ClientSession() as session,
|
|
342
|
+
session.post(
|
|
343
|
+
RENAC_API_ROOT + "station/energy",
|
|
344
|
+
json=req_json,
|
|
345
|
+
headers=headers,
|
|
346
|
+
timeout=timeout,
|
|
347
|
+
) as resp,
|
|
348
|
+
):
|
|
349
|
+
if resp.status == 200:
|
|
350
|
+
response = await resp.json(content_type=None)
|
|
351
|
+
if "data" in response:
|
|
352
|
+
data = response["data"]
|
|
353
|
+
else:
|
|
354
|
+
_LOGGER.info("Null results. assuming a new Token is required")
|
|
355
|
+
self.token = None
|
|
356
|
+
else:
|
|
357
|
+
raise ("Failed to read sensor " + str(resp.status))
|
|
358
|
+
return data
|
|
359
|
+
|
|
360
|
+
def get_inverter_data(self) -> RenacInverterData:
|
|
361
|
+
"""Get details about the inverter itslef."""
|
|
362
|
+
inverterData = None
|
|
363
|
+
self.ensure_login()
|
|
364
|
+
station_id = self.get_station_id()
|
|
365
|
+
req_json = {
|
|
366
|
+
"station_id": station_id,
|
|
367
|
+
"user_id": self.emailSn,
|
|
368
|
+
"status": 0,
|
|
369
|
+
"offset": 0,
|
|
370
|
+
"rows": 1,
|
|
371
|
+
}
|
|
372
|
+
headers = {"Token": self.token}
|
|
373
|
+
resp = requests.post(
|
|
374
|
+
API_ROOT + "station/list", headers=headers, json=req_json, timeout=60
|
|
375
|
+
)
|
|
376
|
+
if resp.status == 200:
|
|
377
|
+
response = resp.json(content_type=None)
|
|
378
|
+
if "data" in response:
|
|
379
|
+
data = response["data"]["list"][0]
|
|
380
|
+
inverterData = RenacInverterData(
|
|
381
|
+
version=data[0].get("VERSION"),
|
|
382
|
+
fwversion=data[0].get("FIRMWARE_VER"),
|
|
383
|
+
name=data[0].get("STATION_NAME"),
|
|
384
|
+
equipment_type=data[0].get("EQU_TYPE"),
|
|
385
|
+
registration_time=data[0].get("REG_TIME"),
|
|
386
|
+
equipment_serial=data[0].get("INV_SN"),
|
|
387
|
+
)
|
|
388
|
+
else:
|
|
389
|
+
_LOGGER.info("Null results. assuming a new Token is required")
|
|
390
|
+
self.token = None
|
|
391
|
+
else:
|
|
392
|
+
raise ("Failed to read sensor " + str(resp.status))
|
|
393
|
+
return inverterData
|
|
394
|
+
|
|
395
|
+
async def async_get_inverter_data(self) -> RenacInverterData:
|
|
396
|
+
"""Get details about the inverter itslef."""
|
|
397
|
+
if self.inverterData is None:
|
|
398
|
+
await self.async_ensure_login()
|
|
399
|
+
station_id = await self.async_get_station_id()
|
|
400
|
+
req_json = {
|
|
401
|
+
"station_id": station_id,
|
|
402
|
+
"user_id": self.emailSn,
|
|
403
|
+
"status": 0,
|
|
404
|
+
"offset": 0,
|
|
405
|
+
"rows": 1,
|
|
406
|
+
}
|
|
407
|
+
headers = {"Token": self.token}
|
|
408
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
409
|
+
async with (
|
|
410
|
+
aiohttp.ClientSession() as session,
|
|
411
|
+
session.post(
|
|
412
|
+
BG_API_ROOT + "equList",
|
|
413
|
+
json=req_json,
|
|
414
|
+
headers=headers,
|
|
415
|
+
timeout=timeout,
|
|
416
|
+
) as resp,
|
|
417
|
+
):
|
|
418
|
+
if resp.status == 200:
|
|
419
|
+
response = await resp.json(content_type=None)
|
|
420
|
+
if "data" in response:
|
|
421
|
+
_LOGGER.warning(response)
|
|
422
|
+
data = response["data"]["list"]
|
|
423
|
+
if len(data) > 0:
|
|
424
|
+
self.inverterData = RenacInverterData(
|
|
425
|
+
version=data[0].get("VERSION"),
|
|
426
|
+
fwversion=data[0].get("FIRMWARE_VER"),
|
|
427
|
+
name=data[0].get("STATION_NAME"),
|
|
428
|
+
equipment_type=data[0].get("EQU_TYPE"),
|
|
429
|
+
registration_time=data[0].get("REG_TIME"),
|
|
430
|
+
equipment_serial=data[0].get("INV_SN"),
|
|
431
|
+
)
|
|
432
|
+
else:
|
|
433
|
+
_LOGGER.info("Null results. assuming a new Token is required")
|
|
434
|
+
self.token = None
|
|
435
|
+
else:
|
|
436
|
+
raise ("Failed to read sensor " + str(resp.status))
|
|
437
|
+
return self.inverterData
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class InverterFactory:
|
|
441
|
+
"""The inverter factory."""
|
|
442
|
+
|
|
443
|
+
def getInverter(self, api_client: PyRenac) -> Inverter:
|
|
444
|
+
"""Get the inverter based on his type."""
|
|
445
|
+
inverterType = api_client.getType()
|
|
446
|
+
if inverterType is inverterType.ONGRID:
|
|
447
|
+
return OnGridInverter(api_client)
|
|
448
|
+
if inverterType is InverterType.HYBRID:
|
|
449
|
+
return HybridInverter(api_client)
|
|
450
|
+
raise ValueError("Unsupported Inverter type")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrenac
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: A library to fetch data from Renac Inverter
|
|
5
|
+
Author-email: gastush <gastush@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gastush/pyrenac
|
|
8
|
+
Project-URL: Issues, https://github.com/gastush/pyrenac/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# pyrenac
|
|
17
|
+
Python library to fetch data from Renac Inverter
|
|
18
|
+
|
|
19
|
+
[](https://github.com/gastush/pyrenac/actions/workflows/publish-to-pypi.yml)
|
|
20
|
+
|
|
21
|
+
The only tested inverter so far is a R3-8K-DT (that's the one I own)
|
|
22
|
+
I tried to add basic support for qn Hybrid model based on some sample data I got, but could not test it live yet...
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyrenac
|