├── .github └── workflows │ ├── needs-reply-remove.yml │ └── needs-reply.yml ├── LICENSE ├── README.md └── TuyaCloud.php /.github/workflows/needs-reply-remove.yml: -------------------------------------------------------------------------------- 1 | name: Remove needs-reply label 2 | 3 | on: 4 | issue_comment: 5 | types: 6 | - created 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Remove needs-reply label 13 | uses: octokit/request-action@v2.x 14 | continue-on-error: true 15 | with: 16 | route: DELETE /repos/:repository/issues/:issue/labels/:label 17 | repository: ${{ github.repository }} 18 | issue: ${{ github.event.issue.number }} 19 | label: waiting-for-requestor 20 | days-before-close: 3 21 | close-message: This issue has been automatically closed because the requestor didn't provide any additional comment. 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/needs-reply.yml: -------------------------------------------------------------------------------- 1 | name: Close old issues that need reply 2 | 3 | on: 4 | issues: 5 | types: 6 | - labeled 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Close old issues that need reply 13 | uses: dwieeb/needs-reply@v2 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | issue-label: waiting-for-requestor 17 | days-before-close: 1 18 | close-message: This issue has been automatically closed because the requestor didn't provide any additional comment. 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Aymeric 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tuyacloud-php 2 | 3 | PHP Library to access to the SmartLife / Tuya objects using the Tuya Cloud API. 4 | 5 | Note: you need a Smartlife Account created with an email, not with Google Connection or similar third party. You could also create a new account from email and then invite this account to the "family" of your main Smartlife Account. 6 | 7 | ## Required 8 | 9 | A developer account must be created on the [Tuya platform](https://eu.platform.tuya.com/). I'm not sure how the details, but from the menu, access to [`Cloud` > `Development`](https://eu.platform.tuya.com/cloud/) and create a cloud project. 10 | 11 | Once the project is created, it will show you the **access key** and **access secret**. 12 | 13 | Under the tab `devices`, you can tie the project with your Tuya/SmartLife app by scanning a QR Code. If it worked, you will see all your devices listed on this page (with their `device_id`). 14 | 15 | You need to know which region/server you're using, and then you have to use the correct URL: 16 | - China Data Center: https://openapi.tuyacn.com 17 | - Western America Data Center: https://openapi.tuyaus.com 18 | - Central Europe Data Center: https://openapi.tuyaeu.com 19 | - India Data Center: https://openapi.tuyain.com 20 | 21 | 22 | ## Usage 23 | 24 | ```php 25 | 'https://openapi.tuyaeu.com', // URL API of Tuya 30 | 'accessKey' => 'nhepe4mrrtz8wju45mk3', // access key of your app 31 | 'secretKey' => 'sf94ryyrfvg3awvg4174m88wjpksytre', // access secret of your app 32 | ]; 33 | 34 | $tuya = new TuyaCloud($options); 35 | try { 36 | // to get the device status 37 | // you must pass the device_id 38 | $response = $tuya->getDevice('bfa18afnfyre87eb7ne0'); 39 | echo '
';
40 |   print_r($response);
41 |   echo '
'; 42 | 43 | // to send a command 44 | // you can pass a JSON string: '{"commands":[{"code":"switch_led","value":true}]}' 45 | // or a strClass object 46 | // or an array like the below one: 47 | $commands = [ 48 | "commands" => [ 49 | [ 50 | "code" => "switch_led", 51 | "value" => true 52 | ] 53 | ] 54 | ]; 55 | $response = $tuya->setDevice('bfa18afnfyre87eb7ne0', $commands); 56 | 57 | // we can retrieve all the scenes (including their id) 58 | $response = $tuya->getScenes(); 59 | echo '
';
60 |   print_r($response);
61 |   echo '
'; 62 | 63 | // and we can start a scene 64 | $response = $tuya->startScene('the_scene_id'); 65 | } catch (Exception $e) { 66 | echo 'Error: ' . $e->getMessage(); 67 | } 68 | ?> 69 | ``` 70 | 71 | The different commands can be foundby going to your project in the [Tuya Cloud Development platform](https://eu.platform.tuya.com/cloud/), then click on the device and you can find the options. 72 | 73 | Examples: 74 | - a curtain can have the `command` `open`, `close`, or `stop` 75 | - a light will have `switch_led` with `true` or `false` 76 | - a power strip with 2 outlets will have `switch_1` with `true` or `false`, and the same for `switch_2` 77 | -------------------------------------------------------------------------------- /TuyaCloud.php: -------------------------------------------------------------------------------- 1 | baseUrl = $options['baseUrl']; 10 | $this->accessKey = $options['accessKey']; 11 | $this->secretKey = $options['secretKey']; 12 | } 13 | 14 | // Function to sign the request 15 | private function signRequest($method, $path, $timestamp, $accessToken = '', $body = '') { 16 | $ctxHash = hash_init('sha256'); 17 | hash_update($ctxHash, $body); 18 | $contentHash = bin2hex(hash_final($ctxHash, true)); 19 | 20 | // build the stringToSign 21 | $stringToSign = strtoupper($method) . "\n" . $contentHash . "\n\n" . urldecode($path); 22 | 23 | // build signStr with accessKey, accessToken and timestamp 24 | $signStr = $this->accessKey . $accessToken . $timestamp . $stringToSign; 25 | 26 | return strtoupper(hash_hmac('sha256', $signStr, $this->secretKey)); 27 | } 28 | 29 | public function getAccessToken() { 30 | $timestamp = round(microtime(true) * 1000); 31 | 32 | // check if we have a valid token in memory 33 | $SHM_KEY = ftok(__FILE__, chr(1)); 34 | $keyStore = crc32("tuyacloud_token"); 35 | $store = shm_attach($SHM_KEY); 36 | if (shm_has_var($store, $keyStore)) { 37 | $dataStore = shm_get_var($store, $keyStore); 38 | $dataStore = json_decode($dataStore); 39 | if ($dataStore->expire > $timestamp) { 40 | return $dataStore->access_token; 41 | } 42 | } 43 | 44 | // if not, we retrieve it 45 | $path = '/v1.0/token?grant_type=1'; // URL to get the acces token 46 | 47 | $signature = $this->signRequest('GET', $path, $timestamp); 48 | 49 | // En-têtes de requête 50 | $headers = [ 51 | 'client_id: ' . $this->accessKey, 52 | 'sign: ' . $signature, 53 | 't: ' . $timestamp, 54 | 'sign_method: HMAC-SHA256' 55 | ]; 56 | 57 | // send the request 58 | $ch = curl_init(); 59 | curl_setopt($ch, CURLOPT_URL, $this->baseUrl . $path); 60 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 61 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 62 | 63 | $response = curl_exec($ch); 64 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 65 | curl_close($ch); 66 | 67 | $data = json_decode($response, true); 68 | if (isset($data['result']['access_token'])) { 69 | // save token 70 | $dataStore = new stdClass(); 71 | $dataStore->access_token = $data['result']['access_token']; 72 | $dataStore->expire = $timestamp+($data['result']['expire_time']*1000); 73 | if (!shm_put_var($store, $keyStore, json_encode($dataStore))) throw new Exception("[tuyacloud] 'shm_put_var' failed to store the access_token"); 74 | return $data['result']['access_token']; 75 | } 76 | 77 | throw new Exception('[tuyacloud] Unable to retrieve access token'); 78 | } 79 | 80 | private function sendRequest($path, $method, $data = "{}") { 81 | // $data can be a JSON string, an Array or an Object 82 | // so we test 83 | // check the JSON string is valid 84 | if (is_string($data)) { 85 | json_decode($data); 86 | if (json_last_error() !== JSON_ERROR_NONE) { 87 | throw new InvalidArgumentException("[tuyacloud] The argument must be a valid JSON string, an array, or a stdClass object."); 88 | } 89 | } 90 | // encode the array to a JSON string 91 | else if (is_array($data)) { 92 | $data = json_encode($data); 93 | } 94 | // if it's a stdClass object, convert it to a JSON string 95 | else if (is_object($data) && $data instanceof stdClass) { 96 | $data = json_encode($data); 97 | } 98 | 99 | $timestamp = round(microtime(true) * 1000); 100 | $token = $this->getAccessToken(); 101 | $sign = $this->signRequest($method, $path, $timestamp, $token, $data); 102 | 103 | $url = $this->baseUrl . $path; 104 | $headers = [ 105 | 'Accept: application/json, text/plain, */*', 106 | 't: ' . $timestamp, 107 | 'sign: ' . $sign, 108 | 'client_id: ' . $this->accessKey, 109 | 'sign_method: HMAC-SHA256', 110 | 'access_token: ' . $token, 111 | 'Content-Type: application/json' 112 | ]; 113 | 114 | $ch = curl_init(); 115 | curl_setopt($ch, CURLOPT_URL, $url); 116 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 117 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); 118 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 119 | 120 | if ($data !== null) { 121 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 122 | } 123 | 124 | $response = curl_exec($ch); 125 | curl_close($ch); 126 | 127 | return json_decode($response, true); 128 | } 129 | 130 | /** 131 | * Get the device status/properties (`switch_led`, `bright_value`, `fan_switch`, …) 132 | * 133 | * @param {String} $deviceId The device id 134 | * @return {Object} {success:(boolean), result:[{code, value}]} 135 | */ 136 | public function getDevice($deviceId) { 137 | if (!isset($deviceId)) throw "[tuyacloud] You have to pass the `device_id` as an argument to this function 'getDevice'."; 138 | return $this->sendRequest('/v1.0/iot-03/devices/' . $deviceId. '/status', 'GET'); 139 | } 140 | 141 | /** 142 | * Set the device properties (`switch_led`, `bright_value`, `fan_switch`, …) 143 | * 144 | * @param {String} $deviceId The device id 145 | * @param {String|Object|Array} $commands An array of commands (e.g. `[{"code":"switch_led", "value":false}, {"code":"fan_switch", "value":true}`) 146 | * @return {Object} {success:(boolean), result:[{code, value}]} 147 | */ 148 | public function setDevice($deviceId, $commands) { 149 | if (func_num_args() != 2) throw "[tuyacloud] You have to pass the `device_id` and the `commands` as arguments to this function 'setDevice'."; 150 | return $this->sendRequest('/v1.0/iot-03/devices/' . $deviceId. '/commands', 'POST', $commands); 151 | } 152 | 153 | /** 154 | * Return a list of scenes for the user 155 | * 156 | * @return {Array} An array of {id, name, running_mode, space_id, status, type} 157 | */ 158 | public function getScenes() { 159 | // retrieve the spaces 160 | // https://developer.tuya.com/en/docs/cloud/75a240f09b?id=Kcp2kv5bcvne7 161 | $spaces = $this->sendRequest('/v2.0/cloud/space/child', 'GET'); 162 | if ($spaces['success'] != 1) throw "[tuyacloud] An error occured with space/child: ".$spaces['error_msg']; 163 | // for each space, retrieve the related scenes 164 | $spaces = $spaces['result']['data']; 165 | $ret = []; 166 | foreach($spaces as $spaceId) { 167 | // if we need to find the space name: 168 | // $spaceDetails = $this->sendRequest('/v2.0/cloud/space/'.$spaceId, 'GET'); 169 | // if ($spaceDetails['success'] != 1) throw "[tuyacloud] An error occured with space/".$spaceId.": ".$spaceDetails['error_msg']; 170 | // $spaceName = $spaceDetails['result']['name']; 171 | $scenesDetails = $this->sendRequest('/v2.0/cloud/scene/rule?space_id='.$spaceId, 'GET'); 172 | if ($scenesDetails['success'] != 1) throw "[tuyacloud] An error occured with scene/rule?space_id=".$spaceId.": ".$scenesDetails['error_msg']; 173 | foreach($scenesDetails['result']['list'] as $scenes) { 174 | array_push($ret, $scenes); 175 | } 176 | } 177 | return $ret; 178 | } 179 | 180 | /** 181 | * To start a scene 182 | * 183 | * @param {String} $sceneId The scene id 184 | * @return {Object} The result of the action 185 | */ 186 | public function startScene($sceneId) { 187 | if (!isset($sceneId)) throw "[tuyacloud] You have to pass the `scene_id` as an argument to this function 'startScene'."; 188 | return $this->sendRequest('/v2.0/cloud/scene/rule/'.$sceneId.'/actions/trigger', 'POST'); 189 | } 190 | } 191 | ?> 192 | --------------------------------------------------------------------------------