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 */