From 113b1eb9a10b8acf5c63fa46e0b584807090e14e Mon Sep 17 00:00:00 2001 From: loosrob <79396812+loosrob@users.noreply.github.com> Date: Sun, 21 Aug 2022 19:24:35 +0200 Subject: [PATCH] Add support for Shelly Plus 2PM --- shelly/README.md | 1 + shelly/integrationpluginshelly.cpp | 259 ++++++++++++++++++++++------ shelly/integrationpluginshelly.json | 2 +- 3 files changed, 209 insertions(+), 53 deletions(-) diff --git a/shelly/README.md b/shelly/README.md index 881b190c..681eedb7 100644 --- a/shelly/README.md +++ b/shelly/README.md @@ -9,6 +9,7 @@ The currently supported devices are: * Shelly 1L * Shelly 2 * Shelly 2.5 +* Shelly Plus 2PM * Shelly Plug / PlugS * Shelly RGBW2 * Shelly Dimmer / Dimmer 2 diff --git a/shelly/integrationpluginshelly.cpp b/shelly/integrationpluginshelly.cpp index c05ba9d1..9a53f371 100644 --- a/shelly/integrationpluginshelly.cpp +++ b/shelly/integrationpluginshelly.cpp @@ -299,7 +299,7 @@ void IntegrationPluginShelly::discoverThings(ThingDiscoveryInfo *info) } else if (info->thingClassId() == shelly2ThingClassId) { namePattern = QRegExp("^shellyswitch-[0-9A-Z]+$"); } else if (info->thingClassId() == shelly25ThingClassId) { - namePattern = QRegExp("^shellyswitch25-[0-9A-Z]+$"); + namePattern = QRegExp("^(shellyswitch25|ShellyPlus2PM)-[0-9A-Z]+$"); } else if (info->thingClassId() == shellyButton1ThingClassId) { namePattern = QRegExp("^shellybutton1-[0-9-A-Z]+$"); } else if (info->thingClassId() == shellyEmThingClassId) { @@ -467,7 +467,7 @@ void IntegrationPluginShelly::executeAction(ThingActionInfo *info) QVariantMap params; params.insert("id", relay - 1); params.insert("on", on); - ShellyRpcReply *reply = m_rpcClients.value(thing)->sendRequest("Switch.Set", params); + ShellyRpcReply *reply = m_rpcClients.value(thing)->sendRequest("Switch.Set", params); // Switch.Set not supported by Shelly Plus 2PM in shutter mode; will return error "No handler for Switch.Set" connect(reply, &ShellyRpcReply::finished, info, [info](ShellyRpcReply::Status status, const QVariantMap &/*response*/){ info->finish(status == ShellyRpcReply::StatusSuccess ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); }); @@ -658,65 +658,117 @@ void IntegrationPluginShelly::executeAction(ThingActionInfo *info) } if (action.actionTypeId() == shellyRollerOpenActionTypeId) { - url.setPath(QString("/roller/%1").arg(info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1)); - QUrlQuery query; - query.addQueryItem("go", "open"); - url.setQuery(query); - QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); - connect(reply, &QNetworkReply::finished, info, [info, reply](){ - info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); - }); + if (shellyId.contains("Plus")) { + QVariantMap params; + int channelNbr = info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1; + params.insert("id", channelNbr); + ShellyRpcReply *reply = m_rpcClients.value(thing)->sendRequest("Cover.Open", params); + connect(reply, &ShellyRpcReply::finished, info, [info](ShellyRpcReply::Status status, const QVariantMap &/*response*/){ + info->finish(status == ShellyRpcReply::StatusSuccess ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } else { + url.setPath(QString("/roller/%1").arg(info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1)); + QUrlQuery query; + query.addQueryItem("go", "open"); + url.setQuery(query); + QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [info, reply](){ + info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } return; } if (action.actionTypeId() == shellyRollerCloseActionTypeId) { - url.setPath(QString("/roller/%1").arg(info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1)); - QUrlQuery query; - query.addQueryItem("go", "close"); - url.setQuery(query); - QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); - connect(reply, &QNetworkReply::finished, info, [info, reply](){ - info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); - }); + if (shellyId.contains("Plus")) { + QVariantMap params; + int channelNbr = info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1; + params.insert("id", channelNbr); + ShellyRpcReply *reply = m_rpcClients.value(thing)->sendRequest("Cover.Close", params); + connect(reply, &ShellyRpcReply::finished, info, [info](ShellyRpcReply::Status status, const QVariantMap &/*response*/){ + info->finish(status == ShellyRpcReply::StatusSuccess ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } else { + url.setPath(QString("/roller/%1").arg(info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1)); + QUrlQuery query; + query.addQueryItem("go", "close"); + url.setQuery(query); + QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [info, reply](){ + info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } return; } if (action.actionTypeId() == shellyRollerStopActionTypeId) { - url.setPath(QString("/roller/%1").arg(info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1)); - QUrlQuery query; - query.addQueryItem("go", "stop"); - url.setQuery(query); - QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); - connect(reply, &QNetworkReply::finished, info, [info, reply](){ - info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); - }); + if (shellyId.contains("Plus")) { + QVariantMap params; + int channelNbr = info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1; + params.insert("id", channelNbr); + ShellyRpcReply *reply = m_rpcClients.value(thing)->sendRequest("Cover.Stop", params); + connect(reply, &ShellyRpcReply::finished, info, [info](ShellyRpcReply::Status status, const QVariantMap &/*response*/){ + info->finish(status == ShellyRpcReply::StatusSuccess ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } else { + url.setPath(QString("/roller/%1").arg(info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1)); + QUrlQuery query; + query.addQueryItem("go", "stop"); + url.setQuery(query); + QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [info, reply](){ + info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } return; } if (action.actionTypeId() == shellyRollerCalibrateActionTypeId) { - url.setPath(QString("/roller/%1/calibrate").arg(info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1)); - QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); - connect(reply, &QNetworkReply::finished, info, [info, reply](){ - info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); - }); + if (shellyId.contains("Plus")) { + QVariantMap params; + int channelNbr = info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1; + params.insert("id", channelNbr); + ShellyRpcReply *reply = m_rpcClients.value(thing)->sendRequest("Cover.Calibrate", params); + connect(reply, &ShellyRpcReply::finished, info, [info](ShellyRpcReply::Status status, const QVariantMap &/*response*/){ + info->finish(status == ShellyRpcReply::StatusSuccess ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } else { + url.setPath(QString("/roller/%1/calibrate").arg(info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1)); + QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [info, reply](){ + info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } return; } if (action.actionTypeId() == shellyRollerPercentageActionTypeId) { - url.setPath(QString("/roller/%1").arg(info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1)); - QUrlQuery query; - query.addQueryItem("go", "to_pos"); - query.addQueryItem("roller_pos", QString::number(100 - info->action().paramValue(shellyRollerPercentageActionPercentageParamTypeId).toUInt())); - url.setQuery(query); - QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); - connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); - connect(reply, &QNetworkReply::finished, info, [info, reply](){ - info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); - }); + if (shellyId.contains("Plus")) { + QVariantMap params; + int channelNbr = info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1; + int positionTarget = info->action().paramValue(shellyRollerPercentageActionPercentageParamTypeId).toInt(); + params.insert("id", channelNbr); + params.insert("pos", positionTarget); + ShellyRpcReply *reply = m_rpcClients.value(thing)->sendRequest("Cover.GoToPosition", params); + connect(reply, &ShellyRpcReply::finished, info, [info](ShellyRpcReply::Status status, const QVariantMap &/*response*/){ + info->finish(status == ShellyRpcReply::StatusSuccess ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } else { + url.setPath(QString("/roller/%1").arg(info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1)); + QUrlQuery query; + query.addQueryItem("go", "to_pos"); + query.addQueryItem("roller_pos", info->action().paramValue(shellyRollerPercentageActionPercentageParamTypeId).toString()); + url.setQuery(query); + QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [info, reply](){ + info->finish(reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure); + }); + } return; } @@ -1589,15 +1641,59 @@ void IntegrationPluginShelly::setupGen2(ThingSetupInfo *info) } qCDebug(dcShelly) << "Init response:" << response; m_rpcClients.insert(info->thing(), client); - info->finish(Thing::ThingErrorNoError); - if (myThings().filterByParentId(info->thing()->id()).count() == 0) { - if (info->thing()->thingClassId() == shelly1pmThingClassId) { + if (info->thing()->thingClassId() == shelly1pmThingClassId) { + info->finish(Thing::ThingErrorNoError); + + if (myThings().filterByParentId(info->thing()->id()).count() == 0) { ThingDescriptor switchChild(shellySwitchThingClassId, info->thing()->name() + " switch", QString(), info->thing()->id()); switchChild.setParams(ParamList() << Param(shellySwitchThingChannelParamTypeId, 1)); emit autoThingsAppeared({switchChild}); } } + + if (info->thing()->thingClassId() == shelly25ThingClassId) { + // Make sure the shelly 2.5 is in the mode we expect it to be (roller/cover or relay/switch) + bool rollerMode = info->thing()->paramValue(rollerModeParamTypeMap.value(info->thing()->thingClassId())).toBool(); + QVariantMap params; + if(rollerMode) { + params.insert("name", "cover"); + } else { + params.insert("name", "switch"); + } + ShellyRpcReply *reply2 = client->sendRequest("Shelly.SetProfile", params); + connect(reply2, &ShellyRpcReply::finished, info, [info](ShellyRpcReply::Status status, const QVariantMap &/*response*/){ + if (status != ShellyRpcReply::StatusSuccess) { + qCWarning(dcShelly) << "Error during shelly setup"; + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to configure shelly device.")); + return; + } + info->finish(Thing::ThingErrorNoError); + }); + + if (myThings().filterByParentId(info->thing()->id()).count() == 0) { + ThingDescriptor switchChild(shellySwitchThingClassId, info->thing()->name() + " switch 1", QString(), info->thing()->id()); + switchChild.setParams(ParamList() << Param(shellySwitchThingChannelParamTypeId, 1)); + emit autoThingsAppeared({switchChild}); + ThingDescriptor switch2Child(shellySwitchThingClassId, info->thing()->name() + " switch 2", QString(), info->thing()->id()); + switch2Child.setParams(ParamList() << Param(shellySwitchThingChannelParamTypeId, 2)); + emit autoThingsAppeared({switch2Child}); + } + + if (rollerMode == true) { + ThingDescriptor rollerShutterChild(shellyRollerThingClassId, info->thing()->name() + " connected shutter", QString(), info->thing()->id()); + rollerShutterChild.setParams(ParamList() << Param(shellyRollerThingChannelParamTypeId, 1)); + emit autoThingsAppeared({rollerShutterChild}); + // Create 2 measurement channels for Shelly Plus 2PM (unless in roller mode) + } else { + ThingDescriptor channelChild(shellyPowerMeterChannelThingClassId, info->thing()->name() + " channel 1", QString(), info->thing()->id()); + channelChild.setParams(ParamList() << Param(shellyPowerMeterChannelThingChannelParamTypeId, 1)); + emit autoThingsAppeared({channelChild}); + ThingDescriptor channel2Child(shellyPowerMeterChannelThingClassId, info->thing()->name() + " channel 2", QString(), info->thing()->id()); + channel2Child.setParams(ParamList() << Param(shellyPowerMeterChannelThingChannelParamTypeId, 2)); + emit autoThingsAppeared({channel2Child}); + } + } }); }); @@ -1617,14 +1713,65 @@ void IntegrationPluginShelly::setupGen2(ThingSetupInfo *info) qCDebug(dcShelly) << "notification received" << qUtf8Printable(QJsonDocument::fromVariant(notification).toJson()); if (notification.contains("switch:0")) { QVariantMap switch0 = notification.value("switch:0").toMap(); - if (switch0.contains("apower") && thing->hasState("currentPower")) { + if (switch0.contains("apower") && thing->hasState("currentPower")) { // for shellyplus1pm thing->setStateValue("currentPower", switch0.value("apower").toDouble()); } - if (switch0.contains("aenergy") && thing->hasState("totalEnergyConsumed")) { - thing->setStateValue("totalEnergyConsumed", notification.value("switch:0").toMap().value("aenergy").toMap().value("total").toDouble() / 1000); + Thing *parentThing = myThings().filterByParentId(thing->id()).findByParams({Param(shellyPowerMeterChannelThingChannelParamTypeId, 1)}); + if (parentThing) { + if (switch0.contains("apower")) { + parentThing->setStateValue("currentPower", switch0.value("apower").toDouble()); + } + if (switch0.contains("aenergy")) { + parentThing->setStateValue("totalEnergyConsumed", notification.value("switch:0").toMap().value("aenergy").toMap().value("total").toDouble() / 1000); + } + } else { + if (switch0.contains("aenergy") && thing->hasState("totalEnergyConsumed")) { // for shellyplus1pm + thing->setStateValue("totalEnergyConsumed", notification.value("switch:0").toMap().value("aenergy").toMap().value("total").toDouble() / 1000); + } } - if (switch0.contains("output") && thing->hasState("power")) { + if (switch0.contains("output") && thing->hasState("power")) { // for shellyplus1pm thing->setStateValue("power", switch0.value("output").toBool()); + } else if (switch0.contains("output") && thing->hasState("channel1")) { // for shellyplus2pm + thing->setStateValue("channel1", switch0.value("output").toBool()); + } + } + if (notification.contains("switch:1")) { + QVariantMap switch1 = notification.value("switch:1").toMap(); + Thing *parentThing = myThings().filterByParentId(thing->id()).findByParams({Param(shellyPowerMeterChannelThingChannelParamTypeId, 2)}); + if (parentThing) { + if (switch1.contains("apower")) { + parentThing->setStateValue("currentPower", switch1.value("apower").toDouble()); + } + if (switch1.contains("aenergy")) { + parentThing->setStateValue("totalEnergyConsumed", notification.value("switch:1").toMap().value("aenergy").toMap().value("total").toDouble() / 1000); + } + } + if (switch1.contains("output") && thing->hasState("channel2")) { // for shellyplus2pm + thing->setStateValue("channel2", switch1.value("output").toBool()); + } + } + if (notification.contains("cover:0")) { + QVariantMap cover0 = notification.value("cover:0").toMap(); + Thing *t = myThings().filterByParentId(thing->id()).findByParams({Param(shellyRollerThingChannelParamTypeId, 1)}); + if (cover0.contains("apower") && t) { + t->setStateValue("currentPower", cover0.value("apower").toDouble()); + } + if (cover0.contains("aenergy") && t) { + t->setStateValue("totalEnergyConsumed", notification.value("cover:0").toMap().value("aenergy").toMap().value("total").toDouble()); + } + if (cover0.contains("current_pos") && t) { + t->setStateValue("percentage", notification.value("cover:0").toMap().value("current_pos").toInt()); + } + if (cover0.contains("state") && t) { + QString coverState = notification.value("cover:0").toMap().value("state").toString(); + bool movingBool = false; + if (coverState == "opening" || coverState == "closing" || coverState == "calibrating") { + movingBool = true; + } + t->setStateValue("moving", movingBool); + } + if (cover0.contains("output") && thing->hasState("channel1")) { // for shellyplus2pm + thing->setStateValue("power", cover0.value("output").toBool()); } } if (notification.contains("input:0")) { @@ -1635,6 +1782,14 @@ void IntegrationPluginShelly::setupGen2(ThingSetupInfo *info) t->emitEvent("pressed"); } } + if (notification.contains("input:1")) { + QVariantMap input1 = notification.value("input:1").toMap(); + Thing *t = myThings().filterByParentId(thing->id()).findByParams({Param(shellySwitchThingChannelParamTypeId, 2)}); + if (t) { + t->setStateValue("power", input1.value("state").toBool()); + t->emitEvent("pressed"); + } + } }); } diff --git a/shelly/integrationpluginshelly.json b/shelly/integrationpluginshelly.json index 5377830e..e74170ae 100644 --- a/shelly/integrationpluginshelly.json +++ b/shelly/integrationpluginshelly.json @@ -495,7 +495,7 @@ { "id": "465efb0d-da68-4177-a040-940c7f451e29", "name": "shelly25", - "displayName": "Shelly 2.5", + "displayName": "Shelly 2.5/Shelly Plus 2PM", "createMethods": ["discovery"], "interfaces": [ "gateway", "wirelessconnectable", "update" ], "paramTypes": [