mirror of
				https://github.com/f4exb/sdrangel.git
				synced 2025-11-03 13:11:20 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			332 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			332 lines
		
	
	
		
			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";
 | 
						|
    }
 | 
						|
}
 |