New Plugin: Meross
parent
750afb77f9
commit
aa980b2924
|
|
@ -471,6 +471,14 @@ Description: nymea.io plugin for mec electronics devices
|
||||||
This package will add support for the mec meter to nymea.
|
This package will add support for the mec meter to nymea.
|
||||||
|
|
||||||
|
|
||||||
|
Package: nymea-plugin-meross
|
||||||
|
Architecture: any
|
||||||
|
Depends: ${shlibs:Depends},
|
||||||
|
${misc:Depends},
|
||||||
|
Description: nymea integration plugin for meross devices
|
||||||
|
This package will add support for the meross devices to nymea.
|
||||||
|
|
||||||
|
|
||||||
Package: nymea-plugin-mailnotification
|
Package: nymea-plugin-mailnotification
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Depends: ${shlibs:Depends},
|
Depends: ${shlibs:Depends},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginmeross.so
|
||||||
|
meross/translations/*.qm usr/share/nymea/translations/
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Meross
|
||||||
|
|
||||||
|
This integration plugin allows nymea to control meross power sockets with energy metering.
|
||||||
|
|
||||||
|
## Supported devices
|
||||||
|
|
||||||
|
Currently, only the meross smart plug with energy metering MSS310 is supported.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
The meross smart plug needs to be set up with the meross app and connected to the same network
|
||||||
|
as the nymea system. The device can be discovered by nymea in the local network and will communicate
|
||||||
|
via the local REST API of the device. However, given that the devices require a signing key to respond
|
||||||
|
to API calls, the plugin will require the user to log into meross cloud during setup and will
|
||||||
|
obtain the key from the cloud API. No other calls will be made to the meross cloud and the
|
||||||
|
user credentials will not be stored (thus need to be re-entered when setting up multiple devices).
|
||||||
|
|
||||||
|
The device can be used in nymea along with the meross app, or, if desired, also disconnected from
|
||||||
|
the meross cloud (e.g. by blocking internet access via a firewall) without impairing functionality
|
||||||
|
within nymea.
|
||||||
|
|
@ -0,0 +1,374 @@
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*
|
||||||
|
* Copyright 2013 - 2022, nymea GmbH
|
||||||
|
* Contact: contact@nymea.io
|
||||||
|
*
|
||||||
|
* This file is part of nymea.
|
||||||
|
* This project including source code and documentation is protected by
|
||||||
|
* copyright law, and remains the property of nymea GmbH. All rights, including
|
||||||
|
* reproduction, publication, editing and translation, are reserved. The use of
|
||||||
|
* this project is subject to the terms of a license agreement to be concluded
|
||||||
|
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
|
||||||
|
* under https://nymea.io/license
|
||||||
|
*
|
||||||
|
* GNU Lesser General Public License Usage
|
||||||
|
* Alternatively, this project may be redistributed and/or modified under the
|
||||||
|
* terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
* Software Foundation; version 3. This project 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with this project. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* For any further details and any questions please contact us under
|
||||||
|
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||||
|
* https://nymea.io/license/faq
|
||||||
|
*
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
|
||||||
|
#include "integrationpluginmeross.h"
|
||||||
|
#include "plugininfo.h"
|
||||||
|
|
||||||
|
#include <plugintimer.h>
|
||||||
|
#include <network/networkdevicediscovery.h>
|
||||||
|
#include <network/networkaccessmanager.h>
|
||||||
|
#include <network/networkdevicediscoveryreply.h>
|
||||||
|
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QAuthenticator>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QMetaEnum>
|
||||||
|
|
||||||
|
IntegrationPluginMeross::IntegrationPluginMeross()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
IntegrationPluginMeross::~IntegrationPluginMeross()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntegrationPluginMeross::discoverThings(ThingDiscoveryInfo *info)
|
||||||
|
{
|
||||||
|
NetworkDeviceDiscoveryReply *reply = hardwareManager()->networkDeviceDiscovery()->discover();
|
||||||
|
connect(reply, &NetworkDeviceDiscoveryReply::finished, info, [info, reply, this](){
|
||||||
|
foreach (const NetworkDeviceInfo &deviceInfo, reply->networkDeviceInfos()) {
|
||||||
|
qCDebug(dcMeross) << "Discovery result" << deviceInfo;
|
||||||
|
if (deviceInfo.hostName().toLower().startsWith("meross_smart_plug")) {
|
||||||
|
|
||||||
|
ThingDescriptor descriptor(plugThingClassId, "Meross Smart Plug", deviceInfo.macAddress());
|
||||||
|
descriptor.setParams({Param(plugThingMacAddressParamTypeId, deviceInfo.macAddress())});
|
||||||
|
|
||||||
|
Thing *existingThing = myThings().findByParams(descriptor.params());
|
||||||
|
if (existingThing) {
|
||||||
|
qCInfo(dcMeross) << "Existing smart plug discovered";
|
||||||
|
descriptor.setThingId(existingThing->id());
|
||||||
|
} else {
|
||||||
|
qCInfo(dcMeross) << "New smart plug discovered";
|
||||||
|
}
|
||||||
|
|
||||||
|
info->addThingDescriptor(descriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qCInfo(dcMeross) << "Discovery finished." << info->thingDescriptors().count() << "results.";
|
||||||
|
info->finish(Thing::ThingErrorNoError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntegrationPluginMeross::startPairing(ThingPairingInfo *info)
|
||||||
|
{
|
||||||
|
info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter your Meross login credentials."));
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntegrationPluginMeross::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret)
|
||||||
|
{
|
||||||
|
QNetworkRequest request(QUrl("https://iot.meross.com/v1/Auth/login"));
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
QVariantMap params;
|
||||||
|
params.insert("email", username);
|
||||||
|
params.insert("password", secret);
|
||||||
|
QByteArray encodedParams = QJsonDocument::fromVariant(params).toJson(QJsonDocument::Compact).toBase64();
|
||||||
|
|
||||||
|
QByteArray nonce = QUuid::createUuid().toString().remove(QRegExp("[{}-]")).left(16).toUtf8();
|
||||||
|
QByteArray initKey = "23x17ahWarFH6w29";
|
||||||
|
QByteArray timestamp = QByteArray::number(QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000);
|
||||||
|
QByteArray signature = initKey + timestamp + nonce + encodedParams;
|
||||||
|
|
||||||
|
signature = QCryptographicHash::hash(signature, QCryptographicHash::Md5).toHex();
|
||||||
|
|
||||||
|
QUrlQuery query;
|
||||||
|
query.addQueryItem("params", encodedParams);
|
||||||
|
query.addQueryItem("sign", signature);
|
||||||
|
query.addQueryItem("timestamp", timestamp);
|
||||||
|
query.addQueryItem("nonce", nonce);
|
||||||
|
|
||||||
|
qCDebug(dcMeross) << "Requesting" << request.url() << query.toString();
|
||||||
|
|
||||||
|
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, query.toString().toUtf8());
|
||||||
|
connect(reply, &QNetworkReply::finished, info, [=](){
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
qCWarning(dcMeross()) << "Error retrieving device key from cloud:" << reply->error() << reply->errorString();
|
||||||
|
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Failed to retrieve the device key from the Meross cloud."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray payload = reply->readAll();
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(payload, &error);
|
||||||
|
if (error.error != QJsonParseError::NoError) {
|
||||||
|
qCWarning(dcMeross) << "Failed to parse JSON from Meross cloud:" << error.error << error.errorString() << payload;
|
||||||
|
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Failed to retrieve the device key from the Meross cloud."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap params = jsonDoc.toVariant().toMap();
|
||||||
|
|
||||||
|
if (params.value("apiStatus").toInt() != 0 || params.value("sysStatus").toInt() != 0) {
|
||||||
|
qCWarning(dcMeross()) << "Error retrieving device key from cloud:" << reply->error() << reply->errorString();
|
||||||
|
info->finish(Thing::ThingErrorAuthenticationFailure, params.value("info").toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap data = params.value("data").toMap();
|
||||||
|
|
||||||
|
qCDebug(dcMeross) << "key data:" << qUtf8Printable(jsonDoc.toJson());
|
||||||
|
pluginStorage()->beginGroup(info->thingId().toString());
|
||||||
|
pluginStorage()->setValue("key", data.value("key").toString());
|
||||||
|
pluginStorage()->endGroup();
|
||||||
|
|
||||||
|
info->finish(Thing::ThingErrorNoError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntegrationPluginMeross::setupThing(ThingSetupInfo *info)
|
||||||
|
{
|
||||||
|
Thing *thing = info->thing();
|
||||||
|
|
||||||
|
pluginStorage()->beginGroup(thing->id().toString());
|
||||||
|
m_keys.insert(thing, pluginStorage()->value("key").toByteArray());
|
||||||
|
pluginStorage()->endGroup();
|
||||||
|
|
||||||
|
NetworkDeviceMonitor *monitor = m_deviceMonitors.take(thing);
|
||||||
|
if (monitor) {
|
||||||
|
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor);
|
||||||
|
}
|
||||||
|
PluginTimer *timer = m_timers.take(thing);
|
||||||
|
if (timer) {
|
||||||
|
hardwareManager()->pluginTimerManager()->unregisterTimer(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(MacAddress(thing->paramValue(plugThingMacAddressParamTypeId).toString()));
|
||||||
|
m_deviceMonitors.insert(thing, monitor);
|
||||||
|
|
||||||
|
timer = hardwareManager()->pluginTimerManager()->registerTimer(5);
|
||||||
|
m_timers.insert(thing, timer);
|
||||||
|
|
||||||
|
connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [timer, thing](bool reachable) {
|
||||||
|
thing->setStateValue("connected", reachable);
|
||||||
|
if (reachable) {
|
||||||
|
timer->start();
|
||||||
|
} else {
|
||||||
|
timer->stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(timer, &PluginTimer::currentTickChanged, this, [this, thing](qlonglong tick){
|
||||||
|
if (tick % 5 == 0) {
|
||||||
|
pollDevice5s(thing);
|
||||||
|
} else if (tick == 0) {
|
||||||
|
pollDevice60s(thing);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pollDevice5s(thing);
|
||||||
|
pollDevice60s(thing);
|
||||||
|
|
||||||
|
info->finish(Thing::ThingErrorNoError);
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntegrationPluginMeross::thingRemoved(Thing *thing)
|
||||||
|
{
|
||||||
|
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_deviceMonitors.take(thing));
|
||||||
|
hardwareManager()->pluginTimerManager()->unregisterTimer(m_timers.take(thing));
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntegrationPluginMeross::executeAction(ThingActionInfo *info)
|
||||||
|
{
|
||||||
|
if (info->action().actionTypeId() == plugPowerActionTypeId) {
|
||||||
|
QVariantMap payload;
|
||||||
|
QVariantMap togglex;
|
||||||
|
togglex.insert("channel", 0);
|
||||||
|
togglex.insert("onoff", info->action().paramValue(plugPowerActionPowerParamTypeId).toBool() ? 1 : 0);
|
||||||
|
payload.insert("togglex", togglex);
|
||||||
|
QNetworkReply *reply = request(info->thing(), "Appliance.Control.ToggleX", SET, payload);
|
||||||
|
connect(reply, &QNetworkReply::finished, info, [=](){
|
||||||
|
qCDebug(dcMeross) << "reply" << reply->error() << reply->errorString() << reply->readAll();
|
||||||
|
info->finish(Thing::ThingErrorNoError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntegrationPluginMeross::pollDevice5s(Thing *thing)
|
||||||
|
{
|
||||||
|
QNetworkReply *systemReply = request(thing, "Appliance.System.All");
|
||||||
|
connect(systemReply, &QNetworkReply::finished, thing, [=](){
|
||||||
|
if (systemReply->error() != QNetworkReply::NoError) {
|
||||||
|
qCWarning(dcMeross) << "Error polling" << thing->name() << ":" << systemReply->error() << systemReply->errorString();
|
||||||
|
thing->setStateValue(plugConnectedStateTypeId, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QByteArray data = systemReply->readAll();
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||||
|
if (error.error != QJsonParseError::NoError) {
|
||||||
|
qCWarning(dcMeross) << "Error parsing JSON reply from" << thing->name() << ":" << error.error << error.errorString();
|
||||||
|
thing->setStateValue(plugConnectedStateTypeId, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(dcMeross) << "System:" << qUtf8Printable(jsonDoc.toJson());
|
||||||
|
|
||||||
|
QVariantMap payload = jsonDoc.toVariant().toMap().value("payload").toMap().value("all").toMap();
|
||||||
|
|
||||||
|
QVariantMap digest = payload.value("digest").toMap();
|
||||||
|
if (digest.value("togglex").toList().count() != 1) {
|
||||||
|
qCWarning(dcMeross) << "Unexpected reply payload. Expected 1 togglex entry, got:" << qUtf8Printable(QJsonDocument::fromVariant(digest.value("togglex")).toJson());
|
||||||
|
thing->setStateValue(plugConnectedStateTypeId, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thing->setStateValue(plugConnectedStateTypeId, true);
|
||||||
|
|
||||||
|
thing->setStateValue(plugPowerStateTypeId, digest.value("togglex").toList().at(0).toMap().value("onoff").toInt() == 1);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
QNetworkReply *electricityReply = request(thing, "Appliance.Control.Electricity");
|
||||||
|
connect(electricityReply, &QNetworkReply::finished, thing, [electricityReply, thing](){
|
||||||
|
if (electricityReply->error() != QNetworkReply::NoError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(electricityReply->readAll(), &error);
|
||||||
|
if (error.error != QJsonParseError::NoError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qCDebug(dcMeross()) << "Electricity:" << qUtf8Printable(jsonDoc.toJson());
|
||||||
|
|
||||||
|
QVariantMap electricityMap = jsonDoc.toVariant().toMap().value("payload").toMap().value("electricity").toMap();
|
||||||
|
double power = electricityMap.value("power").toDouble() / 1000;
|
||||||
|
thing->setStateValue(plugCurrentPowerStateTypeId, power);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void IntegrationPluginMeross::pollDevice60s(Thing *thing)
|
||||||
|
{
|
||||||
|
// Signal strength
|
||||||
|
QNetworkReply *runtimeReply = request(thing, "Appliance.System.Runtime");
|
||||||
|
connect(runtimeReply, &QNetworkReply::finished, thing, [runtimeReply, thing](){
|
||||||
|
if (runtimeReply->error() != QNetworkReply::NoError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(runtimeReply->readAll(), &error);
|
||||||
|
if (error.error != QJsonParseError::NoError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qCDebug(dcMeross()) << "Runtime:" << qUtf8Printable(jsonDoc.toJson());
|
||||||
|
|
||||||
|
thing->setStateValue(plugSignalStrengthStateTypeId, jsonDoc.toVariant().toMap().value("payload").toMap().value("runtime").toMap().value("signal").toInt());
|
||||||
|
});
|
||||||
|
|
||||||
|
QNetworkReply *consumptionReply = request(thing, "Appliance.Control.ConsumptionX");
|
||||||
|
connect(consumptionReply, &QNetworkReply::finished, thing, [consumptionReply, thing, this](){
|
||||||
|
if (consumptionReply->error() != QNetworkReply::NoError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(consumptionReply->readAll(), &error);
|
||||||
|
if (error.error != QJsonParseError::NoError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
qCDebug(dcMeross()) << "Consumption:" << qUtf8Printable(jsonDoc.toJson());
|
||||||
|
|
||||||
|
// We get a list of (max 10 or so) daily consumption totals but we're only interested in the grand total
|
||||||
|
// So we're keeping a copy of the list and and add up changes in that list to the total
|
||||||
|
double total = thing->stateValue(plugTotalEnergyConsumedStateTypeId).toDouble();
|
||||||
|
|
||||||
|
QStringList timestamps;
|
||||||
|
|
||||||
|
pluginStorage()->beginGroup(thing->id().toString());
|
||||||
|
pluginStorage()->beginGroup("consumptionLogs");
|
||||||
|
foreach (const QVariant &entry, jsonDoc.toVariant().toMap().value("payload").toMap().value("consumptionx").toList()) {
|
||||||
|
QVariantMap entryMap = entry.toMap();
|
||||||
|
QString timestamp = entryMap.value("date").toString();
|
||||||
|
int value = entryMap.value("value").toInt();
|
||||||
|
int loggedValue = pluginStorage()->value(timestamp).toInt();
|
||||||
|
|
||||||
|
// qCDebug(dcMeross) << "entry:" << timestamp << "value" << value << loggedValue;
|
||||||
|
|
||||||
|
if (loggedValue != value) {
|
||||||
|
total -= 1.0 * loggedValue / 1000;
|
||||||
|
total += 1.0 * value / 1000;
|
||||||
|
pluginStorage()->setValue(timestamp, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamps.append(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up old timestamps from pluginstorage
|
||||||
|
foreach (const QString &childKey, pluginStorage()->childKeys()) {
|
||||||
|
if (!timestamps.contains(childKey)) {
|
||||||
|
pluginStorage()->remove(childKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pluginStorage()->endGroup();
|
||||||
|
pluginStorage()->endGroup();
|
||||||
|
|
||||||
|
thing->setStateValue(plugTotalEnergyConsumedStateTypeId, total);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply* IntegrationPluginMeross::request(Thing *thing, const QString &nameSpace, Method method, const QVariantMap &payload)
|
||||||
|
{
|
||||||
|
QByteArray key = m_keys.value(thing);
|
||||||
|
|
||||||
|
QString messageId = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
|
||||||
|
qulonglong timestamp = QDateTime::currentDateTime().toMSecsSinceEpoch();
|
||||||
|
quint16 timestampMs = timestamp % 1000;
|
||||||
|
timestamp = timestamp / 1000;
|
||||||
|
QByteArray signature = QCryptographicHash::hash(QString(messageId + key + QString::number(timestamp)).toUtf8(), QCryptographicHash::Md5).toHex();
|
||||||
|
|
||||||
|
QVariantMap header;
|
||||||
|
header.insert("from", "Meross");
|
||||||
|
header.insert("messageId", messageId);
|
||||||
|
header.insert("method", QMetaEnum::fromType<IntegrationPluginMeross::Method>().valueToKey(method));
|
||||||
|
header.insert("namespace", nameSpace);
|
||||||
|
header.insert("payloadVersion", 1);
|
||||||
|
header.insert("timestamp", timestamp);
|
||||||
|
header.insert("timestampMs", timestampMs);
|
||||||
|
header.insert("sign", signature);
|
||||||
|
|
||||||
|
QVariantMap data;
|
||||||
|
data.insert("header", header);
|
||||||
|
data.insert("payload", payload);
|
||||||
|
|
||||||
|
QUrl url;
|
||||||
|
url.setScheme("http");
|
||||||
|
url.setHost(m_deviceMonitors.value(thing)->networkDeviceInfo().address().toString());
|
||||||
|
url.setPath("/config");
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
|
qCDebug(dcMeross) << "Requesting with key" << key << qUtf8Printable(QJsonDocument::fromVariant(data).toJson());
|
||||||
|
|
||||||
|
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(data).toJson(QJsonDocument::Compact));
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*
|
||||||
|
* Copyright 2013 - 2022, nymea GmbH
|
||||||
|
* Contact: contact@nymea.io
|
||||||
|
*
|
||||||
|
* This file is part of nymea.
|
||||||
|
* This project including source code and documentation is protected by
|
||||||
|
* copyright law, and remains the property of nymea GmbH. All rights, including
|
||||||
|
* reproduction, publication, editing and translation, are reserved. The use of
|
||||||
|
* this project is subject to the terms of a license agreement to be concluded
|
||||||
|
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
|
||||||
|
* under https://nymea.io/license
|
||||||
|
*
|
||||||
|
* GNU Lesser General Public License Usage
|
||||||
|
* Alternatively, this project may be redistributed and/or modified under the
|
||||||
|
* terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
* Software Foundation; version 3. This project 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
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with this project. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* For any further details and any questions please contact us under
|
||||||
|
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||||
|
* https://nymea.io/license/faq
|
||||||
|
*
|
||||||
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||||
|
|
||||||
|
#ifndef INTEGRATIONPLUGINMEROSS_H
|
||||||
|
#define INTEGRATIONPLUGINMEROSS_H
|
||||||
|
|
||||||
|
#include "integrations/integrationplugin.h"
|
||||||
|
#include "extern-plugininfo.h"
|
||||||
|
|
||||||
|
class PluginTimer;
|
||||||
|
class NetworkDeviceMonitor;
|
||||||
|
class QNetworkReply;
|
||||||
|
|
||||||
|
class IntegrationPluginMeross: public IntegrationPlugin
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginmeross.json")
|
||||||
|
Q_INTERFACES(IntegrationPlugin)
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum Method {
|
||||||
|
GET,
|
||||||
|
SET
|
||||||
|
};
|
||||||
|
Q_ENUM(Method)
|
||||||
|
|
||||||
|
explicit IntegrationPluginMeross();
|
||||||
|
~IntegrationPluginMeross();
|
||||||
|
|
||||||
|
void discoverThings(ThingDiscoveryInfo *info) override;
|
||||||
|
void startPairing(ThingPairingInfo *info) override;
|
||||||
|
void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override;
|
||||||
|
|
||||||
|
void setupThing(ThingSetupInfo *info) override;
|
||||||
|
void thingRemoved(Thing *thing) override;
|
||||||
|
|
||||||
|
void executeAction(ThingActionInfo *info) override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void pollDevice5s(Thing *thing);
|
||||||
|
void pollDevice60s(Thing *thing);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void retrieveKey();
|
||||||
|
|
||||||
|
QNetworkReply *request(Thing *thing, const QString &nameSpace, Method method = GET, const QVariantMap &payload = QVariantMap());
|
||||||
|
|
||||||
|
QHash<Thing*, QByteArray> m_keys;
|
||||||
|
QHash<Thing*, NetworkDeviceMonitor*> m_deviceMonitors;
|
||||||
|
QHash<Thing*, PluginTimer*> m_timers;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // INTEGRATIONPLUGINMEROSS_H
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
{
|
||||||
|
"name": "meross",
|
||||||
|
"displayName": "ANEL-Elektronik AG",
|
||||||
|
"id": "4b11bdcd-f45b-4b88-9383-267381713227",
|
||||||
|
"vendors": [
|
||||||
|
{
|
||||||
|
"name": "meross",
|
||||||
|
"displayName": "meross",
|
||||||
|
"id": "878df3e1-5f73-4d4f-8e65-bb050fda935f",
|
||||||
|
"thingClasses": [
|
||||||
|
{
|
||||||
|
"id": "162ef487-e05b-4c92-b4d0-cc5a7ed77134",
|
||||||
|
"name": "plug",
|
||||||
|
"displayName": "Smart plug",
|
||||||
|
"createMethods": ["discovery"],
|
||||||
|
"setupMethod": "userandpassword",
|
||||||
|
"interfaces": [ "powersocket", "smartmeterconsumer", "wirelessconnectable" ],
|
||||||
|
"paramTypes": [
|
||||||
|
{
|
||||||
|
"id": "1e273e10-3ea0-4337-a221-3b8e26c6e7dc",
|
||||||
|
"name":"macAddress",
|
||||||
|
"displayName": "MAC address",
|
||||||
|
"type": "QString"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateTypes": [
|
||||||
|
{
|
||||||
|
"id": "9cde6321-2abf-4a58-a1d6-c7418edb9747",
|
||||||
|
"name": "connected",
|
||||||
|
"displayName": "Connected",
|
||||||
|
"displayNameEvent": "Connected changed",
|
||||||
|
"type": "bool",
|
||||||
|
"defaultValue": false,
|
||||||
|
"cached": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ec75ed0a-18e5-401b-a011-52dcb2b26c34",
|
||||||
|
"name": "signalStrength",
|
||||||
|
"displayName": "Signal strength",
|
||||||
|
"displayNameEvent": "Signal strength changed",
|
||||||
|
"type": "uint",
|
||||||
|
"unit": "Percentage",
|
||||||
|
"minValue": 0,
|
||||||
|
"maxValue": 100,
|
||||||
|
"defaultValue": 0,
|
||||||
|
"filter": "adaptive",
|
||||||
|
"cached": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0ff2166b-4620-4f7a-ba56-066eead97bc3",
|
||||||
|
"name": "power",
|
||||||
|
"displayName": "Power",
|
||||||
|
"displayNameEvent": "Power",
|
||||||
|
"displayNameAction": "Set power",
|
||||||
|
"type": "bool",
|
||||||
|
"defaultValue": false,
|
||||||
|
"writable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "79a72494-afe8-46a5-8185-abbb81a6ca48",
|
||||||
|
"name": "currentPower",
|
||||||
|
"displayName": "Power consumption",
|
||||||
|
"displayNameEvent": "Power consumption changed",
|
||||||
|
"type": "double",
|
||||||
|
"unit": "Watt",
|
||||||
|
"defaultValue": 0,
|
||||||
|
"cached": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6b187a9e-f9ed-4952-a078-26380962f42b",
|
||||||
|
"name": "totalEnergyConsumed",
|
||||||
|
"displayName": "Total consumed energy",
|
||||||
|
"displayNameEvent": "Total consumed energy changed",
|
||||||
|
"type": "double",
|
||||||
|
"unit": "KiloWattHour",
|
||||||
|
"defaultValue": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
include(../plugins.pri)
|
||||||
|
|
||||||
|
QT += network
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
integrationpluginmeross.cpp \
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
integrationpluginmeross.h \
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"title": "Meross",
|
||||||
|
"tagline": "Integrates meross devices with nymea.",
|
||||||
|
"icon": "meross-icon.jpg",
|
||||||
|
"stability": "community",
|
||||||
|
"offline": true,
|
||||||
|
"technologies": [
|
||||||
|
"network"
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
"socket",
|
||||||
|
"energy"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE TS>
|
||||||
|
<TS version="2.1">
|
||||||
|
<context>
|
||||||
|
<name>IntegrationPluginMeross</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../integrationpluginmeross.cpp" line="85"/>
|
||||||
|
<source>Please enter your Meross login credentials.</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../integrationpluginmeross.cpp" line="117"/>
|
||||||
|
<location filename="../integrationpluginmeross.cpp" line="126"/>
|
||||||
|
<source>Failed to retrieve the device key from the Meross cloud.</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>meross</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../../../build/nymea-plugins-Desktop-Debug/meross/plugininfo.h" line="36"/>
|
||||||
|
<source>ANEL-Elektronik AG</source>
|
||||||
|
<extracomment>The name of the plugin meross ({4b11bdcd-f45b-4b88-9383-267381713227})</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../../../build/nymea-plugins-Desktop-Debug/meross/plugininfo.h" line="39"/>
|
||||||
|
<source>Connected</source>
|
||||||
|
<extracomment>The name of the StateType ({9cde6321-2abf-4a58-a1d6-c7418edb9747}) of ThingClass plug</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../../../build/nymea-plugins-Desktop-Debug/meross/plugininfo.h" line="42"/>
|
||||||
|
<source>MAC address</source>
|
||||||
|
<extracomment>The name of the ParamType (ThingClass: plug, Type: thing, ID: {1e273e10-3ea0-4337-a221-3b8e26c6e7dc})</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../../../build/nymea-plugins-Desktop-Debug/meross/plugininfo.h" line="45"/>
|
||||||
|
<location filename="../../../build/nymea-plugins-Desktop-Debug/meross/plugininfo.h" line="48"/>
|
||||||
|
<source>Power</source>
|
||||||
|
<extracomment>The name of the ParamType (ThingClass: plug, ActionType: power, ID: {0ff2166b-4620-4f7a-ba56-066eead97bc3})
|
||||||
|
----------
|
||||||
|
The name of the StateType ({0ff2166b-4620-4f7a-ba56-066eead97bc3}) of ThingClass plug</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../../../build/nymea-plugins-Desktop-Debug/meross/plugininfo.h" line="51"/>
|
||||||
|
<source>Power consumption</source>
|
||||||
|
<extracomment>The name of the StateType ({79a72494-afe8-46a5-8185-abbb81a6ca48}) of ThingClass plug</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../../../build/nymea-plugins-Desktop-Debug/meross/plugininfo.h" line="54"/>
|
||||||
|
<source>Set power</source>
|
||||||
|
<extracomment>The name of the ActionType ({0ff2166b-4620-4f7a-ba56-066eead97bc3}) of ThingClass plug</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../../../build/nymea-plugins-Desktop-Debug/meross/plugininfo.h" line="57"/>
|
||||||
|
<source>Signal strength</source>
|
||||||
|
<extracomment>The name of the StateType ({ec75ed0a-18e5-401b-a011-52dcb2b26c34}) of ThingClass plug</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../../../build/nymea-plugins-Desktop-Debug/meross/plugininfo.h" line="60"/>
|
||||||
|
<source>Smart plug</source>
|
||||||
|
<extracomment>The name of the ThingClass ({162ef487-e05b-4c92-b4d0-cc5a7ed77134})</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../../../build/nymea-plugins-Desktop-Debug/meross/plugininfo.h" line="63"/>
|
||||||
|
<source>Total consumed energy</source>
|
||||||
|
<extracomment>The name of the StateType ({6b187a9e-f9ed-4952-a078-26380962f42b}) of ThingClass plug</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../../../build/nymea-plugins-Desktop-Debug/meross/plugininfo.h" line="66"/>
|
||||||
|
<source>meross</source>
|
||||||
|
<extracomment>The name of the vendor ({878df3e1-5f73-4d4f-8e65-bb050fda935f})</extracomment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
</TS>
|
||||||
|
|
@ -33,6 +33,7 @@ PLUGIN_DIRS = \
|
||||||
lgsmarttv \
|
lgsmarttv \
|
||||||
lifx \
|
lifx \
|
||||||
mecelectronics \
|
mecelectronics \
|
||||||
|
meross \
|
||||||
mailnotification \
|
mailnotification \
|
||||||
mqttclient \
|
mqttclient \
|
||||||
mystrom \
|
mystrom \
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue