diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt
index ddcb346ee..803e468c2 100644
--- a/sdrbase/CMakeLists.txt
+++ b/sdrbase/CMakeLists.txt
@@ -207,6 +207,10 @@ set(sdrbase_SOURCES
util/timeutil.cpp
util/visa.cpp
util/weather.cpp
+ util/iot/device.cpp
+ util/iot/homeassistant.cpp
+ util/iot/tplink.cpp
+ util/iot/visa.cpp
plugin/plugininterface.cpp
plugin/pluginapi.cpp
@@ -426,6 +430,10 @@ set(sdrbase_HEADERS
util/timeutil.h
util/visa.h
util/weather.h
+ util/iot/device.h
+ util/iot/homeassistant.h
+ util/iot/tplink.h
+ util/iot/visa.h
webapi/webapiadapter.h
webapi/webapiadapterbase.h
diff --git a/sdrbase/util/iot/device.cpp b/sdrbase/util/iot/device.cpp
new file mode 100644
index 000000000..bca6ed126
--- /dev/null
+++ b/sdrbase/util/iot/device.cpp
@@ -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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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& 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& 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& 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(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(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;
+}
diff --git a/sdrbase/util/iot/device.h b/sdrbase/util/iot/device.h
new file mode 100644
index 000000000..802e4687e
--- /dev/null
+++ b/sdrbase/util/iot/device.h
@@ -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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDE_DEVICE_H
+#define INCLUDE_DEVICE_H
+
+#include
+
+#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 m_controls;
+ QList 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& settings, const QString& protocol="TPLink");
+ static const QStringList m_typeStrings;
+ static const QStringList m_widgetTypeStrings;
+
+ virtual void getDevices() = 0;
+
+signals:
+ void deviceList(const QList &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& settings, const QString& protocol="TPLink", DeviceDiscoverer::DeviceInfo *info=nullptr);
+ static bool checkSettings(const QHash& 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); // 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 */
diff --git a/sdrbase/util/iot/homeassistant.cpp b/sdrbase/util/iot/homeassistant.cpp
new file mode 100644
index 000000000..df07ed4f5
--- /dev/null
+++ b/sdrbase/util/iot/homeassistant.cpp
@@ -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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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 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 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";
+ }
+}
diff --git a/sdrbase/util/iot/homeassistant.h b/sdrbase/util/iot/homeassistant.h
new file mode 100644
index 000000000..7fc8ea54a
--- /dev/null
+++ b/sdrbase/util/iot/homeassistant.h
@@ -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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#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 */
diff --git a/sdrbase/util/iot/tplink.cpp b/sdrbase/util/iot/tplink.cpp
new file mode 100644
index 000000000..119becfda
--- /dev/null
+++ b/sdrbase/util/iot/tplink.cpp
@@ -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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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 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";
+ }
+}
diff --git a/sdrbase/util/iot/tplink.h b/sdrbase/util/iot/tplink.h
new file mode 100644
index 000000000..3ffd4a37d
--- /dev/null
+++ b/sdrbase/util/iot/tplink.h
@@ -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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#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 m_getStateReplies;
+ QList m_devices;
+
+public slots:
+ void handleReply(QNetworkReply* reply);
+};
+
+#endif /* INCLUDE_IOT_TPLINK_H */
diff --git a/sdrbase/util/iot/visa.cpp b/sdrbase/util/iot/visa.cpp
new file mode 100644
index 000000000..c705df49a
--- /dev/null
+++ b/sdrbase/util/iot/visa.cpp
@@ -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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "util/iot/visa.h"
+#include "util/visa.h"
+#include "util/simpleserializer.h"
+
+VISADevice::VISADevice(const QHash 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 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 &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 status;
+
+ for (auto c : m_info.m_controls)
+ {
+ if (m_controls.contains(c->m_id))
+ {
+ VISAControl *control = reinterpret_cast(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(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(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(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(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(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 instruments = m_visa.instruments(filterP);
+
+ // Convert to list of devices
+ QList 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;
+ }
+}
diff --git a/sdrbase/util/iot/visa.h b/sdrbase/util/iot/visa.h
new file mode 100644
index 000000000..e147c1f07
--- /dev/null
+++ b/sdrbase/util/iot/visa.h
@@ -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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#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 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 &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 */