sdrangel/sdrbase/util/iot/homeassistant.cpp

332 wiersze
14 KiB
C++

///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 Jon Beniston, M7RCE <jon@beniston.com> //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include "util/iot/homeassistant.h"
HomeAssistantDevice::HomeAssistantDevice(const QString& apiKey, const QString& url, const QString &deviceId,
const QStringList &controls, const QStringList &sensors,
DeviceDiscoverer::DeviceInfo *info) :
Device(info),
m_deviceId(deviceId),
m_apiKey(apiKey),
m_url(url)
{
m_entities = controls;
m_entities.append(sensors);
m_networkManager = new QNetworkAccessManager();
QObject::connect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&HomeAssistantDevice::handleReply
);
}
HomeAssistantDevice::~HomeAssistantDevice()
{
QObject::disconnect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&HomeAssistantDevice::handleReply
);
delete m_networkManager;
}
void HomeAssistantDevice::getState()
{
// Get state for all entities of the device
for (auto entity : m_entities)
{
QUrl url(m_url + "/api/states/" + entity);
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer " + m_apiKey.toLocal8Bit());
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->get(request);
recordGetRequest(reply);
}
}
void HomeAssistantDevice::setState(const QString &controlId, bool state)
{
QString domain = controlId.left(controlId.indexOf("."));
QUrl url(m_url + "/api/services/" + domain + "/turn_" + (state ? "on" : "off"));
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer " + m_apiKey.toLocal8Bit());
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject object {
{"entity_id", controlId}
};
QJsonDocument document;
document.setObject(object);
m_networkManager->post(request, document.toJson());
// 750ms guard, to try to avoid toggling of widget, while state updates on server
// Perhaps should be a setting
recordSetRequest(controlId, 750);
}
void HomeAssistantDevice::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument document = QJsonDocument::fromJson(data, &error);
if (!document.isNull())
{
//qDebug() << "Received " << document;
// POSTs to /api/services return an array, GETs from /api/states return an object
if (document.isObject())
{
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("entity_id")) && obj.contains(QStringLiteral("state")))
{
QString entityId = obj.value(QStringLiteral("entity_id")).toString();
if (getAfterSet(reply, entityId))
{
QHash<QString, QVariant> status;
QString state = obj.value(QStringLiteral("state")).toString();
bool dOk;
bool iOk;
int i = state.toInt(&iOk);
double d = state.toDouble(&dOk);
if ((state == "on") || (state == "playing")) {
status.insert(entityId, 1);
} else if ((state == "off") || (state == "paused")) {
status.insert(entityId, 0);
} else if (iOk) {
status.insert(entityId, i);
} else if (dOk) {
status.insert(entityId, d);
} else {
status.insert(entityId, state);
}
emit deviceUpdated(status);
}
}
}
}
else
{
qDebug() << "HomeAssistantDevice::handleReply: Error parsing JSON: " << error.errorString() << " at offset " << error.offset;
}
}
else
{
qDebug() << "HomeAssistantDevice::handleReply: error: " << reply->error();
}
removeGetRequest(reply);
reply->deleteLater();
}
else
{
qDebug() << "HomeAssistantDevice::handleReply: reply is null";
}
}
HomeAssistantDeviceDiscoverer::HomeAssistantDeviceDiscoverer(const QString& apiKey, const QString& url) :
m_apiKey(apiKey),
m_url(url)
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&HomeAssistantDeviceDiscoverer::handleReply
);
}
HomeAssistantDeviceDiscoverer::~HomeAssistantDeviceDiscoverer()
{
QObject::disconnect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&HomeAssistantDeviceDiscoverer::handleReply
);
delete m_networkManager;
}
void HomeAssistantDeviceDiscoverer::getDevices()
{
QUrl url(m_url+ "/api/template");
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer " + m_apiKey.toLocal8Bit());
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
// Use templates to get a list of devices and associated entities
QString tpl =
"{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq',None)| list %}\n"
"{%- set ns = namespace(devices = []) %}\n"
"{%- for device in devices %}\n"
" {%- set entities = device_entities(device) | list %}\n"
" {%- if entities %}\n"
" {%- set ens = namespace(entityobjs = []) %}\n"
" {%- for entity in entities %}\n"
" {%- set entityobj = {'entity_id': entity, 'name': state_attr(entity,'friendly_name'), 'unit_of_measurement': state_attr(entity,'unit_of_measurement')} %}\n"
" {%- set ens.entityobjs = ens.entityobjs + [ entityobj ] %}\n"
" {%- endfor %}\n"
" {%- set obj = {'device_id': device, 'name': device_attr(device,'name'), 'name_by_user': device_attr(device,'name_by_user'), 'model': device_attr(device,'model'), 'entities': ens.entityobjs } %}\n"
" {%- set ns.devices = ns.devices + [ obj ] %}\n"
" {%- endif %}\n"
"{%- endfor %}\n"
"{{ ns.devices | tojson }}";
QJsonObject object {
{"template", tpl}
};
QJsonDocument document;
document.setObject(object);
m_networkManager->post(request, document.toJson());
}
void HomeAssistantDeviceDiscoverer::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QList<DeviceInfo> devices;
QByteArray data = reply->readAll();
//qDebug() << "Received " << data;
QJsonParseError error;
QJsonDocument document = QJsonDocument::fromJson(data, &error);
if (!document.isNull())
{
if (document.isArray())
{
for (auto deviceRef : document.array())
{
QJsonObject deviceObj = deviceRef.toObject();
if (deviceObj.contains(QStringLiteral("device_id")) && deviceObj.contains(QStringLiteral("entities")))
{
QJsonArray entitiesArray = deviceObj.value(QStringLiteral("entities")).toArray();
if (entitiesArray.size() > 0)
{
DeviceInfo info;
info.m_id = deviceObj.value(QStringLiteral("device_id")).toString();
if (deviceObj.contains(QStringLiteral("name_by_user"))) {
info.m_name = deviceObj.value(QStringLiteral("name_by_user")).toString();
}
if (info.m_name.isEmpty() && deviceObj.contains(QStringLiteral("name"))) {
info.m_name = deviceObj.value(QStringLiteral("name")).toString();
}
if (deviceObj.contains(QStringLiteral("model"))) {
info.m_model = deviceObj.value(QStringLiteral("model")).toString();
}
for (auto entityRef : entitiesArray)
{
QJsonObject entityObj = entityRef.toObject();
QString entity = entityObj.value(QStringLiteral("entity_id")).toString();
QString name = entityObj.value(QStringLiteral("name")).toString();
QString domain = entity.left(entity.indexOf('.'));
if (domain == "binary_sensor")
{
SensorInfo *sensorInfo = new SensorInfo();
sensorInfo->m_name = name;
sensorInfo->m_id = entity;
sensorInfo->m_type = DeviceDiscoverer::BOOL;
sensorInfo->m_units = entityObj.value(QStringLiteral("unit_of_measurement")).toString();
info.m_sensors.append(sensorInfo);
}
else if (domain == "sensor")
{
SensorInfo *sensorInfo = new SensorInfo();
sensorInfo->m_name = name;
sensorInfo->m_id = entity;
sensorInfo->m_type = DeviceDiscoverer::FLOAT; // FIXME: Auto?
sensorInfo->m_units = entityObj.value(QStringLiteral("unit_of_measurement")).toString();
info.m_sensors.append(sensorInfo);
}
else if ((domain == "switch") || (domain == "light") || (domain == "media_player")) // Entities that support turn_on/turn_off
{
ControlInfo *controlInfo = new ControlInfo();
controlInfo->m_name = name;
controlInfo->m_id = entity;
controlInfo->m_type = DeviceDiscoverer::BOOL;
info.m_controls.append(controlInfo);
}
else
{
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: Unsupported domain: " << domain;
}
}
if ((info.m_controls.size() > 0) || (info.m_sensors.size() > 0))
{
devices.append(info);
}
}
else
{
//qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: No entities " << deviceObj.value(QStringLiteral("device_id")).toString();
}
}
else
{
//qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: device_id or entities missing";
}
}
}
else
{
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: Document is not an array: " << document;
}
}
else
{
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: Error parsing JSON: " << error.errorString() << " at offset " << error.offset;
}
emit deviceList(devices);
}
else
{
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: error: " << reply->error() << ":" << reply->errorString();
// Get QNetworkReply::AuthenticationRequiredError if token is invalid
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
emit error("Home Assistant: Authentication failed. Check access token is valid.");
} else {
emit error(QString("Home Assistant: Network error. %1").arg(reply->errorString()));
}
}
reply->deleteLater();
}
else
{
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: reply is null";
}
}