Skip to content

Commit e07e9fe

Browse files
authored
Merge pull request #56 from vchrisb/enhancements
* adds and fixes docstrings in complete project, adresses further improve code quality #55 * adds docstring flake8 tests to github action * removes unused _e3dc_rscp.py * fixes flake8 extend-ignore configuration * enhances get_battery_data by adding dcb data, fixed Read - DCB Data - from Batery #41 * fixes and enhances get_pvi_data, fixes PVI Data werden falsch abgerufen #39 * enhances get_power_data * fixes E741 and F601 flake8 test * configuration object for E3DC class to set system specific configuration, like multiple batteries or index of PM * added get_pvis_data, get_powermeters_data and get_batteries_data * added simplified functions: sendRequestTag and rscpFindTagIndex * rscpFindTag and rscpFindTagIndex return None if tag is not found * improved README.md * reverted default pmIndex=0 for S10 Pro
2 parents 3b2d75d + f25966e commit e07e9fe

13 files changed

Lines changed: 1409 additions & 960 deletions

.github/workflows/validate.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Install dependencies
1818
run: |
1919
python -m pip install --upgrade pip
20-
pip install black flake8 isort
20+
pip install black flake8 flake8-docstrings isort
2121
- name: Run flake8
2222
run: flake8
2323
- name: Run isort

AUTHORS

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# This is the list of python-e3dc's contributors.
2+
#
3+
# This does not necessarily list everyone who has contributed code.
4+
# To see the full list of contributors, see the revision history.
5+
6+
* Francesco Santini @fsantini
7+
* Christopher Banck @vchrisb
8+
* Thomas Spalinger @spali
9+
* one4many @one4many
10+
* Steff @steff333
11+
* is00709 @is00709
12+
* Gregor Wolf @gregorwolf
13+
* Max Dhom @mdhom

README.md

Lines changed: 124 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,176 @@
11
# python-e3dc
22

3+
[![PyPI version](https://badge.fury.io/py/pye3dc.svg)](https://badge.fury.io/py/pye3dc)
4+
[![GitHub license](https://img.shields.io/github/license/fsantini/python-e3dc)](https://github.com/fsantini/python-e3dc/blob/master/LICENSE)
5+
[![Codestyle](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
6+
37
**NOTE: With Release 0.6.0 at least Python 3.7 is required**
48

5-
Python API for querying E3/DC systems, either through the manufacturer's portal or directly via RSCP connection
9+
Python API for querying an [E3/DC](https://e3dc.de/) systems
610

7-
This library provides an interface to query an E3/DC solar power management system through the web interface of the manufacturer.
11+
This is supported either directly via RSCP connection or through the manufacturer's portal. The RSCP implementation has currently the most capabilities.
812

9-
In order to use it you need (web connection):
13+
In order to use it you need:
1014
- Your user name
1115
- Your password
12-
- The serial number of the system which can be found when logging into the E3/DC webpage:
13-
![E3/DC screenshot](doc-ima/sn.png)
16+
- The IP address of the E3/DC system
17+
- The RSCP Password (encryption key), as set on the device under Main Page -> Personalize -> User profile -> RSCP password
1418

15-
Alternatively, for a local connection, you need:
19+
Alternatively, for a web connection, you need:
1620
- Your user name
1721
- Your password
18-
- The IP address of the E3/DC system
19-
- The encryption key as set under the preferences of the system.
22+
- The serial number of the system, which can be found when logging into the E3/DC webpage.
2023

2124
## Installation
2225

2326
This package can be installed from pip:
2427

2528
`pip install pye3dc`
2629

27-
## Usage
30+
## Local Connection
31+
32+
### Configuration
33+
34+
There is a great variety of E3/DC implementation configurations, that can't automatically be detected. For example the `index` of the root power meter can be either `0` or `6`, depending how the system was installed. It is also possible to not only have one power meter or inverter.
35+
This library assumes, that there is one inverter installed and the root power meter has an index of `6` for S10 mini and `0` for other systems.
36+
37+
For any other conigurations, there is an optional `configuration` object that can be used to alter the defaults:
38+
39+
```
40+
{
41+
"pvis": [
42+
{
43+
"index": 0,
44+
"strings": 2,
45+
"phases": 3
46+
}
47+
],
48+
"powermeters": [
49+
{
50+
"index": 6
51+
}
52+
],
53+
"batteries": [
54+
{
55+
"index": 0,
56+
"dcbs": 2
57+
}
58+
]
59+
}
60+
```
61+
62+
> Note: Not all options need to be configured.
63+
64+
### Usage
2865

2966
An example script using the library is the following:
67+
3068
```python
3169
from e3dc import E3DC
3270

3371
TCP_IP = '192.168.1.57'
3472
USERNAME = 'test@test.com'
3573
PASS = 'MySecurePassword'
3674
KEY = 'abc123'
37-
SERIALNUMBER = '1234567890'
38-
39-
print("web connection")
40-
e3dc = E3DC(E3DC.CONNECT_WEB, username=USERNAME, password=PASS, serialNumber = SERIALNUMBER, isPasswordMd5=False)
41-
# connect to the portal and poll the status. This might raise an exception in case of failed login. This operation is performed with Ajax
42-
print(e3dc.poll())
43-
# Poll the status of the switches using a remote RSCP connection via websockets
44-
# return value is in the format {'id': switchID, 'type': switchType, 'name': switchName, 'status': switchStatus}
45-
print(e3dc.poll_switches())
75+
CONFIG = {}
76+
# CONFIG = {"powermeters": [{"index": 6}]}
4677

4778
print("local connection")
48-
e3dc = E3DC(E3DC.CONNECT_LOCAL, username=USERNAME, password=PASS, ipAddress = TCP_IP, key = KEY)
79+
e3dc = E3DC(E3DC.CONNECT_LOCAL, username=USERNAME, password=PASS, ipAddress = TCP_IP, key = KEY, configuration = CONFIG)
4980
# The following connections are performed through the RSCP interface
5081
print(e3dc.poll())
51-
print(e3dc.poll_switches())
82+
print(e3dc.get_pvi_data())
5283
```
5384

54-
## poll() return values
85+
### poll() return values
5586

5687
Poll returns a dictionary like the following:
5788
```python
5889
{
59-
'consumption': {'battery': 470, 'house': 477, 'wallbox': 0}, # consumption in W. Positive values are exiting the system
60-
'production': {'grid': -4, 'solar': 951}, # production in W. Positive values are entering the system
61-
'stateOfCharge' : 77, # battery charge status in %
62-
'sysStatus': '2623', # status
63-
'time': datetime.datetime(2017, 8, 14, 7, 6, 13) # timestamp of the poll
64-
}
90+
'autarky': 100,
91+
'consumption': {
92+
'battery': 470,
93+
'house': 477,
94+
'wallbox': 0
95+
},
96+
'production': {
97+
'solar' : 951,
98+
'add' : 0,
99+
'grid' : -4
100+
},
101+
'stateOfCharge': 77,
102+
'selfConsumption': 100,
103+
'time': datetime.datetime(2021, 8, 14, 7, 6, 13)
104+
}
65105
```
66106

67-
## Setting swiches
107+
### Available methods
68108

69-
The e3dcObj.set_switch_onoff(switchID, value, keepAlive = False) method sets a smart switch on or off, where value is a boolean and True = on, False = off.
70-
The switchID is a number returned by the poll_switches method. This method only supports on/off switches and not dimmers or motors.
109+
* `poll()`
110+
* `get_system_info()`
111+
* `get_system_status()`
112+
* `poll_switches()`
113+
* `get_idle_periods()`
114+
* `set_idle_periods()`
115+
* `get_db_data()`
116+
* `get_battery_data()`
117+
* `get_batteries_data()`
118+
* `get_pvi_data()`
119+
* `get_pvis_data()`
120+
* `get_powermeter_data()`
121+
* `get_powermeters_data()`
122+
* `get_power_settings()`
123+
* `set_power_limits()`
124+
* `set_powersave()`
125+
* `set_weather_regulated_charge()`
71126

72-
## Note: The RSCP interface
127+
> A documentation for these methods is not yet generated. Please have a look at the docstrings in `_e3dc.py` for details.
73128
74-
The switch statuses are obtained and manipulated via a rather complicated protocol, called by E3/DC RSCP. This protocol is binary and based on websockets.
129+
### Note: The RSCP interface
75130

76-
The E3DC object automatically connects to the websocket and authenticates. Both the poll_switches and set_switch_onoff methods accept an optional keepAlive parameter.
131+
The communication to an E3/DC system has to be implementes via a rather complicated protocol, called by E3/DC RSCP. This protocol is binary and based on websockets. The documentation provided by E3/DC is limited and outdated . It can be found in the E3/DC download portal.
77132

78133
If keepAlive is false, the websocket connection is closed after the command. This makes sense because these requests are not meant to be made as often as the status requests, however, if keepAlive is True, the connection is left open and kept alive in the background in a separate thread.
79134

135+
## Web connection
136+
137+
### Usage
138+
139+
An example script using the library is the following:
140+
141+
```python
142+
from e3dc import E3DC
143+
144+
TCP_IP = '192.168.1.57'
145+
USERNAME = 'test@test.com'
146+
PASS = 'MySecurePassword'
147+
SERIALNUMBER = '1234567890'
148+
149+
print("web connection")
150+
e3dc = E3DC(E3DC.CONNECT_WEB, username=USERNAME, password=PASS, serialNumber = SERIALNUMBER, isPasswordMd5=False)
151+
# connect to the portal and poll the status. This might raise an exception in case of failed login. This operation is performed with Ajax
152+
print(e3dc.poll())
153+
# Poll the status of the switches using a remote RSCP connection via websockets
154+
# return value is in the format {'id': switchID, 'type': switchType, 'name': switchName, 'status': switchStatus}
155+
print(e3dc.poll_switches())
156+
```
157+
80158
## Known limitations
81159

82160
One limitation of the package concerns the implemented RSCP methods. At the moment, only switch status requests and setting of on/off switches is implemented. I also lack the hardware to test different configurations. However, the RSCP protocol is (to my knowledge) fully implemented and it should be easy to extend the requests to other cases.
83161

84-
# Copyright notice
162+
## Projects using this library
163+
164+
* [e3dc-rest](https://github.com/vchrisb/e3dc-rest): a simple REST API to access an E3/DC system
165+
* [e3dc-to-mqtt](https://github.com/mdhom/e3dc-to-mqtt): publish E3/DC data via MQTT
166+
167+
## Contribution
168+
169+
* open an issue before making a pull request
170+
* note the E3/DC system you tested with and implementation details
171+
* pull request checks will enforce code styling (black, flake8, isort)
172+
* consider adding yourself to `AUTHORS`
173+
174+
## Copyright notice
85175

86176
The Rijndael algorithm comes from the python-cryptoplus package by Philippe Teuwen (https://github.com/doegox/python-cryptoplus) and distributed under a MIT license.

e3dc/_RSCPEncryptDecrypt.py

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,38 @@
77

88

99
class ParameterError(Exception):
10+
"""Class for Parameter Error Exception."""
11+
1012
pass
1113

1214

1315
def zeroPad_multiple(string, value):
14-
l = len(string)
15-
if l % value == 0:
16+
"""Zero padding string."""
17+
length = len(string)
18+
if length % value == 0:
1619
return string
17-
newL = int(value * math.ceil(float(l) / value))
20+
newL = int(value * math.ceil(float(length) / value))
1821
return string.ljust(newL, b"\x00")
1922

2023

2124
def truncate_multiple(string, value):
22-
l = len(string)
23-
if l % value == 0:
25+
"""Truncating sting."""
26+
length = len(string)
27+
if length % value == 0:
2428
return string
25-
newL = int(value * math.floor(float(l) / value))
29+
newL = int(value * math.floor(float(length) / value))
2630
return string[:newL]
2731

2832

2933
class RSCPEncryptDecrypt:
34+
"""A class for encrypting and decrypting RSCP data."""
35+
3036
def __init__(self, key):
37+
"""Constructor of a RSCP encryption and decryption class.
38+
39+
Args:
40+
key (str): RSCP encryption key
41+
"""
3142
if len(key) > KEY_SIZE:
3243
raise ParameterError("Key must be <%d bytes" % (KEY_SIZE))
3344

@@ -38,6 +49,7 @@ def __init__(self, key):
3849
self.oldDecrypt = b""
3950

4051
def encrypt(self, plainText):
52+
"""Method to encryt plain text."""
4153
encryptor = RijndaelCbc(
4254
self.key,
4355
self.encryptIV,
@@ -49,14 +61,16 @@ def encrypt(self, plainText):
4961
return encText
5062

5163
def decrypt(self, encText, previouslyProcessedData=None):
64+
"""Method to decryt encrypted text."""
5265
if previouslyProcessedData is None:
53-
l = len(self.oldDecrypt)
54-
if l % BLOCK_SIZE == 0:
55-
previouslyProcessedData = l
66+
length = len(self.oldDecrypt)
67+
if length % BLOCK_SIZE == 0:
68+
previouslyProcessedData = length
5669
else:
57-
previouslyProcessedData = int(BLOCK_SIZE * math.floor(l / BLOCK_SIZE))
70+
previouslyProcessedData = int(
71+
BLOCK_SIZE * math.floor(length / BLOCK_SIZE)
72+
)
5873

59-
# print previouslyProcessedData
6074
# previouslyProcessedData was passed by the parent: it means that a frame was decoded and there was some data left. This does not include the padding zeros
6175
if previouslyProcessedData % BLOCK_SIZE != 0:
6276
previouslyProcessedData = int(
@@ -79,15 +93,3 @@ def decrypt(self, encText, previouslyProcessedData=None):
7993
block_size=BLOCK_SIZE,
8094
)
8195
return decryptor.decrypt(toDecrypt)
82-
83-
84-
if __name__ == "__main__":
85-
ed = RSCPEncryptDecrypt(b"love")
86-
enc = ed.encrypt(b"hello")
87-
print(enc)
88-
dec = ed.decrypt(enc)
89-
print(dec)
90-
enc2 = ed.encrypt(b"hello")
91-
print(enc2)
92-
dec2 = ed.decrypt(enc2)
93-
print(dec2)

e3dc/__init__.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
"""
2-
3-
E3DC Library for Python
4-
5-
Python class to connect to an E3/DC system through the internet portal
6-
Copyright 2017 Francesco Santini <francesco.santini@gmail.com>
7-
Licensed under a MIT license. See LICENSE for details
1+
"""E3DC Library for Python.
82
3+
Python class to connect to an E3/DC system.
4+
Copyright 2017 Francesco Santini <francesco.santini@gmail.com>.
5+
Licensed under a MIT license. See LICENSE for details.
96
"""
107

118
from ._e3dc import E3DC, AuthenticationError, PollError
@@ -14,13 +11,13 @@
1411
from ._rscpLib import FrameError
1512

1613
__all__ = [
17-
E3DC,
18-
AuthenticationError,
19-
PollError,
20-
CommunicationError,
21-
RSCPAuthenticationError,
22-
RequestTimeoutError,
23-
SocketNotReady,
24-
FrameError,
14+
"E3DC",
15+
"AuthenticationError",
16+
"PollError",
17+
"CommunicationError",
18+
"RSCPAuthenticationError",
19+
"RequestTimeoutError",
20+
"SocketNotReady",
21+
"FrameError",
2522
]
2623
__version__ = "0.6.0"

0 commit comments

Comments
 (0)