Async Python library for Tecnosystemi Polaris 5X HVAC Control Unit IoT devices
Project description
๐ก๏ธ Open Polaris Local API
Asynchronous Python library for Tecnosystemi Polaris 5X HVAC Control Unit devices
Features โข Installation โข Quick Start โข Documentation โข Examples โข Scripts โข Testing
โจ Features
๐ Performance
- Built with asyncio for non-blocking operations
- Efficient TCP communication โ short-lived connections per command (mirrors official app)
- Automatic retry on timeout or transient failure
- Compact
stato_rpolling with transparent fallback to fullstato
๐ Reliability
- Auto-retry with configurable attempts and delay
- Graceful fallback:
stato_rโstatoon older firmware - Robust error decoding via bitmask for both CU and zone errors
๐ฏ Developer Friendly
- Type-safe dataclasses โ
PolarisDeviceandPolarisZone - Async context manager support
- Comprehensive logging with optional verbose mode
- Supports both local TCP (snake_case) and cloud API (PascalCase) response formats
๐ Auto-Discovery
- Subnet scan โ finds all Polaris devices on your LAN without knowing their IPs
- Concurrent TCP probing with configurable timeout and concurrency
- Identifies devices by validating the Polaris protocol response
๐๏ธ Full Control
- CU-level: power, cooling mode, operating mode
- Zone-level: temperature setpoint, on/off, chrono mode, fan speed, shutter
- Human-readable error decoding (bitmask โ error string list)
๐ฆ Installation
pip (recommended)
Install directly from a GitHub tag:
pip install "open-polaris-local-api @ git+https://github.com/VoidElle/open-polaris-local-api.git@v1.2.1"
Home Assistant integration
Add to your integration's manifest.json:
"requirements": [
"open-polaris-local-api @ git+https://github.com/VoidElle/open-polaris-local-api.git@v1.2.1"
]
Manual
- Clone this repository into your project
- Ensure the
open_polaris_local_api/folder is on your Python path - Import with
from open_polaris_local_api import PolarisLocalClient
๐ Quick Start
Auto-Discovery
Find all Polaris devices on your network without knowing their IPs:
import asyncio
from open_polaris_local_api import PolarisAutoDiscovery
async def main():
ips = await PolarisAutoDiscovery.discover(pin="1234", subnet="192.168.1.0/24")
print(f"Found {len(ips)} device(s): {ips}")
# ["192.168.1.42"]
asyncio.run(main())
Single Device
import asyncio
from open_polaris_local_api import PolarisLocalClient
async def main():
client = PolarisLocalClient(
ip="192.168.1.100",
pin="1234",
device_id="living_room",
verbose=True,
)
async with client:
device, zones = await client.async_update()
print(f"โ {device.name} โ {device.cooling_mode_name}, on={device.is_on}")
for zone in zones:
print(f" Zone {zone.zone_id} '{zone.name}': {zone.current_temp}ยฐC โ {zone.set_temp}ยฐC")
# Turn on and set heating
await client.set_heating_mode()
await client.set_zone_temp(zones[0], 21.0)
if __name__ == "__main__":
asyncio.run(main())
Zone Control
async def control_zones():
async with PolarisLocalClient(ip="192.168.1.100", pin="1234") as client:
_, zones = await client.async_update()
# Turn off a zone
await client.turn_zone_off(zones[0])
# Set temperature on all zones
for zone in zones:
await client.set_zone_temp(zone, 20.0)
CU-Level Control
async def control_cu():
async with PolarisLocalClient(ip="192.168.1.100", pin="1234") as client:
# Switch to cooling mode (raffrescamento)
await client.set_cooling_mode(1)
# Switch to dehumidification
await client.set_cooling_mode(2)
# Switch to ventilation
await client.set_cooling_mode(3)
# Back to heating
await client.set_heating_mode()
# Full power off
await client.turn_off()
๐ Documentation
- Auto-Discovery
- Configuration
- Connection Management
- Device Control (CU)
- Zone Control
- Data Models
- Error Handling
- Library Structure
๐ Auto-Discovery
Scans every host in the given subnet over TCP port 1235 and returns the IPs of all responding Polaris devices. No prior knowledge of device IPs required.
from open_polaris_local_api import PolarisAutoDiscovery
ips = await PolarisAutoDiscovery.discover(pin="1234", subnet="192.168.1.0/24")
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
pin โญ |
str |
required | ๐ Device PIN used for the probe |
subnet โญ |
str |
required | ๐ CIDR subnet to scan (e.g. "192.168.1.0/24") |
port |
int |
1235 |
๐ก TCP port to probe |
probe_timeout |
float |
1.5 |
โฑ๏ธ Per-host timeout in seconds |
max_concurrent |
int |
50 |
โก Maximum simultaneous TCP probes |
verbose |
bool |
False |
๐ข Enable debug logging |
Notes
- A 254-host
/24scan with defaults completes in roughly 8 seconds - Devices running older firmware (no
stato_rsupport) are still discovered โ ares=4reply is enough to confirm a Polaris device - Tune
probe_timeoutupward if devices are on a slow or congested network
โ๏ธ Configuration
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
ip โญ |
str |
required | ๐ IP address of the Polaris CU device |
pin โญ |
str |
required | ๐ PIN code for authentication |
device_id |
str |
None |
๐ท๏ธ Friendly identifier (auto-set to polaris_{ip}:{port} if omitted) |
port |
int |
1235 |
๐ก TCP port on the device |
timeout |
float |
5.0 |
โฑ๏ธ Per-command socket timeout (seconds) |
retry_attempts |
int |
2 |
๐ Number of retry attempts on failure |
retry_delay |
float |
1.0 |
โณ Delay between retries (seconds) |
verbose |
bool |
False |
๐ข Enable verbose debug logging |
๐ Connection Management
Connect
Verifies connectivity by performing an initial async_update. Sets _connected=True and populates cached device/zones state.
await client.connect()
Raises: TimeoutError or ConnectionError on failure.
Disconnect
Marks the client as disconnected. TCP is stateless per-command, so no socket needs closing.
await client.disconnect()
# or
await client.close()
Context Manager (recommended)
async with PolarisLocalClient(ip="192.168.1.100", pin="1234") as client:
# automatically connects on enter, disconnects on exit
device, zones = await client.async_update()
๐ฅ๏ธ Device Control (CU)
Full Status Refresh
device, zones = await client.async_update()
Returns a tuple of (PolarisDevice, list[PolarisZone]) and updates the internal cache.
Raw Status
raw: dict = await client.get_status()
Tries stato_r first (compact), falls back to stato if unsupported (res=4).
Power
await client.turn_on() # is_off=False
await client.turn_off() # is_off=True
Operating Mode
await client.set_heating_mode() # cooling=False, mode=0
await client.set_cooling_mode(1) # raffrescamento
await client.set_cooling_mode(2) # dehumidification
await client.set_cooling_mode(3) # ventilation
Full CU Update
await client.update_cu(
is_off=False,
is_cooling=True,
operating_mode=1, # 0=heating, 1=cooling, 2=dehum, 3=vent
)
๐ก๏ธ Zone Control
Convenience Methods
await client.turn_zone_on(zone)
await client.turn_zone_off(zone)
await client.set_zone_temp(zone, 21.5)
Full Zone Update
await client.update_zone(
zone,
is_off=False,
set_temp=21.0,
is_crono=False,
fancoil_set=2,
serranda_set=3,
)
All parameters are optional; omitted values fall back to the zone's current state.
Note:
set_tempmust be known (either from lastasync_updateor provided explicitly). If neither is available,ValueErroris raised.
๐๏ธ Data Models
PolarisDevice
Represents the Polaris Control Unit (CU).
| Property / Field | Type | Description |
|---|---|---|
serial |
str |
Device serial number |
name |
str |
Device name |
fw_ver |
str |
Firmware version |
ip |
str |
IP address |
is_off |
bool |
Whether CU is off |
is_on |
bool (property) |
Inverse of is_off |
is_cooling |
bool |
Cooling active |
operating_mode |
int |
0=heating, 1=cooling, 2=dehum, 3=vent |
cooling_mode_name |
str (property) |
Human-readable mode name |
t_can |
int |
Canal temperature setpoint (ยฐC) |
f_inv |
int |
Winter fan speed |
f_est |
int |
Summer fan speed |
ir_present |
int |
IR module present flag |
num_errors |
int |
CU error bitmask |
has_error |
bool (property) |
num_errors != 0 |
active_errors |
list[str] (property) |
Decoded error strings |
zones |
list[PolarisZone] |
Zones (populated by async_update) |
PolarisZone
Represents a single HVAC zone.
| Property / Field | Type | Description |
|---|---|---|
zone_id |
int |
Zone ID |
name |
str |
Zone name |
current_temp |
float | None |
Current temperature (ยฐC) |
set_temp |
float | None |
Temperature setpoint (ยฐC) |
is_off |
bool |
Whether zone is off |
is_on |
bool (property) |
Inverse of is_off |
is_cooling |
bool |
Cooling active |
fancoil |
int |
Fan coil current speed (-1 = not installed) |
fancoil_set |
int |
Fan coil setpoint (-1 = not installed) |
serranda |
int |
Shutter position (-1 = not installed) |
serranda_set |
int |
Shutter setpoint (-1 = not installed) |
is_crono_mode |
bool |
Chrono (scheduled) mode active |
is_master |
bool |
Zone is master |
humidity |
float | None |
Current humidity (%) |
set_humidity |
float | None |
Humidity setpoint (%) |
num_error |
int |
Zone error bitmask |
has_error |
bool (property) |
num_error != 0 |
active_errors |
list[str] (property) |
Decoded error strings |
Both models accept both local TCP (snake_case, ridotto, full) and cloud API (PascalCase) response formats via the from_local() factory method.
โ ๏ธ Error Handling
PolarisApiError
Raised on communication failures:
from open_polaris_local_api import PolarisApiError
try:
async with PolarisLocalClient(ip="192.168.1.100", pin="1234") as client:
device, zones = await client.async_update()
except TimeoutError:
print("Device unreachable")
except PolarisApiError as e:
print(f"Communication error: {e}")
Device / Zone Errors
device, zones = await client.async_update()
if device.has_error:
print(f"CU errors: {device.active_errors}")
for zone in zones:
if zone.has_error:
print(f"Zone '{zone.name}' errors: {zone.active_errors}")
๐ฆ Library Structure
open-polaris-local-api/
โโโ scripts/
โ โโโ bump_version.sh # Bump version across all files
โ โโโ run_tests.sh # Run the full test suite
โโโ examples/
โ โโโ README.md # Examples documentation
โ โโโ basic_control.py # Connect, read status, control a single device
โ โโโ auto_discovery.py # Discover devices then read their status
โ โโโ multi_device.py # Concurrent control of multiple devices
โ โโโ monitoring.py # Continuous polling with error alerts
โโโ open_polaris_local_api/
โ โโโ __init__.py # exports: PolarisLocalClient, PolarisApiError, PolarisDevice, PolarisZone, PolarisAutoDiscovery
โ โโโ client.py # PolarisLocalClient, PolarisApiError
โ โโโ models.py # PolarisDevice, PolarisZone dataclasses + parsing helpers
โ โโโ polaris_auto_discovery.py # PolarisAutoDiscovery โ subnet scanner
โโโ tests/
โโโ test_models.py
โโโ test_polaris_client.py
โโโ test_polaris_auto_discovery.py
๐ก Examples
Ready-to-run example scripts are available in the examples/ directory.
See examples/README.md for full documentation and usage instructions.
๐ ๏ธ Scripts
Utility scripts live in the scripts/ directory and must be run from the repo root.
scripts/bump_version.sh
Keeps all version references in sync across pyproject.toml and README.md in one command.
./scripts/bump_version.sh patch # 1.0.1 โ 1.0.2
./scripts/bump_version.sh minor # 1.0.1 โ 1.1.0
./scripts/bump_version.sh major # 1.0.1 โ 2.0.0
./scripts/bump_version.sh 1.2.3 # set an explicit version
./scripts/bump_version.sh # interactive menu
scripts/run_tests.sh
Runs the full unit test suite with verbose output.
./scripts/run_tests.sh
๐งช Testing
./scripts/run_tests.sh
๐ Requirements
- Python 3.11+
- asyncio support
- Local network access to the Polaris CU device (TCP port 1235)
- No third-party dependencies โ stdlib only
๐ค Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
๐ License
This project is licensed under the MIT License โ see the LICENSE file for details.
๐ Support
- ๐ Issues: Report a bug
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file open_polaris_local_api-1.2.1.tar.gz.
File metadata
- Download URL: open_polaris_local_api-1.2.1.tar.gz
- Upload date:
- Size: 26.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92a52386bace1f395190a8134f6fa2e86dc230af28659f5475d5c0f3d9c22c33
|
|
| MD5 |
ed834ca63d9f4bb5c9a3effc4ac15f88
|
|
| BLAKE2b-256 |
c2d6601afd713932b282c15906a57e919b48147c2edc248c5f0e3ee1f9fb3f14
|
Provenance
The following attestation bundles were made for open_polaris_local_api-1.2.1.tar.gz:
Publisher:
publish.yml on VoidElle/open-polaris-local-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
open_polaris_local_api-1.2.1.tar.gz -
Subject digest:
92a52386bace1f395190a8134f6fa2e86dc230af28659f5475d5c0f3d9c22c33 - Sigstore transparency entry: 1710542718
- Sigstore integration time:
-
Permalink:
VoidElle/open-polaris-local-api@1b76911d6398e641ec157a431e26a14169ac7c18 -
Branch / Tag:
refs/tags/v1.2.1 - Owner: https://github.com/VoidElle
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1b76911d6398e641ec157a431e26a14169ac7c18 -
Trigger Event:
release
-
Statement type:
File details
Details for the file open_polaris_local_api-1.2.1-py3-none-any.whl.
File metadata
- Download URL: open_polaris_local_api-1.2.1-py3-none-any.whl
- Upload date:
- Size: 17.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4a04c979067abf6644e15265e41234b8649752f7b1ff0e73fb1b61986d616705
|
|
| MD5 |
25db302d6bc493c8ef38a693188bb565
|
|
| BLAKE2b-256 |
a8e0c5d4adca99c95feeaadb18e3d274c851eb71b01700d179ac486c6dff9708
|
Provenance
The following attestation bundles were made for open_polaris_local_api-1.2.1-py3-none-any.whl:
Publisher:
publish.yml on VoidElle/open-polaris-local-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
open_polaris_local_api-1.2.1-py3-none-any.whl -
Subject digest:
4a04c979067abf6644e15265e41234b8649752f7b1ff0e73fb1b61986d616705 - Sigstore transparency entry: 1710542729
- Sigstore integration time:
-
Permalink:
VoidElle/open-polaris-local-api@1b76911d6398e641ec157a431e26a14169ac7c18 -
Branch / Tag:
refs/tags/v1.2.1 - Owner: https://github.com/VoidElle
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1b76911d6398e641ec157a431e26a14169ac7c18 -
Trigger Event:
release
-
Statement type: