2022-09-16 05:01:25 -04:00
|
|
|
///////////////////////////////////////////////////////////////////////////////////
|
2023-11-19 00:43:20 -05:00
|
|
|
// Copyright (C) 2022 Jon Beniston, M7RCE <jon@beniston.com> //
|
2022-09-16 05:01:25 -04:00
|
|
|
// //
|
|
|
|
// 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),
|
2022-09-16 06:41:40 -04:00
|
|
|
m_deviceId(deviceId),
|
2022-09-16 05:01:25 -04:00
|
|
|
m_apiKey(apiKey),
|
2022-09-16 06:41:40 -04:00
|
|
|
m_url(url)
|
2022-09-16 05:01:25 -04:00
|
|
|
{
|
|
|
|
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");
|
2022-09-16 09:29:55 -04:00
|
|
|
QNetworkReply *reply = m_networkManager->get(request);
|
|
|
|
recordGetRequest(reply);
|
2022-09-16 05:01:25 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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());
|
2022-09-16 09:29:55 -04:00
|
|
|
|
|
|
|
// 750ms guard, to try to avoid toggling of widget, while state updates on server
|
|
|
|
// Perhaps should be a setting
|
|
|
|
recordSetRequest(controlId, 750);
|
2022-09-16 05:01:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
void HomeAssistantDevice::handleReply(QNetworkReply* reply)
|
|
|
|
{
|
|
|
|
if (reply)
|
|
|
|
{
|
|
|
|
if (!reply->error())
|
|
|
|
{
|
2022-09-16 09:29:55 -04:00
|
|
|
QByteArray data = reply->readAll();
|
|
|
|
QJsonParseError error;
|
|
|
|
QJsonDocument document = QJsonDocument::fromJson(data, &error);
|
|
|
|
if (!document.isNull())
|
2022-09-16 05:01:25 -04:00
|
|
|
{
|
2022-09-16 09:29:55 -04:00
|
|
|
//qDebug() << "Received " << document;
|
|
|
|
// POSTs to /api/services return an array, GETs from /api/states return an object
|
|
|
|
if (document.isObject())
|
2022-09-16 05:01:25 -04:00
|
|
|
{
|
2022-09-16 09:29:55 -04:00
|
|
|
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);
|
|
|
|
}
|
2022-09-16 05:01:25 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-09-16 09:29:55 -04:00
|
|
|
qDebug() << "HomeAssistantDevice::handleReply: Error parsing JSON: " << error.errorString() << " at offset " << error.offset;
|
2022-09-16 05:01:25 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
qDebug() << "HomeAssistantDevice::handleReply: error: " << reply->error();
|
|
|
|
}
|
2022-09-16 09:29:55 -04:00
|
|
|
removeGetRequest(reply);
|
2022-09-16 05:01:25 -04:00
|
|
|
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
|
|
|
|
{
|
2022-09-16 09:29:55 -04:00
|
|
|
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: Error parsing JSON: " << error.errorString() << " at offset " << error.offset;
|
2022-09-16 05:01:25 -04:00
|
|
|
}
|
|
|
|
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";
|
|
|
|
}
|
|
|
|
}
|