hyperion.ng/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.cpp
2024-12-27 12:28:25 +01:00

445 lines
13 KiB
C++

// Local-Hyperion includes
#include "LedDeviceHomeAssistant.h"
#include <ssdp/SSDPDiscover.h>
// mDNS discover
#ifdef ENABLE_MDNS
#include <mdns/MdnsBrowser.h>
#include <mdns/MdnsServiceRegister.h>
#endif
#include <utils/NetUtils.h>
#include <utils/ColorRgb.h>
#include <algorithm>
// Constants
namespace {
const bool verbose = false;
// Configuration settings
const char CONFIG_HOST[] = "host";
const char CONFIG_PORT[] = "port";
const char CONFIG_AUTH_TOKEN[] = "token";
const char CONFIG_ENITYIDS[] = "entityIds";
const char CONFIG_BRIGHTNESS[] = "brightness";
const char CONFIG_BRIGHTNESS_OVERWRITE[] = "overwriteBrightness";
const char CONFIG_FULL_BRIGHTNESS_AT_START[] = "fullBrightnessAtStart";
const char CONFIG_TRANSITIONTIME[] = "transitionTime";
const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true;
const bool DEFAULT_IS_FULL_BRIGHTNESS_AT_START = true;
const int BRI_MAX = 255;
// Home Assistant API
const int API_DEFAULT_PORT = 8123;
const char API_BASE_PATH[] = "/api/";
const char API_STATES[] = "states";
const char API_LIGHT_TURN_ON[] = "services/light/turn_on";
const char API_LIGHT_TURN_OFF[] = "services/light/turn_off";
const char ENTITY_ID[] = "entity_id";
const char RGB_COLOR[] = "rgb_color";
const char BRIGHTNESS[] = "brightness";
const char TRANSITION[] = "transition";
const char FLASH[] = "flash";
// // Home Assistant ssdp services
const char SSDP_ID[] = "ssdp:all";
const char SSDP_FILTER_HEADER[] = "ST";
const char SSDP_FILTER[] = "(.*)home-assistant.io(.*)";
} //End of constants
LedDeviceHomeAssistant::LedDeviceHomeAssistant(const QJsonObject& deviceConfig)
: LedDevice(deviceConfig)
, _restApi(nullptr)
, _apiPort(API_DEFAULT_PORT)
, _isBrightnessOverwrite(DEFAULT_IS_BRIGHTNESS_OVERWRITE)
, _isFullBrightnessAtStart(DEFAULT_IS_FULL_BRIGHTNESS_AT_START)
, _brightness(BRI_MAX)
, _transitionTime(0)
{
#ifdef ENABLE_MDNS
QMetaObject::invokeMethod(MdnsBrowser::getInstance().data(), "browseForServiceType",
Qt::QueuedConnection, Q_ARG(QByteArray, MdnsServiceRegister::getServiceType(_activeDeviceType)));
#endif
}
LedDevice* LedDeviceHomeAssistant::construct(const QJsonObject& deviceConfig)
{
return new LedDeviceHomeAssistant(deviceConfig);
}
LedDeviceHomeAssistant::~LedDeviceHomeAssistant()
{
delete _restApi;
_restApi = nullptr;
}
bool LedDeviceHomeAssistant::init(const QJsonObject& deviceConfig)
{
bool isInitOK{ false };
if (LedDevice::init(deviceConfig))
{
// Overwrite non supported/required features
if (deviceConfig["rewriteTime"].toInt(0) > 0)
{
Info(_log, "Home Assistant lights do not require rewrites. Refresh time is ignored.");
setRewriteTime(0);
}
DebugIf(verbose, _log, "deviceConfig: [%s]", QString(QJsonDocument(_devConfig).toJson(QJsonDocument::Compact)).toUtf8().constData());
//Set hostname as per configuration and default port
_hostName = deviceConfig[CONFIG_HOST].toString();
_apiPort = deviceConfig[CONFIG_PORT].toInt(API_DEFAULT_PORT);
_bearerToken = deviceConfig[CONFIG_AUTH_TOKEN].toString();
_isBrightnessOverwrite = _devConfig[CONFIG_BRIGHTNESS_OVERWRITE].toBool(DEFAULT_IS_BRIGHTNESS_OVERWRITE);
_isFullBrightnessAtStart = _devConfig[CONFIG_FULL_BRIGHTNESS_AT_START].toBool(DEFAULT_IS_FULL_BRIGHTNESS_AT_START);
_brightness = _devConfig[CONFIG_BRIGHTNESS].toInt(BRI_MAX);
int transitionTimeMs = _devConfig[CONFIG_TRANSITIONTIME].toInt(0);
_transitionTime = transitionTimeMs / 1000.0;
Debug(_log, "Hostname/IP : %s", QSTRING_CSTR(_hostName));
Debug(_log, "Port : %d", _apiPort);
Debug(_log, "Overwrite Brightn.: %s", _isBrightnessOverwrite ? "Yes" : "No");
Debug(_log, "Set Brightness to : %d", _brightness);
Debug(_log, "Full Bri. at start: %s", _isFullBrightnessAtStart ? "Yes" : "No");
Debug(_log, "Transition Time : %d ms", transitionTimeMs);
_lightEntityIds = _devConfig[CONFIG_ENITYIDS].toVariant().toStringList();
int configuredLightsCount = _lightEntityIds.size();
if (configuredLightsCount == 0)
{
this->setInError("No light entity-ids configured");
isInitOK = false;
}
else
{
Debug(_log, "Lights configured : %d", configuredLightsCount);
isInitOK = true;
}
}
return isInitOK;
}
bool LedDeviceHomeAssistant::initLedsConfiguration()
{
bool isInitOK = false;
//Currently on one light is supported
QString lightEntityId = _lightEntityIds[0];
//Get properties for configured light entitiy to check availability
_restApi->setPath({ API_STATES, lightEntityId });
httpResponse response = _restApi->get();
if (response.error())
{
QString errorReason = QString("%1 get properties failed with error: '%2'").arg(_activeDeviceType, response.getErrorReason());
this->setInError(errorReason);
}
else
{
QJsonObject propertiesDetails = response.getBody().object();
if (propertiesDetails.isEmpty())
{
QString errorReason = QString("Light [%1] does not exist").arg(lightEntityId);
this->setInError(errorReason);
}
else
{
if (propertiesDetails.value("state").toString().compare("unavailable") == 0)
{
Warning(_log, "Light [%s] is currently unavailable", QSTRING_CSTR(lightEntityId));
}
isInitOK = true;
}
}
return isInitOK;
}
bool LedDeviceHomeAssistant::openRestAPI()
{
bool isInitOK{ true };
if (_restApi == nullptr)
{
if (_apiPort == 0)
{
_apiPort = API_DEFAULT_PORT;
}
_restApi = new ProviderRestApi(_address.toString(), _apiPort);
_restApi->setLogger(_log);
_restApi->setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
_restApi->setHeader("Authorization", QByteArrayLiteral("Bearer ") + _bearerToken.toUtf8());
//Base-path is api-path
_restApi->setBasePath(API_BASE_PATH);
}
return isInitOK;
}
int LedDeviceHomeAssistant::open()
{
int retval = -1;
_isDeviceReady = false;
if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort))
{
if (openRestAPI())
{
// Read LedDevice configuration and validate against device configuration
if (initLedsConfiguration())
{
// Everything is OK, device is ready
_isDeviceReady = true;
retval = 0;
}
}
else
{
_restApi->setHost(_address.toString());
_restApi->setPort(_apiPort);
}
}
return retval;
}
QJsonArray LedDeviceHomeAssistant::discoverSsdp() const
{
QJsonArray deviceList;
SSDPDiscover ssdpDiscover;
ssdpDiscover.skipDuplicateKeys(true);
ssdpDiscover.setSearchFilter(SSDP_FILTER, SSDP_FILTER_HEADER);
QString searchTarget = SSDP_ID;
if (ssdpDiscover.discoverServices(searchTarget) > 0)
{
deviceList = ssdpDiscover.getServicesDiscoveredJson();
}
return deviceList;
}
QJsonObject LedDeviceHomeAssistant::discover(const QJsonObject& /*params*/)
{
QJsonObject devicesDiscovered;
devicesDiscovered.insert("ledDeviceType", _activeDeviceType);
QJsonArray deviceList;
#ifdef ENABLE_MDNS
QString discoveryMethod("mDNS");
deviceList = MdnsBrowser::getInstance().data()->getServicesDiscoveredJson(
MdnsServiceRegister::getServiceType(_activeDeviceType),
MdnsServiceRegister::getServiceNameFilter(_activeDeviceType),
DEFAULT_DISCOVER_TIMEOUT
);
#else
QString discoveryMethod("ssdp");
deviceList = discoverSsdp();
#endif
devicesDiscovered.insert("discoveryMethod", discoveryMethod);
devicesDiscovered.insert("devices", deviceList);
DebugIf(verbose, _log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData());
return devicesDiscovered;
}
QJsonObject LedDeviceHomeAssistant::getProperties(const QJsonObject& params)
{
DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData());
QJsonObject properties;
_hostName = params[CONFIG_HOST].toString("");
_apiPort = API_DEFAULT_PORT;
_bearerToken = params[CONFIG_AUTH_TOKEN].toString("");
Info(_log, "Get properties for %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName));
if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort))
{
if (openRestAPI())
{
QString filter = params["filter"].toString("");
_restApi->setPath(filter);
// Perform request
httpResponse response = _restApi->get();
if (response.error())
{
Warning(_log, "%s get properties failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason()));
}
QJsonObject propertiesDetails;
const QJsonDocument jsonDoc = response.getBody();
if (jsonDoc.isArray()) {
const QJsonArray jsonArray = jsonDoc.array();
QVector<QJsonValue> filteredVector;
// Iterate over the array and filter objects with entity_id starting with "light."
for (const QJsonValue& value : jsonArray)
{
QJsonObject obj = value.toObject();
QString entityId = obj[ENTITY_ID].toString();
if (entityId.startsWith("light."))
{
filteredVector.append(obj);
}
}
// Sort the filtered vector by "friendly_name" in ascending order
std::sort(filteredVector.begin(), filteredVector.end(), [](const QJsonValue& a, const QJsonValue& b) {
QString nameA = a.toObject()["attributes"].toObject()["friendly_name"].toString();
QString nameB = b.toObject()["attributes"].toObject()["friendly_name"].toString();
return nameA < nameB; // Ascending order
});
// Convert the sorted vector back to a QJsonArray
QJsonArray sortedArray;
for (const QJsonValue& value : filteredVector) {
sortedArray.append(value);
}
propertiesDetails.insert("lightEntities", sortedArray);
}
if (!propertiesDetails.isEmpty())
{
propertiesDetails.insert("ledCount", 1);
}
properties.insert("properties", propertiesDetails);
}
DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData());
}
return properties;
}
void LedDeviceHomeAssistant::identify(const QJsonObject& params)
{
DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData());
_hostName = params[CONFIG_HOST].toString("");
_apiPort = API_DEFAULT_PORT;
_bearerToken = params[CONFIG_AUTH_TOKEN].toString("");
Info(_log, "Identify %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName));
if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort))
{
if (openRestAPI())
{
QJsonArray lightEntityIds = params[ENTITY_ID].toArray();
_restApi->setPath(API_LIGHT_TURN_ON);
QJsonObject serviceAttributes{ {ENTITY_ID, lightEntityIds} };
serviceAttributes.insert(FLASH, "short");
httpResponse response = _restApi->post(serviceAttributes);
if (response.error())
{
Warning(_log, "%s identification failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason()));
}
}
}
}
bool LedDeviceHomeAssistant::powerOn()
{
bool isOn = false;
if (_isDeviceReady)
{
_restApi->setPath(API_LIGHT_TURN_ON);
QJsonObject serviceAttributes{ {ENTITY_ID, QJsonArray::fromStringList(_lightEntityIds)} };
if (_isFullBrightnessAtStart)
{
serviceAttributes.insert(BRIGHTNESS, BRI_MAX);
}
httpResponse response = _restApi->post(serviceAttributes);
if (response.error())
{
QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason());
this->setInError(errorReason);
isOn = false;
}
else {
isOn = true;
}
}
return isOn;
}
bool LedDeviceHomeAssistant::powerOff()
{
bool isOff = true;
if (_isDeviceReady)
{
_restApi->setPath(API_LIGHT_TURN_OFF);
QJsonObject serviceAttributes{ {ENTITY_ID, QJsonArray::fromStringList(_lightEntityIds)} };
httpResponse response = _restApi->post(serviceAttributes);
if (response.error())
{
QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason());
this->setInError(errorReason);
isOff = false;
}
}
return isOff;
}
int LedDeviceHomeAssistant::write(const std::vector<ColorRgb>& ledValues)
{
int retVal = 0;
QJsonObject serviceAttributes{ {ENTITY_ID, QJsonArray::fromStringList(_lightEntityIds)} };
ColorRgb ledValue = ledValues.at(0);
// http://hostname:port/api/services/light/turn_on
// {
// "entity_id": [ entity-IDs ],
// "rgb_color": [R,G,B]
// }
_restApi->setPath(API_LIGHT_TURN_ON);
serviceAttributes.insert(RGB_COLOR, QJsonArray{ ledValue.red, ledValue.green, ledValue.blue });
int brightness = _brightness;
// Some devices cannot deal with a black color and brightness > 0
if (ledValue == ColorRgb::BLACK)
{
brightness = 0;
}
// Add brightness attribute if applicable
if (brightness == 0 || _isBrightnessOverwrite)
{
serviceAttributes.insert(BRIGHTNESS, brightness);
}
if (_transitionTime > 0)
{
serviceAttributes.insert(TRANSITION, _transitionTime);
}
httpResponse response = _restApi->post(serviceAttributes);
if (response.error())
{
Warning(_log, "Updating lights failed with error: '%s'", QSTRING_CSTR(response.getErrorReason()));
retVal = -1;
}
return retVal;
}