OpenHAP is a Perl-based HomeKit Accessory Protocol (HAP) server for OpenBSD. It bridges MQTT-connected Tasmota devices to Apple HomeKit using SRP-6a, Ed25519, X25519, ChaCha20-Poly1305, HKDF-SHA-512, and TLV8 encoding over encrypted HTTP/1.1 sessions.
Core Principles:
- Correctness over features: Handle all edge cases, return early on errors, avoid deep nesting
- Security by default: Use
/dev/urandom, design for pledge(2)/unveil(2), fail closed, drop privileges early, never trust external input - Documentation: Man pages (mdoc(7)) are authoritative, POD in separate
.podfiles, document all public methods - Configuration: Simple syntax, sensible defaults, lowercase_with_underscores keys
Module Organization: Core protocol (HAP.pm, HTTP.pm, TLV.pm), Security (Crypto.pm, SRP.pm, Pairing.pm, Session.pm), Data model (Accessory.pm, Service.pm, Characteristic.pm, Bridge.pm), Configuration (Config.pm, Storage.pm), Integration (MQTT.pm), Devices (Tasmota/*.pm).
Always follow the Conventional Commits standard for all commit messages.
Format: <type>[optional scope]: <description>
Types: feat, fix, docs, style, refactor, perf, test, build,
ci, chore
Scopes (optional): Use module/component names like hap, mqtt, crypto,
bridge, config, daemon, tasmota, vm, etc.
Examples:
feat(mqtt): add support for retained messages
fix(crypto): correct ChaCha20-Poly1305 nonce handling
docs(readme): update installation instructions
refactor(bridge): simplify accessory registration
test(srp): add edge case tests for SRP-6a
chore(deps): update CryptX to 0.080
Indicate breaking changes with ! or in the footer:
feat(hap)!: change default port to 51828
BREAKING CHANGE: The default port has changed from 51827 to 51828.
Update your configuration files accordingly.
Before Committing:
Always run make check before committing. This runs code tidying checks
(make tidy), linting (make lint), and tests (make test). All checks must
pass before code can be committed. Use make tidy-fix to automatically fix Perl
formatting issues if needed.
Documentation must always be kept up-to-date with any code changes.
The project has three primary documentation sources, each serving a distinct purpose with no overlap of information:
-
Man pages (
man/) — The main authoritative technical documentationopenhapd.8— Daemon operation, command-line options, signalshapctl.8— Control utility usage and commandsopenhapd.conf.5— Configuration file format and all options- Written in mdoc(7) format
-
README.md — High-level project introduction and quick start
- What the project is and why it exists
- Key features and capabilities
- Quick start guide with minimal setup
- Links to detailed documentation (man pages, INSTALL.md)
-
INSTALL.md — Complete installation and setup procedures
- Prerequisites and dependencies
- Step-by-step installation instructions
- Platform-specific notes
- Post-installation verification
When making changes:
- Update man pages when changing functionality, options, or configuration
- Update README.md when changing project scope, features, or quick start steps
- Update INSTALL.md when changing installation requirements or procedures
- Ensure no information is duplicated across these documents
- Use POD files (
.pod) to document internal APIs and module interfaces
For quick testing of changes in an actual OpenBSD environment:
- Provision the code to the VM:
make vm-provision - Run commands in the VM:
bin/openhvf ssh '<command>'
Example workflow:
make vm-provision
bin/openhvf ssh 'rcctl restart openhapd'
bin/openhvf ssh 'tail -f /var/log/daemon'Run the complete integration test suite (provisions VM, runs all tests):
make integrationThis builds the package, provisions it to the VM, and runs all integration tests.
Always use v5.36 (enables strict, warnings, say, signatures). Follow
OpenBSD style(9): 8-character tabs, continuation lines indent 4 spaces.
Signatures and Methods: Use object-oriented style with signatures. Packages
under OpenHAP::. Name object $self, prefix internal methods with _:
sub new($class, $state) { bless {state => $state}, $class; }
sub method($self, $p1, $p2) { ... }
sub _internal($self, $param) { ... }Default values: sub foo($self, $default = undef) { ... } Variadic:
sub wrapper($self, @p) { do_something(@p); } Omit parentheses for zero-arg
calls: $object->width Explicit return except for no-return or constant
methods: sub isFile($) { 1; } Do not name unused parameters, document with
comments: sub foo($, $) { }
Formatting: Function brace on own line, control structure brace on same line:
sub method($self, $param1, $param2)
{
if ($condition) {
...
}
return $result;
}Anonymous subs: indent one tab, as arguments start on new line:
my $s = sub($self) { ... };
f($a, $b,
sub($self) { ... });Data Structures: Use autovivification: push @{$self->{list}}, $value Check
existence: @{$self->{list}} > 0 No quotes on simple hash keys, omit arrows:
$self->{a}{b}
Syntax: Omit parentheses for built-ins and prototyped functions. Use modern
operators: $value //= $something
Packages: Multiple related classes per file allowed. No multiple
inheritance. Inheritance via our @ISA:
package OpenHAP::Derived;
our @ISA = qw(OpenHAP::Base);Delegation passing @_ unchanged (only case for code refs without signatures):
sub visit_notary { &Lender::visit_notary; } # no parensConstants via constant pragma:
use constant { DEFAULT_PORT => 51827 };Full Example:
# ex:ts=8 sw=4:
# $OpenBSD$
#
# Copyright (c) YEAR Author <email@example.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
use v5.36;
package OpenHAP::Example;
use constant { DEFAULT_VALUE => 42 };
sub new($class, $state) { bless {state => $state}, $class; }
sub value($self) { return $self->{value} // DEFAULT_VALUE; }
sub set_value($self, $v) { $self->{value} = $v; return $self; }
sub process($self, $data, $options = undef)
{
return if !defined $data;
my $result = $self->_transform($data);
$result = $self->_apply_options($result, $options) if defined $options;
return $result;
}
sub _transform($self, $data) { ...; return $transformed; }
1;Dependencies: Perl modules in cpanfile, OS packages in deps/[OS].txt.
Unified dependency format in deps/*.txt:
<environment> <type> <package-name>
environment:runtime,test,developtype:pkg(OS package) orcpan(CPAN module)package-name: Name in the respective package system
Examples:
runtime pkg mosquitto
runtime pkg p5-JSON-XS
runtime cpan Net::MQTT::Simple
test pkg p5-Perl-Critic
develop pkg p5-Net-SSH2
OS dependencies (deps/):
- Format:
environment type package - Files:
OpenBSD.txt,Darwin.txt,Linux.txt(matchesunameoutput) - Environments:
runtime,test,develop - Types:
pkg(OS package preferred),cpan(CPAN fallback)
Makefile targets:
make deps— Install runtime OS packages + Perl modulesmake deps-test— Install runtime + test dependenciesmake deps-develop— Install all dependencies (runtime + test + develop)
Cross-platform dependency management via scripts/deps.sh (handles both OS
packages and CPAN modules) and scripts/ftp.sh (ftp/wget/curl).
The cpanfile is maintained for development convenience and carton
compatibility, but production deployments should use deps/*.txt with
make deps.
Use require for conditional module loading. Minimize dependencies.
Error Handling: Return undef for recoverable errors, die for programming errors. Use try/catch for cleanup:
use OpenHAP::Error;
try { ... } catch { ... };Failable methods:
sub load_data($self, $file)
{
open my $fh, '<', $file or do { warn "Cannot open $file: $!"; return; };
# Process...
return $data;
}Signal Handling: Use object-based handlers that auto-restore:
my $handler = OpenHAP::SigHandler->new;
$handler->set('INT', 'TERM', sub($sig) { ... });Register exit cleanup: OpenHAP::Handler->atend(sub($) { ... });
Caching: Lazy initialization:
OpenHAP::Auto::cache(expensive_value, sub($self) { ... }); Call as
$self->expensive_value - computes once, caches.
Structure tests with use v5.36, use Test::More, skip if dependencies
unavailable. Group related tests in blocks. Run via make test,
prove -l -v t/openhap/, or prove -l t/openhap/foo.t. Quality: make lint
(Perl::Critic severity 4).
#!/usr/bin/env perl
use v5.36;
use Test::More;
use FindBin qw($RealBin);
use lib "$RealBin/../lib";
BEGIN { eval { require SomeModule }; plan skip_all => 'SomeModule not available' if $@; }
use_ok('OpenHAP::ModuleName');
{ my $obj = OpenHAP::ModuleName->new($state); ok(defined $obj, 'Created'); }
done_testing();Integration tests verify end-to-end functionality without workarounds. Test the real system as users and external systems interact with it.
Key Principles:
-
Test Real Interfaces — Make actual HTTP requests to HAP endpoints, connect to sockets, execute commands (
hapctl,rcctl), query databases. Don't parse logs or inspect internals. -
Verify Complete Data Flow — Test request/response cycles:
MQTT publish → device state change → HAP query verification -
Use Production Tools — Test with
hapctl,rcctl, actual HTTP clients. Validate responses match protocol specs (HAP, MQTT, HTTP). -
Be Resilient — Handle timing variations, skip when dependencies unavailable, check multiple conditions:
$rcctl_ok || $socket_ok
Example:
# Good: Test actual functionality
my $running = system('rcctl check openhapd >/dev/null 2>&1') == 0;
ok($running, 'daemon is running');
my $socket = IO::Socket::INET->new(PeerAddr => '127.0.0.1', PeerPort => 51827);
ok(defined $socket, 'server accepts connections');
my $status = `hapctl status 2>&1`;
ok($status =~ /openhapd/, 'hapctl reports state');
# Bad: Parse logs instead of testing real functionality
my $log = `grep "Started" /var/log/daemon`;
ok($log, 'daemon started'); # Wrong!Avoid: Log parsing, bespoke workarounds, testing implementation details, generic feature tests, assuming success from absence of errors.
Test Organization: Unit tests (t/openhap/*.t) for modules, integration
tests (t/openhap/integration/*.t) for complete system in VM, ad-hoc testing
(make vm-provision) for quick manual checks.
Reading /dev/urandom:
sub generate_random_bytes($length)
{
open my $fh, '<', '/dev/urandom' or die "Cannot open /dev/urandom: $!";
read $fh, (my $bytes), $length;
close $fh;
return $bytes;
}File locking:
use Fcntl qw(:flock);
open my $fh, '+<', $file or return;
flock($fh, LOCK_EX) or return;
# ... work ...
close $fh;Method chaining: $object->configure->initialize->start
Forking:
my $pid = fork;
if ($pid == 0) { $DB::inhibit_exit = 0; exec @command or exit 1; }- ❌
evalfor flow control - ❌ Ignore return values of system calls
- ❌ Regex when simple string operations suffice
- ❌ Features without tests
- ❌ Indirect object notation (
new ClassvsClass->new) - ❌ Code that fails
make lint - ❌ Dependencies without justification
- ❌ Threads (use IO::Select for multiplexing)
- ❌ Code refs without parentheses (except delegation)
- ❌ Old-style function prototypes unless creating syntax
- ❌
wantarray()to change semantics (optimization only)