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