Skip to content

Commit 66a7a23

Browse files
authored
Merge pull request #76 from vchrisb/pyright
* added type hints * added pyright static type checking * code refactoring * web connection is only using rscp via websocket * enhanced test scripts
2 parents 3cc8067 + 5005c45 commit 66a7a23

16 files changed

Lines changed: 683 additions & 661 deletions

.github/workflows/validate.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ jobs:
1717
- name: Install dependencies
1818
run: |
1919
python -m pip install --upgrade pip
20-
pip install black flake8 flake8-docstrings isort
20+
pip install .[develop]
2121
- name: Run flake8
2222
run: flake8
2323
- name: Run isort
2424
run: isort ./ --check
2525
- name: Run black
2626
run: black ./ --check
27+
- name: Run pyright
28+
run: pyright
2729
- name: Run test install
2830
run: pip install .

README.md

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ This package can be installed from pip:
3030

3131
`pip install pye3dc`
3232

33-
## Local Connection
34-
35-
### Configuration
33+
## Configuration
3634

3735
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. Additional power meter can have an ID of `1-4` and there might be also multiple inverter.
3836
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.
@@ -64,7 +62,9 @@ For any other configurations, there is an optional `configuration` object that c
6462

6563
> Note: Not all options need to be configured.
6664
67-
### Usage
65+
## Usage
66+
67+
### Local Connection
6868

6969
An example script using the library is the following:
7070

@@ -79,13 +79,34 @@ CONFIG = {}
7979
# CONFIG = {"powermeters": [{"index": 6}]}
8080

8181
print("local connection")
82-
e3dc = E3DC(E3DC.CONNECT_LOCAL, username=USERNAME, password=PASS, ipAddress = TCP_IP, key = KEY, configuration = CONFIG)
82+
e3dc_obj = E3DC(E3DC.CONNECT_LOCAL, username=USERNAME, password=PASS, ipAddress = TCP_IP, key = KEY, configuration = CONFIG)
8383
# The following connections are performed through the RSCP interface
84-
print(e3dc.poll())
85-
print(e3dc.get_pvi_data())
84+
print(e3dc_obj.poll(keepAlive=True))
85+
print(e3dc_obj.get_pvi_data(keepAlive=True))
86+
e3dc_obj.disconnect()
87+
```
88+
89+
### Web Connection
90+
91+
An example script using the library is the following:
92+
93+
```python
94+
from e3dc import E3DC
95+
96+
USERNAME = 'test@test.com'
97+
PASS = 'MySecurePassword'
98+
SERIALNUMBER = 'S10-012345678910'
99+
CONFIG = {}
100+
101+
print("web connection")
102+
e3dc_obj = E3DC(E3DC.CONNECT_WEB, username=USERNAME, password=PASS, serialNumber = SERIALNUMBER, isPasswordMd5=False, configuration = CONFIG)
103+
# connect to the portal and poll the status. This might raise an exception in case of failed login. This operation is performed with Ajax
104+
print(e3dc_obj.poll(keepAlive=True))
105+
print(e3dc_obj.get_pvi_data(keepAlive=True))
106+
e3dc_obj.disconnect()
86107
```
87108

88-
### poll() return values
109+
## Example: poll() return values
89110

90111
Poll returns a dictionary like the following:
91112

@@ -108,7 +129,7 @@ Poll returns a dictionary like the following:
108129
}
109130
```
110131

111-
### Available methods
132+
## Available methods
112133

113134
- `poll()`
114135
- `get_system_info()`
@@ -117,10 +138,13 @@ Poll returns a dictionary like the following:
117138
- `get_idle_periods()`
118139
- `set_idle_periods()`
119140
- `get_db_data()`
141+
- `get_batteries()`
120142
- `get_battery_data()`
121143
- `get_batteries_data()`
144+
- `get_pvis()`
122145
- `get_pvi_data()`
123146
- `get_pvis_data()`
147+
- `get_powermeters()`
124148
- `get_powermeter_data()`
125149
- `get_powermeters_data()`
126150
- `get_power_settings()`
@@ -130,34 +154,12 @@ Poll returns a dictionary like the following:
130154

131155
See the full documentation on [ReadTheDocs](https://python-e3dc.readthedocs.io/en/latest/)
132156

133-
### Note: The RSCP interface
157+
## Note: The RSCP interface
134158

135159
The communication to an E3/DC system has to be implemented 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.
136160

137161
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.
138162

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

162164
## Known limitations
163165

@@ -174,6 +176,8 @@ One limitation of the package concerns the implemented RSCP methods. This projec
174176

175177
- Open an issue before making a pull request
176178
- Note the E3/DC system you tested with and implementation details
177-
- Pull request checks will enforce code styling (black, flake8, flake8-docstrings, isort)
179+
- Pull request checks will enforce code styling
180+
- Install development dependencies `pip install -U --upgrade-strategy eager .[develop]`
181+
- Run `tools/validate.sh` before creating a commit.
178182
- Make sure to support Python versions >= 3.8
179183
- Consider adding yourself to `AUTHORS`

e3dc/_RSCPEncryptDecrypt.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from __future__ import annotations # required for python < 3.9
2+
13
import math
24

3-
from py3rijndael import RijndaelCbc, ZeroPadding
5+
from py3rijndael import RijndaelCbc, ZeroPadding # type: ignore
46

57
KEY_SIZE = 32
68
BLOCK_SIZE = 32
@@ -12,7 +14,7 @@ class ParameterError(Exception):
1214
pass
1315

1416

15-
def zeroPad_multiple(string, value):
17+
def zeroPad_multiple(string: bytes, value: int) -> bytes:
1618
"""Zero padding string."""
1719
length = len(string)
1820
if length % value == 0:
@@ -21,7 +23,7 @@ def zeroPad_multiple(string, value):
2123
return string.ljust(newL, b"\x00")
2224

2325

24-
def truncate_multiple(string, value):
26+
def truncate_multiple(string: bytes, value: int) -> bytes:
2527
"""Truncating sting."""
2628
length = len(string)
2729
if length % value == 0:
@@ -33,22 +35,21 @@ def truncate_multiple(string, value):
3335
class RSCPEncryptDecrypt:
3436
"""A class for encrypting and decrypting RSCP data."""
3537

36-
def __init__(self, key):
38+
def __init__(self, key: bytes):
3739
"""Constructor of a RSCP encryption and decryption class.
3840
3941
Args:
40-
key (str): RSCP encryption key
42+
key (bytes): RSCP encryption key
4143
"""
4244
if len(key) > KEY_SIZE:
4345
raise ParameterError("Key must be <%d bytes" % (KEY_SIZE))
44-
4546
self.key = key.ljust(KEY_SIZE, b"\xff")
4647
self.encryptIV = b"\xff" * BLOCK_SIZE
4748
self.decryptIV = b"\xff" * BLOCK_SIZE
4849
self.remainingData = b""
4950
self.oldDecrypt = b""
5051

51-
def encrypt(self, plainText):
52+
def encrypt(self, plainText: bytes) -> bytes:
5253
"""Method to encryt plain text."""
5354
encryptor = RijndaelCbc(
5455
self.key,
@@ -60,7 +61,9 @@ def encrypt(self, plainText):
6061
self.encryptIV = encText[-BLOCK_SIZE:]
6162
return encText
6263

63-
def decrypt(self, encText, previouslyProcessedData=None):
64+
def decrypt(
65+
self, encText: bytes, previouslyProcessedData: int | None = None
66+
) -> bytes:
6467
"""Method to decryt encrypted text."""
6568
if previouslyProcessedData is None:
6669
length = len(self.oldDecrypt)
@@ -92,4 +95,4 @@ def decrypt(self, encText, previouslyProcessedData=None):
9295
padding=ZeroPadding(BLOCK_SIZE),
9396
block_size=BLOCK_SIZE,
9497
)
95-
return decryptor.decrypt(toDecrypt)
98+
return decryptor.decrypt(toDecrypt) # pyright: ignore [reportUnknownMemberType]

e3dc/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Licensed under a MIT license. See LICENSE for details.
66
"""
77

8-
from ._e3dc import E3DC, AuthenticationError, PollError
8+
from ._e3dc import E3DC, AuthenticationError, NotAvailableError, PollError, SendError
99
from ._e3dc_rscp_local import CommunicationError, RSCPAuthenticationError, RSCPKeyError
1010
from ._e3dc_rscp_web import RequestTimeoutError, SocketNotReady
1111
from ._rscpLib import FrameError

0 commit comments

Comments
 (0)