// 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();
    }
}
