Merge PR #166: New plugin: Shelly

master
Jenkins nymea 2019-10-22 01:09:53 +02:00
commit 01da2a7487
10 changed files with 769 additions and 51 deletions

81
debian/control vendored
View File

@ -211,6 +211,22 @@ Description: nymea.io plugin for dweet.io
This package will install the nymea.io plugin for dweet.io
Package: nymea-plugin-elgato
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Replaces: guh-plugin-elgato
Description: nymea.io plugin for elgato
The nymea daemon is a plugin based IoT (Internet of Things) server. The
server works like a translator for devices, things and services and
allows them to interact.
With the powerful rule engine you are able to connect any device available
in the system and create individual scenes and behaviors for your environment.
.
This package will install the nymea.io plugin for elgato
Package: nymea-plugin-elro
Architecture: any
Depends: ${shlibs:Depends},
@ -657,38 +673,6 @@ Description: nymea.io plugin for HTTP commander
This package will install the nymea.io plugin for the HTPP commander
Package: nymea-plugin-simulation
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Replaces: guh-plugin-simulation
Description: nymea.io plugin for simulated devices
The nymea daemon is a plugin based IoT (Internet of Things) server. The
server works like a translator for devices, things and services and
allows them to interact.
With the powerful rule engine you are able to connect any device available
in the system and create individual scenes and behaviors for your environment.
.
This package will install the nymea.io plugin for simulated devices
Package: nymea-plugin-elgato
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Replaces: guh-plugin-elgato
Description: nymea.io plugin for elgato
The nymea daemon is a plugin based IoT (Internet of Things) server. The
server works like a translator for devices, things and services and
allows them to interact.
With the powerful rule engine you are able to connect any device available
in the system and create individual scenes and behaviors for your environment.
.
This package will install the nymea.io plugin for elgato
Package: nymea-plugin-senic
Architecture: any
Depends: ${shlibs:Depends},
@ -705,6 +689,38 @@ Description: nymea.io plugin for senic
This package will install the nymea.io plugin for senic
Package: nymea-plugin-shelly
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Replaces: guh-plugin-simulation
Description: nymea.io plugin for Shelly devices
The nymea daemon is a plugin based IoT (Internet of Things) server. The
server works like a translator for devices, things and services and
allows them to interact.
With the powerful rule engine you are able to connect any device available
in the system and create individual scenes and behaviors for your environment.
.
This package will install the nymea.io plugin for Shelly devices
Package: nymea-plugin-simulation
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Replaces: guh-plugin-simulation
Description: nymea.io plugin for simulated devices
The nymea daemon is a plugin based IoT (Internet of Things) server. The
server works like a translator for devices, things and services and
allows them to interact.
With the powerful rule engine you are able to connect any device available
in the system and create individual scenes and behaviors for your environment.
.
This package will install the nymea.io plugin for simulated devices
Package: nymea-plugin-sonos
Architecture: any
Depends: ${shlibs:Depends},
@ -836,6 +852,7 @@ Depends: nymea-plugin-awattar,
nymea-plugin-tasmota,
nymea-plugin-wemo,
nymea-plugin-elgato,
nymea-plugin-shelly,
nymea-plugin-senic,
nymea-plugin-sonos,
nymea-plugin-keba,

1
debian/nymea-plugin-shelly.install.in vendored Normal file
View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginshelly.so

View File

@ -38,7 +38,7 @@ void DevicePluginMqttClient::setupDevice(DeviceSetupInfo *info)
MqttClient *client = nullptr;
if (device->deviceClassId() == internalMqttClientDeviceClassId) {
client = hardwareManager()->mqttProvider()->createInternalClient(device->id());
client = hardwareManager()->mqttProvider()->createInternalClient(device->id().toString());
} else if (device->deviceClassId() == mqttClientDeviceClassId){
client = new MqttClient("nymea-" + device->id().toString().remove(QRegExp("[{}]")).left(8), this);
client->setUsername(device->paramValue(mqttClientDeviceUsernameParamTypeId).toString());

View File

@ -35,6 +35,7 @@ PLUGIN_DIRS = \
osdomotics \
philipshue \
pushbullet \
shelly \
systemmonitor \
remotessh \
senic \

29
shelly/README.md Normal file
View File

@ -0,0 +1,29 @@
# Shelly
The Shelly plugin adds support for Shelly devices (https://shelly.cloud).
Currently the Shelly1 and Shelly1PM are supported.
## Requirements
Shelly devices communicate with via MQTT. This means, in order to add Shelly devices to nymea, the nymea instance is required
to have the MQTT broker enabled in the nymea settings and the Shelly device needs to be connected to the same WiFi as nymea is
in. New Shelly devices will open a WiFi named with their name as SSID. For instance, a Shelly 1 would appear as "shelly1-XXXXXX".
Connect to this WiFi and open the webpage that will pop up. From there, it can be configured it to connect to the same
network where the nymea system is located. No other options need to be set as they can be configured using nymea later on.
## Setting up devices
Once the Shelly is connected to the WiFi, a device discovery in nymea can be performed and will list the Shelly device.
During setup, the connected device can be configured. If the Shelly is connected to e.g. a light bulb, choose "Light" here.
Optionally, a username and password can be set. If the Shelly device is already configured to require authentication,
the username and password here must match the ones set on the Shelly. NOTE: If the Shelly is not configured to require a
login yet, but credentials are entered during setup, the Shelly device will be configured to require authentication from
now on.
## Plugin properties
When adding a Shelly device it will add a new Gateway type device representing the Shelly device itself. It will allow
basic monitoring (such as the connected state) and interaction (e.g. reboot the Shelly device). In addition to that, a
power switch device will appear which will reflect presses on the Shelly's SW input. This power switch device also
offers the possiblity to configure the used switch (e.g. toggle, momentary, edge or detached from the Shelly's output).
If a connected device has been selected during setup, an additional device, e.g. the light will appear in the system and
can be used to control the power output of the Shelly, e.g. turning on or off the connected light.

View File

@ -0,0 +1,395 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2019 Michael Zanetti <michael.zanetti@nymea.io> *
* *
* This file is part of nymea. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or (at your option) any later version. *
* *
* This library 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 library; If not, see *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "devicepluginshelly.h"
#include "plugininfo.h"
#include <QUrlQuery>
#include <QNetworkReply>
#include <QHostAddress>
#include <QJsonDocument>
#include "hardwaremanager.h"
#include "network/networkaccessmanager.h"
#include "network/mqtt/mqttprovider.h"
#include "network/mqtt/mqttchannel.h"
#include "network/zeroconf/zeroconfservicebrowser.h"
#include "platform/platformzeroconfcontroller.h"
DevicePluginShelly::DevicePluginShelly()
{
m_connectedStateTypesMap[shellySwitchDeviceClassId] = shellySwitchConnectedStateTypeId;
m_connectedStateTypesMap[shellyGenericDeviceClassId] = shellyGenericConnectedStateTypeId;
m_connectedStateTypesMap[shellyLightDeviceClassId] = shellyLightConnectedStateTypeId;
m_powerActionTypesMap[shellyGenericPowerActionTypeId] = shellyGenericDeviceClassId;
m_powerActionTypesMap[shellyLightPowerActionTypeId] = shellyLightDeviceClassId;
m_powerActionParamTypesMap[shellyGenericPowerActionTypeId] = shellyGenericPowerActionPowerParamTypeId;
m_powerActionParamTypesMap[shellyLightPowerActionTypeId] = shellyLightPowerActionPowerParamTypeId;
m_powerStateTypeMap[shellyGenericDeviceClassId] = shellyGenericPowerStateTypeId;
m_powerStateTypeMap[shellyLightDeviceClassId] = shellyLightPowerStateTypeId;
}
DevicePluginShelly::~DevicePluginShelly()
{
}
void DevicePluginShelly::init()
{
m_zeroconfBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_http._tcp");
}
void DevicePluginShelly::discoverDevices(DeviceDiscoveryInfo *info)
{
foreach (const ZeroConfServiceEntry &entry, m_zeroconfBrowser->serviceEntries()) {
// qCDebug(dcShelly()) << "Have entry" << entry;
QRegExp namePattern;
if (info->deviceClassId() == shelly1DeviceClassId) {
namePattern = QRegExp("^shelly(1|1pm)-[0-9A-Z]+$");
}
if (!entry.name().contains(namePattern)) {
continue;
}
DeviceDescriptor descriptor(shelly1DeviceClassId, entry.name(), entry.hostAddress().toString());
ParamList params;
params << Param(shelly1DeviceIdParamTypeId, entry.name());
descriptor.setParams(params);
Device *existingDevice = myDevices().findByParams(params);
if (existingDevice) {
descriptor.setDeviceId(existingDevice->id());
}
info->addDeviceDescriptor(descriptor);
qCDebug(dcShelly()) << "Found shelly device!" << entry;
}
info->finish(Device::DeviceErrorNoError);
}
void DevicePluginShelly::setupDevice(DeviceSetupInfo *info)
{
Device *device = info->device();
if (device->deviceClassId() == shelly1DeviceClassId) {
setupShellyGateway(info);
return;
}
setupShellyChild(info);
}
void DevicePluginShelly::deviceRemoved(Device *device)
{
if (m_mqttChannels.contains(device)) {
hardwareManager()->mqttProvider()->releaseChannel(m_mqttChannels.take(device));
}
qCDebug(dcShelly()) << "Device removed" << device->name();
}
void DevicePluginShelly::executeAction(DeviceActionInfo *info)
{
Device *device = info->device();
Action action = info->action();
if (action.actionTypeId() == shelly1RebootActionTypeId) {
QUrl url;
url.setScheme("http");
url.setHost(getIP(info->device()));
url.setPath("/reboot");
url.setUserName(device->paramValue(shelly1DeviceUsernameParamTypeId).toString());
url.setPassword(device->paramValue(shelly1DevicePasswordParamTypeId).toString());
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 ? Device::DeviceErrorNoError : Device::DeviceErrorHardwareFailure);
});
return;
}
if (m_powerActionTypesMap.contains(action.actionTypeId())) {
Device *parentDevice = myDevices().findById(device->parentId());
MqttChannel *channel = m_mqttChannels.value(parentDevice);
QString shellyId = parentDevice->paramValue(shelly1DeviceIdParamTypeId).toString();
ParamTypeId powerParamTypeId = m_powerActionParamTypesMap.value(action.actionTypeId());
bool on = action.param(powerParamTypeId).value().toBool();
channel->publish("shellies/" + shellyId + "/relay/0/command", on ? "on" : "off");
info->finish(Device::DeviceErrorNoError);
return;
}
qCWarning(dcShelly()) << "Unhandled execute action call for device" << device;
}
void DevicePluginShelly::onClientConnected(MqttChannel *channel)
{
Device *device = m_mqttChannels.key(channel);
if (!device) {
qCWarning(dcShelly()) << "Received a client connect for a device we don't know!";
return;
}
device->setStateValue(shelly1ConnectedStateTypeId, true);
foreach (Device *child, myDevices().filterByParentDeviceId(device->id())) {
child->setStateValue(m_connectedStateTypesMap[child->deviceClassId()], true);
}
}
void DevicePluginShelly::onClientDisconnected(MqttChannel *channel)
{
Device *device = m_mqttChannels.key(channel);
if (!device) {
qCWarning(dcShelly()) << "Received a client disconnect for a device we don't know!";
return;
}
device->setStateValue(shelly1ConnectedStateTypeId, false);
foreach (Device *child, myDevices().filterByParentDeviceId(device->id())) {
child->setStateValue(m_connectedStateTypesMap[child->deviceClassId()], false);
}
}
void DevicePluginShelly::onPublishReceived(MqttChannel *channel, const QString &topic, const QByteArray &payload)
{
Device *device = m_mqttChannels.key(channel);
if (!device) {
qCWarning(dcShelly()) << "Received a publish message for a device we don't know!";
return;
}
QString shellyId = device->paramValue(shelly1DeviceIdParamTypeId).toString();
if (topic == "shellies/" + shellyId + "/input/0") {
// "1" or "0"
// Emit event button pressed
bool on = payload == "1";
foreach (Device *child, myDevices().filterByParentDeviceId(device->id())) {
if (child->deviceClassId() == shellySwitchDeviceClassId) {
if (child->stateValue(shellySwitchPowerStateTypeId).toBool() != on) {
child->setStateValue(shellySwitchPowerStateTypeId, on);
emit emitEvent(Event(shellySwitchPressedEventTypeId, child->id()));
}
}
}
}
if (topic == "shellies/" + shellyId + "/relay/0") {
bool on = payload == "on";
foreach (Device *child, myDevices().filterByParentDeviceId(device->id())) {
if (m_powerStateTypeMap.contains(child->deviceClassId())) {
child->setStateValue(m_powerStateTypeMap.value(child->deviceClassId()), on);
}
}
}
qCDebug(dcShelly()) << "Publish received from" << device->name() << topic << payload;
}
void DevicePluginShelly::setupShellyGateway(DeviceSetupInfo *info)
{
QString shellyId = info->device()->paramValue(shelly1DeviceIdParamTypeId).toString();
ZeroConfServiceEntry zeroConfEntry;
foreach (const ZeroConfServiceEntry &entry, m_zeroconfBrowser->serviceEntries()) {
if (entry.name() == shellyId) {
zeroConfEntry = entry;
}
}
QHostAddress address;
pluginStorage()->beginGroup(info->device()->id().toString());
if (zeroConfEntry.isValid()) {
address = zeroConfEntry.hostAddress().toString();
pluginStorage()->setValue("cachedAddress", address.toString());
} else {
qCWarning(dcShelly()) << "Could not find Shelly device on zeroconf. Trying cached address.";
address = pluginStorage()->value("cachedAddress").toString();
}
pluginStorage()->endGroup();
if (address.isNull()) {
qCWarning(dcShelly()) << "Unable to determine Shelly's network address. Failed to set up device.";
info->finish(Device::DeviceErrorHardwareNotAvailable, QT_TR_NOOP("Unable to find the device in the network."));
return;
}
MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(shellyId, QHostAddress(address), {"shellies"});
if (!channel) {
qCWarning(dcShelly()) << "Failed to create MQTT channel.";
return info->finish(Device::DeviceErrorHardwareFailure, QT_TR_NOOP("Error creating MQTT channel. Please check MQTT server settings."));
}
QUrl url;
url.setScheme("http");
url.setHost(address.toString());
url.setPort(80);
url.setPath("/settings");
url.setUserName(info->device()->paramValue(shelly1DeviceUsernameParamTypeId).toString());
url.setPassword(info->device()->paramValue(shelly1DevicePasswordParamTypeId).toString());
QUrlQuery query;
query.addQueryItem("mqtt_server", channel->serverAddress().toString() + ":" + QString::number(channel->serverPort()));
query.addQueryItem("mqtt_user", channel->username());
query.addQueryItem("mqtt_pass", channel->password());
query.addQueryItem("mqtt_enable", "true");
url.setQuery(query);
QNetworkRequest request(url);
qCDebug(dcShelly()) << "Connecting to" << url.toString();
QNetworkReply *reply = hardwareManager()->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(info, &DeviceSetupInfo::aborted, channel, [this, channel](){
hardwareManager()->mqttProvider()->releaseChannel(channel);
});
connect(reply, &QNetworkReply::finished, info, [this, info, reply, channel, address](){
if (reply->error() != QNetworkReply::NoError) {
hardwareManager()->mqttProvider()->releaseChannel(channel);
qCWarning(dcShelly()) << "Error fetching device settings" << reply->error() << reply->errorString();
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
info->finish(Device::DeviceErrorAuthenticationFailure, QT_TR_NOOP("Username and password not set correctly."));
} else {
info->finish(Device::DeviceErrorHardwareFailure, QT_TR_NOOP("Error connecting to Shelly device."));
}
return;
}
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(dcShelly()) << "Error parsing settings reply" << error.errorString() << "\n" << data;
info->finish(Device::DeviceErrorHardwareFailure, QT_TR_NOOP("Unexpected data received from Shelly device."));
hardwareManager()->mqttProvider()->releaseChannel(channel);
return;
}
qCDebug(dcShelly()) << "Settings data" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
m_mqttChannels.insert(info->device(), channel);
connect(channel, &MqttChannel::clientConnected, this, &DevicePluginShelly::onClientConnected);
connect(channel, &MqttChannel::clientDisconnected, this, &DevicePluginShelly::onClientDisconnected);
connect(channel, &MqttChannel::publishReceived, this, &DevicePluginShelly::onPublishReceived);
DeviceDescriptors autoChilds;
// Always create the switch device if we don't have one yet
if (myDevices().filterByParentDeviceId(info->device()->id()).filterByDeviceClassId(shellySwitchDeviceClassId).isEmpty()) {
DeviceDescriptor switchChild(shellySwitchDeviceClassId, "Shelly switch", QString(), info->device()->id());
autoChilds.append(switchChild);
}
// Add connected devices as configured in params
if (info->device()->paramValue(shelly1DeviceConnectedDeviceParamTypeId).toString() == "Generic") {
if (myDevices().filterByParentDeviceId(info->device()->id()).filterByDeviceClassId(shellyGenericDeviceClassId).isEmpty()) {
DeviceDescriptor genericChild(shellyGenericDeviceClassId, "Shelly connected device", QString(), info->device()->id());
autoChilds.append(genericChild);
}
}
if (info->device()->paramValue(shelly1DeviceConnectedDeviceParamTypeId).toString() == "Light") {
if (myDevices().filterByParentDeviceId(info->device()->id()).filterByDeviceClassId(shellyLightDeviceClassId).isEmpty()) {
DeviceDescriptor genericChild(shellyLightDeviceClassId, "Shelly connected light", QString(), info->device()->id());
autoChilds.append(genericChild);
}
}
info->finish(Device::DeviceErrorNoError);
emit autoDevicesAppeared(autoChilds);
// Make sure authentication is enalbed if the user wants it
QString username = info->device()->paramValue(shelly1DeviceUsernameParamTypeId).toString();
QString password = info->device()->paramValue(shelly1DevicePasswordParamTypeId).toString();
if (!username.isEmpty()) {
QUrl url;
url.setScheme("http");
url.setHost(address.toString());
url.setPort(80);
url.setPath("/settings/login");
url.setUserName(username);
url.setPassword(password);
QUrlQuery query;
query.addQueryItem("username", username);
query.addQueryItem("password", password);
query.addQueryItem("enabled", "true");
url.setQuery(query);
QNetworkRequest request(url);
qCDebug(dcShelly()) << "Enabling auth" << username << password;
QNetworkReply *reply = hardwareManager()->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
}
});
}
void DevicePluginShelly::setupShellyChild(DeviceSetupInfo *info)
{
Device *device = info->device();
// Connect to settings changes to store them to the device
connect(info->device(), &Device::settingChanged, this, [this, device](const ParamTypeId &paramTypeId, const QVariant &value){
Device *parentDevice = myDevices().findById(device->parentId());
pluginStorage()->beginGroup(parentDevice->id().toString());
QString address = pluginStorage()->value("cachedAddress").toString();
pluginStorage()->endGroup();
QUrl url;
url.setScheme("http");
url.setHost(address);
url.setPort(80);
url.setPath("/settings/relay/0");
url.setUserName(parentDevice->paramValue(shelly1DeviceUsernameParamTypeId).toString());
url.setPassword(parentDevice->paramValue(shelly1DevicePasswordParamTypeId).toString());
QUrlQuery query;
if (paramTypeId == shellySwitchSettingsButtonTypeParamTypeId) {
query.addQueryItem("btn_type", value.toString());
}
if (paramTypeId == shellySwitchSettingsInvertButtonParamTypeId) {
query.addQueryItem("btn_reverse", value.toBool() ? "1" : "0");
}
if (paramTypeId == shellyGenericSettingsDefaultStateParamTypeId || paramTypeId == shellyLightSettingsDefaultStateParamTypeId) {
query.addQueryItem("default_state", value.toString());
}
url.setQuery(query);
QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
});
info->finish(Device::DeviceErrorNoError);
}
QString DevicePluginShelly::getIP(Device *device) const
{
Device *d = device;
if (!device->parentId().isNull()) {
d = myDevices().findById(device->parentId());
}
pluginStorage()->beginGroup(d->id().toString());
QString ip = pluginStorage()->value("cachedAddress").toString();
pluginStorage()->endGroup();
return ip;
}

View File

@ -0,0 +1,70 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2019 Michael Zanetti <michael.zanetti@nymea.io> *
* *
* This file is part of nymea. *
* *
* nymea is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, version 2 of the License. *
* *
* nymea is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with nymea. If not, see <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef DEVICEPLUGINSHELLY_H
#define DEVICEPLUGINSHELLY_H
#include "devices/deviceplugin.h"
class ZeroConfServiceBrowser;
class MqttChannel;
class DevicePluginShelly: public DevicePlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "devicepluginshelly.json")
Q_INTERFACES(DevicePlugin)
public:
explicit DevicePluginShelly();
~DevicePluginShelly() override;
void init() override;
void discoverDevices(DeviceDiscoveryInfo *info) override;
void setupDevice(DeviceSetupInfo *info) override;
void deviceRemoved(Device *device) override;
void executeAction(DeviceActionInfo *info) override;
private slots:
void onClientConnected(MqttChannel* channel);
void onClientDisconnected(MqttChannel* channel);
void onPublishReceived(MqttChannel* channel, const QString &topic, const QByteArray &payload);
private:
void setupShellyGateway(DeviceSetupInfo *info);
void setupShellyChild(DeviceSetupInfo *info);
QString getIP(Device *device) const;
private:
ZeroConfServiceBrowser *m_zeroconfBrowser = nullptr;
QHash<Device*, MqttChannel*> m_mqttChannels;
QHash<DeviceClassId, StateTypeId> m_connectedStateTypesMap;
QHash<DeviceClassId, StateTypeId> m_powerStateTypeMap;
QHash<ActionTypeId, DeviceClassId> m_powerActionTypesMap;
QHash<ActionTypeId, ParamTypeId> m_powerActionParamTypesMap;
};
#endif // DEVICEPLUGINSHELLY_H

View File

@ -0,0 +1,196 @@
{
"name": "shelly",
"displayName": "Shelly",
"id": "6162773b-0435-408c-a4f8-7860d38031a9",
"vendors": [
{
"name": "shelly",
"displayName": "Shelly",
"id": "d8e45fc2-90af-492e-8305-50baa1ec4c18",
"deviceClasses": [
{
"id": "f810b66a-7177-4397-9771-4229abaabbb6",
"name": "shelly1",
"displayName": "Shelly1 / Shelly1PM",
"createMethods": ["discovery"],
"interfaces": [ "gateway" ],
"paramTypes": [
{
"id": "1d301dc0-5e48-473f-a611-8e407289e545",
"name":"id",
"displayName": "Shelly ID",
"type": "QString",
"readOnly": true
},
{
"id": "d0e0499e-faa0-432a-a760-c295b0aefed0",
"name": "connectedDevice",
"displayName": "Connected device",
"type": "QString",
"allowedValues": ["None", "Generic", "Light"],
"defaultValue": "Generic"
},
{
"id": "fa1aa0f6-93b2-410d-a2c5-7b2f45eae679",
"name": "username",
"displayName": "Username (optional)",
"type": "QString"
},
{
"id": "d29b8399-bfa6-4146-921d-a1d43ca4e184",
"name": "password",
"displayName": "Password (optional)",
"type": "QString"
}
],
"stateTypes": [
{
"id": "e5d41e05-2296-457e-97d8-98a5ac0de615",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"type": "bool",
"defaultValue": false,
"cached": false
}
],
"actionTypes": [
{
"id": "b4067d54-36c5-4d30-bbc3-c8c712d6fd32",
"name": "reboot",
"displayName": "Reboot"
}
]
},
{
"id": "6de35a17-0f54-4397-894d-4321b64c53d1",
"name": "shellySwitch",
"displayName": "Shelly switch",
"createMethods": ["auto"],
"interfaces": [ "powerswitch", "connectable"],
"settingsTypes": [
{
"id": "ce9f1650-5e12-40f4-97de-27af86afa40b",
"name": "buttonType",
"displayName": "Button type",
"allowedValues": ["momentary", "toggle", "edge", "detached"],
"type": "QString",
"defaultValue": "toggle"
},
{
"id": "f31eb52b-9aaf-409d-8bba-badda7c1a249",
"name": "invertButton",
"displayName": "Invert button",
"type": "bool",
"defaultValue": false
}
],
"stateTypes": [
{
"id": "0c233312-7b8f-4ca3-880d-523cab9b3ccb",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected or disconnected",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "20f74d88-0683-4d3a-9513-6b29b5112b7b",
"name": "power",
"displayName": "On/Off",
"displayNameEvent": "On/Off toggled",
"type": "bool",
"defaultValue": false
}
],
"eventTypes": [
{
"id": "41498655-1943-4b46-ac36-adea7bafab87",
"name": "pressed",
"displayName": "Pressed"
}
]
},
{
"id": "512c3c7d-d6a6-4d2a-bccd-83147e5f9a25",
"name": "shellyGeneric",
"displayName": "Shelly connected device",
"createMethods": ["auto"],
"interfaces": ["power", "connectable"],
"settingsTypes": [
{
"id": "7d35aea3-1444-48c8-9732-a41bfc3b9d75",
"name": "defaultState",
"displayName": "Default state",
"allowedValues": ["on", "off", "last", "switch"],
"defaultValue": "off",
"type": "QString"
}
],
"stateTypes": [
{
"id": "4a141674-faa6-4953-8272-5b4a4da84d31",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected or disconnected",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "72d7dbba-757c-4b03-a092-1d3f374fa961",
"name": "power",
"displayName": "Power",
"displayNameEvent": "Turned on or off",
"displayNameAction": "Turn on or off",
"type": "bool",
"defaultValue": false,
"writable": true
}
]
},
{
"id": "62a2d6b8-d70d-45fc-ba8c-1c680282a399",
"name": "shellyLight",
"displayName": "Shelly connected light",
"createMethods": ["auto"],
"interfaces": ["light", "connectable"],
"settingsTypes": [
{
"id": "4fe9ae31-3657-41bf-bd40-a219d58465d3",
"name": "defaultState",
"displayName": "Default state",
"allowedValues": ["on", "off", "last", "switch"],
"defaultValue": "off",
"type": "QString"
}
],
"stateTypes": [
{
"id": "61b7d8ac-d229-4268-8143-6edb2eca978d",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected or disconnected",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "2ee5bfab-271e-4b95-9464-122a5208f1a5",
"name": "power",
"displayName": "Power",
"displayNameEvent": "Turned on or off",
"displayNameAction": "Turn on or off",
"type": "bool",
"defaultValue": false,
"writable": true
}
]
}
]
}
]
}

9
shelly/shelly.pro Normal file
View File

@ -0,0 +1,9 @@
include(../plugins.pri)
QT += network
SOURCES += \
devicepluginshelly.cpp \
HEADERS += \
devicepluginshelly.h \

View File

@ -87,7 +87,7 @@ void DevicePluginTasmota::setupDevice(DeviceSetupInfo *info)
//: Error setting up device
return info->finish(Device::DeviceErrorInvalidParameter, QT_TR_NOOP("The given IP address is not valid."));
}
MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(device->id(), deviceAddress);
MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(device->id().toString().remove(QRegExp("[{}-]")), deviceAddress);
if (!channel) {
qCWarning(dcTasmota) << "Failed to create MQTT channel.";
//: Error setting up device
@ -103,7 +103,7 @@ void DevicePluginTasmota::setupDevice(DeviceSetupInfo *info)
configItems.insert("MqttUser", channel->username());
configItems.insert("MqttPassword", channel->password());
configItems.insert("Topic", "sonoff");
configItems.insert("FullTopic", channel->topicPrefix() + "/%topic%/");
configItems.insert("FullTopic", channel->topicPrefixList().first() + "/%topic%/");
QStringList configList;
foreach (const QString &key, configItems.keys()) {
@ -229,8 +229,8 @@ void DevicePluginTasmota::executeAction(DeviceActionInfo *info)
}
ParamTypeId channelParamTypeId = m_channelParamTypeMap.value(device->deviceClassId());
ParamTypeId powerActionParamTypeId = ParamTypeId(m_powerStateTypeMap.value(device->deviceClassId()).toString());
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(channelParamTypeId).toString() << (action.param(powerActionParamTypeId).value().toBool() ? "ON" : "OFF");
channel->publish(channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(channelParamTypeId).toString().toLower(), action.param(powerActionParamTypeId).value().toBool() ? "ON" : "OFF");
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(channelParamTypeId).toString() << (action.param(powerActionParamTypeId).value().toBool() ? "ON" : "OFF");
channel->publish(channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(channelParamTypeId).toString().toLower(), action.param(powerActionParamTypeId).value().toBool() ? "ON" : "OFF");
device->setStateValue(m_powerStateTypeMap.value(device->deviceClassId()), action.param(powerActionParamTypeId).value().toBool());
return info->finish(Device::DeviceErrorNoError);
}
@ -244,20 +244,20 @@ void DevicePluginTasmota::executeAction(DeviceActionInfo *info)
ParamTypeId openingChannelParamTypeId = m_openingChannelParamTypeMap.value(device->deviceClassId());
ParamTypeId closingChannelParamTypeId = m_closingChannelParamTypeMap.value(device->deviceClassId());
if (action.actionTypeId() == tasmotaShutterOpenActionTypeId) {
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString() << "OFF";
channel->publish(channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString().toLower(), "OFF");
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString() << "ON";
channel->publish(channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString().toLower(), "ON");
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString() << "OFF";
channel->publish(channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString().toLower(), "OFF");
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString() << "ON";
channel->publish(channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString().toLower(), "ON");
} else if (action.actionTypeId() == tasmotaShutterCloseActionTypeId) {
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString() << "OFF";
channel->publish(channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString().toLower(), "OFF");
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString() << "ON";
channel->publish(channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString().toLower(), "ON");
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString() << "OFF";
channel->publish(channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString().toLower(), "OFF");
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString() << "ON";
channel->publish(channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString().toLower(), "ON");
} else { // Stop
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString() << "OFF";
channel->publish(channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString().toLower(), "OFF");
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString() << "OFF";
channel->publish(channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString().toLower(), "OFF");
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString() << "OFF";
channel->publish(channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(openingChannelParamTypeId).toString().toLower(), "OFF");
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString() << "OFF";
channel->publish(channel->topicPrefixList().first() + "/sonoff/cmnd/" + device->paramValue(closingChannelParamTypeId).toString().toLower(), "OFF");
}
return info->finish(Device::DeviceErrorNoError);
}
@ -295,7 +295,7 @@ void DevicePluginTasmota::onPublishReceived(MqttChannel *channel, const QString
qCDebug(dcTasmota) << "Publish received from Sonoff device:" << topic << payload;
Device *dev = m_mqttChannels.key(channel);
if (m_ipAddressParamTypeMap.contains(dev->deviceClassId())) {
if (topic.startsWith(channel->topicPrefix() + "/sonoff/POWER")) {
if (topic.startsWith(channel->topicPrefixList().first() + "/sonoff/POWER")) {
QString channelName = topic.split("/").last();
foreach (Device *child, myDevices()) {
@ -314,7 +314,7 @@ void DevicePluginTasmota::onPublishReceived(MqttChannel *channel, const QString
}
}
}
if (topic.startsWith(channel->topicPrefix() + "/sonoff/STATE")) {
if (topic.startsWith(channel->topicPrefixList().first() + "/sonoff/STATE")) {
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(payload, &error);
if (error.error != QJsonParseError::NoError) {