Change all, use hass websocket.
223
API.pm
Normal file
@@ -0,0 +1,223 @@
|
||||
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;
|
||||
@@ -1,3 +1,12 @@
|
||||
#### Version 0.10
|
||||
##### Changed
|
||||
- Now using websocket to Home Assistant
|
||||
- Only handles Entities assigned to Areas, first menu is select Area
|
||||
|
||||
#### Version 0.9
|
||||
##### Update
|
||||
- Failed fix to handle auth
|
||||
|
||||
#### Version 0.8
|
||||
##### Added
|
||||
- Add entity to handle group with common domain
|
||||
|
||||
173
HASS.pm
@@ -1,173 +0,0 @@
|
||||
package Plugins::Assistant::HASS;
|
||||
|
||||
use strict;
|
||||
use JSON::XS::VersionOneAndTwo;
|
||||
use threads::shared;
|
||||
|
||||
use Slim::Networking::SimpleAsyncHTTP;
|
||||
use Slim::Networking::SqueezeNetwork;
|
||||
use Slim::Utils::Log;
|
||||
use Slim::Utils::Prefs;
|
||||
|
||||
my $log = logger('plugin.assistant');
|
||||
my $cache;
|
||||
my $prefs = preferences('plugin.assistant');
|
||||
|
||||
|
||||
sub init {
|
||||
($cache) = @_;
|
||||
}
|
||||
|
||||
|
||||
sub testHassConnection {
|
||||
my ( $client, $cb, $params, $args ) = @_;
|
||||
|
||||
if (defined $prefs->get('connect')) {
|
||||
my $http = Slim::Networking::SimpleAsyncHTTP->new(
|
||||
sub {
|
||||
$log->info("Connected to Home Assistant at (".$prefs->get('connect').")");
|
||||
},
|
||||
sub {
|
||||
$log->error("Error (".$prefs->get('connect')."): $_[1]");
|
||||
},
|
||||
{
|
||||
timeout => 5,
|
||||
},
|
||||
);
|
||||
|
||||
$http->get(
|
||||
$prefs->get('connect'),
|
||||
'x-ha-access' => $prefs->get('pass'),
|
||||
'Content-Type' => 'application/json',
|
||||
'charset' => 'UTF-8',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub getEntities {
|
||||
my ( $client, $cb, $params, $args ) = @_;
|
||||
|
||||
our $result :shared = [];
|
||||
our $counter :shared = 0;
|
||||
|
||||
if (defined $args->{'entity_ids'}) {
|
||||
foreach my $entity_id(@{$args->{'entity_ids'}}) {
|
||||
|
||||
$counter++;
|
||||
Plugins::Assistant::HASS::getEntity(
|
||||
$client,
|
||||
sub {
|
||||
my $entity = shift;
|
||||
if (defined $entity) {
|
||||
push @$result, $entity;
|
||||
}
|
||||
$counter--;
|
||||
if ($counter <= 0) {
|
||||
$cb->($result);
|
||||
}
|
||||
},
|
||||
$params,
|
||||
{
|
||||
entity_id => $entity_id,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
Plugins::Assistant::HASS::getEntity(
|
||||
$client,
|
||||
sub {
|
||||
my $entities = shift;
|
||||
foreach my $entity(@$entities) {
|
||||
push @$result, $entity;
|
||||
}
|
||||
$cb->($result);
|
||||
},
|
||||
$params,
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub getEntity {
|
||||
my ($client, $cb, $params, $args) = @_;
|
||||
|
||||
my $url = $prefs->get('connect').'states';
|
||||
if (defined $args->{'entity_id'}) {
|
||||
$url = $url.'/'.$args->{'entity_id'};
|
||||
}
|
||||
|
||||
$log->debug('Get Entity: ', $url);
|
||||
|
||||
my $http = Slim::Networking::SimpleAsyncHTTP->new(
|
||||
sub {
|
||||
my $response = shift;
|
||||
my $params = $response->params('params');
|
||||
my $result;
|
||||
if ( $response->headers->content_type =~ /json/ ) {
|
||||
$result = decode_json($response->content);
|
||||
}
|
||||
$cb->($result);
|
||||
},
|
||||
sub {
|
||||
$log->error("Error (".$url."): $_[1]");
|
||||
$cb->();
|
||||
},
|
||||
{
|
||||
params => $params,
|
||||
timeout => 5,
|
||||
},
|
||||
);
|
||||
|
||||
$http->get(
|
||||
$url,
|
||||
'x-ha-access' => $prefs->get('pass'),
|
||||
'Content-Type' => 'application/json',
|
||||
'charset' => 'UTF-8',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
sub services {
|
||||
my ($client, $cb, $params, $args) = @_;
|
||||
|
||||
|
||||
my $url = $prefs->get('connect').'services/'.$args->{'domain'}.'/'.$args->{'service'};
|
||||
my $req->{'entity_id'} = $args->{'entity_id'};
|
||||
|
||||
$log->debug($url.' { '.$req->{'entity_id'}.' }');
|
||||
|
||||
my $http = Slim::Networking::SimpleAsyncHTTP->new(
|
||||
sub {
|
||||
my $response = shift;
|
||||
my $params = $response->params('params');
|
||||
my $result;
|
||||
if ( $response->headers->content_type =~ /json/ ) {
|
||||
$log->debug($response->content);
|
||||
$result = decode_json($response->content);
|
||||
}
|
||||
$cb->($client, $result, $params, $args);
|
||||
},
|
||||
sub {
|
||||
$log->error("Error (".$url."): $_[1]");
|
||||
$cb->();
|
||||
},
|
||||
{
|
||||
timeout => 5,
|
||||
},
|
||||
);
|
||||
|
||||
$http->post(
|
||||
$url,
|
||||
'x-ha-access' => $prefs->get('pass'),
|
||||
'Content-Type' => 'application/json',
|
||||
'charset' => 'UTF-8',
|
||||
encode_json($req),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
1;
|
||||
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 590 B |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -8,11 +8,9 @@
|
||||
<input type="text" name="pref_pass" value="[% prefs.pass == '_pass_' ? '' : prefs.pass %]">
|
||||
[% END %]
|
||||
|
||||
[% WRAPPER setting title="PLUGIN_ASSISTANT_SHOW_HOME" desc="PLUGIN_ASSISTANT_SHOW_HOME_DESC" %]
|
||||
<select class="stdedit" name="pref_show_home">
|
||||
<option [% IF NOT prefs.show_home %]selected [% END %]value="0">[% 'NO' | getstring %]</option>
|
||||
<option [% IF prefs.show_home %]selected [% END %]value="1">[% 'YES' | getstring %]</option>
|
||||
</select>
|
||||
[% WRAPPER setting title="PLUGIN_ASSISTANT_ENABLED" %]
|
||||
<input type="checkbox" [% IF prefs.pref_enabled %]checked="checked"[% END %] class="stdedit" name="pref_enabled" id="pref_enabled" />
|
||||
<label for="pref_enabled">[% "PLUGIN_ASSISTANT_ENABLE_DESC" | string %]</label>
|
||||
[% END %]
|
||||
|
||||
[% PROCESS settings/footer.html %]
|
||||
|
||||
50
Handlers.pm
Normal file
@@ -0,0 +1,50 @@
|
||||
package Plugins::Assistant::Handlers;
|
||||
|
||||
use strict;
|
||||
|
||||
our %entities = (
|
||||
a => sub {
|
||||
my ($data) = @_;
|
||||
my @results;
|
||||
foreach my $entity (keys %{$data->{a}}) {
|
||||
push @results, {
|
||||
entity => $entity,
|
||||
name => $data->{a}->{$entity}->{a}->{friendly_name} // $entity,
|
||||
icon => $data->{a}->{$entity}->{a}->{icon},
|
||||
state => $data->{a}->{$entity}->{s}
|
||||
}
|
||||
}
|
||||
return \@results;
|
||||
},
|
||||
c => sub {
|
||||
my ($data) = @_;
|
||||
my @results;
|
||||
foreach my $entity (keys %{$data->{c}}) {
|
||||
push @results, {
|
||||
entity => $entity,
|
||||
state => $data->{c}->{$entity}->{'+'}->{s}
|
||||
}
|
||||
}
|
||||
return \@results;
|
||||
}
|
||||
);
|
||||
|
||||
our %service = (
|
||||
light => sub {
|
||||
my ($state) = @_;
|
||||
my $results = $state eq 'off' ? 'turn_on' : 'turn_off';
|
||||
return $results;
|
||||
},
|
||||
switch => sub {
|
||||
my ($state) = @_;
|
||||
my $results = $state eq 'off' ? 'turn_on' : 'turn_off';
|
||||
return $results;
|
||||
},
|
||||
cover => sub {
|
||||
my ($state) = @_;
|
||||
my $results = $state eq 'open' ? 'close_cover' : 'open_cover';
|
||||
return $results;
|
||||
}
|
||||
);
|
||||
|
||||
1;
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Hans Karlinius
|
||||
Copyright (c) 2026 Hans Karlinius
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
372
Plugin.pm
@@ -2,255 +2,221 @@ package Plugins::Assistant::Plugin;
|
||||
|
||||
use strict;
|
||||
use base qw(Slim::Plugin::OPMLBased);
|
||||
|
||||
use JSON::XS::VersionOneAndTwo;
|
||||
use threads::shared;
|
||||
use Data::Dumper;
|
||||
|
||||
use Slim::Utils::Log;
|
||||
use Slim::Utils::OSDetect;
|
||||
use Slim::Utils::Prefs;
|
||||
use Slim::Utils::Strings qw(string cstring);
|
||||
|
||||
use Plugins::Assistant::HASS;
|
||||
use Plugins::Assistant::API;
|
||||
use Plugins::Assistant::Handlers;
|
||||
|
||||
use constant IMAGE_PATH => 'plugins/Assistant/html/images/';
|
||||
use constant IMAGE_UNKNOWN => 'group_unknown';
|
||||
|
||||
my $log = Slim::Utils::Log->addLogCategory(
|
||||
{
|
||||
'category' => 'plugin.assistant',
|
||||
'defaultLevel' => 'ERROR',
|
||||
'defaultLevel' => 'INFO',
|
||||
'description' => 'PLUGIN_ASSISTANT',
|
||||
}
|
||||
);
|
||||
|
||||
my $prefs = preferences('plugin.assistant');
|
||||
my $cache = Slim::Utils::Cache->new('assistant', 3);
|
||||
my %entities;
|
||||
my @images = ('cover_closed', 'cover_open', 'group_on', 'group_off', 'group_unknown', 'light_off', 'light_on', 'switch_off', 'switch_on');
|
||||
|
||||
my $menuItems;
|
||||
my @domains = ('light', 'switch', 'cover');
|
||||
|
||||
sub initPlugin {
|
||||
my $class = shift;
|
||||
|
||||
Plugins::Assistant::HASS->init($cache);
|
||||
|
||||
$class->SUPER::initPlugin(
|
||||
feed => \&handleFeed,
|
||||
tag => 'assistant',
|
||||
menu => 'radios',
|
||||
is_app => 1,
|
||||
weight => 1,
|
||||
);
|
||||
|
||||
if (main::WEBUI) {
|
||||
require Plugins::Assistant::Settings;
|
||||
Plugins::Assistant::Settings->new();
|
||||
}
|
||||
|
||||
$class->SUPER::initPlugin(
|
||||
feed => \&handleFeed,
|
||||
tag => 'assistant',
|
||||
menu => 'apps',
|
||||
);
|
||||
|
||||
if ($prefs->get('enabled')) {
|
||||
require Plugins::Assistant::API;
|
||||
Plugins::Assistant::API->init();
|
||||
|
||||
Slim::Utils::Timers::setTimer(
|
||||
undef,
|
||||
time() + 5,
|
||||
\&Assistant,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sub shutdownPlugin {
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug('shutdown');
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug(Dumper(Plugins::Assistant::API->getStatus()));
|
||||
|
||||
Slim::Utils::Timers::killTimers(undef, \&Assistant);
|
||||
Plugins::Assistant::API->shutdown();
|
||||
}
|
||||
|
||||
sub getDisplayName { 'PLUGIN_ASSISTANT' }
|
||||
|
||||
|
||||
# don't add this plugin to the Extras menu
|
||||
sub playerMenu {}
|
||||
|
||||
sub Assistant {
|
||||
main::INFOLOG && $log->is_info && $log->info('Assistant');
|
||||
|
||||
eval {
|
||||
Plugins::Assistant::API->getAreas(sub {
|
||||
my $areas = shift;
|
||||
my $jsAreas = decode_json($areas);
|
||||
foreach my $area (@$jsAreas) {
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug("Area ", JSON::XS->new->pretty->encode($area));
|
||||
push @$menuItems, {
|
||||
name => $area->{name},
|
||||
image => _imagePath($area->{picture}),
|
||||
areaId => $area->{area_id},
|
||||
};
|
||||
};
|
||||
foreach my $menuItem (@$menuItems) {
|
||||
Plugins::Assistant::API->getEntities(sub {
|
||||
my $entities = shift;
|
||||
my @entitySubscriptions;
|
||||
my $jsEntities = decode_json($entities)->{referenced_entities};
|
||||
my $domain;
|
||||
my $items = [];
|
||||
foreach my $entity (@$jsEntities) {
|
||||
$domain = substr($entity, 0, index($entity, '.')),
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug('Domain: ', $domain, ' - Entity: ', $entity);
|
||||
next unless grep {$_ eq $domain} @domains;
|
||||
push @entitySubscriptions, $entity;
|
||||
push @$items, {
|
||||
entity => $entity,
|
||||
domain => $domain
|
||||
}
|
||||
}
|
||||
if (@$items) {
|
||||
$menuItem->{items} = $items;
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug('Entity subscriptions for ',$menuItem->{name} , ' - ', join ',', @entitySubscriptions);
|
||||
Plugins::Assistant::API->subscribeEntities(\&AssistanEntity, @entitySubscriptions);
|
||||
}
|
||||
}, $menuItem->{areaId});
|
||||
}
|
||||
});
|
||||
1;
|
||||
} or do {
|
||||
my $e = $@;
|
||||
$log->error('Failed to getAreas: ', $e);
|
||||
|
||||
Slim::Utils::Timers::setTimer(
|
||||
undef,
|
||||
time() + 5,
|
||||
\&Assistant,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sub AssistantAction {
|
||||
my ($client, $cb, $params, $args) = @_;
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug('AssistantAction start ', JSON::XS->new->pretty->encode($args));
|
||||
|
||||
my $service = $Plugins::Assistant::Handlers::service{$args->{domain}}($args->{state});
|
||||
my $actionRequest = {
|
||||
domain => $args->{domain},
|
||||
entity => $args->{entity},
|
||||
service => $service
|
||||
};
|
||||
|
||||
Plugins::Assistant::API->serviceAction (sub {
|
||||
my $result = shift;
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug('AssistantAction sent ', $result);
|
||||
}, $actionRequest);
|
||||
|
||||
$cb->();
|
||||
return;
|
||||
}
|
||||
|
||||
sub AssistanEntity {
|
||||
my $entities = shift;
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug('AssistanEntity entities ', $entities);
|
||||
my $decoded = JSON::XS->new->decode($entities);
|
||||
return unless ref $decoded eq 'HASH';
|
||||
|
||||
my $key = (keys %{$decoded})[0];
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug('Handler key ', $key);
|
||||
unless (ref($Plugins::Assistant::Handlers::entities{$key}) eq 'CODE') {
|
||||
die "No handler for key: $key\n";
|
||||
}
|
||||
my $entities = $Plugins::Assistant::Handlers::entities{$key}($decoded);
|
||||
|
||||
foreach my $entity (@$entities) {
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug('Entity update ', JSON::XS->new->pretty->encode($entity));
|
||||
my $found = 0;
|
||||
foreach my $menuItem (@$menuItems) {
|
||||
my $menuItemItems = $menuItem->{items};
|
||||
foreach my $menuItemEntity (@$menuItemItems) {
|
||||
if ($menuItemEntity->{entity} eq $entity->{entity}) {
|
||||
$found = 1;
|
||||
$menuItemEntity->{name} //= $entity->{name};
|
||||
$menuItemEntity->{entityIcon} //= $entity->{icon};
|
||||
$menuItemEntity->{state} = $entity->{state};
|
||||
$menuItemEntity->{image} = _imageWithStatePath($menuItemEntity->{domain}, $entity->{state});
|
||||
$menuItemEntity->{nextWindow} = 'parent';
|
||||
$menuItemEntity->{type} = 'link';
|
||||
$menuItemEntity->{url} = \&AssistantAction;
|
||||
$menuItemEntity->{passthrough} = [{
|
||||
entity => $entity->{entity},
|
||||
domain => $menuItemEntity->{domain},
|
||||
state => $entity->{state}
|
||||
}];
|
||||
$menuItemEntity->{fetched} = 0;
|
||||
$log->debug('Updated menu item:');
|
||||
$log->debug(Dumper($menuItemEntity));
|
||||
last;
|
||||
}
|
||||
}
|
||||
last if $found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub handleFeed {
|
||||
my ($client, $cb, $args) = @_;
|
||||
|
||||
Plugins::Assistant::HASS::getEntities(
|
||||
$client,
|
||||
sub {
|
||||
my $tentities = shift;
|
||||
|
||||
my $items = [];
|
||||
my $order = 1000;
|
||||
|
||||
foreach my $tentity(@$tentities) {
|
||||
$entities{$tentity->{'entity_id'}} = $tentity;
|
||||
if ($prefs->get('enabled') && $menuItems) {
|
||||
$cb->({
|
||||
items => $menuItems,
|
||||
});
|
||||
return;
|
||||
}
|
||||
die "Assistant is not enabled or something is wrong...";
|
||||
}
|
||||
|
||||
foreach my $id(keys %entities) {
|
||||
sub _imagePath {
|
||||
my ($image) = @_;
|
||||
|
||||
my ($namespace, $name) = split('\.', $id, 2);
|
||||
if (($namespace eq 'group' && (!$entities{$id}->{'attributes'}->{'hidden'} || $entities{$id}->{'attributes'}->{'view'}))
|
||||
|| $prefs->get('show_home') == 1) {
|
||||
my $base = $prefs->get('connect') || '';
|
||||
|
||||
my $item = getItem($id);
|
||||
$item->{'order'} = $order++ if (!defined $item->{'order'});
|
||||
$log->debug('getEntities: '.$id.' - '.$item->{'name'}.' - '.$item->{'order'});
|
||||
push @$items, $item;
|
||||
}
|
||||
}
|
||||
$items = [ sort { uc($a->{order}) cmp uc($b->{order}) } @$items ];
|
||||
$cb->(
|
||||
{
|
||||
items => $items,
|
||||
}
|
||||
);
|
||||
},
|
||||
$args,
|
||||
);
|
||||
unless ($base && $image) {
|
||||
return Plugins::Assistant::Plugin->_pluginDataFor('icon');
|
||||
}
|
||||
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug(' ImagePath: ', $base.$image);
|
||||
my $resize_url = Slim::Web::ImageProxy::proxiedImage($base.$image);
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug(' ImagePathResize: ', $resize_url);
|
||||
|
||||
sub getItem {
|
||||
|
||||
my ($id) = @_;
|
||||
my ($namespace, $name) = split('\.', $id, 2);
|
||||
|
||||
$log->debug($id);
|
||||
|
||||
if ($namespace eq 'group') {
|
||||
|
||||
my $gorder = 2000;
|
||||
my $gitems = [];
|
||||
|
||||
# Add unique entity for group of same type excluded group
|
||||
# As I do beleive is similar to what HASS does :)
|
||||
my %seen;
|
||||
my @uniqueGroup = grep {not $seen{$_}++ } map { /^(?!group)(\S*)\./ } @{$entities{$id}->{'attributes'}->{'entity_id'}};
|
||||
if (scalar(@uniqueGroup) == 1) {
|
||||
|
||||
$namespace = @uniqueGroup[0];
|
||||
|
||||
my $tid = $namespace.'.'.$name;
|
||||
$entities{$tid} = $entities{$id};
|
||||
if (!grep(/$tid/, @{$entities{$id}->{'attributes'}->{'entity_id'}})) {
|
||||
push @{$entities{$id}->{'attributes'}->{'entity_id'}}, $tid;
|
||||
}
|
||||
return Slim::Web::ImageProxy::proxiedImage($base.$image);
|
||||
}
|
||||
|
||||
foreach my $gid(@{$entities{$id}->{'attributes'}->{'entity_id'}}) {
|
||||
|
||||
my $gitem = getItem($gid, %entities);
|
||||
|
||||
$gitem->{'order'} = $gorder++ if (!defined $gitem->{'order'});
|
||||
$log->debug($id.' - '.$gitem->{'name'}.' - '.$gitem->{'order'});
|
||||
push @$gitems, $gitem;
|
||||
sub _imageWithStatePath {
|
||||
my ($domain, $state) = @_;
|
||||
my $path;
|
||||
unless ($domain && $state) { return; }
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug(' ImageWithStatePath: ', IMAGE_PATH.$domain.'_'.$state.'.png');
|
||||
return IMAGE_PATH.$domain.'_'.$state.'.png';
|
||||
}
|
||||
$gitems = [ sort { uc($a->{order}) cmp uc($b->{order}) } @$gitems ];
|
||||
|
||||
return {
|
||||
name => $entities{$id}->{'attributes'}->{'friendly_name'},
|
||||
image => getImage($namespace.'_'.$entities{$id}->{'state'}),
|
||||
order => $entities{$id}->{'attributes'}->{'order'},
|
||||
type => 'link',
|
||||
items => $gitems,
|
||||
};
|
||||
|
||||
} elsif ($namespace eq 'light' || $namespace eq 'switch') {
|
||||
|
||||
return {
|
||||
name => $entities{$id}->{'attributes'}->{'friendly_name'},
|
||||
image => getImage($namespace.'_'.$entities{$id}->{'state'}),
|
||||
order => $entities{$id}->{'attributes'}->{'order'},
|
||||
nextWindow => 'refresh',
|
||||
type => 'link',
|
||||
url => \&servicesCall,
|
||||
passthrough => [
|
||||
{
|
||||
entity_id => $entities{$id}->{'entity_id'},
|
||||
domain => $namespace,
|
||||
service => $entities{$id}->{'state'} eq 'on' ? 'turn_off' : 'turn_on',
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
} elsif ($namespace eq 'cover') {
|
||||
|
||||
my $service = 'stop_cover';
|
||||
|
||||
if ($entities{$id}->{'state'} eq 'closed') {
|
||||
$service = 'open_cover';
|
||||
} elsif ($entities{$id}->{'state'} eq 'open') {
|
||||
$service = 'close_cover';
|
||||
}
|
||||
|
||||
return {
|
||||
name => $entities{$id}->{'attributes'}->{'friendly_name'},
|
||||
image => getImage($namespace.'_'.$entities{$id}->{'state'}),
|
||||
order => $entities{$id}->{'attributes'}->{'order'},
|
||||
nextWindow => 'refresh',
|
||||
type => 'link',
|
||||
url => \&servicesCall,
|
||||
passthrough => [
|
||||
{
|
||||
entity_id => $entities{$id}->{'entity_id'},
|
||||
domain => $namespace,
|
||||
service => $service,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
} elsif ($namespace eq 'sensor') {
|
||||
|
||||
my $name = $entities{$id}->{'attributes'}->{'friendly_name'}.' '.$entities{$id}->{'state'}.$entities{$id}->{'attributes'}->{'unit_of_measurement'};
|
||||
|
||||
$name =~ s/\R//g;
|
||||
|
||||
return {
|
||||
name => $name,
|
||||
order => $entities{$id}->{'attributes'}->{'order'},
|
||||
type => 'text',
|
||||
};
|
||||
|
||||
} else {
|
||||
|
||||
return {
|
||||
name => $entities{$id}->{'attributes'}->{'friendly_name'}.' '.$entities{$id}->{'state'},
|
||||
order => $entities{$id}->{'attributes'}->{'order'},
|
||||
type => 'text',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub getImage {
|
||||
my ($img) = @_;
|
||||
|
||||
if (grep(/^$img$/, @images)) {
|
||||
return IMAGE_PATH.$img.'.png';
|
||||
} else {
|
||||
return IMAGE_PATH.IMAGE_UNKNOWN.'.png';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub servicesCall {
|
||||
my ($client, $cb, $params, $args) = @_;
|
||||
|
||||
Plugins::Assistant::HASS::services(
|
||||
$client,
|
||||
sub {
|
||||
my ($client, $result, $params, $args) = @_;
|
||||
my $newstate = '';
|
||||
|
||||
foreach my $entity (@$result) {
|
||||
if ($entity->{'entity_id'} eq $args->{'entity_id'}) {
|
||||
$newstate = $entity->{'state'};
|
||||
}
|
||||
}
|
||||
|
||||
my $items = [];
|
||||
|
||||
push @$items,
|
||||
{
|
||||
name => $entities{$args->{'entity_id'}}->{'attributes'}->{'friendly_name'}.' '.$newstate,
|
||||
type => 'text',
|
||||
showBriefly => 1,
|
||||
};
|
||||
$cb->(
|
||||
{
|
||||
items => $items,
|
||||
}
|
||||
);
|
||||
},
|
||||
$params,
|
||||
$args,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
1;
|
||||
@@ -5,9 +5,8 @@ This is a Plugin for Squeezebox server where you can control entities in Home As
|
||||
## Supports
|
||||
|
||||
- Lights on/off
|
||||
- Cover open/close
|
||||
- Switch on/off
|
||||
- Generic entity to handle group
|
||||
- Cover open/close
|
||||
|
||||
## Tested on
|
||||
|
||||
@@ -17,7 +16,7 @@ This is a Plugin for Squeezebox server where you can control entities in Home As
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Menues are not updated without going back and forward
|
||||
- Only handles Areas and the Entities assigned to Areas.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
31
Settings.pm
@@ -7,37 +7,36 @@ use Slim::Utils::Prefs;
|
||||
|
||||
my $prefs = preferences('plugin.assistant');
|
||||
|
||||
|
||||
sub name {
|
||||
return 'PLUGIN_ASSISTANT';
|
||||
return Slim::Web::HTTP::CSRF->protectName('PLUGIN_ASSISTANT');
|
||||
}
|
||||
|
||||
|
||||
sub prefs {
|
||||
return ($prefs, qw(connect pass show_home));
|
||||
}
|
||||
|
||||
|
||||
sub page {
|
||||
return 'plugins/Assistant/settings.html';
|
||||
return Slim::Web::HTTP::CSRF->protectURI('plugins/Assistant/settings.html');
|
||||
}
|
||||
|
||||
sub prefs {
|
||||
return ($prefs, qw(connect pass enabled));
|
||||
}
|
||||
|
||||
sub handler {
|
||||
my ($class, $client, $params, $callback, @args) = @_;
|
||||
|
||||
$params->{'pref_enabled'} = defined $params->{'pref_enabled'} ? 1 : 0;
|
||||
|
||||
if ( $params->{saveSettings} ) {
|
||||
$prefs->set('connect', $params->{pref_connect});
|
||||
$prefs->set('pass', $params->{pref_pass});
|
||||
$prefs->set('show_home', $params->{pref_show_home});
|
||||
$prefs->set('enabled', $params->{pref_enabled});
|
||||
$prefs->savenow();
|
||||
|
||||
if ( $params->{'pref_enabled'} ) {
|
||||
Plugins::Assistant::Plugin->initPlugin();
|
||||
} else {
|
||||
Plugins::Assistant::Plugin->shutdownPlugin();
|
||||
}
|
||||
}
|
||||
|
||||
if ( $prefs->get('connect') ) {
|
||||
Plugins::Assistant::HASS->testHassConnection();
|
||||
return $class->SUPER::handler($client, $params, $callback, @args);
|
||||
}
|
||||
|
||||
return $class->SUPER::handler($client, $params);
|
||||
}
|
||||
|
||||
1;
|
||||
289
SimpleAsyncWS.pm
Normal file
@@ -0,0 +1,289 @@
|
||||
package Plugins::Assistant::SimpleAsyncWS;
|
||||
|
||||
# Lyrion Music Server Copyright 2024 Lyrion Community.
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License,
|
||||
# version 2.
|
||||
|
||||
# This class provides a non-blocking WebSockets client connection from Lyrion Music Server.
|
||||
|
||||
# This class is intended for plugins and other code needing simply to
|
||||
# handle a persistent websockets connection. If you have more complex
|
||||
# needs consider writing a fuller implementation.
|
||||
|
||||
# This is a copy with some changes of the original SimpleWS file.
|
||||
|
||||
use strict;
|
||||
|
||||
use IO::Socket;
|
||||
use IO::Socket::SSL;
|
||||
use IO::Select;
|
||||
use Protocol::WebSocket::Client;
|
||||
use URI;
|
||||
|
||||
use Slim::Utils::Log;
|
||||
use Slim::Utils::Prefs;
|
||||
|
||||
my $log = logger('plugin.assistant');
|
||||
|
||||
sub new {
|
||||
my ($class, $url, $cbConnected, $cbConnectFailed, $cbRead, $cbReadFailed) = @_;
|
||||
|
||||
my $self = {
|
||||
client => 0,
|
||||
tcp_socket => 0,
|
||||
socket_open => 0,
|
||||
continue_listening => 0,
|
||||
cb_Read => 0,
|
||||
cb_Read_Failed => 0,
|
||||
};
|
||||
|
||||
bless $self, $class;
|
||||
|
||||
$self->_connect( $url, $cbConnected, $cbConnectFailed, $cbRead, $cbReadFailed);
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
||||
sub close {
|
||||
my ($self) = @_;
|
||||
|
||||
Slim::Utils::Timers::killTimers($self, \&_receive);
|
||||
|
||||
main::INFOLOG && $log->is_info && $log->info("Close web socket connect with status: " . $self->{tcp_socket}->connected() );
|
||||
|
||||
$self->{continue_listening} = 0;
|
||||
$self->{client}->disconnect;
|
||||
$self->{tcp_socket}->close if $self->{socket_open};
|
||||
$self->{socket_open} = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
sub _connect {
|
||||
my ($self, $url, $cbConnected, $cbConnectFailed, $cbRead, $cbReadFailed) = @_;
|
||||
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug("Connecting to webSocket $url");
|
||||
|
||||
my $uri = URI->new($url);
|
||||
my $proto = $uri->scheme;
|
||||
my $host = $uri->host;
|
||||
my $path = $uri->path;
|
||||
my $port = $uri->port;
|
||||
|
||||
if (! (($proto =~ /ws|wss/) && $host) ) {
|
||||
$log->warn("Failed to parse $url");
|
||||
$cbConnectFailed->("Failed to parse Host/Port for ws URL from $url");
|
||||
return;
|
||||
} elsif ($port == 433 ) {
|
||||
$proto = 'wss';
|
||||
}
|
||||
|
||||
main::INFOLOG && $log->is_info && $log->info("Attempting to open socket to $proto://$host:$port...");
|
||||
|
||||
if ($proto eq 'wss') {
|
||||
IO::Socket::SSL::set_defaults(SSL_verify_mode => Net::SSLeay::VERIFY_NONE())
|
||||
if preferences('server')->get('insecureHTTPS');
|
||||
|
||||
$self->{tcp_socket} = IO::Socket::SSL->new(
|
||||
PeerAddr => $host,
|
||||
PeerPort => "$proto($port)",
|
||||
Proto => 'tcp',
|
||||
Blocking => 1,
|
||||
SSL_startHandshake => 1,
|
||||
) or $cbConnectFailed->("Failed to connect to socket: $!,$SSL_ERROR");
|
||||
} else {
|
||||
$self->{tcp_socket} = IO::Socket::INET->new(
|
||||
PeerAddr => $host,
|
||||
PeerPort => "$proto($port)",
|
||||
Proto => 'tcp',
|
||||
Blocking => 1,
|
||||
) or $cbConnectFailed->("Failed to connect to socket: $!");
|
||||
}
|
||||
|
||||
main::INFOLOG && $log->is_info && $log->info("Starting To Listen Async");
|
||||
$self->{cb_Read} = $cbRead;
|
||||
$self->{cb_Read_Failed} = $cbReadFailed;
|
||||
$self->{continue_listening} = 1;
|
||||
$self->_receive();
|
||||
|
||||
|
||||
main::INFOLOG && $log->is_info && $log->info("Trying to create Protocol::WebSocket::Client handler for $url...");
|
||||
$self->{client} = Protocol::WebSocket::Client->new(url => $url);
|
||||
$self->{socket_open} = 1;
|
||||
|
||||
# Set up the various methods for the WS Protocol handler
|
||||
# On Write: take the buffer (WebSocket packet) and send it on the socket.
|
||||
$self->{client}->on(
|
||||
write => sub {
|
||||
my $client = shift;
|
||||
my ($buf) = @_;
|
||||
|
||||
#main::DEBUGLOG && $log->is_debug && $log->debug("Sending $buf ...");
|
||||
|
||||
syswrite $self->{tcp_socket}, $buf if $self->{socket_open};
|
||||
}
|
||||
);
|
||||
|
||||
# On Connect: this is what happens after the handshake succeeds, and we
|
||||
# are "connected" to the service.
|
||||
$self->{client}->on(
|
||||
connect => sub {
|
||||
my $client = shift;
|
||||
main::INFOLOG && $log->is_info && $log->info("Successfully Connected to $url...", $client);
|
||||
$cbConnected->();
|
||||
}
|
||||
);
|
||||
|
||||
$self->{client}->on(
|
||||
error => sub {
|
||||
my $client = shift;
|
||||
my ($buf) = @_;
|
||||
|
||||
$log->error("ERROR ON WEBSOCKET: $buf");
|
||||
$self->{tcp_socket}->close;
|
||||
die "Websocket error, socket closed";
|
||||
}
|
||||
);
|
||||
|
||||
$self->{client}->on(
|
||||
read => sub {
|
||||
my $client = shift;
|
||||
my ($buf) = @_;
|
||||
main::INFOLOG && $log->is_info && $log->info("Message Recieved : $buf");
|
||||
|
||||
$self->_read($buf);
|
||||
}
|
||||
);
|
||||
|
||||
$self->{client}->on(
|
||||
ping => sub {
|
||||
my $client = shift;
|
||||
my ($buf) = @_;
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug("Ping sent, sending pong : " . sprintf("%v02X", $buf));
|
||||
$client->pong($buf);
|
||||
}
|
||||
);
|
||||
|
||||
main::INFOLOG && $log->is_info && $log->info("connecting to client");
|
||||
$self->{client}->connect;
|
||||
|
||||
# read until handshake is complete. This is blocking but should be over quickly.
|
||||
while (!$self->{client}->{hs}->is_done){
|
||||
my $recv_data;
|
||||
|
||||
#my $bytes_read = sysread $self->{tcp_socket}, $recv_data, 16384;
|
||||
my $bytes_read = sysread $self->{tcp_socket}, $recv_data, 16;
|
||||
#$log->debug('<DATA> ', $recv_data);
|
||||
|
||||
if (!defined $bytes_read) {
|
||||
$log->error("sysread on tcp_socket failed: $!");
|
||||
$cbConnectFailed->("WS Handshake failed");
|
||||
return;
|
||||
}elsif ($bytes_read == 0) {
|
||||
$log->error("Connection terminated.");
|
||||
$cbConnectFailed->("WS Handshake failed");
|
||||
return;
|
||||
}
|
||||
|
||||
$self->{client}->read($recv_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
sub _read {
|
||||
my ($self, $buf) = @_;
|
||||
|
||||
$self->{cb_Read}->($buf);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
sub _receive {
|
||||
my ($self) = @_;
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug("Starting Listening");
|
||||
|
||||
eval {
|
||||
if ($self->{continue_listening}) {
|
||||
|
||||
my $s = IO::Select->new();
|
||||
$s->add($self->{tcp_socket});
|
||||
$! = 0;
|
||||
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug("Checking the socket for something to read");
|
||||
my @ready = $s->can_read(0);
|
||||
|
||||
if (@ready) {
|
||||
my $recv_data;
|
||||
my $bytes_read = sysread $ready[0], $recv_data, 16384;
|
||||
if (!defined $bytes_read) {
|
||||
|
||||
$log->error("Error reading from socket : $!");
|
||||
$self->{cb_Read_Failed}->();
|
||||
|
||||
# poll again in 1 second
|
||||
$self->_continueListen(1);
|
||||
|
||||
} elsif ($bytes_read == 0) {
|
||||
|
||||
# Remote socket closed
|
||||
$log->error("Connection terminated by remote. $!");
|
||||
|
||||
$self->{cb_Read_Failed}->();
|
||||
|
||||
close();
|
||||
|
||||
} else {
|
||||
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug("Received data : $recv_data ");
|
||||
$self->{client}->read($recv_data);
|
||||
|
||||
# if Async, poll immediately so that we pull everything off the socket if something is there.
|
||||
$self->_continueListen(1);
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
main::DEBUGLOG && $log->is_debug && $log->debug("No Data Present, continue listening");
|
||||
|
||||
# poll again in 1 second
|
||||
$self->_continueListen(1);
|
||||
|
||||
}
|
||||
}
|
||||
} or do {
|
||||
my $e = $@;
|
||||
$log->error('Failed receive: ', $e);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub _continueListen {
|
||||
my ($self, $pollTimeSeconds) = @_;
|
||||
|
||||
eval {
|
||||
Slim::Utils::Timers::setTimer($self, time() + $pollTimeSeconds, \&_receive);
|
||||
} or do {
|
||||
my $e = $@;
|
||||
$log->error('Failed timer: ', $e);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sub send {
|
||||
my ($self, $buf) = @_;
|
||||
|
||||
main::INFOLOG && $log->is_info && $log->info("Sending on web socket : $buf ");
|
||||
$self->{client}->write($buf);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -3,7 +3,6 @@
|
||||
<name>PLUGIN_ASSISTANT</name>
|
||||
<creator>Hans Karlinius</creator>
|
||||
<defaultState>enabled</defaultState>
|
||||
<needsMySB>false</needsMySB>
|
||||
<description>PLUGIN_ASSISTANT_DESCRIPTION</description>
|
||||
<email>hans.karlinius@live.com</email>
|
||||
<icon>plugins/Assistant/html/images/icon.png</icon>
|
||||
@@ -11,9 +10,9 @@
|
||||
<module>Plugins::Assistant::Plugin</module>
|
||||
<optionsURL>plugins/Assistant/settings.html</optionsURL>
|
||||
<targetApplication>
|
||||
<id>Logitech Media Server</id>
|
||||
<id>Lyrion Media Server</id>
|
||||
<maxVersion>*</maxVersion>
|
||||
<minVersion>7.6</minVersion>
|
||||
<minVersion>9.0</minVersion>
|
||||
</targetApplication>
|
||||
<type>2</type>
|
||||
<version>0.8</version>
|
||||
|
||||
12
strings.txt
@@ -5,16 +5,16 @@ PLUGIN_ASSISTANT_DESCRIPTION
|
||||
EN Remote controlling entities in Home Assistant.
|
||||
|
||||
PLUGIN_ASSISTANT_CONNECT
|
||||
EN Home Assistant connect url
|
||||
EN Home Assistant connect URL
|
||||
|
||||
PLUGIN_ASSISTANT_CONNECT_DESC
|
||||
EN Should be the same as used for web access with addition of /api/ at the end like http://localhost:8123/api/
|
||||
EN Should be the same as used for web access like http://homeassistant.local:8123/
|
||||
|
||||
PLUGIN_ASSISTANT_SHOW_HOME
|
||||
EN Show "home" in menu
|
||||
PLUGIN_ASSISTANT_ENABLED
|
||||
EN Enable Assistant plugin
|
||||
|
||||
PLUGIN_ASSISTANT_SHOW_HOME_DESC
|
||||
EN By not showing "home" only groups will be shown for a cleaner looking menu.
|
||||
PLUGIN_ASSISTANT_ENABLE_DESC
|
||||
EN Enable and start the plugin requires connection URL and API password.
|
||||
|
||||
PLUGIN_ASSISTANT_PASS
|
||||
EN Home Assistant API password
|
||||
|
||||