diff --git a/debian-qt5/control b/debian-qt5/control
index 11c4d634..49515d2f 100644
--- a/debian-qt5/control
+++ b/debian-qt5/control
@@ -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},
diff --git a/debian-qt5/nymea-plugin-openccu.install.in b/debian-qt5/nymea-plugin-openccu.install.in
new file mode 100644
index 00000000..985b3f3b
--- /dev/null
+++ b/debian-qt5/nymea-plugin-openccu.install.in
@@ -0,0 +1 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginopenccu.so
diff --git a/debian-qt6/control b/debian-qt6/control
index ece193d0..5fc5822c 100644
--- a/debian-qt6/control
+++ b/debian-qt6/control
@@ -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},
diff --git a/debian-qt6/nymea-plugin-openccu.install.in b/debian-qt6/nymea-plugin-openccu.install.in
new file mode 120000
index 00000000..d2609790
--- /dev/null
+++ b/debian-qt6/nymea-plugin-openccu.install.in
@@ -0,0 +1 @@
+../debian-qt5/nymea-plugin-openccu.install.in
\ No newline at end of file
diff --git a/nymea-plugins.pro b/nymea-plugins.pro
index 01b19cd3..459ef528 100644
--- a/nymea-plugins.pro
+++ b/nymea-plugins.pro
@@ -47,7 +47,8 @@ PLUGIN_DIRS = \
notifyevents \
nuki \
onewire \
- openuv \
+ openccu \
+ openuv \
openweathermap \
osdomotics \
philipshue \
diff --git a/openccu/integrationpluginopenccu.cpp b/openccu/integrationpluginopenccu.cpp
new file mode 100644
index 00000000..a9de9003
--- /dev/null
+++ b/openccu/integrationpluginopenccu.cpp
@@ -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 .
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#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();
+ }
+}
diff --git a/openccu/integrationpluginopenccu.h b/openccu/integrationpluginopenccu.h
new file mode 100644
index 00000000..3d6ff018
--- /dev/null
+++ b/openccu/integrationpluginopenccu.h
@@ -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 .
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#ifndef INTEGRATIONPLUGINOPENCCU_H
+#define INTEGRATIONPLUGINOPENCCU_H
+
+#include
+#include
+
+#include
+#include
+#include
+
+#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 channels;
+ };
+
+ PluginTimer *m_pluginTimer = nullptr;
+ QHash m_thermostats;
+ QHash 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 &errors);
+
+};
+
+#endif // INTEGRATIONPLUGINOPENCCU_H
diff --git a/openccu/integrationpluginopenccu.json b/openccu/integrationpluginopenccu.json
new file mode 100644
index 00000000..2ef30cc2
--- /dev/null
+++ b/openccu/integrationpluginopenccu.json
@@ -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
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/openccu/meta.json b/openccu/meta.json
new file mode 100644
index 00000000..ef3f6cfc
--- /dev/null
+++ b/openccu/meta.json
@@ -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"
+ ]
+}
diff --git a/openccu/openccu.png b/openccu/openccu.png
new file mode 100644
index 00000000..ec5235b9
Binary files /dev/null and b/openccu/openccu.png differ
diff --git a/openccu/openccu.pro b/openccu/openccu.pro
new file mode 100644
index 00000000..e2db3eb7
--- /dev/null
+++ b/openccu/openccu.pro
@@ -0,0 +1,11 @@
+include(../plugins.pri)
+
+QT += network
+
+SOURCES += \
+ integrationpluginopenccu.cpp
+
+HEADERS += \
+ integrationpluginopenccu.h
+
+