/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
*
* Copyright 2013 - 2021, nymea GmbH
* Contact: contact@nymea.io

* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "integrationpluginzigbeedevelco.h"
#include "plugininfo.h"
#include "hardware/zigbee/zigbeehardwareresource.h"

#include <QDebug>

#include <zigbeeutils.h>

IntegrationPluginZigbeeDevelco::IntegrationPluginZigbeeDevelco()
{
    m_ieeeAddressParamTypeIds[ioModuleThingClassId] = ioModuleThingIeeeAddressParamTypeId;
    m_ieeeAddressParamTypeIds[airQualitySensorThingClassId] = airQualitySensorThingIeeeAddressParamTypeId;

    m_networkUuidParamTypeIds[ioModuleThingClassId] = ioModuleThingNetworkUuidParamTypeId;
    m_networkUuidParamTypeIds[airQualitySensorThingClassId] = airQualitySensorThingNetworkUuidParamTypeId;

    m_connectedStateTypeIds[ioModuleThingClassId] = ioModuleConnectedStateTypeId;
    m_connectedStateTypeIds[airQualitySensorThingClassId] = airQualitySensorConnectedStateTypeId;

    m_signalStrengthStateTypeIds[ioModuleThingClassId] = ioModuleSignalStrengthStateTypeId;
    m_signalStrengthStateTypeIds[airQualitySensorThingClassId] = airQualitySensorSignalStrengthStateTypeId;
}

QString IntegrationPluginZigbeeDevelco::name() const
{
    return "Develco";
}

bool IntegrationPluginZigbeeDevelco::handleNode(ZigbeeNode *node, const QUuid &networkUuid)
{
    // Filter for develco manufacturer code
    if (node->nodeDescriptor().manufacturerCode != Zigbee::Develco)
        return false;

    bool handled = false;
    if (node->modelName() == "IOMZB-110" || node->modelName() == "DIOZB-110") {
        if (node->hasEndpoint(IO_MODULE_EP_INPUT1) && node->hasEndpoint(IO_MODULE_EP_INPUT2) &&
                node->hasEndpoint(IO_MODULE_EP_INPUT3) && node->hasEndpoint(IO_MODULE_EP_INPUT4) &&
                node->hasEndpoint(IO_MODULE_EP_OUTPUT1 && node->hasEndpoint(IO_MODULE_EP_OUTPUT2))) {
            qCDebug(dcZigbeeDevelco()) << "Found IO module" << node << networkUuid.toString();
            initIoModule(node);
            createThing(ioModuleThingClassId, networkUuid, node);
            handled = true;
        }
    } else if (node->modelName() == "AQSZB-110") {
        if (node->hasEndpoint(AIR_QUALITY_SENSOR_EP_SENSOR)) {
            qCDebug(dcZigbeeDevelco()) << "Found air quality sensor" << node << networkUuid.toString();
            initAirQualitySensor(node);
            createThing(airQualitySensorThingClassId, networkUuid, node);
            handled = true;
        }
    }

    return handled;
}

void IntegrationPluginZigbeeDevelco::handleRemoveNode(ZigbeeNode *node, const QUuid &networkUuid)
{
    Q_UNUSED(networkUuid)
    Thing *thing = m_thingNodes.key(node);
    if (thing) {
        qCDebug(dcZigbeeDevelco()) << node << "for" << thing << "has left the network.";
        emit autoThingDisappeared(thing->id());
        // Removing it from our map to prevent a loop that would ask the zigbee network to remove this node (see thingRemoved())
        m_thingNodes.remove(thing);
    }
}

void IntegrationPluginZigbeeDevelco::init()
{
    hardwareManager()->zigbeeResource()->registerHandler(this, ZigbeeHardwareResource::HandlerTypeVendor);
}

void IntegrationPluginZigbeeDevelco::setupThing(ThingSetupInfo *info)
{
    qCDebug(dcZigbeeDevelco()) << "Setup" << info->thing();

    // Get the node for this thing
    Thing *thing = info->thing();
    QUuid networkUuid = thing->paramValue(m_networkUuidParamTypeIds.value(thing->thingClassId())).toUuid();
    ZigbeeAddress zigbeeAddress = ZigbeeAddress(thing->paramValue(m_ieeeAddressParamTypeIds.value(thing->thingClassId())).toString());
    ZigbeeNode *node = m_thingNodes.value(thing);
    if (!node) {
        node = hardwareManager()->zigbeeResource()->claimNode(this, networkUuid, zigbeeAddress);
        if (!node) {
            qCWarning(dcZigbeeDevelco()) << "Coud not find zigbee node for" << thing;
            info->finish(Thing::ThingErrorHardwareNotAvailable);
            return;
        }
    }

    m_thingNodes.insert(thing, node);

    // Update signal strength
    thing->setStateValue(m_signalStrengthStateTypeIds.value(thing->thingClassId()), qRound(node->lqi() * 100.0 / 255.0));
    connect(node, &ZigbeeNode::lqiChanged, thing, [this, thing](quint8 lqi){
        uint signalStrength = qRound(lqi * 100.0 / 255.0);
        qCDebug(dcZigbeeDevelco()) << thing << "signal strength changed" << signalStrength << "%";
        thing->setStateValue(m_signalStrengthStateTypeIds.value(thing->thingClassId()), signalStrength);
    });

    if (thing->thingClassId() == ioModuleThingClassId) {
        // Set the version from the manufacturer specific attribute in base cluster
        ZigbeeNodeEndpoint *primaryEndpoint = node->getEndpoint(IO_MODULE_EP_INPUT1);
        if (!primaryEndpoint) {
            qCWarning(dcZigbeeDevelco()) << "Failed to set up IO module" << thing << ". Could not find endpoint for version parsing.";
            info->finish(Thing::ThingErrorSetupFailed);
            return;
        }

        if (primaryEndpoint->hasInputCluster(ZigbeeClusterLibrary::ClusterIdBasic)) {
            ZigbeeCluster *basicCluster = primaryEndpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdBasic);
            if (basicCluster->hasAttribute(DEVELCO_BASIC_ATTRIBUTE_SW_VERSION)) {
                thing->setStateValue(ioModuleVersionStateTypeId, parseDevelcoVersionString(primaryEndpoint));
            } else {
                readDevelcoFirmwareVersion(node, primaryEndpoint);
            }

            connect(basicCluster, &ZigbeeCluster::attributeChanged, this, [=](const ZigbeeClusterAttribute &attribute){
                if (attribute.id() == DEVELCO_BASIC_ATTRIBUTE_SW_VERSION) {
                    thing->setStateValue(ioModuleVersionStateTypeId, parseDevelcoVersionString(primaryEndpoint));
                }
            });
        }

        // Handle reachable state from node
        thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), node->reachable());
        connect(node, &ZigbeeNode::reachableChanged, thing, [=](bool reachable){
            thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), reachable);
            if (reachable) {
                readDevelcoFirmwareVersion(node, node->getEndpoint(IO_MODULE_EP_INPUT1));
                readIoModuleOutputPowerStates(thing);
                readIoModuleInputPowerStates(thing);
            }
        });


        // Output 1
        ZigbeeNodeEndpoint *output1Endpoint = node->getEndpoint(IO_MODULE_EP_OUTPUT1);
        if (!output1Endpoint) {
            qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for output 1 on" << thing << node;
        } else {
            ZigbeeClusterOnOff *onOffCluster = output1Endpoint->inputCluster<ZigbeeClusterOnOff>(ZigbeeClusterLibrary::ClusterIdOnOff);
            if (!onOffCluster) {
                qCWarning(dcZigbeeDevelco()) << "Could not find On/Off cluster on" << thing << node << output1Endpoint;
            } else {
                if (onOffCluster->hasAttribute(ZigbeeClusterOnOff::AttributeOnOff)) {
                    thing->setStateValue(ioModuleOutput1StateTypeId, onOffCluster->power());
                }
                connect(onOffCluster, &ZigbeeClusterOnOff::powerChanged, thing, [thing](bool power){
                    qCDebug(dcZigbeeDevelco()) << thing << "output 1 power changed to" << power;
                    thing->setStateValue(ioModuleOutput1StateTypeId, power);
                });
            }
        }

        // Output 2
        ZigbeeNodeEndpoint *output2Endpoint = node->getEndpoint(IO_MODULE_EP_OUTPUT2);
        if (!output2Endpoint) {
            qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for output 2 on" << thing << node;
        } else {
            ZigbeeClusterOnOff *onOffCluster = output2Endpoint->inputCluster<ZigbeeClusterOnOff>(ZigbeeClusterLibrary::ClusterIdOnOff);
            if (!onOffCluster) {
                qCWarning(dcZigbeeDevelco()) << "Could not find On/Off cluster on" << thing << node << output2Endpoint;
            } else {
                if (onOffCluster->hasAttribute(ZigbeeClusterOnOff::AttributeOnOff)) {
                    thing->setStateValue(ioModuleOutput2StateTypeId, onOffCluster->power());
                }
                connect(onOffCluster, &ZigbeeClusterOnOff::powerChanged, thing, [thing](bool power){
                    qCDebug(dcZigbeeDevelco()) << thing << "output 2 power changed to" << power;
                    thing->setStateValue(ioModuleOutput2StateTypeId, power);
                });
            }
        }

        // Input 1
        ZigbeeNodeEndpoint *input1Endpoint = node->getEndpoint(IO_MODULE_EP_INPUT1);
        if (!input1Endpoint) {
            qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for input 1 on" << thing << node;
        } else {
            ZigbeeClusterBinaryInput *binaryInputCluster = input1Endpoint->inputCluster<ZigbeeClusterBinaryInput>(ZigbeeClusterLibrary::ClusterIdBinaryInput);
            if (!binaryInputCluster) {
                qCWarning(dcZigbeeDevelco()) << "Could not find BinaryInput cluster on" << thing << node << input1Endpoint;
            } else {
                if (binaryInputCluster->hasAttribute(ZigbeeClusterLibrary::ClusterIdBinaryInput)) {
                    thing->setStateValue(ioModuleInput1StateTypeId, binaryInputCluster->presentValue());
                }
                connect(binaryInputCluster, &ZigbeeClusterBinaryInput::presentValueChanged, thing, [thing](bool presentValue){
                    qCDebug(dcZigbeeDevelco()) << thing << "input 1 changed to" << presentValue;
                    thing->setStateValue(ioModuleInput1StateTypeId, presentValue);
                });
            }
        }

        // Input 2
        ZigbeeNodeEndpoint *input2Endpoint = node->getEndpoint(IO_MODULE_EP_INPUT2);
        if (!input2Endpoint) {
            qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for input 2 on" << thing << node;
        } else {
            ZigbeeClusterBinaryInput *binaryInputCluster = input2Endpoint->inputCluster<ZigbeeClusterBinaryInput>(ZigbeeClusterLibrary::ClusterIdBinaryInput);
            if (!binaryInputCluster) {
                qCWarning(dcZigbeeDevelco()) << "Could not find BinaryInput cluster on" << thing << node << input2Endpoint;
            } else {
                if (binaryInputCluster->hasAttribute(ZigbeeClusterLibrary::ClusterIdBinaryInput)) {
                    thing->setStateValue(ioModuleInput2StateTypeId, binaryInputCluster->presentValue());
                }
                connect(binaryInputCluster, &ZigbeeClusterBinaryInput::presentValueChanged, thing, [thing](bool presentValue){
                    qCDebug(dcZigbeeDevelco()) << thing << "input 2 changed to" << presentValue;
                    thing->setStateValue(ioModuleInput2StateTypeId, presentValue);
                });
            }
        }

        // Input 3
        ZigbeeNodeEndpoint *input3Endpoint = node->getEndpoint(IO_MODULE_EP_INPUT3);
        if (!input3Endpoint) {
            qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for input 3 on" << thing << node;
        } else {
            ZigbeeClusterBinaryInput *binaryInputCluster = input3Endpoint->inputCluster<ZigbeeClusterBinaryInput>(ZigbeeClusterLibrary::ClusterIdBinaryInput);
            if (!binaryInputCluster) {
                qCWarning(dcZigbeeDevelco()) << "Could not find BinaryInput cluster on" << thing << node << input3Endpoint;
            } else {
                if (binaryInputCluster->hasAttribute(ZigbeeClusterLibrary::ClusterIdBinaryInput)) {
                    thing->setStateValue(ioModuleInput3StateTypeId, binaryInputCluster->presentValue());
                }
                connect(binaryInputCluster, &ZigbeeClusterBinaryInput::presentValueChanged, thing, [thing](bool presentValue){
                    qCDebug(dcZigbeeDevelco()) << thing << "input 3 changed to" << presentValue;
                    thing->setStateValue(ioModuleInput3StateTypeId, presentValue);
                });
            }
        }

        // Input 4
        ZigbeeNodeEndpoint *input4Endpoint = node->getEndpoint(IO_MODULE_EP_INPUT4);
        if (!input4Endpoint) {
            qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for input 4 on" << thing << node;
        } else {
            ZigbeeClusterBinaryInput *binaryInputCluster = input4Endpoint->inputCluster<ZigbeeClusterBinaryInput>(ZigbeeClusterLibrary::ClusterIdBinaryInput);
            if (!binaryInputCluster) {
                qCWarning(dcZigbeeDevelco()) << "Could not find BinaryInput cluster on" << thing << node << input4Endpoint;
            } else {
                if (binaryInputCluster->hasAttribute(ZigbeeClusterLibrary::ClusterIdBinaryInput)) {
                    thing->setStateValue(ioModuleInput4StateTypeId, binaryInputCluster->presentValue());
                }
                connect(binaryInputCluster, &ZigbeeClusterBinaryInput::presentValueChanged, thing, [thing](bool presentValue){
                    qCDebug(dcZigbeeDevelco()) << thing << "input 4 changed to" << presentValue;
                    thing->setStateValue(ioModuleInput4StateTypeId, presentValue);
                });
            }
        }
    } else if (thing->thingClassId() == airQualitySensorThingClassId) {
        ZigbeeNodeEndpoint *sensorEndpoint = node->getEndpoint(AIR_QUALITY_SENSOR_EP_SENSOR);
        if (!sensorEndpoint) {
            qCWarning(dcZigbeeDevelco()) << "Failed to set up air quality sensor" << thing << ". Could not find endpoint for version parsing.";
            info->finish(Thing::ThingErrorSetupFailed);
            return;
        }

        // Handle reachable state from node
        thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), node->reachable());
        connect(node, &ZigbeeNode::reachableChanged, thing, [=](bool reachable){
            thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), reachable);
        });

        // Version state
        if (sensorEndpoint->hasInputCluster(ZigbeeClusterLibrary::ClusterIdBasic)) {
            ZigbeeCluster *basicCluster = sensorEndpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdBasic);
            if (basicCluster->hasAttribute(DEVELCO_BASIC_ATTRIBUTE_SW_VERSION)) {
                thing->setStateValue(airQualitySensorVersionStateTypeId, parseDevelcoVersionString(sensorEndpoint));
            }

            connect(basicCluster, &ZigbeeCluster::attributeChanged, this, [=](const ZigbeeClusterAttribute &attribute){
                if (attribute.id() == DEVELCO_BASIC_ATTRIBUTE_SW_VERSION) {
                    thing->setStateValue(airQualitySensorVersionStateTypeId, parseDevelcoVersionString(sensorEndpoint));
                }
            });
        }

        // Temperature
        ZigbeeClusterTemperatureMeasurement *temperatureCluster = sensorEndpoint->inputCluster<ZigbeeClusterTemperatureMeasurement>(ZigbeeClusterLibrary::ClusterIdTemperatureMeasurement);
        if (!temperatureCluster) {
            qCWarning(dcZigbeeDevelco()) << "Could not find the temperature measurement server cluster on" << thing << sensorEndpoint;
        } else {
            // Only set the state if the cluster actually has the attribute
            if (temperatureCluster->hasAttribute(ZigbeeClusterTemperatureMeasurement::AttributeMeasuredValue)) {
                thing->setStateValue(airQualitySensorTemperatureStateTypeId, temperatureCluster->temperature());
            }
            connect(temperatureCluster, &ZigbeeClusterTemperatureMeasurement::temperatureChanged, thing, [thing](double temperature){
                qCDebug(dcZigbeeDevelco()) << thing << "temperature changed" << temperature << "°C";
                thing->setStateValue(airQualitySensorTemperatureStateTypeId, temperature);
            });
        }

        // Humidity
        ZigbeeClusterRelativeHumidityMeasurement *humidityCluster = sensorEndpoint->inputCluster<ZigbeeClusterRelativeHumidityMeasurement>(ZigbeeClusterLibrary::ClusterIdRelativeHumidityMeasurement);
        if (!humidityCluster) {
            qCWarning(dcZigbeeDevelco()) << "Could not find the humidity measurement server cluster on" << thing << sensorEndpoint;
        } else {
            // Only set the state if the cluster actually has the attribute
            if (humidityCluster->hasAttribute(ZigbeeClusterRelativeHumidityMeasurement::AttributeMeasuredValue)) {
                thing->setStateValue(airQualitySensorHumidityStateTypeId, humidityCluster->humidity());
            }
            connect(humidityCluster, &ZigbeeClusterRelativeHumidityMeasurement::humidityChanged, thing, [thing](double humidity){
                qCDebug(dcZigbeeDevelco()) << thing << "humidity changed" << humidity << "%";
                thing->setStateValue(airQualitySensorHumidityStateTypeId, humidity);
            });
        }

        // Battery voltage
        ZigbeeClusterPowerConfiguration *powerConfigurationCluster = sensorEndpoint->inputCluster<ZigbeeClusterPowerConfiguration>(ZigbeeClusterLibrary::ClusterIdPowerConfiguration);
        if (!powerConfigurationCluster) {
            qCWarning(dcZigbeeDevelco()) << "Could not find the power configuration server cluster on" << thing << sensorEndpoint;
        } else {
            // Only set the state if the cluster actually has the attribute
            if (powerConfigurationCluster->hasAttribute(ZigbeeClusterPowerConfiguration::AttributeBatteryVoltage)) {
                int batteryVoltage = powerConfigurationCluster->attribute(ZigbeeClusterPowerConfiguration::AttributeBatteryVoltage).dataType().toUInt8() * 100;
                qCDebug(dcZigbeeDevelco()) << thing << "battery voltage" << batteryVoltage << "mV";
                thing->setStateValue(airQualitySensorBatteryCriticalStateTypeId, batteryVoltage < 2500);
            }
            connect(powerConfigurationCluster, &ZigbeeClusterPowerConfiguration::attributeChanged, thing, [=](const ZigbeeClusterAttribute &attribute){
                if (attribute.id() == ZigbeeClusterPowerConfiguration::AttributeBatteryVoltage) {
                    int batteryVoltage = attribute.dataType().toUInt8() * 100;
                    qCDebug(dcZigbeeDevelco()) << thing << "battery voltage" << batteryVoltage << "mV";
                    thing->setStateValue(airQualitySensorBatteryCriticalStateTypeId, batteryVoltage < 2500);
                }
            });
        }

        // VOC
        ZigbeeCluster *vocCluster = sensorEndpoint->getInputCluster(static_cast<ZigbeeClusterLibrary::ClusterId>(AIR_QUALITY_SENSOR_VOC_MEASUREMENT_CLUSTER_ID));
        if (!vocCluster) {
            qCWarning(dcZigbeeDevelco()) << "Could not find the VOC measurement server cluster on" << thing << sensorEndpoint;
        } else {
            // Only set the state if the cluster actually has the attribute
            if (vocCluster->hasAttribute(AIR_QUALITY_SENSOR_VOC_MEASUREMENT_ATTRIBUTE_MEASURED_VALUE)) {
                ZigbeeClusterAttribute measuredValueAttribute = vocCluster->attribute(AIR_QUALITY_SENSOR_VOC_MEASUREMENT_ATTRIBUTE_MEASURED_VALUE);
                bool valueOk = false;
                quint16 value = measuredValueAttribute.dataType().toUInt16(&valueOk);
                if (valueOk) {
                    if (value == 0xFFFF) {
                        qCWarning(dcZigbeeDevelco()) << "Received invalid VOC measurment. The sensor is not ready yet.";
                    } else {
                        qCDebug(dcZigbeeDevelco()) << thing << "VOC changed" << value << "ppm";
                        thing->setStateValue(airQualitySensorVocStateTypeId, value);
                        updateIndoorAirQuality(thing, value);
                    }
                } else {
                    qCWarning(dcZigbeeDevelco()) << "Failed to convert VOC measurment value" << measuredValueAttribute;
                }
            }

            connect(vocCluster, &ZigbeeCluster::attributeChanged, thing, [=](const ZigbeeClusterAttribute &attribute){
                if (attribute.id() == AIR_QUALITY_SENSOR_VOC_MEASUREMENT_ATTRIBUTE_MEASURED_VALUE) {
                    bool valueOk = false;
                    quint16 value = attribute.dataType().toUInt16(&valueOk);
                    if (valueOk) {
                        if (value == 0xFFFF) {
                            qCWarning(dcZigbeeDevelco()) << "Received invalid VOC measurment. The sensor is not ready yet.";
                        } else {
                            qCDebug(dcZigbeeDevelco()) << thing << "VOC changed" << value << "ppm";
                            thing->setStateValue(airQualitySensorVocStateTypeId, value);
                            updateIndoorAirQuality(thing, value);
                        }
                    } else {
                        qCWarning(dcZigbeeDevelco()) << "Failed to convert VOC measurment value" << attribute;
                    }
                }
            });
        }
    }

    info->finish(Thing::ThingErrorNoError);
}

void IntegrationPluginZigbeeDevelco::postSetupThing(Thing *thing)
{
    if (thing->thingClassId() == ioModuleThingClassId) {
        if (m_thingNodes.value(thing)->reachable()) {
            readIoModuleOutputPowerStates(thing);
            readIoModuleInputPowerStates(thing);
        }
    }
}

void IntegrationPluginZigbeeDevelco::executeAction(ThingActionInfo *info)
{
    if (!hardwareManager()->zigbeeResource()->available()) {
        info->finish(Thing::ThingErrorHardwareNotAvailable);
        return;
    }

    // Get the node
    Thing *thing = info->thing();
    ZigbeeNode *node = m_thingNodes.value(thing);
    if (!node->reachable()) {
        info->finish(Thing::ThingErrorHardwareNotAvailable);
        return;
    }

    if (thing->thingClassId() == ioModuleThingClassId) {
        // Identify
        if (info->action().actionTypeId() == ioModuleAlertActionTypeId) {
            ZigbeeNodeEndpoint *primaryEndpoint = node->getEndpoint(IO_MODULE_EP_INPUT1);
            if (!primaryEndpoint) {
                qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for execute action on" << thing << node;
                info->finish(Thing::ThingErrorHardwareFailure);
                return;
            }

            ZigbeeClusterIdentify *identifyCluster = primaryEndpoint->inputCluster<ZigbeeClusterIdentify>(ZigbeeClusterLibrary::ClusterIdIdentify);
            if (!identifyCluster) {
                qCWarning(dcZigbeeDevelco()) << "Could not find identify cluster for" << thing << "in" << node;
                info->finish(Thing::ThingErrorHardwareFailure);
                return;
            }

            // Send the command trough the network
            ZigbeeClusterReply *reply = identifyCluster->identify(2);
            connect(reply, &ZigbeeClusterReply::finished, this, [reply, info](){
                // Note: reply will be deleted automatically
                if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
                    info->finish(Thing::ThingErrorHardwareFailure);
                } else {
                    info->finish(Thing::ThingErrorNoError);
                }
            });
            return;
        }

        // Output 1
        if (info->action().actionTypeId() == ioModuleOutput1ActionTypeId) {
            bool power = info->action().paramValue(ioModuleOutput1ActionOutput1ParamTypeId).toBool();
            qCDebug(dcZigbeeDevelco()) << "Set output 1 power of" << thing << "to" << power;

            ZigbeeNodeEndpoint *output1Endpoint = node->getEndpoint(IO_MODULE_EP_OUTPUT1);
            if (!output1Endpoint) {
                qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for output 1 on" << thing << node;
                info->finish(Thing::ThingErrorHardwareFailure);
                return;
            }

            ZigbeeClusterOnOff *onOffCluster = output1Endpoint->inputCluster<ZigbeeClusterOnOff>(ZigbeeClusterLibrary::ClusterIdOnOff);
            if (!onOffCluster) {
                qCWarning(dcZigbeeDevelco()) << "Could not find On/Off cluster on" << thing << node << output1Endpoint;
                info->finish(Thing::ThingErrorHardwareFailure);
                return;
            }

            ZigbeeClusterReply *reply = (power ? onOffCluster->commandOn() : onOffCluster->commandOff());
            connect(reply, &ZigbeeClusterReply::finished, info, [=](){
                // Note: reply will be deleted automatically
                if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
                    qCWarning(dcZigbeeDevelco()) << "Failed to set power for output 1 on" << thing << reply->error();
                    info->finish(Thing::ThingErrorHardwareFailure);
                } else {
                    info->finish(Thing::ThingErrorNoError);
                    qCDebug(dcZigbeeDevelco()) << "Set power on output 1 finished successfully for" << thing;
                    thing->setStateValue(ioModuleOutput1StateTypeId, power);
                }
            });
            return;
        }

        // Output 2
        if (info->action().actionTypeId() == ioModuleOutput2ActionTypeId) {
            bool power = info->action().paramValue(ioModuleOutput2ActionOutput2ParamTypeId).toBool();
            qCDebug(dcZigbeeDevelco()) << "Set output 2 power of" << thing << "to" << power;

            ZigbeeNodeEndpoint *output2Endpoint = node->getEndpoint(IO_MODULE_EP_OUTPUT2);
            if (!output2Endpoint) {
                qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for output 2 on" << thing << node;
                info->finish(Thing::ThingErrorHardwareFailure);
                return;
            }

            ZigbeeClusterOnOff *onOffCluster = output2Endpoint->inputCluster<ZigbeeClusterOnOff>(ZigbeeClusterLibrary::ClusterIdOnOff);
            if (!onOffCluster) {
                qCWarning(dcZigbeeDevelco()) << "Could not find On/Off cluster on" << thing << node << output2Endpoint;
                info->finish(Thing::ThingErrorHardwareFailure);
                return;
            }

            ZigbeeClusterReply *reply = (power ? onOffCluster->commandOn() : onOffCluster->commandOff());
            connect(reply, &ZigbeeClusterReply::finished, info, [=](){
                // Note: reply will be deleted automatically
                if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
                    qCWarning(dcZigbeeDevelco()) << "Failed to set power for output 2 on" << thing << reply->error();
                    info->finish(Thing::ThingErrorHardwareFailure);
                } else {
                    info->finish(Thing::ThingErrorNoError);
                    qCDebug(dcZigbeeDevelco()) << "Set power on output 2 finished successfully for" << thing;
                    thing->setStateValue(ioModuleOutput2StateTypeId, power);
                }
            });
            return;
        }

        // Impulse action
        if (info->action().actionTypeId() == ioModuleImpulseOutput1ActionTypeId || info->action().actionTypeId() == ioModuleImpulseOutput2ActionTypeId) {
            // Uint for time is 1/10 s
            uint impulseDuration = thing->settings().paramValue(ioModuleSettingsImpulseDurationParamTypeId).toUInt();
            quint16 impulseDurationScaled = static_cast<quint16>(qRound(impulseDuration / 100.0));

            ZigbeeNodeEndpoint *endpoint = nullptr;
            if (info->action().actionTypeId() == ioModuleImpulseOutput1ActionTypeId) {
                endpoint = node->getEndpoint(IO_MODULE_EP_OUTPUT1);
                qCDebug(dcZigbeeDevelco()) << "Execute output 1 impulse with" << impulseDurationScaled * 100 << "ms";
            } else if (info->action().actionTypeId() == ioModuleImpulseOutput2ActionTypeId) {
                endpoint = node->getEndpoint(IO_MODULE_EP_OUTPUT2);
                qCDebug(dcZigbeeDevelco()) << "Execute output 2 impulse with" << impulseDurationScaled * 100 << "ms";
            }

            if (!endpoint) {
                qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for impulse action on" << thing << node;
                info->finish(Thing::ThingErrorHardwareFailure);
                return;
            }

            ZigbeeClusterOnOff *onOffCluster = endpoint->inputCluster<ZigbeeClusterOnOff>(ZigbeeClusterLibrary::ClusterIdOnOff);
            if (!onOffCluster) {
                qCWarning(dcZigbeeDevelco()) << "Could not find On/Off cluster on" << thing << node << endpoint;
                info->finish(Thing::ThingErrorHardwareFailure);
                return;
            }

            ZigbeeClusterReply *reply = onOffCluster->commandOnWithTimedOff(false, impulseDurationScaled, 0);
            connect(reply, &ZigbeeClusterReply::finished, info, [=](){
                // Note: reply will be deleted automatically
                if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
                    qCWarning(dcZigbeeDevelco()) << "Failed to set on with timed off on" << thing << endpoint << reply->error();
                    info->finish(Thing::ThingErrorHardwareFailure);
                } else {
                    info->finish(Thing::ThingErrorNoError);
                    qCDebug(dcZigbeeDevelco()) << "Set on with timed off on finished successfully for" << thing << endpoint;
                }
            });
            return;
        }
    }

    info->finish(Thing::ThingErrorUnsupportedFeature);
}

void IntegrationPluginZigbeeDevelco::thingRemoved(Thing *thing)
{
    ZigbeeNode *node = m_thingNodes.take(thing);
    if (node) {
        QUuid networkUuid = thing->paramValue(m_networkUuidParamTypeIds.value(thing->thingClassId())).toUuid();
        hardwareManager()->zigbeeResource()->removeNodeFromNetwork(networkUuid, node);
    }
}

void IntegrationPluginZigbeeDevelco::createThing(const ThingClassId &thingClassId, const QUuid &networkUuid, ZigbeeNode *node)
{
    ThingDescriptor descriptor(thingClassId);
    QString deviceClassName = supportedThings().findById(thingClassId).displayName();
    descriptor.setTitle(QString("%1 (%2 - %3)").arg(deviceClassName, node->manufacturerName(), node->modelName()));

    ParamList params;
    params.append(Param(m_networkUuidParamTypeIds[thingClassId], networkUuid.toString()));
    params.append(Param(m_ieeeAddressParamTypeIds[thingClassId], node->extendedAddress().toString()));
    descriptor.setParams(params);
    emit autoThingsAppeared({descriptor});
}

QString IntegrationPluginZigbeeDevelco::parseDevelcoVersionString(ZigbeeNodeEndpoint *endpoint)
{
    QString versionString;
    ZigbeeCluster *basicCluster = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdBasic);
    if (!basicCluster) {
        qCWarning(dcZigbeeDevelco()) << "Could not find basic cluster on" << endpoint << "for version parsing";
        return versionString;
    }

    if (!basicCluster->hasAttribute(DEVELCO_BASIC_ATTRIBUTE_SW_VERSION)) {
        qCWarning(dcZigbeeDevelco()) << "Could not find manufacturer specific develco software version attribute in basic cluster on" << endpoint;
        return versionString;
    }

    ZigbeeClusterAttribute versionAttribute = basicCluster->attribute(DEVELCO_BASIC_ATTRIBUTE_SW_VERSION);
    // 1 Byte octet string length, 3 byte version infromation
    if (versionAttribute.dataType().data().length() < 4 || versionAttribute.dataType().data().at(0) != 3) {
        qCWarning(dcZigbeeDevelco()) << "Failed to parse version string from manufacturer specific develco software version attribute" << versionAttribute;
        return versionString;
    }

    int majorVersion = static_cast<int>(versionAttribute.dataType().data().at(1));
    int minorVersion = static_cast<int>(versionAttribute.dataType().data().at(2));
    int patchVersion = static_cast<int>(versionAttribute.dataType().data().at(3));
    versionString = QString("%1.%2.%3").arg(majorVersion).arg(minorVersion).arg(patchVersion);
    //qCDebug(dcZigbeeDevelco()) << versionAttribute << ZigbeeUtils::convertByteArrayToHexString(versionAttribute.dataType().data()) << versionString;

    return versionString;
}

void IntegrationPluginZigbeeDevelco::initIoModule(ZigbeeNode *node)
{
    qCDebug(dcZigbeeDevelco()) << "Start initializing IO Module" << node;
    readDevelcoFirmwareVersion(node, node->getEndpoint(IO_MODULE_EP_INPUT1));

    // Binding and reporting outputs
    configureOnOffPowerReporting(node, node->getEndpoint(IO_MODULE_EP_OUTPUT1));
    configureOnOffPowerReporting(node, node->getEndpoint(IO_MODULE_EP_OUTPUT2));

    // Binding and reporting inputs
    configureBinaryInputReporting(node, node->getEndpoint(IO_MODULE_EP_INPUT1));
    configureBinaryInputReporting(node, node->getEndpoint(IO_MODULE_EP_INPUT2));
    configureBinaryInputReporting(node, node->getEndpoint(IO_MODULE_EP_INPUT3));
    configureBinaryInputReporting(node, node->getEndpoint(IO_MODULE_EP_INPUT4));
}

void IntegrationPluginZigbeeDevelco::initAirQualitySensor(ZigbeeNode *node)
{
    qCDebug(dcZigbeeDevelco()) << "Start initializing air quality sensor" << node;
    readDevelcoFirmwareVersion(node, node->getEndpoint(AIR_QUALITY_SENSOR_EP_SENSOR));

    configureTemperatureReporting(node, node->getEndpoint(AIR_QUALITY_SENSOR_EP_SENSOR));
    configureHumidityReporting(node, node->getEndpoint(AIR_QUALITY_SENSOR_EP_SENSOR));
    configureBattryVoltageReporting(node, node->getEndpoint(AIR_QUALITY_SENSOR_EP_SENSOR));
    configureVocReporting(node, node->getEndpoint(AIR_QUALITY_SENSOR_EP_SENSOR));
}

void IntegrationPluginZigbeeDevelco::configureOnOffPowerReporting(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
    qCDebug(dcZigbeeDevelco()) << "Bind on/off cluster to coordinator IEEE address" << node << endpoint;
    ZigbeeDeviceObjectReply * zdoReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdOnOff, hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01);
    connect(zdoReply, &ZigbeeDeviceObjectReply::finished, node, [=](){
        if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
            qCWarning(dcZigbeeDevelco()) << "Failed to bind on/off cluster to coordinator" << zdoReply->error();
        } else {
            qCDebug(dcZigbeeDevelco()) << "Bind on/off cluster to coordinator finished successfully";
        }

        // Configure attribute reporting
        ZigbeeClusterLibrary::AttributeReportingConfiguration reportingConfig;
        reportingConfig.attributeId = ZigbeeClusterOnOff::AttributeOnOff;
        reportingConfig.minReportingInterval = 0;
        reportingConfig.maxReportingInterval = 600;
        reportingConfig.dataType = Zigbee::Bool;

        qCDebug(dcZigbeeDevelco()) << "Configure attribute reporting for on/off cluster" << node << endpoint;
        ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdOnOff)->configureReporting({reportingConfig});
        connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){
            if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) {
                qCWarning(dcZigbeeDevelco()) << "Failed configure attribute reporting on on/off cluster" << reportingReply->error();
            } else {
                qCDebug(dcZigbeeDevelco()) << "Attribute reporting configuration finished for on/off cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload);
            }
        });
    });
}

void IntegrationPluginZigbeeDevelco::configureBinaryInputReporting(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
    qCDebug(dcZigbeeDevelco()) << "Bind binary input cluster to coordinator IEEE address" << node << endpoint;
    ZigbeeDeviceObjectReply * zdoReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdBinaryInput, hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01);
    connect(zdoReply, &ZigbeeDeviceObjectReply::finished, node, [=](){
        if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
            qCWarning(dcZigbeeDevelco()) << "Failed to bind binary input cluster to coordinator" << zdoReply->error();
        } else {
            qCDebug(dcZigbeeDevelco()) << "Bind binary input cluster to coordinator finished successfully";
        }

        // Configure attribute reporting
        ZigbeeClusterLibrary::AttributeReportingConfiguration reportingConfig;
        reportingConfig.attributeId = ZigbeeClusterBinaryInput::AttributePresentValue;
        reportingConfig.minReportingInterval = 0;
        reportingConfig.maxReportingInterval = 600;
        reportingConfig.dataType = Zigbee::Bool;

        qCDebug(dcZigbeeDevelco()) << "Configure attribute reporting for binary input cluster" << node << endpoint;
        ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdBinaryInput)->configureReporting({reportingConfig});
        connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){
            if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) {
                qCWarning(dcZigbeeDevelco()) << "Failed configure attribute reporting on binary input cluster" << reportingReply->error();
            } else {
                qCDebug(dcZigbeeDevelco()) << "Attribute reporting configuration finished for on binary input cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload);
            }
        });
    });
}

void IntegrationPluginZigbeeDevelco::configureTemperatureReporting(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
    // Note: don not read initial value since it returns an invalid masurement. The device needs some time for initial measuring

    qCDebug(dcZigbeeDevelco()) << "Bind temperature measurement cluster to coordinator IEEE address" << node << endpoint;
    ZigbeeDeviceObjectReply * zdoReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdTemperatureMeasurement, hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01);
    connect(zdoReply, &ZigbeeDeviceObjectReply::finished, node, [=](){
        if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
            qCWarning(dcZigbeeDevelco()) << "Failed to bind temperature measurement cluster to coordinator" << zdoReply->error();
        } else {
            qCDebug(dcZigbeeDevelco()) << "Bind temperature measurement cluster to coordinator finished successfully";
        }

        // Configure attribute reporting
        ZigbeeClusterLibrary::AttributeReportingConfiguration reportingConfig;
        reportingConfig.attributeId = ZigbeeClusterTemperatureMeasurement::AttributeMeasuredValue;
        reportingConfig.dataType = Zigbee::Int16;
        reportingConfig.minReportingInterval = 60;
        reportingConfig.maxReportingInterval = 300;
        reportingConfig.reportableChange = ZigbeeDataType(static_cast<qint16>(10)).data();

        qCDebug(dcZigbeeDevelco()) << "Configure attribute reporting for temperature measurement cluster" << node << endpoint;
        ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdTemperatureMeasurement)->configureReporting({reportingConfig});
        connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){
            if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) {
                qCWarning(dcZigbeeDevelco()) << "Failed configure attribute reporting on temperature measurement cluster" << reportingReply->error();
            } else {
                qCDebug(dcZigbeeDevelco()) << "Attribute reporting configuration finished for on temperature measurement cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload);
            }
        });
    });
}

void IntegrationPluginZigbeeDevelco::configureHumidityReporting(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
    // Note: don not read initial value since it returns an invalid masurement. The device needs some time for initial measuring
    qCDebug(dcZigbeeDevelco()) << "Bind humidity measurement cluster to coordinator IEEE address" << node << endpoint;
    ZigbeeDeviceObjectReply * zdoReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdRelativeHumidityMeasurement, hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01);
    connect(zdoReply, &ZigbeeDeviceObjectReply::finished, node, [=](){
        if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
            qCWarning(dcZigbeeDevelco()) << "Failed to bind humidity measurement cluster to coordinator" << zdoReply->error();
        } else {
            qCDebug(dcZigbeeDevelco()) << "Bind humidity measurement cluster to coordinator finished successfully";
        }

        // Configure attribute reporting
        ZigbeeClusterLibrary::AttributeReportingConfiguration reportingConfig;
        reportingConfig.attributeId = ZigbeeClusterRelativeHumidityMeasurement::AttributeMeasuredValue;
        reportingConfig.dataType = Zigbee::Uint16;
        reportingConfig.minReportingInterval = 60;
        reportingConfig.maxReportingInterval = 300;
        reportingConfig.reportableChange = ZigbeeDataType(static_cast<quint16>(10)).data();

        qCDebug(dcZigbeeDevelco()) << "Configure attribute reporting for humidity measurement cluster" << node << endpoint;
        ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdRelativeHumidityMeasurement)->configureReporting({reportingConfig});
        connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){
            if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) {
                qCWarning(dcZigbeeDevelco()) << "Failed configure attribute reporting on humidity measurement cluster" << reportingReply->error();
            } else {
                qCDebug(dcZigbeeDevelco()) << "Attribute reporting configuration finished for on humidity measurement cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload);
            }
        });
    });
}

void IntegrationPluginZigbeeDevelco::configureBattryVoltageReporting(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
    // Note: don not read initial value since it returns an invalid masurement. The device needs some time for initial measuring
    qCDebug(dcZigbeeDevelco()) << "Bind power configuration cluster to coordinator IEEE address" << node << endpoint;
    ZigbeeDeviceObjectReply * zdoReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdPowerConfiguration, hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01);
    connect(zdoReply, &ZigbeeDeviceObjectReply::finished, node, [=](){
        if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
            qCWarning(dcZigbeeDevelco()) << "Failed to bind power configuration cluster to coordinator" << zdoReply->error();
        } else {
            qCDebug(dcZigbeeDevelco()) << "Bind power configuration cluster to coordinator finished successfully";
        }

        // Configure attribute reporting
        ZigbeeClusterLibrary::AttributeReportingConfiguration reportingConfig;
        reportingConfig.attributeId = ZigbeeClusterPowerConfiguration::AttributeBatteryVoltage;
        reportingConfig.dataType = Zigbee::Uint8;
        reportingConfig.minReportingInterval = 60;
        reportingConfig.maxReportingInterval = 300;
        reportingConfig.reportableChange = ZigbeeDataType(static_cast<quint8>(1)).data();

        qCDebug(dcZigbeeDevelco()) << "Configure attribute reporting for power configuration cluster" << node << endpoint;
        ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdPowerConfiguration)->configureReporting({reportingConfig});
        connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){
            if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) {
                qCWarning(dcZigbeeDevelco()) << "Failed configure attribute reporting on power configuration cluster" << reportingReply->error();
            } else {
                qCDebug(dcZigbeeDevelco()) << "Attribute reporting configuration finished for on power configuration cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload);
            }
        });
    });
}

void IntegrationPluginZigbeeDevelco::configureVocReporting(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
    // Note: don not read initial value since it returns an invalid masurement. The device needs some time for initial measuring

    qCDebug(dcZigbeeDevelco()) << "Bind VOC measurement cluster to coordinator IEEE address" << node << endpoint;
    ZigbeeDeviceObjectReply * zdoReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), AIR_QUALITY_SENSOR_VOC_MEASUREMENT_CLUSTER_ID, hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01);
    connect(zdoReply, &ZigbeeDeviceObjectReply::finished, node, [=](){
        if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
            qCWarning(dcZigbeeDevelco()) << "Failed to bind VOC measurement cluster to coordinator" << zdoReply->error();
        } else {
            qCDebug(dcZigbeeDevelco()) << "Bind VOC measurement cluster to coordinator finished successfully";
        }

        // Configure attribute reporting
        ZigbeeClusterLibrary::AttributeReportingConfiguration reportingConfig;
        reportingConfig.attributeId = AIR_QUALITY_SENSOR_VOC_MEASUREMENT_ATTRIBUTE_MEASURED_VALUE;
        reportingConfig.dataType = Zigbee::Uint16;
        reportingConfig.minReportingInterval = 60;
        reportingConfig.maxReportingInterval = 300;
        reportingConfig.reportableChange = ZigbeeDataType(static_cast<quint16>(10)).data();

        qCDebug(dcZigbeeDevelco()) << "Configure attribute reporting for VOC measurement cluster" << node << endpoint;
        ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(static_cast<ZigbeeClusterLibrary::ClusterId>(AIR_QUALITY_SENSOR_VOC_MEASUREMENT_CLUSTER_ID))->configureReporting({reportingConfig}, Zigbee::Develco);
        connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){
            if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) {
                qCWarning(dcZigbeeDevelco()) << "Failed configure attribute reporting on VOC measurement cluster" << reportingReply->error();
            } else {
                qCDebug(dcZigbeeDevelco()) << "Attribute reporting configuration finished for on VOC measurement cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload);
            }
        });
    });
}

void IntegrationPluginZigbeeDevelco::updateIndoorAirQuality(Thing *thing, uint voc)
{
    if (voc <= 65) {
        thing->setStateValue(airQualitySensorIndoorAirQualityStateTypeId, "Excellent");
    } else if (voc <= 220) {
        thing->setStateValue(airQualitySensorIndoorAirQualityStateTypeId, "Good");
    } else if (voc <= 660) {
        thing->setStateValue(airQualitySensorIndoorAirQualityStateTypeId, "Moderate");
    } else if (voc <= 2200) {
        thing->setStateValue(airQualitySensorIndoorAirQualityStateTypeId, "Poor");
    } else {
        thing->setStateValue(airQualitySensorIndoorAirQualityStateTypeId, "Unhealthy");
    }
}

void IntegrationPluginZigbeeDevelco::readDevelcoFirmwareVersion(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
    // Read manufacturer specific basic cluster attribute 0x8000
    ZigbeeClusterBasic *basicCluster = endpoint->inputCluster<ZigbeeClusterBasic>(ZigbeeClusterLibrary::ClusterIdBasic);
    if (!basicCluster) {
        qCWarning(dcZigbeeDevelco()) << "Could not find basic cluster for manufacturer specific attribute reading on" << node << endpoint;
        return;
    }

    // We have to read the color capabilities
    ZigbeeClusterReply *reply = basicCluster->readAttributes({DEVELCO_BASIC_ATTRIBUTE_SW_VERSION}, Zigbee::Develco);
    connect(reply, &ZigbeeClusterReply::finished, node, [=](){
        if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
            qCWarning(dcZigbeeDevelco()) << "Failed to read manufacturer specific version attribute on" << node << endpoint << basicCluster;
            return;
        }

        qCDebug(dcZigbeeDevelco()) << "Reading develco manufacturer specific version attributes finished successfully";
    });
}

void IntegrationPluginZigbeeDevelco::readOnOffPowerAttribute(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
    qCDebug(dcZigbeeDevelco()) << "Reading power states of" << node << "on" << endpoint;

    ZigbeeClusterOnOff *onOffCluster = endpoint->inputCluster<ZigbeeClusterOnOff>(ZigbeeClusterLibrary::ClusterIdOnOff);
    if (!onOffCluster) {
        qCWarning(dcZigbeeDevelco()) << "Could not find On/Off cluster on" << node << endpoint;
    } else {
        ZigbeeClusterReply *reply = onOffCluster->readAttributes({ZigbeeClusterOnOff::AttributeOnOff});
        connect(reply, &ZigbeeClusterReply::finished, node, [=](){
            if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
                qCWarning(dcZigbeeDevelco()) << "Failed to read On/Off power attribute from" << node << endpoint << onOffCluster;
            }
            // Will be updated trough the attribute changed signal
        });
    }
}

void IntegrationPluginZigbeeDevelco::readBinaryInputPresentValueAttribute(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
    ZigbeeClusterBinaryInput *binaryInputCluster = endpoint->inputCluster<ZigbeeClusterBinaryInput>(ZigbeeClusterLibrary::ClusterIdBinaryInput);
    if (!binaryInputCluster) {
        qCWarning(dcZigbeeDevelco()) << "Could not find BinaryInput cluster on" << node << endpoint;
    } else {
        ZigbeeClusterReply *reply = binaryInputCluster->readAttributes({ZigbeeClusterBinaryInput::AttributePresentValue});
        connect(reply, &ZigbeeClusterReply::finished, node, [=](){
            if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
                qCWarning(dcZigbeeDevelco()) << "Failed to read binary input value attribute from" << node << endpoint << binaryInputCluster;
            }
            // Will be updated trough the attribute changed signal
        });
    }
}

void IntegrationPluginZigbeeDevelco::readIoModuleOutputPowerStates(Thing *thing)
{
    ZigbeeNode *node = m_thingNodes.value(thing);
    if (!node) {
        qCWarning(dcZigbeeDevelco()) << "Could not find zigbee node for" << thing;
        return;
    }

    qCDebug(dcZigbeeDevelco()) << "Start reading power states of" << thing << node;

    // Read output 1 power state
    ZigbeeNodeEndpoint *output1Endpoint = node->getEndpoint(IO_MODULE_EP_OUTPUT1);
    if (!output1Endpoint) {
        qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for output 1 on" << thing << node;
    } else {
        readOnOffPowerAttribute(node, output1Endpoint);
    }

    // Read output 2 power state
    ZigbeeNodeEndpoint *output2Endpoint = node->getEndpoint(IO_MODULE_EP_OUTPUT2);
    if (!output2Endpoint) {
        qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for output 2 on" << thing << node;
    } else {
        readOnOffPowerAttribute(node, output2Endpoint);
    }
}

void IntegrationPluginZigbeeDevelco::readIoModuleInputPowerStates(Thing *thing)
{
    ZigbeeNode *node = m_thingNodes.value(thing);
    if (!node) {
        qCWarning(dcZigbeeDevelco()) << "Could not find zigbee node for" << thing;
        return;
    }

    // Read input 1 state
    ZigbeeNodeEndpoint *input1Endpoint = node->getEndpoint(IO_MODULE_EP_INPUT1);
    if (!input1Endpoint) {
        qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for input 1 on" << thing << node;
    } else {
        readBinaryInputPresentValueAttribute(node, input1Endpoint);
    }

    // Read input 2 state
    ZigbeeNodeEndpoint *input2Endpoint = node->getEndpoint(IO_MODULE_EP_INPUT2);
    if (!input2Endpoint) {
        qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for input 2 on" << thing << node;
    } else {
        readBinaryInputPresentValueAttribute(node, input2Endpoint);
    }

    // Read input 3 state
    ZigbeeNodeEndpoint *input3Endpoint = node->getEndpoint(IO_MODULE_EP_INPUT3);
    if (!input3Endpoint) {
        qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for input 3 on" << thing << node;
    } else {
        readBinaryInputPresentValueAttribute(node, input3Endpoint);
    }

    // Read input 4 state
    ZigbeeNodeEndpoint *input4Endpoint = node->getEndpoint(IO_MODULE_EP_INPUT4);
    if (!input4Endpoint) {
        qCWarning(dcZigbeeDevelco()) << "Could not find endpoint for input 4 on" << thing << node;
    } else {
        readBinaryInputPresentValueAttribute(node, input4Endpoint);
    }
}

