powersync-plugins/espsomfyrts/integrationpluginespsomfyrt...

441 lines
19 KiB
C++

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2024, 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 <https://www.gnu.org/licenses/>.
*
* 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 "integrationpluginespsomfyrts.h"
#include <math.h>
#include <QUrlQuery>
#include <QHostAddress>
#include <QDataStream>
#include <QJsonDocument>
#include <QJsonParseError>
#include "plugininfo.h"
#include "espsomfyrtsdiscovery.h"
IntegrationPluginEspSomfyRts::IntegrationPluginEspSomfyRts()
{
}
void IntegrationPluginEspSomfyRts::init()
{
}
void IntegrationPluginEspSomfyRts::discoverThings(ThingDiscoveryInfo *info)
{
if (!hardwareManager()->networkDeviceDiscovery()->available()) {
qCWarning(dcESPSomfyRTS()) << "Failed to discover network devices. The network device discovery is not available.";
info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Unable to discover devices in your network."));
return;
}
qCInfo(dcESPSomfyRTS()) << "Starting network discovery...";
EspSomfyRtsDiscovery *discovery = new EspSomfyRtsDiscovery(hardwareManager()->networkManager(), hardwareManager()->networkDeviceDiscovery(), info);
connect(discovery, &EspSomfyRtsDiscovery::discoveryFinished, info, [=](){
ThingDescriptors descriptors;
qCInfo(dcESPSomfyRTS()) << "Discovery finished. Found" << discovery->results().count() << "devices";
foreach (const EspSomfyRtsDiscovery::Result &result, discovery->results()) {
qCInfo(dcESPSomfyRTS()) << "Discovered device on" << result.networkDeviceInfo;
QString title = "ESP Somfy RTS (" + result.name + ")";
QString description = result.networkDeviceInfo.address().toString();
ThingDescriptor descriptor(espSomfyRtsThingClassId, title, description);
ParamList params;
params << Param(espSomfyRtsThingMacAddressParamTypeId, result.networkDeviceInfo.thingParamValueMacAddress());
params << Param(espSomfyRtsThingHostNameParamTypeId, result.networkDeviceInfo.thingParamValueHostName());
params << Param(espSomfyRtsThingAddressParamTypeId, result.networkDeviceInfo.thingParamValueAddress());
descriptor.setParams(params);
// Check if we already have set up this device
Thing *existingThing = myThings().findByParams(params);
if (existingThing) {
qCDebug(dcESPSomfyRTS()) << "This thing already exists in the system:" << result.networkDeviceInfo;
descriptor.setThingId(existingThing->id());
}
info->addThingDescriptor(descriptor);
}
info->finish(Thing::ThingErrorNoError);
});
discovery->startDiscovery();
}
void IntegrationPluginEspSomfyRts::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
if (thing->thingClassId() == espSomfyRtsThingClassId) {
if (!hardwareManager()->networkDeviceDiscovery()->available()) {
qCWarning(dcESPSomfyRTS()) << "Cannot set up thing because the network discovery is not available.";
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing);
if (!monitor) {
qCWarning(dcESPSomfyRTS()) << "Could not register monitor with the given parameters" << thing << thing->params();
info->finish(Thing::ThingErrorInvalidParameter);
return;
}
EspSomfyRts *somfy = new EspSomfyRts(monitor, thing);
m_somfys.insert(thing, somfy);
connect(somfy, &EspSomfyRts::connectedChanged, thing, [this, thing](bool connected){
onEspSomfyConnectedChanged(thing, connected);
});
connect(somfy, &EspSomfyRts::signalStrengthChanged, thing, [thing](uint signalStrength){
thing->setStateValue(espSomfyRtsSignalStrengthStateTypeId, signalStrength);
});
connect(somfy, &EspSomfyRts::firmwareVersionChanged, thing, [thing](const QString &firmwareVersion){
thing->setStateValue(espSomfyRtsFirmwareVersionStateTypeId, firmwareVersion);
});
connect(somfy, &EspSomfyRts::shadeStateReceived, thing, [this](const QVariantMap &shadeState){
int shadeId = shadeState.value("shadeId").toInt();
if (m_shadeThings.contains(shadeId)) {
processShadeState(m_shadeThings.value(shadeId), shadeState);
}
});
info->finish(Thing::ThingErrorNoError);
return;
} else {
qCDebug(dcESPSomfyRTS()) << "Setting up" << thing->thingClass().name() << thing->name();
m_shadeThings.insert(thing->paramValue("shadeId").toUInt(), thing);
info->finish(Thing::ThingErrorNoError);
}
}
void IntegrationPluginEspSomfyRts::postSetupThing(Thing *thing)
{
if (thing->thingClassId() == espSomfyRtsThingClassId) {
EspSomfyRts *somfy = m_somfys.value(thing);
onEspSomfyConnectedChanged(thing, somfy->connected());
if (!m_refreshTimer) {
m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(60);
connect(m_refreshTimer, &PluginTimer::timeout, thing, [this, thing](){
if (m_somfys.value(thing)->connected()) {
synchronizeShades(thing);
}
});
}
} else {
Thing *parent = myThings().findById(thing->parentId());
EspSomfyRts *somfy = m_somfys.value(parent);
if (!parent || !somfy)
return;
thing->setStateValue("connected", somfy->connected());
}
}
void IntegrationPluginEspSomfyRts::thingRemoved(Thing *thing)
{
if (thing->thingClassId() == espSomfyRtsThingClassId) {
EspSomfyRts *somfy = m_somfys.take(thing);
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(somfy->monitor());
}
}
void IntegrationPluginEspSomfyRts::executeAction(ThingActionInfo *info)
{
Thing *thing = info->thing();
Action action = info->action();
if (thing->thingClassId() == awningThingClassId) {
if (!thing->stateValue(awningConnectedStateTypeId).toBool()) {
qCWarning(dcESPSomfyRTS()) << "Could not execute command because the thing is not connected" << thing;
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
Thing *parentThing = myThings().findById(thing->parentId());
EspSomfyRts *somfy = m_somfys.value(parentThing);
if (!parentThing || !somfy) {
qCWarning(dcESPSomfyRTS()) << "Could not execute command because the parent thing could not be found for" << thing;
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
QVariantMap requestMap;
requestMap.insert("shadeId", thing->paramValue(awningThingShadeIdParamTypeId).toUInt());
if (action.actionTypeId() == awningOpenActionTypeId) {
requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandDown));
} else if (action.actionTypeId() == awningStopActionTypeId) {
requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandMy));
} else if (action.actionTypeId() == awningCloseActionTypeId) {
requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandUp));
} else if (action.actionTypeId() == awningPercentageActionTypeId) {
requestMap.insert("target", action.paramValue(awningPercentageActionPercentageParamTypeId).toUInt());
}
QNetworkRequest request(somfy->shadeCommandUrl());
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = hardwareManager()->networkManager()->put(request, QJsonDocument::fromVariant(requestMap).toJson());
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [reply, info](){
if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcESPSomfyRTS()) << "Could not execute command on" << info->thing() << "because the network request finished with error" << reply->errorString();
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
qCDebug(dcESPSomfyRTS()) << "Executed command successfully on" << info->thing();
info->finish(Thing::ThingErrorNoError);
});
return;
}
if (thing->thingClassId() == venetianBlindThingClassId) {
if (!thing->stateValue(venetianBlindConnectedStateTypeId).toBool()) {
qCWarning(dcESPSomfyRTS()) << "Could not execute command because the thing is not connected" << thing;
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
Thing *parentThing = myThings().findById(thing->parentId());
EspSomfyRts *somfy = m_somfys.value(parentThing);
if (!parentThing || !somfy) {
qCWarning(dcESPSomfyRTS()) << "Could not execute command because the parent thing could not be found for" << thing;
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
QVariantMap requestMap;
requestMap.insert("shadeId", thing->paramValue(venetianBlindThingShadeIdParamTypeId).toUInt());
QUrl url = somfy->shadeCommandUrl();
if (action.actionTypeId() == venetianBlindOpenActionTypeId) {
requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandUp));
} else if (action.actionTypeId() == venetianBlindStopActionTypeId) {
requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandMy));
} else if (action.actionTypeId() == venetianBlindCloseActionTypeId) {
requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandDown));
} else if (action.actionTypeId() == venetianBlindPercentageActionTypeId) {
requestMap.insert("target", action.paramValue(venetianBlindPercentageActionPercentageParamTypeId).toUInt());
} else if (action.actionTypeId() == venetianBlindAngleActionTypeId) {
url = somfy->tiltCommandUrl();
State angleState = thing->state(venetianBlindAngleStateTypeId);
int minValue = angleState.minValue().toInt();
int maxValue = angleState.maxValue().toInt();
int angle = action.paramValue(venetianBlindAngleActionAngleParamTypeId).toInt();
int percentage = calculatePercentageFromAngle(minValue, maxValue, angle);
qCDebug(dcESPSomfyRTS()) << "######" << percentage;
requestMap.insert("target", percentage);
}
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
qCDebug(dcESPSomfyRTS()) << "PUT" << url.toString() << qUtf8Printable(QJsonDocument::fromVariant(requestMap).toJson(QJsonDocument::Compact));
QNetworkReply *reply = hardwareManager()->networkManager()->put(request, QJsonDocument::fromVariant(requestMap).toJson(QJsonDocument::Compact));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [reply, info](){
if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcESPSomfyRTS()) << "Could not execute command on" << info->thing() << "because the network request finished with error" << reply->errorString();
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
qCDebug(dcESPSomfyRTS()) << "Executed command successfully on" << info->thing();
info->finish(Thing::ThingErrorNoError);
});
}
}
int IntegrationPluginEspSomfyRts::calculateAngleFromPercentage(int minAngle, int maxAngle, int percentage)
{
int minValue = qMin(minAngle, maxAngle);
int maxValue = qMax(minAngle, maxAngle);
int range = maxValue - minValue;
int angle = std::round(range * percentage / 100.0) + minAngle;
//qCDebug(dcESPSomfyRTS()) << "Calculate angle" << angle << "for percentage" << percentage << "min:" << minValue << "max:" << maxValue << "range:" << range;
return angle;
}
int IntegrationPluginEspSomfyRts::calculatePercentageFromAngle(int minAngle, int maxAngle, int angle)
{
int minValue = qMin(minAngle, maxAngle);
int maxValue = qMax(minAngle, maxAngle);
int range = maxValue - minValue;
int percentage = std::round(angle * 100.0 / range) + 50;
//qCDebug(dcESPSomfyRTS()) << "Calculated percentage" << percentage << "for angle" << angle << "min:" << minValue << "max:" << maxValue << "range:" << range;
// FIXME: check the percentage of the negative part if asymetric
return percentage;
}
void IntegrationPluginEspSomfyRts::createThingForShade(const QVariantMap &shadeMap, const ThingId &parentThingId)
{
QString shadeName = shadeMap.value("name").toString();
uint shadeId = shadeMap.value("shadeId").toUInt();
EspSomfyRts::ShadeType shadeType = static_cast<EspSomfyRts::ShadeType>(shadeMap.value("shadeType").toInt());
qCDebug(dcESPSomfyRTS()) << "Creating thing for" << shadeType << shadeId << shadeName;
ThingDescriptor desciptor;
ThingDescriptors desciptors;
switch (shadeType) {
case EspSomfyRts::ShadeTypeAwning:
desciptor = ThingDescriptor(awningThingClassId, shadeName);
desciptor.setParams(ParamList() << Param(awningThingShadeIdParamTypeId, shadeId));
desciptor.setParentId(parentThingId);
desciptors.append(desciptor);
break;
case EspSomfyRts::ShadeTypeBlind:
desciptor = ThingDescriptor(venetianBlindThingClassId, shadeName);
desciptor.setParams(ParamList() << Param(venetianBlindThingShadeIdParamTypeId, shadeId));
desciptor.setParentId(parentThingId);
desciptors.append(desciptor);
break;
default:
break;
}
if (desciptors.isEmpty())
return;
emit autoThingsAppeared(desciptors);
}
void IntegrationPluginEspSomfyRts::processShadeState(Thing *thing, const QVariantMap &shadeState)
{
if (thing->thingClassId() == awningThingClassId) {
if (shadeState.contains("position"))
thing->setStateValue(awningPercentageStateTypeId, shadeState.value("position").toInt());
if (shadeState.contains("direction"))
thing->setStateValue(awningMovingStateTypeId, shadeState.value("direction").toInt() != EspSomfyRts::MovingDirectionRest);
return;
}
if (thing->thingClassId() == venetianBlindThingClassId) {
if (shadeState.contains("position"))
thing->setStateValue(venetianBlindPercentageStateTypeId, shadeState.value("position").toInt());
if (shadeState.contains("direction"))
thing->setStateValue(venetianBlindMovingStateTypeId, shadeState.value("direction").toInt() != EspSomfyRts::MovingDirectionRest);
State angleState = thing->state(venetianBlindAngleStateTypeId);
int angle = calculateAngleFromPercentage(angleState.minValue().toInt(), angleState.maxValue().toInt(), shadeState.value("tiltPosition").toInt());
thing->setStateValue(venetianBlindAngleStateTypeId, angle);
return;
}
}
void IntegrationPluginEspSomfyRts::synchronizeShades(Thing *thing)
{
EspSomfyRts *somfy = m_somfys.value(thing);
qCDebug(dcESPSomfyRTS()) << "Synchronize shades of" << thing->name() << somfy->address().toString();
QUrl url = somfy->shadesUrl();
QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, thing, [this, reply, thing](){
if (reply->error() != QNetworkReply::NoError) {
qCDebug(dcESPSomfyRTS()) << "Get shades reply finished with error" << reply->errorString();
return;
}
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
qCWarning(dcESPSomfyRTS()) << "Get shades reply contains invalid JSON data" << jsonError.errorString();
return;
}
QList<ThingId> handledThingIds;
QVariantList shadesList = jsonDoc.toVariant().toList();
// Get shades we need to add
QList<QVariantMap> shadesToCreateThingFor;
foreach (const QVariant &shadeVariant, shadesList) {
QVariantMap shadeMap = shadeVariant.toMap();
// Check if we have a thing for this shade ID
uint shadeId = shadeMap.value("shadeId").toUInt();
if (!m_shadeThings.contains(shadeId)) {
shadesToCreateThingFor.append(shadeMap);
} else {
// We already have a shade for this map, let's update the states
processShadeState(m_shadeThings.value(shadeId), shadeMap);
handledThingIds.append(m_shadeThings.value(shadeId)->id());
}
// TODO: check if a shade has changed the type, in that case,
// remove the old one and recreate a new one with the matching thing class
}
// Remove things if shade does not exist any more
foreach (Thing *existingThing, myThings().filterByParentId(thing->id())) {
if (!handledThingIds.contains(existingThing->id())) {
qCDebug(dcESPSomfyRTS()) << "Removing thing" << existingThing << "because the shade with ID" << existingThing->paramValue("shadeId").toUInt() << "does not exist any more on the ESP Somfy RTS.";
emit autoThingDisappeared(existingThing->id());
}
}
// Add things for shades new shades
foreach (const QVariantMap &shadeMap, shadesToCreateThingFor) {
createThingForShade(shadeMap, thing->id());
}
});
}
void IntegrationPluginEspSomfyRts::onEspSomfyConnectedChanged(Thing *thing, bool connected)
{
thing->setStateValue(espSomfyRtsConnectedStateTypeId, connected);
foreach(Thing *childThing, myThings().filterByParentId(thing->id())) {
childThing->setStateValue("connected", connected);
}
if (connected) {
synchronizeShades(thing);
}
}