Metadata-Version: 2.4
Name: rscp_lib
Version: 1.0.0
Summary: Asyncio Python client for E3/DC's RSCP (Remote Storage Control Protocol)
Author-email: Tobias Terhaar <31091813+tobias-terhaar@users.noreply.github.com>
License: MIT License
        
        Copyright (c) 2025 Tobias Terhaar
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/tobias-terhaar/rscp-lib
Project-URL: Issues, https://github.com/tobias-terhaar/rscp-lib/issues
Project-URL: Repository, https://github.com/tobias-terhaar/rscp-lib
Keywords: e3dc,rscp,home-assistant,asyncio,energy,solar
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Home Automation
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: py3rijndael
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Requires-Dist: pytest-asyncio>=0.21; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: coverage; extra == "test"
Dynamic: license-file

# rscp_lib

An asyncio Python client library for E3/DC's **RSCP** (Remote Storage Control Protocol) — the TCP protocol used to communicate with E3/DC home-storage and energy-management devices (default port `5033`).

The library handles the full protocol stack:

- TCP framing (`0xDCE3` magic, timestamp, length)
- AES/Rijndael-256 CBC encryption with stateful IV chaining
- Authentication (user/password)
- Typed tag/value (TLV) (de)serialization, including nested containers
- A small path/filter query language for extracting values from responses

## Requirements

- Python 3.9+
- [`py3rijndael`](https://pypi.org/project/py3rijndael/)

```bash
pip install py3rijndael
```

## Installation

The package is not yet published to PyPI. Install directly from the repository:

```bash
pip install git+https://github.com/<your-user>/e3dc_rscp_lib.git
```

or clone and add the project root to your `PYTHONPATH`.

## Quick start

```python
import asyncio

from rscp_lib.RscpConnection import RscpConnection
from rscp_lib.RscpEncryption import RscpEncryption
from rscp_lib.RscpFrame import RscpFrame
from rscp_lib.RscpValue import RscpValue


async def main():
    cipher = RscpEncryption("YOUR_RSCP_PASSPHRASE")
    conn = RscpConnection(
        host="192.168.1.50",
        port=5033,
        ciphersuite=cipher,
        username="user@example.com",
        password="portal-password",
    )

    await conn.connect()
    if not await conn.authorize():
        raise RuntimeError("authentication failed")

    # Request the current PV power
    request = RscpValue().withTagName("TAG_EMS_REQ_POWER_PV", None)
    await conn.send(RscpFrame().packFrame(request))

    data = await conn.receive()
    frame = RscpFrame()
    frame.unpack(data)

    for value in frame.getRscpValues():
        print(value.toString())

    conn.disconnect()


asyncio.run(main())
```

## Building requests

Simple values use `withTagName`:

```python
RscpValue().withTagName("TAG_EMS_REQ_POWER_BAT", None)
```

Nested containers can be built declaratively via `construct_rscp_value`:

```python
req = RscpValue.construct_rscp_value(
    "TAG_RSCP_REQ_AUTHENTICATION",
    [
        ["TAG_RSCP_AUTHENTICATION_USER", "user@example.com"],
        ["TAG_RSCP_AUTHENTICATION_PASSWORD", "secret"],
    ],
)
```

## Reading responses

For navigating deeply nested container responses, use the path helper:

```python
values = frame.getRscpValues()

# Direct child
soc = RscpValue.get_tag_by_path(values, "TAG_EMS_BAT_SOC")

# Nested path
v = RscpValue.get_tag_by_path(values, "TAG_PVI_DATA/TAG_PVI_DC_POWER")

# Filter a container by a child tag's value, then descend
string0 = RscpValue.get_tag_by_path(
    values,
    "TAG_PVI_DATA(TAG_PVI_INDEX==0)/TAG_PVI_DC_POWER",
)
```

## Architecture

The stack is built from four composable layers:

| Layer | Module | Responsibility |
|-------|--------|----------------|
| Tag/Value (TLV) | `RscpValue.py` | Typed tag-value encoding with nested containers |
| Frame | `RscpFrame.py` | Wire frame header, timestamp, and length |
| Encryption | `RscpEncryption.py` | Rijndael-256 CBC with rolling IV |
| Connection | `RscpConnection.py` | Async TCP socket, authentication, send/receive |

`RscpTags.py` contains the full tag dictionary (tag name → tag code + declared type), used for both packing outgoing values and decoding incoming ones.

## Protocol notes

A few things worth knowing when extending the library:

- Encryption IVs start as `0xff` × 32 and are updated to the last ciphertext block after every operation. A new connection must call `RscpEncryption.reset()` (`RscpConnection.connect()` does this automatically).
- `TAG_RSCP_AUTHENTICATION` responses on **failure** come back as `Int32` instead of the declared `UChar8` — handled as a special case in `RscpValue.unpack`.
- Error tags use type id `0xFF`; a 1-byte payload is an `Error8`, a 4-byte payload an `Error32`. Check `RscpValue.isError` after unpacking.
- Ciphertext whose length is not a multiple of the block size cannot be decrypted — `decrypt()` returns `None` and a warning is logged. The caller is expected to read more bytes and retry.
- Outgoing frames set nanoseconds to `0`; only `int(time.time())` is transmitted.

## Status

This is an independent implementation of the RSCP protocol based on publicly available documentation and is **not affiliated with or endorsed by E3/DC GmbH**. The tag dictionary is fairly complete, but not every tag combination has been exercised against real hardware. Contributions and bug reports are welcome.

## License

The code is distributed under MIT license. See LICENSE file for details.
