Skip to main content

PowerView Connect

An async Python client for Hunter Douglas / Luxaflex PowerView Gen 3 gateways, over their local HTTP API and shade event stream.

It is built on what a real Gen 3 gateway was measured to do (see the protocol notes):

  • A move sends only the rails it names. The gateway leaves a rail that is not sent where it is, so a command to the top rail of a top-down/bottom-up shade can never undo a command to the bottom rail sent a moment before.
  • The event stream says when a shade starts and stops moving, whatever moved it: this client, the PowerView app, a scene, a voice assistant. Nothing needs polling while nothing moves.
  • A moving shade reports its live position, so a shade can be followed by polling it, and only it, while it moves. ShadeTracker decides when a move has ended.

aiohttp is its only dependency. It is unofficial: PowerView is a trademark of Hunter Douglas, which has nothing to do with this library.

Installation

pip install powerview-connect

Usage

import asyncio
import aiohttp
from powerview_connect import EventKind, PowerViewClient, Rail

async def main():
    async with aiohttp.ClientSession() as session:
        client = PowerViewClient(session, "<gateway-ip>")

        home = await client.get_home()                 # gateways, shades, scenes
        for shade in home.shades.values():
            print(shade.id, shade.name, shade.capability, dict(shade.positions))

        await client.set_positions([154], secondary=0.5)   # the top rail only
        await client.set_positions([154], primary=0.2)     # the bottom rail only

        async with client.shade_events(idle_timeout=360) as events:
            async for event in events:
                if event.kind == EventKind.MOTION_STOPPED:
                    print(event.shade_id, "stopped at", dict(event.current))

asyncio.run(main())

Use the address of the primary gateway: it lists the whole home, auxiliary gateways and their shades included. home.gateway_at(host) says which gateway an address is.

What a gateway reports

Each Gateway of the home carries its name, ip, model, firmware, radio_firmware, whether it is_primary, and shade_ids: the shades it serves. Every gateway, auxiliary ones too, answers get_gateway() on a client of its own address with a GatewayStatus: its brand, hardware_version, firmware, the network interface in use (wifi says whether it is on Wi-Fi) and internet_connected. A gateway that does not answer raises CannotConnectError.

for gateway in home.gateways:
    status = await PowerViewClient(session, gateway.ip, read_only=True).get_gateway()
    print(gateway.name, status.firmware, status.internet_connected, gateway.shade_ids)

What a shade reports

A Shade carries its name, room, capabilities (named by capability), type, power_type (1 is hard-wired), firmware, positions, and signal_strength: how well the gateway hears it, in dBm, closer to 0 being stronger. battery names the battery status (BatteryStatus); a hard-wired shade reports one too, so it means something only where hard_wired is false.

Positions

Every rail runs from 0.0, fully closed, to 1.0, fully open:

Rail Is 0.0 1.0
Rail.PRIMARY the bottom rail of a top-down/bottom-up shade, the only rail of most others down up
Rail.SECONDARY the top rail of a top-down/bottom-up shade up down
Rail.TILT the vanes of a shade that tilts closed open

A top-down/bottom-up shade's own firmware stops a rail where it meets the other one, so a move that would cross them is safe to send.

Commands

Method Sends
set_positions(ids, primary=, secondary=, tilt=, velocity=) only the rails given, to every shade in ids, in one request
stop(ids) stop where they are
activate_scene(scene_id) a scene stored on the gateway
jog(shade_id) a short move back and forth, to show which shade it is

A command the gateway reports an error for raises CommandError. A client made with read_only=True refuses every command with ReadOnlyError before anything is sent.

Events

shade_events(idle_timeout) opens the event stream; iterating it yields a ShadeEvent for each event, with kind, shade_id, current and, for a motion start, target and eta:

EventKind When
MOTION_STARTED a move starts, from any source; the motor follows some seconds later
MOTION_STOPPED a move ends, with the final positions
BATTERY_ALERT right before every move, by hard-wired shades too
SHADE_ONLINE, SHADE_OFFLINE a shade is reachable again, or no longer

The stream ends when the gateway closes it, and raises CannotConnectError when it breaks or sends nothing for idle_timeout seconds; the gateway sends nothing while nothing moves, so reopen it. eta is the gateway's estimate of the move's time, which real moves took about twice as long as.

Following a move

ShadeTracker follows one shade. It does no I/O and reads no clock: give it the events, the positions polled while the shade moves, and the commands sent, each with the time.

tracker = ShadeTracker(shade.positions)
tracker.motion_started(event, now=time.monotonic())        # or tracker.command(now, {Rail.PRIMARY: 0.2})
while tracker.poll((await client.get_shade(shade.id)).positions, now=time.monotonic()):
    print(tracker.current[Rail.PRIMARY], tracker.direction(Rail.PRIMARY))
    await asyncio.sleep(1)

A move ends with whichever comes first: motion-stopped, the target reached and still for 1.5 s, no change for 6 s after the shade moved, no move at all 25 s after the command, or a timeout.

Testing your code

powerview_connect.testing.SimulatedGateway is a gateway on 127.0.0.1 that answers as a real one was measured to, for your own tests:

from powerview_connect import Capabilities, PowerViewClient, Rail
from powerview_connect.testing import SimulatedGateway

async with SimulatedGateway() as gateway:
    gateway.add_room(1, "Living Room")
    gateway.add_shade(10, "Window", room_id=1, capabilities=Capabilities.TOP_DOWN_BOTTOM_UP)
    client = PowerViewClient(session, gateway.host)
    await client.set_positions([10], primary=0.5)    # motion-started on every stream
    gateway.move(10, {Rail.PRIMARY: 0.2})            # part of the way, as a poll sees it
    gateway.finish(10)                               # at the target; motion-stopped
    print(gateway.requests[-1].body)                 # {'positions': {'primary': 0.5}}

SimulatedGateway(primary=False) answers as an auxiliary gateway; list one in a home with add_gateway(serial, name, ip=auxiliary.host), and give a shade to it with add_shade(..., gateway=serial). interface and internet_state set what /gateway reports.

It also drops streams (drop_streams), goes offline and comes back (close, start), reports a shade offline (set_online) and reports errors for commands (command_errors).

Metadata

Release files for powerview-connect 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for powerview-connect 1.1.0
File Size Uploaded
powerview_connect-1.1.0.tar.gz 52.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for powerview-connect 1.1.0
File Interpreter ABI Platform
powerview_connect-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 73.3 kB

Release files / powerview_connect-1.1.0.tar.gz

Download URL powerview_connect-1.1.0.tar.gz
Size 52.4 kB
Tags Source
SHA-256 checksum
How to use checksums
47214127d4557e1a50070896852e892c883fdab1fde11869208b85a41552053e
BLAKE2b-256 checksum
How to use checksums
02ac2745766a751332af77648334ebfdcac3217f8192490bf2350cd70e33d136
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / powerview_connect-1.1.0-py3-none-any.whl

Download URL powerview_connect-1.1.0-py3-none-any.whl
Size 20.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7c42876ec57dbdf491479505ff1c7d019847d8fe51566dbbc2c2f95d5f56d43a
BLAKE2b-256 checksum
How to use checksums
d1f0c139eac91f84e52155a31068a69cdbe613da65e1ae185e1d32089294506e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page