Initial scaffold
Astro 6 + Starlight 0.39 documentation site for omni-pca, organised around the Diatáxis framework (Tutorials / How-to / Reference / Explanation), plus a chronological Journey page and Changelog. Theme: muted slate-blue with amber accents. astro-icon + lucide preinstalled. Astro telemetry and Starlight devToolbar both off. Deployment: multi-stage Dockerfile (node:25-alpine builder -> caddy:2-alpine runtime), inner Caddy serves static dist on :80, outer caddy-docker-proxy on the host terminates TLS for hai-omni-pro-ii.warehack.ing.
This commit is contained in:
commit
c5e72c679b
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
dist
|
||||
.astro
|
||||
.git
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
*.tsbuildinfo
|
||||
.DS_Store
|
||||
.vscode
|
||||
README.md
|
||||
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
||||
# Copy to .env and adjust as needed.
|
||||
COMPOSE_PROJECT=hai-omni-docs
|
||||
DOMAIN=hai-omni-pro-ii.warehack.ing
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
Caddyfile
Normal file
22
Caddyfile
Normal file
@ -0,0 +1,22 @@
|
||||
# Inner Caddy: just serves the static dist on :80.
|
||||
# The host's caddy-docker-proxy handles TLS and routing for the public hostname.
|
||||
|
||||
:80 {
|
||||
root * /srv
|
||||
encode zstd gzip
|
||||
file_server
|
||||
|
||||
# SPA-ish fallback: prefer the directory's index.html, then a 404 page.
|
||||
try_files {path} {path}/ /404.html
|
||||
|
||||
header {
|
||||
# Light security defaults; the outer Caddy can override.
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Permissions-Policy "interest-cohort=()"
|
||||
}
|
||||
|
||||
# Long cache for fingerprinted assets emitted by Astro.
|
||||
@hashed path_regexp \.[A-Za-z0-9_-]{8,}\.(js|css|woff2?|svg|png|jpg|jpeg|webp|avif)$
|
||||
header @hashed Cache-Control "public, max-age=31536000, immutable"
|
||||
}
|
||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@ -0,0 +1,37 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# ---- builder ----
|
||||
FROM node:25-alpine AS builder
|
||||
|
||||
ENV ASTRO_TELEMETRY_DISABLED=1 \
|
||||
NODE_ENV=production \
|
||||
CI=true
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install deps in their own layer for better caching.
|
||||
COPY package.json package-lock.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci --include=dev
|
||||
|
||||
# Copy the rest of the source and build.
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# ---- runtime ----
|
||||
FROM caddy:2-alpine AS runtime
|
||||
|
||||
# Run as non-root.
|
||||
RUN addgroup -S docs && adduser -S -G docs docs
|
||||
|
||||
WORKDIR /srv
|
||||
|
||||
COPY --from=builder /app/dist /srv
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
# caddy:2-alpine ships with a sane default user model; keep ports unprivileged.
|
||||
EXPOSE 80
|
||||
|
||||
USER docs
|
||||
|
||||
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||
50
Makefile
Normal file
50
Makefile
Normal file
@ -0,0 +1,50 @@
|
||||
# hai-omni-docs Makefile
|
||||
#
|
||||
# Local dev: make dev (Astro dev server, hot reload, no docker)
|
||||
# Smoke build: make ci (npm run build — verify static output)
|
||||
# Deploy: make build && make up
|
||||
#
|
||||
# Reads .env if present so DOMAIN / COMPOSE_PROJECT propagate to compose.
|
||||
|
||||
SHELL := /bin/bash
|
||||
COMPOSE := docker compose
|
||||
|
||||
.PHONY: help dev install ci build up down restart logs ps clean
|
||||
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
|
||||
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
install: ## Install npm dependencies
|
||||
npm install
|
||||
|
||||
dev: ## Start Astro dev server with hot reload (host)
|
||||
npm run dev
|
||||
|
||||
ci: ## Smoke-test the static build locally
|
||||
npm run build
|
||||
|
||||
build: ## Build the docker image
|
||||
$(COMPOSE) build
|
||||
|
||||
up: ## Start the docs container in the background
|
||||
$(COMPOSE) up -d
|
||||
@echo
|
||||
@echo "==> Tailing recent logs ..."
|
||||
@$(COMPOSE) logs -f --tail=20
|
||||
|
||||
down: ## Stop and remove the docs container
|
||||
$(COMPOSE) down
|
||||
|
||||
restart: ## Recreate the docs container
|
||||
$(COMPOSE) up -d --force-recreate
|
||||
@$(COMPOSE) logs -f --tail=20
|
||||
|
||||
logs: ## Follow logs
|
||||
$(COMPOSE) logs -f --tail=50
|
||||
|
||||
ps: ## Show container status
|
||||
$(COMPOSE) ps
|
||||
|
||||
clean: ## Remove build artifacts
|
||||
rm -rf dist .astro
|
||||
54
README.md
Normal file
54
README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# hai-omni-docs
|
||||
|
||||
Documentation site for [`omni-pca`](https://github.com/rsp2k/omni-pca) — a
|
||||
reverse-engineered Python library and Home Assistant integration for HAI/Leviton
|
||||
Omni Pro II home automation panels. Built with Astro + Starlight, organised
|
||||
around the Diátaxis framework.
|
||||
|
||||
Live: <https://hai-omni-pro-ii.warehack.ing>
|
||||
|
||||
## Local development
|
||||
|
||||
```sh
|
||||
make install # one-time
|
||||
make dev # http://localhost:4321 with hot reload
|
||||
```
|
||||
|
||||
## Production build (smoke test)
|
||||
|
||||
```sh
|
||||
make ci # runs `npm run build`, output in ./dist
|
||||
```
|
||||
|
||||
## Deploy via Caddy
|
||||
|
||||
The container ships its static `dist/` from an inner Caddy on `:80`; the host's
|
||||
`caddy-docker-proxy` terminates TLS and routes the configured `DOMAIN` to it via
|
||||
the external `caddy` network.
|
||||
|
||||
```sh
|
||||
cp .env.example .env # adjust COMPOSE_PROJECT / DOMAIN as needed
|
||||
make build
|
||||
make up # then tails logs
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
content/docs/
|
||||
index.mdx
|
||||
start/ # Overview + Quick start
|
||||
tutorials/ # Diátaxis: learning-oriented
|
||||
how-to/ # Diátaxis: task-oriented
|
||||
reference/ # Diátaxis: information-oriented (protocol, file format, API)
|
||||
explanation/ # Diátaxis: understanding-oriented (quirks, architecture, bugs)
|
||||
journey.md # Chronological retrospective
|
||||
changelog.md
|
||||
styles/theme.css # Slate-blue + amber accent
|
||||
assets/logo.svg
|
||||
```
|
||||
|
||||
## Source project
|
||||
|
||||
- Library + integration: <https://github.com/rsp2k/omni-pca>
|
||||
89
astro.config.mjs
Normal file
89
astro.config.mjs
Normal file
@ -0,0 +1,89 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import icon from 'astro-icon';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://hai-omni-pro-ii.warehack.ing',
|
||||
telemetry: false,
|
||||
devToolbar: { enabled: false },
|
||||
integrations: [
|
||||
icon({
|
||||
include: {
|
||||
lucide: ['*'],
|
||||
},
|
||||
}),
|
||||
starlight({
|
||||
title: 'HAI Omni Pro II — omni-pca docs',
|
||||
description:
|
||||
'Reverse-engineered Python library and Home Assistant integration for HAI/Leviton Omni Pro II home automation panels.',
|
||||
logo: {
|
||||
src: './src/assets/logo.svg',
|
||||
replacesTitle: false,
|
||||
},
|
||||
favicon: '/favicon.svg',
|
||||
customCss: ['./src/styles/theme.css'],
|
||||
social: [
|
||||
{
|
||||
icon: 'github',
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/rsp2k/omni-pca',
|
||||
},
|
||||
],
|
||||
editLink: {
|
||||
// Placeholder — update once the docs repo lives somewhere on GitHub.
|
||||
baseUrl: 'https://github.com/rsp2k/hai-omni-docs/edit/main/',
|
||||
},
|
||||
lastUpdated: true,
|
||||
pagination: true,
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Start here',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ label: 'Overview', slug: '' },
|
||||
{ label: 'Quick start', slug: 'start/quickstart' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Tutorials',
|
||||
collapsed: true,
|
||||
items: [{ autogenerate: { directory: 'tutorials' } }],
|
||||
},
|
||||
{
|
||||
label: 'How-to guides',
|
||||
collapsed: true,
|
||||
items: [{ autogenerate: { directory: 'how-to' } }],
|
||||
},
|
||||
{
|
||||
label: 'Reference',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ label: 'Protocol', slug: 'reference/protocol' },
|
||||
{ label: 'File format', slug: 'reference/file-format' },
|
||||
{ label: 'Library API', slug: 'reference/library-api' },
|
||||
{ label: 'HA entities', slug: 'reference/ha-entities' },
|
||||
{ label: 'HA services', slug: 'reference/ha-services' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Explanation',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ label: 'Two non-public quirks', slug: 'explanation/quirks' },
|
||||
{ label: 'Architecture', slug: 'explanation/architecture' },
|
||||
{ label: 'The PC Access bug', slug: 'explanation/pc-access-bug' },
|
||||
],
|
||||
},
|
||||
{ label: 'Journey', slug: 'journey' },
|
||||
{ label: 'Changelog', slug: 'changelog' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
vite: {
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
},
|
||||
});
|
||||
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@ -0,0 +1,19 @@
|
||||
services:
|
||||
docs:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: ${COMPOSE_PROJECT:-hai-omni-docs}/docs:latest
|
||||
container_name: ${COMPOSE_PROJECT:-hai-omni-docs}-docs
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- caddy
|
||||
expose:
|
||||
- "80"
|
||||
labels:
|
||||
caddy: ${DOMAIN:-hai-omni-pro-ii.warehack.ing}
|
||||
caddy.reverse_proxy: "{{upstreams 80}}"
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
external: true
|
||||
6854
package-lock.json
generated
Normal file
6854
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "hai-omni-docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"description": "Documentation site for omni-pca — a Python library and Home Assistant integration for HAI/Leviton Omni Pro II panels.",
|
||||
"author": "Ryan Malloy <ryan@supported.systems>",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.39.2",
|
||||
"@iconify-json/lucide": "^1.2.106",
|
||||
"astro": "^6.2.2",
|
||||
"astro-icon": "^1.1.5",
|
||||
"sharp": "^0.34.5"
|
||||
}
|
||||
}
|
||||
9
public/favicon.svg
Normal file
9
public/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect x="3" y="3" width="26" height="26" rx="3" fill="#1a1d22" stroke="#4f86c6" stroke-width="1.5"/>
|
||||
<rect x="3" y="3" width="26" height="6" rx="3" fill="#2c3138"/>
|
||||
<circle cx="7" cy="6" r="1" fill="#f0a830"/>
|
||||
<circle cx="10.5" cy="6" r="1" fill="#4f86c6"/>
|
||||
<rect x="7" y="13" width="18" height="2" rx="1" fill="#4f86c6"/>
|
||||
<rect x="7" y="17" width="18" height="2" rx="1" fill="#4f86c6" opacity="0.7"/>
|
||||
<rect x="7" y="21" width="12" height="2" rx="1" fill="#4f86c6" opacity="0.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 563 B |
9
src/assets/logo.svg
Normal file
9
src/assets/logo.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="6" y="6" width="20" height="20" rx="2"/>
|
||||
<line x1="6" y1="11" x2="26" y2="11"/>
|
||||
<circle cx="9.5" cy="8.5" r="0.6" fill="#f0a830" stroke="none"/>
|
||||
<circle cx="12" cy="8.5" r="0.6" fill="currentColor" stroke="none"/>
|
||||
<line x1="10" y1="16" x2="22" y2="16"/>
|
||||
<line x1="10" y1="20" x2="22" y2="20"/>
|
||||
<line x1="10" y1="23" x2="18" y2="23"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 544 B |
7
src/content.config.ts
Normal file
7
src/content.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||
};
|
||||
6
src/content/docs/changelog.md
Normal file
6
src/content/docs/changelog.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Changelog
|
||||
description: Notable changes to omni-pca.
|
||||
---
|
||||
|
||||
TODO: filled by parallel agent.
|
||||
6
src/content/docs/explanation/architecture.md
Normal file
6
src/content/docs/explanation/architecture.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Architecture
|
||||
description: How the library, the integration, and the panel fit together.
|
||||
---
|
||||
|
||||
TODO: filled by parallel agent.
|
||||
6
src/content/docs/explanation/pc-access-bug.md
Normal file
6
src/content/docs/explanation/pc-access-bug.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: The PC Access bug
|
||||
description: A latent defect in HAI's official client and why it matters.
|
||||
---
|
||||
|
||||
TODO: filled by parallel agent.
|
||||
6
src/content/docs/explanation/quirks.md
Normal file
6
src/content/docs/explanation/quirks.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: The two non-public quirks
|
||||
description: Why public Omni-Link clients silently fail on the first encrypted message.
|
||||
---
|
||||
|
||||
TODO: filled by parallel agent.
|
||||
0
src/content/docs/how-to/.gitkeep
Normal file
0
src/content/docs/how-to/.gitkeep
Normal file
61
src/content/docs/index.mdx
Normal file
61
src/content/docs/index.mdx
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
title: HAI Omni Pro II — omni-pca
|
||||
description: Reverse-engineered Python library and Home Assistant integration for HAI/Leviton Omni Pro II home automation panels.
|
||||
template: doc
|
||||
---
|
||||
|
||||
import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components';
|
||||
|
||||
## What it is
|
||||
|
||||
`omni-pca` is an async Python library and a matching Home Assistant custom
|
||||
component for HAI / Leviton **Omni Pro II**, **Omni IIe**, **Omni LTe**, and
|
||||
**Lumina** panels. It speaks Omni-Link II straight to the controller over TCP,
|
||||
opens an encrypted session, and surfaces the panel's typed object model — zones,
|
||||
units, areas, thermostats, buttons, programs, codes, messages — plus the
|
||||
unsolicited push-event stream the panel emits on state changes.
|
||||
|
||||
The protocol layer was built from a clean-room decompilation of HAI's PC Access
|
||||
3.17. Every opcode, byte layout, and crypto step is cited back to the source
|
||||
line in the decompiled C# (`clsOmniLinkConnection.cs`, `clsHAC.cs`, etc.).
|
||||
|
||||
## Two non-public protocol quirks
|
||||
|
||||
The wire protocol — as actually implemented in PC Access 3.17 — has two
|
||||
non-public quirks that public Omni-Link clients miss. Without them the panel
|
||||
will accept your TCP connection, complete the unencrypted handshake, and then
|
||||
silently drop you on the first encrypted message:
|
||||
|
||||
1. **Session key XOR mix.** The AES-128 session key is *not* the panel's
|
||||
`ControllerKey` directly. Bytes `[11..16)` of the ControllerKey are XORed
|
||||
with a 5-byte `SessionID` nonce that the controller sends in
|
||||
`ControllerAckNewSession`. Bytes `[0..11)` are the ControllerKey verbatim.
|
||||
2. **Per-block XOR pre-whitening before AES.** Before each 16-byte block is
|
||||
AES-encrypted, its first two bytes are XORed with the packet's 16-bit
|
||||
sequence number (high byte first). The same mask is applied to *every*
|
||||
block of the packet. Decrypt reverses it.
|
||||
|
||||
Both are unambiguous in the decompiled C# (`clsOmniLinkConnection.cs:1886-1892`
|
||||
and `:396-401`). Neither appears in `jomnilinkII`, `pyomnilink`, or any
|
||||
third-party Omni-Link writeup we found. See [the quirks
|
||||
explainer](/explanation/quirks/) for the full story.
|
||||
|
||||
## Where to start
|
||||
|
||||
<CardGrid>
|
||||
<LinkCard
|
||||
title="Decode your .pca file"
|
||||
href="/start/quickstart/"
|
||||
description="Pull the panel's IP, port, and AES-128 ControllerKey out of an encrypted .pca config export — no panel hardware required."
|
||||
/>
|
||||
<LinkCard
|
||||
title="Protocol reference"
|
||||
href="/reference/protocol/"
|
||||
description="Byte-level Omni-Link II handshake, packet layouts, key derivation, steady-state encryption, sequence numbers, and teardown."
|
||||
/>
|
||||
<LinkCard
|
||||
title="The Journey"
|
||||
href="/journey/"
|
||||
description="Chronological retrospective of the reverse-engineering work — pile of binaries to 351 passing tests in a few days."
|
||||
/>
|
||||
</CardGrid>
|
||||
6
src/content/docs/journey.md
Normal file
6
src/content/docs/journey.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Journey
|
||||
description: Chronological retrospective of the omni-pca reverse-engineering work.
|
||||
---
|
||||
|
||||
TODO: filled by parallel agent — chronological retrospective of the reverse-engineering work.
|
||||
6
src/content/docs/reference/file-format.md
Normal file
6
src/content/docs/reference/file-format.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: File format
|
||||
description: On-disk layout of .pca and PCA01.CFG files.
|
||||
---
|
||||
|
||||
TODO: filled by parallel agent — `.pca` / `PCA01.CFG` layout.
|
||||
6
src/content/docs/reference/ha-entities.md
Normal file
6
src/content/docs/reference/ha-entities.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: HA entities
|
||||
description: Home Assistant entities exposed by the integration.
|
||||
---
|
||||
|
||||
TODO: filled by parallel agent — Home Assistant entity catalog.
|
||||
6
src/content/docs/reference/ha-services.md
Normal file
6
src/content/docs/reference/ha-services.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: HA services
|
||||
description: Home Assistant services exposed by the integration.
|
||||
---
|
||||
|
||||
TODO: filled by parallel agent — Home Assistant service catalog.
|
||||
6
src/content/docs/reference/library-api.md
Normal file
6
src/content/docs/reference/library-api.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Library API
|
||||
description: Public surface of the omni_pca Python library.
|
||||
---
|
||||
|
||||
TODO: filled by parallel agent — `omni_pca` Python API surface.
|
||||
6
src/content/docs/reference/protocol.md
Normal file
6
src/content/docs/reference/protocol.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Protocol
|
||||
description: Omni-Link II handshake, packet/message structure, and opcode reference.
|
||||
---
|
||||
|
||||
TODO: filled by parallel agent — handshake, packet/message structure, opcodes.
|
||||
112
src/content/docs/start/quickstart.md
Normal file
112
src/content/docs/start/quickstart.md
Normal file
@ -0,0 +1,112 @@
|
||||
---
|
||||
title: Quick start
|
||||
description: Three minimal flows — decode your .pca file, drive the panel from Python, and add the integration to Home Assistant.
|
||||
---
|
||||
|
||||
Three flows. Each one assumes you have an Omni Pro II (or compatible) panel
|
||||
and a `.pca` configuration export from PC Access. The first flow needs nothing
|
||||
else — no panel reachable on the network, no HA install. The second adds the
|
||||
panel itself. The third stacks Home Assistant on top.
|
||||
|
||||
## 1. Decode your `.pca` file
|
||||
|
||||
Pull the panel's connection details (IP, port, AES-128 `ControllerKey`) out of
|
||||
the encrypted `.pca` blob. No panel hardware required — the file already
|
||||
contains everything you need.
|
||||
|
||||
```bash
|
||||
pip install omni-pca
|
||||
# or, no install:
|
||||
uvx omni-pca decode-pca '/path/to/Your.pca' --field controller_key
|
||||
```
|
||||
|
||||
Other useful fields:
|
||||
|
||||
```bash
|
||||
uvx omni-pca decode-pca '/path/to/Your.pca' --field host
|
||||
uvx omni-pca decode-pca '/path/to/Your.pca' --field port
|
||||
uvx omni-pca decode-pca '/path/to/Your.pca' --include-pii # full dump, with account info
|
||||
```
|
||||
|
||||
:::caution[The decrypted `.pca` plaintext contains PII]
|
||||
Account name, address, phone number, and (commonly) factory-default user
|
||||
codes all live in the file in plaintext after decryption. The CLI redacts
|
||||
account fields by default; pass `--include-pii` only when you know what you're
|
||||
doing, and never commit decrypted plaintext to a public repo.
|
||||
:::
|
||||
|
||||
The cipher is *not* AES — it's a Borland-Pascal LCG keystream XORed byte-by-byte
|
||||
with the file. The per-installation key lives inside `PCA01.CFG`, encrypted
|
||||
with a hardcoded key. See the [file format reference](/reference/file-format/)
|
||||
for the byte-level layout.
|
||||
|
||||
## 2. Talk to the panel from Python
|
||||
|
||||
With the `ControllerKey` in hand, open an encrypted session and ask the panel
|
||||
who it is.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from omni_pca import OmniClient
|
||||
|
||||
async def main():
|
||||
async with OmniClient(
|
||||
host="192.168.1.9",
|
||||
port=4369,
|
||||
controller_key=bytes.fromhex("6ba7b4e9b4656de3cd7edd4c650cdb09"),
|
||||
) as panel:
|
||||
info = await panel.get_system_information()
|
||||
print(info.model_name, info.firmware_version)
|
||||
|
||||
# Walk every named zone.
|
||||
zones = await panel.list_zone_names()
|
||||
for index, name in zones.items():
|
||||
print(f" zone {index:>3}: {name}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
The `OmniClient` async context manager runs the four-step secure-session
|
||||
handshake, derives the session key (with the [XOR mix](/explanation/quirks/)),
|
||||
sets up the per-direction sequence counter, and starts the background reader
|
||||
task. When you `await client.get_system_information()` it sends a
|
||||
`RequestSystemInformation` (opcode 22), waits for the matching reply,
|
||||
and parses the payload into a `SystemInformation` dataclass.
|
||||
|
||||
The full surface — commands, status queries, typed event stream — is
|
||||
documented in the [Library API reference](/reference/library-api/).
|
||||
|
||||
:::tip[No panel handy?]
|
||||
The library ships a stateful `MockPanel` that emulates the controller side
|
||||
of the protocol over TCP. Spin it up in-process with
|
||||
`from omni_pca.mock_panel import MockPanel` — see the
|
||||
[architecture overview](/explanation/architecture/) for how the test stack
|
||||
uses it.
|
||||
:::
|
||||
|
||||
## 3. Add to Home Assistant
|
||||
|
||||
```bash
|
||||
# From the project root, copy the integration into your HA config.
|
||||
cp -r custom_components/omni_pca/ \
|
||||
~/.homeassistant/config/custom_components/
|
||||
|
||||
# Restart HA.
|
||||
```
|
||||
|
||||
Then in HA: **Settings → Devices & Services → Add Integration**, search for
|
||||
*HAI/Leviton Omni Panel*, and fill in:
|
||||
|
||||
- **Host** — IP or hostname of the panel (e.g., `192.168.1.9`)
|
||||
- **Port** — `4369` (HAI's reserved port; default)
|
||||
- **Controller Key** — the 32 hex characters from step 1
|
||||
|
||||
The integration creates one HA device per panel plus typed entities for every
|
||||
named object on the controller — alarm panels for areas, lights for units,
|
||||
binary_sensors for zones, climate for thermostats, and so on. State
|
||||
propagates over the panel's unsolicited push channel; a 30-second poll
|
||||
backstops anything that didn't push.
|
||||
|
||||
See the [HA entity catalogue](/reference/ha-entities/) for what gets created
|
||||
and the [HA service reference](/reference/ha-services/) for the seven
|
||||
services you can call from automations.
|
||||
0
src/content/docs/tutorials/.gitkeep
Normal file
0
src/content/docs/tutorials/.gitkeep
Normal file
60
src/styles/theme.css
Normal file
60
src/styles/theme.css
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* HAI Omni Pro II docs theme.
|
||||
*
|
||||
* Muted slate-blue base with warm amber accents — evocative of an
|
||||
* indicator LED on a beige 1990s alarm panel. No purple anywhere.
|
||||
*
|
||||
* Starlight color tokens reference:
|
||||
* https://starlight.astro.build/guides/css-and-tailwind/#theming
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Dark theme (default) — slate-blue with amber accent */
|
||||
--sl-color-accent-low: #1c2a3a;
|
||||
--sl-color-accent: #4f86c6;
|
||||
--sl-color-accent-high: #b9d3ee;
|
||||
|
||||
--sl-color-white: #ffffff;
|
||||
--sl-color-gray-1: #e6e8eb;
|
||||
--sl-color-gray-2: #b8bcc2;
|
||||
--sl-color-gray-3: #7e848d;
|
||||
--sl-color-gray-4: #4a4f57;
|
||||
--sl-color-gray-5: #2c3138;
|
||||
--sl-color-gray-6: #1a1d22;
|
||||
--sl-color-black: #101317;
|
||||
|
||||
/* Amber accent for callouts, badges, highlighted prose */
|
||||
--accent-amber: #f0a830;
|
||||
--accent-amber-soft: #f5c875;
|
||||
}
|
||||
|
||||
:root[data-theme='light'] {
|
||||
--sl-color-accent-low: #d8e4f2;
|
||||
--sl-color-accent: #2d5a8c;
|
||||
--sl-color-accent-high: #1c3a5e;
|
||||
|
||||
--sl-color-white: #101317;
|
||||
--sl-color-gray-1: #2c3138;
|
||||
--sl-color-gray-2: #4a4f57;
|
||||
--sl-color-gray-3: #7e848d;
|
||||
--sl-color-gray-4: #b8bcc2;
|
||||
--sl-color-gray-5: #e6e8eb;
|
||||
--sl-color-gray-6: #f4f5f7;
|
||||
--sl-color-black: #ffffff;
|
||||
|
||||
--accent-amber: #b87010;
|
||||
--accent-amber-soft: #d68a2a;
|
||||
}
|
||||
|
||||
/* Subtle amber tint on inline code so reverse-engineered opcodes / hex
|
||||
* stand out without being shouty. */
|
||||
:not(pre) > code {
|
||||
color: var(--accent-amber);
|
||||
background-color: color-mix(in srgb, var(--accent-amber) 10%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--accent-amber) 25%, transparent);
|
||||
}
|
||||
|
||||
/* Slightly tighter heading rhythm — reads better for technical reference. */
|
||||
.sl-markdown-content h2 {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
5
tsconfig.json
Normal file
5
tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strictest",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user