Shelly: Add support for the Shelly TRV

This commit is contained in:
Michael Zanetti 2022-07-30 21:02:00 +02:00
parent 860fbac0e8
commit fd5307772c
5 changed files with 1401 additions and 3535 deletions

View File

@ -19,6 +19,7 @@ The currently supported devices are:
* Shelly i3
* Shelly Motion
* Shelly Vintage
* Shelly TRV
## Requirements
The Shelly device needs to be connected to the same WiFi as nymea is in. New Shelly devices will open a WiFi named with

View File

@ -75,6 +75,7 @@ static QHash<ThingClassId, ParamTypeId> idParamTypeMap = {
{shellyHTThingClassId, shellyHTThingIdParamTypeId},
{shellyI3ThingClassId, shellyI3ThingIdParamTypeId},
{shellyMotionThingClassId, shellyMotionThingIdParamTypeId},
{shellyTrvThingClassId, shellyTrvThingIdParamTypeId},
};
static QHash<ThingClassId, ParamTypeId> usernameParamTypeMap = {
@ -91,7 +92,8 @@ static QHash<ThingClassId, ParamTypeId> usernameParamTypeMap = {
{shellyEm3ThingClassId, shellyEm3ThingUsernameParamTypeId},
{shellyHTThingClassId, shellyHTThingUsernameParamTypeId},
{shellyI3ThingClassId, shellyI3ThingUsernameParamTypeId},
{shellyMotionThingClassId, shellyMotionThingUsernameParamTypeId}
{shellyMotionThingClassId, shellyMotionThingUsernameParamTypeId},
{shellyTrvThingClassId, shellyTrvThingUsernameParamTypeId},
};
static QHash<ThingClassId, ParamTypeId> passwordParamTypeMap = {
@ -108,7 +110,8 @@ static QHash<ThingClassId, ParamTypeId> passwordParamTypeMap = {
{shellyEm3ThingClassId, shellyEm3ThingPasswordParamTypeId},
{shellyHTThingClassId, shellyHTThingPasswordParamTypeId},
{shellyI3ThingClassId, shellyI3ThingPasswordParamTypeId},
{shellyMotionThingClassId, shellyMotionThingPasswordParamTypeId}
{shellyMotionThingClassId, shellyMotionThingPasswordParamTypeId},
{shellyTrvThingClassId, shellyTrvThingPasswordParamTypeId}
};
static QHash<ThingClassId, ParamTypeId> rollerModeParamTypeMap = {
@ -138,6 +141,7 @@ static QHash<ActionTypeId, ThingClassId> rebootActionTypeMap = {
{shelly2RebootActionTypeId, shelly2ThingClassId},
{shelly25RebootActionTypeId, shelly25ThingClassId},
{shellyI3RebootActionTypeId, shellyI3ThingClassId},
{shellyTrvRebootActionTypeId, shellyTrvThingClassId},
};
static QHash<ActionTypeId, ThingClassId> powerActionTypesMap = {
@ -228,7 +232,8 @@ static QHash<ActionTypeId, ThingClassId> updateActionTypesMap = {
{shellyEm3PerformUpdateActionTypeId, shellyEm3ThingClassId},
{shellyHTPerformUpdateActionTypeId, shellyHTThingClassId},
{shellyI3PerformUpdateActionTypeId, shellyI3ThingClassId},
{shellyMotionPerformUpdateActionTypeId, shellyMotionThingClassId}
{shellyMotionPerformUpdateActionTypeId, shellyMotionThingClassId},
{shellyTrvPerformUpdateActionTypeId, shellyTrvThingClassId}
};
// Settings
@ -294,6 +299,8 @@ void IntegrationPluginShelly::discoverThings(ThingDiscoveryInfo *info)
namePattern = QRegExp("shellyix3-[0-9A-Z]+$");
} else if (info->thingClassId() == shellyMotionThingClassId) {
namePattern = QRegExp("shellymotionsensor-[0-9A-Z]+$");
} else if (info->thingClassId() == shellyTrvThingClassId) {
namePattern = QRegExp("shellytrv-[0-9A-Z]+$");
}
if (!entry.name().contains(namePattern)) {
continue;
@ -384,8 +391,10 @@ void IntegrationPluginShelly::executeAction(ThingActionInfo *info)
QUrl url;
url.setScheme("http");
url.setHost(getIP(info->thing()).toString());
url.setUserName(thing->paramValue(usernameParamTypeMap.value(thing->thingClassId())).toString());
url.setPassword(thing->paramValue(passwordParamTypeMap.value(thing->thingClassId())).toString());
if (!thing->paramValue(usernameParamTypeMap.value(thing->thingClassId())).toString().isEmpty()) {
url.setUserName(thing->paramValue(usernameParamTypeMap.value(thing->thingClassId())).toString());
url.setPassword(thing->paramValue(passwordParamTypeMap.value(thing->thingClassId())).toString());
}
if (rebootActionTypeMap.contains(action.actionTypeId())) {
if (shellyId.contains("Plus")) {
@ -574,6 +583,61 @@ void IntegrationPluginShelly::executeAction(ThingActionInfo *info)
return;
}
if (action.actionTypeId() == shellyTrvTargetTemperatureActionTypeId) {
double targetValue = action.paramValue(shellyTrvTargetTemperatureActionTargetTemperatureParamTypeId).toDouble();
url.setPath(QString("/thermostats/0"));
QUrlQuery query;
query.addQueryItem("target_t", QString::number(targetValue));
query.addQueryItem("target_t_enabled", "true");
url.setQuery(query);
qCDebug(dcShelly()) << "Requesting:" << url;
QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [info, reply, targetValue](){
// The Shelly TRV seems to reply with OK, but then takes ages to actually set the value
// If we send another value within that time frame, it will again reply with OK but just ognore it...
// As a workaround we'll make nymea wait for a second until allowing to send the next action.
info->thing()->setStateValue(shellyTrvTargetTemperatureStateTypeId, targetValue);
Thing::ThingError status = reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure;
QTimer::singleShot(1000, info, [info, status](){
info->finish(status);
});
});
return;
}
if (action.actionTypeId() == shellyTrvValvePositionActionTypeId) {
int targetValue = action.paramValue(shellyTrvValvePositionActionValvePositionParamTypeId).toInt();
url.setPath(QString("/thermostats/0"));
QUrlQuery query;
query.addQueryItem("pos", QString::number(targetValue));
url.setQuery(query);
QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [info, reply, targetValue](){
// The Shelly TRV seems to reply with OK, but then takes ages to actually set the value
// If we send another value within that time frame, it will again reply with OK but just ognore it...
// As a workaround we'll make nymea wait for a second until allowing to send the next action.
info->thing()->setStateValue(shellyTrvValvePositionStateTypeId, targetValue);
Thing::ThingError status = reply->error() == QNetworkReply::NoError ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure;
QTimer::singleShot(1000, info, [info, status](){
info->finish(status);
});
});
return;
}
if (action.actionTypeId() == shellyTrvBoostActionTypeId) {
url.setPath(QString("/thermostats/0"));
QUrlQuery query;
query.addQueryItem("boost_minutes", thing->setting(shellyTrvSettingsBoostDurationParamTypeId).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;
}
if (action.actionTypeId() == shellyRollerOpenActionTypeId) {
url.setPath(QString("/roller/%1").arg(info->thing()->paramValue(shellyRollerThingChannelParamTypeId).toInt() - 1));
QUrlQuery query;
@ -790,8 +854,12 @@ void IntegrationPluginShelly::onMulticastMessageReceived(const QHostAddress &sou
case 3101:
thing->setStateValue("temperature", value.toDouble());
break;
case 3103:
thing->setStateValue("humidity", value.toDouble());
case 3103: // This is target tempererature for the TRV, but humidity for other sensors
if (thing->thingClassId() == shellyTrvThingClassId) {
thing->setStateValue("targetTemperature", value.toDouble());
} else {
thing->setStateValue("humidity", value.toDouble());
}
break;
case 3106:
thing->setStateValue("lightIntensity", value.toInt());
@ -804,6 +872,13 @@ void IntegrationPluginShelly::onMulticastMessageReceived(const QHostAddress &sou
}
thing->setStateValue("batteryCritical", thing->stateValue("batteryLevel").toUInt() < 10);
break;
case 3121:
thing->setStateValue("valvePosition", value.toUInt());
thing->setStateValue("heatingOn", value.toUInt() > 0);
break;
case 3122:
thing->setStateValue("boost", value.toUInt() > 0);
break;
case 4101: // power meter for channel 1
if (thing->hasState("currentPower")) {
thing->setStateValue("currentPower", value);
@ -1132,8 +1207,10 @@ void IntegrationPluginShelly::setupGen1(ThingSetupInfo *info)
url.setHost(address.toString());
url.setPort(80);
url.setPath("/settings");
url.setUserName(info->thing()->paramValue(usernameParamTypeMap.value(info->thing()->thingClassId())).toString());
url.setPassword(info->thing()->paramValue(passwordParamTypeMap.value(info->thing()->thingClassId())).toString());
if (!thing->paramValue(usernameParamTypeMap.value(thing->thingClassId())).toString().isEmpty()) {
url.setUserName(info->thing()->paramValue(usernameParamTypeMap.value(info->thing()->thingClassId())).toString());
url.setPassword(info->thing()->paramValue(passwordParamTypeMap.value(info->thing()->thingClassId())).toString());
}
QUrlQuery query;
query.addQueryItem("coiot_enable", "true");
@ -1181,6 +1258,10 @@ void IntegrationPluginShelly::setupGen1(ThingSetupInfo *info)
info->thing()->setSettingValue(shellyI3SettingsLongpushMinDurationParamTypeId, settingsMap.value("longpush_duration_ms").toMap().value("min").toUInt());
info->thing()->setSettingValue(shellyI3SettingsLongpushMaxDurationParamTypeId, settingsMap.value("longpush_duration_ms").toMap().value("max").toUInt());
info->thing()->setSettingValue(shellyI3SettingsMultipushTimeBetweenPushesParamTypeId, settingsMap.value("multipush_time_between_pushes_ms").toMap().value("max").toUInt());
} else if (info->thing()->thingClassId() == shellyTrvThingClassId) {
info->thing()->setSettingValue(shellyTrvSettingsChildLockParamTypeId, settingsMap.value("child_lock").toBool());
info->thing()->setSettingValue(shellyTrvSettingsDisplayFlippedParamTypeId, settingsMap.value("display").toMap().value("flipped").toBool());
info->thing()->setSettingValue(shellyTrvSettingsDisplayBrightnessParamTypeId, settingsMap.value("display").toMap().value("brightness").toUInt());
}
ThingDescriptors autoChilds;
@ -1235,6 +1316,7 @@ void IntegrationPluginShelly::setupGen1(ThingSetupInfo *info)
}
info->finish(Thing::ThingErrorNoError);
info->thing()->setStateValue("connected", true);
emit autoThingsAppeared(autoChilds);
@ -1281,7 +1363,8 @@ void IntegrationPluginShelly::setupGen1(ThingSetupInfo *info)
// Handle thing settings of gateway devices
if (info->thing()->thingClassId() == shellyPlugThingClassId ||
info->thing()->thingClassId() == shellyButton1ThingClassId ||
info->thing()->thingClassId() == shellyI3ThingClassId) {
info->thing()->thingClassId() == shellyI3ThingClassId ||
info->thing()->thingClassId() == shellyTrvThingClassId) {
connect(info->thing(), &Thing::settingChanged, this, [this, thing, shellyId](const ParamTypeId &settingTypeId, const QVariant &value) {
pluginStorage()->beginGroup(thing->id().toString());
@ -1316,6 +1399,15 @@ void IntegrationPluginShelly::setupGen1(ThingSetupInfo *info)
|| settingTypeId == shellyI3SettingsMultipushTimeBetweenPushesParamTypeId) {
url.setPath("/settings");
query.addQueryItem("multipush_time_between_pushes_ms_max", value.toString());
} else if (settingTypeId == shellyTrvSettingsChildLockParamTypeId) {
url.setPath("/settings");
query.addQueryItem("child_lock", value.toString());
} else if (settingTypeId == shellyTrvSettingsDisplayBrightnessParamTypeId) {
url.setPath("/settings");
query.addQueryItem("display_brightness", value.toString());
} else if (settingTypeId == shellyTrvSettingsDisplayFlippedParamTypeId) {
url.setPath("/settings");
query.addQueryItem("display_flipped", value.toString());
}
url.setQuery(query);

View File

@ -1458,6 +1458,209 @@
}
]
},
{
"id": "52932a47-38cd-4dce-b338-88122ce4ab8a",
"name": "shellyTrv",
"displayName": "Shelly TRV",
"createMethods": ["discovery"],
"interfaces": ["thermostat", "temperaturesensor", "wirelessconnectable", "battery", "update"],
"paramTypes": [
{
"id": "20e21853-cfc1-4f78-9ba3-12682f81e021",
"name":"id",
"displayName": "Shelly ID",
"type": "QString",
"readOnly": true
},
{
"id": "ee942141-3102-4b6a-87dc-0fc6481924e5",
"name": "username",
"displayName": "Username (optional)",
"type": "QString"
},
{
"id": "bf4fead2-7a1a-44e7-bd32-ea2064944098",
"name": "password",
"displayName": "Password (optional)",
"type": "QString"
}
],
"settingsTypes": [
{
"id": "3a1dbc59-5b61-4650-a0fa-e127d337169e",
"name": "boostDuration",
"displayName": "Boost duration (minutes)",
"type": "uint",
"minValue": 0,
"maxValue": 120,
"defaultValue": 10
},
{
"id": "38a98b85-9c6e-4dc8-8d73-5248532d2ed8",
"name": "childLock",
"displayName": "Child lock",
"type": "bool",
"defaultValue": false
},
{
"id": "83cfbdb7-a807-4a81-9eb0-5e0d62efdbaf",
"name": "displayFlipped",
"displayName": "Display flipped",
"type": "bool",
"defaultValue": false
},
{
"id": "e0f7aae7-d576-4897-9626-2cc7e452b30a",
"name": "displayBrightness",
"displayName": "Display brightness",
"type": "uint",
"minValue": 1,
"maxValue": 7,
"defaultValue": 7
}
],
"stateTypes": [
{
"id": "d9a26a08-5735-403a-ab02-7638bd0a471f",
"name": "temperature",
"displayName": "Temperature",
"displayNameEvent": "Temperature changed",
"type": "double",
"unit": "DegreeCelsius",
"defaultValue": 0
},
{
"id": "9800babf-a6cc-4eda-b42e-8f5481b61aea",
"name": "targetTemperature",
"displayName": "Target temperature",
"displayNameEvent": "Target temperature changed",
"displayNameAction": "Set target temperature",
"type": "double",
"unit": "DegreeCelsius",
"defaultValue": 0,
"minValue": 4,
"maxValue": 31,
"writable": true
},
{
"id": "e442ca7a-ee17-482b-aae4-579915029abf",
"name": "valvePosition",
"displayName": "Valve position",
"displayNameEvent": "Valve position changed",
"displayNameAction": "Set valve position",
"type": "uint",
"unit": "Percentage",
"defaultValue": 0,
"minValue": 0,
"maxValue": 100,
"writable": true
},
{
"id": "1935b7fa-72a5-4aee-877e-d656cd79d688",
"name": "heatingOn",
"displayName": "Heating",
"displayNameEvent": "Heating changed",
"type": "bool",
"defaultValue": false
},
{
"id": "ef74da4d-70f4-49cd-9697-a8e2bf25dee1",
"name": "boost",
"displayName": "Boost",
"displayNameEvent": "Boost enabled/disabled",
"displayNameAction": "Enable/disable boost",
"type": "bool",
"defaultValue": false,
"writable": true
},
{
"id": "a5944856-6b0f-4b45-9d9f-fe0f3c2de8aa",
"name": "windowOpen",
"displayName": "Window open",
"displayNameEvent": "Window opened/closed",
"type": "bool",
"defaultValue": false
},
{
"id": "92fa20b0-2b66-4d68-8819-6eeb43f1c0fb",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected or disconnected",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "42e080f3-c00c-49d2-b046-7bd26331b8f0",
"name": "signalStrength",
"displayName": "Signal strength",
"displayNameEvent": "Signal strength changed",
"type": "uint",
"unit": "Percentage",
"minValue": 0,
"maxValue": 100,
"defaultValue": 0,
"cached": false
},
{
"id": "f45dff98-41ac-43bb-a005-24294973611b",
"name": "batteryLevel",
"displayName": "Battery level",
"displayNameEvent": "Battery level changed",
"type": "int",
"minValue": 0,
"maxValue": 100,
"unit": "Percentage",
"defaultValue": 0
},
{
"id": "48c3a619-331e-44eb-b080-877fdcfd03f6",
"name": "batteryCritical",
"displayName": "Battery level critical",
"displayNameEvent": "Battery critical changed",
"type": "bool",
"defaultValue": false
},
{
"id": "113625e0-e171-48e6-a9ca-6a13b75b9234",
"name": "updateStatus",
"displayName": "Update status",
"displayNameEvent": "Update status changed",
"type": "QString",
"possibleValues": ["idle", "available", "updating"],
"defaultValue": "idle"
},
{
"id": "ad5cdb22-42ce-4592-ad72-61a854981f69",
"name": "currentVersion",
"displayName": "Current firmware version",
"displayNameEvent": "Current firmware version changed",
"type": "QString",
"defaultValue": ""
},
{
"id": "f3e00825-cd05-45e6-bc99-457bf74255c5",
"name": "availableVersion",
"displayName": "Available firmware version",
"displayNameEvent": "Available firmware version changed",
"type": "QString",
"defaultValue": ""
}
],
"actionTypes": [
{
"id": "27e4c7f5-1828-443c-a18d-6d79382e001d",
"name": "performUpdate",
"displayName": "Start firmware update"
},
{
"id": "4cef8e3a-853b-4313-8f70-d22122e7bb04",
"name": "reboot",
"displayName": "Reboot device"
}
]
},
{
"id": "6de35a17-0f54-4397-894d-4321b64c53d1",
"name": "shellySwitch",

File diff suppressed because it is too large Load Diff