From 1df025c93d609fdf1a0ac83dd47ccf5b1875bb0c Mon Sep 17 00:00:00 2001 From: Pawel Janisio Date: Wed, 19 Jun 2024 19:14:09 +0200 Subject: [PATCH] 1.25.23 - changed name of method fromforceUpdate to forceGetData - added forceUpdate Device using websockets - added method createUpdateData in Websockets to handle updating deviceparameters - code cleanup for Websockets - updated README - updated and cleaned index.php --- README.md | 4 +- index.php | 87 ++++++++++++++---------------- src/Devices.php | 47 ++++++++++++++-- src/WebSocketsClient.php | 112 +++++++++++++++++++++++---------------- 4 files changed, 154 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 2ce0dc2..6157d09 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ewelinkApiPhp -API connector for Sonoff/ewelink devices using simple webapi based on OAuth2. +API connector for Sonoff/ewelink devices using simple webapi based on OAuth2 with Websockets features. PHP 7.4+, **no other dependiencies required**. @@ -15,6 +15,7 @@ PHP 7.4+, **no other dependiencies required**. - set parameter for Multichannel devices - check if device is Online - debug all requests and responses to debug.log +- use Websocket connection to get and update parameters ## Public key and secret @@ -71,6 +72,7 @@ ewelinkApiPhp/ │ ├── HttpClient.php │ ├── Token.php │ └── Utils.php +│ └── WebSocketsClient.php │ ├── autoloader.php └── index.php diff --git a/index.php b/index.php index 6b3f3fa..8e2a3cd 100644 --- a/index.php +++ b/index.php @@ -10,8 +10,8 @@ require_once __DIR__ . '/autoloader.php'; -$httpClient = new HttpClient(); -$token = new Token($httpClient); +$http = new HttpClient(); +$token = new Token($http); if (isset($_GET['code']) && isset($_GET['region'])) { try { @@ -29,87 +29,82 @@ echo '

Token expiry time: ' . date('Y-m-d H:i:s', $tokenData['atExpiredTime'] / 1000) . '

'; try { - // Initialize Devices class which will also initialize Home class and fetch family data - $devices = new Devices($httpClient); - $devicesData = $devices->fetchDevicesData(); + $devs = new Devices($http); + $devsData = $devs->fetchDevicesData(); echo '

Devices Data

'; - echo '
' . print_r($devicesData, true) . '
'; + echo '
' . print_r($devsData, true) . '
'; - $devicesList = $devices->getDevicesList(); + $devsList = $devs->getDevicesList(); echo '

Devices List

'; - echo '
' . print_r($devicesList, true) . '
'; + echo '
' . print_r($devsList, true) . '
'; - // Example usage of searchDeviceParam - $searchKey = 'productModel'; // example key to search for - $deviceId = '100142b205'; // example device ID - $searchResult = $devices->searchDeviceParam($searchKey, $deviceId); + $searchKey = 'productModel'; + $devId = '100142b205'; + $searchRes = $devs->searchDeviceParam($searchKey, $devId); echo '

Search Result

'; - echo '
' . print_r($searchResult, true) . '
'; + echo '
' . print_r($searchRes, true) . '
'; - // Example usage of getDeviceParamLive - $liveParam = ['voltage', 'current', 'power']; // example parameter to get - $liveResult = $devices->getDeviceParamLive($deviceId, $liveParam); + $liveParam = ['voltage', 'current', 'power']; + $liveRes = $devs->getDeviceParamLive($devId, $liveParam); echo '

Live Device Parameter

'; - echo '
' . print_r($liveResult, true) . '
'; + echo '
' . print_r($liveRes, true) . '
'; - // Example usage of getAllDeviceParamLive - $refreshResult = $devices->getAllDeviceParamLive($deviceId); + $allLiveParams = $devs->getAllDeviceParamLive($devId); echo '

Get All Device Parameters Live

'; - echo '
' . print_r($refreshResult, true) . '
'; + echo '
' . print_r($allLiveParams, true) . '
'; - // Example usage of setDeviceStatus for multi-channel device - $multiChannelDeviceId = '1000663128'; - $multiChannelParams = [ + $multiDevId = '1000663128'; + $multiParams = [ ['switch' => 'off', 'outlet' => 0], ['switch' => 'off', 'outlet' => 1], ['switch' => 'off', 'outlet' => 2], ['switch' => 'off', 'outlet' => 3] ]; - $setStatusResult = $devices->setDeviceStatus($multiChannelDeviceId, $multiChannelParams); + $setMultiRes = $devs->setDeviceStatus($multiDevId, $multiParams); echo '

Set Multi-Channel Device Status Result

'; - echo '
' . print_r($setStatusResult, true) . '
'; + echo '
' . print_r($setMultiRes, true) . '
'; - // Example usage of setDeviceStatus for single-channel device - $singleChannelDeviceId = '10011015b6'; - $singleChannelParams = ['switch' => 'off']; - $setStatusResultSingle = $devices->setDeviceStatus($singleChannelDeviceId, $singleChannelParams); + $singleDevId = '10011015b6'; + $singleParams = ['switch' => 'off']; + $setSingleRes = $devs->setDeviceStatus($singleDevId, $singleParams); echo '

Set Single-Channel Device Status Result

'; - echo '
' . print_r($setStatusResultSingle, true) . '
'; + echo '
' . print_r($setSingleRes, true) . '
'; - // Example usage of setDeviceStatus for single-channel device with multiple parameters - $singleChannelParamsMultiple = [ + $singleParamsMulti = [ ['colorR' => 0], ['colorG' => 153], ['colorB' => 0] ]; - $setStatusResultSingleMultiple = $devices->setDeviceStatus($singleChannelDeviceId, $singleChannelParamsMultiple); + $setSingleMultiRes = $devs->setDeviceStatus($singleDevId, $singleParamsMulti); echo '

Set Single-Channel Device Status Result (Multiple Parameters)

'; - echo '
' . print_r($setStatusResultSingleMultiple, true) . '
'; + echo '
' . print_r($setSingleMultiRes, true) . '
'; - // Example usage of isOnline - $deviceIdentifier = 'Ledy salon'; // example device name or ID - $isOnlineResult = $devices->isOnline($deviceIdentifier); + $devIdent = 'Ledy salon'; + $onlineRes = $devs->isOnline($devIdent); echo '

Is Device Online?

'; - echo $deviceIdentifier . ' is ' . ($isOnlineResult ? 'online' : 'offline') . '.'; + echo $devIdent . ' is ' . ($onlineRes ? 'online' : 'offline') . '.'; - // Example usage of getFamilyData - $home = $httpClient->getHome(); + $home = $http->getHome(); $familyData = $home->fetchFamilyData(); echo '

Family Data

'; echo '
' . print_r($familyData, true) . '
'; echo '

Current Family ID: ' . htmlspecialchars($home->getCurrentFamilyId()) . '

'; - // Example usage of forceUpdate with three parameters - $forceUpdateParams = ['current', 'power', 'voltage']; - $forceUpdateResult = $devices->forceUpdate($deviceId, $forceUpdateParams); - echo '

Force Update Result

'; - echo '
' . print_r($forceUpdateResult, true) . '
'; + $forceParams = ['current', 'power', 'voltage']; + $forceRes = $devs->forceGetData($devId, $forceParams); + echo '

Force Get Data Result

'; + echo '
' . print_r($forceRes, true) . '
'; + + $updateParams = ['switch' => 'on']; + $updateRes = $devs->forceUpdateDevice($devId, $updateParams, 3); + echo '

Force Update Device Result

'; + echo '
' . print_r($updateRes, true) . '
'; } catch (Exception $e) { echo 'Error: ' . $e->getMessage(); } } else { - $loginUrl = $httpClient->getLoginUrl(); + $loginUrl = $http->getLoginUrl(); echo 'Authorize ewelinkApiPhp'; } } diff --git a/src/Devices.php b/src/Devices.php index f5d2cd8..838e469 100644 --- a/src/Devices.php +++ b/src/Devices.php @@ -8,7 +8,6 @@ * Description: API connector for Sonoff / ewelink devices */ - require_once __DIR__ . '/WebSocketClient.php'; require_once __DIR__ . '/Utils.php'; require_once __DIR__ . '/Constants.php'; @@ -376,14 +375,14 @@ public function getDeviceHistory($deviceId) { } /** - * Force update the status of a device using WebSocket. + * Force get data of a device using WebSocket. * * @param string $deviceId The device ID. * @param array|string $params The parameters to query. * @return array The response data. * @throws Exception If there is an error during the process. */ - public function forceUpdate($deviceId, $params) { + public function forceGetData($deviceId, $params) { $device = $this->getDeviceById($deviceId); if (!$device) { throw new Exception('Device not found.'); @@ -408,5 +407,47 @@ public function forceUpdate($deviceId, $params) { return $response['params']; } + + /** + * Force update the status of a device using WebSocket. + * + * @param string $deviceId The device ID. + * @param array $params The parameters to update. + * @param int $sleepSec The number of seconds to wait before verifying the update (default is 3 seconds). + * @return array The response data. + * @throws Exception If there is an error during the process. + */ + public function forceUpdateDevice($deviceId, $params, $sleepSec = 3) { + $device = $this->getDeviceById($deviceId); + if (!$device) { + throw new Exception('Device not found.'); + } + + $wsClient = new WebSocketClient($this->httpClient); + $handshakeResponse = $wsClient->handshake($device); + + if (isset($handshakeResponse['error']) && $handshakeResponse['error'] != 0) { + throw new Exception('Handshake Error: ' . $handshakeResponse['msg']); + } + + $data = $wsClient->createUpdateData($device, $params, $device['apikey']); + $wsClient->send(json_encode($data)); + + // Keep heartbeat running and verify changes + $response = json_decode($wsClient->receive(), true); + + if (isset($response['error']) && $response['error'] != 0) { + throw new Exception('Error: ' . $response['msg']); + } + + sleep($sleepSec); // Wait for a while to let the changes take effect + + // Check if the parameters have been updated + $updatedParams = $this->forceGetData($deviceId, array_keys($params)); + + $wsClient->close(); + + return $updatedParams; + } } ?> diff --git a/src/WebSocketsClient.php b/src/WebSocketsClient.php index 1c7ed03..1c22e52 100644 --- a/src/WebSocketsClient.php +++ b/src/WebSocketsClient.php @@ -21,9 +21,9 @@ class WebSocketClient { private $path; private $key; private $utils; + private $httpClient; private $hbInterval; private $pid; - private $httpClient; /** * Constructor for the WebSocketClient class. @@ -68,7 +68,7 @@ private function resolveWebSocketUrl() { $ip = gethostbyname($emptyRequestResponse['domain']); $this->url = 'wss://' . $ip . ':' . $emptyRequestResponse['port'] . '/api/ws'; - + $parts = parse_url($this->url); $this->host = gethostbyname($parts['host']); // Resolve the domain to IP $this->port = $parts['port']; @@ -132,8 +132,8 @@ public function connect() { */ public function handshake($device) { $this->connect(); - $data = $this->createHandshakeData($device); - $this->send(json_encode($data)); + $handshakeData = $this->createHandshakeData($device); + $this->send(json_encode($handshakeData)); $response = $this->receive(); $responseData = json_decode($response, true); if (isset($responseData['config']['hbInterval'])) { @@ -142,6 +142,66 @@ public function handshake($device) { return $responseData; } + /** + * Create handshake data for WebSocket connection. + * + * @param array $device The device data. + * @return array The handshake data. + */ + public function createHandshakeData($device) { + $utils = new Utils(); + $tokenData = $this->httpClient->getTokenData(); + return [ + 'action' => 'userOnline', + 'version' => 8, + 'ts' => time(), + 'at' => $tokenData['accessToken'], + 'userAgent' => 'app', + 'apikey' => $device['apikey'], + 'appid' => Constants::APPID, + 'nonce' => $utils->generateNonce(), + 'sequence' => strval(round(microtime(true) * 1000)) + ]; + } + + /** + * Create query data for WebSocket connection. + * + * @param array $device The device data. + * @param array|string $params The parameters to query. + * @return array The query data. + */ + public function createQueryData($device, $params) { + return [ + 'action' => 'query', + 'deviceid' => $device['deviceid'], + 'apikey' => $device['apikey'], + 'sequence' => strval(round(microtime(true) * 1000)), + 'params' => is_array($params) ? $params : [$params], + 'userAgent' => 'app' + ]; + } + + /** + * Create update data for WebSocket connection. + * + * @param array $device The device data. + * @param array $params The parameters to update. + * @param string $selfApikey The receiver's apikey. + * @return array The update data. + */ + public function createUpdateData($device, $params, $selfApikey) { + return [ + 'action' => 'update', + 'apikey' => $device['apikey'], + 'selfApikey' => $selfApikey, + 'deviceid' => $device['deviceid'], + 'params' => $params, + 'userAgent' => 'app', + 'sequence' => strval(round(microtime(true) * 1000)) + ]; + } + /** * Send data over the WebSocket connection. * @@ -251,7 +311,7 @@ private function hybi10Encode($payload, $type = 'text', $masked = true) { } $frame = implode('', $frameHead); - for ($i = 0; $payloadLength > $i; $i++) { + for ($i = 0; $i < $payloadLength; $i++) { $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; } @@ -285,7 +345,7 @@ private function hybi10Decode($data) { $mask = substr($bytes, 2, 4); $codedData = substr($bytes, 6); } - for ($i = 0; strlen($codedData) > $i; $i++) { + for ($i = 0; $i < strlen($codedData); $i++) { $decodedData .= $codedData[$i] ^ $mask[$i % 4]; } } else { @@ -330,46 +390,6 @@ private function startHeartbeat($interval) { } } - /** - * Create handshake data for WebSocket connection. - * - * @param array $device The device data. - * @return array The handshake data. - */ - public function createHandshakeData($device) { - $utils = new Utils(); - $tokenData = $this->httpClient->getTokenData(); - return [ - 'action' => 'userOnline', - 'version' => 8, - 'ts' => time(), - 'at' => $tokenData['accessToken'], - 'userAgent' => 'app', - 'apikey' => $device['apikey'], - 'appid' => Constants::APPID, - 'nonce' => $utils->generateNonce(), - 'sequence' => strval(round(microtime(true) * 1000)) - ]; - } - - /** - * Create query data for WebSocket connection. - * - * @param array $device The device data. - * @param array|string $params The parameters to query. - * @return array The query data. - */ - public function createQueryData($device, $params) { - return [ - 'action' => 'query', - 'deviceid' => $device['deviceid'], - 'apikey' => $device['apikey'], - 'sequence' => strval(round(microtime(true) * 1000)), - 'params' => is_array($params) ? $params : [$params], - 'userAgent' => 'app' - ]; - } - /** * Get the WebSocket URL. *