Metadata-Version: 2.5
Name: pytriplesolar
Version: 0.1.0a1
Summary: Async library for Triple Solar PVT heat pumps over Modbus/TLS.
Project-URL: Homepage, https://github.com/florianschut/pytriplesolar
Project-URL: Issues, https://github.com/florianschut/pytriplesolar/issues
Author: Florian Schut
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: heat-pump,modbus,modbus-tls,pvt,triple-solar
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Home Automation
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: aiohttp>=3.11
Requires-Dist: cryptography>=44
Requires-Dist: modbus-connection<5,>=4.12.7
Provides-Extra: test
Requires-Dist: modbus-connection[tmodbus]<5,>=4.12.7; extra == 'test'
Requires-Dist: pytest-asyncio>=0.25; extra == 'test'
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

# pytriplesolar

Asynchronous Python library for the **Triple Solar PVT Heat Pump 5.0**, over
Modbus/TLS.

It is built on [`modbus-connection`](https://pypi.org/project/modbus-connection/):
the library describes the heat pump's registers as typed components and takes a
`ModbusUnit`. It never opens a connection itself, so the application decides
how and when to connect, and can share one connection between users.

## Requirements

- Python 3.13+
- A Triple Solar heat pump with Modbus TCP set to *secure* (the default), on
  the same network. Discovery uses mDNS, which does not cross routers or VLANs.

## Pairing

Modbus/TLS uses mutual certificate authentication. A client pairs once to get
a certificate signed by the heat pump's own root CA:

1. Enable pairing mode in the My Triple Solar app. The heat pump then runs a
   single-use signing service for one minute, announced over mDNS-SD as
   `_ts-csr._tcp`.
2. Browse for that service with your zeroconf library of choice. Then pass its
   address, port and host name to `async_pair`:

```python
import aiohttp
from pytriplesolar import async_pair

async with aiohttp.ClientSession() as session:
    credentials = await async_pair(
        session, "192.0.2.10", 6509, device_hostname="hpcb-a1b2c3.local"
    )
```

`Credentials` holds the root CA, the client certificate and key (PEM), plus the
host name the heat pump was paired under. Store it with `as_dict()` and restore
it with `Credentials.from_dict()`. It contains a private key, so keep it
private.

## Reading and writing

Find the paired heat pump's `_mbap-s._tcp` service (port 802), then connect
with the credentials:

```python
from pathlib import Path

from modbus_connection.tmodbus import connect_tls
from pytriplesolar import Profile, SpaceConditioningMode, TripleSolarHeatPump

files = credentials.write_files(Path("certs"))  # blocking: ssl needs files
params = files.tls_params("192.0.2.10")

connection = await connect_tls(
    params.host,
    port=params.port,
    verify=params.verify,
    check_hostname=params.check_hostname,
    client_cert=params.client_cert,
    client_key=params.client_key,
)
heat_pump = TripleSolarHeatPump(connection.for_unit(1))

report = await heat_pump.async_update()
print(heat_pump.general.state, [str(a) for a in heat_pump.alerts.active])
print(heat_pump.dhw.tank_temperature, heat_pump.heat_pump_module_1.electric_power)

await heat_pump.settings.write("dhw_profile", Profile.COMFORT)
await heat_pump.dhw_comfort.write("tank_temperature_setpoint", 55.0)
await heat_pump.control.write("mode", SpaceConditioningMode.HEATING)
```

The server certificate is verified against the heat pump's root CA. That CA is
unique to the unit, so the connection is pinned to the paired heat pump. The
host name is not checked, because the heat pump is normally reached by IP.

## Sub-systems

| Attribute | Registers | Contents |
| --- | --- | --- |
| `general` | input | overall state, total rated power |
| `alerts` | input | up to 20 active alerts, decoded to service codes (`F10203`) |
| `dhw` | input | DHW state, demand, modulation, tank temperature and setpoint, valve, active profile |
| `space_heating` / `space_cooling` | input | state, demand, modulation, sink supply temperature and setpoint |
| `space_conditioning_input` | input | room/ambient temperature, humidity, room setpoint, thermostat modulation |
| `heat_pump_module_1` / `_2` | input | module state, compressor frequency, pumps, cooling valve, source/sink temperatures, electric/thermal power, compressor starts |
| `settings` | holding (R/W) | space heating / cooling / DHW enabled and profile |
| `dhw_eco` / `dhw_normal` / `dhw_comfort` | holding (R/W) | tank setpoint (10–70 °C), lower hysteresis (0–50 °C), modulation (20–100 %) per DHW profile |
| `control` | holding (R/W) | external control: mode, modulation level (0–100 %), power budget (W) |

`heat_pump_module_2` is `None` on a unit with a single module (detected on the
first update).

Values the heat pump does not currently have (for example, no room thermostat
connected) read as `None`. The heat pump refuses a whole block of registers as
soon as one value in it is missing, so the library then re-reads that
sub-system one register at a time.

### External control

The `control` registers live in memory only. The heat pump acts on them only
while a Modbus client has been active within the last 30 seconds
(`CONTROL_KEEPALIVE_SECONDS`). Poll faster than that while controlling it.

## Development

```sh
python -m venv .venv
.venv/bin/pip install -e ".[test]"
.venv/bin/pytest
```

Tests run against `modbus-connection`'s in-memory mock unit via its pytest
plugin (`mock_modbus_unit` fixture), so no hardware is needed.

## License

Apache-2.0
