New plugin: Add OpenCCU support

This commit is contained in:
Simon Stürz 2025-11-17 11:21:46 +01:00
parent 1da9322e81
commit f20c44c651
11 changed files with 1210 additions and 1 deletions

View File

@ -486,6 +486,17 @@ Description: nymea integration plugin for one wire devices
This package contains the nymea integration plugin for one wire devices.
Package: nymea-plugin-openccu
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
Conflicts: nymea-plugins-translations (<< 1.0.1)
Description: nymea integration plugin for OpenCCU
This package contains the nymea integration plugin for OpenCCU
based floor heating systems. The integration connects to the
xmlapi integration of OpenCCU.
Package: nymea-plugin-openuv
Architecture: any
Depends: ${shlibs:Depends},

View File

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

View File

@ -440,6 +440,17 @@ Description: nymea integration plugin for one wire devices
This package contains the nymea integration plugin for one wire devices.
Package: nymea-plugin-openccu
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
Conflicts: nymea-plugins-translations (<< 1.0.1)
Description: nymea integration plugin for OpenCCU
This package contains the nymea integration plugin for OpenCCU
based floor heating systems. The integration connects to the
xmlapi integration of OpenCCU.
Package: nymea-plugin-openuv
Architecture: any
Depends: ${shlibs:Depends},

View File

@ -0,0 +1 @@
../debian-qt5/nymea-plugin-openccu.install.in

View File

@ -47,7 +47,8 @@ PLUGIN_DIRS = \
notifyevents \
nuki \
onewire \
openuv \
openccu \
openuv \
openweathermap \
osdomotics \
philipshue \

View File

@ -0,0 +1,770 @@
// 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 <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationpluginopenccu.h"
#include "plugininfo.h"
#include <integrations/thing.h>
#include <network/networkaccessmanager.h>
#include <QUrl>
#include <QUrlQuery>
#include <QXmlStreamReader>
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<QSslError> &errors)
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(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();
}
}

View File

@ -0,0 +1,101 @@
// 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 <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef INTEGRATIONPLUGINOPENCCU_H
#define INTEGRATIONPLUGINOPENCCU_H
#include <integrations/integrationplugin.h>
#include <plugintimer.h>
#include <QSslError>
#include <QUrlQuery>
#include <QXmlStreamReader>
#include "extern-plugininfo.h"
class IntegrationPluginOpenCCU : public IntegrationPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginopenccu.json")
Q_INTERFACES(IntegrationPlugin)
public:
explicit IntegrationPluginOpenCCU();
void init() override;
void discoverThings(ThingDiscoveryInfo *info) override;
void setupThing(ThingSetupInfo *info) override;
void postSetupThing(Thing *thing) override;
void thingRemoved(Thing *thing) override;
void executeAction(ThingActionInfo *info) override;
private:
struct DeviceInfo {
int iseId = -1;
QString name;
QString interface;
QString deviceType;
QString serialNumber;
};
struct ThermostatChannel {
int index = 1;
int iseId = -1;
int targetTemperatureId = -1;
int modeId = -1;
int controlModeId = -1;
};
struct Thermostat {
QHash<int, ThermostatChannel> channels;
};
PluginTimer *m_pluginTimer = nullptr;
QHash<Thing *, Thermostat> m_thermostats;
QHash<Thing *, bool> m_usingSsl;
void getDevices(Thing *thing);
void getDeviceTypeList(Thing *thing);
void getState(Thing *thing);
void getStateList(Thing *thing);
void getChannels(Thing *floorHeatinController);
void processThingStateList(QXmlStreamReader *xml, Thing *thing);
Thing *gatewayThing(Thing *thing);
void setGatewayConnected(Thing *gatewayThing, bool connected);
void setFloorHeatingControllerConnected(Thing *controllerThing, bool connected);
void updateFloorHeatingValve(Thing *controllerThing, int levelIseId, const QString &levelValue, int levelStatus, int valveState);
int getSignalStrenthFromRssi(int rssi);
QUrl buildUrl(Thing *thing, const QString &method, const QUrlQuery &query = QUrlQuery());
private slots:
void onSslError(const QList<QSslError> &errors);
};
#endif // INTEGRATIONPLUGINOPENCCU_H

View File

@ -0,0 +1,289 @@
{
"name": "OpenCCU",
"displayName": "OpenCCU",
"id": "1debe558-38dc-4f4f-803f-e8fdd478803c",
"vendors": [
{
"name": "openCCU",
"displayName": "OpenUCC",
"id": "aebdd54c-0a97-4828-b2a1-806d6c8aef75",
"thingClasses": [
{
"id": "9fb7b7f0-0a92-40ad-a08e-dde9d88af09f",
"name": "openCCU",
"displayName": "OpenCCU Gateway",
"interfaces": ["gateway", "networkdevice"],
"createMethods": ["discovery", "user"],
"paramTypes": [
{
"id": "fc016355-4da1-4b24-830e-8ee7eb4406db",
"name": "address",
"displayName": "Host address",
"type": "QString",
"inputType": "IPv4Address",
"defaultValue": ""
},
{
"id": "71f51cae-bbc1-41fd-b992-87731e21c887",
"name": "hostName",
"displayName": "Host name",
"type": "QString",
"inputType": "TextLine",
"defaultValue": ""
},
{
"id": "109b9ae2-105f-4ba6-9cb0-f89409cb7aaf",
"name":"macAddress",
"displayName": "MAC address",
"type": "QString",
"inputType": "MacAddress",
"defaultValue": ""
},
{
"id": "87c02788-0c78-4c9d-a3b8-a0646c9a0d66",
"name":"token",
"displayName": "Token",
"type": "QString",
"defaultValue": ""
}
],
"stateTypes": [
{
"id": "3fbebc9b-01ed-4431-94d7-e741b6362de6",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"type": "bool",
"defaultValue": false
}
]
},
{
"id": "dbcd9d61-5348-4762-b84f-adc48e0ba2ee",
"name": "thermostat",
"displayName": "Thermostat",
"interfaces": ["thermostat", "heating", "temperaturesensor", "humiditysensor", "battery", "wirelessconnectable"],
"createMethods": ["auto"],
"paramTypes": [
{
"id": "2b628a08-96e0-47d3-a094-1f5b018c8091",
"name": "serialNumber",
"displayName": "Serial number",
"type": "QString",
"inputType": "TextLine",
"readOnly": true
},
{
"id": "596108b0-2f77-4bb4-b902-b3f2029ca68f",
"name": "type",
"displayName": "Type",
"type": "QString",
"inputType": "TextLine",
"readOnly": true
},
{
"id": "6876ce23-86e7-4518-bc52-0d04b7be0b40",
"name": "iseId",
"displayName": "ID",
"type": "QString",
"inputType": "TextLine",
"readOnly": true
}
],
"stateTypes": [
{
"id": "46f52c89-7797-43db-99db-0a985e1d4c21",
"name": "connected",
"displayName": "Connected",
"type": "bool",
"defaultValue": false
},
{
"id": "e848e60e-975a-435f-8aa2-c7c250d441fc",
"name": "signalStrength",
"displayName": "Signal strength",
"type": "uint",
"unit": "Percentage",
"minValue": 0,
"maxValue": 100,
"defaultValue": 0,
"cached": false
},
{
"id": "97fdbb35-9517-4245-803c-b08a556161f5",
"name": "reachable",
"displayName": "Reachable",
"type": "bool",
"defaultValue": false
},
{
"id": "c2c89939-3b40-466e-8d67-e10f1ffc23b6",
"name": "batteryCritical",
"displayName": "Battery critical",
"type": "bool",
"defaultValue": false
},
{
"id": "dba3188d-2231-4269-81bd-561681aba8dd",
"name": "mode",
"displayName": "Mode",
"displayNameAction": "Set mode",
"type": "QString",
"defaultValue": "Auto",
"writable": true,
"possibleValues": [
"Auto",
"Manual"
]
},
{
"id": "2b22147b-beb6-4e42-8343-94fbfa52e09a",
"name": "power",
"displayName": "Power",
"displayNameAction": "Set power",
"type": "bool",
"writable": true,
"defaultValue": false
},
{
"id": "ae425a76-674b-4a20-99aa-dbdb5c7f495e",
"name": "windowOpenDetected",
"displayName": "Window open detected",
"type": "bool",
"defaultValue": false
},
{
"id": "18c0de57-df78-4d24-9d30-8756df8615c1",
"name": "temperature",
"displayName": "Temperature",
"unit": "DegreeCelsius",
"type": "double",
"defaultValue": 0
},
{
"id": "7be037b7-ba71-4983-b927-0bfbbaa03225",
"name": "targetTemperature",
"displayName": "Target temperature",
"displayNameAction": "Set target temperature",
"unit": "DegreeCelsius",
"type": "double",
"defaultValue": 0,
"minValue": 5,
"maxValue": 25,
"writable": true
},
{
"id": "31d9bb84-1031-45d9-8b76-308210a8b248",
"name": "humidity",
"displayName": "Humidity",
"unit": "Percentage",
"type": "double",
"defaultValue": 0,
"minValue": 0,
"maxValue": 100
}
]
},
{
"id": "e7c30062-9c74-41de-b4ab-e0ac5863650b",
"name": "floorHeatingController",
"displayName": "Floorheating controller",
"interfaces": ["wirelessconnectable"],
"createMethods": ["auto"],
"paramTypes": [
{
"id": "1487caff-66d6-425d-9c60-4de022836673",
"name": "serialNumber",
"displayName": "Serial number",
"type": "QString",
"inputType": "TextLine",
"readOnly": true
},
{
"id": "535c500a-65f7-406c-a05c-6c51f5822bd0",
"name": "type",
"displayName": "Type",
"type": "QString",
"inputType": "TextLine",
"readOnly": true
},
{
"id": "b81f1849-0acf-4721-b4b7-66c46142fd5c",
"name": "iseId",
"displayName": "ID",
"type": "QString",
"inputType": "TextLine",
"readOnly": true
}
],
"stateTypes": [
{
"id": "162c3ba2-4884-4974-a473-89494862317d",
"name": "connected",
"displayName": "Connected",
"type": "bool",
"defaultValue": false
},
{
"id": "be5a113d-80b1-4ec2-8394-ad5fa089d81f",
"name": "signalStrength",
"displayName": "Signal strength",
"type": "uint",
"unit": "Percentage",
"minValue": 0,
"maxValue": 100,
"defaultValue": 0,
"cached": false
},
{
"id": "198681be-8514-4590-a06a-1ef97876cf2a",
"name": "reachable",
"displayName": "Reachable",
"type": "bool",
"defaultValue": false
}
]
},
{
"id": "5d80574b-0ce0-4fbe-9985-631c854208e4",
"name": "floorHeatingValve",
"displayName": "Floor heating valve",
"interfaces": ["valve", "wirelessconnectable"],
"createMethods": ["auto"],
"paramTypes": [
{
"id": "b3fd0a41-f4d3-47de-9db7-eb8915e0f6a5",
"name": "iseId",
"displayName": "ID",
"type": "QString",
"inputType": "TextLine",
"readOnly": true
}
],
"stateTypes": [
{
"id": "669940b5-0582-4f0f-a250-8eb826c46ac1",
"name": "connected",
"displayName": "Connected",
"type": "bool",
"defaultValue": false
},
{
"id": "834401ba-865a-48c3-882d-2cf1317c0708",
"name": "percentage",
"displayName": "Valve position",
"displayNameAction": "Set valve position",
"type": "int",
"unit": "Percentage",
"minValue": 0,
"maxValue": 100,
"defaultValue": 0,
"writable": true,
"cached": true
}
]
}
]
}
]
}

13
openccu/meta.json Normal file
View File

@ -0,0 +1,13 @@
{
"title": "OpenCCU",
"tagline": "Connect to your OpenCCU gateway (RaspberryPiMatic) and controll your HomematicIP devices.",
"icon": "openccu.png",
"stability": "community",
"offline": true,
"technologies": [
"network"
],
"categories": [
"heating"
]
}

BIN
openccu/openccu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

11
openccu/openccu.pro Normal file
View File

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