diff --git a/.gitignore b/.gitignore index 189d7da..7c239d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ composer.phar composer.lock vendor/ -build/ \ No newline at end of file +build/ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 836f732..c84290a 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,14 @@ return [ 'lamp' => [ 'ip' => '192.168.1.100', //Or hostname eg: home.example.com 'port' => '9999', + 'timeout' => 5 // Optional, timeout setting (how long we will try communicate with device before giving up) ], ]; ``` -You may add as many devices as you wish, as long as you specify the IP address (or host address if required) and port number to access each one. Giving each device a name makes it easy to identify them when coding later. _(Please note that the name you give here does NOT have to match the actual name you might have assigned the device using an official app like Kasa. They do NOT have to match)_ +You may add as many devices as you wish, as long as you specify the IP address (or host address if required) and port number to access each one. Giving each device a name makes it easy to identify them when coding later. _(Please note that the name you give here does NOT have to match the actual name you might have assigned the device using an official app like Kasa. They do NOT have to match) + +You can use the `autoDiscoverTPLinkDevices` method to automatically find networked devices. ## Usage You can access your device either through the `TPLinkManager` class (especially useful if you have multiple devices), or directly using the `TPLinkDevice` class. @@ -130,7 +133,40 @@ If a command requires a parameter, provide that as well: $tpDevice->sendCommand(TPLinkCommand::setLED(false)); ``` -####Toggle Power +#### Auto Discovery +You can search your local network for devices using `TPLinkManager`, using the method `autoDiscoverTPLinkDevices` +all found devices will be added to the 'TPLinkManager' config automatically, exposed using `deviceList()`. + +You must provide the IP range you wish to scan, use it as follows: +```php +//Non laravel + $tpLinkManager->autoDiscoverTPLinkDevices('192.168.0.*'); + +//Laravel + // with facade + TPLink::autoDiscoverTPLinkDevices('192.168.0.*'); + + // without facade + app('tplink')->autoDiscoverTPLinkDevices('192.168.0.*'); + app(TPLinkManager::class)->autoDiscoverTPLinkDevices('192.168.0.*'); +``` + +The auto discovery command will take a while to scan, once completed you can use `deviceList()` method to view the new configuration and any found devices. + +```php +//Non laravel + $tpLinkManager->deviceList(); + +//Laravel + // with facade + $devices = TPLink::deviceList(); + + // without facade + $devices = app('tplink')->deviceList(); + $devices = app(TPLinkManager::class)->deviceList(); +``` + +#### Toggle Power There is one command that is called directly on the `TPLinkDevice` and that is the `togglePower()` method. If you only wish to toggle the current power state of the plug, use it as follows: @@ -216,6 +252,7 @@ Any issues, feedback, suggestions or questions please use issue tracker [here][l - [softScheck](https://github.com/softScheck/tplink-smartplug) (Who did the reverse engineering and provided the secrets on how to talk to the Smartplug.) - [Jonathan Williamson][link-author] - [Syed Irfaq R.](https://github.com/irazasyed) For the idea behind how to manage multiple devices. +- [Shane Rutter](https://shanerutter.co.uk) Auto-Discovery feature ## Disclaimer diff --git a/composer.json b/composer.json index 6ea9fe3..2a3bc04 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ } ], "require": { - "tightenco/collect": "^5.3" + "tightenco/collect": "^5.3", + "s1lentium/iptools": "^1.1" }, "require-dev": { "phpunit/phpunit": "^5.7" diff --git a/src/Laravel/TPLinkServiceProvider.php b/src/Laravel/TPLinkServiceProvider.php index 197f01f..028f877 100644 --- a/src/Laravel/TPLinkServiceProvider.php +++ b/src/Laravel/TPLinkServiceProvider.php @@ -27,20 +27,17 @@ public function boot() */ public function register() { - $this->app->singleton(TPLinkManager::class, - function ($app) { - $config = (array)$app['config']['TPLink']; + $this->app->singleton(TPLinkManager::class, function ($app) { + $config = (array)$app['config']['TPLink']; - return new TPLinkManager($config); - }); + return new TPLinkManager($config); + }); $this->app->alias(TPLinkManager::class, 'tplink'); - //Auto-register the TPLink facade if the user hasn't already //assigned it to another class. if (class_exists(AliasLoader::class)) { - $loader = AliasLoader::getInstance(); if (!array_key_exists('TPLink', $loader->getAliases())) { diff --git a/src/Laravel/config/TPLink.php b/src/Laravel/config/TPLink.php index 7d9cb2b..36cd483 100644 --- a/src/Laravel/config/TPLink.php +++ b/src/Laravel/config/TPLink.php @@ -8,11 +8,13 @@ // 'bedroom' => [ // 'ip' => '192.168.1.100', //Or hostname // 'port' => '9999', +// 'timeout' => 5 // ], // 'livingroom' => [ // 'ip' => '192.168.1.101', //Or hostname // 'port' => '9999', +// 'timeout' => 10 // ], -]; \ No newline at end of file +]; diff --git a/src/TPLinkCommand.php b/src/TPLinkCommand.php index f76e407..f144117 100644 --- a/src/TPLinkCommand.php +++ b/src/TPLinkCommand.php @@ -7,7 +7,6 @@ use InvalidArgumentException; use Illuminate\Support\Collection; - /** * Class TPLinkCommands * @@ -114,7 +113,7 @@ public static function setDeviceAlias($name) public static function setMacAddress($macAddress) { if (filter_var($macAddress, FILTER_VALIDATE_MAC) === false) { - throw new InvalidArgumentException('The supplied MAC address is not valid. Try again using hyphens between each group of characters.'); + throw new InvalidArgumentException('MAC address invalid. Try hyphens between each group of characters.'); } return [ @@ -241,7 +240,7 @@ public static function flashFirmware($confirm = false) ]; } - throw new InvalidArgumentException('You must set the confirm flag to true before flashing firmware is allowed.'); + throw new InvalidArgumentException('Confirm flag to true before flashing firmware is allowed.'); } /** @@ -402,7 +401,7 @@ public static function cloudUnregisterDevice($confirm = false) ]; } - throw new InvalidArgumentException('You must set the confirm flag to true before un-registering the device is allowed.'); + throw new InvalidArgumentException('Confirm flag to true before un-registering the device is allowed.'); } /** @@ -662,7 +661,7 @@ public static function scheduleRuleList() * @param DateTime $dateAndTime The actual Date and Time for this event. * @param bool $turnOn Should the event turn on or off the timer. * @param string $name An event name. On some clients this isn't even seen. - * @param array $daysOfWeekToRepeat (Optional). An array of days of the week this event should repeat. Use normal english like Tues, Saturday etc. + * @param array $daysOfWeekToRepeat (Optional). Days of week event should repeat. Use EN like Tues, Saturday etc. * * @return array */ @@ -690,7 +689,7 @@ public static function scheduleRuleCreate(DateTime $dateAndTime, $turnOn, $name, * @param DateTime $dateAndTime The actual Date and Time for this event. * @param bool $turnOn Should the event turn on or off the timer. * @param string $name An event name. On some clients this isn't even seen. - * @param array $daysOfWeekToRepeat (Optional). An array of days of the week this event should repeat. Use normal english like Tues, Saturday etc. + * @param array $daysOfWeekToRepeat (Optional). Days of week event should repeat. Use EN like Tues, Saturday etc. * * @return array */ @@ -856,7 +855,7 @@ public static function antitheftRuleList() * @param DateTime $startTime The start date/time for the event to begin * @param DateTime $endTime The end date/time for the event to finish. * @param string $name An event name. On some clients this isn't even seen. - * @param array $daysOfWeekToRepeat (Optional). An array of days of the week this event should repeat. Use normal english like Tues, Saturday etc. + * @param array $daysOfWeekToRepeat (Optional). Days of week event should repeat. Use EN like Tues, Saturday etc. * * @return array */ @@ -884,7 +883,7 @@ public static function antitheftRuleCreate(DateTime $startTime, DateTime $endTim * @param DateTime $startTime The start date/time for the event to begin * @param DateTime $endTime The end date/time for the event to finish. * @param string $name An event name. On some clients this isn't even seen. - * @param array $daysOfWeekToRepeat (Optional). An array of days of the week this event should repeat. Use normal english like Tues, Saturday etc. + * @param array $daysOfWeekToRepeat (Optional). Days of week event should repeat. Use EN like Tues, Saturday etc. * * @return array */ @@ -1035,7 +1034,7 @@ protected static function calculateMinutes(DateTime $dateAndTime) * @param DateTime $dateAndTime The actual Date and Time for this event. * @param bool $turnOn Should the event turn on or off the timer. * @param string $name An event name. On some clients this isn't even seen. - * @param array $daysOfWeekToRepeat (Optional). An array of days of the week this event should repeat. Use normal english like Tues, Saturday etc. + * @param array $daysOfWeekToRepeat (Optional) Day of week event should repeat. Use EN like Tues, Saturday etc. * @param Collection $data specific information depending on if the event is repeating or not. * @param string $ruleId The ID of the rule to be edited. * @@ -1103,7 +1102,7 @@ protected static function countdownCommonData($type, $delay, $turnOn, $name, $ru * @param DateTime $startTime The start date/time for the event to begin * @param DateTime $endTime The end date/time for the event to finish. * @param string $name An event name. On some clients this isn't even seen. - * @param array $daysOfWeekToRepeat (Optional). An array of days of the week this event should repeat. Use normal english like Tues, Saturday etc. + * @param array $daysOfWeekToRepeat (Optional) Day of week event should repeat. Use EN like Tues, Saturday etc. * @param Collection $data specific information depending on if the event is repeating or not. * @param string $ruleId The ID of the rule to be edited. * @@ -1144,4 +1143,4 @@ protected static function antitheftCommonData( ], ]; } -} \ No newline at end of file +} diff --git a/src/TPLinkDevice.php b/src/TPLinkDevice.php index a63fdbe..ee9f052 100644 --- a/src/TPLinkDevice.php +++ b/src/TPLinkDevice.php @@ -24,6 +24,16 @@ public function __construct(array $config, $deviceName) $this->deviceName = $deviceName; } + /** + * Return current power status + * + * @return boolean + */ + public function powerStatus() + { + return (bool)json_decode($this->sendCommand(TPLinkCommand::systemInfo()))->system->get_sysinfo->relay_state; + } + /** * Toggle the current status of the switch on/off * @@ -31,9 +41,27 @@ public function __construct(array $config, $deviceName) */ public function togglePower() { - $status = (bool)json_decode($this->sendCommand(TPLinkCommand::systemInfo()))->system->get_sysinfo->relay_state; + return $this->powerStatus() ? $this->sendCommand(TPLinkCommand::powerOff()) : $this->sendCommand(TPLinkCommand::powerOn()); + } - return $status ? $this->sendCommand(TPLinkCommand::powerOff()) : $this->sendCommand(TPLinkCommand::powerOn()); + /** + * Change the current status of the switch to on + * + * @return string + */ + public function powerOn() + { + return $this->sendCommand(TPLinkCommand::powerOn()); + } + + /** + * Change the current status of the switch off + * + * @return string + */ + public function powerOff() + { + return $this->sendCommand(TPLinkCommand::powerOff()); } /** @@ -66,11 +94,11 @@ protected function connectToDevice() "tcp://" . $this->getConfig("ip") . ":" . $this->getConfig("port"), $errorNumber, $errorMessage, - 5 + $this->getConfig('timeout', 5) ); if ($this->client === false) { - throw new UnexpectedValueException("Failed to connect to {$this->deviceName}: $errorMessage ($errorNumber)"); + throw new UnexpectedValueException("Failed connect to {$this->deviceName}: $errorMessage ($errorNumber)"); } } @@ -80,7 +108,7 @@ protected function connectToDevice() * * @return mixed */ - protected function getConfig($key, $default = null) + public function getConfig($key, $default = null) { if (is_array($this->config) && isset($this->config[$key])) { return $this->config[$key]; @@ -101,12 +129,14 @@ protected function encrypt($string) $key = 171; return collect(str_split($string)) - ->reduce(function ($result, $character) use (&$key) { - $key = $key ^ ord($character); - - return $result .= chr($key); - }, - "\0\0\0\0"); + ->reduce( + function ($result, $character) use (&$key) { + $key = $key ^ ord($character); + + return $result .= chr($key); + }, + "\0\0\0\0" + ); } /** @@ -117,7 +147,7 @@ protected function connectionError() { return json_encode([ 'success' => false, - 'message' => "When sending the command to the smartplug {$this->deviceName}, the connection terminated before the command was sent.", + 'message' => "{$this->deviceName} : connection terminated before the command was sent.", ]); } diff --git a/src/TPLinkManager.php b/src/TPLinkManager.php index 6410084..3c624c7 100644 --- a/src/TPLinkManager.php +++ b/src/TPLinkManager.php @@ -3,13 +3,15 @@ namespace Williamson\TPLinkSmartplug; use InvalidArgumentException; +use IPTools\Range; +use Tightenco\Collect\Support\Collection; class TPLinkManager { protected $config; protected $devices; - public function __construct(array $config) + public function __construct(array $config = []) { $this->config = $config; } @@ -20,11 +22,96 @@ public function device($name = 'default') throw new InvalidArgumentException('You have not setup the details for a device named ' . $name); } - return $this->newTPLinkDevice($this->config[$name], $name); + return new TPLinkDevice($this->config[$name], $name); } protected function newTPLinkDevice($config, $name) { + $this->config[$name] = $config; return new TPLinkDevice($config, $name); } -} \ No newline at end of file + + public function deviceList() + { + return $this->config; + } + + /** + * Will return a collection of all TPLink devices auto discovered + * on the IP Range given. + * + * These will already have been added to the global config during + * discovery. + * + * @param $ipRange + * @param int $timeout + * + * @return Collection + */ + public function autoDiscoverTPLinkDevices($ipRange, $timeout = 1) + { + return collect(Range::parse($ipRange)) + ->map(function ($ip) use ($timeout) { + $response = $this->deviceResponse($ip, $timeout); + + return is_null($response) ? $response : $this->validTPLinkResponse($response, $ip); + }) + ->filter(); + } + + /** + * Try sending systemInfo command to an ip. + * Possible we may get a blank response, if querying another device which uses these ports + * + * @param $ip + * @param $timeout + * + * @return null + */ + protected function deviceResponse($ip, $timeout) + { + try { + $device = new TPLinkDevice(['ip' => $ip, 'port' => 9999, 'timeout' => $timeout], 'autodiscovery'); + + return $device->sendCommand(TPLinkCommand::systemInfo()); + } catch (\Exception $exception) { + return null; + } + } + + /** + * Check the returned data JSON decodes + * Make sure is not NULL, some devices may return a single character + * LB100 Series seems to respond on port 9999, however return a bad string + * TODO:: investigate LB100 support + * + * @param $response + * @param $ip + * + * @return mixed|TPLinkDevice + */ + protected function validTPLinkResponse($response, $ip) + { + $jsonResponse = json_decode($response); + + return is_null($jsonResponse) ? $jsonResponse : $this->discoveredDevice($jsonResponse, $ip); + } + + /** + * Create a new discovered device instance and update config to contain new device + * + * @param $jsonResponse + * @param $ip + * + * @return TPLinkDevice + */ + protected function discoveredDevice($jsonResponse, $ip) + { + return $this->newTPLinkDevice([ + 'ip' => (string)$ip, + 'port' => 9999, + 'systemInfo' => $jsonResponse->system->get_sysinfo, + ], $jsonResponse->system->get_sysinfo->alias); + } + +}