1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-11-15 12:51:49 -05:00

util/iot: Add API for accessing IoT / Smart Home devices.

This commit is contained in:
Jon Beniston 2022-09-16 10:01:25 +01:00
parent 83a94fc375
commit a4cd8af538
9 changed files with 2367 additions and 0 deletions

View File

@ -207,6 +207,10 @@ set(sdrbase_SOURCES
util/timeutil.cpp util/timeutil.cpp
util/visa.cpp util/visa.cpp
util/weather.cpp util/weather.cpp
util/iot/device.cpp
util/iot/homeassistant.cpp
util/iot/tplink.cpp
util/iot/visa.cpp
plugin/plugininterface.cpp plugin/plugininterface.cpp
plugin/pluginapi.cpp plugin/pluginapi.cpp
@ -426,6 +430,10 @@ set(sdrbase_HEADERS
util/timeutil.h util/timeutil.h
util/visa.h util/visa.h
util/weather.h util/weather.h
util/iot/device.h
util/iot/homeassistant.h
util/iot/tplink.h
util/iot/visa.h
webapi/webapiadapter.h webapi/webapiadapter.h
webapi/webapiadapterbase.h webapi/webapiadapterbase.h

529
sdrbase/util/iot/device.cpp Normal file
View File

@ -0,0 +1,529 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 Jon Beniston, M7RCE //
// //
// 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/simpleserializer.h"
#include "util/iot/device.h"
#include "util/iot/tplink.h"
#include "util/iot/homeassistant.h"
#include "util/iot/visa.h"
Device::Device(DeviceDiscoverer::DeviceInfo *info)
{
if (info) {
m_info = *info;
}
}
Device* Device::create(const QHash<QString, QVariant>& settings, const QString& protocol, DeviceDiscoverer::DeviceInfo *info)
{
if (checkSettings(settings, protocol))
{
if (protocol == "TPLink")
{
if (settings.contains("deviceId"))
{
return new TPLinkDevice(settings.value("username").toString(),
settings.value("password").toString(),
settings.value("deviceId").toString(),
info);
}
else
{
qDebug() << "Device::create: A deviceId is required for: " << protocol;
return nullptr;
}
}
else if (protocol == "HomeAssistant")
{
if (checkSettings(settings, protocol))
{
if (settings.contains("deviceId"))
{
return new HomeAssistantDevice(settings.value("apiKey").toString(),
settings.value("url").toString(),
settings.value("deviceId").toString(),
settings.value("controlIds").toStringList(),
settings.value("sensorIds").toStringList(),
info);
}
else
{
qDebug() << "Device::create: A deviceId is required for: " << protocol;
return nullptr;
}
}
}
else if (protocol == "VISA")
{
return new VISADevice(settings,
settings.value("deviceId").toString(),
settings.value("controlIds").toStringList(),
settings.value("sensorIds").toStringList(),
info);
}
}
else
{
return nullptr;
}
}
bool Device::checkSettings(const QHash<QString, QVariant>& settings, const QString& protocol)
{
if (protocol == "TPLink")
{
if (settings.contains("username") && settings.contains("password"))
{
return true;
}
else
{
qDebug() << "Device::checkSettings: A username and password are required for: " << protocol;
return false;
}
}
else if (protocol == "HomeAssistant")
{
if (settings.contains("apiKey"))
{
if (settings.contains("url"))
{
return true;
}
else
{
qDebug() << "Device::checkSettings: A host url is required for: " << protocol;
return false;
}
}
else
{
qDebug() << "Device::checkSettings: An apiKey is required for: " << protocol;
return false;
}
}
else if (protocol == "VISA")
{
return true;
}
else
{
qDebug() << "Device::checkSettings: Unsupported protocol: " << protocol;
return false;
}
}
const QStringList DeviceDiscoverer::m_typeStrings = {
"Auto",
"Boolean",
"Integer",
"Float",
"String",
"List",
"Button"
};
const QStringList DeviceDiscoverer::m_widgetTypeStrings = {
"Spin box",
"Dial",
"Slider"
};
DeviceDiscoverer::DeviceDiscoverer()
{
}
DeviceDiscoverer *DeviceDiscoverer::getDiscoverer(const QHash<QString, QVariant>& settings, const QString& protocol)
{
if (Device::checkSettings(settings, protocol))
{
if (protocol == "TPLink")
{
return new TPLinkDeviceDiscoverer(settings.value("username").toString(), settings.value("password").toString());
}
else if (protocol == "HomeAssistant")
{
return new HomeAssistantDeviceDiscoverer(settings.value("apiKey").toString(), settings.value("url").toString());
}
else if (protocol == "VISA")
{
return new VISADeviceDiscoverer(settings.value("resourceFilter").toString());
}
}
return nullptr;
}
DeviceDiscoverer::DeviceInfo::DeviceInfo()
{
}
DeviceDiscoverer::DeviceInfo::DeviceInfo(const DeviceInfo &info)
{
m_name = info.m_name;
m_id = info.m_id;
m_model = info.m_model;
// Take deep-copy of controls and sensors
for (auto const control : info.m_controls) {
ControlInfo *ci = control->clone();
m_controls.append(ci);
}
for (auto const sensor : info.m_sensors) {
m_sensors.append(sensor->clone());
}
}
DeviceDiscoverer::DeviceInfo& DeviceDiscoverer::DeviceInfo::operator=(const DeviceInfo &info)
{
m_name = info.m_name;
m_id = info.m_id;
m_model = info.m_model;
qDeleteAll(m_controls);
m_controls.clear();
qDeleteAll(m_sensors);
m_sensors.clear();
// Take deep-copy of controls and sensors
for (auto const control : info.m_controls) {
m_controls.append(control->clone());
}
for (auto const sensor : info.m_sensors) {
m_sensors.append(sensor->clone());
}
return *this;
}
DeviceDiscoverer::DeviceInfo::~DeviceInfo()
{
qDeleteAll(m_controls);
m_controls.clear();
qDeleteAll(m_sensors);
m_sensors.clear();
}
DeviceDiscoverer::DeviceInfo::operator QString() const
{
QString controls;
QString sensors;
for (auto control : m_controls) {
controls.append((QString)*control);
}
for (auto sensor : m_sensors) {
sensors.append((QString)*sensor);
}
return QString("DeviceInfo: m_name: %1 m_id: %2 m_model: %3 m_controls: %4 m_sensors: %5")
.arg(m_name)
.arg(m_id)
.arg(m_model)
.arg(controls)
.arg(sensors);
}
DeviceDiscoverer::ControlInfo::ControlInfo() :
m_type(AUTO),
m_min(-1000000),
m_max(1000000),
m_scale(1.0f),
m_precision(3),
m_widgetType(SPIN_BOX)
{
}
DeviceDiscoverer::ControlInfo::operator QString() const
{
return QString("ControlInfo: m_name: %1 m_id: %2 m_type: %3")
.arg(m_name)
.arg(m_id)
.arg(DeviceDiscoverer::m_typeStrings[m_type]);
}
DeviceDiscoverer::ControlInfo *DeviceDiscoverer::ControlInfo::clone() const
{
return new ControlInfo(*this);
}
QByteArray DeviceDiscoverer::ControlInfo::serialize() const
{
SimpleSerializer s(1);
s.writeString(1, m_name);
s.writeString(2, m_id);
s.writeS32(3, (int)m_type);
s.writeFloat(4, m_min);
s.writeFloat(5, m_max);
s.writeFloat(6, m_scale);
s.writeS32(7, m_precision);
s.writeList(8, m_values);
s.writeS32(9, (int)m_widgetType);
s.writeString(10, m_units);
return s.final();
}
bool DeviceDiscoverer::ControlInfo::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid()) {
return false;
}
if (d.getVersion() == 1)
{
d.readString(1, &m_name);
d.readString(2, &m_id);
d.readS32(3, (int*)&m_type);
d.readFloat(4, &m_min);
d.readFloat(5, &m_max);
d.readFloat(6, &m_scale, 1.0f);
d.readS32(7, &m_precision, 3);
d.readList(8, &m_values);
d.readS32(9, (int *)&m_widgetType);
d.readString(10, &m_units);
return true;
}
else
{
return false;
}
}
DeviceDiscoverer::SensorInfo::operator QString() const
{
return QString("SensorInfo: m_name: %1 m_id: %2 m_type: %3")
.arg(m_name)
.arg(m_id)
.arg(DeviceDiscoverer::m_typeStrings[m_type]);
}
DeviceDiscoverer::SensorInfo *DeviceDiscoverer::SensorInfo::clone() const
{
return new SensorInfo(*this);
}
QByteArray DeviceDiscoverer::SensorInfo::serialize() const
{
SimpleSerializer s(1);
s.writeString(1, m_name);
s.writeString(2, m_id);
s.writeS32(3, (int)m_type);
s.writeString(4, m_units);
return s.final();
}
bool DeviceDiscoverer::SensorInfo::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid()) {
return false;
}
if (d.getVersion() == 1)
{
d.readString(1, &m_name);
d.readString(2, &m_id);
d.readS32(3, (int*)&m_type);
d.readString(4, &m_units);
return true;
}
else
{
return false;
}
}
QByteArray DeviceDiscoverer::DeviceInfo::serialize() const
{
SimpleSerializer s(1);
s.writeString(1, m_name);
s.writeString(2, m_id);
s.writeString(3, m_model);
s.writeList(10, m_controls);
s.writeList(11, m_sensors);
return s.final();
}
bool DeviceDiscoverer::DeviceInfo::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid()) {
return false;
}
if (d.getVersion() == 1)
{
QByteArray blob;
d.readString(1, &m_name);
d.readString(2, &m_id);
d.readString(3, &m_model);
d.readList(10, &m_controls);
d.readList(11, &m_sensors);
return true;
}
else
{
return false;
}
}
DeviceDiscoverer::ControlInfo *DeviceDiscoverer::DeviceInfo::getControl(const QString &id) const
{
for (auto c : m_controls)
{
if (c->m_id == id) {
return c;
}
}
return nullptr;
}
DeviceDiscoverer::SensorInfo *DeviceDiscoverer::DeviceInfo::getSensor(const QString &id) const
{
for (auto s : m_sensors)
{
if (s->m_id == id) {
return s;
}
}
return nullptr;
}
void DeviceDiscoverer::DeviceInfo::deleteControl(const QString &id)
{
for (int i = 0; i < m_controls.size(); i++)
{
if (m_controls[i]->m_id == id)
{
delete m_controls.takeAt(i);
return;
}
}
}
void DeviceDiscoverer::DeviceInfo::deleteSensor(const QString &id)
{
for (int i = 0; i < m_sensors.size(); i++)
{
if (m_sensors[i]->m_id == id)
{
delete m_sensors.takeAt(i);
return;
}
}
}
QDataStream& operator<<(QDataStream& out, const DeviceDiscoverer::ControlInfo* control)
{
int typeId;
if (const VISADevice::VISAControl* c = dynamic_cast<const VISADevice::VISAControl *>(control)) {
typeId = 1;
} else {
typeId = 0;
}
out << typeId;
out << control->serialize();
return out;
}
QDataStream& operator>>(QDataStream& in, DeviceDiscoverer::ControlInfo*& control)
{
QByteArray data;
int typeId;
in >> typeId;
if (typeId == 1) {
control = new VISADevice::VISAControl();
} else {
control = new DeviceDiscoverer::ControlInfo();
}
in >> data;
control->deserialize(data);
return in;
}
QDataStream& operator<<(QDataStream& out, const DeviceDiscoverer::SensorInfo* sensor)
{
int typeId;
if (const VISADevice::VISASensor* s = dynamic_cast<const VISADevice::VISASensor *>(sensor)) {
typeId = 1;
} else {
typeId = 0;
}
out << typeId;
out << sensor->serialize();
return out;
}
QDataStream& operator>>(QDataStream& in, DeviceDiscoverer::SensorInfo*& sensor)
{
QByteArray data;
int typeId;
in >> typeId;
if (typeId == 1) {
sensor = new VISADevice::VISASensor();
} else {
sensor = new DeviceDiscoverer::SensorInfo();
}
in >> data;
sensor->deserialize(data);
return in;
}
QDataStream& operator<<(QDataStream& out, const VISADevice::VISASensor &sensor)
{
out << sensor.serialize();
return out;
}
QDataStream& operator>>(QDataStream& in, VISADevice::VISASensor& sensor)
{
QByteArray data;
in >> data;
sensor.deserialize(data);
return in;
}
QDataStream& operator<<(QDataStream& out, const VISADevice::VISAControl &control)
{
out << control.serialize();
return out;
}
QDataStream& operator>>(QDataStream& in, VISADevice::VISAControl& control)
{
QByteArray data;
in >> data;
control.deserialize(data);
return in;
}

142
sdrbase/util/iot/device.h Normal file
View File

@ -0,0 +1,142 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 Jon Beniston, M7RCE //
// //
// 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/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_DEVICE_H
#define INCLUDE_DEVICE_H
#include <QtCore>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
class SDRBASE_API DeviceDiscoverer : public QObject
{
Q_OBJECT
public:
enum Type {
AUTO,
BOOL,
INT,
FLOAT,
STRING,
LIST,
BUTTON
};
enum WidgetType {
SPIN_BOX,
DIAL,
SLIDER
};
struct SDRBASE_API ControlInfo {
QString m_name;
QString m_id;
Type m_type; // Data type
float m_min; // Min/max when m_type=INT/FLOAT
float m_max;
float m_scale;
int m_precision;
QStringList m_values; // Allowed values when m_type==LIST or label for button when m_type==BUTTON
WidgetType m_widgetType;// For m_type==FLOAT
QString m_units;
ControlInfo();
operator QString() const;
virtual ControlInfo *clone() const;
virtual QByteArray serialize() const;
virtual bool deserialize(const QByteArray& data);
};
struct SDRBASE_API SensorInfo {
QString m_name;
QString m_id;
Type m_type;
QString m_units; // W/Watts etc
operator QString() const;
virtual SensorInfo *clone() const;
virtual QByteArray serialize() const;
virtual bool deserialize(const QByteArray& data);
};
struct SDRBASE_API DeviceInfo {
QString m_name; // User friendly name
QString m_id; // ID for the device used by the API
QString m_model; // Model name
QList<ControlInfo *> m_controls;
QList<SensorInfo *> m_sensors;
DeviceInfo();
DeviceInfo(const DeviceInfo &info);
~DeviceInfo();
DeviceInfo& operator=(const DeviceInfo &info);
operator QString() const;
QByteArray serialize() const;
bool deserialize(const QByteArray& data);
ControlInfo *getControl(const QString &id) const;
SensorInfo *getSensor(const QString &id) const;
void deleteControl(const QString &id);
void deleteSensor(const QString &id);
};
protected:
DeviceDiscoverer();
public:
static DeviceDiscoverer *getDiscoverer(const QHash<QString, QVariant>& settings, const QString& protocol="TPLink");
static const QStringList m_typeStrings;
static const QStringList m_widgetTypeStrings;
virtual void getDevices() = 0;
signals:
void deviceList(const QList<DeviceInfo> &devices);
void error(const QString &msg);
};
class SDRBASE_API Device : public QObject
{
Q_OBJECT
protected:
Device(DeviceDiscoverer::DeviceInfo *info=nullptr);
public:
static Device* create(const QHash<QString, QVariant>& settings, const QString& protocol="TPLink", DeviceDiscoverer::DeviceInfo *info=nullptr);
static bool checkSettings(const QHash<QString, QVariant>& settings, const QString& protocol);
virtual void getState() = 0;
virtual void setState(const QString &controlId, bool state) {}
virtual void setState(const QString &controlId, int state) {}
virtual void setState(const QString &controlId, float state) {}
virtual void setState(const QString &controlId, const QString &state) {}
virtual QString getProtocol() const = 0;
virtual QString getDeviceId() const = 0;
signals:
void deviceUpdated(QHash<QString, QVariant>); // Called when new state available. Hash keys are control and sensor IDs
void deviceUnavailable(); // Called when device is unavailable. error() isn't signalled, as we expect devices to come and go
void error(const QString &msg); // Called on terminal error, such as invalid authentication details
protected:
DeviceDiscoverer::DeviceInfo m_info;
};
#endif /* INCLUDE_DEVICE_H */

View File

@ -0,0 +1,317 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 Jon Beniston, M7RCE //
// //
// 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"
#include "util/simpleserializer.h"
HomeAssistantDevice::HomeAssistantDevice(const QString& apiKey, const QString& url, const QString &deviceId,
const QStringList &controls, const QStringList &sensors,
DeviceDiscoverer::DeviceInfo *info) :
Device(info),
m_apiKey(apiKey),
m_url(url),
m_deviceId(deviceId)
{
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");
m_networkManager->get(request);
}
}
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());
}
void HomeAssistantDevice::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
//qDebug() << "Received " << document;
if (document.isObject())
{
QHash<QString, QVariant> status;
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("entity_id")) && obj.contains(QStringLiteral("state")))
{
QString entityId = obj.value(QStringLiteral("entity_id")).toString();
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: Document is not an object: " << document;
}
}
else
{
qDebug() << "HomeAssistantDevice::handleReply: error: " << reply->error();
}
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 parson 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";
}
}

View File

@ -0,0 +1,70 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 Jon Beniston, M7RCE //
// //
// 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/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_IOT_HOMEASSISTANT_H
#define INCLUDE_IOT_HOMEASSISTANT_H
#include "util/iot/device.h"
// Supports Home Assistant devices - https://www.home-assistant.io/
class SDRBASE_API HomeAssistantDevice : public Device {
Q_OBJECT
public:
HomeAssistantDevice(const QString& apiKey, const QString& url, const QString &deviceId,
const QStringList &controls, const QStringList &sensors,
DeviceDiscoverer::DeviceInfo *info=nullptr);
~HomeAssistantDevice();
virtual void getState() override;
virtual void setState(const QString &controlId, bool state) override;
virtual QString getProtocol() const override { return "HomeAssistant"; }
virtual QString getDeviceId() const override { return m_deviceId; }
private:
QString m_deviceId;
QStringList m_entities; // List of entities that are part of the device, to get state for (controls and sensors)
QString m_apiKey; // Bearer token
QString m_url; // Typically http://homeassistant.local:8123
QNetworkAccessManager *m_networkManager;
public slots:
void handleReply(QNetworkReply* reply);
};
class SDRBASE_API HomeAssistantDeviceDiscoverer : public DeviceDiscoverer {
Q_OBJECT
public:
HomeAssistantDeviceDiscoverer(const QString& apiKey, const QString& url);
~HomeAssistantDeviceDiscoverer();
virtual void getDevices() override;
private:
QString m_deviceId;
QString m_apiKey; // Bearer token
QString m_url; // Typically http://homeassistant.local:8123
QNetworkAccessManager *m_networkManager;
public slots:
void handleReply(QNetworkReply* reply);
};
#endif /* INCLUDE_IOT_HOMEASSISTANT_H */

634
sdrbase/util/iot/tplink.cpp Normal file
View File

@ -0,0 +1,634 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 Jon Beniston, M7RCE //
// //
// 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/tplink.h"
#include "util/simpleserializer.h"
const QString TPLinkCommon::m_url = "https://wap.tplinkcloud.com";
TPLinkCommon::TPLinkCommon(const QString& username, const QString &password) :
m_loggedIn(false),
m_outstandingRequest(false),
m_username(username),
m_password(password),
m_networkManager(nullptr)
{
}
void TPLinkCommon::login()
{
QUrl url(m_url);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject params {
{"appType", "Kasa_Android"},
{"cloudUserName", m_username},
{"cloudPassword", m_password},
{"terminalUUID", "9cc4653e-338f-48e4-b8ca-6ed3f67631e4"}
};
QJsonObject object {
{"method", "login"},
{"params", params}
};
QJsonDocument document;
document.setObject(object);
m_networkManager->post(request, document.toJson());
}
void TPLinkCommon::handleLoginReply(QNetworkReply* reply, QString &errorMessage)
{
if (reply)
{
if (!reply->error())
{
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if (document.isObject())
{
//qDebug() << "Received " << document;
if (!m_loggedIn)
{
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("error_code")))
{
int errorCode = obj.value(QStringLiteral("error_code")).toInt();
if (!errorCode)
{
if (obj.contains(QStringLiteral("result")))
{
QJsonObject result = obj.value(QStringLiteral("result")).toObject();
if (result.contains(QStringLiteral("token")))
{
m_loggedIn = true;
m_token = result.value(QStringLiteral("token")).toString();
}
else
{
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a token: " << result;
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a result object: " << obj;
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: Non-zero error_code while logging in: " << errorCode;
if (obj.contains(QStringLiteral("msg")))
{
QString msg = obj.value(QStringLiteral("msg")).toString();
qDebug() << "TPLinkDevice::handleReply: Error message: " << msg;
// Typical msg is "Incorrect email or password"
errorMessage = QString("TP-Link: Failed to log in. %1").arg(msg);
}
else
{
errorMessage = QString("TP-Link: Failed to log in. Error code: %1").arg(errorCode);
}
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain an error_code: " << obj;
}
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: Document is not an object: " << document;
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "TPLinkDevice::handleReply: reply is null";
}
if (!m_loggedIn && errorMessage.isEmpty()) {
errorMessage = "TP-Link: Failed to log in.";
}
}
TPLinkDevice::TPLinkDevice(const QString& username, const QString &password, const QString &deviceId, DeviceDiscoverer::DeviceInfo *info) :
Device(info),
TPLinkCommon(username, password),
m_deviceId(deviceId)
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&TPLinkDevice::handleReply
);
login();
}
TPLinkDevice::~TPLinkDevice()
{
QObject::disconnect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&TPLinkDevice::handleReply
);
delete m_networkManager;
}
void TPLinkDevice::getState()
{
if (!m_loggedIn)
{
m_outstandingRequest = true;
return;
}
QUrl url(m_url);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject system;
system.insert("get_sysinfo", QJsonValue());
QJsonObject emeter;
emeter.insert("get_realtime", QJsonValue());
QJsonObject requestData {
{"system", system},
{"emeter", emeter}
};
QJsonObject params {
{"deviceId", m_deviceId},
{"requestData", requestData},
{"token", m_token}
};
QJsonObject object {
{"method", "passthrough"},
{"params", params}
};
QJsonDocument document;
document.setObject(object);
m_networkManager->post(request, document.toJson());
}
void TPLinkDevice::setState(const QString &controlId, bool state)
{
if (!m_loggedIn)
{
// Should we queue these and apply after logged in?
qDebug() << "TPLinkDevice::setState: Unable to set state for " << controlId << " to " << state << " as not yet logged in";
return;
}
QUrl url(m_url);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject stateObj {
{"state", (int)state}
};
QJsonObject system {
{"set_relay_state", stateObj}
};
QJsonObject requestData {
{"system", system}
};
if (controlId != "switch") {
QJsonArray childIds {
controlId
};
QJsonObject context {
{"child_ids", childIds}
};
requestData.insert("context", QJsonValue(context));
}
QJsonObject params {
{"deviceId", m_deviceId},
{"requestData", requestData},
{"token", m_token}
};
QJsonObject object {
{"method", "passthrough"},
{"params", params}
};
QJsonDocument document;
document.setObject(object);
m_networkManager->post(request, document.toJson());
}
void TPLinkDevice::handleReply(QNetworkReply* reply)
{
if (!m_loggedIn)
{
QString errorMessage;
TPLinkCommon::handleLoginReply(reply, errorMessage);
if (!errorMessage.isEmpty())
{
emit error(errorMessage);
}
else if (m_outstandingRequest)
{
m_outstandingRequest = true;
getState();
}
}
else if (reply)
{
if (!reply->error())
{
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if (document.isObject())
{
//qDebug() << "Received " << document;
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("result")))
{
QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject();
QHash<QString, QVariant> status;
if (resultObj.contains(QStringLiteral("responseData")))
{
QJsonObject responseDataObj = resultObj.value(QStringLiteral("responseData")).toObject();
if (responseDataObj.contains(QStringLiteral("system")))
{
QJsonObject systemObj = responseDataObj.value(QStringLiteral("system")).toObject();
if (systemObj.contains(QStringLiteral("get_sysinfo")))
{
QJsonObject sysInfoObj = systemObj.value(QStringLiteral("get_sysinfo")).toObject();
if (sysInfoObj.contains(QStringLiteral("child_num")))
{
int childNum = sysInfoObj.value(QStringLiteral("child_num")).toInt();
QJsonArray children = sysInfoObj.value(QStringLiteral("children")).toArray();
for (auto childRef : children)
{
QJsonObject childObj = childRef.toObject();
if (childObj.contains(QStringLiteral("state")) && childObj.contains(QStringLiteral("id")))
{
int state = childObj.value(QStringLiteral("state")).toInt();
QString id = childObj.value(QStringLiteral("id")).toString();
status.insert(id, state); // key should match id in discoverer
}
}
}
else if (sysInfoObj.contains(QStringLiteral("relay_state")))
{
int state = sysInfoObj.value(QStringLiteral("relay_state")).toInt();
status.insert("switch", state); // key should match id in discoverer
}
}
}
// KP115 has emeter, but KP105 doesn't
if (responseDataObj.contains(QStringLiteral("emeter")))
{
QJsonObject emeterObj = responseDataObj.value(QStringLiteral("emeter")).toObject();
if (emeterObj.contains(QStringLiteral("get_realtime")))
{
QJsonObject realtimeObj = emeterObj.value(QStringLiteral("get_realtime")).toObject();
if (realtimeObj.contains(QStringLiteral("current_ma")))
{
double current = realtimeObj.value(QStringLiteral("current_ma")).toDouble();
status.insert("current", current / 1000.0);
}
if (realtimeObj.contains(QStringLiteral("voltage_mv")))
{
double voltage = realtimeObj.value(QStringLiteral("voltage_mv")).toDouble();
status.insert("voltage", voltage / 1000.0);
}
if (realtimeObj.contains(QStringLiteral("power_mw")))
{
double power = realtimeObj.value(QStringLiteral("power_mw")).toDouble();
status.insert("power", power / 1000.0);
}
}
}
}
emit deviceUpdated(status);
}
else if (obj.contains(QStringLiteral("error_code")))
{
// If a device isn't available, we can get:
// {"error_code":-20002,"msg":"Request timeout"}
// {"error_code":-20571,"msg":"Device is offline"}
int errorCode = obj.value(QStringLiteral("error_code")).toInt();
QString msg = obj.value(QStringLiteral("msg")).toString();
qDebug() << "TPLinkDevice::handleReply: Error code: " << errorCode << " " << msg;
emit deviceUnavailable();
}
else
{
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a result or error_code: " << obj;
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: Document is not an object: " << document;
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "TPLinkDevice::handleReply: reply is null";
}
}
TPLinkDeviceDiscoverer::TPLinkDeviceDiscoverer(const QString& username, const QString &password) :
TPLinkCommon(username, password)
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&TPLinkDeviceDiscoverer::handleReply
);
login();
}
TPLinkDeviceDiscoverer::~TPLinkDeviceDiscoverer()
{
QObject::disconnect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&TPLinkDeviceDiscoverer::handleReply
);
delete m_networkManager;
}
void TPLinkDeviceDiscoverer::getDevices()
{
if (!m_loggedIn)
{
m_outstandingRequest = true;
return;
}
QUrl url(m_url);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject params {
{"token", m_token}
};
QJsonObject object {
{"method", "getDeviceList"},
{"params", params}
};
QJsonDocument document;
document.setObject(object);
m_networkManager->post(request, document.toJson());
}
void TPLinkDeviceDiscoverer::getState(const QString &deviceId)
{
QUrl url(m_url);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject system;
system.insert("get_sysinfo", QJsonValue());
QJsonObject emeter;
emeter.insert("get_realtime", QJsonValue());
QJsonObject requestData {
{"system", system},
{"emeter", emeter}
};
QJsonObject params {
{"deviceId", deviceId},
{"requestData", requestData},
{"token", m_token}
};
QJsonObject object {
{"method", "passthrough"},
{"params", params}
};
QJsonDocument document;
document.setObject(object);
m_getStateReplies.insert(m_networkManager->post(request, document.toJson()), deviceId);
}
void TPLinkDeviceDiscoverer::handleReply(QNetworkReply* reply)
{
if (!m_loggedIn)
{
QString errorMessage;
TPLinkCommon::handleLoginReply(reply, errorMessage);
if (!errorMessage.isEmpty())
{
emit error(errorMessage);
}
else if (m_outstandingRequest)
{
m_outstandingRequest = false;
getDevices();
}
}
else if (reply)
{
if (!reply->error())
{
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if (document.isObject())
{
//qDebug() << "Received " << document;
QJsonObject obj = document.object();
if (m_getStateReplies.contains(reply))
{
// Reply for getState
m_getStateReplies.remove(reply);
QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject();
if (resultObj.contains(QStringLiteral("responseData")))
{
QJsonObject responseDataObj = resultObj.value(QStringLiteral("responseData")).toObject();
if (responseDataObj.contains(QStringLiteral("system")))
{
DeviceInfo info;
QJsonObject systemObj = responseDataObj.value(QStringLiteral("system")).toObject();
if (systemObj.contains(QStringLiteral("get_sysinfo")))
{
QJsonObject sysInfoObj = systemObj.value(QStringLiteral("get_sysinfo")).toObject();
if (sysInfoObj.contains(QStringLiteral("alias"))) {
info.m_name = sysInfoObj.value(QStringLiteral("alias")).toString();
}
if (sysInfoObj.contains(QStringLiteral("model"))) {
info.m_model = sysInfoObj.value(QStringLiteral("model")).toString();
}
if (sysInfoObj.contains(QStringLiteral("deviceId"))) {
info.m_id = sysInfoObj.value(QStringLiteral("deviceId")).toString();
}
if (sysInfoObj.contains(QStringLiteral("child_num")))
{
int childNum = sysInfoObj.value(QStringLiteral("child_num")).toInt();
QJsonArray children = sysInfoObj.value(QStringLiteral("children")).toArray();
int child = 1;
for (auto childRef : children)
{
QJsonObject childObj = childRef.toObject();
ControlInfo *controlInfo = new ControlInfo();
controlInfo->m_id = childObj.value(QStringLiteral("id")).toString();
if (childObj.contains(QStringLiteral("alias"))) {
controlInfo->m_name = childObj.value(QStringLiteral("alias")).toString();
}
controlInfo->m_type = DeviceDiscoverer::BOOL;
info.m_controls.append(controlInfo);
child++;
}
}
else if (sysInfoObj.contains(QStringLiteral("relay_state")))
{
ControlInfo *controlInfo = new ControlInfo();
controlInfo->m_id = "switch";
if (sysInfoObj.contains(QStringLiteral("alias"))) {
controlInfo->m_name = sysInfoObj.value(QStringLiteral("alias")).toString();
}
controlInfo->m_type = DeviceDiscoverer::BOOL;
info.m_controls.append(controlInfo);
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: get_sysinfo missing";
}
// KP115 has energy meter, but KP105 doesn't. KP105 will have emeter object, but without get_realtime sub-object
if (responseDataObj.contains(QStringLiteral("emeter")))
{
QJsonObject emeterObj = responseDataObj.value(QStringLiteral("emeter")).toObject();
if (emeterObj.contains(QStringLiteral("get_realtime")))
{
QJsonObject realtimeObj = emeterObj.value(QStringLiteral("get_realtime")).toObject();
if (realtimeObj.contains(QStringLiteral("current_ma")))
{
SensorInfo *currentSensorInfo = new SensorInfo();
currentSensorInfo->m_name = "Current";
currentSensorInfo->m_id = "current";
currentSensorInfo->m_type = DeviceDiscoverer::FLOAT;
currentSensorInfo->m_units = "A";
info.m_sensors.append(currentSensorInfo);
}
if (realtimeObj.contains(QStringLiteral("voltage_mv")))
{
SensorInfo *voltageSensorInfo = new SensorInfo();
voltageSensorInfo->m_name = "Voltage";
voltageSensorInfo->m_id = "voltage";
voltageSensorInfo->m_type = DeviceDiscoverer::FLOAT;
voltageSensorInfo->m_units = "V";
info.m_sensors.append(voltageSensorInfo);
}
if (realtimeObj.contains(QStringLiteral("power_mw")))
{
SensorInfo *powerSensorInfo = new SensorInfo();
powerSensorInfo->m_name = "Power";
powerSensorInfo->m_id = "power";
powerSensorInfo->m_type = DeviceDiscoverer::FLOAT;
powerSensorInfo->m_units = "W";
info.m_sensors.append(powerSensorInfo);
}
}
}
if (info.m_controls.size() > 0) {
m_devices.append(info);
} else {
qDebug() << "TPLinkDeviceDiscoverer::handleReply: No controls in info";
}
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: No responseData";
}
if (m_getStateReplies.size() == 0)
{
emit deviceList(m_devices);
m_devices.clear();
}
}
else
{
// Reply for getDevice
if (obj.contains(QStringLiteral("result")))
{
QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject();
if (resultObj.contains(QStringLiteral("deviceList")))
{
QJsonArray deviceArray = resultObj.value(QStringLiteral("deviceList")).toArray();
for (auto deviceRef : deviceArray)
{
QJsonObject deviceObj = deviceRef.toObject();
if (deviceObj.contains(QStringLiteral("deviceId")) && deviceObj.contains(QStringLiteral("deviceType")))
{
// In order to discover what controls and sensors a device has, we need to get sysinfo
getState(deviceObj.value(QStringLiteral("deviceId")).toString());
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: deviceList element doesn't contain a deviceId: " << deviceObj;
}
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: result doesn't contain a deviceList: " << resultObj;
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: Object doesn't contain a result: " << obj;
}
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: Document is not an object: " << document;
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: reply is null";
}
}

77
sdrbase/util/iot/tplink.h Normal file
View File

@ -0,0 +1,77 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 Jon Beniston, M7RCE //
// //
// 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/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_IOT_TPLINK_H
#define INCLUDE_IOT_TPLINK_H
#include "util/iot/device.h"
class SDRBASE_API TPLinkCommon {
protected:
TPLinkCommon(const QString& username, const QString &password);
void login();
void handleLoginReply(QNetworkReply* reply, QString &errorMessage);
bool m_loggedIn;
bool m_outstandingRequest; // Issue getState / getDevices after logged in
QString m_username;
QString m_password;
QString m_token;
QNetworkAccessManager *m_networkManager;
static const QString m_url;
};
// Supports TPLink's Kasa plugs - https://www.tp-link.com/uk/smarthome/
class SDRBASE_API TPLinkDevice : public Device, TPLinkCommon {
Q_OBJECT
public:
TPLinkDevice(const QString& username, const QString &password, const QString &deviceId, DeviceDiscoverer::DeviceInfo *info=nullptr);
~TPLinkDevice();
virtual void getState() override;
virtual void setState(const QString &controlId, bool state) override;
virtual QString getProtocol() const override { return "TPLink"; }
virtual QString getDeviceId() const override { return m_deviceId; }
private:
QString m_deviceId;
public slots:
void handleReply(QNetworkReply* reply);
};
class SDRBASE_API TPLinkDeviceDiscoverer : public DeviceDiscoverer, TPLinkCommon {
Q_OBJECT
public:
TPLinkDeviceDiscoverer(const QString& username, const QString &password);
~TPLinkDeviceDiscoverer();
virtual void getDevices() override;
private:
void getState(const QString &deviceId);
QHash<QNetworkReply *, QString> m_getStateReplies;
QList<DeviceInfo> m_devices;
public slots:
void handleReply(QNetworkReply* reply);
};
#endif /* INCLUDE_IOT_TPLINK_H */

506
sdrbase/util/iot/visa.cpp Normal file
View File

@ -0,0 +1,506 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 Jon Beniston, M7RCE //
// //
// 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/visa.h"
#include "util/visa.h"
#include "util/simpleserializer.h"
VISADevice::VISADevice(const QHash<QString, QVariant> settings, const QString &deviceId,
const QStringList &controls, const QStringList &sensors,
DeviceDiscoverer::DeviceInfo *info) :
Device(info),
m_deviceId(deviceId),
m_session(0),
m_controls(controls),
m_sensors(sensors)
{
m_visa.openDefault();
QHashIterator<QString, QVariant> itr(settings);
while (itr.hasNext())
{
itr.next();
QString key = itr.key();
QVariant value = itr.value();
if ((key == "deviceId") || (key == "controlIds") || (key == "sensorIds"))
{
// Nothing to do here
}
else if (key == "logIO")
{
m_visa.setDebugIO(value.toBool());
}
else
{
qDebug() << "VISADevice::VISADevice: Unsupported setting key: " << key << " value: " << value;
}
}
open();
}
VISADevice::~VISADevice()
{
m_visa.close(m_session);
m_visa.closeDefault();
}
bool VISADevice::open()
{
if (!m_session) {
m_session = m_visa.open(m_deviceId);
}
if (!m_session) {
emit deviceUnavailable();
}
return m_session != 0;
}
bool VISADevice::convertToBool(const QString &string, bool &ok)
{
QString lower = string.trimmed().toLower();
if ((lower == "0") || (lower == "false") || (lower == "off"))
{
ok = true;
return false;
}
else if ((lower == "1") || (lower == "true") || (lower == "on"))
{
ok = true;
return true;
}
else
{
ok = false;
return false;
}
}
void VISADevice::convert(QHash<QString, QVariant> &status, const QString &id, DeviceDiscoverer::Type type, const QString &state)
{
if (type == DeviceDiscoverer::BOOL)
{
bool ok;
bool value = convertToBool(state, ok);
if (ok) {
status.insert(id, value);
} else {
status.insert(id, "error");
}
}
else if (type == DeviceDiscoverer::INT)
{
bool ok;
int value = state.toInt(&ok);
if (ok) {
status.insert(id, value);
} else {
status.insert(id, "error");
}
}
else if (type == DeviceDiscoverer::FLOAT)
{
bool ok;
float value = state.toFloat(&ok);
if (ok) {
status.insert(id, value);
} else {
status.insert(id, "error");
}
}
else
{
status.insert(id, state);
}
}
void VISADevice::getState()
{
if (open())
{
QHash<QString, QVariant> status;
for (auto c : m_info.m_controls)
{
if (m_controls.contains(c->m_id))
{
VISAControl *control = reinterpret_cast<VISAControl *>(c);
QString cmds = control->m_getState.trimmed();
if (!cmds.isEmpty())
{
bool error;
QStringList results = m_visa.processCommands(m_session, cmds, &error);
if (!error && (results.size() > 0))
{
// Take last returned value as the state
QString state = results[results.size()-1].trimmed();
convert(status, control->m_id, control->m_type, state);
}
else
{
status.insert(control->m_id, "error");
}
}
}
}
for (auto s : m_info.m_sensors)
{
if (m_sensors.contains(s->m_id))
{
VISASensor *sensor = reinterpret_cast<VISASensor *>(s);
QString cmds = sensor->m_getState.trimmed();
if (!cmds.isEmpty())
{
bool error;
QStringList results = m_visa.processCommands(m_session, cmds, &error);
if (!error && (results.size() > 0))
{
// Take last returned value as the state
QString state = results[results.size()-1].trimmed();
convert(status, sensor->m_id, sensor->m_type, state);
}
else
{
status.insert(sensor->m_id, "error");
}
}
}
}
emit deviceUpdated(status);
}
}
void VISADevice::setState(const QString &controlId, bool state)
{
if (open())
{
for (auto c : m_info.m_controls)
{
VISAControl *control = reinterpret_cast<VISAControl *>(c);
if (control->m_id == controlId)
{
QString commands = QString::asprintf(control->m_setState.toUtf8(), (int)state);
bool error;
m_visa.processCommands(m_session, commands, &error);
if (error) {
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
}
}
}
}
}
void VISADevice::setState(const QString &controlId, int state)
{
if (open())
{
for (auto c : m_info.m_controls)
{
VISAControl *control = reinterpret_cast<VISAControl *>(c);
if (control->m_id == controlId)
{
QString commands = QString::asprintf(control->m_setState.toUtf8(), state);
bool error;
m_visa.processCommands(m_session, commands, &error);
if (error) {
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
}
}
}
}
}
void VISADevice::setState(const QString &controlId, float state)
{
if (open())
{
for (auto c : m_info.m_controls)
{
VISAControl *control = reinterpret_cast<VISAControl *>(c);
if (control->m_id == controlId)
{
QString commands = QString::asprintf(control->m_setState.toUtf8(), state);
bool error;
m_visa.processCommands(m_session, commands, &error);
if (error) {
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
}
}
}
}
}
void VISADevice::setState(const QString &controlId, const QString &state)
{
if (open())
{
for (auto c : m_info.m_controls)
{
VISAControl *control = reinterpret_cast<VISAControl *>(c);
if (control->m_id == controlId)
{
QString commands = QString::asprintf(control->m_setState.toUtf8(), state);
bool error;
m_visa.processCommands(m_session, commands, &error);
if (error) {
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
}
}
}
}
}
VISADeviceDiscoverer::VISADeviceDiscoverer(const QString& resourceFilter) :
m_resourceFilter(resourceFilter)
{
m_session = m_visa.openDefault();
}
VISADeviceDiscoverer::~VISADeviceDiscoverer()
{
m_visa.closeDefault();
}
void VISADeviceDiscoverer::getDevices()
{
QRegularExpression *filterP = nullptr;
QRegularExpression filter(m_resourceFilter);
if (!m_resourceFilter.trimmed().isEmpty()) {
filterP = &filter;
}
// Get list of VISA instruments
QList<VISA::Instrument> instruments = m_visa.instruments(filterP);
// Convert to list of devices
QList<DeviceInfo> devices;
for (auto const &instrument : instruments)
{
DeviceInfo info;
info.m_name = instrument.m_model;
info.m_id = instrument.m_resource;
info.m_model = instrument.m_model;
if ((info.m_name == "DP832") || (info.m_name == "DP832A"))
{
for (int i = 1; i <= 3; i++)
{
VISADevice::VISAControl *output = new VISADevice::VISAControl();
output->m_name = QString("CH%1").arg(i);
output->m_id = QString("control.ch%1").arg(i);
output->m_type = BOOL;
output->m_getState = QString(":OUTPUT? CH%1").arg(i);
output->m_setState = QString(":OUTPUT CH%1,%d").arg(i);
info.m_controls.append(output);
VISADevice::VISAControl *setVoltage = new VISADevice::VISAControl();
setVoltage->m_name = QString("V%1").arg(i);
setVoltage->m_id = QString("control.voltage%1").arg(i);
setVoltage->m_type = FLOAT;
setVoltage->m_min = 0.0f;
setVoltage->m_max = i == 3 ? 5.0f : 30.0f;
setVoltage->m_scale = 1.0f;
setVoltage->m_precision = 3;
setVoltage->m_widgetType = SPIN_BOX;
setVoltage->m_units = "V";
setVoltage->m_getState = QString(":SOURCE%1:VOLTage?").arg(i);
setVoltage->m_setState = QString(":SOURCE%1:VOLTage %f").arg(i);
info.m_controls.append(setVoltage);
VISADevice::VISAControl *setCurrent = new VISADevice::VISAControl();
setCurrent->m_name = QString("i%1").arg(i);
setCurrent->m_id = QString("control.current%1").arg(i);
setCurrent->m_type = FLOAT;
setCurrent->m_min = 0.0f;
setCurrent->m_max = 3.0f;
setCurrent->m_scale = 1.0f;
setCurrent->m_precision = 3;
setCurrent->m_widgetType = SPIN_BOX;
setCurrent->m_units = "A";
setCurrent->m_getState = QString(":SOURCE%1:CURRent?").arg(i);
setCurrent->m_setState = QString(":SOURCE%1:CURRent %f").arg(i);
info.m_controls.append(setCurrent);
VISADevice::VISASensor *voltage = new VISADevice::VISASensor();
voltage->m_name = QString("V%1").arg(i);
voltage->m_id = QString("sensor.voltage%1").arg(i);
voltage->m_type = FLOAT;
voltage->m_units = "V";
voltage->m_getState = QString(":MEASure:VOLTage? CH%1").arg(i);
info.m_sensors.append(voltage);
VISADevice::VISASensor *current = new VISADevice::VISASensor();
current->m_name = QString("i%1").arg(i);
current->m_id = QString("sensor.current%1").arg(i);
current->m_type = FLOAT;
current->m_units = "A";
current->m_getState = QString(":MEASure:CURRent? CH%1").arg(i);
info.m_sensors.append(current);
VISADevice::VISASensor *power = new VISADevice::VISASensor();
power->m_name = QString("P%1").arg(i);
power->m_id = QString("sensor.power%1").arg(i);
power->m_type = FLOAT;
power->m_units = "W";
power->m_getState = QString(":MEASure:POWEr? CH%1").arg(i);
info.m_sensors.append(power);
}
}
else if (info.m_name == "SSA3032X")
{
VISADevice::VISAControl *frequency = new VISADevice::VISAControl();
frequency->m_name = "Frequency";
frequency->m_id = "control.frequency";
frequency->m_type = FLOAT;
frequency->m_min = 0.0f;
frequency->m_max = 3.2e3f;
frequency->m_scale = 1e6f;
frequency->m_precision = 6;
frequency->m_widgetType = SPIN_BOX;
frequency->m_units = "MHz";
frequency->m_getState = ":FREQuency:CENTer?";
frequency->m_setState = ":FREQuency:CENTer %f";
info.m_controls.append(frequency);
VISADevice::VISAControl *span = new VISADevice::VISAControl();
span->m_name = "Span";
span->m_id = "control.span";
span->m_type = FLOAT;
span->m_min = 0.0f;
span->m_max = 3.2e3f;
span->m_scale = 1e6;
span->m_precision = 3;
span->m_widgetType = SPIN_BOX;
span->m_units = "MHz";
span->m_getState = ":FREQuency:SPAN?";
span->m_setState = ":FREQuency:SPAN %f";
info.m_controls.append(span);
VISADevice::VISAControl *markerX = new VISADevice::VISAControl();
markerX->m_name = "Marker X";
markerX->m_id = "control.markerx";
markerX->m_type = FLOAT;
markerX->m_min = 0.0f;
markerX->m_max = 3.2e3f;
markerX->m_scale = 1e6;
markerX->m_precision = 6;
markerX->m_widgetType = SPIN_BOX;
markerX->m_units = "MHz";
markerX->m_getState = ":CALCulate:MARKer1:X?";
markerX->m_setState = ":CALCulate:MARKer1:X %f";
info.m_controls.append(markerX);
VISADevice::VISASensor *markerY = new VISADevice::VISASensor();
markerY->m_name = "Marker Y";
markerY->m_id = "sensor.markery";
markerY->m_type = FLOAT;
markerY->m_units = "dBm";
markerY->m_getState = ":CALCulate:MARKer1:Y?";
info.m_sensors.append(markerY);
}
devices.append(info);
}
emit deviceList(devices);
}
DeviceDiscoverer::ControlInfo *VISADevice::VISAControl::clone() const
{
return new VISAControl(*this);
}
QByteArray VISADevice::VISAControl::serialize() const
{
SimpleSerializer s(1);
s.writeBlob(1, ControlInfo::serialize());
s.writeString(2, m_getState);
s.writeString(3, m_setState);
return s.final();
}
bool VISADevice::VISAControl::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid()) {
return false;
}
if (d.getVersion() == 1)
{
QByteArray blob;
d.readBlob(1, &blob);
ControlInfo::deserialize(blob);
d.readString(2, &m_getState);
d.readString(3, &m_setState);
return true;
}
else
{
return false;
}
}
DeviceDiscoverer::SensorInfo *VISADevice::VISASensor::clone() const
{
return new VISASensor(*this);
}
QByteArray VISADevice::VISASensor::serialize() const
{
SimpleSerializer s(1);
s.writeBlob(1, SensorInfo::serialize());
s.writeString(2, m_getState);
return s.final();
}
bool VISADevice::VISASensor::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid()) {
return false;
}
if (d.getVersion() == 1)
{
QByteArray blob;
d.readBlob(1, &blob);
SensorInfo::deserialize(blob);
d.readString(2, &m_getState);
return true;
}
else
{
return false;
}
}

84
sdrbase/util/iot/visa.h Normal file
View File

@ -0,0 +1,84 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2022 Jon Beniston, M7RCE //
// //
// 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/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_IOT_VISA_H
#define INCLUDE_IOT_VISA_H
#include "util/iot/device.h"
#include "util/visa.h"
class SDRBASE_API VISADevice : public Device {
Q_OBJECT
public:
struct SDRBASE_API VISAControl : public DeviceDiscoverer::ControlInfo {
QString m_getState;
QString m_setState;
virtual DeviceDiscoverer::ControlInfo *clone() const override;
virtual QByteArray serialize() const override;
virtual bool deserialize(const QByteArray& data) override;
};
struct SDRBASE_API VISASensor : public DeviceDiscoverer::SensorInfo {
QString m_getState;
virtual DeviceDiscoverer::SensorInfo *clone() const override;
virtual QByteArray serialize() const override;
virtual bool deserialize(const QByteArray& data) override;
};
VISADevice(const QHash<QString, QVariant> settings, const QString &deviceId,
const QStringList &controls, const QStringList &sensors,
DeviceDiscoverer::DeviceInfo *info=nullptr);
~VISADevice();
virtual void getState() override;
virtual void setState(const QString &controlId, bool state) override;
virtual void setState(const QString &controlId, int state) override;
virtual void setState(const QString &controlId, float state) override;
virtual void setState(const QString &controlId, const QString &state) override;
virtual QString getProtocol() const override { return "VISA"; }
virtual QString getDeviceId() const override { return m_deviceId; }
private:
QString m_deviceId; // VISA resource (E.g. USB0::0xcafe...)
VISA m_visa;
ViSession m_session;
QStringList m_controls; // Control IDs for getState
QStringList m_sensors; // Sensor IDs for getState
bool m_debugIO;
bool open();
bool convertToBool(const QString &string, bool &ok);
void convert(QHash<QString, QVariant> &status, const QString &id, DeviceDiscoverer::Type type, const QString &state);
};
class SDRBASE_API VISADeviceDiscoverer : public DeviceDiscoverer {
Q_OBJECT
public:
VISADeviceDiscoverer(const QString &resourceFilter = "");
~VISADeviceDiscoverer();
virtual void getDevices() override;
private:
VISA m_visa;
ViSession m_session;
QString m_resourceFilter;
};
#endif /* INCLUDE_IOT_VISA_H */