From 8b2ada668d96283cf335faf15aa86464f638549c Mon Sep 17 00:00:00 2001 From: Christopher Banck Date: Tue, 28 Oct 2025 14:45:01 +0100 Subject: [PATCH 1/6] remove python 3.8 and 3.9 --- .github/workflows/make_wheel.yml | 4 +-- .github/workflows/validate.yml | 6 ++-- README.md | 2 +- e3dc/_RSCPEncryptDecrypt.py | 2 -- e3dc/_e3dc.py | 58 +++++++++++++++----------------- e3dc/_e3dc_rscp_local.py | 12 +++---- e3dc/_e3dc_rscp_web.py | 24 ++++++------- e3dc/_rscpLib.py | 24 ++++++------- e3dc/_rscpTags.py | 2 -- pyproject.toml | 7 ++-- 10 files changed, 65 insertions(+), 76 deletions(-) diff --git a/.github/workflows/make_wheel.yml b/.github/workflows/make_wheel.yml index 1d592c1..dcb3027 100644 --- a/.github/workflows/make_wheel.yml +++ b/.github/workflows/make_wheel.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Prepare python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' @@ -21,7 +21,7 @@ jobs: run: pip install build - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Build wheel run: python -m build diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c1e8d68..5c89f5c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/README.md b/README.md index 3f6d9ce..a7d2153 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Codestyle](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Documentation Status](https://readthedocs.org/projects/python-e3dc/badge/?version=latest)](https://python-e3dc.readthedocs.io/en/latest/?badge=latest) -**NOTE: With Release 0.8.0 at least Python 3.8 is required** +**NOTE: With Release 0.10.0 at least Python 3.10 is required** Python API for querying an [E3/DC](https://e3dc.de/) systems diff --git a/e3dc/_RSCPEncryptDecrypt.py b/e3dc/_RSCPEncryptDecrypt.py index 29462bc..def9c86 100644 --- a/e3dc/_RSCPEncryptDecrypt.py +++ b/e3dc/_RSCPEncryptDecrypt.py @@ -1,5 +1,3 @@ -from __future__ import annotations # required for python < 3.9 - import math from py3rijndael import RijndaelCbc, ZeroPadding # type: ignore diff --git a/e3dc/_e3dc.py b/e3dc/_e3dc.py index 62faa0d..481dfcc 100644 --- a/e3dc/_e3dc.py +++ b/e3dc/_e3dc.py @@ -3,15 +3,13 @@ # # Copyright 2017 Francesco Santini # Licensed under a MIT license. See LICENSE for details -from __future__ import annotations # required for python < 3.9 - import datetime import hashlib import struct import time import uuid from calendar import monthrange -from typing import Any, Dict, List, Literal, Tuple +from typing import Any, Literal from ._e3dc_rscp_local import ( E3DC_RSCP_local, @@ -102,9 +100,9 @@ def __init__(self, connectType: int, **kwargs: Any) -> None: self.maxBatChargePower = None self.maxBatDischargePower = None self.startDischargeDefault = None - self.powermeters: List[Dict[str, Any]] = [] - self.pvis: List[Dict[str, Any]] = [] - self.batteries: List[Dict[str, Any]] = [] + self.powermeters: list[dict[str, Any]] = [] + self.pvis: list[dict[str, Any]] = [] + self.batteries: list[dict[str, Any]] = [] self.pmIndexExt = None if "configuration" in kwargs: @@ -203,10 +201,10 @@ def _set_serial(self, serial: str): def sendRequest( self, - request: Tuple[str | int | RscpTag, str | int | RscpType, Any], + request: tuple[str | int | RscpTag, str | int | RscpType, Any], retries: int = 3, keepAlive: bool = False, - ) -> Tuple[str | int | RscpTag, str | int | RscpType, Any]: + ) -> tuple[str | int | RscpTag, str | int | RscpType, Any]: """This function uses the RSCP interface to make a request. Does make retries in case of exceptions like Socket.Error @@ -362,7 +360,7 @@ def poll_switches(self, keepAlive: bool = False): descList = switchDesc[2] # get the payload of the container statusList = switchStatus[2] - switchList: List[Dict[str, Any]] = [] + switchList: list[dict[str, Any]] = [] for switch in range(len(descList)): switchID = rscpFindTagIndex(descList[switch], RscpTag.HA_DATAPOINT_INDEX) @@ -466,7 +464,7 @@ def get_idle_periods(self, keepAlive: bool = False): if idlePeriodsRaw[0] != RscpTag.EMS_GET_IDLE_PERIODS: return None - idlePeriods: Dict[str, List[Dict[str, Any]]] = { + idlePeriods: dict[str, list[dict[str, Any]]] = { "idleCharge": [] * 7, "idleDischarge": [] * 7, } @@ -497,7 +495,7 @@ def get_idle_periods(self, keepAlive: bool = False): return idlePeriods def set_idle_periods( - self, idlePeriods: Dict[str, List[Dict[str, Any]]], keepAlive: bool = False + self, idlePeriods: dict[str, list[dict[str, Any]]], keepAlive: bool = False ): """Set idle periods via rscp protocol. @@ -546,7 +544,7 @@ def set_idle_periods( True if success False if error """ - periodList: List[Tuple[RscpTag, RscpType, Any]] = [] + periodList: list[tuple[RscpTag, RscpType, Any]] = [] if "idleCharge" not in idlePeriods and "idleDischarge" not in idlePeriods: raise ValueError("neither key idleCharge nor idleDischarge in object") @@ -1185,7 +1183,7 @@ def sendWallboxRequest( request: RscpTag = RscpTag.WB_REQ_SET_EXTERN, wbIndex: int = 0, keepAlive: bool = False, - ) -> Tuple[str | int | RscpTag, str | int | RscpType, Any]: + ) -> tuple[str | int | RscpTag, str | int | RscpType, Any]: """Sends a low-level request with WB_EXTERN_DATA to the wallbox via rscp protocol locally. Args: @@ -1294,7 +1292,7 @@ def get_batteries(self, keepAlive: bool = False): ] """ maxBatteries = 8 - outObj: List[Dict[str, int]] = [] + outObj: list[dict[str, int]] = [] for batIndex in range(maxBatteries): try: req = self.sendRequest( @@ -1326,7 +1324,7 @@ def get_batteries(self, keepAlive: bool = False): def get_battery_data( self, batIndex: int | None = None, - dcbs: List[int] | None = None, + dcbs: list[int] | None = None, keepAlive: bool = False, ): """Polls the battery data via rscp protocol. @@ -1470,7 +1468,7 @@ def get_battery_data( dcbCount = rscpFindTagIndex(req, RscpTag.BAT_DCB_COUNT) deviceStateContainer = rscpFindTag(req, RscpTag.BAT_DEVICE_STATE) - outObj: Dict[str, Any] = { + outObj: dict[str, Any] = { "asoc": rscpFindTagIndex(req, RscpTag.BAT_ASOC), "chargeCycles": rscpFindTagIndex(req, RscpTag.BAT_CHARGE_CYCLES), "current": rscpFindTagIndex(req, RscpTag.BAT_CURRENT), @@ -1555,9 +1553,9 @@ def get_battery_data( # Initialize default values for DCB sensorCount = 0 - temperatures: List[float] = [] + temperatures: list[float] = [] seriesCellCount = 0 - voltages: List[float] = [] + voltages: list[float] = [] # Set temperatures, if available for the device temperatures_raw = rscpFindTag(req, RscpTag.BAT_DCB_ALL_CELL_TEMPERATURES) @@ -1586,7 +1584,7 @@ def get_battery_data( for cell_voltage in voltages_data: voltages.append(cell_voltage[2]) - dcbobj: Dict[str, Any] = { + dcbobj: dict[str, Any] = { "current": rscpFindTagIndex(info, RscpTag.BAT_DCB_CURRENT), "currentAvg30s": rscpFindTagIndex( info, RscpTag.BAT_DCB_CURRENT_AVG_30S @@ -1655,7 +1653,7 @@ def get_battery_data( return outObj def get_batteries_data( - self, batteries: List[Dict[str, Any]] | None = None, keepAlive: bool = False + self, batteries: list[dict[str, Any]] | None = None, keepAlive: bool = False ): """Polls the batteries data via rscp protocol. @@ -1669,7 +1667,7 @@ def get_batteries_data( if batteries is None: batteries = self.batteries - outObj: List[Dict[str, Any]] = [] + outObj: list[dict[str, Any]] = [] for battery in batteries: if "dcbs" in battery: @@ -1703,7 +1701,7 @@ def get_pvis(self, keepAlive: bool = False): ] """ maxPvis = 8 - outObj: List[Dict[str, Any]] = [] + outObj: list[dict[str, Any]] = [] for pviIndex in range(maxPvis): req = self.sendRequest( ( @@ -1743,8 +1741,8 @@ def get_pvis(self, keepAlive: bool = False): def get_pvi_data( self, pviIndex: int | None = None, - strings: List[int] | None = None, - phases: List[int] | None = None, + strings: list[int] | None = None, + phases: list[int] | None = None, keepAlive: bool = False, ): """Polls the inverter data via rscp protocol. @@ -1861,7 +1859,7 @@ def get_pvi_data( frequency = rscpFindTag(req, RscpTag.PVI_FREQUENCY_UNDER_OVER) deviceState = rscpFindTag(req, RscpTag.PVI_DEVICE_STATE) - outObj: Dict[str, Any] = { + outObj: dict[str, Any] = { "acMaxApparentPower": rscpFindTagIndex( rscpFindTag(req, RscpTag.PVI_AC_MAX_APPARENTPOWER), RscpTag.PVI_VALUE ), @@ -2040,7 +2038,7 @@ def get_pvi_data( return outObj def get_pvis_data( - self, pvis: List[Dict[str, Any]] | None = None, keepAlive: bool = False + self, pvis: list[dict[str, Any]] | None = None, keepAlive: bool = False ): """Polls the inverters data via rscp protocol. @@ -2054,7 +2052,7 @@ def get_pvis_data( if pvis is None: pvis = self.pvis - outObj: List[Dict[str, Any]] = [] + outObj: list[dict[str, Any]] = [] for pvi in pvis: if "strings" in pvi: @@ -2095,7 +2093,7 @@ def get_powermeters(self, keepAlive: bool = False): ] """ maxPowermeters = 8 - outObj: List[Dict[str, Any]] = [] + outObj: list[dict[str, Any]] = [] for pmIndex in range( maxPowermeters ): # max 8 powermeters according to E3DC spec @@ -2212,7 +2210,7 @@ def get_powermeter_data(self, pmIndex: int | None = None, keepAlive: bool = Fals return outObj def get_powermeters_data( - self, powermeters: List[Dict[str, Any]] | None = None, keepAlive: bool = False + self, powermeters: list[dict[str, Any]] | None = None, keepAlive: bool = False ): """Polls the powermeters data via rscp protocol. @@ -2226,7 +2224,7 @@ def get_powermeters_data( if powermeters is None: powermeters = self.powermeters - outObj: List[Dict[str, Any]] = [] + outObj: list[dict[str, Any]] = [] for powermeter in powermeters: outObj.append( diff --git a/e3dc/_e3dc_rscp_local.py b/e3dc/_e3dc_rscp_local.py index 3004de4..904a180 100644 --- a/e3dc/_e3dc_rscp_local.py +++ b/e3dc/_e3dc_rscp_local.py @@ -4,10 +4,8 @@ # Copyright 2017 Francesco Santini # Licensed under a MIT license. See LICENSE for details -from __future__ import annotations # required for python < 3.9 - import socket -from typing import Any, Tuple +from typing import Any from ._RSCPEncryptDecrypt import RSCPEncryptDecrypt from ._rscpLib import rscpDecode, rscpEncode, rscpFrame @@ -67,7 +65,7 @@ def __init__( self.processedData = None def _send( - self, plainMsg: Tuple[str | int | RscpTag, str | int | RscpType, Any] + self, plainMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] ) -> None: sendData = rscpFrame(rscpEncode(plainMsg)) encData = self.encdec.encrypt(sendData) @@ -81,7 +79,7 @@ def _receive(self): return decData def sendCommand( - self, plainMsg: Tuple[str | int | RscpTag, str | int | RscpType, Any] + self, plainMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] ) -> None: """Sending RSCP command. @@ -91,8 +89,8 @@ def sendCommand( self.sendRequest(plainMsg) # same as sendRequest but doesn't return a value def sendRequest( - self, plainMsg: Tuple[str | int | RscpTag, str | int | RscpType, Any] - ) -> Tuple[str | int | RscpTag, str | int | RscpType, Any]: + self, plainMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] + ) -> tuple[str | int | RscpTag, str | int | RscpType, Any]: """Sending RSCP request. Args: diff --git a/e3dc/_e3dc_rscp_web.py b/e3dc/_e3dc_rscp_web.py index da6a261..5d2dec7 100644 --- a/e3dc/_e3dc_rscp_web.py +++ b/e3dc/_e3dc_rscp_web.py @@ -3,14 +3,12 @@ # # Copyright 2017 Francesco Santini # Licensed under a MIT license. See LICENSE for details -from __future__ import annotations # required for python < 3.9 - import datetime import hashlib import struct import threading import time -from typing import Any, Callable, Tuple +from typing import Any, Callable import tzlocal from websocket import ABNF, WebSocketApp @@ -135,10 +133,10 @@ def reset(self): self.virtAuthLevel = None self.webSerialno = None self.responseCallback: Callable[ - [Tuple[str | int | RscpTag, str | int | RscpType, Any]], None + [tuple[str | int | RscpTag, str | int | RscpType, Any]], None ] self.responseCallbackCalled = False - self.requestResult: Tuple[str | int | RscpTag, str | int | RscpType, Any] + self.requestResult: tuple[str | int | RscpTag, str | int | RscpType, Any] def buildVirtualConn(self): """Method to create Virtual Connection.""" @@ -164,7 +162,7 @@ def buildVirtualConn(self): self.ws.send(virtualConn, ABNF.OPCODE_BINARY) def respondToINFORequest( - self, decoded: Tuple[str | int | RscpTag, str | int | RscpType, Any] + self, decoded: tuple[str | int | RscpTag, str | int | RscpType, Any] ): """Create Response to INFO request.""" TIMEZONE_STR, utcDiffS = calcTimeZone() @@ -231,7 +229,7 @@ def respondToINFORequest( return None # this is no standard request def registerConnectionHandler( - self, decodedMsg: Tuple[str | int | RscpTag, str | int | RscpType, Any] + self, decodedMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] ): """Registering Connection Handler.""" if self.conId == 0: @@ -327,13 +325,13 @@ def on_message(self, message: bytes): ) def _defaultRequestCallback( - self, msg: Tuple[str | int | RscpTag, str | int | RscpType, Any] + self, msg: tuple[str | int | RscpTag, str | int | RscpType, Any] ): self.requestResult = msg def sendRequest( - self, message: Tuple[str | int | RscpTag, str | int | RscpType, Any] - ) -> Tuple[str | int | RscpTag, str | int | RscpType, Any]: + self, message: tuple[str | int | RscpTag, str | int | RscpType, Any] + ) -> tuple[str | int | RscpTag, str | int | RscpType, Any]: """Send a request and wait for a response.""" self._sendRequest_internal(rscpFrame(rscpEncode(message))) for _ in range(self.TIMEOUT * 10): @@ -346,16 +344,16 @@ def sendRequest( return self.requestResult def sendCommand( - self, message: Tuple[str | int | RscpTag, str | int | RscpType, Any] + self, message: tuple[str | int | RscpTag, str | int | RscpType, Any] ): """Send a command.""" return self._sendRequest_internal(rscpFrame(rscpEncode(message))) def _sendRequest_internal( self, - innerFrame: bytes | Tuple[str | int | RscpTag, str | int | RscpType, Any], + innerFrame: bytes | tuple[str | int | RscpTag, str | int | RscpType, Any], callback: ( - Callable[[Tuple[str | int | RscpTag, str | int | RscpType, Any]], None] + Callable[[tuple[str | int | RscpTag, str | int | RscpType, Any]], None] | None ) = None, ): diff --git a/e3dc/_rscpLib.py b/e3dc/_rscpLib.py index 4dde4fa..03f1dde 100644 --- a/e3dc/_rscpLib.py +++ b/e3dc/_rscpLib.py @@ -4,13 +4,11 @@ # Copyright 2017 Francesco Santini # Licensed under a MIT license. See LICENSE for details -from __future__ import annotations # required for python < 3.9 - import math import struct import time import zlib -from typing import Any, List, Tuple +from typing import Any, cast from ._rscpTags import ( RscpTag, @@ -62,9 +60,9 @@ def set_debug(debug: bool): def rscpFindTag( - decodedMsg: Tuple[str | int | RscpTag, str | int | RscpType, Any] | None, + decodedMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] | None, tag: int | str | RscpTag, -) -> Tuple[str | int | RscpTag, str | int | RscpType, Any] | None: +) -> tuple[str | int | RscpTag, str | int | RscpType, Any] | None: """Finds a submessage with a specific tag. Args: @@ -85,8 +83,8 @@ def rscpFindTag( if decodedMsg[0] == tagStr: return decodedMsg if isinstance(decodedMsg[2], list): - msgList: List[Tuple[str | int | RscpTag, str | int | RscpType, Any]] = ( - decodedMsg[2] + msgList: list[tuple[str | int | RscpTag, str | int | RscpType, Any]] = cast( + list[tuple[str | int | RscpTag, str | int | RscpType, Any]], decodedMsg[2] ) for msg in msgList: msgValue = rscpFindTag(msg, tag) @@ -96,7 +94,7 @@ def rscpFindTag( def rscpFindTagIndex( - decodedMsg: Tuple[str | int | RscpTag, str | int | RscpType, Any] | None, + decodedMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] | None, tag: int | str | RscpTag, index: int = 2, ) -> Any: @@ -129,7 +127,7 @@ class FrameError(Exception): def rscpEncode( - tag: int | str | RscpTag | Tuple[str | int | RscpTag, str | int | RscpType, Any], + tag: int | str | RscpTag | tuple[str | int | RscpTag, str | int | RscpType, Any], rscptype: int | str | RscpType | None = None, data: Any = None, ) -> bytes: @@ -174,7 +172,9 @@ def rscpEncode( elif rscptype == RscpType.Container: if isinstance(data, list): newData = b"" - dataList: List[Tuple[str | int | RscpTag, str | int | RscpType, Any]] = data + dataList: list[tuple[str | int | RscpTag, str | int | RscpType, Any]] = ( + cast(list[tuple[str | int | RscpTag, str | int | RscpType, Any]], data) + ) for dataChunk in dataList: newData += rscpEncode( dataChunk[0], dataChunk[1], dataChunk[2] @@ -248,7 +248,7 @@ def rscpFrameDecode(frameData: bytes, returnFrameLen: bool = False): def rscpDecode( data: bytes, -) -> Tuple[Tuple[str | int | RscpTag, str | int | RscpType, Any], int]: +) -> tuple[tuple[str | int | RscpTag, str | int | RscpType, Any], int]: """Decodes RSCP data.""" headerFmt = ( " Date: Tue, 28 Oct 2025 15:06:50 +0100 Subject: [PATCH 2/6] TypeAlias for RscpMessage --- e3dc/__init__.py | 3 ++- e3dc/_e3dc_rscp_local.py | 15 ++++----------- e3dc/_e3dc_rscp_web.py | 38 ++++++++++++-------------------------- e3dc/_rscpLib.py | 25 ++++++++++++------------- 4 files changed, 30 insertions(+), 51 deletions(-) diff --git a/e3dc/__init__.py b/e3dc/__init__.py index 64090b8..a24e630 100644 --- a/e3dc/__init__.py +++ b/e3dc/__init__.py @@ -8,7 +8,7 @@ from ._e3dc import E3DC, AuthenticationError, NotAvailableError, PollError, SendError from ._e3dc_rscp_local import CommunicationError, RSCPAuthenticationError, RSCPKeyError from ._e3dc_rscp_web import RequestTimeoutError, SocketNotReady -from ._rscpLib import FrameError +from ._rscpLib import FrameError, RscpMessage from ._rscpLib import set_debug as set_rscp_debug __all__ = [ @@ -23,6 +23,7 @@ "RequestTimeoutError", "SocketNotReady", "FrameError", + "RscpMessage", "set_rscp_debug", ] __version__ = "0.9.3" diff --git a/e3dc/_e3dc_rscp_local.py b/e3dc/_e3dc_rscp_local.py index 904a180..c45a47d 100644 --- a/e3dc/_e3dc_rscp_local.py +++ b/e3dc/_e3dc_rscp_local.py @@ -5,10 +5,9 @@ # Licensed under a MIT license. See LICENSE for details import socket -from typing import Any from ._RSCPEncryptDecrypt import RSCPEncryptDecrypt -from ._rscpLib import rscpDecode, rscpEncode, rscpFrame +from ._rscpLib import RscpMessage, rscpDecode, rscpEncode, rscpFrame from ._rscpTags import RscpError, RscpTag, RscpType PORT = 5033 @@ -64,9 +63,7 @@ def __init__( self.encdec: RSCPEncryptDecrypt self.processedData = None - def _send( - self, plainMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] - ) -> None: + def _send(self, plainMsg: RscpMessage) -> None: sendData = rscpFrame(rscpEncode(plainMsg)) encData = self.encdec.encrypt(sendData) self.socket.send(encData) @@ -78,9 +75,7 @@ def _receive(self): decData = rscpDecode(self.encdec.decrypt(data))[0] return decData - def sendCommand( - self, plainMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] - ) -> None: + def sendCommand(self, plainMsg: RscpMessage) -> None: """Sending RSCP command. Args: @@ -88,9 +83,7 @@ def sendCommand( """ self.sendRequest(plainMsg) # same as sendRequest but doesn't return a value - def sendRequest( - self, plainMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] - ) -> tuple[str | int | RscpTag, str | int | RscpType, Any]: + def sendRequest(self, plainMsg: RscpMessage) -> RscpMessage: """Sending RSCP request. Args: diff --git a/e3dc/_e3dc_rscp_web.py b/e3dc/_e3dc_rscp_web.py index 5d2dec7..fc4e54d 100644 --- a/e3dc/_e3dc_rscp_web.py +++ b/e3dc/_e3dc_rscp_web.py @@ -8,12 +8,13 @@ import struct import threading import time -from typing import Any, Callable +from typing import Callable import tzlocal from websocket import ABNF, WebSocketApp from ._rscpLib import ( + RscpMessage, rscpDecode, rscpEncode, rscpFindTag, @@ -132,11 +133,9 @@ def reset(self): self.virtConId = None self.virtAuthLevel = None self.webSerialno = None - self.responseCallback: Callable[ - [tuple[str | int | RscpTag, str | int | RscpType, Any]], None - ] + self.responseCallback: Callable[[RscpMessage], None] self.responseCallbackCalled = False - self.requestResult: tuple[str | int | RscpTag, str | int | RscpType, Any] + self.requestResult: RscpMessage def buildVirtualConn(self): """Method to create Virtual Connection.""" @@ -161,9 +160,7 @@ def buildVirtualConn(self): # print("--------------------- Sending virtual conn") self.ws.send(virtualConn, ABNF.OPCODE_BINARY) - def respondToINFORequest( - self, decoded: tuple[str | int | RscpTag, str | int | RscpType, Any] - ): + def respondToINFORequest(self, decoded: RscpMessage): """Create Response to INFO request.""" TIMEZONE_STR, utcDiffS = calcTimeZone() @@ -228,9 +225,7 @@ def respondToINFORequest( return "" return None # this is no standard request - def registerConnectionHandler( - self, decodedMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] - ): + def registerConnectionHandler(self, decodedMsg: RscpMessage): """Registering Connection Handler.""" if self.conId == 0: self.conId = rscpFindTagIndex(decodedMsg, RscpTag.SERVER_CONNECTION_ID) @@ -324,14 +319,10 @@ def on_message(self, message: bytes): ABNF.OPCODE_BINARY, ) - def _defaultRequestCallback( - self, msg: tuple[str | int | RscpTag, str | int | RscpType, Any] - ): + def _defaultRequestCallback(self, msg: RscpMessage): self.requestResult = msg - def sendRequest( - self, message: tuple[str | int | RscpTag, str | int | RscpType, Any] - ) -> tuple[str | int | RscpTag, str | int | RscpType, Any]: + def sendRequest(self, message: RscpMessage) -> RscpMessage: """Send a request and wait for a response.""" self._sendRequest_internal(rscpFrame(rscpEncode(message))) for _ in range(self.TIMEOUT * 10): @@ -343,24 +334,19 @@ def sendRequest( return self.requestResult - def sendCommand( - self, message: tuple[str | int | RscpTag, str | int | RscpType, Any] - ): + def sendCommand(self, message: RscpMessage): """Send a command.""" return self._sendRequest_internal(rscpFrame(rscpEncode(message))) def _sendRequest_internal( self, - innerFrame: bytes | tuple[str | int | RscpTag, str | int | RscpType, Any], - callback: ( - Callable[[tuple[str | int | RscpTag, str | int | RscpType, Any]], None] - | None - ) = None, + innerFrame: bytes | RscpMessage, + callback: Callable[[RscpMessage], None] | None = None, ): """Internal send request method. Args: - innerFrame (Union[tuple, ]): inner frame + innerFrame (tuple | bytes): inner frame callback (str): callback method synchronous (bool): If True, the method waits for a response (i.e. exits after calling callback). If True and callback = None, the method returns the (last) response message diff --git a/e3dc/_rscpLib.py b/e3dc/_rscpLib.py index 03f1dde..29501a3 100644 --- a/e3dc/_rscpLib.py +++ b/e3dc/_rscpLib.py @@ -8,7 +8,7 @@ import struct import time import zlib -from typing import Any, cast +from typing import Any, TypeAlias, cast from ._rscpTags import ( RscpTag, @@ -21,6 +21,9 @@ getStrRscpType, ) +# Type alias for RSCP messages +RscpMessage: TypeAlias = tuple[str | int | RscpTag, str | int | RscpType, Any] + DEBUG_DICT = {"print_rscp": False} @@ -60,9 +63,9 @@ def set_debug(debug: bool): def rscpFindTag( - decodedMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] | None, + decodedMsg: RscpMessage | None, tag: int | str | RscpTag, -) -> tuple[str | int | RscpTag, str | int | RscpType, Any] | None: +) -> RscpMessage | None: """Finds a submessage with a specific tag. Args: @@ -83,9 +86,7 @@ def rscpFindTag( if decodedMsg[0] == tagStr: return decodedMsg if isinstance(decodedMsg[2], list): - msgList: list[tuple[str | int | RscpTag, str | int | RscpType, Any]] = cast( - list[tuple[str | int | RscpTag, str | int | RscpType, Any]], decodedMsg[2] - ) + msgList: list[RscpMessage] = cast(list[RscpMessage], decodedMsg[2]) for msg in msgList: msgValue = rscpFindTag(msg, tag) if msgValue is not None: @@ -94,7 +95,7 @@ def rscpFindTag( def rscpFindTagIndex( - decodedMsg: tuple[str | int | RscpTag, str | int | RscpType, Any] | None, + decodedMsg: RscpMessage | None, tag: int | str | RscpTag, index: int = 2, ) -> Any: @@ -127,7 +128,7 @@ class FrameError(Exception): def rscpEncode( - tag: int | str | RscpTag | tuple[str | int | RscpTag, str | int | RscpType, Any], + tag: int | str | RscpTag | RscpMessage, rscptype: int | str | RscpType | None = None, data: Any = None, ) -> bytes: @@ -172,9 +173,7 @@ def rscpEncode( elif rscptype == RscpType.Container: if isinstance(data, list): newData = b"" - dataList: list[tuple[str | int | RscpTag, str | int | RscpType, Any]] = ( - cast(list[tuple[str | int | RscpTag, str | int | RscpType, Any]], data) - ) + dataList: list[RscpMessage] = cast(list[RscpMessage], data) for dataChunk in dataList: newData += rscpEncode( dataChunk[0], dataChunk[1], dataChunk[2] @@ -248,7 +247,7 @@ def rscpFrameDecode(frameData: bytes, returnFrameLen: bool = False): def rscpDecode( data: bytes, -) -> tuple[tuple[str | int | RscpTag, str | int | RscpType, Any], int]: +) -> tuple[RscpMessage, int]: """Decodes RSCP data.""" headerFmt = ( " Date: Tue, 28 Oct 2025 15:07:10 +0100 Subject: [PATCH 3/6] replace Optional in docstrings --- e3dc/_e3dc.py | 62 +++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/e3dc/_e3dc.py b/e3dc/_e3dc.py index 481dfcc..8aac428 100644 --- a/e3dc/_e3dc.py +++ b/e3dc/_e3dc.py @@ -74,7 +74,7 @@ def __init__(self, connectType: int, **kwargs: Any) -> None: key (str): encryption key as set in the E3DC settings - required for CONNECT_LOCAL serialNumber (str): the serial number of the system to monitor - required for CONNECT_WEB isPasswordMd5 (bool): indicates whether the password is already md5 digest (recommended, default = True) - required for CONNECT_WEB - configuration (Optional[dict]): dict containing details of the E3DC configuration. {"pvis": [{"index": 0, "strings": 2, "phases": 3}], "powermeters": [{"index": 0}], "batteries": [{"index": 0, "dcbs": 1}]} + configuration (dict | None): dict containing details of the E3DC configuration. {"pvis": [{"index": 0, "strings": 2, "phases": 3}], "powermeters": [{"index": 0}], "batteries": [{"index": 0, "dcbs": 1}]} port (int, optional): port number for local connection. Defaults to None, which means default port 5033 is used. """ self.connectType = connectType @@ -994,8 +994,8 @@ def get_wallbox_data(self, wbIndex: int = 0, keepAlive: bool = False): """Polls the wallbox status via rscp protocol locally. Args: - wbIndex (Optional[int]): Index of the wallbox to poll data for - keepAlive (Optional[bool]): True to keep connection alive + wbIndex (int | None): Index of the wallbox to poll data for + keepAlive (bool | None): True to keep connection alive Returns: dict: Dictionary containing the wallbox status structured as follows:: @@ -1093,8 +1093,8 @@ def set_wallbox_sunmode( Args: enable (bool): True to enable sun mode, otherwise false, - wbIndex (Optional[int]): index of the requested wallbox, - keepAlive (Optional[bool]): True to keep connection alive + wbIndex (int | None): index of the requested wallbox, + keepAlive (bool | None): True to keep connection alive Returns: True if success @@ -1111,8 +1111,8 @@ def set_wallbox_schuko( Args: on (bool): True to activate the Schuko, otherwise false - wbIndex (Optional[int]): index of the requested wallbox, - keepAlive (Optional[bool]): True to keep connection alive + wbIndex (int | None): index of the requested wallbox, + keepAlive (bool | None): True to keep connection alive Returns: True if success (wallbox has understood the request, but might have ignored an unsupported value) @@ -1129,8 +1129,8 @@ def set_wallbox_max_charge_current( Args: max_charge_current (int): maximum allowed charge current in A - wbIndex (Optional[int]): index of the requested wallbox, - keepAlive (Optional[bool]): True to keep connection alive + wbIndex (int | None): index of the requested wallbox, + keepAlive (bool | None): True to keep connection alive Returns: True if success (wallbox has understood the request, but might have clipped the value) @@ -1150,8 +1150,8 @@ def toggle_wallbox_charging( """Toggles charging of the wallbox via rscp protocol locally. Args: - wbIndex (Optional[int]): index of the requested wallbox, - keepAlive (Optional[bool]): True to keep connection alive + wbIndex (int | None): index of the requested wallbox, + keepAlive (bool | None): True to keep connection alive Returns: True if success @@ -1165,8 +1165,8 @@ def toggle_wallbox_phases(self, wbIndex: int = 0, keepAlive: bool = False) -> bo """Toggles the number of phases used for charging by the wallbox between 1 and 3 via rscp protocol locally. Args: - wbIndex (Optional[int]): index of the requested wallbox, - keepAlive (Optional[bool]): True to keep connection alive + wbIndex (int | None): index of the requested wallbox, + keepAlive (bool | None): True to keep connection alive Returns: True if success @@ -1189,9 +1189,9 @@ def sendWallboxRequest( Args: dataIndex (int): byte index in the WB_EXTERN_DATA array (values: 0-5) value (int): byte value to be set in the WB_EXTERN_DATA array at the given index - request (Optional[RscpTag]): request identifier (WB_REQ_SET_EXTERN, WB_REQ_SET_PARAM_1 or WB_REQ_SET_PARAM_2), - wbIndex (Optional[int]): index of the requested wallbox, - keepAlive (Optional[bool]): True to keep connection alive + request (RscpTag | None): request identifier (WB_REQ_SET_EXTERN, WB_REQ_SET_PARAM_1 or WB_REQ_SET_PARAM_2), + wbIndex (int | None): index of the requested wallbox, + keepAlive (bool | None): True to keep connection alive Returns: An object with the received data @@ -1235,9 +1235,9 @@ def sendWallboxSetRequest( Args: dataIndex (int): byte index in the WB_EXTERN_DATA array (values: 0-5) value (int): byte value to be set in the WB_EXTERN_DATA array at the given index - request (Optional[RscpTag]): request identifier (WB_REQ_SET_EXTERN, WB_REQ_SET_PARAM_1 or WB_REQ_SET_PARAM_2), - wbIndex (Optional[int]): index of the requested wallbox, - keepAlive (Optional[bool]): True to keep connection alive + request (RscpTag | None): request identifier (WB_REQ_SET_EXTERN, WB_REQ_SET_PARAM_1 or WB_REQ_SET_PARAM_2), + wbIndex (int | None): index of the requested wallbox, + keepAlive (bool | None): True to keep connection alive Returns: True if success @@ -1260,7 +1260,7 @@ def set_battery_to_car_mode(self, enabled: bool, keepAlive: bool = False): Args: enabled (bool): True to enable charging the car using the battery - keepAlive (Optional[bool]): True to keep connection alive + keepAlive (bool | None): True to keep connection alive Returns: True if success @@ -1330,8 +1330,8 @@ def get_battery_data( """Polls the battery data via rscp protocol. Args: - batIndex (Optional[int]): battery index - dcbs (Optional[list]): dcb list + batIndex (int | None): battery index + dcbs (list | None): dcb list keepAlive (bool): True to keep connection alive. Defaults to False. Returns: @@ -1658,7 +1658,7 @@ def get_batteries_data( """Polls the batteries data via rscp protocol. Args: - batteries (Optional[dict]): batteries dict + batteries (dict | None): batteries dict keepAlive (bool): True to keep connection alive. Defaults to False. Returns: @@ -1749,8 +1749,8 @@ def get_pvi_data( Args: pviIndex (int): pv inverter index - strings (Optional[list]): string list - phases (Optional[list]): phase list + strings (list | None): string list + phases (list | None): phase list keepAlive (bool): True to keep connection alive. Defaults to False. Returns: @@ -2043,7 +2043,7 @@ def get_pvis_data( """Polls the inverters data via rscp protocol. Args: - pvis (Optional[dict]): pvis dict + pvis (dict | None): pvis dict keepAlive (bool): True to keep connection alive. Defaults to False. Returns: @@ -2126,7 +2126,7 @@ def get_powermeter_data(self, pmIndex: int | None = None, keepAlive: bool = Fals """Polls the power meter data via rscp protocol. Args: - pmIndex (Optional[int]): power meter index + pmIndex (int | None): power meter index keepAlive (bool): True to keep connection alive. Defaults to False. Returns: @@ -2215,7 +2215,7 @@ def get_powermeters_data( """Polls the powermeters data via rscp protocol. Args: - powermeters (Optional[dict]): powermeters dict + powermeters (dict | None): powermeters dict keepAlive (bool): True to keep connection alive. Defaults to False. Returns: @@ -2297,9 +2297,9 @@ def set_power_limits( Args: enable (bool): True/False - max_charge (Optional[int]): maximum charge power - max_discharge (Optional[int]: maximum discharge power - discharge_start (Optional[int]: power where discharged is started + max_charge (int | None): maximum charge power + max_discharge (int | None): maximum discharge power + discharge_start (int | None): power where discharged is started keepAlive (bool): True to keep connection alive. Defaults to False. Returns: From 00099d0b728d45e9d4e2c4d53cc63d02ad2c4526 Mon Sep 17 00:00:00 2001 From: Christopher Banck Date: Tue, 28 Oct 2025 15:11:27 +0100 Subject: [PATCH 4/6] Bump version from 0.9.3 to 0.10.0 --- e3dc/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e3dc/__init__.py b/e3dc/__init__.py index a24e630..c851c82 100644 --- a/e3dc/__init__.py +++ b/e3dc/__init__.py @@ -26,4 +26,4 @@ "RscpMessage", "set_rscp_debug", ] -__version__ = "0.9.3" +__version__ = "0.10.0" From e5117681f731fbbf9abbe97fc8894dcd57341501 Mon Sep 17 00:00:00 2001 From: Christopher Banck Date: Tue, 28 Oct 2025 15:52:24 +0100 Subject: [PATCH 5/6] add pyright back --- .github/workflows/validate.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 5c89f5c..c98e7ab 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -24,5 +24,7 @@ jobs: run: isort ./ --check - name: Run black run: black ./ --check + - name: Run pyright + run: pyright - name: Run test install run: pip install . From 74bab4ca86bd72697e631b5d23abde224da74136 Mon Sep 17 00:00:00 2001 From: Christopher Banck Date: Tue, 28 Oct 2025 16:54:53 +0100 Subject: [PATCH 6/6] improve port logic --- e3dc/_e3dc_rscp_local.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e3dc/_e3dc_rscp_local.py b/e3dc/_e3dc_rscp_local.py index c45a47d..33da6af 100644 --- a/e3dc/_e3dc_rscp_local.py +++ b/e3dc/_e3dc_rscp_local.py @@ -10,7 +10,7 @@ from ._rscpLib import RscpMessage, rscpDecode, rscpEncode, rscpFrame from ._rscpTags import RscpError, RscpTag, RscpType -PORT = 5033 +DEFAULT_PORT = 5033 BUFFER_SIZE = 1024 * 32 @@ -42,7 +42,7 @@ class E3DC_RSCP_local: """A class describing an E3DC system connection using RSCP protocol locally.""" def __init__( - self, username: str, password: str, ip: str, key: str, port: int | None = PORT + self, username: str, password: str, ip: str, key: str, port: int | None = None ): """Constructor of an E3DC RSCP local object. @@ -56,7 +56,7 @@ def __init__( self.username = username.encode("utf-8") self.password = password.encode("utf-8") self.ip = ip - self.port = port if port else PORT + self.port = port or DEFAULT_PORT self.key = key.encode("utf-8") self.socket: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.connected: bool = False