/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2019 Michael Zanetti * * * * 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 * * . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "devicepluginshelly.h" #include "plugininfo.h" #include #include #include #include #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 ¶mTypeId, 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; }