diff --git a/src/Interfaces/YeePHPInterface.php b/src/Interfaces/YeePHPInterface.php index ebb9f5d..8a32c20 100644 --- a/src/Interfaces/YeePHPInterface.php +++ b/src/Interfaces/YeePHPInterface.php @@ -45,9 +45,10 @@ public function getBrightness(): int; /** * Return the current light color * - * @return string + * @param string $type The type of color + * @return array */ - public function getColor(): string; + public function getColor(string $type): array; /** * Return the current light name @@ -66,10 +67,26 @@ public function toggle(): self; /** * Set the color of the light * - * @param int $hexColor The light color in hexadecimal (eg: 0xFFFFFF) + * @param int $color The light color value in hexadecimal, color temperature or hue (eg: 0xFFFFFF) + * @param array $params The parameters for the color change. + * @return $this + */ + public function setColor(int $color, array $params): self; + + /** + * Start a color flow + * + * @param array $flowExpression Array of expressions, they must be profide duration (ms), mode (1, 2 or 7), value (color temperature or rgb hexa) and bright (0 - 100) in order + * @param string $action The action when flow is finished * @return $this */ - public function setColor(int $hexColor): self; + public function startColorFlow(array $flowExpression, string $action): self; + + /** + * Stop the current color flow + * + */ + public function stopColorFlow(): void; /** * Define the desired light brightness. @@ -95,6 +112,13 @@ public function setName(string $name): self; */ public function setPower(string $power): self; + /** + * Save the current state to the device memory + * + * @return $this + */ + public function setDefault(): self; + /** * Send the parameters to the light. * @return bool diff --git a/src/YeePHP.php b/src/YeePHP.php index 01a81e9..fbf5247 100644 --- a/src/YeePHP.php +++ b/src/YeePHP.php @@ -37,7 +37,12 @@ class YeePHP implements YeePHPInterface 'set_bright', 'set_name', 'set_rgb', - 'set_power' + 'set_power', + 'set_default', + 'set_ct_abx', + 'set_hsv', + 'start_cf', + 'stop_cf' ]; /** @@ -46,10 +51,54 @@ class YeePHP implements YeePHPInterface public const ALLOWED_PROPS = [ 'bright', 'rgb', + 'ct', + 'hue', + 'sat', 'name', 'power' ]; + /** + * The list of allowed fade effects + */ + public const ALLOWED_FADE_EFFECTS = [ + 'smooth', + 'sudden' + ]; + + /** + * The list of allowed fade effects + */ + public const ALLOWED_COLOR_TYPES = [ + 'ct', + 'rgb', + 'hsv' + ]; + + /** + * The list of allowed color flow actions + */ + public const ALLOWED_FLOW_ACTIONS = [ + "recover", + "stay", + "turnoff" + ]; + + /** + * The default fade effect + */ + public const DEDAULT_FADE_EFFECT = 'smooth'; + + /** + * The default fade delay + */ + public const DEDAULT_FADE_DELAY = 300; + + /** + * The default color flow action + */ + public const DEDAULT_FLOW_ACTION = "recover"; + /** * The socket connected to the light * @@ -57,9 +106,6 @@ class YeePHP implements YeePHPInterface */ protected $socket; - protected string $fadeEffect = 'smooth'; - protected int $fadeDelay = 300; - /** * Light constructor. * @param string $ip The light IP @@ -68,17 +114,14 @@ class YeePHP implements YeePHPInterface */ public function __construct(string $ip, int $port = 55443) { - if(filter_var($ip, FILTER_VALIDATE_IP)) - { + if (filter_var($ip, FILTER_VALIDATE_IP)) { $this->lightIP = $ip; - } - else - { + } else { throw new Exception('Invalid IP address : ' . $ip); } $this->lightPort = $port; - if(!$this->connect()) + if (!$this->connect()) throw new Exception('Can\'t connect to Yeelight device'); } @@ -103,7 +146,7 @@ public function isConnected(): bool */ public function isOn(): bool { - return $this->getProp('power'); + return $this->getProp('power')[0] == 'on'; } /** @@ -127,15 +170,30 @@ public function getPort(): int */ public function getBrightness(): int { - return dechex($this->getProp('bright')); + return intval($this->getProp('bright')[0]); } /** * @inheritDoc + * @throws Exception */ - public function getColor(): string + public function getColor(string $type = 'rgb'): array { - return dechex($this->getProp('rgb')); + if (!in_array($type, self::ALLOWED_COLOR_TYPES, true)) + throw new Exception('Invalid color type ' . $type . ' available effects : ( ' . implode(" , ", self::ALLOWED_COLOR_TYPES) . ' )'); + + switch ($type) { + case 'ct': + return ["ct" => $this->getProp('ct')[0]]; + case 'rgb': + return ["rgb" => dechex($this->getProp('rgb')[0])]; + case 'hsv': { + $res = $this->getProps(['hue', 'sat']); + return ['hue' => $res[0], 'sat' => $res[1]]; + } + default: + throw new Exception('Invalid color type !'); + } } /** @@ -143,7 +201,7 @@ public function getColor(): string */ public function getName(): string { - return $this->getProp('name'); + return $this->getProp('name')[0]; } /** @@ -159,27 +217,70 @@ public function toggle(): self /** * @inheritDoc */ - public function setColor(int $hexColor): self + public function setColor(int $color, array $params = []): self { - // TODO: check if the color is a valid hexadecimal number + if (!array_key_exists('type', $params)) + $params['type'] = 'rgb'; + if (!in_array($params['type'], self::ALLOWED_COLOR_TYPES, true)) + throw new Exception('Invalid color type ' . $params['type'] . ' available effects : ( ' . implode(" , ", self::ALLOWED_COLOR_TYPES) . ' )'); - $this->createJob('set_rgb', [ - $hexColor, - $this->fadeEffect, - $this->fadeDelay - ]); + $params = $this->checkColorValue($color, $params); + $params = $this->checkFadeParams($params); + + $this->createColorJob($color, $params); + + return $this; + } + + /** + * @inheritDoc + */ + public function startColorFlow(array $flowExpression, string $action = self::DEDAULT_FLOW_ACTION): self + { + if (count($flowExpression) == 0) + throw new Exception('Flow expression can\'t be empty !'); + + if (!in_array($action, self::ALLOWED_FLOW_ACTIONS, true)) + throw new Exception('Invalid action type ' . $action . ' available actions : ( ' . implode(" , ", self::ALLOWED_FLOW_ACTIONS) . ' )'); + + try { + self::array_every(fn ($expression) => $this->checkExpression($expression), $flowExpression); + } catch (Exception $e) { + throw new Exception('Invalid flow expression ! ' . $e->getMessage()); + } + + $params = [ + count($flowExpression), + array_search($action, self::ALLOWED_FLOW_ACTIONS), + implode(",", array_merge(...$flowExpression)) + ]; + + $this->createJob('start_cf', $params); return $this; } + /** + * @inheritDoc + */ + public function stopColorFlow(): void + { + $job = $this->createJobArray('stop_cf', []); + + $res = $this->makeRequest($job); + + if (!$res) + throw new Exception('Error during stoping color flow'); + } + /** * @inheritDoc */ public function setBrightness(int $amount): self { - if($amount > 100) + if ($amount > 100) $amount = 100; - else if($amount < 0) + else if ($amount < 0) $amount = 0; $this->createJob('set_bright', [ @@ -206,18 +307,28 @@ public function setName(string $name): self */ public function setPower(string $power): self { - if(!$power === 'on' || !$power === 'off') + if (!$power === 'on' || !$power === 'off') throw new Exception('Invalid power state: ' . $power); $this->createJob('set_power', [ $power, - $this->fadeEffect, - $this->fadeDelay + self::DEDAULT_FADE_EFFECT, + self::DEDAULT_FADE_DELAY ]); return $this; } + /** + * @inheritDoc + */ + public function setDefault(): self + { + $this->createJob('set_default', []); + + return $this; + } + /** * @inheritDoc * @throws Exception @@ -226,10 +337,9 @@ public function commit(): bool { $success = false; - foreach ($this->jobs as $job) - { + foreach ($this->jobs as $job) { $res = $this->makeRequest($job); - if(!empty($res) || is_null($res)) + if (!empty($res) || is_null($res)) $success = true; } @@ -238,25 +348,31 @@ public function commit(): bool return $success; } + /** + * @inheritDoc + */ public function disconnect(): bool { - try - { + try { return fclose($this->socket); - } - catch (\TypeError $e) - { + } catch (Exception $e) { return false; } - } + + /** + * Create socket connection to the light + * + * @return bool + * @throws Exception + */ protected function connect(): bool { - $sock = fsockopen($this->lightIP, $this->lightPort, $errCode, $errStr, 3); - if(!$sock) return false; + $sock = fsockopen($this->lightIP, $this->lightPort, $errCode, $errStr, 30); + if (!$sock) return false; - stream_set_blocking($sock, false); + // stream_set_blocking($sock, false); $this->socket = $sock; return true; @@ -266,19 +382,42 @@ protected function connect(): bool * Get a certain prop value * * @param string $prop The prop name (refer to doc) - * @return string|null + * @return array|null * @throws Exception */ - protected function getProp(string $prop): string + protected function getProp(string $prop): ?array { - if(!in_array($prop, self::ALLOWED_PROPS)) + if (!in_array($prop, self::ALLOWED_PROPS)) throw new Exception('Invalid prop supplied ' . $prop); $job = $this->createJobArray('get_prop', [$prop]); + + $res = $this->makeRequest($job); + + if ($res[0] == "ok") + throw new Exception('Problem in result'); // TODO + + return $res; + } + + /** + * Get a certain prop value + * + * @param string $prop The prop name (refer to doc) + * @return array|null + * @throws Exception + */ + protected function getProps(array $props): ?array + { + if (!self::array_every(fn ($value) => in_array($value, self::ALLOWED_PROPS), $props)) + throw new Exception('Invalid props supplied ' . $props); + + $job = $this->createJobArray('get_prop', $props); + $res = $this->makeRequest($job); - if(!$res) - $res = ''; + if ($res[0] == "ok") + throw new Exception('Problem in result'); // TODO return $res; } @@ -290,7 +429,7 @@ protected function getProp(string $prop): string */ protected function checkIsOnline(): bool { - if(socket_get_status($this->socket) === []) + if (socket_get_status($this->socket) === []) throw new Exception('Device is offline!'); return true; @@ -300,33 +439,35 @@ protected function checkIsOnline(): bool * Make a request to the light * * @param array $job The job created by the createJob() method - * @return string|null + * @return array|null * @throws Exception */ - protected function makeRequest(array $job): ?string + protected function makeRequest(array $job): ?array { $this->checkIsOnline(); $requestStr = json_encode($job) . "\r\n"; - fwrite($this->socket, $requestStr, strlen($requestStr)); + fwrite($this->socket, $requestStr); fflush($this->socket); - usleep(100 * 700); // 0.7s -> wait for the light response + // usleep(100 * 1000); // 0.7s -> wait for the light response + $res = fgets($this->socket); - $resultStr = null; - if($res) - { + $result = null; + + if (!empty($res)) { $res = json_decode($res, true); - if(!array_key_exists('error', $res) && array_key_exists('result', $res)) - $resultStr = $res['result'][0]; + if (!array_key_exists('error', $res) && array_key_exists('result', $res)) + $result = $res['result']; } - return $resultStr; + + return $result; } /** @@ -341,15 +482,181 @@ protected function createJob(string $method, array $params): void $this->jobs[] = $this->createJobArray($method, $params); } + + + + /** + * Convert an method and his params into a job array + * + * @return array + * @throws Exception + */ private function createJobArray(string $method, array $params): array { - if(!in_array($method, self::ALLOWED_METHODS)) + if (!in_array($method, self::ALLOWED_METHODS)) throw new Exception('Invalid method supplied ' . $method); return [ 'id' => !empty($this->jobs) ? count($this->jobs) : 0, 'method' => $method, - 'params' => $params + 'params' => array_filter($params, fn ($value) => !is_null($value)) ]; } + + /** + * Check if the color value is correct + * + * @param int $value the color value + * @param array $params The parameters for the color change. + * @return array + * @throws Exception + */ + private function checkColorValue(int $value, array $params) + { + switch ($params['type']) { + case 'ct': { + if ($value < 1700 || $value > 6500) + throw new Exception('Invalid color value ' . $value . '! value must be range from 1700 to 6500'); + }; + break; + case 'rgb': { + if ($value < 0 || $value > 16777215) + throw new Exception('Invalid color value ' . $value . '! value must be range from 0 to 16777215'); + }; + break; + case 'hsv': { + if ($value < 0 || $value > 359) + throw new Exception('Invalid color value ' . $value . '! value must be range from 0 to 359'); + if (!array_key_exists('sat', $params)) { + $params['sat'] = 50; + } else { + if ($params['sat'] > 100) + $params['sat'] = 100; + else if ($params['sat'] < 0) + $params['sat'] = 0; + } + }; + break; + default: + throw new Exception('Invalid color type !'); + } + return $params; + } + + /** + * Check if the fade params are correct + * + * @param array $params The fade parameters. + * @param string $defaultFadeEffect The default fade effect value. + * @return array + * @throws Exception + */ + private function checkFadeParams(array $params, string $defaultFadeEffect = self::DEDAULT_FADE_EFFECT) + { + if (array_key_exists('effect', $params)) { + if (!in_array($params['effect'], self::ALLOWED_FADE_EFFECTS)) + throw new Exception('Invalid effect value ! available effects : ' . implode(" ", self::ALLOWED_FADE_EFFECTS)); + } else { + $params['effect'] = $defaultFadeEffect; + } + if ($params['effect'] === 'sudden') { + if (array_key_exists('delay', $params)) { + unset($params['delay']); + } + return $params; + } else { + if (array_key_exists('delay', $params)) { + if ($params['delay'] < 30 || $params['delay'] > 3000) + throw new Exception('Invalid delay value ' . $params['delay'] . '! value must be range from 30 to 3000'); + } else { + $params['delay'] = self::DEDAULT_FADE_DELAY; + } + } + + return $params; + } + + private function checkExpression($expression) + { + if (!is_array($expression)) + throw new Exception("Expression must be an aray !"); + + if (count($expression) !== 4) + throw new Exception("Expression must have 4 values !"); + + $duration = $expression[0]; + $mode = $expression[1]; + $value = $expression[2]; + $bright = $expression[3]; + + if (!in_array($mode, [1, 2, 7], true)) + throw new Exception('Invalid expression mode ' . $mode . ' available modes : ( ' . implode(" , ", [1, 2, 7]) . ' )'); + + if (!is_integer($duration)) + throw new Exception('Invalid expression duration ' . $duration . ', must be an integer !'); + + if (!is_integer($value)) + throw new Exception('Invalid expression value ' . $value . ', must be an integer !'); + + if (!is_integer($bright)) + throw new Exception('Invalid expression bright ' . $bright . ', must be an integer !'); + + if ($bright != -1 && ($bright < 1 || $bright > 100)) + throw new Exception('Invalid expression bright ' . $bright . ', must -1 to be skipped or be range from 1 to 100 !'); + + if ($mode == 1 && ($value < 0 || $value > 16777215)) + throw new Exception('Invalid expression value ' . $value . ', must be range from 0 to 16777215 !'); + + if ($mode == 2 && ($value < 1700 || $value > 6500)) + throw new Exception('Invalid expression value ' . $value . ', must be range from 1700 to 6500 !'); + } + + /** + * Create a job for color changing + * + * @param int $value The light value color in hexadecimal, color temperature or hue (eg: 0xFFFFFF) + * @param array $params The parameters for the color change. + * @return void + * @throws Exception + */ + private function createColorJob(int $value, array $params) + { + + switch ($params['type']) { + case 'ct': + $this->createJob('set_ct_abx', [ + $value, + $params['effect'], + (isset($params['delay']) ? $params['delay'] : 0) + ]); + break; + case 'rgb': + $this->createJob('set_rgb', [ + $value, + $params['effect'], + (isset($params['delay']) ? $params['delay'] : 0) + ]); + break; + case 'hsv': + $this->createJob('set_hsv', [ + $value, + $params['sat'], + $params['effect'], + (isset($params['delay']) ? $params['delay'] : 0) + ]); + break; + default: + throw new Exception('Invalid color type !'); + } + } + + private static function array_every(callable $callback, $arr) + { + foreach ($arr as $ele) { + if (!call_user_func($callback, $ele)) { + return false; + } + } + return true; + } }