App-OpenHAP

 view release on metacpan or  search on metacpan

bin/openhapd  view on Meta::CPAN

use Protocol::HAP::Crypto;
use Fugu::Daemon;
use Fugu::Log;
use Fugu::Mdnsd;
use Fugu::MQTT;
use Fugu::Privdrop;
use Fugu::Sandbox;
use Fugu::Signal;
use App::OpenHAP::Devices;
use App::OpenHAP::Host;
use Protocol::HAP::SetupCode qw(validate_setup_code);

# Default configuration
my $config_file  = '/etc/openhapd.conf';
my $foreground   = 0;
my $check_config = 0;
my $verbose      = 0;

GetOptions(
	'c|config=s'   => \$config_file,
	'f|foreground' => \$foreground,

bin/openhapd  view on Meta::CPAN

# install; a file that exists but does not parse is not, and the
# daemon must not run on defaults that the operator did not choose.
my $config = Fugu::Config->new( file => $config_file );
if ( -f $config_file && !$config->load ) {
	print STDERR $config->error . "\n";
	exit 1;
}

# Refuse an unusable setup code here, before the daemonize step. The
# engine requires a valid code, and it must not fail with a raw die
# after the process left the terminal. validate_setup_code also
# rejects the trivial codes that the HAP specification disallows.
my $hap_pin = $config->get( 'hap_pin', '1995-1018' );
unless ( validate_setup_code($hap_pin) ) {
	print STDERR "openhapd: hap_pin \"$hap_pin\" is not a valid"
	    . " setup code: use 8 digits, not a trivial pattern, for"
	    . " example 1995-1018\n";
	exit 1;
}

# Refuse an unknown log level or facility the same way, and name the
# value. openhapd.conf(5) lists exactly these spellings; a daemon
# that silently mapped an unknown one to a default would serve a
# level the operator did not choose.

bin/openhapd  view on Meta::CPAN

my $log = Fugu::Log->new(
	mode     => $log_mode,
	ident    => 'openhapd',
	level    => $verbose ? 'debug' : $log_level,
	facility => $log_facility,
);
Fugu::Log->set_default($log);

$log->info('OpenHAP daemon initializing');

# Get the HAP settings. hap_pin was read and validated above,
# before the -n exit and the daemonize.
my $hap_name = $config->get( 'hap_name', 'OpenHAP Bridge' );
my $hap_port = $config->get( 'hap_port', 51827 );
my $db_path  = $config->get( 'db_path',  '/var/db/openhapd' );

# Where hapctl asks the running daemon what it is doing. The value
# "off" turns the socket off, for an operator who wants no local
# control channel at all.
my $control_path = $config->get( 'control', '/var/run/openhapd/control.sock' );
$control_path = undef if lc $control_path eq 'off';

lib/App/OpenHAP/Devices.pm  view on Meta::CPAN


# App::OpenHAP::Devices - turn the device blocks of the configuration
# into accessories on the bridge.
#
# One table describes every device type. Each entry says what the type
# is called in a log line, which class builds it, and what that class
# needs beyond the fields every device has. Adding a type is one
# entry, and no second place to keep true.
#
# The class of the entry does the work of building. The loader only
# decides which one, validates the fields the configuration must
# carry, and subscribes the result to MQTT.
#
# A device class loads when the configuration asks for it, not at
# compile time. The classes drag JSON::XS and the whole accessory
# model behind them, and a tool that only reads the device blocks
# needs none of it. The daemon builds its devices before it pledges,
# thus the late load costs it nothing.
# The lightbulb family differs only in its capability mask, so those
# entries carry a caps field instead of one closure each. The mask
# values are the CAP_* constants of the class, spelled as numbers

lib/App/OpenHAP/Devices.pm  view on Meta::CPAN

	# the build, and the log name.
	my $entry = $DEVICE{"$dev_type/$dev_subtype"};
	unless ($entry) {
		Fugu::Log->default->debug(
			'Skipping unsupported device type: %s/%s',
			$dev_type, $dev_subtype );
		return;
	}

	# Validate the required fields
	return unless $self->_validate_device($device);

	# Create the device and catch errors
	my $accessory;
	eval {
		$accessory =
		    $self->_instantiate_device( $device, $mqtt, $entry );
	};
	if ($@) {
		Fugu::Log->default->error( 'Failed to create %s "%s": %s',
			$entry->{name}, $device->{name}, $@ );

lib/App/OpenHAP/Devices.pm  view on Meta::CPAN

			%{ $block->{settings} },
			type    => $type,
			subtype => $subtype,
			id      => $id,
		    };
	}

	return @devices;
}

# $self->_validate_device($device):
#	Validate the required device fields. The method returns true
#	if the device is valid.
sub _validate_device ( $self, $device )
{
	unless ( defined $device->{name} && $device->{name} ne '' ) {
		Fugu::Log->default->error(
			'Device missing required field: name');
		return;
	}

	unless ( defined $device->{topic} && $device->{topic} ne '' ) {
		Fugu::Log->default->error(
			'Device "%s" missing required field: topic',

lib/Protocol/HAP/SetupCode.pm  view on Meta::CPAN

# 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 Protocol::HAP::SetupCode;
our $VERSION = '0.1.0';
use Exporter qw(import);
our @EXPORT_OK = qw(normalize_setup_code validate_setup_code);

# Protocol::HAP::SetupCode - the rules of the 8-digit setup code.
#
# The specification says "setup code" [HAP-Pairing §2]. The word PIN is
# the one it replaced, so no name here uses it.

# Invalid setup codes per HAP specification
# These are sequential or trivial patterns. Do not use them.
use constant INVALID_SETUP_CODES => qw(
    00000000 11111111 22222222 33333333 44444444

lib/Protocol/HAP/SetupCode.pm  view on Meta::CPAN


	# Remove the dashes and spaces
	$code =~ s/[-\s]//g;

	# Make sure the setup code is exactly 8 digits
	return unless $code =~ /^\d{8}$/;

	return $code;
}

# validate_setup_code($code):
#	Validate that the setup code meets the HAP requirements
#	Returns: 1 if the setup code is valid, undef if it is invalid
sub validate_setup_code ($code)
{
	# Normalize the setup code first
	my $normalized = normalize_setup_code($code);
	return unless defined $normalized;

	# Check the setup code against the list of invalid codes
	my %invalid = map { $_ => 1 } INVALID_SETUP_CODES;
	return if exists $invalid{$normalized};

	return 1;

lib/Protocol/HAP/SetupCode.pod  view on Meta::CPAN

=head1 NAME

Protocol::HAP::SetupCode - the rules of the HomeKit setup code

=head1 SYNOPSIS

    use Protocol::HAP::SetupCode qw(normalize_setup_code validate_setup_code);

    # Normalize a setup code (remove dashes and spaces)
    my $normalized = normalize_setup_code('9876-5432');  # Returns '98765432'
    my $normalized = normalize_setup_code('9876 5432');  # Returns '98765432'
    my $normalized = normalize_setup_code('98765432');   # Returns '98765432'

    # Validate a setup code
    if (validate_setup_code('9876-5432')) {
        print "Valid setup code\n";
    }

    if (!validate_setup_code('1234-5678')) {
        print "Invalid setup code (sequential pattern)\n";
    }

=head1 DESCRIPTION

This module handles HAP setup codes: the 8-digit numeric codes that
pairing uses. The specification calls them setup codes
[HAP-Pairing §2], so no name here uses the word it replaced.

The HAP specification says that dashes and spaces are format characters

lib/Protocol/HAP/SetupCode.pod  view on Meta::CPAN


This function removes the dashes and the spaces from a setup code. It
returns the 8-digit numeric string.

    my $normalized = normalize_setup_code('9876-5432');
    # Returns: '98765432'

The function returns C<undef> if the input format is not valid. A valid
input has exactly 8 digits after normalization.

=head2 validate_setup_code($code)

This function does a check of a setup code against these HAP
requirements:

=over 4

=item * The code must have exactly 8 digits after the removal of the
dashes and the spaces.

=item * The code must not be a trivial or sequential pattern.

=back

The function returns C<1> if the setup code is valid. It returns
C<undef> if the setup code is not valid.

    if (validate_setup_code('9876-5432')) {
        # the setup code is valid
    }

=head1 INVALID SETUP CODES

The HAP specification rejects these setup codes:

    00000000  11111111  22222222  33333333  44444444
    55555555  66666666  77777777  88888888  99999999
    12345678  87654321

t/openhap/integration/configuration.t  view on Meta::CPAN

use FindBin qw($RealBin);
use lib "$RealBin/../../../lib";

use App::OpenHAP::Test::Integration;

my $env = App::OpenHAP::Test::Integration->new;
$env->setup;

my $config_file = $env->{config_file};

# Test 1: openhapd -n validates configuration
my $daemon_check = system("openhapd -n -c $config_file >/dev/null 2>&1");
is($daemon_check, 0, 'openhapd -n validates configuration');

# Test 2: Configuration contains required HAP settings
my $hap_name = $env->get_config_value('hap_name');
my $hap_port = $env->get_config_value('hap_port');
ok(defined $hap_name, 'configuration has hap_name');
ok(defined $hap_port, 'configuration has hap_port');

# Test 3: HAP port is valid
ok($hap_port =~ /^\d+$/ && $hap_port >= 1024 && $hap_port <= 65535,
   'hap_port is valid');

t/openhap/integration/hapctl.t  view on Meta::CPAN

$env->setup;

my $config_file = $env->{config_file};
my $hapctl = '/usr/local/bin/hapctl';

# Test 1: hapctl without arguments shows usage
my $no_args_output = `$hapctl 2>&1`;
my $shows_usage = $no_args_output =~ /(Usage|help|command)/i;
ok($shows_usage, 'shows usage without arguments');

# Test 2: hapctl check validates configuration
my $check_result = system("$hapctl -c $config_file check >/dev/null 2>&1");
is($check_result, 0, 'check command validates configuration');

# Test 3: hapctl check provides meaningful output
my $check_output = `$hapctl -c $config_file check 2>&1`;
my $check_meaningful = $check_output =~ /(Configuration.*valid|Configured devices:\s*\d+)/i;
ok($check_meaningful, 'check output is meaningful');

# Test 4: hapctl status reports daemon state
my $status_output = `$hapctl -c $config_file status 2>&1`;
my $status_works = $? == 0 && length($status_output) > 0;
ok($status_works, 'status command works');

t/protocol/setupcode.t  view on Meta::CPAN

{
	my $code = Protocol::HAP::SetupCode::normalize_setup_code(undef);
	is($code, undef, 'Handle undefined input');
}

{
	my $code = Protocol::HAP::SetupCode::normalize_setup_code('1234-');
	is($code, undef, 'Reject an incomplete setup code');
}

# Test validate_setup_code: valid setup codes
{
	ok(Protocol::HAP::SetupCode::validate_setup_code('9876-5432'), 'A valid setup code with a dash');
}

{
	ok(Protocol::HAP::SetupCode::validate_setup_code('98765432'), 'A valid setup code without a dash');
}

{
	ok(Protocol::HAP::SetupCode::validate_setup_code('1111-2222'), 'A valid setup code with repeated digits');
}

{
	ok(Protocol::HAP::SetupCode::validate_setup_code('0000-0001'), 'A valid setup code that starts with zeros');
}

# Test validate_setup_code: HAP disallows trivial setup codes (Apple HAP R2
# 5.3). The trivial-code blacklist is not in spec/.
{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('00000000'), 'Reject 00000000');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('11111111'), 'Reject 11111111');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('22222222'), 'Reject 22222222');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('33333333'), 'Reject 33333333');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('44444444'), 'Reject 44444444');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('55555555'), 'Reject 55555555');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('66666666'), 'Reject 66666666');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('77777777'), 'Reject 77777777');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('88888888'), 'Reject 88888888');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('99999999'), 'Reject 99999999');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('12345678'), 'Reject 12345678');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('87654321'), 'Reject 87654321');
}

# Test validate_setup_code: it must also reject invalid codes with dashes
{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('0000-0000'), 'Reject 0000-0000');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('1111-1111'), 'Reject 1111-1111');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('1234-5678'), 'Reject 1234-5678 (sequential)');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('8765-4321'), 'Reject 8765-4321 (reverse sequential)');
}

# Test validate_setup_code: malformed input
{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('123-4567'), 'Reject a malformed setup code (7 digits)');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code('abcd-efgh'), 'Reject a non-numeric setup code');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code(''), 'Reject empty string');
}

{
	ok(!Protocol::HAP::SetupCode::validate_setup_code(undef), 'Reject undefined input');
}

# Test edge cases
{
	my $code = Protocol::HAP::SetupCode::normalize_setup_code('----1234----5678----');
	is($code, '12345678', 'Handle excessive dashes');
}

{
	my $code = Protocol::HAP::SetupCode::normalize_setup_code('   1234   5678   ');



( run in 1.010 second using v1.01-cache-2.11-cpan-d01c6094234 )