From aa980b29245cc85623a02663c99fda3154382221 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 11 Apr 2022 13:03:10 +0200 Subject: [PATCH] New Plugin: Meross --- debian/control | 8 + debian/nymea-plugin-meross.install.in | 2 + meross/README.md | 20 + meross/integrationpluginmeross.cpp | 374 ++++++++++++++++++ meross/integrationpluginmeross.h | 81 ++++ meross/integrationpluginmeross.json | 83 ++++ meross/meross.pro | 9 + meross/meta.json | 14 + ...1bdcd-f45b-4b88-9383-267381713227-en_US.ts | 84 ++++ nymea-plugins.pro | 1 + 10 files changed, 676 insertions(+) create mode 100644 debian/nymea-plugin-meross.install.in create mode 100644 meross/README.md create mode 100644 meross/integrationpluginmeross.cpp create mode 100644 meross/integrationpluginmeross.h create mode 100644 meross/integrationpluginmeross.json create mode 100644 meross/meross.pro create mode 100644 meross/meta.json create mode 100644 meross/translations/4b11bdcd-f45b-4b88-9383-267381713227-en_US.ts diff --git a/debian/control b/debian/control index 0c024b86..fef1d47f 100644 --- a/debian/control +++ b/debian/control @@ -471,6 +471,14 @@ Description: nymea.io plugin for mec electronics devices 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 Architecture: any Depends: ${shlibs:Depends}, diff --git a/debian/nymea-plugin-meross.install.in b/debian/nymea-plugin-meross.install.in new file mode 100644 index 00000000..70d800c3 --- /dev/null +++ b/debian/nymea-plugin-meross.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginmeross.so +meross/translations/*.qm usr/share/nymea/translations/ diff --git a/meross/README.md b/meross/README.md new file mode 100644 index 00000000..22868787 --- /dev/null +++ b/meross/README.md @@ -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. diff --git a/meross/integrationpluginmeross.cpp b/meross/integrationpluginmeross.cpp new file mode 100644 index 00000000..2301de15 --- /dev/null +++ b/meross/integrationpluginmeross.cpp @@ -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 . +* +* 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 +#include +#include +#include + +#include +#include +#include +#include +#include + +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().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; +} + diff --git a/meross/integrationpluginmeross.h b/meross/integrationpluginmeross.h new file mode 100644 index 00000000..9123c5a8 --- /dev/null +++ b/meross/integrationpluginmeross.h @@ -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 . +* +* 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 m_keys; + QHash m_deviceMonitors; + QHash m_timers; +}; + +#endif // INTEGRATIONPLUGINMEROSS_H diff --git a/meross/integrationpluginmeross.json b/meross/integrationpluginmeross.json new file mode 100644 index 00000000..16d2efbf --- /dev/null +++ b/meross/integrationpluginmeross.json @@ -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 + } + ] + } + ] + } + ] +} diff --git a/meross/meross.pro b/meross/meross.pro new file mode 100644 index 00000000..2b2019e8 --- /dev/null +++ b/meross/meross.pro @@ -0,0 +1,9 @@ +include(../plugins.pri) + +QT += network + +SOURCES += \ + integrationpluginmeross.cpp \ + +HEADERS += \ + integrationpluginmeross.h \ diff --git a/meross/meta.json b/meross/meta.json new file mode 100644 index 00000000..d86ab3fa --- /dev/null +++ b/meross/meta.json @@ -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" + ] +} diff --git a/meross/translations/4b11bdcd-f45b-4b88-9383-267381713227-en_US.ts b/meross/translations/4b11bdcd-f45b-4b88-9383-267381713227-en_US.ts new file mode 100644 index 00000000..78cd4b2a --- /dev/null +++ b/meross/translations/4b11bdcd-f45b-4b88-9383-267381713227-en_US.ts @@ -0,0 +1,84 @@ + + + + + IntegrationPluginMeross + + + Please enter your Meross login credentials. + + + + + + Failed to retrieve the device key from the Meross cloud. + + + + + meross + + + ANEL-Elektronik AG + The name of the plugin meross ({4b11bdcd-f45b-4b88-9383-267381713227}) + + + + + Connected + The name of the StateType ({9cde6321-2abf-4a58-a1d6-c7418edb9747}) of ThingClass plug + + + + + MAC address + The name of the ParamType (ThingClass: plug, Type: thing, ID: {1e273e10-3ea0-4337-a221-3b8e26c6e7dc}) + + + + + + Power + 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 + + + + + Power consumption + The name of the StateType ({79a72494-afe8-46a5-8185-abbb81a6ca48}) of ThingClass plug + + + + + Set power + The name of the ActionType ({0ff2166b-4620-4f7a-ba56-066eead97bc3}) of ThingClass plug + + + + + Signal strength + The name of the StateType ({ec75ed0a-18e5-401b-a011-52dcb2b26c34}) of ThingClass plug + + + + + Smart plug + The name of the ThingClass ({162ef487-e05b-4c92-b4d0-cc5a7ed77134}) + + + + + Total consumed energy + The name of the StateType ({6b187a9e-f9ed-4952-a078-26380962f42b}) of ThingClass plug + + + + + meross + The name of the vendor ({878df3e1-5f73-4d4f-8e65-bb050fda935f}) + + + + diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 781fa5bc..d07c828d 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -33,6 +33,7 @@ PLUGIN_DIRS = \ lgsmarttv \ lifx \ mecelectronics \ + meross \ mailnotification \ mqttclient \ mystrom \