mirror of
https://github.com/f4exb/sdrangel.git
synced 2024-11-15 12:51:49 -05:00
util/iot: Add API for accessing IoT / Smart Home devices.
This commit is contained in:
parent
83a94fc375
commit
a4cd8af538
@ -207,6 +207,10 @@ set(sdrbase_SOURCES
|
|||||||
util/timeutil.cpp
|
util/timeutil.cpp
|
||||||
util/visa.cpp
|
util/visa.cpp
|
||||||
util/weather.cpp
|
util/weather.cpp
|
||||||
|
util/iot/device.cpp
|
||||||
|
util/iot/homeassistant.cpp
|
||||||
|
util/iot/tplink.cpp
|
||||||
|
util/iot/visa.cpp
|
||||||
|
|
||||||
plugin/plugininterface.cpp
|
plugin/plugininterface.cpp
|
||||||
plugin/pluginapi.cpp
|
plugin/pluginapi.cpp
|
||||||
@ -426,6 +430,10 @@ set(sdrbase_HEADERS
|
|||||||
util/timeutil.h
|
util/timeutil.h
|
||||||
util/visa.h
|
util/visa.h
|
||||||
util/weather.h
|
util/weather.h
|
||||||
|
util/iot/device.h
|
||||||
|
util/iot/homeassistant.h
|
||||||
|
util/iot/tplink.h
|
||||||
|
util/iot/visa.h
|
||||||
|
|
||||||
webapi/webapiadapter.h
|
webapi/webapiadapter.h
|
||||||
webapi/webapiadapterbase.h
|
webapi/webapiadapterbase.h
|
||||||
|
529
sdrbase/util/iot/device.cpp
Normal file
529
sdrbase/util/iot/device.cpp
Normal file
@ -0,0 +1,529 @@
|
|||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Copyright (C) 2022 Jon Beniston, M7RCE //
|
||||||
|
// //
|
||||||
|
// This program is free software; you can redistribute it and/or modify //
|
||||||
|
// it under the terms of the GNU General Public License as published by //
|
||||||
|
// the Free Software Foundation as version 3 of the License, or //
|
||||||
|
// (at your option) any later version. //
|
||||||
|
// //
|
||||||
|
// This program is distributed in the hope that it will be useful, //
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||||
|
// GNU General Public License V3 for more details. //
|
||||||
|
// //
|
||||||
|
// You should have received a copy of the GNU General Public License //
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#include "util/simpleserializer.h"
|
||||||
|
#include "util/iot/device.h"
|
||||||
|
#include "util/iot/tplink.h"
|
||||||
|
#include "util/iot/homeassistant.h"
|
||||||
|
#include "util/iot/visa.h"
|
||||||
|
|
||||||
|
Device::Device(DeviceDiscoverer::DeviceInfo *info)
|
||||||
|
{
|
||||||
|
if (info) {
|
||||||
|
m_info = *info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Device* Device::create(const QHash<QString, QVariant>& settings, const QString& protocol, DeviceDiscoverer::DeviceInfo *info)
|
||||||
|
{
|
||||||
|
if (checkSettings(settings, protocol))
|
||||||
|
{
|
||||||
|
if (protocol == "TPLink")
|
||||||
|
{
|
||||||
|
if (settings.contains("deviceId"))
|
||||||
|
{
|
||||||
|
return new TPLinkDevice(settings.value("username").toString(),
|
||||||
|
settings.value("password").toString(),
|
||||||
|
settings.value("deviceId").toString(),
|
||||||
|
info);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "Device::create: A deviceId is required for: " << protocol;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (protocol == "HomeAssistant")
|
||||||
|
{
|
||||||
|
if (checkSettings(settings, protocol))
|
||||||
|
{
|
||||||
|
if (settings.contains("deviceId"))
|
||||||
|
{
|
||||||
|
return new HomeAssistantDevice(settings.value("apiKey").toString(),
|
||||||
|
settings.value("url").toString(),
|
||||||
|
settings.value("deviceId").toString(),
|
||||||
|
settings.value("controlIds").toStringList(),
|
||||||
|
settings.value("sensorIds").toStringList(),
|
||||||
|
info);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "Device::create: A deviceId is required for: " << protocol;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (protocol == "VISA")
|
||||||
|
{
|
||||||
|
return new VISADevice(settings,
|
||||||
|
settings.value("deviceId").toString(),
|
||||||
|
settings.value("controlIds").toStringList(),
|
||||||
|
settings.value("sensorIds").toStringList(),
|
||||||
|
info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Device::checkSettings(const QHash<QString, QVariant>& settings, const QString& protocol)
|
||||||
|
{
|
||||||
|
if (protocol == "TPLink")
|
||||||
|
{
|
||||||
|
if (settings.contains("username") && settings.contains("password"))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "Device::checkSettings: A username and password are required for: " << protocol;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (protocol == "HomeAssistant")
|
||||||
|
{
|
||||||
|
if (settings.contains("apiKey"))
|
||||||
|
{
|
||||||
|
if (settings.contains("url"))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "Device::checkSettings: A host url is required for: " << protocol;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "Device::checkSettings: An apiKey is required for: " << protocol;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (protocol == "VISA")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "Device::checkSettings: Unsupported protocol: " << protocol;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList DeviceDiscoverer::m_typeStrings = {
|
||||||
|
"Auto",
|
||||||
|
"Boolean",
|
||||||
|
"Integer",
|
||||||
|
"Float",
|
||||||
|
"String",
|
||||||
|
"List",
|
||||||
|
"Button"
|
||||||
|
};
|
||||||
|
|
||||||
|
const QStringList DeviceDiscoverer::m_widgetTypeStrings = {
|
||||||
|
"Spin box",
|
||||||
|
"Dial",
|
||||||
|
"Slider"
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceDiscoverer::DeviceDiscoverer()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer *DeviceDiscoverer::getDiscoverer(const QHash<QString, QVariant>& settings, const QString& protocol)
|
||||||
|
{
|
||||||
|
if (Device::checkSettings(settings, protocol))
|
||||||
|
{
|
||||||
|
if (protocol == "TPLink")
|
||||||
|
{
|
||||||
|
return new TPLinkDeviceDiscoverer(settings.value("username").toString(), settings.value("password").toString());
|
||||||
|
}
|
||||||
|
else if (protocol == "HomeAssistant")
|
||||||
|
{
|
||||||
|
return new HomeAssistantDeviceDiscoverer(settings.value("apiKey").toString(), settings.value("url").toString());
|
||||||
|
}
|
||||||
|
else if (protocol == "VISA")
|
||||||
|
{
|
||||||
|
return new VISADeviceDiscoverer(settings.value("resourceFilter").toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DeviceDiscoverer::DeviceInfo::DeviceInfo()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::DeviceInfo::DeviceInfo(const DeviceInfo &info)
|
||||||
|
{
|
||||||
|
m_name = info.m_name;
|
||||||
|
m_id = info.m_id;
|
||||||
|
m_model = info.m_model;
|
||||||
|
// Take deep-copy of controls and sensors
|
||||||
|
for (auto const control : info.m_controls) {
|
||||||
|
ControlInfo *ci = control->clone();
|
||||||
|
m_controls.append(ci);
|
||||||
|
}
|
||||||
|
for (auto const sensor : info.m_sensors) {
|
||||||
|
m_sensors.append(sensor->clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::DeviceInfo& DeviceDiscoverer::DeviceInfo::operator=(const DeviceInfo &info)
|
||||||
|
{
|
||||||
|
m_name = info.m_name;
|
||||||
|
m_id = info.m_id;
|
||||||
|
m_model = info.m_model;
|
||||||
|
qDeleteAll(m_controls);
|
||||||
|
m_controls.clear();
|
||||||
|
qDeleteAll(m_sensors);
|
||||||
|
m_sensors.clear();
|
||||||
|
// Take deep-copy of controls and sensors
|
||||||
|
for (auto const control : info.m_controls) {
|
||||||
|
m_controls.append(control->clone());
|
||||||
|
}
|
||||||
|
for (auto const sensor : info.m_sensors) {
|
||||||
|
m_sensors.append(sensor->clone());
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::DeviceInfo::~DeviceInfo()
|
||||||
|
{
|
||||||
|
qDeleteAll(m_controls);
|
||||||
|
m_controls.clear();
|
||||||
|
qDeleteAll(m_sensors);
|
||||||
|
m_sensors.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::DeviceInfo::operator QString() const
|
||||||
|
{
|
||||||
|
QString controls;
|
||||||
|
QString sensors;
|
||||||
|
|
||||||
|
for (auto control : m_controls) {
|
||||||
|
controls.append((QString)*control);
|
||||||
|
}
|
||||||
|
for (auto sensor : m_sensors) {
|
||||||
|
sensors.append((QString)*sensor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString("DeviceInfo: m_name: %1 m_id: %2 m_model: %3 m_controls: %4 m_sensors: %5")
|
||||||
|
.arg(m_name)
|
||||||
|
.arg(m_id)
|
||||||
|
.arg(m_model)
|
||||||
|
.arg(controls)
|
||||||
|
.arg(sensors);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DeviceDiscoverer::ControlInfo::ControlInfo() :
|
||||||
|
m_type(AUTO),
|
||||||
|
m_min(-1000000),
|
||||||
|
m_max(1000000),
|
||||||
|
m_scale(1.0f),
|
||||||
|
m_precision(3),
|
||||||
|
m_widgetType(SPIN_BOX)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::ControlInfo::operator QString() const
|
||||||
|
{
|
||||||
|
return QString("ControlInfo: m_name: %1 m_id: %2 m_type: %3")
|
||||||
|
.arg(m_name)
|
||||||
|
.arg(m_id)
|
||||||
|
.arg(DeviceDiscoverer::m_typeStrings[m_type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::ControlInfo *DeviceDiscoverer::ControlInfo::clone() const
|
||||||
|
{
|
||||||
|
return new ControlInfo(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray DeviceDiscoverer::ControlInfo::serialize() const
|
||||||
|
{
|
||||||
|
SimpleSerializer s(1);
|
||||||
|
|
||||||
|
s.writeString(1, m_name);
|
||||||
|
s.writeString(2, m_id);
|
||||||
|
s.writeS32(3, (int)m_type);
|
||||||
|
s.writeFloat(4, m_min);
|
||||||
|
s.writeFloat(5, m_max);
|
||||||
|
s.writeFloat(6, m_scale);
|
||||||
|
s.writeS32(7, m_precision);
|
||||||
|
s.writeList(8, m_values);
|
||||||
|
s.writeS32(9, (int)m_widgetType);
|
||||||
|
s.writeString(10, m_units);
|
||||||
|
|
||||||
|
return s.final();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeviceDiscoverer::ControlInfo::deserialize(const QByteArray& data)
|
||||||
|
{
|
||||||
|
SimpleDeserializer d(data);
|
||||||
|
|
||||||
|
if (!d.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.getVersion() == 1)
|
||||||
|
{
|
||||||
|
d.readString(1, &m_name);
|
||||||
|
d.readString(2, &m_id);
|
||||||
|
d.readS32(3, (int*)&m_type);
|
||||||
|
d.readFloat(4, &m_min);
|
||||||
|
d.readFloat(5, &m_max);
|
||||||
|
d.readFloat(6, &m_scale, 1.0f);
|
||||||
|
d.readS32(7, &m_precision, 3);
|
||||||
|
d.readList(8, &m_values);
|
||||||
|
d.readS32(9, (int *)&m_widgetType);
|
||||||
|
d.readString(10, &m_units);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::SensorInfo::operator QString() const
|
||||||
|
{
|
||||||
|
return QString("SensorInfo: m_name: %1 m_id: %2 m_type: %3")
|
||||||
|
.arg(m_name)
|
||||||
|
.arg(m_id)
|
||||||
|
.arg(DeviceDiscoverer::m_typeStrings[m_type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::SensorInfo *DeviceDiscoverer::SensorInfo::clone() const
|
||||||
|
{
|
||||||
|
return new SensorInfo(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray DeviceDiscoverer::SensorInfo::serialize() const
|
||||||
|
{
|
||||||
|
SimpleSerializer s(1);
|
||||||
|
|
||||||
|
s.writeString(1, m_name);
|
||||||
|
s.writeString(2, m_id);
|
||||||
|
s.writeS32(3, (int)m_type);
|
||||||
|
s.writeString(4, m_units);
|
||||||
|
|
||||||
|
return s.final();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeviceDiscoverer::SensorInfo::deserialize(const QByteArray& data)
|
||||||
|
{
|
||||||
|
SimpleDeserializer d(data);
|
||||||
|
|
||||||
|
if (!d.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.getVersion() == 1)
|
||||||
|
{
|
||||||
|
d.readString(1, &m_name);
|
||||||
|
d.readString(2, &m_id);
|
||||||
|
d.readS32(3, (int*)&m_type);
|
||||||
|
d.readString(4, &m_units);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray DeviceDiscoverer::DeviceInfo::serialize() const
|
||||||
|
{
|
||||||
|
SimpleSerializer s(1);
|
||||||
|
|
||||||
|
s.writeString(1, m_name);
|
||||||
|
s.writeString(2, m_id);
|
||||||
|
s.writeString(3, m_model);
|
||||||
|
s.writeList(10, m_controls);
|
||||||
|
s.writeList(11, m_sensors);
|
||||||
|
|
||||||
|
return s.final();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeviceDiscoverer::DeviceInfo::deserialize(const QByteArray& data)
|
||||||
|
{
|
||||||
|
SimpleDeserializer d(data);
|
||||||
|
|
||||||
|
if (!d.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.getVersion() == 1)
|
||||||
|
{
|
||||||
|
QByteArray blob;
|
||||||
|
|
||||||
|
d.readString(1, &m_name);
|
||||||
|
d.readString(2, &m_id);
|
||||||
|
d.readString(3, &m_model);
|
||||||
|
d.readList(10, &m_controls);
|
||||||
|
d.readList(11, &m_sensors);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::ControlInfo *DeviceDiscoverer::DeviceInfo::getControl(const QString &id) const
|
||||||
|
{
|
||||||
|
for (auto c : m_controls)
|
||||||
|
{
|
||||||
|
if (c->m_id == id) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::SensorInfo *DeviceDiscoverer::DeviceInfo::getSensor(const QString &id) const
|
||||||
|
{
|
||||||
|
for (auto s : m_sensors)
|
||||||
|
{
|
||||||
|
if (s->m_id == id) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeviceDiscoverer::DeviceInfo::deleteControl(const QString &id)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < m_controls.size(); i++)
|
||||||
|
{
|
||||||
|
if (m_controls[i]->m_id == id)
|
||||||
|
{
|
||||||
|
delete m_controls.takeAt(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeviceDiscoverer::DeviceInfo::deleteSensor(const QString &id)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < m_sensors.size(); i++)
|
||||||
|
{
|
||||||
|
if (m_sensors[i]->m_id == id)
|
||||||
|
{
|
||||||
|
delete m_sensors.takeAt(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QDataStream& operator<<(QDataStream& out, const DeviceDiscoverer::ControlInfo* control)
|
||||||
|
{
|
||||||
|
int typeId;
|
||||||
|
if (const VISADevice::VISAControl* c = dynamic_cast<const VISADevice::VISAControl *>(control)) {
|
||||||
|
typeId = 1;
|
||||||
|
} else {
|
||||||
|
typeId = 0;
|
||||||
|
}
|
||||||
|
out << typeId;
|
||||||
|
out << control->serialize();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDataStream& operator>>(QDataStream& in, DeviceDiscoverer::ControlInfo*& control)
|
||||||
|
{
|
||||||
|
QByteArray data;
|
||||||
|
int typeId;
|
||||||
|
in >> typeId;
|
||||||
|
if (typeId == 1) {
|
||||||
|
control = new VISADevice::VISAControl();
|
||||||
|
} else {
|
||||||
|
control = new DeviceDiscoverer::ControlInfo();
|
||||||
|
}
|
||||||
|
in >> data;
|
||||||
|
control->deserialize(data);
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDataStream& operator<<(QDataStream& out, const DeviceDiscoverer::SensorInfo* sensor)
|
||||||
|
{
|
||||||
|
int typeId;
|
||||||
|
if (const VISADevice::VISASensor* s = dynamic_cast<const VISADevice::VISASensor *>(sensor)) {
|
||||||
|
typeId = 1;
|
||||||
|
} else {
|
||||||
|
typeId = 0;
|
||||||
|
}
|
||||||
|
out << typeId;
|
||||||
|
out << sensor->serialize();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDataStream& operator>>(QDataStream& in, DeviceDiscoverer::SensorInfo*& sensor)
|
||||||
|
{
|
||||||
|
|
||||||
|
QByteArray data;
|
||||||
|
int typeId;
|
||||||
|
in >> typeId;
|
||||||
|
if (typeId == 1) {
|
||||||
|
sensor = new VISADevice::VISASensor();
|
||||||
|
} else {
|
||||||
|
sensor = new DeviceDiscoverer::SensorInfo();
|
||||||
|
}
|
||||||
|
in >> data;
|
||||||
|
sensor->deserialize(data);
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDataStream& operator<<(QDataStream& out, const VISADevice::VISASensor &sensor)
|
||||||
|
{
|
||||||
|
out << sensor.serialize();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDataStream& operator>>(QDataStream& in, VISADevice::VISASensor& sensor)
|
||||||
|
{
|
||||||
|
QByteArray data;
|
||||||
|
in >> data;
|
||||||
|
sensor.deserialize(data);
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDataStream& operator<<(QDataStream& out, const VISADevice::VISAControl &control)
|
||||||
|
{
|
||||||
|
out << control.serialize();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDataStream& operator>>(QDataStream& in, VISADevice::VISAControl& control)
|
||||||
|
{
|
||||||
|
QByteArray data;
|
||||||
|
in >> data;
|
||||||
|
control.deserialize(data);
|
||||||
|
return in;
|
||||||
|
}
|
142
sdrbase/util/iot/device.h
Normal file
142
sdrbase/util/iot/device.h
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Copyright (C) 2022 Jon Beniston, M7RCE //
|
||||||
|
// //
|
||||||
|
// This program is free software; you can redistribute it and/or modify //
|
||||||
|
// it under the terms of the GNU General Public License as published by //
|
||||||
|
// the Free Software Foundation as version 3 of the License, or //
|
||||||
|
// (at your option) any later version. //
|
||||||
|
// //
|
||||||
|
// This program is distributed in the hope that it will be useful, //
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||||
|
// GNU General Public License V3 for more details. //
|
||||||
|
// //
|
||||||
|
// You should have received a copy of the GNU General Public License //
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#ifndef INCLUDE_DEVICE_H
|
||||||
|
#define INCLUDE_DEVICE_H
|
||||||
|
|
||||||
|
#include <QtCore>
|
||||||
|
|
||||||
|
#include "export.h"
|
||||||
|
|
||||||
|
class QNetworkAccessManager;
|
||||||
|
class QNetworkReply;
|
||||||
|
|
||||||
|
class SDRBASE_API DeviceDiscoverer : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
enum Type {
|
||||||
|
AUTO,
|
||||||
|
BOOL,
|
||||||
|
INT,
|
||||||
|
FLOAT,
|
||||||
|
STRING,
|
||||||
|
LIST,
|
||||||
|
BUTTON
|
||||||
|
};
|
||||||
|
enum WidgetType {
|
||||||
|
SPIN_BOX,
|
||||||
|
DIAL,
|
||||||
|
SLIDER
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SDRBASE_API ControlInfo {
|
||||||
|
QString m_name;
|
||||||
|
QString m_id;
|
||||||
|
Type m_type; // Data type
|
||||||
|
float m_min; // Min/max when m_type=INT/FLOAT
|
||||||
|
float m_max;
|
||||||
|
float m_scale;
|
||||||
|
int m_precision;
|
||||||
|
QStringList m_values; // Allowed values when m_type==LIST or label for button when m_type==BUTTON
|
||||||
|
WidgetType m_widgetType;// For m_type==FLOAT
|
||||||
|
QString m_units;
|
||||||
|
|
||||||
|
ControlInfo();
|
||||||
|
operator QString() const;
|
||||||
|
virtual ControlInfo *clone() const;
|
||||||
|
virtual QByteArray serialize() const;
|
||||||
|
virtual bool deserialize(const QByteArray& data);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SDRBASE_API SensorInfo {
|
||||||
|
QString m_name;
|
||||||
|
QString m_id;
|
||||||
|
Type m_type;
|
||||||
|
QString m_units; // W/Watts etc
|
||||||
|
|
||||||
|
operator QString() const;
|
||||||
|
virtual SensorInfo *clone() const;
|
||||||
|
virtual QByteArray serialize() const;
|
||||||
|
virtual bool deserialize(const QByteArray& data);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SDRBASE_API DeviceInfo {
|
||||||
|
QString m_name; // User friendly name
|
||||||
|
QString m_id; // ID for the device used by the API
|
||||||
|
QString m_model; // Model name
|
||||||
|
QList<ControlInfo *> m_controls;
|
||||||
|
QList<SensorInfo *> m_sensors;
|
||||||
|
|
||||||
|
DeviceInfo();
|
||||||
|
DeviceInfo(const DeviceInfo &info);
|
||||||
|
~DeviceInfo();
|
||||||
|
DeviceInfo& operator=(const DeviceInfo &info);
|
||||||
|
operator QString() const;
|
||||||
|
QByteArray serialize() const;
|
||||||
|
bool deserialize(const QByteArray& data);
|
||||||
|
ControlInfo *getControl(const QString &id) const;
|
||||||
|
SensorInfo *getSensor(const QString &id) const;
|
||||||
|
void deleteControl(const QString &id);
|
||||||
|
void deleteSensor(const QString &id);
|
||||||
|
};
|
||||||
|
|
||||||
|
protected:
|
||||||
|
DeviceDiscoverer();
|
||||||
|
|
||||||
|
public:
|
||||||
|
static DeviceDiscoverer *getDiscoverer(const QHash<QString, QVariant>& settings, const QString& protocol="TPLink");
|
||||||
|
static const QStringList m_typeStrings;
|
||||||
|
static const QStringList m_widgetTypeStrings;
|
||||||
|
|
||||||
|
virtual void getDevices() = 0;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void deviceList(const QList<DeviceInfo> &devices);
|
||||||
|
void error(const QString &msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
class SDRBASE_API Device : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
protected:
|
||||||
|
Device(DeviceDiscoverer::DeviceInfo *info=nullptr);
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
static Device* create(const QHash<QString, QVariant>& settings, const QString& protocol="TPLink", DeviceDiscoverer::DeviceInfo *info=nullptr);
|
||||||
|
static bool checkSettings(const QHash<QString, QVariant>& settings, const QString& protocol);
|
||||||
|
|
||||||
|
virtual void getState() = 0;
|
||||||
|
virtual void setState(const QString &controlId, bool state) {}
|
||||||
|
virtual void setState(const QString &controlId, int state) {}
|
||||||
|
virtual void setState(const QString &controlId, float state) {}
|
||||||
|
virtual void setState(const QString &controlId, const QString &state) {}
|
||||||
|
virtual QString getProtocol() const = 0;
|
||||||
|
virtual QString getDeviceId() const = 0;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void deviceUpdated(QHash<QString, QVariant>); // Called when new state available. Hash keys are control and sensor IDs
|
||||||
|
void deviceUnavailable(); // Called when device is unavailable. error() isn't signalled, as we expect devices to come and go
|
||||||
|
void error(const QString &msg); // Called on terminal error, such as invalid authentication details
|
||||||
|
|
||||||
|
protected:
|
||||||
|
DeviceDiscoverer::DeviceInfo m_info;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* INCLUDE_DEVICE_H */
|
317
sdrbase/util/iot/homeassistant.cpp
Normal file
317
sdrbase/util/iot/homeassistant.cpp
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Copyright (C) 2022 Jon Beniston, M7RCE //
|
||||||
|
// //
|
||||||
|
// This program is free software; you can redistribute it and/or modify //
|
||||||
|
// it under the terms of the GNU General Public License as published by //
|
||||||
|
// the Free Software Foundation as version 3 of the License, or //
|
||||||
|
// (at your option) any later version. //
|
||||||
|
// //
|
||||||
|
// This program is distributed in the hope that it will be useful, //
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||||
|
// GNU General Public License V3 for more details. //
|
||||||
|
// //
|
||||||
|
// You should have received a copy of the GNU General Public License //
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#include "util/iot/homeassistant.h"
|
||||||
|
#include "util/simpleserializer.h"
|
||||||
|
|
||||||
|
HomeAssistantDevice::HomeAssistantDevice(const QString& apiKey, const QString& url, const QString &deviceId,
|
||||||
|
const QStringList &controls, const QStringList &sensors,
|
||||||
|
DeviceDiscoverer::DeviceInfo *info) :
|
||||||
|
Device(info),
|
||||||
|
m_apiKey(apiKey),
|
||||||
|
m_url(url),
|
||||||
|
m_deviceId(deviceId)
|
||||||
|
{
|
||||||
|
m_entities = controls;
|
||||||
|
m_entities.append(sensors);
|
||||||
|
m_networkManager = new QNetworkAccessManager();
|
||||||
|
QObject::connect(
|
||||||
|
m_networkManager,
|
||||||
|
&QNetworkAccessManager::finished,
|
||||||
|
this,
|
||||||
|
&HomeAssistantDevice::handleReply
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeAssistantDevice::~HomeAssistantDevice()
|
||||||
|
{
|
||||||
|
QObject::disconnect(
|
||||||
|
m_networkManager,
|
||||||
|
&QNetworkAccessManager::finished,
|
||||||
|
this,
|
||||||
|
&HomeAssistantDevice::handleReply
|
||||||
|
);
|
||||||
|
delete m_networkManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeAssistantDevice::getState()
|
||||||
|
{
|
||||||
|
// Get state for all entities of the device
|
||||||
|
for (auto entity : m_entities)
|
||||||
|
{
|
||||||
|
QUrl url(m_url + "/api/states/" + entity);
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer " + m_apiKey.toLocal8Bit());
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
m_networkManager->get(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeAssistantDevice::setState(const QString &controlId, bool state)
|
||||||
|
{
|
||||||
|
QString domain = controlId.left(controlId.indexOf("."));
|
||||||
|
QUrl url(m_url + "/api/services/" + domain + "/turn_" + (state ? "on" : "off"));
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer " + m_apiKey.toLocal8Bit());
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
|
QJsonObject object {
|
||||||
|
{"entity_id", controlId}
|
||||||
|
};
|
||||||
|
QJsonDocument document;
|
||||||
|
document.setObject(object);
|
||||||
|
|
||||||
|
m_networkManager->post(request, document.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeAssistantDevice::handleReply(QNetworkReply* reply)
|
||||||
|
{
|
||||||
|
if (reply)
|
||||||
|
{
|
||||||
|
if (!reply->error())
|
||||||
|
{
|
||||||
|
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
|
||||||
|
//qDebug() << "Received " << document;
|
||||||
|
if (document.isObject())
|
||||||
|
{
|
||||||
|
QHash<QString, QVariant> status;
|
||||||
|
QJsonObject obj = document.object();
|
||||||
|
|
||||||
|
if (obj.contains(QStringLiteral("entity_id")) && obj.contains(QStringLiteral("state")))
|
||||||
|
{
|
||||||
|
QString entityId = obj.value(QStringLiteral("entity_id")).toString();
|
||||||
|
QString state = obj.value(QStringLiteral("state")).toString();
|
||||||
|
bool dOk;
|
||||||
|
bool iOk;
|
||||||
|
int i = state.toInt(&iOk);
|
||||||
|
double d = state.toDouble(&dOk);
|
||||||
|
if ((state == "on") || (state == "playing")) {
|
||||||
|
status.insert(entityId, 1);
|
||||||
|
} else if ((state == "off") || (state == "paused")) {
|
||||||
|
status.insert(entityId, 0);
|
||||||
|
} else if (iOk) {
|
||||||
|
status.insert(entityId, i);
|
||||||
|
} else if (dOk) {
|
||||||
|
status.insert(entityId, d);
|
||||||
|
} else {
|
||||||
|
status.insert(entityId, state);
|
||||||
|
}
|
||||||
|
emit deviceUpdated(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "HomeAssistantDevice::handleReply: Document is not an object: " << document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "HomeAssistantDevice::handleReply: error: " << reply->error();
|
||||||
|
}
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "HomeAssistantDevice::handleReply: reply is null";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeAssistantDeviceDiscoverer::HomeAssistantDeviceDiscoverer(const QString& apiKey, const QString& url) :
|
||||||
|
m_apiKey(apiKey),
|
||||||
|
m_url(url)
|
||||||
|
{
|
||||||
|
m_networkManager = new QNetworkAccessManager();
|
||||||
|
QObject::connect(
|
||||||
|
m_networkManager,
|
||||||
|
&QNetworkAccessManager::finished,
|
||||||
|
this,
|
||||||
|
&HomeAssistantDeviceDiscoverer::handleReply
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeAssistantDeviceDiscoverer::~HomeAssistantDeviceDiscoverer()
|
||||||
|
{
|
||||||
|
QObject::disconnect(
|
||||||
|
m_networkManager,
|
||||||
|
&QNetworkAccessManager::finished,
|
||||||
|
this,
|
||||||
|
&HomeAssistantDeviceDiscoverer::handleReply
|
||||||
|
);
|
||||||
|
delete m_networkManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeAssistantDeviceDiscoverer::getDevices()
|
||||||
|
{
|
||||||
|
QUrl url(m_url+ "/api/template");
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setRawHeader("Authorization", "Bearer " + m_apiKey.toLocal8Bit());
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
|
// Use templates to get a list of devices and associated entities
|
||||||
|
QString tpl =
|
||||||
|
"{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq',None)| list %}\n"
|
||||||
|
"{%- set ns = namespace(devices = []) %}\n"
|
||||||
|
"{%- for device in devices %}\n"
|
||||||
|
" {%- set entities = device_entities(device) | list %}\n"
|
||||||
|
" {%- if entities %}\n"
|
||||||
|
" {%- set ens = namespace(entityobjs = []) %}\n"
|
||||||
|
" {%- for entity in entities %}\n"
|
||||||
|
" {%- set entityobj = {'entity_id': entity, 'name': state_attr(entity,'friendly_name'), 'unit_of_measurement': state_attr(entity,'unit_of_measurement')} %}\n"
|
||||||
|
" {%- set ens.entityobjs = ens.entityobjs + [ entityobj ] %}\n"
|
||||||
|
" {%- endfor %}\n"
|
||||||
|
" {%- set obj = {'device_id': device, 'name': device_attr(device,'name'), 'name_by_user': device_attr(device,'name_by_user'), 'model': device_attr(device,'model'), 'entities': ens.entityobjs } %}\n"
|
||||||
|
" {%- set ns.devices = ns.devices + [ obj ] %}\n"
|
||||||
|
" {%- endif %}\n"
|
||||||
|
"{%- endfor %}\n"
|
||||||
|
"{{ ns.devices | tojson }}";
|
||||||
|
|
||||||
|
QJsonObject object {
|
||||||
|
{"template", tpl}
|
||||||
|
};
|
||||||
|
QJsonDocument document;
|
||||||
|
document.setObject(object);
|
||||||
|
|
||||||
|
m_networkManager->post(request, document.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
void HomeAssistantDeviceDiscoverer::handleReply(QNetworkReply* reply)
|
||||||
|
{
|
||||||
|
if (reply)
|
||||||
|
{
|
||||||
|
if (!reply->error())
|
||||||
|
{
|
||||||
|
QList<DeviceInfo> devices;
|
||||||
|
QByteArray data = reply->readAll();
|
||||||
|
//qDebug() << "Received " << data;
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument document = QJsonDocument::fromJson(data, &error);
|
||||||
|
if (!document.isNull())
|
||||||
|
{
|
||||||
|
if (document.isArray())
|
||||||
|
{
|
||||||
|
for (auto deviceRef : document.array())
|
||||||
|
{
|
||||||
|
QJsonObject deviceObj = deviceRef.toObject();
|
||||||
|
if (deviceObj.contains(QStringLiteral("device_id")) && deviceObj.contains(QStringLiteral("entities")))
|
||||||
|
{
|
||||||
|
QJsonArray entitiesArray = deviceObj.value(QStringLiteral("entities")).toArray();
|
||||||
|
if (entitiesArray.size() > 0)
|
||||||
|
{
|
||||||
|
DeviceInfo info;
|
||||||
|
info.m_id = deviceObj.value(QStringLiteral("device_id")).toString();
|
||||||
|
|
||||||
|
if (deviceObj.contains(QStringLiteral("name_by_user"))) {
|
||||||
|
info.m_name = deviceObj.value(QStringLiteral("name_by_user")).toString();
|
||||||
|
}
|
||||||
|
if (info.m_name.isEmpty() && deviceObj.contains(QStringLiteral("name"))) {
|
||||||
|
info.m_name = deviceObj.value(QStringLiteral("name")).toString();
|
||||||
|
}
|
||||||
|
if (deviceObj.contains(QStringLiteral("model"))) {
|
||||||
|
info.m_model = deviceObj.value(QStringLiteral("model")).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto entityRef : entitiesArray)
|
||||||
|
{
|
||||||
|
QJsonObject entityObj = entityRef.toObject();
|
||||||
|
QString entity = entityObj.value(QStringLiteral("entity_id")).toString();
|
||||||
|
QString name = entityObj.value(QStringLiteral("name")).toString();
|
||||||
|
QString domain = entity.left(entity.indexOf('.'));
|
||||||
|
if (domain == "binary_sensor")
|
||||||
|
{
|
||||||
|
SensorInfo *sensorInfo = new SensorInfo();
|
||||||
|
sensorInfo->m_name = name;
|
||||||
|
sensorInfo->m_id = entity;
|
||||||
|
sensorInfo->m_type = DeviceDiscoverer::BOOL;
|
||||||
|
sensorInfo->m_units = entityObj.value(QStringLiteral("unit_of_measurement")).toString();
|
||||||
|
info.m_sensors.append(sensorInfo);
|
||||||
|
}
|
||||||
|
else if (domain == "sensor")
|
||||||
|
{
|
||||||
|
SensorInfo *sensorInfo = new SensorInfo();
|
||||||
|
sensorInfo->m_name = name;
|
||||||
|
sensorInfo->m_id = entity;
|
||||||
|
sensorInfo->m_type = DeviceDiscoverer::FLOAT; // FIXME: Auto?
|
||||||
|
sensorInfo->m_units = entityObj.value(QStringLiteral("unit_of_measurement")).toString();
|
||||||
|
info.m_sensors.append(sensorInfo);
|
||||||
|
}
|
||||||
|
else if ((domain == "switch") || (domain == "light") || (domain == "media_player")) // Entities that support turn_on/turn_off
|
||||||
|
{
|
||||||
|
ControlInfo *controlInfo = new ControlInfo();
|
||||||
|
controlInfo->m_name = name;
|
||||||
|
controlInfo->m_id = entity;
|
||||||
|
controlInfo->m_type = DeviceDiscoverer::BOOL;
|
||||||
|
info.m_controls.append(controlInfo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: Unsupported domain: " << domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((info.m_controls.size() > 0) || (info.m_sensors.size() > 0))
|
||||||
|
{
|
||||||
|
devices.append(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: No entities " << deviceObj.value(QStringLiteral("device_id")).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: device_id or entities missing";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: Document is not an array: " << document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: Error parson JSON: " << error.errorString() << " at offset " << error.offset;
|
||||||
|
}
|
||||||
|
emit deviceList(devices);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: error: " << reply->error() << ":" << reply->errorString();
|
||||||
|
// Get QNetworkReply::AuthenticationRequiredError if token is invalid
|
||||||
|
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||||
|
emit error("Home Assistant: Authentication failed. Check access token is valid.");
|
||||||
|
} else {
|
||||||
|
emit error(QString("Home Assistant: Network error. %1").arg(reply->errorString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: reply is null";
|
||||||
|
}
|
||||||
|
}
|
70
sdrbase/util/iot/homeassistant.h
Normal file
70
sdrbase/util/iot/homeassistant.h
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Copyright (C) 2022 Jon Beniston, M7RCE //
|
||||||
|
// //
|
||||||
|
// This program is free software; you can redistribute it and/or modify //
|
||||||
|
// it under the terms of the GNU General Public License as published by //
|
||||||
|
// the Free Software Foundation as version 3 of the License, or //
|
||||||
|
// (at your option) any later version. //
|
||||||
|
// //
|
||||||
|
// This program is distributed in the hope that it will be useful, //
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||||
|
// GNU General Public License V3 for more details. //
|
||||||
|
// //
|
||||||
|
// You should have received a copy of the GNU General Public License //
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#ifndef INCLUDE_IOT_HOMEASSISTANT_H
|
||||||
|
#define INCLUDE_IOT_HOMEASSISTANT_H
|
||||||
|
|
||||||
|
#include "util/iot/device.h"
|
||||||
|
|
||||||
|
// Supports Home Assistant devices - https://www.home-assistant.io/
|
||||||
|
class SDRBASE_API HomeAssistantDevice : public Device {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
|
||||||
|
HomeAssistantDevice(const QString& apiKey, const QString& url, const QString &deviceId,
|
||||||
|
const QStringList &controls, const QStringList &sensors,
|
||||||
|
DeviceDiscoverer::DeviceInfo *info=nullptr);
|
||||||
|
~HomeAssistantDevice();
|
||||||
|
virtual void getState() override;
|
||||||
|
virtual void setState(const QString &controlId, bool state) override;
|
||||||
|
virtual QString getProtocol() const override { return "HomeAssistant"; }
|
||||||
|
virtual QString getDeviceId() const override { return m_deviceId; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
QString m_deviceId;
|
||||||
|
QStringList m_entities; // List of entities that are part of the device, to get state for (controls and sensors)
|
||||||
|
QString m_apiKey; // Bearer token
|
||||||
|
QString m_url; // Typically http://homeassistant.local:8123
|
||||||
|
QNetworkAccessManager *m_networkManager;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void handleReply(QNetworkReply* reply);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class SDRBASE_API HomeAssistantDeviceDiscoverer : public DeviceDiscoverer {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
|
||||||
|
HomeAssistantDeviceDiscoverer(const QString& apiKey, const QString& url);
|
||||||
|
~HomeAssistantDeviceDiscoverer();
|
||||||
|
virtual void getDevices() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
QString m_deviceId;
|
||||||
|
QString m_apiKey; // Bearer token
|
||||||
|
QString m_url; // Typically http://homeassistant.local:8123
|
||||||
|
QNetworkAccessManager *m_networkManager;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void handleReply(QNetworkReply* reply);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* INCLUDE_IOT_HOMEASSISTANT_H */
|
634
sdrbase/util/iot/tplink.cpp
Normal file
634
sdrbase/util/iot/tplink.cpp
Normal file
@ -0,0 +1,634 @@
|
|||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Copyright (C) 2022 Jon Beniston, M7RCE //
|
||||||
|
// //
|
||||||
|
// This program is free software; you can redistribute it and/or modify //
|
||||||
|
// it under the terms of the GNU General Public License as published by //
|
||||||
|
// the Free Software Foundation as version 3 of the License, or //
|
||||||
|
// (at your option) any later version. //
|
||||||
|
// //
|
||||||
|
// This program is distributed in the hope that it will be useful, //
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||||
|
// GNU General Public License V3 for more details. //
|
||||||
|
// //
|
||||||
|
// You should have received a copy of the GNU General Public License //
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#include "util/iot/tplink.h"
|
||||||
|
#include "util/simpleserializer.h"
|
||||||
|
|
||||||
|
const QString TPLinkCommon::m_url = "https://wap.tplinkcloud.com";
|
||||||
|
|
||||||
|
TPLinkCommon::TPLinkCommon(const QString& username, const QString &password) :
|
||||||
|
m_loggedIn(false),
|
||||||
|
m_outstandingRequest(false),
|
||||||
|
m_username(username),
|
||||||
|
m_password(password),
|
||||||
|
m_networkManager(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void TPLinkCommon::login()
|
||||||
|
{
|
||||||
|
QUrl url(m_url);
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
QJsonObject params {
|
||||||
|
{"appType", "Kasa_Android"},
|
||||||
|
{"cloudUserName", m_username},
|
||||||
|
{"cloudPassword", m_password},
|
||||||
|
{"terminalUUID", "9cc4653e-338f-48e4-b8ca-6ed3f67631e4"}
|
||||||
|
};
|
||||||
|
QJsonObject object {
|
||||||
|
{"method", "login"},
|
||||||
|
{"params", params}
|
||||||
|
};
|
||||||
|
QJsonDocument document;
|
||||||
|
document.setObject(object);
|
||||||
|
|
||||||
|
m_networkManager->post(request, document.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TPLinkCommon::handleLoginReply(QNetworkReply* reply, QString &errorMessage)
|
||||||
|
{
|
||||||
|
if (reply)
|
||||||
|
{
|
||||||
|
if (!reply->error())
|
||||||
|
{
|
||||||
|
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
|
||||||
|
if (document.isObject())
|
||||||
|
{
|
||||||
|
//qDebug() << "Received " << document;
|
||||||
|
if (!m_loggedIn)
|
||||||
|
{
|
||||||
|
QJsonObject obj = document.object();
|
||||||
|
if (obj.contains(QStringLiteral("error_code")))
|
||||||
|
{
|
||||||
|
int errorCode = obj.value(QStringLiteral("error_code")).toInt();
|
||||||
|
if (!errorCode)
|
||||||
|
{
|
||||||
|
if (obj.contains(QStringLiteral("result")))
|
||||||
|
{
|
||||||
|
QJsonObject result = obj.value(QStringLiteral("result")).toObject();
|
||||||
|
if (result.contains(QStringLiteral("token")))
|
||||||
|
{
|
||||||
|
m_loggedIn = true;
|
||||||
|
m_token = result.value(QStringLiteral("token")).toString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a token: " << result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a result object: " << obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: Non-zero error_code while logging in: " << errorCode;
|
||||||
|
if (obj.contains(QStringLiteral("msg")))
|
||||||
|
{
|
||||||
|
QString msg = obj.value(QStringLiteral("msg")).toString();
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: Error message: " << msg;
|
||||||
|
// Typical msg is "Incorrect email or password"
|
||||||
|
errorMessage = QString("TP-Link: Failed to log in. %1").arg(msg);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorMessage = QString("TP-Link: Failed to log in. Error code: %1").arg(errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain an error_code: " << obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: Document is not an object: " << document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: error: " << reply->error();
|
||||||
|
}
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: reply is null";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_loggedIn && errorMessage.isEmpty()) {
|
||||||
|
errorMessage = "TP-Link: Failed to log in.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TPLinkDevice::TPLinkDevice(const QString& username, const QString &password, const QString &deviceId, DeviceDiscoverer::DeviceInfo *info) :
|
||||||
|
Device(info),
|
||||||
|
TPLinkCommon(username, password),
|
||||||
|
m_deviceId(deviceId)
|
||||||
|
{
|
||||||
|
m_networkManager = new QNetworkAccessManager();
|
||||||
|
QObject::connect(
|
||||||
|
m_networkManager,
|
||||||
|
&QNetworkAccessManager::finished,
|
||||||
|
this,
|
||||||
|
&TPLinkDevice::handleReply
|
||||||
|
);
|
||||||
|
login();
|
||||||
|
}
|
||||||
|
|
||||||
|
TPLinkDevice::~TPLinkDevice()
|
||||||
|
{
|
||||||
|
QObject::disconnect(
|
||||||
|
m_networkManager,
|
||||||
|
&QNetworkAccessManager::finished,
|
||||||
|
this,
|
||||||
|
&TPLinkDevice::handleReply
|
||||||
|
);
|
||||||
|
delete m_networkManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TPLinkDevice::getState()
|
||||||
|
{
|
||||||
|
if (!m_loggedIn)
|
||||||
|
{
|
||||||
|
m_outstandingRequest = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QUrl url(m_url);
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
QJsonObject system;
|
||||||
|
system.insert("get_sysinfo", QJsonValue());
|
||||||
|
QJsonObject emeter;
|
||||||
|
emeter.insert("get_realtime", QJsonValue());
|
||||||
|
QJsonObject requestData {
|
||||||
|
{"system", system},
|
||||||
|
{"emeter", emeter}
|
||||||
|
};
|
||||||
|
QJsonObject params {
|
||||||
|
{"deviceId", m_deviceId},
|
||||||
|
{"requestData", requestData},
|
||||||
|
{"token", m_token}
|
||||||
|
};
|
||||||
|
QJsonObject object {
|
||||||
|
{"method", "passthrough"},
|
||||||
|
{"params", params}
|
||||||
|
};
|
||||||
|
QJsonDocument document;
|
||||||
|
document.setObject(object);
|
||||||
|
|
||||||
|
m_networkManager->post(request, document.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TPLinkDevice::setState(const QString &controlId, bool state)
|
||||||
|
{
|
||||||
|
if (!m_loggedIn)
|
||||||
|
{
|
||||||
|
// Should we queue these and apply after logged in?
|
||||||
|
qDebug() << "TPLinkDevice::setState: Unable to set state for " << controlId << " to " << state << " as not yet logged in";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QUrl url(m_url);
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
QJsonObject stateObj {
|
||||||
|
{"state", (int)state}
|
||||||
|
};
|
||||||
|
QJsonObject system {
|
||||||
|
{"set_relay_state", stateObj}
|
||||||
|
};
|
||||||
|
QJsonObject requestData {
|
||||||
|
{"system", system}
|
||||||
|
};
|
||||||
|
if (controlId != "switch") {
|
||||||
|
QJsonArray childIds {
|
||||||
|
controlId
|
||||||
|
};
|
||||||
|
QJsonObject context {
|
||||||
|
{"child_ids", childIds}
|
||||||
|
};
|
||||||
|
requestData.insert("context", QJsonValue(context));
|
||||||
|
}
|
||||||
|
QJsonObject params {
|
||||||
|
{"deviceId", m_deviceId},
|
||||||
|
{"requestData", requestData},
|
||||||
|
{"token", m_token}
|
||||||
|
};
|
||||||
|
QJsonObject object {
|
||||||
|
{"method", "passthrough"},
|
||||||
|
{"params", params}
|
||||||
|
};
|
||||||
|
QJsonDocument document;
|
||||||
|
document.setObject(object);
|
||||||
|
|
||||||
|
m_networkManager->post(request, document.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TPLinkDevice::handleReply(QNetworkReply* reply)
|
||||||
|
{
|
||||||
|
if (!m_loggedIn)
|
||||||
|
{
|
||||||
|
QString errorMessage;
|
||||||
|
TPLinkCommon::handleLoginReply(reply, errorMessage);
|
||||||
|
if (!errorMessage.isEmpty())
|
||||||
|
{
|
||||||
|
emit error(errorMessage);
|
||||||
|
}
|
||||||
|
else if (m_outstandingRequest)
|
||||||
|
{
|
||||||
|
m_outstandingRequest = true;
|
||||||
|
getState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (reply)
|
||||||
|
{
|
||||||
|
if (!reply->error())
|
||||||
|
{
|
||||||
|
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
|
||||||
|
if (document.isObject())
|
||||||
|
{
|
||||||
|
//qDebug() << "Received " << document;
|
||||||
|
QJsonObject obj = document.object();
|
||||||
|
if (obj.contains(QStringLiteral("result")))
|
||||||
|
{
|
||||||
|
QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject();
|
||||||
|
QHash<QString, QVariant> status;
|
||||||
|
|
||||||
|
if (resultObj.contains(QStringLiteral("responseData")))
|
||||||
|
{
|
||||||
|
QJsonObject responseDataObj = resultObj.value(QStringLiteral("responseData")).toObject();
|
||||||
|
if (responseDataObj.contains(QStringLiteral("system")))
|
||||||
|
{
|
||||||
|
QJsonObject systemObj = responseDataObj.value(QStringLiteral("system")).toObject();
|
||||||
|
if (systemObj.contains(QStringLiteral("get_sysinfo")))
|
||||||
|
{
|
||||||
|
QJsonObject sysInfoObj = systemObj.value(QStringLiteral("get_sysinfo")).toObject();
|
||||||
|
if (sysInfoObj.contains(QStringLiteral("child_num")))
|
||||||
|
{
|
||||||
|
int childNum = sysInfoObj.value(QStringLiteral("child_num")).toInt();
|
||||||
|
QJsonArray children = sysInfoObj.value(QStringLiteral("children")).toArray();
|
||||||
|
for (auto childRef : children)
|
||||||
|
{
|
||||||
|
QJsonObject childObj = childRef.toObject();
|
||||||
|
if (childObj.contains(QStringLiteral("state")) && childObj.contains(QStringLiteral("id")))
|
||||||
|
{
|
||||||
|
int state = childObj.value(QStringLiteral("state")).toInt();
|
||||||
|
QString id = childObj.value(QStringLiteral("id")).toString();
|
||||||
|
status.insert(id, state); // key should match id in discoverer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (sysInfoObj.contains(QStringLiteral("relay_state")))
|
||||||
|
{
|
||||||
|
int state = sysInfoObj.value(QStringLiteral("relay_state")).toInt();
|
||||||
|
status.insert("switch", state); // key should match id in discoverer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// KP115 has emeter, but KP105 doesn't
|
||||||
|
if (responseDataObj.contains(QStringLiteral("emeter")))
|
||||||
|
{
|
||||||
|
QJsonObject emeterObj = responseDataObj.value(QStringLiteral("emeter")).toObject();
|
||||||
|
if (emeterObj.contains(QStringLiteral("get_realtime")))
|
||||||
|
{
|
||||||
|
QJsonObject realtimeObj = emeterObj.value(QStringLiteral("get_realtime")).toObject();
|
||||||
|
if (realtimeObj.contains(QStringLiteral("current_ma")))
|
||||||
|
{
|
||||||
|
double current = realtimeObj.value(QStringLiteral("current_ma")).toDouble();
|
||||||
|
status.insert("current", current / 1000.0);
|
||||||
|
}
|
||||||
|
if (realtimeObj.contains(QStringLiteral("voltage_mv")))
|
||||||
|
{
|
||||||
|
double voltage = realtimeObj.value(QStringLiteral("voltage_mv")).toDouble();
|
||||||
|
status.insert("voltage", voltage / 1000.0);
|
||||||
|
}
|
||||||
|
if (realtimeObj.contains(QStringLiteral("power_mw")))
|
||||||
|
{
|
||||||
|
double power = realtimeObj.value(QStringLiteral("power_mw")).toDouble();
|
||||||
|
status.insert("power", power / 1000.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit deviceUpdated(status);
|
||||||
|
}
|
||||||
|
else if (obj.contains(QStringLiteral("error_code")))
|
||||||
|
{
|
||||||
|
// If a device isn't available, we can get:
|
||||||
|
// {"error_code":-20002,"msg":"Request timeout"}
|
||||||
|
// {"error_code":-20571,"msg":"Device is offline"}
|
||||||
|
int errorCode = obj.value(QStringLiteral("error_code")).toInt();
|
||||||
|
QString msg = obj.value(QStringLiteral("msg")).toString();
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: Error code: " << errorCode << " " << msg;
|
||||||
|
|
||||||
|
emit deviceUnavailable();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a result or error_code: " << obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: Document is not an object: " << document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: error: " << reply->error();
|
||||||
|
}
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDevice::handleReply: reply is null";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TPLinkDeviceDiscoverer::TPLinkDeviceDiscoverer(const QString& username, const QString &password) :
|
||||||
|
TPLinkCommon(username, password)
|
||||||
|
{
|
||||||
|
m_networkManager = new QNetworkAccessManager();
|
||||||
|
QObject::connect(
|
||||||
|
m_networkManager,
|
||||||
|
&QNetworkAccessManager::finished,
|
||||||
|
this,
|
||||||
|
&TPLinkDeviceDiscoverer::handleReply
|
||||||
|
);
|
||||||
|
login();
|
||||||
|
}
|
||||||
|
|
||||||
|
TPLinkDeviceDiscoverer::~TPLinkDeviceDiscoverer()
|
||||||
|
{
|
||||||
|
QObject::disconnect(
|
||||||
|
m_networkManager,
|
||||||
|
&QNetworkAccessManager::finished,
|
||||||
|
this,
|
||||||
|
&TPLinkDeviceDiscoverer::handleReply
|
||||||
|
);
|
||||||
|
delete m_networkManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TPLinkDeviceDiscoverer::getDevices()
|
||||||
|
{
|
||||||
|
if (!m_loggedIn)
|
||||||
|
{
|
||||||
|
m_outstandingRequest = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QUrl url(m_url);
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
QJsonObject params {
|
||||||
|
{"token", m_token}
|
||||||
|
};
|
||||||
|
QJsonObject object {
|
||||||
|
{"method", "getDeviceList"},
|
||||||
|
{"params", params}
|
||||||
|
};
|
||||||
|
QJsonDocument document;
|
||||||
|
document.setObject(object);
|
||||||
|
|
||||||
|
m_networkManager->post(request, document.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TPLinkDeviceDiscoverer::getState(const QString &deviceId)
|
||||||
|
{
|
||||||
|
QUrl url(m_url);
|
||||||
|
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
QJsonObject system;
|
||||||
|
system.insert("get_sysinfo", QJsonValue());
|
||||||
|
QJsonObject emeter;
|
||||||
|
emeter.insert("get_realtime", QJsonValue());
|
||||||
|
QJsonObject requestData {
|
||||||
|
{"system", system},
|
||||||
|
{"emeter", emeter}
|
||||||
|
};
|
||||||
|
QJsonObject params {
|
||||||
|
{"deviceId", deviceId},
|
||||||
|
{"requestData", requestData},
|
||||||
|
{"token", m_token}
|
||||||
|
};
|
||||||
|
QJsonObject object {
|
||||||
|
{"method", "passthrough"},
|
||||||
|
{"params", params}
|
||||||
|
};
|
||||||
|
QJsonDocument document;
|
||||||
|
document.setObject(object);
|
||||||
|
|
||||||
|
m_getStateReplies.insert(m_networkManager->post(request, document.toJson()), deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TPLinkDeviceDiscoverer::handleReply(QNetworkReply* reply)
|
||||||
|
{
|
||||||
|
if (!m_loggedIn)
|
||||||
|
{
|
||||||
|
QString errorMessage;
|
||||||
|
TPLinkCommon::handleLoginReply(reply, errorMessage);
|
||||||
|
if (!errorMessage.isEmpty())
|
||||||
|
{
|
||||||
|
emit error(errorMessage);
|
||||||
|
}
|
||||||
|
else if (m_outstandingRequest)
|
||||||
|
{
|
||||||
|
m_outstandingRequest = false;
|
||||||
|
getDevices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (reply)
|
||||||
|
{
|
||||||
|
if (!reply->error())
|
||||||
|
{
|
||||||
|
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
|
||||||
|
if (document.isObject())
|
||||||
|
{
|
||||||
|
//qDebug() << "Received " << document;
|
||||||
|
QJsonObject obj = document.object();
|
||||||
|
|
||||||
|
if (m_getStateReplies.contains(reply))
|
||||||
|
{
|
||||||
|
// Reply for getState
|
||||||
|
m_getStateReplies.remove(reply);
|
||||||
|
QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject();
|
||||||
|
if (resultObj.contains(QStringLiteral("responseData")))
|
||||||
|
{
|
||||||
|
QJsonObject responseDataObj = resultObj.value(QStringLiteral("responseData")).toObject();
|
||||||
|
if (responseDataObj.contains(QStringLiteral("system")))
|
||||||
|
{
|
||||||
|
DeviceInfo info;
|
||||||
|
QJsonObject systemObj = responseDataObj.value(QStringLiteral("system")).toObject();
|
||||||
|
if (systemObj.contains(QStringLiteral("get_sysinfo")))
|
||||||
|
{
|
||||||
|
QJsonObject sysInfoObj = systemObj.value(QStringLiteral("get_sysinfo")).toObject();
|
||||||
|
if (sysInfoObj.contains(QStringLiteral("alias"))) {
|
||||||
|
info.m_name = sysInfoObj.value(QStringLiteral("alias")).toString();
|
||||||
|
}
|
||||||
|
if (sysInfoObj.contains(QStringLiteral("model"))) {
|
||||||
|
info.m_model = sysInfoObj.value(QStringLiteral("model")).toString();
|
||||||
|
}
|
||||||
|
if (sysInfoObj.contains(QStringLiteral("deviceId"))) {
|
||||||
|
info.m_id = sysInfoObj.value(QStringLiteral("deviceId")).toString();
|
||||||
|
}
|
||||||
|
if (sysInfoObj.contains(QStringLiteral("child_num")))
|
||||||
|
{
|
||||||
|
int childNum = sysInfoObj.value(QStringLiteral("child_num")).toInt();
|
||||||
|
QJsonArray children = sysInfoObj.value(QStringLiteral("children")).toArray();
|
||||||
|
int child = 1;
|
||||||
|
for (auto childRef : children)
|
||||||
|
{
|
||||||
|
QJsonObject childObj = childRef.toObject();
|
||||||
|
ControlInfo *controlInfo = new ControlInfo();
|
||||||
|
controlInfo->m_id = childObj.value(QStringLiteral("id")).toString();
|
||||||
|
if (childObj.contains(QStringLiteral("alias"))) {
|
||||||
|
controlInfo->m_name = childObj.value(QStringLiteral("alias")).toString();
|
||||||
|
}
|
||||||
|
controlInfo->m_type = DeviceDiscoverer::BOOL;
|
||||||
|
info.m_controls.append(controlInfo);
|
||||||
|
child++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (sysInfoObj.contains(QStringLiteral("relay_state")))
|
||||||
|
{
|
||||||
|
ControlInfo *controlInfo = new ControlInfo();
|
||||||
|
controlInfo->m_id = "switch";
|
||||||
|
if (sysInfoObj.contains(QStringLiteral("alias"))) {
|
||||||
|
controlInfo->m_name = sysInfoObj.value(QStringLiteral("alias")).toString();
|
||||||
|
}
|
||||||
|
controlInfo->m_type = DeviceDiscoverer::BOOL;
|
||||||
|
info.m_controls.append(controlInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDeviceDiscoverer::handleReply: get_sysinfo missing";
|
||||||
|
}
|
||||||
|
// KP115 has energy meter, but KP105 doesn't. KP105 will have emeter object, but without get_realtime sub-object
|
||||||
|
if (responseDataObj.contains(QStringLiteral("emeter")))
|
||||||
|
{
|
||||||
|
QJsonObject emeterObj = responseDataObj.value(QStringLiteral("emeter")).toObject();
|
||||||
|
if (emeterObj.contains(QStringLiteral("get_realtime")))
|
||||||
|
{
|
||||||
|
QJsonObject realtimeObj = emeterObj.value(QStringLiteral("get_realtime")).toObject();
|
||||||
|
if (realtimeObj.contains(QStringLiteral("current_ma")))
|
||||||
|
{
|
||||||
|
SensorInfo *currentSensorInfo = new SensorInfo();
|
||||||
|
currentSensorInfo->m_name = "Current";
|
||||||
|
currentSensorInfo->m_id = "current";
|
||||||
|
currentSensorInfo->m_type = DeviceDiscoverer::FLOAT;
|
||||||
|
currentSensorInfo->m_units = "A";
|
||||||
|
info.m_sensors.append(currentSensorInfo);
|
||||||
|
}
|
||||||
|
if (realtimeObj.contains(QStringLiteral("voltage_mv")))
|
||||||
|
{
|
||||||
|
SensorInfo *voltageSensorInfo = new SensorInfo();
|
||||||
|
voltageSensorInfo->m_name = "Voltage";
|
||||||
|
voltageSensorInfo->m_id = "voltage";
|
||||||
|
voltageSensorInfo->m_type = DeviceDiscoverer::FLOAT;
|
||||||
|
voltageSensorInfo->m_units = "V";
|
||||||
|
info.m_sensors.append(voltageSensorInfo);
|
||||||
|
}
|
||||||
|
if (realtimeObj.contains(QStringLiteral("power_mw")))
|
||||||
|
{
|
||||||
|
SensorInfo *powerSensorInfo = new SensorInfo();
|
||||||
|
powerSensorInfo->m_name = "Power";
|
||||||
|
powerSensorInfo->m_id = "power";
|
||||||
|
powerSensorInfo->m_type = DeviceDiscoverer::FLOAT;
|
||||||
|
powerSensorInfo->m_units = "W";
|
||||||
|
info.m_sensors.append(powerSensorInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info.m_controls.size() > 0) {
|
||||||
|
m_devices.append(info);
|
||||||
|
} else {
|
||||||
|
qDebug() << "TPLinkDeviceDiscoverer::handleReply: No controls in info";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDeviceDiscoverer::handleReply: No responseData";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_getStateReplies.size() == 0)
|
||||||
|
{
|
||||||
|
emit deviceList(m_devices);
|
||||||
|
m_devices.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Reply for getDevice
|
||||||
|
if (obj.contains(QStringLiteral("result")))
|
||||||
|
{
|
||||||
|
QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject();
|
||||||
|
if (resultObj.contains(QStringLiteral("deviceList")))
|
||||||
|
{
|
||||||
|
QJsonArray deviceArray = resultObj.value(QStringLiteral("deviceList")).toArray();
|
||||||
|
for (auto deviceRef : deviceArray)
|
||||||
|
{
|
||||||
|
QJsonObject deviceObj = deviceRef.toObject();
|
||||||
|
if (deviceObj.contains(QStringLiteral("deviceId")) && deviceObj.contains(QStringLiteral("deviceType")))
|
||||||
|
{
|
||||||
|
// In order to discover what controls and sensors a device has, we need to get sysinfo
|
||||||
|
getState(deviceObj.value(QStringLiteral("deviceId")).toString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDeviceDiscoverer::handleReply: deviceList element doesn't contain a deviceId: " << deviceObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDeviceDiscoverer::handleReply: result doesn't contain a deviceList: " << resultObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDeviceDiscoverer::handleReply: Object doesn't contain a result: " << obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDeviceDiscoverer::handleReply: Document is not an object: " << document;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDeviceDiscoverer::handleReply: error: " << reply->error();
|
||||||
|
}
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "TPLinkDeviceDiscoverer::handleReply: reply is null";
|
||||||
|
}
|
||||||
|
}
|
77
sdrbase/util/iot/tplink.h
Normal file
77
sdrbase/util/iot/tplink.h
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Copyright (C) 2022 Jon Beniston, M7RCE //
|
||||||
|
// //
|
||||||
|
// This program is free software; you can redistribute it and/or modify //
|
||||||
|
// it under the terms of the GNU General Public License as published by //
|
||||||
|
// the Free Software Foundation as version 3 of the License, or //
|
||||||
|
// (at your option) any later version. //
|
||||||
|
// //
|
||||||
|
// This program is distributed in the hope that it will be useful, //
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||||
|
// GNU General Public License V3 for more details. //
|
||||||
|
// //
|
||||||
|
// You should have received a copy of the GNU General Public License //
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#ifndef INCLUDE_IOT_TPLINK_H
|
||||||
|
#define INCLUDE_IOT_TPLINK_H
|
||||||
|
|
||||||
|
#include "util/iot/device.h"
|
||||||
|
|
||||||
|
class SDRBASE_API TPLinkCommon {
|
||||||
|
protected:
|
||||||
|
TPLinkCommon(const QString& username, const QString &password);
|
||||||
|
void login();
|
||||||
|
void handleLoginReply(QNetworkReply* reply, QString &errorMessage);
|
||||||
|
|
||||||
|
bool m_loggedIn;
|
||||||
|
bool m_outstandingRequest; // Issue getState / getDevices after logged in
|
||||||
|
QString m_username;
|
||||||
|
QString m_password;
|
||||||
|
QString m_token;
|
||||||
|
QNetworkAccessManager *m_networkManager;
|
||||||
|
|
||||||
|
static const QString m_url;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Supports TPLink's Kasa plugs - https://www.tp-link.com/uk/smarthome/
|
||||||
|
class SDRBASE_API TPLinkDevice : public Device, TPLinkCommon {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
|
||||||
|
TPLinkDevice(const QString& username, const QString &password, const QString &deviceId, DeviceDiscoverer::DeviceInfo *info=nullptr);
|
||||||
|
~TPLinkDevice();
|
||||||
|
virtual void getState() override;
|
||||||
|
virtual void setState(const QString &controlId, bool state) override;
|
||||||
|
virtual QString getProtocol() const override { return "TPLink"; }
|
||||||
|
virtual QString getDeviceId() const override { return m_deviceId; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
QString m_deviceId;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void handleReply(QNetworkReply* reply);
|
||||||
|
};
|
||||||
|
|
||||||
|
class SDRBASE_API TPLinkDeviceDiscoverer : public DeviceDiscoverer, TPLinkCommon {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
|
||||||
|
TPLinkDeviceDiscoverer(const QString& username, const QString &password);
|
||||||
|
~TPLinkDeviceDiscoverer();
|
||||||
|
virtual void getDevices() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void getState(const QString &deviceId);
|
||||||
|
|
||||||
|
QHash<QNetworkReply *, QString> m_getStateReplies;
|
||||||
|
QList<DeviceInfo> m_devices;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void handleReply(QNetworkReply* reply);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* INCLUDE_IOT_TPLINK_H */
|
506
sdrbase/util/iot/visa.cpp
Normal file
506
sdrbase/util/iot/visa.cpp
Normal file
@ -0,0 +1,506 @@
|
|||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Copyright (C) 2022 Jon Beniston, M7RCE //
|
||||||
|
// //
|
||||||
|
// This program is free software; you can redistribute it and/or modify //
|
||||||
|
// it under the terms of the GNU General Public License as published by //
|
||||||
|
// the Free Software Foundation as version 3 of the License, or //
|
||||||
|
// (at your option) any later version. //
|
||||||
|
// //
|
||||||
|
// This program is distributed in the hope that it will be useful, //
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||||
|
// GNU General Public License V3 for more details. //
|
||||||
|
// //
|
||||||
|
// You should have received a copy of the GNU General Public License //
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#include "util/iot/visa.h"
|
||||||
|
#include "util/visa.h"
|
||||||
|
#include "util/simpleserializer.h"
|
||||||
|
|
||||||
|
VISADevice::VISADevice(const QHash<QString, QVariant> settings, const QString &deviceId,
|
||||||
|
const QStringList &controls, const QStringList &sensors,
|
||||||
|
DeviceDiscoverer::DeviceInfo *info) :
|
||||||
|
Device(info),
|
||||||
|
m_deviceId(deviceId),
|
||||||
|
m_session(0),
|
||||||
|
m_controls(controls),
|
||||||
|
m_sensors(sensors)
|
||||||
|
{
|
||||||
|
m_visa.openDefault();
|
||||||
|
|
||||||
|
QHashIterator<QString, QVariant> itr(settings);
|
||||||
|
while (itr.hasNext())
|
||||||
|
{
|
||||||
|
itr.next();
|
||||||
|
QString key = itr.key();
|
||||||
|
QVariant value = itr.value();
|
||||||
|
|
||||||
|
if ((key == "deviceId") || (key == "controlIds") || (key == "sensorIds"))
|
||||||
|
{
|
||||||
|
// Nothing to do here
|
||||||
|
}
|
||||||
|
else if (key == "logIO")
|
||||||
|
{
|
||||||
|
m_visa.setDebugIO(value.toBool());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << "VISADevice::VISADevice: Unsupported setting key: " << key << " value: " << value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
VISADevice::~VISADevice()
|
||||||
|
{
|
||||||
|
m_visa.close(m_session);
|
||||||
|
m_visa.closeDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VISADevice::open()
|
||||||
|
{
|
||||||
|
if (!m_session) {
|
||||||
|
m_session = m_visa.open(m_deviceId);
|
||||||
|
}
|
||||||
|
if (!m_session) {
|
||||||
|
emit deviceUnavailable();
|
||||||
|
}
|
||||||
|
return m_session != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VISADevice::convertToBool(const QString &string, bool &ok)
|
||||||
|
{
|
||||||
|
QString lower = string.trimmed().toLower();
|
||||||
|
if ((lower == "0") || (lower == "false") || (lower == "off"))
|
||||||
|
{
|
||||||
|
ok = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if ((lower == "1") || (lower == "true") || (lower == "on"))
|
||||||
|
{
|
||||||
|
ok = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ok = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VISADevice::convert(QHash<QString, QVariant> &status, const QString &id, DeviceDiscoverer::Type type, const QString &state)
|
||||||
|
{
|
||||||
|
if (type == DeviceDiscoverer::BOOL)
|
||||||
|
{
|
||||||
|
bool ok;
|
||||||
|
bool value = convertToBool(state, ok);
|
||||||
|
if (ok) {
|
||||||
|
status.insert(id, value);
|
||||||
|
} else {
|
||||||
|
status.insert(id, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type == DeviceDiscoverer::INT)
|
||||||
|
{
|
||||||
|
bool ok;
|
||||||
|
int value = state.toInt(&ok);
|
||||||
|
if (ok) {
|
||||||
|
status.insert(id, value);
|
||||||
|
} else {
|
||||||
|
status.insert(id, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type == DeviceDiscoverer::FLOAT)
|
||||||
|
{
|
||||||
|
bool ok;
|
||||||
|
float value = state.toFloat(&ok);
|
||||||
|
if (ok) {
|
||||||
|
status.insert(id, value);
|
||||||
|
} else {
|
||||||
|
status.insert(id, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status.insert(id, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VISADevice::getState()
|
||||||
|
{
|
||||||
|
if (open())
|
||||||
|
{
|
||||||
|
QHash<QString, QVariant> status;
|
||||||
|
|
||||||
|
for (auto c : m_info.m_controls)
|
||||||
|
{
|
||||||
|
if (m_controls.contains(c->m_id))
|
||||||
|
{
|
||||||
|
VISAControl *control = reinterpret_cast<VISAControl *>(c);
|
||||||
|
QString cmds = control->m_getState.trimmed();
|
||||||
|
if (!cmds.isEmpty())
|
||||||
|
{
|
||||||
|
bool error;
|
||||||
|
QStringList results = m_visa.processCommands(m_session, cmds, &error);
|
||||||
|
if (!error && (results.size() > 0))
|
||||||
|
{
|
||||||
|
// Take last returned value as the state
|
||||||
|
QString state = results[results.size()-1].trimmed();
|
||||||
|
convert(status, control->m_id, control->m_type, state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status.insert(control->m_id, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto s : m_info.m_sensors)
|
||||||
|
{
|
||||||
|
if (m_sensors.contains(s->m_id))
|
||||||
|
{
|
||||||
|
VISASensor *sensor = reinterpret_cast<VISASensor *>(s);
|
||||||
|
QString cmds = sensor->m_getState.trimmed();
|
||||||
|
if (!cmds.isEmpty())
|
||||||
|
{
|
||||||
|
bool error;
|
||||||
|
QStringList results = m_visa.processCommands(m_session, cmds, &error);
|
||||||
|
if (!error && (results.size() > 0))
|
||||||
|
{
|
||||||
|
// Take last returned value as the state
|
||||||
|
QString state = results[results.size()-1].trimmed();
|
||||||
|
convert(status, sensor->m_id, sensor->m_type, state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status.insert(sensor->m_id, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit deviceUpdated(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VISADevice::setState(const QString &controlId, bool state)
|
||||||
|
{
|
||||||
|
if (open())
|
||||||
|
{
|
||||||
|
for (auto c : m_info.m_controls)
|
||||||
|
{
|
||||||
|
VISAControl *control = reinterpret_cast<VISAControl *>(c);
|
||||||
|
if (control->m_id == controlId)
|
||||||
|
{
|
||||||
|
QString commands = QString::asprintf(control->m_setState.toUtf8(), (int)state);
|
||||||
|
bool error;
|
||||||
|
m_visa.processCommands(m_session, commands, &error);
|
||||||
|
if (error) {
|
||||||
|
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VISADevice::setState(const QString &controlId, int state)
|
||||||
|
{
|
||||||
|
if (open())
|
||||||
|
{
|
||||||
|
for (auto c : m_info.m_controls)
|
||||||
|
{
|
||||||
|
VISAControl *control = reinterpret_cast<VISAControl *>(c);
|
||||||
|
if (control->m_id == controlId)
|
||||||
|
{
|
||||||
|
QString commands = QString::asprintf(control->m_setState.toUtf8(), state);
|
||||||
|
bool error;
|
||||||
|
m_visa.processCommands(m_session, commands, &error);
|
||||||
|
if (error) {
|
||||||
|
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VISADevice::setState(const QString &controlId, float state)
|
||||||
|
{
|
||||||
|
if (open())
|
||||||
|
{
|
||||||
|
for (auto c : m_info.m_controls)
|
||||||
|
{
|
||||||
|
VISAControl *control = reinterpret_cast<VISAControl *>(c);
|
||||||
|
if (control->m_id == controlId)
|
||||||
|
{
|
||||||
|
QString commands = QString::asprintf(control->m_setState.toUtf8(), state);
|
||||||
|
bool error;
|
||||||
|
m_visa.processCommands(m_session, commands, &error);
|
||||||
|
if (error) {
|
||||||
|
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VISADevice::setState(const QString &controlId, const QString &state)
|
||||||
|
{
|
||||||
|
if (open())
|
||||||
|
{
|
||||||
|
for (auto c : m_info.m_controls)
|
||||||
|
{
|
||||||
|
VISAControl *control = reinterpret_cast<VISAControl *>(c);
|
||||||
|
if (control->m_id == controlId)
|
||||||
|
{
|
||||||
|
QString commands = QString::asprintf(control->m_setState.toUtf8(), state);
|
||||||
|
bool error;
|
||||||
|
m_visa.processCommands(m_session, commands, &error);
|
||||||
|
if (error) {
|
||||||
|
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VISADeviceDiscoverer::VISADeviceDiscoverer(const QString& resourceFilter) :
|
||||||
|
m_resourceFilter(resourceFilter)
|
||||||
|
{
|
||||||
|
m_session = m_visa.openDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
VISADeviceDiscoverer::~VISADeviceDiscoverer()
|
||||||
|
{
|
||||||
|
m_visa.closeDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VISADeviceDiscoverer::getDevices()
|
||||||
|
{
|
||||||
|
QRegularExpression *filterP = nullptr;
|
||||||
|
QRegularExpression filter(m_resourceFilter);
|
||||||
|
if (!m_resourceFilter.trimmed().isEmpty()) {
|
||||||
|
filterP = &filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get list of VISA instruments
|
||||||
|
QList<VISA::Instrument> instruments = m_visa.instruments(filterP);
|
||||||
|
|
||||||
|
// Convert to list of devices
|
||||||
|
QList<DeviceInfo> devices;
|
||||||
|
for (auto const &instrument : instruments)
|
||||||
|
{
|
||||||
|
DeviceInfo info;
|
||||||
|
info.m_name = instrument.m_model;
|
||||||
|
info.m_id = instrument.m_resource;
|
||||||
|
info.m_model = instrument.m_model;
|
||||||
|
|
||||||
|
if ((info.m_name == "DP832") || (info.m_name == "DP832A"))
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= 3; i++)
|
||||||
|
{
|
||||||
|
VISADevice::VISAControl *output = new VISADevice::VISAControl();
|
||||||
|
output->m_name = QString("CH%1").arg(i);
|
||||||
|
output->m_id = QString("control.ch%1").arg(i);
|
||||||
|
output->m_type = BOOL;
|
||||||
|
output->m_getState = QString(":OUTPUT? CH%1").arg(i);
|
||||||
|
output->m_setState = QString(":OUTPUT CH%1,%d").arg(i);
|
||||||
|
info.m_controls.append(output);
|
||||||
|
|
||||||
|
VISADevice::VISAControl *setVoltage = new VISADevice::VISAControl();
|
||||||
|
setVoltage->m_name = QString("V%1").arg(i);
|
||||||
|
setVoltage->m_id = QString("control.voltage%1").arg(i);
|
||||||
|
setVoltage->m_type = FLOAT;
|
||||||
|
setVoltage->m_min = 0.0f;
|
||||||
|
setVoltage->m_max = i == 3 ? 5.0f : 30.0f;
|
||||||
|
setVoltage->m_scale = 1.0f;
|
||||||
|
setVoltage->m_precision = 3;
|
||||||
|
setVoltage->m_widgetType = SPIN_BOX;
|
||||||
|
setVoltage->m_units = "V";
|
||||||
|
setVoltage->m_getState = QString(":SOURCE%1:VOLTage?").arg(i);
|
||||||
|
setVoltage->m_setState = QString(":SOURCE%1:VOLTage %f").arg(i);
|
||||||
|
info.m_controls.append(setVoltage);
|
||||||
|
|
||||||
|
VISADevice::VISAControl *setCurrent = new VISADevice::VISAControl();
|
||||||
|
setCurrent->m_name = QString("i%1").arg(i);
|
||||||
|
setCurrent->m_id = QString("control.current%1").arg(i);
|
||||||
|
setCurrent->m_type = FLOAT;
|
||||||
|
setCurrent->m_min = 0.0f;
|
||||||
|
setCurrent->m_max = 3.0f;
|
||||||
|
setCurrent->m_scale = 1.0f;
|
||||||
|
setCurrent->m_precision = 3;
|
||||||
|
setCurrent->m_widgetType = SPIN_BOX;
|
||||||
|
setCurrent->m_units = "A";
|
||||||
|
setCurrent->m_getState = QString(":SOURCE%1:CURRent?").arg(i);
|
||||||
|
setCurrent->m_setState = QString(":SOURCE%1:CURRent %f").arg(i);
|
||||||
|
info.m_controls.append(setCurrent);
|
||||||
|
|
||||||
|
VISADevice::VISASensor *voltage = new VISADevice::VISASensor();
|
||||||
|
voltage->m_name = QString("V%1").arg(i);
|
||||||
|
voltage->m_id = QString("sensor.voltage%1").arg(i);
|
||||||
|
voltage->m_type = FLOAT;
|
||||||
|
voltage->m_units = "V";
|
||||||
|
voltage->m_getState = QString(":MEASure:VOLTage? CH%1").arg(i);
|
||||||
|
info.m_sensors.append(voltage);
|
||||||
|
|
||||||
|
VISADevice::VISASensor *current = new VISADevice::VISASensor();
|
||||||
|
current->m_name = QString("i%1").arg(i);
|
||||||
|
current->m_id = QString("sensor.current%1").arg(i);
|
||||||
|
current->m_type = FLOAT;
|
||||||
|
current->m_units = "A";
|
||||||
|
current->m_getState = QString(":MEASure:CURRent? CH%1").arg(i);
|
||||||
|
info.m_sensors.append(current);
|
||||||
|
|
||||||
|
VISADevice::VISASensor *power = new VISADevice::VISASensor();
|
||||||
|
power->m_name = QString("P%1").arg(i);
|
||||||
|
power->m_id = QString("sensor.power%1").arg(i);
|
||||||
|
power->m_type = FLOAT;
|
||||||
|
power->m_units = "W";
|
||||||
|
power->m_getState = QString(":MEASure:POWEr? CH%1").arg(i);
|
||||||
|
info.m_sensors.append(power);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (info.m_name == "SSA3032X")
|
||||||
|
{
|
||||||
|
VISADevice::VISAControl *frequency = new VISADevice::VISAControl();
|
||||||
|
frequency->m_name = "Frequency";
|
||||||
|
frequency->m_id = "control.frequency";
|
||||||
|
frequency->m_type = FLOAT;
|
||||||
|
frequency->m_min = 0.0f;
|
||||||
|
frequency->m_max = 3.2e3f;
|
||||||
|
frequency->m_scale = 1e6f;
|
||||||
|
frequency->m_precision = 6;
|
||||||
|
frequency->m_widgetType = SPIN_BOX;
|
||||||
|
frequency->m_units = "MHz";
|
||||||
|
frequency->m_getState = ":FREQuency:CENTer?";
|
||||||
|
frequency->m_setState = ":FREQuency:CENTer %f";
|
||||||
|
info.m_controls.append(frequency);
|
||||||
|
|
||||||
|
VISADevice::VISAControl *span = new VISADevice::VISAControl();
|
||||||
|
span->m_name = "Span";
|
||||||
|
span->m_id = "control.span";
|
||||||
|
span->m_type = FLOAT;
|
||||||
|
span->m_min = 0.0f;
|
||||||
|
span->m_max = 3.2e3f;
|
||||||
|
span->m_scale = 1e6;
|
||||||
|
span->m_precision = 3;
|
||||||
|
span->m_widgetType = SPIN_BOX;
|
||||||
|
span->m_units = "MHz";
|
||||||
|
span->m_getState = ":FREQuency:SPAN?";
|
||||||
|
span->m_setState = ":FREQuency:SPAN %f";
|
||||||
|
info.m_controls.append(span);
|
||||||
|
|
||||||
|
VISADevice::VISAControl *markerX = new VISADevice::VISAControl();
|
||||||
|
markerX->m_name = "Marker X";
|
||||||
|
markerX->m_id = "control.markerx";
|
||||||
|
markerX->m_type = FLOAT;
|
||||||
|
markerX->m_min = 0.0f;
|
||||||
|
markerX->m_max = 3.2e3f;
|
||||||
|
markerX->m_scale = 1e6;
|
||||||
|
markerX->m_precision = 6;
|
||||||
|
markerX->m_widgetType = SPIN_BOX;
|
||||||
|
markerX->m_units = "MHz";
|
||||||
|
markerX->m_getState = ":CALCulate:MARKer1:X?";
|
||||||
|
markerX->m_setState = ":CALCulate:MARKer1:X %f";
|
||||||
|
info.m_controls.append(markerX);
|
||||||
|
|
||||||
|
VISADevice::VISASensor *markerY = new VISADevice::VISASensor();
|
||||||
|
markerY->m_name = "Marker Y";
|
||||||
|
markerY->m_id = "sensor.markery";
|
||||||
|
markerY->m_type = FLOAT;
|
||||||
|
markerY->m_units = "dBm";
|
||||||
|
markerY->m_getState = ":CALCulate:MARKer1:Y?";
|
||||||
|
info.m_sensors.append(markerY);
|
||||||
|
}
|
||||||
|
|
||||||
|
devices.append(info);
|
||||||
|
}
|
||||||
|
emit deviceList(devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::ControlInfo *VISADevice::VISAControl::clone() const
|
||||||
|
{
|
||||||
|
return new VISAControl(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray VISADevice::VISAControl::serialize() const
|
||||||
|
{
|
||||||
|
SimpleSerializer s(1);
|
||||||
|
|
||||||
|
s.writeBlob(1, ControlInfo::serialize());
|
||||||
|
s.writeString(2, m_getState);
|
||||||
|
s.writeString(3, m_setState);
|
||||||
|
|
||||||
|
return s.final();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VISADevice::VISAControl::deserialize(const QByteArray& data)
|
||||||
|
{
|
||||||
|
SimpleDeserializer d(data);
|
||||||
|
|
||||||
|
if (!d.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.getVersion() == 1)
|
||||||
|
{
|
||||||
|
QByteArray blob;
|
||||||
|
|
||||||
|
d.readBlob(1, &blob);
|
||||||
|
ControlInfo::deserialize(blob);
|
||||||
|
d.readString(2, &m_getState);
|
||||||
|
d.readString(3, &m_setState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceDiscoverer::SensorInfo *VISADevice::VISASensor::clone() const
|
||||||
|
{
|
||||||
|
return new VISASensor(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray VISADevice::VISASensor::serialize() const
|
||||||
|
{
|
||||||
|
SimpleSerializer s(1);
|
||||||
|
|
||||||
|
s.writeBlob(1, SensorInfo::serialize());
|
||||||
|
s.writeString(2, m_getState);
|
||||||
|
return s.final();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VISADevice::VISASensor::deserialize(const QByteArray& data)
|
||||||
|
{
|
||||||
|
SimpleDeserializer d(data);
|
||||||
|
|
||||||
|
if (!d.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.getVersion() == 1)
|
||||||
|
{
|
||||||
|
QByteArray blob;
|
||||||
|
|
||||||
|
d.readBlob(1, &blob);
|
||||||
|
SensorInfo::deserialize(blob);
|
||||||
|
d.readString(2, &m_getState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
84
sdrbase/util/iot/visa.h
Normal file
84
sdrbase/util/iot/visa.h
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Copyright (C) 2022 Jon Beniston, M7RCE //
|
||||||
|
// //
|
||||||
|
// This program is free software; you can redistribute it and/or modify //
|
||||||
|
// it under the terms of the GNU General Public License as published by //
|
||||||
|
// the Free Software Foundation as version 3 of the License, or //
|
||||||
|
// (at your option) any later version. //
|
||||||
|
// //
|
||||||
|
// This program is distributed in the hope that it will be useful, //
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
|
||||||
|
// GNU General Public License V3 for more details. //
|
||||||
|
// //
|
||||||
|
// You should have received a copy of the GNU General Public License //
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#ifndef INCLUDE_IOT_VISA_H
|
||||||
|
#define INCLUDE_IOT_VISA_H
|
||||||
|
|
||||||
|
#include "util/iot/device.h"
|
||||||
|
#include "util/visa.h"
|
||||||
|
|
||||||
|
class SDRBASE_API VISADevice : public Device {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
|
||||||
|
struct SDRBASE_API VISAControl : public DeviceDiscoverer::ControlInfo {
|
||||||
|
QString m_getState;
|
||||||
|
QString m_setState;
|
||||||
|
|
||||||
|
virtual DeviceDiscoverer::ControlInfo *clone() const override;
|
||||||
|
virtual QByteArray serialize() const override;
|
||||||
|
virtual bool deserialize(const QByteArray& data) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SDRBASE_API VISASensor : public DeviceDiscoverer::SensorInfo {
|
||||||
|
QString m_getState;
|
||||||
|
|
||||||
|
virtual DeviceDiscoverer::SensorInfo *clone() const override;
|
||||||
|
virtual QByteArray serialize() const override;
|
||||||
|
virtual bool deserialize(const QByteArray& data) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
VISADevice(const QHash<QString, QVariant> settings, const QString &deviceId,
|
||||||
|
const QStringList &controls, const QStringList &sensors,
|
||||||
|
DeviceDiscoverer::DeviceInfo *info=nullptr);
|
||||||
|
~VISADevice();
|
||||||
|
virtual void getState() override;
|
||||||
|
virtual void setState(const QString &controlId, bool state) override;
|
||||||
|
virtual void setState(const QString &controlId, int state) override;
|
||||||
|
virtual void setState(const QString &controlId, float state) override;
|
||||||
|
virtual void setState(const QString &controlId, const QString &state) override;
|
||||||
|
virtual QString getProtocol() const override { return "VISA"; }
|
||||||
|
virtual QString getDeviceId() const override { return m_deviceId; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_deviceId; // VISA resource (E.g. USB0::0xcafe...)
|
||||||
|
VISA m_visa;
|
||||||
|
ViSession m_session;
|
||||||
|
QStringList m_controls; // Control IDs for getState
|
||||||
|
QStringList m_sensors; // Sensor IDs for getState
|
||||||
|
bool m_debugIO;
|
||||||
|
|
||||||
|
bool open();
|
||||||
|
bool convertToBool(const QString &string, bool &ok);
|
||||||
|
void convert(QHash<QString, QVariant> &status, const QString &id, DeviceDiscoverer::Type type, const QString &state);
|
||||||
|
};
|
||||||
|
|
||||||
|
class SDRBASE_API VISADeviceDiscoverer : public DeviceDiscoverer {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
|
||||||
|
VISADeviceDiscoverer(const QString &resourceFilter = "");
|
||||||
|
~VISADeviceDiscoverer();
|
||||||
|
virtual void getDevices() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
VISA m_visa;
|
||||||
|
ViSession m_session;
|
||||||
|
QString m_resourceFilter;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* INCLUDE_IOT_VISA_H */
|
Loading…
Reference in New Issue
Block a user