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/.travis.yml b/.travis.yml index 1410252..dd27797 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: php php: + - 7.2 - 7.1 - 7.0 - 5.6 diff --git a/README.md b/README.md index 89c4835..c549bea 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -PHP Library to Control and Access a TP-Link Smartplug! +PHP Library to Control and Access a TP-Link Smartplug OR TP Smart Bulbs! =========

Smart Plug
@@ -29,9 +29,9 @@ composer require williamson/tplinksmartplug ### Laravel Installation/Integration -This library supports Laravel's auto discovery feature for auto registering the service provider and facade. If your version of Laravel supports auto discovery, after you have added this package YOU ARE NOW DONE! +###### Now Supports Laravel 5.5 auto package discovery (you do not need to do the below step if you have Laravel 5.5+) -If you are using a very old version of Laravel, once this package is installed, you need to register the package's service provider, in `config/app.php`: +Once the TPLink Smartplug library is installed, you need to register the library's service provider, in `config/app.php`: ```php 'providers' => [ @@ -41,13 +41,7 @@ If you are using a very old version of Laravel, once this package is installed, ``` ##### Facades -Only if your version of Laravel does NOT support auto discovery then add the following to the aliases section of 'app.php'. -```php -'aliases' => [ - //... - "TPLink" => Williamson\TPLinkSmartplug\Laravel\Facades\TPLinkFacade::class -] -``` +By default, this library will *automatically* register a facade to be used in Laravel. The package checks first to ensure `TPLink` has not already be registered and if this is the case, will register `TPLink` as your quick access to the library. More examples to follow. ##### Config file This package requires a config file so that you can provide the address/details of the TPLink devices you would like to control. To generate this file, run the following command: @@ -65,13 +59,17 @@ The config file is a very simple array structured file. A config file is require [ - 'ip' => '192.168.1.100', //Or hostname eg: home.example.com + '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) + 'timeout_stream' => 5, // Optional, timeout setting for stream (how long to wait for the response from the device) ], ]; ``` -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) + +If you do not know the IP address of your devices, you can use the `autoDiscoverTPLinkDevices` method to automatically scan and find devices on your network for you to save into your config file. ## Usage You can access your device either through the `TPLinkManager` class (especially useful if you have multiple devices), or directly using the `TPLinkDevice` class. @@ -138,10 +136,53 @@ If a command requires a parameter, provide that as well: $tpDevice->sendCommand(TPLinkCommand::setLED(false)); ``` -####Toggle Power -There is one command that is called directly on the `TPLinkDevice` and that is the `togglePower()` method. +#### 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()`. + +However this information will not automatically be persisted. You must save the details into your config file if you wish to avoid having to keep scanning your network. + +You must provide the IP range you wish to scan, examples of usage are as follows: +```php +//Non laravel + $tpLinkManager->autoDiscoverTPLinkDevices('192.168.0.*'); + $tpLinkManager->autoDiscoverTPLinkDevices('192.168.0.10-192.168.0.40'); + +//Laravel + // with facade + TPLink::autoDiscoverTPLinkDevices('192.168.0.*'); + TPLink::autoDiscoverTPLinkDevices('192.168.0.10-192.168.0.40'); + + // without facade + app('tplink')->autoDiscoverTPLinkDevices('192.168.0.*'); + app('tplink')->autoDiscoverTPLinkDevices('192.168.0.10-192.168.0.40'); + + app(TPLinkManager::class)->autoDiscoverTPLinkDevices('192.168.0.*'); + app(TPLinkManager::class)->autoDiscoverTPLinkDevices('192.168.0.10-192.168.0.40'); +``` + +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 are a few convenience commands that can be called directly on the `TPLinkDevice`. Those methods are + `togglePower()` + `powerOn()` + `powerOff()` -If you only wish to toggle the current power state of the plug, use it as follows: +Eg. if you only wish to toggle the current power state of the plug, use it as follows: ```php //Non laravel @@ -224,6 +265,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) Various features such as Auto-Discovery ## Disclaimer @@ -234,7 +276,7 @@ See License section for more details. This project is released under the [MIT][link-license] License. -© 2017 [Jonathan Williamson][link-author], All rights reserved. +© 2019 [Jonathan Williamson][link-author], All rights reserved. [link-author]: https://github.com/jonnywilliamson [link-repo]: https://github.com/jonnywilliamson/tplinksmartplug diff --git a/composer.json b/composer.json index c3922f6..79c0476 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "williamson/tplinksmartplug", "description": "A PHP library to control and receive information from a TP-Link smartplug.", "license": "MIT", - "keywords": ["TPLink", "PHP", "HS110", "HS100", "Timer", "SmartPlug", "Laravel"], + "keywords": ["TPLink", "PHP", "HS110", "HS100", "Timer", "SmartPlug", "Laravel", "SmartBulbs", "Bulbs"], "authors": [ { "name": "Jonathan Williamson", @@ -10,7 +10,8 @@ } ], "require": { - "tightenco/collect": "^5.3" + "illuminate/collections": ">5.0", + "s1lentium/iptools": "^1.1" }, "require-dev": { "phpunit/phpunit": "^5.7" diff --git a/src/Laravel/TPLinkServiceProvider.php b/src/Laravel/TPLinkServiceProvider.php index f70ef04..372afc0 100644 --- a/src/Laravel/TPLinkServiceProvider.php +++ b/src/Laravel/TPLinkServiceProvider.php @@ -2,19 +2,14 @@ namespace Williamson\TPLinkSmartplug\Laravel; +use Illuminate\Foundation\AliasLoader; use Illuminate\Support\ServiceProvider; use Williamson\TPLinkSmartplug\TPLinkManager; +use Williamson\TPLinkSmartplug\Laravel\Facades\TPLinkFacade; class TPLinkServiceProvider extends ServiceProvider { - /** - * Indicates if loading of the provider is deferred. - * - * @var bool - */ - protected $defer = true; - /** * Bootstrap the application services. * @@ -37,15 +32,15 @@ public function register() }); $this->app->alias(TPLinkManager::class, 'tplink'); - } - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return [TPLinkManager::class]; + // Auto-register the TPLink facade if the user hasn't already + // assigned it to another class. Takes care of Laravel <5.5 users. + if (class_exists(AliasLoader::class)) { + $loader = AliasLoader::getInstance(); + + if (!array_key_exists('TPLink', $loader->getAliases())) { + $loader->alias('TPLink', TPLinkFacade::class); + } + } } } diff --git a/src/Laravel/config/TPLink.php b/src/Laravel/config/TPLink.php index 7d9cb2b..dbc476b 100644 --- a/src/Laravel/config/TPLink.php +++ b/src/Laravel/config/TPLink.php @@ -1,18 +1,27 @@ [ - 'ip' => '192.168.1.100', //Or hostname eg: home.example.com - 'port' => '9999', + '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) + 'timeout_stream' => 3, // Optional, timeout setting for stream (how long to wait for the response from the device) + 'deviceType' => 'IOT.SMARTPLUGSWITCH', // Smart Bulbs are also supported: 'IOT.SMARTBULB' ], // 'bedroom' => [ -// 'ip' => '192.168.1.100', //Or hostname -// 'port' => '9999', +// 'ip' => '192.168.1.101', +// 'port' => '9999', +// 'timeout' => 5, +// 'timeout_stream' => 3, +// 'deviceType' => 'IOT.SMARTPLUGSWITCH', // ], // 'livingroom' => [ -// 'ip' => '192.168.1.101', //Or hostname -// 'port' => '9999', +// 'ip' => '192.168.1.102', +// 'port' => '9999', +// 'timeout' => 10, +// 'timeout_stream' => 3, +// 'deviceType' => 'IOT.SMARTBULB', // ], ]; \ No newline at end of file diff --git a/src/TPLinkCommand.php b/src/TPLinkCommand.php index f76e407..6199ff2 100644 --- a/src/TPLinkCommand.php +++ b/src/TPLinkCommand.php @@ -4,10 +4,10 @@ use DateTime; use stdClass; +use Exception; use InvalidArgumentException; use Illuminate\Support\Collection; - /** * Class TPLinkCommands * @@ -114,7 +114,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 +241,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 +402,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 +662,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 */ @@ -683,6 +683,144 @@ public static function scheduleRuleCreate(DateTime $dateAndTime, $turnOn, $name, ]; } + /** + * Depending on if the event is to be repeated during the week or not return the + * data needed to create the event. + * + * @param DateTime $dateAndTime + * @param $weekDaysToRepeat + * + * @return Collection + */ + protected static function formatDates(DateTime $dateAndTime, $weekDaysToRepeat) + { + if (empty($weekDaysToRepeat)) { + $data = collect([ + 'year' => $dateAndTime->format('Y'), + 'month' => $dateAndTime->format('n'), + 'day' => $dateAndTime->format('j'), + 'wday' => self::createDayMatrix($dateAndTime), + ]); + } else { + $data = collect(['wday' => self::createRepeatingDayMatrix($weekDaysToRepeat)]); + } + + return $data; + } + + /** + * Create the array/matrix required for single events + * + * @param DateTime $dateAndTime + * + * @return array + */ + protected static function createDayMatrix(DateTime $dateAndTime) + { + $weekMatrix = self::emptyMatrix(); + $weekMatrix[$dateAndTime->format('w')] = 1; + + return $weekMatrix; + } + + /** + * Create an empty matrix. + * + * @return array + */ + protected static function emptyMatrix() + { + return [0, 0, 0, 0, 0, 0, 0]; + } + + /** + * Create the array/matrix required for repeating/reoccuring events + * + * @param array $daysToReoccur + * + * @return array + */ + protected static function createRepeatingDayMatrix(array $daysToReoccur) + { + $weekMatrix = self::emptyMatrix(); + + foreach ($daysToReoccur as $dayString) { + try { + $weekMatrix[(new DateTime($dayString))->format('w')] = 1; + } catch (Exception $e) { + throw new InvalidArgumentException("Invalid date string provided. {$e->getMessage()}"); + } + } + + return $weekMatrix; + } + + /** + * @param string $type The type of action that should be performed, add or edit. + * @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) 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. + * + * @return array + */ + protected static function ruleCommonData( + $type, + DateTime $dateAndTime, + $turnOn, + $name, + $daysOfWeekToRepeat, + $data, + $ruleId + ) { + return [ + $type => [ + 'id' => $ruleId, + 'enable' => 1, + 'name' => "$name", + 'sact' => (int)$turnOn, + 'repeat' => (int)!empty($daysOfWeekToRepeat), + 'smin' => self::calculateMinutes($dateAndTime), + 'emin' => 0, + 'wday' => (array)$data->get('wday'), + 'day' => (int)$data->get('day', 0), + 'month' => (int)$data->get('month', 0), + 'year' => (int)$data->get('year', 0), + 'etime_opt' => -1, + 'eact' => -1, + 'stime_opt' => 0, + 'force' => 0, + 'longitude' => 0, + 'latitude' => 0, + ], + 'set_overall_enable' => [ + 'enable' => 1, + ], + ]; + } + + /** + * All start/end times on the device are recorded as minutes from midnight. + * + * Return the required minute value from the supplied DateTime object + * + * @param DateTime $dateAndTime + * + * @return int + */ + protected static function calculateMinutes(DateTime $dateAndTime) + { + $datetime2 = clone $dateAndTime; + $datetime2->setTime(00, 00, 00); + + $interval = $dateAndTime->diff($datetime2); + + //Timer needs minutes since midnight + return ($interval->h * 60 + $interval->i); + } + /** * Edit Schedule Rule with given ID * @@ -690,7 +828,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 */ @@ -787,6 +925,28 @@ public static function countdownRuleCreate($delay, $turnOn, $name = 'countdown') ]; } + /** + * @param string $type The type of action that should be performed, add or edit. + * @param int $delay The number of secs until the event should fire. + * @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 string $ruleId The id of the rule to edit. + * + * @return array + */ + protected static function countdownCommonData($type, $delay, $turnOn, $name, $ruleId) + { + return [ + $type => [ + 'id' => $ruleId, + 'enable' => 1, + 'delay' => (int)$delay, + 'act' => (int)$turnOn, + 'name' => $name, + ], + ]; + } + /** * Edit Countdown Rule with specified ID * @@ -856,7 +1016,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 */ @@ -877,6 +1037,53 @@ public static function antitheftRuleCreate(DateTime $startTime, DateTime $endTim ]; } + /** + * @param string $type The type of action that should be performed, add or edit. + * @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) 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. + * + * @return array + */ + protected static function antitheftCommonData( + $type, + DateTime $startTime, + DateTime $endTime, + $name, + $daysOfWeekToRepeat, + $data, + $ruleId + ) { + return [ + $type => [ + 'id' => $ruleId, + 'enable' => 1, + 'frequency' => 5, + 'name' => "$name", + 'repeat' => (int)!empty($daysOfWeekToRepeat), + 'smin' => self::calculateMinutes($startTime), + 'emin' => self::calculateMinutes($endTime), + 'wday' => (array)$data->get('wday'), + 'day' => (int)$data->get('day', 0), + 'month' => (int)$data->get('month', 0), + 'year' => (int)$data->get('year', 0), + 'stime_opt' => 0, + 'etime_opt' => 0, + 'duration' => 2, + 'lastfor' => 1, + 'force' => 0, + 'longitude' => 0, + 'latitude' => 0, + ], + 'set_overall_enable' => [ + 'enable' => 1, + ], + ]; + } + /** * Edit Anti theft Rule with given ID * @@ -884,7 +1091,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 */ @@ -943,205 +1150,62 @@ public static function antitheftRuleWipeAll() } /** - * Depending on if the event is to be repeated during the week or not return the - * data needed to create the event. - * - * @param DateTime $dateAndTime - * @param $weekDaysToRepeat - * - * @return Collection - */ - protected static function formatDates(DateTime $dateAndTime, $weekDaysToRepeat) - { - if (empty($weekDaysToRepeat)) { - $data = collect([ - 'year' => $dateAndTime->format('Y'), - 'month' => $dateAndTime->format('n'), - 'day' => $dateAndTime->format('j'), - 'wday' => self::createDayMatrix($dateAndTime), - ]); - } else { - $data = collect(['wday' => self::createRepeatingDayMatrix($weekDaysToRepeat)]); - } - - return $data; - } - - /** - * Create the array/matrix required for single events - * - * @param DateTime $dateAndTime + * @param int $brightness + * @param int $transPeriod + * @param int $hue + * @param int $saturation + * @param int $color_temp + * @param string $mode * * @return array */ - protected static function createDayMatrix(DateTime $dateAndTime) - { - $weekMatrix = self::emptyMatrix(); - $weekMatrix[$dateAndTime->format('w')] = 1; - - return $weekMatrix; - } - - /** - * Create an empty matrix. - * - * @return array - */ - protected static function emptyMatrix() - { - return [0, 0, 0, 0, 0, 0, 0]; - } - - /** - * Create the array/matrix required for repeating/reoccuring events - * - * @param array $daysToReoccur - * - * @return array - */ - protected static function createRepeatingDayMatrix(array $daysToReoccur) - { - $weekMatrix = self::emptyMatrix(); - - foreach ($daysToReoccur as $dayString) { - $weekMatrix[(new DateTime($dayString))->format('w')] = 1; - } - - return $weekMatrix; - } - - /** - * All start/end times on the device are recorded as minutes from midnight. - * - * Return the required minute value from the supplied DateTime object - * - * @param DateTime $dateAndTime - * - * @return int - */ - protected static function calculateMinutes(DateTime $dateAndTime) - { - $datetime2 = clone $dateAndTime; - $datetime2->setTime(00, 00, 00); - - $interval = $dateAndTime->diff($datetime2); - - //Timer needs minutes since midnight - return ($interval->h * 60 + $interval->i); - } - - /** - * @param string $type The type of action that should be performed, add or edit. - * @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 Collection $data specific information depending on if the event is repeating or not. - * @param string $ruleId The ID of the rule to be edited. - * - * @return array - */ - protected static function ruleCommonData( - $type, - DateTime $dateAndTime, - $turnOn, - $name, - $daysOfWeekToRepeat, - $data, - $ruleId + public static function lightControlValues( + $brightness = 100, + $transPeriod = 100, + $hue = 120, + $saturation = 150, + $color_temp = 2700, + $mode = 'normal' ) { return [ - $type => [ - 'id' => $ruleId, - 'enable' => 1, - 'name' => "$name", - 'sact' => (int)$turnOn, - 'repeat' => (int)!empty($daysOfWeekToRepeat), - 'smin' => self::calculateMinutes($dateAndTime), - 'emin' => 0, - 'wday' => (array)$data->get('wday'), - 'day' => (int)$data->get('day', 0), - 'month' => (int)$data->get('month', 0), - 'year' => (int)$data->get('year', 0), - 'etime_opt' => -1, - 'eact' => -1, - 'stime_opt' => 0, - 'force' => 0, - 'longitude' => 0, - 'latitude' => 0, - ], - 'set_overall_enable' => [ - 'enable' => 1, - ], + "transition_period" => $transPeriod, + "mode" => $mode, + "hue" => $hue, + "saturation" => $saturation, + "color_temp" => $color_temp, + "brightness" => $brightness, ]; } /** - * @param string $type The type of action that should be performed, add or edit. - * @param int $delay The number of secs until the event should fire. - * @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 string $ruleId The id of the rule to edit. + * @param array $params * * @return array */ - protected static function countdownCommonData($type, $delay, $turnOn, $name, $ruleId) + public static function lightOn($params = []) { + $cmd = array_merge(["ignore_default" => 1, "on_off" => 1], $params); + return [ - $type => [ - 'id' => $ruleId, - 'enable' => 1, - 'delay' => (int)$delay, - 'act' => (int)$turnOn, - 'name' => $name, + 'smartlife.iot.smartbulb.lightingservice' => [ + 'transition_light_state' => $cmd, ], ]; } /** - * @param string $type The type of action that should be performed, add or edit. - * @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 Collection $data specific information depending on if the event is repeating or not. - * @param string $ruleId The ID of the rule to be edited. - * * @return array */ - protected static function antitheftCommonData( - $type, - DateTime $startTime, - DateTime $endTime, - $name, - $daysOfWeekToRepeat, - $data, - $ruleId - ) { + public static function lightOff() + { return [ - $type => [ - 'id' => $ruleId, - 'enable' => 1, - 'frequency' => 5, - 'name' => "$name", - 'repeat' => (int)!empty($daysOfWeekToRepeat), - 'smin' => self::calculateMinutes($startTime), - 'emin' => self::calculateMinutes($endTime), - 'wday' => (array)$data->get('wday'), - 'day' => (int)$data->get('day', 0), - 'month' => (int)$data->get('month', 0), - 'year' => (int)$data->get('year', 0), - 'stime_opt' => 0, - 'etime_opt' => 0, - 'duration' => 2, - 'lastfor' => 1, - 'force' => 0, - 'longitude' => 0, - 'latitude' => 0, - ], - 'set_overall_enable' => [ - 'enable' => 1, + 'smartlife.iot.smartbulb.lightingservice' => [ + 'transition_light_state' => [ + "ignore_default" => 1, + "on_off" => 0, + ], ], ]; } -} \ No newline at end of file + +} diff --git a/src/TPLinkDevice.php b/src/TPLinkDevice.php index a63fdbe..439211f 100644 --- a/src/TPLinkDevice.php +++ b/src/TPLinkDevice.php @@ -10,6 +10,8 @@ class TPLinkDevice protected $config; protected $deviceName; protected $client; + protected $encryptionKey; + protected $deviceType; /** @@ -17,11 +19,14 @@ class TPLinkDevice * * @param array $config * @param string $deviceName + * @param int $encryptionKey */ - public function __construct(array $config, $deviceName) + public function __construct(array $config, $deviceName, $encryptionKey = 171) { $this->config = $config; $this->deviceName = $deviceName; + $this->deviceType = isset($config['deviceType']) ? $config['deviceType'] : "IOT.SMARTPLUGSWITCH"; + $this->encryptionKey = $encryptionKey; } /** @@ -31,9 +36,72 @@ 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->powerOff() : $this->powerOn(); + } + + + /** + * Change the current status of the switch off + * + * @return string + */ + public function powerOff() + { + if ($this->deviceType() == "IOT.SMARTBULB") { + return $this->sendCommand(TPLinkCommand::lightOff()); + } + + if ($this->deviceType() == "IOT.SMARTPLUGSWITCH") { + return $this->sendCommand(TPLinkCommand::powerOff()); + } + + return ''; + } + + /** + * Change the current status of the switch to on + * + * @return string + */ + public function powerOn() + { + if ($this->deviceType() == "IOT.SMARTBULB") { + return $this->sendCommand(TPLinkCommand::lightOn()); + } + + if ($this->deviceType() == "IOT.SMARTPLUGSWITCH") { + return $this->sendCommand(TPLinkCommand::powerOn()); + } - return $status ? $this->sendCommand(TPLinkCommand::powerOff()) : $this->sendCommand(TPLinkCommand::powerOn()); + return ''; + } + + /** + * Return current power status + * + * @return boolean + */ + public function powerStatus() + { + if ($this->deviceType() == "IOT.SMARTBULB") { + return (bool)json_decode($this->sendCommand(TPLinkCommand::systemInfo()))->system->get_sysinfo->light_state->on_off; + } + + if ($this->deviceType() == "IOT.SMARTPLUGSWITCH") { + return (bool)json_decode($this->sendCommand(TPLinkCommand::systemInfo()))->system->get_sysinfo->relay_state; + } + + return false; + } + + /** + * What type of device is this? + * + * @return string + */ + public function deviceType() + { + return $this->deviceType; } /** @@ -52,6 +120,7 @@ public function sendCommand(array $command) } $response = $this->decrypt(stream_get_contents($this->client)); + $this->disconnect(); return $response; @@ -66,11 +135,14 @@ protected function connectToDevice() "tcp://" . $this->getConfig("ip") . ":" . $this->getConfig("port"), $errorNumber, $errorMessage, - 5 + $this->getConfig('timeout', 5) ); + // Set stream timeout (important or some devices will cause the stream read function to hang for a period) + stream_set_timeout($this->client, $this->getConfig('timeout_stream', 1)); + 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 +152,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]; @@ -98,15 +170,17 @@ protected function getConfig($key, $default = null) */ protected function encrypt($string) { - $key = 171; + $key = $this->encryptionKey; 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 = ord($character) ^ $key; + + return $result .= chr($key); + }, + strrev(pack('I', strlen($string))) + ); } /** @@ -117,7 +191,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.", ]); } @@ -126,17 +200,19 @@ protected function connectionError() * * Must ignore the first 4 bytes of the response to decrypt properly. * - * @param $data + * @param $data + * + * @param bool $stripHeader * * @return mixed */ - protected function decrypt($data) + protected function decrypt($data, $stripHeader = true) { - $key = 171; + $key = $this->encryptionKey; - return collect(str_split(substr($data, 4))) + return collect(str_split(substr($data, ($stripHeader) ? 4 : 0))) ->reduce(function ($result, $character) use (&$key) { - $a = $key ^ ord($character); + $a = ord($character) ^ $key; $key = ord($character); return $result .= chr($a); diff --git a/src/TPLinkManager.php b/src/TPLinkManager.php index 283930d..e53b7f8 100644 --- a/src/TPLinkManager.php +++ b/src/TPLinkManager.php @@ -2,6 +2,9 @@ namespace Williamson\TPLinkSmartplug; +use Exception; +use Illuminate\Support\Collection; +use IPTools\Range; use InvalidArgumentException; class TPLinkManager @@ -9,22 +12,182 @@ class TPLinkManager protected $config; protected $devices; - public function __construct(array $config) + public function __construct(array $config = []) { $this->config = $config; } + /** + * Get a device object pre-configured in config + * + * @param string $name + * + * @return TPLinkDevice + */ public function device($name = 'default') { if (!isset($this->config[$name]) || !is_array($this->config[$name]) || empty($this->config[$name]['ip'])) { throw new InvalidArgumentException('You have not setup all 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) + /** + * Return current config + * + * @return array + */ + 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 + * @param int $timeoutStream + * @param null $callbackFunction + * @param int $maxDiscoveredDevices + * + * @return Collection + */ + public function autoDiscoverTPLinkDevices( + $ipRange, + $timeout = 1, + $timeoutStream = 1, + $callbackFunction = null, + $maxDiscoveredDevices = 0 + ) { + $discoveredCount = 0; + + return collect(Range::parse($ipRange)) + ->map(function ($ip) use ( + $timeout, + $timeoutStream, + $callbackFunction, + $maxDiscoveredDevices, + &$discoveredCount + ) { + // Discovered max devices, do not do checks for further devices + if ($maxDiscoveredDevices > 0 && $discoveredCount >= $maxDiscoveredDevices) { + return null; + } + + $response = $this->deviceResponse((string)$ip, $timeout, $timeoutStream); + $device = $this->validTPLinkResponse($response, (string)$ip, $callbackFunction); + + if ($device) { + $discoveredCount++; + + return $device; + } + + return $response; + }) + ->filter() + ->values(); + } + + /** + * 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 + * @param $timeoutStream + * + * @return null + */ + protected function deviceResponse($ip, $timeout, $timeoutStream) + { + try { + $device = new TPLinkDevice([ + 'ip' => $ip, + 'port' => 9999, + 'timeout' => $timeout, + 'timeout_stream' => $timeoutStream, + ], '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 + * + * @param $response + * @param $ip + * @param null $callbackFunction + * + * @return mixed|TPLinkDevice + */ + protected function validTPLinkResponse($response, $ip, $callbackFunction = null) + { + $jsonResponse = json_decode($response); + + return is_null($jsonResponse) ? $jsonResponse : $this->discoveredDevice($jsonResponse, $ip, $callbackFunction); + } + + /** + * Create a new discovered device instance and update config to contain new device + * + * @param $jsonResponse + * @param $ip + * @param null $callbackFunction + * + * @return TPLinkDevice + */ + protected function discoveredDevice($jsonResponse, $ip, $callbackFunction = null) + { + $device = $this->newDevice( + [ + 'ip' => (string)$ip, + 'port' => 9999, + 'systemInfo' => $jsonResponse->system->get_sysinfo, + 'deviceType' => $this->detectDeviceType($jsonResponse), + ], + $this->detectDeviceName($jsonResponse)); + + // Callback function during discovery + if ($callbackFunction) { + call_user_func($callbackFunction, $device); + } + + return $device; + } + + /** + * Add a new device to the config setup + * + * @param $config + * @param $name + * + * @return TPLinkDevice + */ + public function newDevice($config, $name) + { + $this->config[$name] = $config; + return new TPLinkDevice($config, $name); } + + private function detectDeviceType($jsonResponse) + { + return (isset($jsonResponse->system->get_sysinfo->type)) ? $jsonResponse->system->get_sysinfo->type : $jsonResponse->system->get_sysinfo->mic_type; + } + + private function detectDeviceName($jsonResponse) + { + return $jsonResponse->system->get_sysinfo->alias; + } } \ No newline at end of file