Files
squeeze-remote-hass/API.pm
2026-03-20 22:19:43 +01:00

224 lines
4.9 KiB
Perl

package Plugins::Assistant::API;
use strict;
use base qw(Slim::Utils::Accessor);
use JSON::XS::VersionOneAndTwo;
use Plugins::Assistant::SimpleAsyncWS;
use Slim::Utils::Log;
use Slim::Utils::Prefs;
my %pendingCb;
our $ws = 0;
my $authenticated = 0;
my $log = logger('plugin.assistant');
my $prefs = preferences('plugin.assistant');
my $messageId = 1;
sub init {
my ($class, $args) = @_;
Slim::Utils::Timers::setTimer(
undef,
time(),
\&_connect,
);
}
sub getAreas {
my ($self, $cb, $args) = @_;
main::DEBUGLOG && $log->is_debug && $log->debug('getAreas');
$self->_write({
type => 'config/area_registry/list'
}, $cb);
}
sub getEntities {
my ($self, $cb, $areaId) = @_;
main::DEBUGLOG && $log->is_debug && $log->debug('getEnteties for ', $areaId);
$self->_write({
type => 'extract_from_target',
target => {
area_id => [$areaId]
},
}, $cb);
}
sub subscribeEntities {
my ($self, $cb, @entities) = @_;
my $subscribeEntities = join ',', @entities;
main::DEBUGLOG && $log->is_debug && $log->debug('subscribeEntities ', $subscribeEntities);
$self->_write({
type => 'subscribe_entities',
entity_ids => $subscribeEntities
}, $cb);
}
sub serviceAction {
my ($self, $cb, $actionRequest) = @_;
main::DEBUGLOG && $log->is_debug && $log->debug('serviceAction ', JSON::XS->new->pretty->encode($actionRequest));
$self->_write({
type => 'call_service',
domain => $actionRequest->{domain},
service => $actionRequest->{service},
target => {
entity_id => $actionRequest->{entity}
}
}, $cb);
}
sub getImage {
my ($self, $image) = @_;
unless ($image) { return; }
my $base = $prefs->get('connect') || '';
unless ($base) {
$log->error('Connect URL not set');
return;
}
return $base.$image;
}
sub _connect {
my ($class, $args) = @_;
main::DEBUGLOG && $log->is_debug && $log->debug('Connecting');
# Build ws(s)://<host>/api/websocket from connect pref
my $base = $prefs->get('connect') || '';
unless ($base) {
main::DEBUGLOG && $log->is_debug && $log->error('Connect URL not set');
return;
}
my $hostpart = $base =~ m{^(https?://[^/]+)}i ? $1 : $base;
my $ws_url = $hostpart;
$ws_url =~ s{^https}{wss}i; $ws_url =~ s{^http}{ws}i;
$ws_url .= '/api/websocket' unless $ws_url =~ m{/api/websocket$}i;
main::DEBUGLOG && $log->is_debug && $log->debug('Connect URL: '.$ws_url);
eval {
$ws = Plugins::Assistant::SimpleAsyncWS->new(
$ws_url,
\&_connected,
\&_connectError,
\&_message,
\&_readError
);
1;
} or do {
my $e = $@;
$log->error('Failed connecting: ', $e);
Slim::Utils::Timers::setTimer(
undef,
time() + 10,
\&_connect,
);
}
}
sub _connectAuth {
main::DEBUGLOG && $log->is_debug && $log->debug('Authenticating');
my $token = $prefs->get('pass') || '';
if ($ws && $token) {
my $auth = encode_json({ type => 'auth', access_token => $token });
$ws->send($auth);
} else {
$log->error('Token not set or no connection');
}
}
sub _write {
my ($self, $msg, $cb) = @_;
die "Not authenticated" unless $authenticated;
my $sentMessageId = $messageId++;
my $sentMessage = {%$msg, id => $sentMessageId};
$pendingCb{$sentMessageId} = $cb;
my $req = encode_json($sentMessage);
main::DEBUGLOG && $log->is_debug && $log->debug('Write message: ', $req);
$ws->send($req) if $ws;
}
sub _message {
my ($buf) = @_;
my $payload;
eval { $payload = decode_json($buf) };
if ($@) {
$log->error('Failed to decode websocket message');
return;
}
if (!$ws) {
main::DEBUGLOG && $log->is_debug && $log->debug('Client not done connecting');
}
my $type = $payload->{type} || '';
$log->debug('Got message: ', $type);
if ($type eq 'auth_required') {
Plugins::Assistant::API->_connectAuth();
return;
}
elsif ($type eq 'auth_ok') {
$authenticated = 1;
return;
}
elsif ($type eq 'result' && defined $payload->{id}) {
main::DEBUGLOG && $log->is_debug && $log->debug('Got message type result and id='.$payload->{id}.' success='.$payload->{success});
my $result = encode_json($payload->{result} || []);
$pendingCb{$payload->{id}}->($result);
return;
}
elsif ($type eq 'event' && defined $payload->{id}) {
main::DEBUGLOG && $log->is_debug && $log->debug('Got message type result and id='.$payload->{id});
my $event = encode_json($payload->{event} || []);
$pendingCb{$payload->{id}}->($event);
return;
}
# Other message types can be handled/logged here
main::INFOLOG && $log->is_info && $log->info('Unhandled message type: ' . $type);
}
sub _connected {
main::DEBUGLOG && $log->is_debug && $log->debug('Connected');
}
sub _connectError {
$log->error('WebSocket connection error');
}
sub _readError {
$log->error('WebSocket read error');
}
sub getStatus {
my ($self) = @_;
return {
connected => $ws->{socket_open} // 0,
listening => $ws->{continue_listening} // 0,
authenticated => $ws->{client}->{hs}->is_done // 0,
url => $ws->{client}->{url}
} if $ws;
}
sub shutdown {
Slim::Utils::Timers::killTimers(undef, \&_connect);
$ws->close() unless !$ws;
}
1;