hyperion.ng/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp

1511 lines
40 KiB
C++

#include "LedDeviceYeelight.h"
#include <ssdp/SSDPDiscover.h>
#include <utils/QStringUtils.h>
// Qt includes
#include <QEventLoop>
#include <QtNetwork>
#include <QTcpServer>
#include <QColor>
#include <QDateTime>
#include <chrono>
#include <thread>
// Constants
namespace {
const bool verbose = false;
const bool verbose3 = false;
constexpr std::chrono::milliseconds WRITE_TIMEOUT{1000}; // device write timeout in ms
constexpr std::chrono::milliseconds READ_TIMEOUT{1000}; // device read timeout in ms
constexpr std::chrono::milliseconds CONNECT_TIMEOUT{1000}; // device connect timeout in ms
constexpr std::chrono::milliseconds CONNECT_STREAM_TIMEOUT{1000}; // device streaming connect timeout in ms
const bool TEST_CORRELATION_IDS = false; //Ignore, if yeelight sends responses in different order as request commands
// Configuration settings
const char CONFIG_LIGHTS [] = "lights";
const char CONFIG_COLOR_MODEL [] = "colorModel";
const char CONFIG_TRANS_EFFECT [] = "transEffect";
const char CONFIG_TRANS_TIME [] = "transTime";
const char CONFIG_EXTRA_TIME_DARKNESS[] = "extraTimeDarkness";
const char CONFIG_DEBUGLEVEL [] = "debugLevel";
const char CONFIG_BRIGHTNESS_MIN[] = "brightnessMin";
const char CONFIG_BRIGHTNESS_SWITCHOFF[] = "brightnessSwitchOffOnMinimum";
const char CONFIG_BRIGHTNESS_MAX[] = "brightnessMax";
const char CONFIG_BRIGHTNESSFACTOR[] = "brightnessFactor";
const char CONFIG_RESTORE_STATE[] = "restoreOriginalState";
const char CONFIG_QUOTA_WAIT_TIME[] = "quotaWait";
// Yeelights API
const int API_DEFAULT_PORT = 55443;
const quint16 API_DEFAULT_QUOTA_WAIT_TIME = 1000;
// Yeelight API Command
const char API_COMMAND_ID[] = "id";
const char API_COMMAND_METHOD[] = "method";
const char API_COMMAND_PARAMS[] = "params";
const char API_COMMAND_PROPS[] = "props";
const char API_PARAM_CLASS_COLOR[] = "color";
const char API_PARAM_CLASS_HSV[] = "hsv";
const char API_PROP_NAME[] = "name";
const char API_PROP_MODEL[] = "model";
const char API_PROP_FWVER[] = "fw_ver";
const char API_PROP_POWER[] = "power";
const char API_PROP_MUSIC[] = "music_on";
const char API_PROP_RGB[] = "rgb";
const char API_PROP_CT[] = "ct";
const char API_PROP_COLORFLOW[] = "cf";
const char API_PROP_BRIGHT[] = "bright";
// List of Result Information
const char API_RESULT_ID[] = "id";
const char API_RESULT[] = "result";
//const char API_RESULT_OK[] = "OK";
// List of Error Information
const char API_ERROR[] = "error";
const char API_ERROR_CODE[] = "code";
const char API_ERROR_MESSAGE[] = "message";
// Yeelight ssdp services
const char SSDP_ID[] = "wifi_bulb";
const char SSDP_FILTER[] = "yeelight(.*)";
const char SSDP_FILTER_HEADER[] = "Location";
const quint16 SSDP_PORT = 1982;
} //End of constants
YeelightLight::YeelightLight( Logger *log, const QString &hostname, quint16 port = API_DEFAULT_PORT)
:_log(log)
,_debugLevel(0)
,_isInError(false)
,_host (hostname)
,_port(port)
,_tcpSocket(nullptr)
,_tcpStreamSocket(nullptr)
,_correlationID(0)
,_lastWriteTime(QDateTime::currentMSecsSinceEpoch())
,_lastColorRgbValue(0)
,_transitionEffect(YeelightLight::API_EFFECT_SMOOTH)
,_transitionDuration(API_PARAM_DURATION.count())
,_extraTimeDarkness(API_PARAM_EXTRA_TIME_DARKNESS.count())
,_brightnessMin(0)
,_isBrightnessSwitchOffMinimum(false)
,_brightnessMax(100)
,_brightnessFactor(1.0)
,_transitionEffectParam(API_PARAM_EFFECT_SMOOTH)
,_waitTimeQuota(API_DEFAULT_QUOTA_WAIT_TIME)
,_isOn(false)
,_isInMusicMode(false)
{
_name = hostname;
}
YeelightLight::~YeelightLight()
{
log (3,"~YeelightLight()","" );
delete _tcpSocket;
log (2,"~YeelightLight()","void" );
}
void YeelightLight::setHostname( const QString &hostname, quint16 port = API_DEFAULT_PORT )
{
log (3,"setHostname()","" );
_host = hostname;
_port =port;
}
void YeelightLight::setStreamSocket( QTcpSocket* socket )
{
log (3,"setStreamSocket()","" );
_tcpStreamSocket = socket;
}
bool YeelightLight::open()
{
_isInError = false;
bool rc = false;
if ( _tcpSocket == nullptr )
{
_tcpSocket = new QTcpSocket();
}
if ( _tcpSocket->state() == QAbstractSocket::ConnectedState )
{
log (2,"open()","Device is already connected, skip opening: [%d]", _tcpSocket->state());
rc = true;
}
else
{
_tcpSocket->connectToHost( _host, _port);
if ( _tcpSocket->waitForConnected( CONNECT_TIMEOUT.count() ) )
{
if ( _tcpSocket->state() != QAbstractSocket::ConnectedState )
{
this->setInError( _tcpSocket->errorString() );
rc = false;
}
else
{
log (2,"open()","Successfully opened Yeelight: %s", QSTRING_CSTR(_host));
rc = true;
}
}
else
{
this->setInError( _tcpSocket->errorString() );
rc = false;
}
}
return rc;
}
bool YeelightLight::close()
{
bool rc = true;
if ( _tcpSocket != nullptr )
{
// Test, if device requires closing
if ( _tcpSocket->isOpen() )
{
log (2,"close()","Close Yeelight: %s", QSTRING_CSTR(_host));
_tcpSocket->close();
// Everything is OK -> device is closed
}
}
if ( _tcpStreamSocket != nullptr )
{
// Test, if stream socket requires closing
if ( _tcpStreamSocket->isOpen() )
{
log (2,"close()","Close stream Yeelight: %s", QSTRING_CSTR(_host));
_tcpStreamSocket->close();
}
}
return rc;
}
int YeelightLight::writeCommand( const QJsonDocument &command )
{
QJsonArray result;
return writeCommand(command, result );
}
int YeelightLight::writeCommand( const QJsonDocument &command, QJsonArray &result )
{
log( 3,
"writeCommand()",
"isON[%d], isInMusicMode[%d]",
static_cast<int>( _isOn ), static_cast<int>( _isInMusicMode ) );
if (_debugLevel >= 2)
{
QString help = command.toJson(QJsonDocument::Compact);
log (2,"writeCommand()","%s", QSTRING_CSTR(help));
}
int rc = -1;
if ( ! _isInError && _tcpSocket->isOpen() )
{
qint64 bytesWritten = _tcpSocket->write( command.toJson(QJsonDocument::Compact) + "\r\n");
if (bytesWritten == -1 )
{
this->setInError( QString ("Write Error: %1").arg(_tcpSocket->errorString()) );
}
else
{
if ( ! _tcpSocket->waitForBytesWritten(WRITE_TIMEOUT.count()) )
{
QString errorReason = QString ("(%1) %2").arg(_tcpSocket->error()).arg( _tcpSocket->errorString());
log ( 2, "Error:", "bytesWritten: [%lld], %s", bytesWritten, QSTRING_CSTR(errorReason));
this->setInError ( errorReason );
}
else
{
log ( 3, "Success:", "Bytes written [%lld]", bytesWritten );
// Avoid to overrun the Yeelight Command Quota
qint64 elapsedTime = QDateTime::currentMSecsSinceEpoch() - _lastWriteTime;
if ( elapsedTime < _waitTimeQuota )
{
int waitTime = _waitTimeQuota;
log ( 1, "writeCommand():", "Wait %dms, elapsedTime: %dms < quotaTime: %dms", waitTime, static_cast<int>(elapsedTime), _waitTimeQuota);
// Wait time (in ms) before doing next write to not overrun Yeelight command quota
std::this_thread::sleep_for(std::chrono::milliseconds(_waitTimeQuota));
}
}
if ( _tcpSocket->waitForReadyRead(READ_TIMEOUT.count()) )
{
do
{
log ( 3, "Reading:", "Bytes available [%lld]", _tcpSocket->bytesAvailable() );
while ( _tcpSocket->canReadLine() )
{
QByteArray response = _tcpSocket->readLine();
YeelightResponse yeeResponse = handleResponse( _correlationID, response );
switch ( yeeResponse.error() ) {
case YeelightResponse::API_NOTIFICATION:
rc=0;
break;
case YeelightResponse::API_OK:
result = yeeResponse.getResult();
rc=0;
break;
case YeelightResponse::API_ERROR:
result = yeeResponse.getResult();
QString errorReason = QString ("(%1) %2").arg(yeeResponse.getErrorCode()).arg( yeeResponse.getErrorReason() );
if ( yeeResponse.getErrorCode() != -1)
{
this->setInError ( errorReason );
rc =-1;
}
else
{
//(-1) client quota exceeded
log ( 1, "writeCommand():", "%s", QSTRING_CSTR(errorReason) );
rc = -2;
}
break;
}
}
log ( 3, "Info:", "Trying to read more responses");
}
while ( _tcpSocket->waitForReadyRead(500) );
}
log ( 3, "Info:", "No more responses available");
}
//In case of no error or quota exceeded, update late write time avoiding immediate next write
if ( rc == 0 || rc == -2 )
{
_lastWriteTime = QDateTime::currentMSecsSinceEpoch();
}
}
else
{
log ( 2, "Info:", "Skip write. Device is in error");
}
log (3,"writeCommand() rc","%d", rc );
return rc;
}
bool YeelightLight::streamCommand( const QJsonDocument &command )
{
// ToDo: Wofür gibt es isON, wenn es beim StreamCommand nicht verwendet wird?
//log (3,"streamCommand()","isON[%d], isInMusicMode[%d]", _isOn, _isInMusicMode );
if (_debugLevel >= 2)
{
QString help = command.toJson(QJsonDocument::Compact);
log (3,"streamCommand()","%s", QSTRING_CSTR(help));
}
bool rc = false;
if ( ! _isInError && _tcpStreamSocket->isOpen() )
{
qint64 bytesWritten = _tcpStreamSocket->write( command.toJson(QJsonDocument::Compact) + "\r\n");
if (bytesWritten == -1 )
{
this->setInError( QString ("Streaming Error %1").arg(_tcpStreamSocket->errorString()) );
}
else
{
if ( ! _tcpStreamSocket->waitForBytesWritten(WRITE_TIMEOUT.count()) )
{
int error = _tcpStreamSocket->error();
QString errorReason = QString ("(%1) %2").arg(error).arg( _tcpStreamSocket->errorString());
log ( 1, "Error:", "bytesWritten: [%lld], %s", bytesWritten, QSTRING_CSTR(errorReason));
if ( error == QAbstractSocket::RemoteHostClosedError )
{
log (1,"streamCommand()","RemoteHostClosedError - Give it a retry");
_isInMusicMode = false;
rc = true;
}
else
{
this->setInError ( errorReason );
}
}
else
{
log ( 3, "Success:", "Bytes written [%lld]", bytesWritten );
rc = true;
}
}
}
else
{
log ( 2, "Info:", "Skip write. Device is in error");
}
//log (2,"streamCommand() rc","%d, isON[%d], isInMusicMode[%d]", rc, _isOn, _isInMusicMode );
return rc;
}
YeelightResponse YeelightLight::handleResponse(int correlationID, QByteArray const &response )
{
log (3,"handleResponse()","" );
//std::cout << _name.toStdString() <<"| Response: [" << response.toStdString() << "]" << std::endl << std::flush;
YeelightResponse yeeResponse;
QString errorReason;
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(response, &error);
if (error.error != QJsonParseError::NoError)
{
yeeResponse.setErrorCode (-10000);
yeeResponse.setErrorReason( "Got invalid response" );
}
else
{
QString strJson(jsonDoc.toJson(QJsonDocument::Compact));
QJsonObject jsonObj = jsonDoc.object();
if ( !jsonObj[API_COMMAND_METHOD].isNull() )
{
yeeResponse.setError(YeelightResponse::API_NOTIFICATION);
yeeResponse.setResult( QJsonArray() );
// Do process notifications only for debugging
if ( verbose3 )
{
log ( 3, "Info:", "Notification found : [%s]", QSTRING_CSTR( jsonObj[API_COMMAND_METHOD].toString()));
QString method = jsonObj[API_COMMAND_METHOD].toString();
if ( method == API_COMMAND_PROPS )
{
if ( jsonObj.contains(API_COMMAND_PARAMS) && jsonObj[API_COMMAND_PARAMS].isObject() )
{
QVariantMap paramsMap = jsonObj[API_COMMAND_PARAMS].toVariant().toMap();
// Loop over all children.
for (const QString & property : paramsMap.keys())
{
QString value = paramsMap[property].toString();
log ( 3, "Notification ID:", "[%s]:[%s]", QSTRING_CSTR( property ), QSTRING_CSTR( value ));
}
}
}
else
{
log ( 1, "Error:", "Invalid notification message: [%s]", strJson.toUtf8().constData() );
}
}
}
else
{
int id = jsonObj[API_RESULT_ID].toInt();
//log ( 3, "Correlation ID:", "%d", id );
if ( id != correlationID && TEST_CORRELATION_IDS)
{
errorReason = QString ("%1| API is out of sync, received ID [%2], expected [%3]").
arg( _name ).arg( id ).arg( correlationID );
yeeResponse.setErrorCode (-11000);
yeeResponse.setErrorReason( errorReason );
this->setInError ( errorReason );
}
else
{
if ( jsonObj.contains(API_RESULT) && jsonObj[API_RESULT].isArray() )
{
// API call returned an result
yeeResponse.setResult( jsonObj[API_RESULT].toArray() );
// Break down result only for debugging
if ( verbose3 )
{
// Debug output
if(!yeeResponse.getResult().empty())
{
for(const QJsonValueRef item : yeeResponse.getResult())
{
log ( 3, "Result:", "%s", QSTRING_CSTR( item.toString() ));
}
}
}
}
else
{
yeeResponse.setError(YeelightResponse::API_ERROR);
if ( jsonObj.contains(API_ERROR) && jsonObj[API_ERROR].isObject() )
{
QVariantMap errorMap = jsonObj[API_ERROR].toVariant().toMap();
yeeResponse.setErrorCode (errorMap.value(API_ERROR_CODE).toInt());
yeeResponse.setErrorReason( errorMap.value(API_ERROR_MESSAGE).toString() );
}
else
{
yeeResponse.setErrorCode (-10010);
yeeResponse.setErrorReason( "No valid result message" );
log ( 1, "Reply:", "[%s]", strJson.toUtf8().constData());
}
}
}
}
}
log (3,"handleResponse()", "yeeResponse.error [%d]", yeeResponse.error() );
return yeeResponse;
}
void YeelightLight::setInError(const QString& errorMsg)
{
_isInError = true;
Error(_log, "Yeelight device '%s' signals error: '%s'", QSTRING_CSTR( _name ), QSTRING_CSTR(errorMsg));
}
QJsonDocument YeelightLight::getCommand(const QString &method, const QJsonArray &params)
{
//Increment Correlation-ID
++_correlationID;
QJsonObject obj;
obj.insert(API_COMMAND_ID,_correlationID);
obj.insert(API_COMMAND_METHOD,method);
obj.insert(API_COMMAND_PARAMS,params);
return QJsonDocument(obj);
}
QJsonObject YeelightLight::getProperties()
{
log (3,"getProperties()","" );
QJsonObject properties;
//Selected properties
//QJsonArray propertyList = { API_PROP_NAME, API_PROP_MODEL, API_PROP_POWER, API_PROP_RGB, API_PROP_BRIGHT, API_PROP_CT, API_PROP_FWVER };
//All properties
QJsonArray propertyList = {"power","bright","ct","rgb","hue","sat","color_mode","flowing","delayoff","music_on","name","bg_power","bg_flowing","bg_ct","bg_bright","bg_hue","bg_sat","bg_rgb","nl_br","active_mode" };
QJsonDocument command = getCommand( API_METHOD_GETPROP, propertyList );
QJsonArray result;
if ( writeCommand( command, result ) > -1 )
{
// Debug output
if( !result.empty())
{
int i = 0;
for(const QJsonValueRef item : result)
{
log (1,"Property:", "%s = %s", QSTRING_CSTR( propertyList.at(i).toString() ), QSTRING_CSTR( item.toString() ));
properties.insert( propertyList.at(i).toString(), item );
++i;
}
}
}
log (2,"getProperties()","QJsonObject");
return properties;
}
bool YeelightLight::identify()
{
log (3,"identify()","" );
bool rc = true;
/*
count 6, total number of visible state changing before color flow is stopped
action 0, 0 means smart LED recover to the state before the color flow started
Duration: 500, Gradual change timer sleep-time, in milliseconds
Mode: 1, color
Value: 100, RGB value when mode is 1 (blue)
Brightness: 100, Brightness value
Duration: 500
Mode: 1
Value: 16711696 (red)
Brightness: 10
*/
QJsonArray colorflowParams = { API_PROP_COLORFLOW, 6, 0, "500,1,100,100,500,1,16711696,10"};
//Blink White
//QJsonArray colorflowParams = { API_PROP_COLORFLOW, 6, 0, "500,2,4000,1,500,2,4000,50"};
QJsonDocument command = getCommand( API_METHOD_SETSCENE, colorflowParams );
if ( writeCommand( command ) < 0 )
{
rc= false;
}
log( 2, "identify() rc","%d", static_cast<int>(rc) );
return rc;
}
bool YeelightLight::isInMusicMode(bool deviceCheck)
{
bool inMusicMode = false;
if ( deviceCheck )
{
// Get status from device directly
QJsonArray propertylist = { API_PROP_MUSIC };
QJsonDocument command = getCommand( API_METHOD_GETPROP, propertylist );
QJsonArray result;
if ( writeCommand( command, result ) >= 0 )
{
if( !result.empty())
{
inMusicMode = result.at(0).toString() == "1";
}
}
}
else
{
// Test indirectly avoiding command quota
if ( _tcpStreamSocket != nullptr)
{
if ( _tcpStreamSocket->state() == QAbstractSocket::ConnectedState )
{
log (3,"isInMusicMode", "Yes, as socket is in ConnectedState");
inMusicMode = true;
}
else
{
log (1,"isInMusicMode", "No, StreamSocket state: %d", _tcpStreamSocket->state());
}
}
}
_isInMusicMode = inMusicMode;
log( 3, "isInMusicMode()", "%d", static_cast<int>( _isInMusicMode ) );
return _isInMusicMode;
}
void YeelightLight::mapProperties(const QJsonObject &properties)
{
log (3,"mapProperties()","" );
if ( _name.isEmpty() )
{
_name = properties.value(API_PROP_NAME).toString();
if ( _name.isEmpty() )
{
_name = _host;
}
}
_model = properties.value(API_PROP_MODEL).toString();
_fw_ver = properties.value(API_PROP_FWVER).toString();
_power = properties.value(API_PROP_POWER).toString();
_colorRgbValue = properties.value(API_PROP_RGB).toString().toInt();
_bright = properties.value(API_PROP_BRIGHT).toString().toInt();
_ct = properties.value(API_PROP_CT).toString().toInt();
log (2,"mapProperties() rc","void" );
}
void YeelightLight::storeState()
{
log (3,"storeState()","" );
_originalStateProperties = this->getProperties();
mapProperties( _originalStateProperties );
log (2,"storeState() rc","void" );
}
bool YeelightLight::restoreState()
{
log (3,"restoreState()","" );
bool rc = false;
QJsonArray paramlist = { API_PARAM_CLASS_COLOR, _colorRgbValue, _bright };
if ( _isInMusicMode )
{
rc = streamCommand( getCommand( API_METHOD_SETSCENE, paramlist ) );
}
else
{
if ( writeCommand( getCommand( API_METHOD_SETSCENE, paramlist ) ) >= 0 )
{
rc =true;
}
}
log( 2, "restoreState() rc","%d", static_cast<int>(rc) );
return rc;
}
bool YeelightLight::setPower(bool on)
{
return setPower( on, _transitionEffect, _transitionDuration);
}
bool YeelightLight::setPower(bool on, YeelightLight::API_EFFECT effect, int duration, API_MODE mode)
{
bool rc = false;
log( 3,
"setPower()",
"isON[%d], isInMusicMode[%d]",
static_cast<int>( _isOn), static_cast<int>(_isInMusicMode ) );
// Disable music mode to get power-off command executed
if ( !on && _isInMusicMode )
{
if ( _tcpStreamSocket != nullptr )
{
_tcpStreamSocket->close();
}
}
QString powerParam = on ? API_METHOD_POWER_ON : API_METHOD_POWER_OFF;
QString effectParam = effect == YeelightLight::API_EFFECT_SMOOTH ? API_PARAM_EFFECT_SMOOTH : API_PARAM_EFFECT_SUDDEN;
QJsonArray paramlist = { powerParam, effectParam, duration, mode };
// If power off was successful, automatically music-mode is off too
if ( writeCommand( getCommand( API_METHOD_POWER, paramlist ) ) > -1 )
{
_isOn = on;
if ( !on )
{
_isInMusicMode = false;
}
rc =true;
}
log( 2,
"setPower() rc",
"%d, isON[%d], isInMusicMode[)%d]",
static_cast<int>(rc), static_cast<int>( _isOn ), static_cast<int>( _isInMusicMode ) );
return rc;
}
bool YeelightLight::setColorRGB(const ColorRgb &color)
{
bool rc = true;
int colorParam = (color.red * 65536) + (color.green * 256) + color.blue;
if ( colorParam == 0 )
{
colorParam = 1;
}
if ( colorParam != _lastColorRgbValue )
{
int bri = std::max( { color.red, color.green, color.blue } ) * 100 / 255;
int duration = _transitionDuration;
if ( bri < _brightnessMin )
{
if ( _isBrightnessSwitchOffMinimum )
{
log( 2,
"Set Color RGB:",
"Turn off, brightness [%d] < _brightnessMin [%d], "
"_isBrightnessSwitchOffMinimum [%d]",
bri,_brightnessMin, static_cast<int>(_isBrightnessSwitchOffMinimum ) );
// Set brightness to 0
bri = 0;
duration = _transitionDuration + _extraTimeDarkness;
}
else
{
//If not switchOff on MinimumBrightness, avoid switch-off
log( 2,
"Set Color RGB:",
"Set brightness[%d] to minimum brightness [%d], if not _isBrightnessSwitchOffMinimum [%d]",
bri, _brightnessMin, static_cast<int>( _isBrightnessSwitchOffMinimum ) );
bri = _brightnessMin;
}
}
else
{
bri = ( qMin( _brightnessMax, static_cast<int> (_brightnessFactor * qMax( _brightnessMin, bri ) ) ) );
}
log ( 3, "Set Color RGB:", "{%u,%u,%u} -> [%d], [%d], [%d], [%d]", color.red, color.green, color.blue, colorParam, bri, _transitionEffect, _transitionDuration );
QJsonArray paramlist = { API_PARAM_CLASS_COLOR, colorParam, bri };
// Only add transition effect and duration, if device smoothing is configured (older FW do not support this parameters in set_scene
if ( _transitionEffect == YeelightLight::API_EFFECT_SMOOTH )
{
paramlist << _transitionEffectParam << duration;
}
bool writeOK = false;
if ( _isInMusicMode )
{
writeOK = streamCommand( getCommand( API_METHOD_SETSCENE, paramlist ) );
}
else
{
if ( writeCommand( getCommand( API_METHOD_SETSCENE, paramlist ) ) >= 0 )
{
writeOK = true;
}
}
if ( writeOK )
{
_lastColorRgbValue = colorParam;
}
else
{
rc = false;
}
}
//log (2,"setColorRGB() rc","%d, isON[%d], isInMusicMode[%d]", rc, _isOn, _isInMusicMode );
return rc;
}
bool YeelightLight::setColorHSV(const ColorRgb &colorRGB)
{
bool rc = true;
QColor color(colorRGB.red, colorRGB.green, colorRGB.blue);
if ( color != _color )
{
int hue;
int sat;
int bri;
int duration = _transitionDuration;
color.getHsv( &hue, &sat, &bri);
//Align to Yeelight number ranges (hue: 0-359, sat: 0-100, bri: 0-100)
if ( hue == -1)
{
hue = 0;
}
sat = sat * 100 / 255;
bri = bri * 100 / 255;
if ( bri < _brightnessMin )
{
if ( _isBrightnessSwitchOffMinimum )
{
log( 2,
"Set Color HSV:",
"Turn off, brightness [%d] < _brightnessMin [%d], "
"_isBrightnessSwitchOffMinimum [%d]",
bri,
_brightnessMin,
static_cast<int>( _isBrightnessSwitchOffMinimum ) );
// Set brightness to 0
bri = 0;
duration = _transitionDuration + _extraTimeDarkness;
}
else
{
//If not switchOff on MinimumBrightness, avoid switch-off
log( 2,
"Set Color HSV:",
"Set brightness[%d] to minimum brightness [%d], if not _isBrightnessSwitchOffMinimum [%d]",
bri, _brightnessMin, static_cast<int>( _isBrightnessSwitchOffMinimum ));
bri = _brightnessMin;
}
}
else
{
bri = ( qMin( _brightnessMax, static_cast<int> (_brightnessFactor * qMax( _brightnessMin, bri ) ) ) );
}
log ( 2, "Set Color HSV:", "{%u,%u,%u}, [%d], [%d]", hue, sat, bri, _transitionEffect, duration );
QJsonArray paramlist = { API_PARAM_CLASS_HSV, hue, sat, bri };
// Only add transition effect and duration, if device smoothing is configured (older FW do not support this parameters in set_scene
if ( _transitionEffect == YeelightLight::API_EFFECT_SMOOTH )
{
paramlist << _transitionEffectParam << duration;
}
bool writeOK=false;
if ( _isInMusicMode )
{
writeOK = streamCommand( getCommand( API_METHOD_SETSCENE, paramlist ) );
}
else
{
if ( writeCommand( getCommand( API_METHOD_SETSCENE, paramlist ) ) >= 0 )
{
writeOK = true;
}
}
if ( writeOK )
{
_isOn = true;
if ( bri == 0 )
{
_isOn = false;
_isInMusicMode = false;
}
_color = color;
}
else
{
rc = false;
}
}
else
{
//log ( 3, "setColorHSV", "Skip update. Same Color as before");
}
log( 3,
"setColorHSV() rc",
"%d, isON[%d], isInMusicMode[%d]",
static_cast<int>( rc ), static_cast<int>( _isOn ), static_cast<int>( _isInMusicMode ) );
return rc;
}
void YeelightLight::setTransitionEffect(YeelightLight::API_EFFECT effect, int duration)
{
if( effect != _transitionEffect )
{
_transitionEffect = effect;
_transitionEffectParam = effect == YeelightLight::API_EFFECT_SMOOTH ? API_PARAM_EFFECT_SMOOTH : API_PARAM_EFFECT_SUDDEN;
}
if( duration != _transitionDuration )
{
_transitionDuration = duration;
}
}
void YeelightLight::setBrightnessConfig(int min, int max, bool switchoff, int extraTime, double factor)
{
_brightnessMin = min;
_isBrightnessSwitchOffMinimum = switchoff;
_brightnessMax = max;
_brightnessFactor = factor;
_extraTimeDarkness = extraTime;
}
bool YeelightLight::setMusicMode(bool on, const QHostAddress &hostAddress, int port)
{
bool rc = false;
int musicModeParam = on ? API_METHOD_MUSIC_MODE_ON : API_METHOD_MUSIC_MODE_OFF;
QJsonArray paramlist = { musicModeParam };
if ( on )
{
paramlist << hostAddress.toString() << port;
}
// Music Mode is only on, if write did not fail nor quota was exceeded
if ( writeCommand( getCommand( API_METHOD_MUSIC_MODE, paramlist ) ) > -1 )
{
_isInMusicMode = on;
rc = true;
}
log( 2,
"setMusicMode() rc", "%d, isInMusicMode[%d]", static_cast<int>( rc ), static_cast<int>( _isInMusicMode ) );
return rc;
}
void YeelightLight::log(int logLevel, const char* msg, const char* type, ...)
{
if ( logLevel <= _debugLevel)
{
const size_t max_val_length = 1024;
char val[max_val_length];
va_list args;
va_start(args, type);
vsnprintf(val, max_val_length, type, args);
va_end(args);
std::string s = msg;
uint max = 20;
if (max > s.length())
{
s.append(max - s.length(), ' ');
}
Debug( _log, "%d|%15.15s| %s: %s", logLevel, QSTRING_CSTR(_name), s.c_str(), val);
}
}
//---------------------------------------------------------------------------------
LedDeviceYeelight::LedDeviceYeelight(const QJsonObject &deviceConfig)
: LedDevice(deviceConfig)
,_lightsCount (0)
,_outputColorModel(0)
,_transitionEffect(YeelightLight::API_EFFECT_SMOOTH)
,_transitionDuration(API_PARAM_DURATION.count())
,_extraTimeDarkness(0)
,_brightnessMin(0)
,_isBrightnessSwitchOffMinimum(false)
,_brightnessMax(100)
,_brightnessFactor(1.0)
,_waitTimeQuota(API_DEFAULT_QUOTA_WAIT_TIME)
,_debuglevel(0)
,_musicModeServerPort(-1)
{
}
LedDeviceYeelight::~LedDeviceYeelight()
{
delete _tcpMusicModeServer;
}
LedDevice* LedDeviceYeelight::construct(const QJsonObject &deviceConfig)
{
return new LedDeviceYeelight(deviceConfig);
}
bool LedDeviceYeelight::init(const QJsonObject &deviceConfig)
{
// Overwrite non supported/required features
setRewriteTime(0);
if (deviceConfig["rewriteTime"].toInt(0) > 0)
{
Info (_log, "Yeelights do not require rewrites. Refresh time is ignored.");
}
DebugIf(verbose, _log, "deviceConfig: [%s]", QString(QJsonDocument(_devConfig).toJson(QJsonDocument::Compact)).toUtf8().constData() );
bool isInitOK = false;
if ( LedDevice::init(deviceConfig) )
{
Debug(_log, "DeviceType : %s", QSTRING_CSTR( this->getActiveDeviceType() ));
Debug(_log, "LedCount : %d", this->getLedCount());
Debug(_log, "ColorOrder : %s", QSTRING_CSTR( this->getColorOrder() ));
Debug(_log, "RewriteTime : %d", this->getRewriteTime());
Debug(_log, "LatchTime : %d", this->getLatchTime());
//Get device specific configuration
if ( deviceConfig[ CONFIG_COLOR_MODEL ].isString() )
{
_outputColorModel = deviceConfig[ CONFIG_COLOR_MODEL ].toString(QString(MODEL_RGB)).toInt();
}
else
{
_outputColorModel = deviceConfig[ CONFIG_COLOR_MODEL ].toInt(MODEL_RGB);
}
if ( deviceConfig[ CONFIG_TRANS_EFFECT ].isString() )
{
_transitionEffect = static_cast<YeelightLight::API_EFFECT>( deviceConfig[ CONFIG_TRANS_EFFECT ].toString(QString(YeelightLight::API_EFFECT_SMOOTH)).toInt() );
}
else
{
_transitionEffect = static_cast<YeelightLight::API_EFFECT>( deviceConfig[ CONFIG_TRANS_EFFECT ].toInt(YeelightLight::API_EFFECT_SMOOTH) );
}
_transitionDuration = deviceConfig[ CONFIG_TRANS_TIME ].toInt(API_PARAM_DURATION.count());
_extraTimeDarkness = _devConfig[CONFIG_EXTRA_TIME_DARKNESS].toInt(0);
_brightnessMin = _devConfig[CONFIG_BRIGHTNESS_MIN].toInt(0);
_isBrightnessSwitchOffMinimum = _devConfig[CONFIG_BRIGHTNESS_SWITCHOFF].toBool(true);
_brightnessMax = _devConfig[CONFIG_BRIGHTNESS_MAX].toInt(100);
_brightnessFactor = _devConfig[CONFIG_BRIGHTNESSFACTOR].toDouble(1.0);
if ( deviceConfig[ CONFIG_DEBUGLEVEL ].isString() )
{
_debuglevel = deviceConfig[ CONFIG_DEBUGLEVEL ].toString(QString("0")).toInt();
}
else
{
_debuglevel = deviceConfig[ CONFIG_DEBUGLEVEL ].toInt(0);
}
QString outputColorModel = _outputColorModel == MODEL_RGB ? "RGB": "HSV";
QString transitionEffect = _transitionEffect == YeelightLight::API_EFFECT_SMOOTH ? API_PARAM_EFFECT_SMOOTH : API_PARAM_EFFECT_SUDDEN;
Debug(_log, "colorModel : %s", QSTRING_CSTR(outputColorModel));
Debug(_log, "Transitioneffect : %s", QSTRING_CSTR(transitionEffect));
Debug(_log, "Transitionduration: %d", _transitionDuration);
Debug(_log, "Extra time darkn. : %d", _extraTimeDarkness );
Debug(_log, "Brightn. Min : %d", _brightnessMin );
Debug(_log, "Brightn. Min Off : %d", _isBrightnessSwitchOffMinimum );
Debug(_log, "Brightn. Max : %d", _brightnessMax );
Debug(_log, "Brightn. Factor : %.2f", _brightnessFactor );
_isRestoreOrigState = _devConfig[CONFIG_RESTORE_STATE].toBool(false);
Debug(_log, "RestoreOrigState : %d", _isRestoreOrigState);
_waitTimeQuota = _devConfig[CONFIG_QUOTA_WAIT_TIME].toInt(0);
Debug(_log, "Wait time (quota) : %d", _waitTimeQuota );
Debug(_log, "Debuglevel : %d", _debuglevel);
QJsonArray configuredYeelightLights = _devConfig[CONFIG_LIGHTS].toArray();
int configuredYeelightsCount = 0;
for (const QJsonValueRef light : configuredYeelightLights)
{
QString hostName = light.toObject().value("host").toString();
int port = light.toObject().value("port").toInt(API_DEFAULT_PORT);
if ( !hostName.isEmpty() )
{
QString name = light.toObject().value("name").toString();
Debug(_log, "Light [%u] - %s (%s:%d)", configuredYeelightsCount, QSTRING_CSTR(name), QSTRING_CSTR(hostName), port );
++configuredYeelightsCount;
}
}
Debug(_log, "Light configured : %d", configuredYeelightsCount );
int configuredLedCount = this->getLedCount();
if (configuredYeelightsCount < configuredLedCount )
{
QString errorReason = QString("Not enough Yeelights [%1] for configured LEDs [%2] found!")
.arg(configuredYeelightsCount)
.arg(configuredLedCount);
this->setInError(errorReason);
isInitOK = false;
}
else
{
if ( configuredYeelightsCount > configuredLedCount )
{
Warning(_log, "More Yeelights defined [%d] than configured LEDs [%d].", configuredYeelightsCount, configuredLedCount );
}
_lightsAddressList.clear();
for (int j = 0; j < static_cast<int>( configuredLedCount ); ++j)
{
QString hostName = configuredYeelightLights[j].toObject().value("host").toString();
int port = configuredYeelightLights[j].toObject().value("port").toInt(API_DEFAULT_PORT);
QStringList addressparts = QStringUtils::split(hostName,":", QStringUtils::SplitBehavior::SkipEmptyParts);
QString apiHost = addressparts[0];
int apiPort = port;
_lightsAddressList.append( {apiHost, apiPort} );
}
if ( updateLights(_lightsAddressList) )
{
isInitOK = true;
}
}
}
return isInitOK;
}
bool LedDeviceYeelight::startMusicModeServer()
{
DebugIf(verbose, _log, "enabled [%d], _isDeviceReady [%d]", _isEnabled, _isDeviceReady);
bool rc = false;
if ( _tcpMusicModeServer == nullptr )
{
_tcpMusicModeServer = new QTcpServer(this);
}
if ( ! _tcpMusicModeServer->isListening() )
{
if (! _tcpMusicModeServer->listen())
{
QString errorReason = QString ("(%1) %2").arg(_tcpMusicModeServer->serverError()).arg( _tcpMusicModeServer->errorString());
Error( _log, "Error: MusicModeServer: %s", QSTRING_CSTR(errorReason));
this->setInError ( errorReason );
Error( _log, "Failed to start music mode server");
}
else
{
QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();
// use the first non-localhost IPv4 address
for (int i = 0; i < ipAddressesList.size(); ++i) {
if (ipAddressesList.at(i) != QHostAddress::LocalHost &&
(ipAddressesList.at(i).toIPv4Address() != 0U))
{
_musicModeServerAddress = ipAddressesList.at(i);
break;
}
}
if ( _musicModeServerAddress.isNull() )
{
Error( _log, "Failed to resolve IP for music mode server");
}
}
}
if ( _tcpMusicModeServer->isListening() )
{
_musicModeServerPort = _tcpMusicModeServer->serverPort();
Debug (_log, "The music mode server is running at %s:%d", QSTRING_CSTR(_musicModeServerAddress.toString()), _musicModeServerPort);
rc = true;
}
DebugIf(verbose, _log, "rc [%d], enabled [%d], _isDeviceReady [%d]", rc, _isEnabled, _isDeviceReady);
return rc;
}
bool LedDeviceYeelight::stopMusicModeServer()
{
DebugIf(verbose, _log, "enabled [%d], _isDeviceReady [%d]", _isEnabled, _isDeviceReady);
bool rc = false;
if ( _tcpMusicModeServer != nullptr )
{
Debug(_log, "Stop MusicModeServer");
_tcpMusicModeServer->close();
rc = true;
}
DebugIf(verbose, _log, "rc [%d], enabled [%d], _isDeviceReady [%d]", rc, _isEnabled, _isDeviceReady);
return rc;
}
int LedDeviceYeelight::open()
{
DebugIf(verbose, _log, "enabled [%d], _isDeviceReady [%d]", _isEnabled, _isDeviceReady);
int retval = -1;
_isDeviceReady = false;
// Open/Start LedDevice based on configuration
if ( !_lights.empty() )
{
if ( startMusicModeServer() )
{
int lightsInError = 0;
for (YeelightLight& light : _lights)
{
light.setTransitionEffect( _transitionEffect, _transitionDuration );
light.setBrightnessConfig( _brightnessMin, _brightnessMax, _isBrightnessSwitchOffMinimum, _extraTimeDarkness, _brightnessFactor );
light.setQuotaWaitTime(_waitTimeQuota);
light.setDebuglevel(_debuglevel);
if ( ! light.open() )
{
Error( _log, "Failed to open [%s]", QSTRING_CSTR(light.getName()) );
++lightsInError;
}
}
if ( lightsInError < static_cast<int>(_lights.size()) )
{
// Everything is OK -> enable device
_isDeviceReady = true;
retval = 0;
}
else
{
this->setInError( "All Yeelights failed to be opened!" );
}
}
}
else
{
// On error/exceptions, set LedDevice in error
}
DebugIf(verbose, _log, "retval [%d], enabled [%d], _isDeviceReady [%d]", retval, _isEnabled, _isDeviceReady);
return retval;
}
int LedDeviceYeelight::close()
{
DebugIf(verbose, _log, "enabled [%d], _isDeviceReady [%d]", _isEnabled, _isDeviceReady);
int retval = 0;
_isDeviceReady = false;
// LedDevice specific closing activities
//Close all Yeelight lights
for (YeelightLight& light : _lights)
{
light.close();
}
//Close music mode server
stopMusicModeServer();
DebugIf(verbose, _log, "retval [%d], enabled [%d], _isDeviceReady [%d]", retval, _isEnabled, _isDeviceReady);
return retval;
}
bool LedDeviceYeelight::updateLights(const QVector<yeelightAddress> &list)
{
bool rc = false;
DebugIf(verbose, _log, "enabled [%d], _isDeviceReady [%d]", _isEnabled, _isDeviceReady);
if(!_lightsAddressList.empty())
{
// search user light-id inside map and create light if found
_lights.clear();
_lights.reserve( static_cast<ulong>( _lightsAddressList.size() ));
for(auto & yeelightAddress : _lightsAddressList )
{
QString host = yeelightAddress.host;
if ( list.contains(yeelightAddress) )
{
int port = yeelightAddress.port;
Debug(_log,"Add Yeelight %s:%d", QSTRING_CSTR(host), port );
_lights.emplace_back( _log, host, port );
}
else
{
Warning(_log,"Configured light-address %s is not available", QSTRING_CSTR(host) );
}
}
setLightsCount ( static_cast<uint>( _lights.size() ));
rc = true;
}
return rc;
}
bool LedDeviceYeelight::powerOn()
{
if ( _isDeviceReady)
{
//Power-on all Yeelights
for (YeelightLight& light : _lights)
{
if ( light.isReady() && !light.isInMusicMode() )
{
light.setPower(true, YeelightLight::API_EFFECT_SMOOTH, 5000);
}
}
}
return true;
}
bool LedDeviceYeelight::powerOff()
{
if ( _isDeviceReady)
{
writeBlack();
//Power-off all Yeelights
for (YeelightLight& light : _lights)
{
light.setPower( false, _transitionEffect, API_PARAM_DURATION_POWERONOFF.count());
}
}
return true;
}
bool LedDeviceYeelight::storeState()
{
bool rc = true;
for (YeelightLight& light : _lights)
{
light.storeState();
}
return rc;
}
bool LedDeviceYeelight::restoreState()
{
bool rc = true;
for (YeelightLight& light : _lights)
{
light.restoreState();
if ( !light.wasOriginallyOn() )
{
light.setPower( false, _transitionEffect, API_PARAM_DURATION_POWERONOFF.count());
}
}
return rc;
}
QJsonArray LedDeviceYeelight::discover()
{
QJsonArray deviceList;
SSDPDiscover discover;
discover.setPort(SSDP_PORT);
discover.skipDuplicateKeys(true);
discover.setSearchFilter(SSDP_FILTER, SSDP_FILTER_HEADER);
QString searchTarget = SSDP_ID;
if ( discover.discoverServices(searchTarget) > 0 )
{
deviceList = discover.getServicesDiscoveredJson();
}
return deviceList;
}
QJsonObject LedDeviceYeelight::discover(const QJsonObject& /*params*/)
{
QJsonObject devicesDiscovered;
devicesDiscovered.insert("ledDeviceType", _activeDeviceType );
QString discoveryMethod("ssdp");
QJsonArray deviceList;
deviceList = discover();
devicesDiscovered.insert("devices", deviceList);
DebugIf(verbose,_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() );
return devicesDiscovered;
}
QJsonObject LedDeviceYeelight::getProperties(const QJsonObject& params)
{
DebugIf(verbose,_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() );
QJsonObject properties;
QString hostName = params["hostname"].toString("");
quint16 apiPort = static_cast<quint16>( params["port"].toInt(API_DEFAULT_PORT) );
if ( !hostName.isEmpty() )
{
YeelightLight yeelight(_log, hostName, apiPort);
//yeelight.setDebuglevel(3);
if ( yeelight.open() )
{
properties.insert("properties", yeelight.getProperties());
yeelight.close();
}
}
Debug(_log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData() );
return properties;
}
void LedDeviceYeelight::identify(const QJsonObject& params)
{
DebugIf(verbose,_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() );
QString hostName = params["hostname"].toString("");
quint16 apiPort = static_cast<quint16>( params["port"].toInt(API_DEFAULT_PORT) );
Debug (_log, "apiHost [%s], apiPort [%d]", QSTRING_CSTR(hostName), apiPort);
if ( !hostName.isEmpty() )
{
YeelightLight yeelight(_log, hostName, apiPort);
//yeelight.setDebuglevel(3);
if ( yeelight.open() )
{
yeelight.identify();
yeelight.close();
}
}
}
int LedDeviceYeelight::write(const std::vector<ColorRgb> & ledValues)
{
//DebugIf(verbose, _log, "enabled [%d], _isDeviceReady [%d]", _isEnabled, _isDeviceReady);
int rc = -1;
//Update on all Yeelights by iterating through lights and set colors.
unsigned int idx = 0;
int lightsInError = 0;
for (YeelightLight& light : _lights)
{
// Get color
ColorRgb color = ledValues.at(idx);
if ( light.isReady() )
{
bool skipWrite = false;
if ( !light.isInMusicMode() )
{
if ( light.setMusicMode(true, _musicModeServerAddress, _musicModeServerPort) )
{
// Wait for callback of the device to establish streaming socket
if ( _tcpMusicModeServer->waitForNewConnection(CONNECT_STREAM_TIMEOUT.count()) )
{
light.setStreamSocket( _tcpMusicModeServer->nextPendingConnection() );
}
else
{
QString errorReason = QString("(%1) %2").arg(_tcpMusicModeServer->serverError()).arg(_tcpMusicModeServer->errorString());
if (_tcpMusicModeServer->serverError() == QAbstractSocket::TemporaryError)
{
Info(_log, "Ignore write Error [%s]: _tcpMusicModeServer: %s", QSTRING_CSTR(light.getName()), QSTRING_CSTR(errorReason));
skipWrite = true;
}
else
{
Warning(_log, "write Error [%s]: _tcpMusicModeServer: %s", QSTRING_CSTR(light.getName()), QSTRING_CSTR(errorReason));
light.setInError("Failed to get stream socket");
}
}
}
else
{
DebugIf(verbose,_log, "setMusicMode failed due to command quota issue, skip write and try with next");
skipWrite = true;
}
}
if ( !skipWrite )
{
// Update light with given color
if ( _outputColorModel == MODEL_RGB )
{
light.setColorRGB( color );
}
else
{
light.setColorHSV( color );
}
}
}
else
{
++lightsInError;
}
++idx;
}
if ( ! (lightsInError < static_cast<int>(_lights.size())) )
{
this->setInError( "All Yeelights in error - stopping device!" );
}
else
{
// Minimum one Yeelight device is working, continue updating devices
rc = 0;
}
//DebugIf(verbose, _log, "rc [%d]", rc );
return rc;
}