Initial commit

This commit is contained in:
R. Eric Wheeler 2017-11-04 11:53:58 -07:00
commit 779a1e53ce
11 changed files with 714 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.php_cs.cache
vendor/
.idea/
composer.lock

50
.php_cs Normal file
View File

@ -0,0 +1,50 @@
<?php
$header = '';
if(file_exists(__DIR__.'/header.txt')) {
$header = file_get_contents('header.txt');
}
return PhpCsFixer\Config::create()
->setRiskyAllowed(true)
->setRules(
[
'@PSR2' => true,
'@PHP70Migration' => true,
'@PHP70Migration:risky' => true,
'@PHP71Migration' => true,
'@PHP71Migration:risky' => true,
'header_comment' => ['header' => $header],
'ordered_class_elements' => true,
'ordered_imports' => true,
'no_mixed_echo_print' => ['use' => 'print'],
'strict_param' => true,
'strict_comparison' => true,
'single_import_per_statement' => false,
'phpdoc_order' => true,
'array_syntax' => ['syntax' => 'short'],
'phpdoc_add_missing_param_annotation' => true,
'psr4' => true,
'phpdoc_var_without_name' => false,
'no_extra_consecutive_blank_lines' => [
'break',
'continue',
'extra',
'return',
'throw',
'parenthesis_brace_block',
'square_brace_block',
'curly_brace_block',
],
]
)->setFinder(
PhpCsFixer\Finder::create()
->ignoreDotFiles(true)
->ignoreVCS(true)
->name('*.php')
->in([
'src',
'tests',
])
);

30
composer.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "sikofitt/samsung-tv",
"description": "Very simple library to change channels and inputs on old samsung tvs",
"type": "library",
"require": {
"guzzlehttp/guzzle": "^6.3",
"react/socket-client": "^0.7.0",
"guzzlehttp/streams": "^3.0",
"symfony/console": "^3.3||^4.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.7",
"squizlabs/php_codesniffer": "^3.1",
"phpunit/phpunit": "^6.4",
"symfony/var-dumper": "^3.3"
},
"autoload": {
"psr-4": {
"Sikofitt\\SamsungTV\\":"src/Sikofitt/SamsungTV"
}
},
"license": "MPL-2.0",
"authors": [
{
"name": "R. Eric Wheeler",
"email": "sikofitt@gmail.com"
}
],
"minimum-stability": "stable"
}

11
console.php Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env php
<?php
require __DIR__.'/vendor/autoload.php';
\Symfony\Component\Debug\Debug::enable();
$app = new \Symfony\Component\Console\Application();
$app->add(new \Sikofitt\SamsungTV\Console\Command\ChannelCommand());
$app->add(new \Sikofitt\SamsungTV\Console\Command\InputCommand());
$app->run();

14
header.txt Normal file
View File

@ -0,0 +1,14 @@
Copyleft (C) 2017 http://sikofitt.com sikofitt@gmail.com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

47
phpcs.xml.dist Normal file
View File

@ -0,0 +1,47 @@
<?xml version="1.0"?>
<ruleset name="PHP_CodeSniffer">
<description>The coding standard for PHP_CodeSniffer itself.</description>
<file>vendor/autoload.php</file>
<file>src</file>
<file>tests</file>
<exclude-pattern>*/Standards/*/Tests/*\.(inc|css|js)</exclude-pattern>
<arg name="basepath" value="."/>
<arg name="colors" />
<arg name="parallel" value="75" />
<arg value="np"/>
<!-- Don't hide tokenizer exceptions -->
<rule ref="Internal.Tokenizer.Exception">
<type>error</type>
</rule>
<rule ref="PSR1">
<exclude name="PSR1.Classes.ClassDeclaration" />
<type>error</type>
</rule>
<rule ref="PSR2">
<exclude name="PSR2.Namespaces.UseDeclaration" />
<exclude name="PSR2.Namespaces.NamespaceDeclaration" />
<type>error</type>
</rule>
<rule ref="Squiz">
<exclude name="Squiz" />
</rule>
<!-- Check var names, but we don't want leading underscores for private vars -->
<rule ref="Squiz.NamingConventions.ValidVariableName" />
<rule ref="Squiz.NamingConventions.ValidVariableName.PrivateNoUnderscore">
<severity>0</severity>
</rule>
<!-- Have 12 chars padding maximum and always show as errors -->
<rule ref="Generic.Formatting.MultipleStatementAlignment">
<properties>
<property name="maxPadding" value="12"/>
<property name="error" value="true"/>
</properties>
</rule>
</ruleset>

27
phpunit.xml.dist Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<!--printerClass="PHPUnit_Util_TestDox_ResultPrinter_HTML"-->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.3/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>src</directory>
</whitelist>
</filter>
</phpunit>

View File

@ -0,0 +1,179 @@
<?php declare(strict_types=1);
/*
* Copyleft (C) 2017 http://sikofitt.com sikofitt@gmail.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Sikofitt\SamsungTV\Actions;
use React\EventLoop\StreamSelectLoop;
use React\Stream\DuplexResourceStream;
use React\Stream\ReadableResourceStream;
use React\Stream\ThroughStream;
use React\Stream\WritableResourceStream;
use Sikofitt\SamsungTV\Packet\PacketFactory;
class Actions
{
public const KEY_ENTER = 'KEY_ENTER';
public const KEY_0 = 'KEY_0';
public const KEY_1 = 'KEY_1';
public const KEY_2 = 'KEY_2';
public const KEY_3 = 'KEY_3';
public const KEY_4 = 'KEY_4';
public const KEY_5 = 'KEY_5';
public const KEY_6 = 'KEY_6';
public const KEY_7 = 'KEY_7';
public const KEY_8 = 'KEY_8';
public const KEY_9 = 'KEY_9';
public const KEY_PLUS100 = 'KEY_PLUS100'; // - key
public const KEY_HDMI = 'KEY_HDMI'; // Apparently not
public const KEY_HDMI1 = 'KEY_HDMI1'; // the same
public const KEY_HDMI2 = 'KEY_HDMI2';
public const KEY_HDMI3 = 'KEY_HDMI3';
public const KEY_HDMI4 = 'KEY_HDMI4';
public const KEY_AV1 = 'KEY_AV1';
public const KEY_AV2 = 'KEY_AV2';
public const KEY_AV3 = 'KEY_AV3';
public const KEY_TV = 'KEY_TV';
public static $keyMap = [
'9' => self::KEY_9,
'8' => self::KEY_8,
'7' => self::KEY_7,
'6' => self::KEY_6,
'5' => self::KEY_5,
'4' => self::KEY_4,
'3' => self::KEY_3,
'2' => self::KEY_2,
'1' => self::KEY_1,
'0' => self::KEY_0,
'-' => self::KEY_PLUS100,
'.' => self::KEY_PLUS100,
'E' => self::KEY_ENTER,
'H' => self::KEY_HDMI,
'H1' => self::KEY_HDMI1,
'H2' => self::KEY_HDMI2,
'H3' => self::KEY_HDMI3,
'H4' => self::KEY_HDMI4,
'T' => self::KEY_TV,
'AV1' => self::KEY_AV1,
'AV2' => self::KEY_AV2,
'AV3' => self::KEY_AV3,
];
private $packetFactory;
private $address;
public function __construct(string $tvAddress, int $port = 55000)
{
$this->packetFactory = new PacketFactory();
$this->address = sprintf('%s:%d', $tvAddress, $port);
}
public function transformChannels(string $channel): \Generator
{
$codes = str_split($channel);
foreach ($codes as $code) {
yield $this->transformChannel($code);
}
}
public function transformChannel(string $channel): string
{
if (array_key_exists(strtoupper($channel), self::$keyMap)) {
return base64_encode(self::$keyMap[strtoupper($channel)]);
}
return '';
}
public function sendKey(string $keyCode): void
{
$loop = new StreamSelectLoop();
$loop->nextTick(function (): void {
usleep(140000);
});
$connection = stream_socket_client($this->address);
$stream = new WritableResourceStream($connection, $loop);
$stream->on('read', function($stream) {
dump($stream);
});
$stream->write($this->packetFactory->getStartPacket());
$key = $this->transformChannel($keyCode);
$stream->write($this->packetFactory->getKeyPacket($key));
$loop->run();
$stream->close();
}
public function changeChannel(string $channel): void
{
// Add enter at the end of the channel string.
$channelArr = str_split($channel.'E');
foreach ($channelArr as $item) {
$this->sendKey($item);
}
}
/**
* @param string $input the source code in self::$keyMap
*/
public function changeInput(string $input): void
{
switch (strtoupper($input)) {
default:
case 'TV':
$this->sendKey('T');
break;
case 'HDMI':
$this->sendKey('H');
break;
case 'HDMI1':
$this->sendKey('H1');
break;
case 'HDMI2':
$this->sendKey('H2');
break;
case 'HDMI3':
$this->sendKey('H3');
break;
case 'HDMI4':
$this->sendKey('H4');
break;
case 'AV1':
$this->sendKey('AV1');
break;
case 'AV2':
$this->sendKey('AV2');
break;
case 'AV3':
$this->sendKey('AV3');
break;
}
}
}

View File

@ -0,0 +1,67 @@
<?php declare(strict_types=1);
/*
* Copyleft (C) 2017 http://sikofitt.com sikofitt@gmail.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Sikofitt\SamsungTV\Console\Command;
use Sikofitt\SamsungTV\Actions\Actions;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Command to change channels
*/
class ChannelCommand extends Command
{
/**
* {@inheritDoc}
*
* @throws \Symfony\Component\Console\Exception\InvalidArgumentException
*/
protected function configure(): void
{
$this->setName('channel')
->setDescription('change the channel')
->addArgument(
'channel',
InputArgument::REQUIRED,
'chanel to change to'
);
}
/**
* {@inheritDoc}
*
* @throws \Symfony\Component\Console\Exception\InvalidArgumentException
*/
protected function execute(InputInterface $input, OutputInterface $output): void
{
$io = new SymfonyStyle($input, $output);
$channel = $input->getArgument('channel');
$actions = new Actions('10.5.4.18');
$actions->changeChannel($channel);
$io->success('Changed the Channel to ' . $channel);
}
}

View File

@ -0,0 +1,118 @@
<?php declare(strict_types=1);
/*
* Copyleft (C) 2017 http://sikofitt.com sikofitt@gmail.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Sikofitt\SamsungTV\Console\Command;
use Sikofitt\SamsungTV\Actions\Actions;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Command to change the input of the Samsung TV.
*/
class InputCommand extends Command
{
/**
* {@inheritDoc}
*
* @throws \Symfony\Component\Console\Exception\InvalidArgumentException
*/
public function configure(): void
{
$this->setName('input')
->setDescription('Change the TV source')
->addOption('port', 'p', InputOption::VALUE_REQUIRED, 'Port of the Samsung TV', 55000)
->addOption('address', 'a', InputOption::VALUE_REQUIRED, 'IP or Host of the SamsungTV')
->addArgument(
'input',
InputArgument::REQUIRED,
'The input channel. For a list type \'list\'.'
);
}
/**
* {@inheritDoc}
*
* @throws \Symfony\Component\Console\Exception\InvalidArgumentException
*/
public function interact(InputInterface $input, OutputInterface $output): void
{
$inputArg = $input->getArgument('input');
if ('list' === strtolower($inputArg)) {
$output->writeln(sprintf('Available inputs are : %s.%s', implode(', ', $this->getAvailableInputs()), PHP_EOL));
exit;
}
if(null === $input->getOption('address')) {
throw new InvalidArgumentException('We need an ip address or host to connect to.');
}
if (false === in_array(strtolower($inputArg), $this->getAvailableInputs(), true)) {
throw new InvalidArgumentException('Input ' . $inputArg . ' is not available');
}
}
/**
* {@inheritDoc}
*
* @throws \Symfony\Component\Console\Exception\InvalidArgumentException
*
* @return null|int
*/
public function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$inputArg = $input->getArgument('input');
try {
$actions = new Actions($input->getOption('address'), $input->getOption('port'));
$actions->changeInput(strtoupper($inputArg));
} catch (\Exception $e) {
$io->error($e->getMessage());
return 1;
} finally {
$io->success('Changed source input to ' . $inputArg);
return 0;
}
}
/**
* @return array
*/
private function getAvailableInputs(): array
{
return [
'tv',
'hdmi',
'hdmi1', // different than hdmi for some reason.
'hdmi2',
'hdmi3',
'hdmi4',
'av1',
'av2',
'av3',
];
}
}

View File

@ -0,0 +1,166 @@
<?php declare(strict_types=1);
/*
* Copyleft (C) 2017 http://sikofitt.com sikofitt@gmail.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Sikofitt\SamsungTV\Packet;
class PacketFactory
{
/**
* @link https://gist.github.com/honza889/b70dff4369ff2f0a2afe#file-remote-php-L63
*
* @return string The Start command
*/
public function getStartPacket(): string
{
return pack(
'C*',
0x00,
0x13,
0x00,
0x69,
0x70,
0x68,
0x6f,
0x6e,
0x65,
0x2e,
0x69,
0x61,
0x70,
0x70,
0x2e,
0x73,
0x61,
0x6d,
0x73,
0x75,
0x6e,
0x67,
0x38,
0x00,
0x64,
0x00,
0x10,
0x00,
0x4d,
0x54,
0x41,
0x75,
0x4d,
0x43,
0x34,
0x77,
0x4c,
0x6a,
0x49,
0x79,
0x4d,
0x67,
0x3d,
0x3d,
0x14,
0x00,
0x63,
0x6d,
0x46,
0x75,
0x5a,
0x47,
0x39,
0x74,
0x55,
0x6d,
0x56,
0x74,
0x62,
0x33,
0x52,
0x6c,
0x53,
0x55,
0x51,
0x3d,
0x0c,
0x00,
0x62,
0x58,
0x6c,
0x53,
0x5a,
0x57,
0x31,
0x76,
0x64,
0x47,
0x55,
0x3d
);
}
/**
* @link https://gist.github.com/honza889/b70dff4369ff2f0a2afe#file-remote-php-L73
*
* @param string $keyCode the base64 encoded key code.
*
* @return string
*/
public function getPayload(string $keyCode): string
{
return pack("C*", 0x00, 0x00, 0x00, 0x00).pack("C*", strlen($keyCode), 0x00).$keyCode;
}
/**
* @link https://gist.github.com/honza889/b70dff4369ff2f0a2afe#file-remote-php-L75-L76
*
* @param string $keyCode the base64 encoded key code.
*
* @return string The final payload to send to the TV.
*/
public function getKeyPacket(string $keyCode): string
{
$payload = $this->getPayload($keyCode);
return pack(
"C*",
0x00,
0x13,
0x00,
0x69,
0x70,
0x68,
0x6f,
0x6e,
0x65,
0x2e,
0x69,
0x61,
0x70,
0x70,
0x2e,
0x73,
0x61,
0x6d,
0x73,
0x75,
0x6e,
0x67
) .
pack("C*", strlen($payload)-1).$payload;
}
}