// SPDX-License-Identifier: GPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-plugins. * * nymea-plugins 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, either version 3 of the License, or * (at your option) any later version. * * nymea-plugins 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-plugins. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "integrationpluginopenccu.h" #include "plugininfo.h" #include #include #include #include #include IntegrationPluginOpenCCU::IntegrationPluginOpenCCU() { } void IntegrationPluginOpenCCU::init() { } void IntegrationPluginOpenCCU::discoverThings(ThingDiscoveryInfo *info) { info->finish(Thing::ThingErrorNoError); } void IntegrationPluginOpenCCU::setupThing(ThingSetupInfo *info) { if (info->thing()->thingClassId() == thermostatThingClassId) { m_thermostats.insert(info->thing(), Thermostat()); } info->finish(Thing::ThingErrorNoError); } void IntegrationPluginOpenCCU::postSetupThing(Thing *thing) { if (!m_pluginTimer) { m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); connect(m_pluginTimer, &PluginTimer::timeout, this, [this](){ // Refresh thermostats foreach (Thing *gatewayThing, myThings().filterByThingClassId(openCCUThingClassId)) { qCDebug(dcOpenCCU()) << "Refresh" << gatewayThing; getStateList(gatewayThing); } }); m_pluginTimer->start(); } if (thing->thingClassId() == openCCUThingClassId) { // Sync devices getDevices(thing); } else if (thing->thingClassId() == floorHeatingControllerThingClassId) { // Sync channels getChannels(thing); } } void IntegrationPluginOpenCCU::thingRemoved(Thing *thing) { if (m_thermostats.contains(thing)) m_thermostats.remove(thing); if (myThings().isEmpty() && m_pluginTimer) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); m_pluginTimer = nullptr; } } void IntegrationPluginOpenCCU::executeAction(ThingActionInfo *info) { if (info->thing()->thingClassId() == thermostatThingClassId) { // Get the parent thing for the URL Thing *gateway = gatewayThing(info->thing()); if (!gateway) { info->finish(Thing::ThingErrorHardwareNotAvailable); return; } QUrlQuery query; if (info->action().actionTypeId() == thermostatModeStateTypeId) { int iseId = m_thermostats.value(info->thing()).channels.value(1).controlModeId; if (iseId < 0) { // A device which has to be controlled using the mode id, not the control mode id... iseId = m_thermostats.value(info->thing()).channels.value(1).modeId; } if (iseId < 0) { qCWarning(dcOpenCCU()) << "The ise ID of the mode is not known yet."; info->finish(Thing::ThingErrorHardwareNotAvailable); return; } query.addQueryItem("ise_id", QString::number(iseId)); if (info->action().paramValue(thermostatModeActionModeParamTypeId).toString() == "Auto") { query.addQueryItem("new_value", QString::number(0)); } else { query.addQueryItem("new_value", QString::number(1)); } QUrl requestUrl = buildUrl(gateway, "statechange.cgi", query); qCDebug(dcOpenCCU()) << "GET statechange.cgi for thermostat mode"; QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(requestUrl)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::sslErrors, this, &IntegrationPluginOpenCCU::onSslError); connect(reply, &QNetworkReply::finished, this, [this, info, reply, gateway](){ if (reply->error() != QNetworkReply::NoError) { qCDebug(dcOpenCCU()) << "Execute action finished with error" << reply->errorString(); setGatewayConnected(gateway, false); info->finish(Thing::ThingErrorHardwareFailure); return; } setGatewayConnected(gateway, true); qCDebug(dcOpenCCU()) << "Execute action finished successfully" << reply->readAll(); info->thing()->setStateValue(thermostatModeStateTypeId, info->action().paramValue(thermostatModeActionModeParamTypeId).toString()); info->finish(Thing::ThingErrorNoError); }); } else if (info->action().actionTypeId() == thermostatTargetTemperatureActionTypeId) { int iseId = m_thermostats.value(info->thing()).channels.value(1).targetTemperatureId; if (iseId < 0) { qCWarning(dcOpenCCU()) << "The ise ID of the target temperature is not known yet."; info->finish(Thing::ThingErrorHardwareNotAvailable); return; } double value = info->action().paramValue(thermostatTargetTemperatureActionTargetTemperatureParamTypeId).toDouble(); value = qRound(value * 2) / 2.0; qCDebug(dcOpenCCU()) << "Setting target temperature of" << info->thing()->name() << "to" << value << "°C"; query.addQueryItem("ise_id", QString::number(iseId)); query.addQueryItem("new_value", QString::number(value)); QUrl requestUrl = buildUrl(gateway, "statechange.cgi", query); qCDebug(dcOpenCCU()) << "GET statechange.cgi for thermostat target temperature"; QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(requestUrl)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::sslErrors, this, &IntegrationPluginOpenCCU::onSslError); connect(reply, &QNetworkReply::finished, this, [this, info, reply, value, gateway](){ if (reply->error() != QNetworkReply::NoError) { qCDebug(dcOpenCCU()) << "Execute action finished with error" << reply->errorString(); setGatewayConnected(gateway, false); info->finish(Thing::ThingErrorHardwareFailure); return; } setGatewayConnected(gateway, true); qCDebug(dcOpenCCU()) << "Execute action finished successfully" << reply->readAll(); info->thing()->setStateValue(thermostatTargetTemperatureStateTypeId, value); info->finish(Thing::ThingErrorNoError); }); } else if (info->action().actionTypeId() == thermostatPowerActionTypeId) { // Note: this has been implemented just to match the interface, this actually does nothing on this thermostats info->finish(Thing::ThingErrorNoError); return; } } else if (info->thing()->thingClassId() == floorHeatingValveThingClassId) { if (info->action().actionTypeId() != floorHeatingValvePercentageActionTypeId) { info->finish(Thing::ThingErrorUnsupportedFeature); return; } Thing *gateway = gatewayThing(info->thing()); if (!gateway) { info->finish(Thing::ThingErrorHardwareNotAvailable); return; } int percentage = info->action().paramValue(floorHeatingValvePercentageActionPercentageParamTypeId).toInt(); percentage = qBound(0, percentage, 100); QUrlQuery query; query.addQueryItem("ise_id", info->thing()->paramValue(floorHeatingValveThingIseIdParamTypeId).toString()); query.addQueryItem("new_value", QString::number(percentage / 100.0, 'f', 6)); QUrl requestUrl = buildUrl(gateway, "statechange.cgi", query); qCDebug(dcOpenCCU()) << "GET statechange.cgi for floor heating valve percentage"; QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(requestUrl)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::sslErrors, this, &IntegrationPluginOpenCCU::onSslError); connect(reply, &QNetworkReply::finished, this, [this, info, reply, percentage, gateway](){ if (reply->error() != QNetworkReply::NoError) { qCDebug(dcOpenCCU()) << "Execute action finished with error" << reply->errorString(); setGatewayConnected(gateway, false); info->finish(Thing::ThingErrorHardwareFailure); return; } setGatewayConnected(gateway, true); qCDebug(dcOpenCCU()) << "Execute action finished successfully" << reply->readAll(); info->thing()->setStateValue(floorHeatingValveConnectedStateTypeId, true); info->thing()->setStateValue(floorHeatingValvePercentageStateTypeId, percentage); info->finish(Thing::ThingErrorNoError); }); } } void IntegrationPluginOpenCCU::getDevices(Thing *thing) { QUrl url = buildUrl(thing, "devicelist.cgi"); QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::sslErrors, this, &IntegrationPluginOpenCCU::onSslError); connect(reply, &QNetworkReply::finished, this, [this, thing, reply](){ if (reply->error() != QNetworkReply::NoError) { qCDebug(dcOpenCCU()) << "Reply finished with error" << reply->errorString(); setGatewayConnected(thing, false); return; } setGatewayConnected(thing, true); QByteArray data = reply->readAll(); //qCDebug(dcOpenCCU()) << "-->" << data; ThingDescriptors descriptors; QString deviceName; QXmlStreamReader xml(data); while(!xml.atEnd() && !xml.hasError()) { xml.readNext(); if (xml.name() == QString("device") && xml.isStartElement()) { qCDebug(dcOpenCCU()) << "-->" << xml.name() << deviceName; foreach (const QXmlStreamAttribute &attribute, xml.attributes()) { qCDebug(dcOpenCCU()) << " " << attribute.name() << attribute.value(); } deviceName = xml.attributes().value("name").toString(); QString deviceType = xml.attributes().value("device_type").toString(); int iseId = xml.attributes().value("ise_id").toInt(); // Thermostats if (deviceType.startsWith("HmIP-STH")) { QString serialNumber = xml.attributes().value("address").toString(); if (myThings().filterByParam(thermostatThingSerialNumberParamTypeId, serialNumber).isEmpty()) { qCDebug(dcOpenCCU()) << "Adding new" << deviceType; ThingDescriptor descriptor(thermostatThingClassId, deviceName, deviceType + " - " + serialNumber, thing->id()); ParamList params; params.append(Param(thermostatThingSerialNumberParamTypeId, serialNumber)); params.append(Param(thermostatThingTypeParamTypeId, deviceType)); params.append(Param(thermostatThingIseIdParamTypeId, iseId)); descriptor.setParams(params); descriptors.append(descriptor); } else { qCDebug(dcOpenCCU()) << "Thing for" << deviceType << serialNumber << "already created."; } } // Floor heating controller // C-8 or C-12 if (deviceType.startsWith("HmIP-FALMOT-C")) { QString serialNumber = xml.attributes().value("address").toString(); if (myThings().filterByParam(floorHeatingControllerThingSerialNumberParamTypeId, serialNumber).isEmpty()) { qCDebug(dcOpenCCU()) << "Adding new" << deviceType; ThingDescriptor descriptor(floorHeatingControllerThingClassId, deviceName, deviceType + " - " + serialNumber, thing->id()); ParamList params; params.append(Param(floorHeatingControllerThingSerialNumberParamTypeId, serialNumber)); params.append(Param(floorHeatingControllerThingTypeParamTypeId, deviceType)); params.append(Param(floorHeatingControllerThingIseIdParamTypeId, iseId)); descriptor.setParams(params); descriptors.append(descriptor); } else { qCDebug(dcOpenCCU()) << "Thing for" << deviceType << serialNumber << "already created."; } } } } if (xml.hasError()) { qCWarning(dcOpenCCU()) << "Could not parse devicelist response:" << xml.errorString(); setGatewayConnected(thing, false); return; } if (!descriptors.isEmpty()) { emit autoThingsAppeared(descriptors); } getStateList(thing); }); } void IntegrationPluginOpenCCU::getDeviceTypeList(Thing *thing) { QUrl url = buildUrl(thing, "devicetypelist.cgi"); QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::sslErrors, this, &IntegrationPluginOpenCCU::onSslError); connect(reply, &QNetworkReply::finished, this, [this, thing, reply](){ if (reply->error() != QNetworkReply::NoError) { qCDebug(dcOpenCCU()) << "Reply finished with error" << reply->errorString(); setGatewayConnected(thing, false); return; } QByteArray data = reply->readAll(); //qCDebug(dcOpenCCU()) << "-->" << data; setGatewayConnected(thing, true); QXmlStreamReader xml(data); while(!xml.atEnd() && !xml.hasError()) { xml.readNext(); if (xml.name() == QString("deviceType")) { qCDebug(dcOpenCCU()) << "-->" << xml.name() << xml.text(); foreach (const QXmlStreamAttribute &attribute, xml.attributes()) { qCDebug(dcOpenCCU()) << " " << attribute.name() << attribute.value(); } } } }); } void IntegrationPluginOpenCCU::getState(Thing *thing) { QUrl url = buildUrl(thing, "state.cgi"); QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::sslErrors, this, &IntegrationPluginOpenCCU::onSslError); connect(reply, &QNetworkReply::finished, this, [this, thing, reply](){ if (reply->error() != QNetworkReply::NoError) { qCDebug(dcOpenCCU()) << "Reply finished with error" << reply->errorString(); setGatewayConnected(thing, false); return; } QByteArray data = reply->readAll(); //qCDebug(dcOpenCCU()) << "-->" << data; setGatewayConnected(thing, true); QXmlStreamReader xml(data); while(!xml.atEnd() && !xml.hasError()) { xml.readNext(); if (xml.name() == QString("device")) { qCDebug(dcOpenCCU()) << "-->" << xml.name() << xml.text(); foreach (const QXmlStreamAttribute &attribute, xml.attributes()) { qCDebug(dcOpenCCU()) << " " << attribute.name() << attribute.value(); } } } }); } void IntegrationPluginOpenCCU::getStateList(Thing *thing) { // filtering if ise_id is not working with API version 2.3 // QUrlQuery query; // if (iseId >= 0) { // query.addQueryItem("ise_id", QString::number(iseId)); // } QUrl url = buildUrl(thing, "statelist.cgi"); qCDebug(dcOpenCCU()) << "GET statelist.cgi"; QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::sslErrors, this, &IntegrationPluginOpenCCU::onSslError); connect(reply, &QNetworkReply::finished, this, [this, thing, reply](){ if (reply->error() != QNetworkReply::NoError) { qCDebug(dcOpenCCU()) << "Reply finished with error" << reply->errorString(); setGatewayConnected(thing, false); return; } QByteArray data = reply->readAll(); //qCDebug(dcOpenCCU()) << "-->" << data; setGatewayConnected(thing, true); QXmlStreamReader xml(data); while(!xml.atEnd() && !xml.hasError()) { xml.readNext(); if (xml.name() == QString("device") && xml.isStartElement()) { qCDebug(dcOpenCCU()) << "-->" << xml.name() << xml.attributes().value("name").toString(); foreach (const QXmlStreamAttribute &attribute, xml.attributes()) { qCDebug(dcOpenCCU()) << " " << attribute.name() << attribute.value(); } int iseId = xml.attributes().value("ise_id").toInt(); foreach (Thing *childThing, myThings().filterByParentId(thing->id())) { if (childThing->paramValue("iseId").toInt() == iseId) { qCDebug(dcOpenCCU()) << "Updating states of" << childThing; processThingStateList(&xml, childThing); } } } } if (xml.hasError()) { qCWarning(dcOpenCCU()) << "Could not parse statelist response:" << xml.errorString(); setGatewayConnected(thing, false); } }); } void IntegrationPluginOpenCCU::getChannels(Thing *floorHeatinController) { qCDebug(dcOpenCCU()) << "Sync channels from" << floorHeatinController; Thing *gateway = gatewayThing(floorHeatinController); if (!gateway) { setFloorHeatingControllerConnected(floorHeatinController, false); return; } QUrl url = buildUrl(gateway, "statelist.cgi"); qCDebug(dcOpenCCU()) << "GET statelist.cgi"; QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::sslErrors, this, &IntegrationPluginOpenCCU::onSslError); connect(reply, &QNetworkReply::finished, this, [this, floorHeatinController, gateway, reply](){ if (reply->error() != QNetworkReply::NoError) { qCDebug(dcOpenCCU()) << "Reply finished with error" << reply->errorString(); setGatewayConnected(gateway, false); setFloorHeatingControllerConnected(floorHeatinController, false); return; } QByteArray data = reply->readAll(); //qCDebug(dcOpenCCU()) << "-->" << qUtf8Printable(data); setGatewayConnected(gateway, true); setFloorHeatingControllerConnected(floorHeatinController, true); int floorHeatingControllerIseId = floorHeatinController->paramValue(floorHeatingControllerThingIseIdParamTypeId).toInt(); QXmlStreamReader xml(data); while(!xml.atEnd() && !xml.hasError()) { xml.readNext(); if (xml.name() == QString("device") && xml.isStartElement()) { qCDebug(dcOpenCCU()) << "-->" << xml.name() << xml.attributes().value("name").toString(); foreach (const QXmlStreamAttribute &attribute, xml.attributes()) { qCDebug(dcOpenCCU()) << " " << attribute.name() << attribute.value(); } int iseId = xml.attributes().value("ise_id").toInt(); if (iseId == floorHeatingControllerIseId) { int currentChannelIndex = 0; ThingDescriptors desciptors; // Read all channels of this device and verify if there is a valve connected and if we have already set up device for it while (!xml.atEnd() && !xml.hasError() && !(xml.name() == QString("device") && xml.isEndElement())) { xml.readNext(); if (xml.name() == QString("channel") && xml.isStartElement()) { // Channel 0 = Maintainance channel currentChannelIndex = xml.attributes().value("index").toInt(); // qCDebug(dcOpenCCU()) << "-->" << xml->name() << xml->attributes().value("name").toString(); // foreach (const QXmlStreamAttribute &attribute, xml->attributes()) { // qCDebug(dcOpenCCU()) << " " << attribute.name() << attribute.value(); // } } if (xml.name() == QString("datapoint") && xml.isStartElement()) { qCDebug(dcOpenCCU()) << " -->" << xml.name() << xml.attributes().value("name").toString(); foreach (const QXmlStreamAttribute &attribute, xml.attributes()) { qCDebug(dcOpenCCU()) << " " << attribute.name() << attribute.value(); } QString type = xml.attributes().value("type").toString(); int iseId = xml.attributes().value("ise_id").toInt(); if (currentChannelIndex == 0) { if (type == "RSSI_DEVICE") { int signalStrength = getSignalStrenthFromRssi(xml.attributes().value("value").toInt()); floorHeatinController->setStateValue(floorHeatingControllerSignalStrengthStateTypeId, signalStrength); qCDebug(dcOpenCCU()) << "Floor heating controller" << floorHeatinController->name() << iseId << "signal strength:" << signalStrength << "%"; } else if (type == "UNREACH") { floorHeatinController->setStateValue(floorHeatingControllerReachableStateTypeId, xml.attributes().value("value").toString() == "false"); setFloorHeatingControllerConnected(floorHeatinController, floorHeatinController->stateValue(floorHeatingControllerReachableStateTypeId).toBool()); qCDebug(dcOpenCCU()) << "Floor heating controller" << floorHeatinController->name() << iseId << "unreachable:" << xml.attributes().value("value").toString(); } } else { // Actual valve channels, if a valve is connected, the value is not empty if (type == "LEVEL") { if (xml.attributes().value("value").toString().isEmpty()) { qCDebug(dcOpenCCU()) << "Floorheating channel" << currentChannelIndex << "has no valve connected"; } else { QString thingName = floorHeatinController->name() + " Channel " + QString::number(currentChannelIndex); if (myThings().filterByParam(floorHeatingValveThingIseIdParamTypeId, iseId).isEmpty()) { qCDebug(dcOpenCCU()) << "Adding new floor heating valve" << iseId; ThingDescriptor desciptor(floorHeatingValveThingClassId, thingName, QString(), floorHeatinController ->id()); ParamList params; params.append(Param(floorHeatingValveThingIseIdParamTypeId, iseId)); desciptor.setParams(params); desciptors.append(desciptor); } else { qCDebug(dcOpenCCU()) << "Thing for" << thingName << "already created."; } } } } } } if (!desciptors.isEmpty()) { emit autoThingsAppeared(desciptors); } } } } if (xml.hasError()) { qCWarning(dcOpenCCU()) << "Could not parse statelist response:" << xml.errorString(); setGatewayConnected(gateway, false); setFloorHeatingControllerConnected(floorHeatinController, false); } }); } void IntegrationPluginOpenCCU::processThingStateList(QXmlStreamReader *xml, Thing *thing) { int currentChannelIndex = 0; int currentLevelIseId = -1; int currentLevelStatus = -1; int currentValveState = -1; QString currentLevelValue; if (thing->thingClassId() == floorHeatingControllerThingClassId) { QString unreach = xml->attributes().value("unreach").toString(); if (!unreach.isEmpty()) { bool reachable = unreach == "false"; thing->setStateValue(floorHeatingControllerReachableStateTypeId, reachable); setFloorHeatingControllerConnected(thing, reachable); } } while (!xml->atEnd() && !xml->hasError() && !(xml->name() == QString("device") && xml->isEndElement())) { xml->readNext(); if (xml->name() == QString("channel") && xml->isStartElement()) { // qCDebug(dcOpenCCU()) << "-->" << xml->name() << xml->attributes().value("name").toString(); // foreach (const QXmlStreamAttribute &attribute, xml->attributes()) { // qCDebug(dcOpenCCU()) << " " << attribute.name() << attribute.value(); // } currentChannelIndex = xml->attributes().value("index").toInt(); currentLevelIseId = -1; currentLevelStatus = -1; currentValveState = -1; currentLevelValue.clear(); if (thing->thingClassId() == thermostatThingClassId) { // Channel 0 = Maintainance channel m_thermostats[thing].channels[currentChannelIndex].index = currentChannelIndex; m_thermostats[thing].channels[currentChannelIndex].iseId = xml->attributes().value("type").toInt(); } } if (xml->name() == QString("channel") && xml->isEndElement()) { if (thing->thingClassId() == floorHeatingControllerThingClassId && currentChannelIndex > 0 && currentLevelIseId >= 0) { updateFloorHeatingValve(thing, currentLevelIseId, currentLevelValue, currentLevelStatus, currentValveState); } } if (xml->name() == QString("datapoint") && xml->isStartElement()) { // qCDebug(dcOpenCCU()) << " -->" << xml->name() << xml->attributes().value("name").toString(); // foreach (const QXmlStreamAttribute &attribute, xml->attributes()) { // qCDebug(dcOpenCCU()) << " " << attribute.name() << attribute.value(); // } QString type = xml->attributes().value("type").toString(); int iseId = xml->attributes().value("ise_id").toInt(); if (thing->thingClassId() == thermostatThingClassId) { if (type == "LOW_BAT") { thing->setStateValue(thermostatBatteryCriticalStateTypeId, xml->attributes().value("value").toString() == "true"); } else if (type == "RSSI_DEVICE") { thing->setStateValue(thermostatSignalStrengthStateTypeId, getSignalStrenthFromRssi(xml->attributes().value("value").toInt())); } else if (type == "UNREACH") { thing->setStateValue(thermostatReachableStateTypeId, xml->attributes().value("value").toString() == "false"); thing->setStateValue(thermostatConnectedStateTypeId, thing->stateValue(thermostatReachableStateTypeId)); qCDebug(dcOpenCCU()) << "Thermostat" << thing->name() << iseId << "unreachable:" << xml->attributes().value("value").toString(); } else if (type == "ACTUAL_TEMPERATURE") { thing->setStateValue(thermostatTemperatureStateTypeId, xml->attributes().value("value").toDouble()); qCDebug(dcOpenCCU()) << "Thermostat" << thing->name() << iseId << "temperature:" << thing->stateValue(thermostatTemperatureStateTypeId).toDouble(); } else if (type == "SET_POINT_TEMPERATURE") { thing->setStateValue(thermostatTargetTemperatureStateTypeId, xml->attributes().value("value").toDouble()); m_thermostats[thing].channels[currentChannelIndex].targetTemperatureId = xml->attributes().value("ise_id").toInt(); qCDebug(dcOpenCCU()) << "Thermostat" << thing->name() << iseId << "target temperature:" << thing->stateValue(thermostatTargetTemperatureStateTypeId).toDouble(); } else if (type == "WINDOW_STATE") { thing->setStateValue(thermostatWindowOpenDetectedStateTypeId, xml->attributes().value("value").toInt() > 1); qCDebug(dcOpenCCU()) << "Thermostat" << thing->name() << iseId << "window state:" << thing->stateValue(thermostatWindowOpenDetectedStateTypeId).toBool(); } else if (type == "HUMIDITY") { thing->setStateValue(thermostatHumidityStateTypeId, xml->attributes().value("value").toDouble()); qCDebug(dcOpenCCU()) << "Thermostat" << thing->name() << iseId << "humidity:" << thing->stateValue(thermostatHumidityStateTypeId).toInt(); } else if (type == "SET_POINT_MODE") { m_thermostats[thing].channels[currentChannelIndex].modeId = iseId; int setPointMode = xml->attributes().value("value").toInt(); qCDebug(dcOpenCCU()) << "Thermostat" << thing->name() << iseId << "mode:" << setPointMode; if (setPointMode == 0) { thing->setStateValue(thermostatModeStateTypeId, "Auto"); } else if (setPointMode == 1) { thing->setStateValue(thermostatModeStateTypeId, "Manual"); } } else if (type == "CONTROL_MODE") { // Some device can be controlled using this data point instead of SET_POINT_MODE. // If this data point exists, we update manual and auto mode using this iseId, not the // iseId of the SET_POINT_MODE m_thermostats[thing].channels[currentChannelIndex].controlModeId = iseId; } } else if (thing->thingClassId() == floorHeatingControllerThingClassId) { if (type == "RSSI_DEVICE") { int signalStrength = getSignalStrenthFromRssi(xml->attributes().value("value").toInt()); thing->setStateValue(floorHeatingControllerSignalStrengthStateTypeId, signalStrength); } else if (type == "UNREACH") { bool isConnected = xml->attributes().value("value").toString() == "false"; thing->setStateValue(floorHeatingControllerReachableStateTypeId, isConnected); setFloorHeatingControllerConnected(thing, isConnected); } else if (type == "LEVEL") { currentLevelIseId = iseId; currentLevelValue = xml->attributes().value("value").toString(); } else if (type == "LEVEL_STATUS") { currentLevelStatus = xml->attributes().value("value").toInt(); } else if (type == "VALVE_STATE") { currentValveState = xml->attributes().value("value").toInt(); } } } } } Thing *IntegrationPluginOpenCCU::gatewayThing(Thing *thing) { Thing *currentThing = thing; while (currentThing) { if (currentThing->thingClassId() == openCCUThingClassId) { return currentThing; } currentThing = myThings().findById(currentThing->parentId()); } return nullptr; } void IntegrationPluginOpenCCU::setGatewayConnected(Thing *gatewayThing, bool connected) { if (!gatewayThing) return; gatewayThing->setStateValue(openCCUConnectedStateTypeId, connected); if (connected) return; foreach (Thing *childThing, myThings().filterByParentId(gatewayThing->id())) { if (childThing->thingClassId() == thermostatThingClassId) { childThing->setStateValue(thermostatConnectedStateTypeId, false); childThing->setStateValue(thermostatReachableStateTypeId, false); } else if (childThing->thingClassId() == floorHeatingControllerThingClassId) { setFloorHeatingControllerConnected(childThing, false); } } } void IntegrationPluginOpenCCU::setFloorHeatingControllerConnected(Thing *controllerThing, bool connected) { controllerThing->setStateValue(floorHeatingControllerConnectedStateTypeId, connected); if (connected) return; controllerThing->setStateValue(floorHeatingControllerReachableStateTypeId, false); foreach (Thing *childThing, myThings().filterByParentId(controllerThing->id())) { if (childThing->thingClassId() == floorHeatingValveThingClassId) { childThing->setStateValue(floorHeatingValveConnectedStateTypeId, false); } } } void IntegrationPluginOpenCCU::updateFloorHeatingValve(Thing *controllerThing, int levelIseId, const QString &levelValue, int levelStatus, int valveState) { Thing *valveThing = nullptr; foreach (Thing *childThing, myThings().filterByParentId(controllerThing->id())) { if (childThing->thingClassId() == floorHeatingValveThingClassId && childThing->paramValue(floorHeatingValveThingIseIdParamTypeId).toInt() == levelIseId) { valveThing = childThing; break; } } if (!valveThing) return; bool controllerConnected = controllerThing->stateValue(floorHeatingControllerConnectedStateTypeId).toBool(); bool valueValid = !levelValue.isEmpty() && levelStatus == 0 && valveState != 0; valveThing->setStateValue(floorHeatingValveConnectedStateTypeId, controllerConnected && valueValid); if (!valueValid) return; int percentage = qBound(0, qRound(levelValue.toDouble() * 100), 100); valveThing->setStateValue(floorHeatingValvePercentageStateTypeId, percentage); qCDebug(dcOpenCCU()) << "Valve position" << valveThing->name() << percentage << "%"; } int IntegrationPluginOpenCCU::getSignalStrenthFromRssi(int rssi) { int signalStrength = 0; if (rssi > -65) { signalStrength = 100; } else if (rssi <= -65 && rssi >= -75) { signalStrength = 75; } else if (rssi <= -75 && rssi >= -85) { signalStrength = 50; } else if (rssi <= -85) { signalStrength = 25; } return signalStrength; } QUrl IntegrationPluginOpenCCU::buildUrl(Thing *thing, const QString &method, const QUrlQuery &query) { QString token = thing->paramValue(openCCUThingTokenParamTypeId).toString(); QString host = thing->paramValue(openCCUThingAddressParamTypeId).toString(); if (host.isEmpty()) { host = thing->paramValue(openCCUThingHostNameParamTypeId).toString(); } QUrlQuery newQuery = query; newQuery.addQueryItem("sid", token); bool usingSsl = m_usingSsl.value(thing, true); QUrl url; url.setScheme(usingSsl ? "https" : "http"); url.setHost(host); url.setPath("/addons/xmlapi/" + method); url.setQuery(newQuery); return url; } void IntegrationPluginOpenCCU::onSslError(const QList &errors) { QNetworkReply *reply = qobject_cast(sender()); if (errors.count() == 1 && errors.first().error() == QSslError::SelfSignedCertificate) { reply->ignoreSslErrors(); } else { qCWarning(dcOpenCCU()) << "SSL error:" << errors.first().error() << errors.first().errorString(); reply->abort(); } }