Harbor Python
Async Python client for connecting locally to Harbor Sleep Cameras.
harbor-python speaks directly to Harbor devices on your local network over MQTT, using the camera certificate material issued for your setup. It provides typed event parsing, device state tracking, command publishing, and helpers for configuring local WHIP streaming targets.
Installation
pip install harbor-python
Python 3.11 or newer is required.
Quick Start
import asyncio
from harbor import Harbor, HarborCamera, HarborCameraConfig, HeartbeatUpdate
async def main() -> None:
config = HarborCameraConfig(
serial="CAMERA_SERIAL",
ip_address="192.168.1.50",
cert_path="/path/to/cert.pem",
key_path="/path/to/key.pem",
)
harbor = Harbor()
camera = HarborCamera(config)
camera.subscribe_updates(
lambda state: print(f"{state.serial} values: {state.values}")
)
camera.subscribe(
HeartbeatUpdate,
lambda event: print(f"temperature: {event.payload.temperature}"),
)
harbor.add_device(camera)
harbor.add_camera_connection(config)
try:
await harbor.start()
await asyncio.Event().wait()
finally:
await harbor.stop()
asyncio.run(main())
On Windows, aiomqtt works best with the selector event loop policy:
import asyncio
import sys
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
Certificate Configuration
HarborCameraConfig accepts certificate material in either form:
HarborCameraConfig(
serial="CAMERA_SERIAL",
ip_address="192.168.1.50",
cert_pem="<certificate PEM contents>",
key_pem="<private key PEM contents>",
)
or:
HarborCameraConfig(
serial="CAMERA_SERIAL",
ip_address="192.168.1.50",
cert_path="/path/to/cert.pem",
key_path="/path/to/key.pem",
cert_dir="/path/to/ca-directory",
)
When both PEM strings and file paths are provided, the in-memory PEM values are used.
Commands
Camera commands can be published directly:
await harbor.publish_camera_command("CAMERA_SERIAL", "some-command", {"value": True})
For request/response commands, use request_camera_command or the settings helper:
settings = await harbor.get_camera_settings("CAMERA_SERIAL")
print(settings.settings)
Camera controls
await harbor.set_camera_on("CAMERA_SERIAL", False) # privacy: pause the stream
await harbor.set_night_mode("CAMERA_SERIAL", "auto") # "auto" | "on" | "off"
await harbor.set_video_flip("CAMERA_SERIAL", True) # rotate the image 180°
await harbor.set_clock_display("CAMERA_SERIAL", False) # clock overlay on the video
await harbor.set_temperature_scale("CAMERA_SERIAL", "C") # "F" | "C"
await harbor.update_camera_settings(
"CAMERA_SERIAL", {"preference_video_ir_brightness": 40}
)
Each control writes one preference and refreshes device state, so
camera.state.values reflects the change once the call returns:
camera.state.values[...] |
Type | Setting |
|---|---|---|
camera_on |
bool |
preference_stream_paused (inverted) |
night_mode_preference |
"auto" | "on" | "off" |
preference_video_night_mode |
night_mode |
bool |
runtime IR state (read-only, see below) |
video_flip |
bool |
preference_video_flip |
clock_display |
bool |
preference_video_has_clock_display |
temperature_scale |
"F" | "C" |
preference_temperature_scale |
set_video_flip and set_clock_display take real booleans — 1/0 and
"true" raise ValueError rather than being sent as a number or string,
which the firmware would reject.
The enum setters validate against the firmware's own option list, exported as
NIGHT_MODE_MODES and TEMPERATURE_SCALES. Matching is exact: "f" raises
ValueError, because the device compares the string verbatim. State values
preserve case for the same reason — what you read back is always something you
can write.
Night mode
Night mode is a three-way preference, not a boolean — "auto", "on" or
"off" (default "auto"). Passing a bool raises ValueError rather than
guessing at a mode. A camera exposes two separate night-mode values:
camera.state.values[...] |
Type | Meaning |
|---|---|---|
night_mode_preference |
"auto" | "on" | "off" |
The setting. This is what set_night_mode writes and what reads back. |
night_mode |
bool |
Whether IR is engaged right now. Read-only and device-driven — under "auto" it flips on its own as light levels change. |
Consumers building a UI entity should bind it to night_mode_preference, since
night_mode moves independently of any command.
Command errors
A rejected command raises HarborCommandError, which carries the parsed
status and the firmware's per-field errors list:
from harbor import HarborCommandError, HarborUnsupportedCommandError
try:
await harbor.set_night_mode("CAMERA_SERIAL", "on")
except HarborUnsupportedCommandError:
... # firmware has no such command; permanent, so stop offering the feature
except HarborCommandError as err:
print(err.status, err.errors) # e.g. "REQUEST_MALFORMED", [{"error_code": "INVALID_VALUE", ...}]
HarborUnsupportedCommandError is a subclass of HarborCommandError, raised
only on a RESOURCE_NOT_FOUND status. That means the firmware has no handler
for the command at all, so retrying can never succeed.
Firmware compatibility
Commands are verified against real hardware, most recently a camera running
os_version 2.8.0 / app_version 2.8.0-rc1+c1b0a32: ping,
get-settings, update-settings, pause-stream, unpause-stream,
set-night-mode-ir-brightness, update-operating-mode, set-scheduled-reboot
and list-viewers all respond OK. See mqtt_home_assistant.md for the full
audit and payload shapes.
WHIP Endpoint
Harbor cameras allow custom WHIP endpoints. This tells the camera where to stream and works with tools that support WHIP, including go2rtc and Frigate.
Setting up WHIP Ingestion (go2rtc)
You can self-host go2rtc in many ways; see the go2rtc installation guide. If you use Home Assistant, the easiest option is the go2rtc add-on.
Once go2rtc is running, add a stream keyed by your camera serial number:
api:
listen: ":1984" # Change this if you use a non-default port.
streams:
"CAMERA_SERIAL":
Setting up WHIP Ingestion (Frigate)
Frigate runs an instance of go2rtc under the hood. Add the following to your Frigate config:
go2rtc:
api:
listen: ":1984"
streams:
"CAMERA_SERIAL":
Setting the Endpoint
- Open your Harbor app
- Go to Live
- Open Camera Settings
- Scroll down and click on Advanced Settings
- Enter the WHIP endpoint, for example:
http://192.168.1.10:1984/api/webrtc?dst=CAMERA_SERIAL
Replace CAMERA_SERIAL with your camera serial number and 192.168.1.10 with the IP address of your go2rtc or Frigate server.
Development
uv sync
uv run pytest
License
Licensed under the Apache License 2.0.
Metadata
Release files for harbor-python 1.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| harbor_python-1.5.0.tar.gz | 27.4 kB | With provenance | Details for harbor_python-1.5.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| harbor_python-1.5.0-py3-none-any.whl | Python 3 | none | any | Details for harbor_python-1.5.0-py3-none-any.whl |
Total release size: 60.2 kB
Release files / harbor_python-1.5.0.tar.gz
| Download URL | harbor_python-1.5.0.tar.gz |
|---|---|
| Size | 27.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e4c68a0e4408fc480ffa1768c808712d08d30140e17b851794434f1ef7547c73
|
|
BLAKE2b-256 checksum How to use checksums |
48f44d31f252fc3bbfe4992c9e6797a5f88c6bc16b9e074205d71111d62f2836
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
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 Jul 26, 2026.
Transparency logRelease files / harbor_python-1.5.0-py3-none-any.whl
| Download URL | harbor_python-1.5.0-py3-none-any.whl |
|---|---|
| Size | 32.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
01524afa8338d1b14d9c5e916d27f8d123e3ff17ab080db68f49cffe53ba56dd
|
|
BLAKE2b-256 checksum How to use checksums |
bba1ee77d814efe812c71ea29352c5630dff5a005ced7e5f23c067b0fe23e257
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
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 Jul 26, 2026.
Transparency logSecurity for harbor-python 1.5.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for harbor_python-1.5.0.tar.gz
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 Jul 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.