Skip to main content

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

Python Version License Version

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_r polling with transparent fallback to full stato

๐Ÿ”„ Reliability

  • Auto-retry with configurable attempts and delay
  • Graceful fallback: stato_r โ†’ stato on older firmware
  • Robust error decoding via bitmask for both CU and zone errors

๐ŸŽฏ Developer Friendly

  • Type-safe dataclasses โ€” PolarisDevice and PolarisZone
  • 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

  1. Clone this repository into your project
  2. Ensure the open_polaris_local_api/ folder is on your Python path
  3. 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

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 /24 scan with defaults completes in roughly 8 seconds
  • Devices running older firmware (no stato_r support) are still discovered โ€” a res=4 reply is enough to confirm a Polaris device
  • Tune probe_timeout upward 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_temp must be known (either from last async_update or provided explicitly). If neither is available, ValueError is 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.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

๐Ÿ“„ License

This project is licensed under the MIT License โ€” see the LICENSE file for details.


๐Ÿ†˜ Support

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

open_polaris_local_api-1.2.1.tar.gz (26.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

open_polaris_local_api-1.2.1-py3-none-any.whl (17.6 kB view details)

Uploaded Python 3

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

Hashes for open_polaris_local_api-1.2.1.tar.gz
Algorithm Hash digest
SHA256 92a52386bace1f395190a8134f6fa2e86dc230af28659f5475d5c0f3d9c22c33
MD5 ed834ca63d9f4bb5c9a3effc4ac15f88
BLAKE2b-256 c2d6601afd713932b282c15906a57e919b48147c2edc248c5f0e3ee1f9fb3f14

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file open_polaris_local_api-1.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for open_polaris_local_api-1.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4a04c979067abf6644e15265e41234b8649752f7b1ff0e73fb1b61986d616705
MD5 25db302d6bc493c8ef38a693188bb565
BLAKE2b-256 a8e0c5d4adca99c95feeaadb18e3d274c851eb71b01700d179ac486c6dff9708

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page