Skip to content

Commit

Permalink
1.25.23
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
PJanisio committed Jun 19, 2024
1 parent 5e6e67f commit 1df025c
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 96 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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**.

Expand All @@ -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

Expand Down Expand Up @@ -71,6 +72,7 @@ ewelinkApiPhp/
│ ├── HttpClient.php
│ ├── Token.php
│ └── Utils.php
│ └── WebSocketsClient.php
├── autoloader.php
└── index.php
Expand Down
87 changes: 41 additions & 46 deletions index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -29,87 +29,82 @@
echo '<p>Token expiry time: ' . date('Y-m-d H:i:s', $tokenData['atExpiredTime'] / 1000) . '</p>';

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 '<h1>Devices Data</h1>';
echo '<pre>' . print_r($devicesData, true) . '</pre>';
echo '<pre>' . print_r($devsData, true) . '</pre>';

$devicesList = $devices->getDevicesList();
$devsList = $devs->getDevicesList();
echo '<h1>Devices List</h1>';
echo '<pre>' . print_r($devicesList, true) . '</pre>';
echo '<pre>' . print_r($devsList, true) . '</pre>';

// 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 '<h1>Search Result</h1>';
echo '<pre>' . print_r($searchResult, true) . '</pre>';
echo '<pre>' . print_r($searchRes, true) . '</pre>';

// 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 '<h1>Live Device Parameter</h1>';
echo '<pre>' . print_r($liveResult, true) . '</pre>';
echo '<pre>' . print_r($liveRes, true) . '</pre>';

// Example usage of getAllDeviceParamLive
$refreshResult = $devices->getAllDeviceParamLive($deviceId);
$allLiveParams = $devs->getAllDeviceParamLive($devId);
echo '<h1>Get All Device Parameters Live</h1>';
echo '<pre>' . print_r($refreshResult, true) . '</pre>';
echo '<pre>' . print_r($allLiveParams, true) . '</pre>';

// 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 '<h1>Set Multi-Channel Device Status Result</h1>';
echo '<pre>' . print_r($setStatusResult, true) . '</pre>';
echo '<pre>' . print_r($setMultiRes, true) . '</pre>';

// 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 '<h1>Set Single-Channel Device Status Result</h1>';
echo '<pre>' . print_r($setStatusResultSingle, true) . '</pre>';
echo '<pre>' . print_r($setSingleRes, true) . '</pre>';

// 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 '<h1>Set Single-Channel Device Status Result (Multiple Parameters)</h1>';
echo '<pre>' . print_r($setStatusResultSingleMultiple, true) . '</pre>';
echo '<pre>' . print_r($setSingleMultiRes, true) . '</pre>';

// Example usage of isOnline
$deviceIdentifier = 'Ledy salon'; // example device name or ID
$isOnlineResult = $devices->isOnline($deviceIdentifier);
$devIdent = 'Ledy salon';
$onlineRes = $devs->isOnline($devIdent);
echo '<h1>Is Device Online?</h1>';
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 '<h1>Family Data</h1>';
echo '<pre>' . print_r($familyData, true) . '</pre>';
echo '<p>Current Family ID: ' . htmlspecialchars($home->getCurrentFamilyId()) . '</p>';

// Example usage of forceUpdate with three parameters
$forceUpdateParams = ['current', 'power', 'voltage'];
$forceUpdateResult = $devices->forceUpdate($deviceId, $forceUpdateParams);
echo '<h1>Force Update Result</h1>';
echo '<pre>' . print_r($forceUpdateResult, true) . '</pre>';
$forceParams = ['current', 'power', 'voltage'];
$forceRes = $devs->forceGetData($devId, $forceParams);
echo '<h1>Force Get Data Result</h1>';
echo '<pre>' . print_r($forceRes, true) . '</pre>';

$updateParams = ['switch' => 'on'];
$updateRes = $devs->forceUpdateDevice($devId, $updateParams, 3);
echo '<h1>Force Update Device Result</h1>';
echo '<pre>' . print_r($updateRes, true) . '</pre>';

} catch (Exception $e) {
echo 'Error: ' . $e->getMessage();
}
} else {
$loginUrl = $httpClient->getLoginUrl();
$loginUrl = $http->getLoginUrl();
echo '<a href="' . htmlspecialchars($loginUrl) . '">Authorize ewelinkApiPhp</a>';
}
}
Expand Down
47 changes: 44 additions & 3 deletions src/Devices.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.');
Expand All @@ -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;
}
}
?>
112 changes: 66 additions & 46 deletions src/WebSocketsClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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'])) {
Expand All @@ -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.
*
Expand Down Expand Up @@ -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];
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
*
Expand Down

0 comments on commit 1df025c

Please sign in to comment.