// 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-modbus.
*
* nymea-plugins-modbus 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-modbus 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-modbus. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "integrationpluginmodbuscommander.h"
#include "plugininfo.h"

#include <hardwaremanager.h>
#include <network/networkdevicediscovery.h>
#include <hardware/modbus/modbusrtumaster.h>
#include <hardware/modbus/modbusrtuhardwareresource.h>

IntegrationPluginModbusCommander::IntegrationPluginModbusCommander()
{
}

void IntegrationPluginModbusCommander::init()
{
    m_slaveAddressParamTypeId.insert(coilThingClassId, coilThingSlaveAddressParamTypeId);
    m_slaveAddressParamTypeId.insert(inputRegisterThingClassId, inputRegisterThingSlaveAddressParamTypeId);
    m_slaveAddressParamTypeId.insert(discreteInputThingClassId, discreteInputThingSlaveAddressParamTypeId);
    m_slaveAddressParamTypeId.insert(holdingRegisterThingClassId, holdingRegisterThingSlaveAddressParamTypeId);

    m_registerAddressParamTypeId.insert(coilThingClassId, coilThingRegisterAddressParamTypeId);
    m_registerAddressParamTypeId.insert(inputRegisterThingClassId, inputRegisterThingRegisterAddressParamTypeId);
    m_registerAddressParamTypeId.insert(discreteInputThingClassId, discreteInputThingRegisterAddressParamTypeId);
    m_registerAddressParamTypeId.insert(holdingRegisterThingClassId, holdingRegisterThingRegisterAddressParamTypeId);

    m_connectedStateTypeId.insert(modbusRTUClientThingClassId, modbusRTUClientConnectedStateTypeId);
    m_connectedStateTypeId.insert(modbusTCPClientThingClassId, modbusTCPClientConnectedStateTypeId);
    m_connectedStateTypeId.insert(coilThingClassId, coilConnectedStateTypeId);
    m_connectedStateTypeId.insert(inputRegisterThingClassId, inputRegisterConnectedStateTypeId);
    m_connectedStateTypeId.insert(discreteInputThingClassId, discreteInputConnectedStateTypeId);
    m_connectedStateTypeId.insert(holdingRegisterThingClassId, holdingRegisterConnectedStateTypeId);

    m_valueStateTypeId.insert(coilThingClassId, coilValueStateTypeId);
    m_valueStateTypeId.insert(inputRegisterThingClassId, inputRegisterValueStateTypeId);
    m_valueStateTypeId.insert(discreteInputThingClassId, discreteInputValueStateTypeId);
    m_valueStateTypeId.insert(holdingRegisterThingClassId, holdingRegisterValueStateTypeId);

    // Plugin configuration
    connect(this, &IntegrationPluginModbusCommander::configValueChanged, this, &IntegrationPluginModbusCommander::onPluginConfigurationChanged);

    // Modbus RTU hardware resource
    connect(hardwareManager()->modbusRtuResource(), &ModbusRtuHardwareResource::modbusRtuMasterRemoved, this, [=](const QUuid &modbusUuid){
        qCDebug(dcModbusCommander()) << "Modbus RTU master has been removed" << modbusUuid.toString();

        // Check if there is any device using this resource
        foreach (Thing *thing, m_modbusRtuMasters.keys()) {
            if (m_modbusRtuMasters.value(thing)->modbusUuid() == modbusUuid) {
                qCWarning(dcModbusCommander()) << "Hardware resource removed for" << thing << ". The thing will not be functional any more until a new resource has been configured for it.";
                m_modbusRtuMasters.remove(thing);
                thing->setStateValue(m_connectedStateTypeId[thing->thingClassId()], false);

                // Set all child things disconnected
                foreach (Thing *childThing, myThings()) {
                    if (childThing->parentId() == thing->id()) {
                        thing->setStateValue(m_connectedStateTypeId[childThing->thingClassId()], false);
                    }
                }
            }
        }
    });
}

void IntegrationPluginModbusCommander::discoverThings(ThingDiscoveryInfo *info)
{
    ThingClassId thingClassId = info->thingClassId();
    if (thingClassId == modbusRTUClientThingClassId) {
        foreach (ModbusRtuMaster *modbusMaster, hardwareManager()->modbusRtuResource()->modbusRtuMasters()) {
            qCDebug(dcModbusCommander()) << "Found RTU master resource" << modbusMaster;
            if (modbusMaster->connected()) {
                ParamList parameters;
                ThingDescriptor thingDescriptor(thingClassId, "Modbus RTU master", modbusMaster->serialPort());
                parameters.append(Param(modbusRTUClientThingModbusMasterUuidParamTypeId, modbusMaster->modbusUuid()));
                thingDescriptor.setParams(parameters);
                info->addThingDescriptor(thingDescriptor);
            } else {
                qCWarning(dcModbusCommander()) << "Found configured resource" << modbusMaster << "but it is not connected. Skipping.";
            }
        }

        info->finish(Thing::ThingErrorNoError);
        return;
    } else if (thingClassId == modbusTCPClientThingClassId) {
        if (!hardwareManager()->networkDeviceDiscovery()->available()) {
            qCWarning(dcModbusCommander()) << "Failed to discover network devices. The network device discovery is not available.";
            info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The discovery is not available."));
            return;
        }

        NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover();
        connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater);
        connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
            ThingDescriptors descriptors;
            qCDebug(dcModbusCommander()) << "Discovery finished. Found" << discoveryReply->networkDeviceInfos().length() << "devices";
            foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) {
                qCDebug(dcModbusCommander()) << networkDeviceInfo;
                QString title;
                if (networkDeviceInfo.hostName().isEmpty()) {
                    title += networkDeviceInfo.address().toString();
                } else {
                    title += networkDeviceInfo.address().toString() + " (" + networkDeviceInfo.hostName() + ")";
                }

                QString description;
                MacAddressInfo macInfo = networkDeviceInfo.macAddressInfos().constFirst();
                description = networkDeviceInfo.address().toString();
                if (!macInfo.vendorName().isEmpty())
                    description += " - " + networkDeviceInfo.macAddressInfos().constFirst().vendorName();

                if (networkDeviceInfo.hostName().isEmpty()) {
                    title = macInfo.macAddress().toString();
                } else {
                    title = networkDeviceInfo.hostName() + " (" + macInfo.macAddress().toString() + ")";
                }

                ThingDescriptor descriptor(modbusTCPClientThingClassId, title, description);

                // Check if we already have set up this device
                Things existingThings = myThings().filterByParam(modbusTCPClientThingIpAddressParamTypeId, networkDeviceInfo.address().toString());
                if (existingThings.length() == 1) {
                    qCDebug(dcModbusCommander()) << "This thing already exists in the system." << existingThings.first() << networkDeviceInfo;
                    descriptor.setThingId(existingThings.first()->id());
                }

                ParamList params;
                params << Param(modbusTCPClientThingIpAddressParamTypeId, networkDeviceInfo.address().toString());
                descriptor.setParams(params);
                info->addThingDescriptor(descriptor);
            }
            info->finish(Thing::ThingErrorNoError);
        });
        return;
    } else if (thingClassId == discreteInputThingClassId) {
        Q_FOREACH(Thing *clientThing, myThings()){
            if (clientThing->thingClassId() == modbusTCPClientThingClassId) {
                ThingDescriptor descriptor(thingClassId, "Discrete input", clientThing->name() + " " + clientThing->paramValue(modbusTCPClientThingIpAddressParamTypeId).toString() + " Port: " + clientThing->paramValue(modbusTCPClientThingPortParamTypeId).toString());
                descriptor.setParentId(clientThing->id());
                info->addThingDescriptor(descriptor);
            }
            if (clientThing->thingClassId() == modbusRTUClientThingClassId) {
                ThingDescriptor descriptor(thingClassId, "Discrete input", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingModbusMasterUuidParamTypeId).toString());
                descriptor.setParentId(clientThing->id());
                info->addThingDescriptor(descriptor);
            }
        }
        info->finish(Thing::ThingErrorNoError);
        return;

    } else if (thingClassId == coilThingClassId) {
        Q_FOREACH(Thing *clientThing, myThings()){
            if (clientThing->thingClassId() == modbusTCPClientThingClassId) {
                ThingDescriptor descriptor(thingClassId, "Coil", clientThing->name() + " " + clientThing->paramValue(modbusTCPClientThingIpAddressParamTypeId).toString() + " Port: " + clientThing->paramValue(modbusTCPClientThingPortParamTypeId).toString());
                descriptor.setParentId(clientThing->id());
                info->addThingDescriptor(descriptor);
            }
            if (clientThing->thingClassId() == modbusRTUClientThingClassId) {
                ThingDescriptor descriptor(thingClassId, "Coil", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingModbusMasterUuidParamTypeId).toString());
                descriptor.setParentId(clientThing->id());
                info->addThingDescriptor(descriptor);
            }
        }
        info->finish(Thing::ThingErrorNoError);
        return;
    } else if (thingClassId == holdingRegisterThingClassId) {
        Q_FOREACH(Thing *clientThing, myThings()){
            if (clientThing->thingClassId() == modbusTCPClientThingClassId) {
                ThingDescriptor descriptor(thingClassId, "Holding register", clientThing->name() + " " + clientThing->paramValue(modbusTCPClientThingIpAddressParamTypeId).toString() + " Port: " + clientThing->paramValue(modbusTCPClientThingPortParamTypeId).toString());
                descriptor.setParentId(clientThing->id());
                info->addThingDescriptor(descriptor);
            }
            if (clientThing->thingClassId() == modbusRTUClientThingClassId) {
                ThingDescriptor descriptor(thingClassId, "Holding register", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingModbusMasterUuidParamTypeId).toString());
                descriptor.setParentId(clientThing->id());
                info->addThingDescriptor(descriptor);
            }
        }
        info->finish(Thing::ThingErrorNoError);
        return;

    } else if (thingClassId == inputRegisterThingClassId) {
        Q_FOREACH(Thing *clientThing, myThings()){
            if (clientThing->thingClassId() == modbusTCPClientThingClassId) {
                ThingDescriptor descriptor(thingClassId, "Input register", clientThing->name() + " " + clientThing->paramValue(modbusTCPClientThingIpAddressParamTypeId).toString() + " Port: " + clientThing->paramValue(modbusTCPClientThingPortParamTypeId).toString());
                descriptor.setParentId(clientThing->id());
                info->addThingDescriptor(descriptor);
            }
            if (clientThing->thingClassId() == modbusRTUClientThingClassId) {
                ThingDescriptor descriptor(thingClassId, "Input register", clientThing->name() + " " + clientThing->paramValue(modbusRTUClientThingModbusMasterUuidParamTypeId).toString());
                descriptor.setParentId(clientThing->id());
                info->addThingDescriptor(descriptor);
            }
        }
        info->finish(Thing::ThingErrorNoError);
        return;
    } else {
        Q_ASSERT_X(false, "discoverThings", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8());
    }
}

void IntegrationPluginModbusCommander::setupThing(ThingSetupInfo *info)
{
    Thing *thing = info->thing();

    if (thing->thingClassId() == modbusTCPClientThingClassId) {

        QHostAddress hostAddress = QHostAddress(thing->paramValue(modbusTCPClientThingIpAddressParamTypeId).toString());
        uint port = thing->paramValue(modbusTCPClientThingPortParamTypeId).toUInt();
        uint numberOfRetries = thing->setting(modbusTCPClientSettingsNumberOfRetriesParamTypeId).toUInt();
        uint timeout = thing->setting(modbusTCPClientSettingsTimeoutParamTypeId).toUInt();

        if (m_modbusTCPMasters.contains(thing)) {
            // In case of a rediscovery
            m_modbusTCPMasters.take(thing)->deleteLater();
        }

        foreach (ModbusTcpMaster *modbusTCPMaster, m_modbusTCPMasters.values()) {
            if ((modbusTCPMaster->hostAddress() == hostAddress) && (modbusTCPMaster->port() == port)) {
                m_modbusTCPMasters.insert(thing, modbusTCPMaster);
                return info->finish(Thing::ThingErrorNoError);
            }
        }

        qCDebug(dcModbusCommander()) << "Setting up TCP client" << thing->name();
        qCDebug(dcModbusCommander()) << "      address:" << hostAddress.toString();
        qCDebug(dcModbusCommander()) << "      port:" << port;
        qCDebug(dcModbusCommander()) << "      number of retries:" << numberOfRetries;
        qCDebug(dcModbusCommander()) << "      timeout:" << timeout;

        ModbusTcpMaster *modbusTCPMaster = new ModbusTcpMaster(hostAddress, port, this);
        connect(modbusTCPMaster, &ModbusTcpMaster::connectionStateChanged, this, &IntegrationPluginModbusCommander::onConnectionStateChanged);
        connect(modbusTCPMaster, &ModbusTcpMaster::writeRequestExecuted, this, &IntegrationPluginModbusCommander::onRequestExecuted);
        connect(modbusTCPMaster, &ModbusTcpMaster::writeRequestError, this, &IntegrationPluginModbusCommander::onRequestError);
        connect(modbusTCPMaster, &ModbusTcpMaster::receivedCoil, this, &IntegrationPluginModbusCommander::onReceivedCoil);
        connect(modbusTCPMaster, &ModbusTcpMaster::receivedDiscreteInput, this, &IntegrationPluginModbusCommander::onReceivedDiscreteInput);
        connect(modbusTCPMaster, &ModbusTcpMaster::receivedHoldingRegister, this, &IntegrationPluginModbusCommander::onReceivedHoldingRegister);
        connect(modbusTCPMaster, &ModbusTcpMaster::receivedInputRegister, this, &IntegrationPluginModbusCommander::onReceivedInputRegister);
        connect(modbusTCPMaster, &ModbusTcpMaster::connectionStateChanged, info, [info, modbusTCPMaster, this] (bool connected) {
            if (connected) {
                info->finish(Thing::ThingErrorNoError);
                m_modbusTCPMasters.insert(info->thing(), modbusTCPMaster);
            }
        });
        connect(thing, &Thing::settingChanged, thing, [thing, modbusTCPMaster] (const ParamTypeId &paramTypeId, const QVariant &value) {
            if (paramTypeId == modbusTCPClientSettingsNumberOfRetriesParamTypeId) {
                qCDebug(dcModbusCommander()) << "Set number of retries" << thing->name() << value.toUInt();
                modbusTCPMaster->setNumberOfRetries(value.toUInt());
            } else if (paramTypeId == modbusTCPClientSettingsTimeoutParamTypeId) {
                qCDebug(dcModbusCommander()) << "Set timeout " << thing->name() << value.toUInt();
                modbusTCPMaster->setTimeout(value.toUInt());
            }
        });
        modbusTCPMaster->connectDevice();

    } else if (thing->thingClassId() == modbusRTUClientThingClassId) {
        QUuid modbusUuid = thing->paramValue(modbusRTUClientThingModbusMasterUuidParamTypeId).toUuid();

        if (!hardwareManager()->modbusRtuResource()->available()) {
            qCWarning(dcModbusCommander()) << "Cannot set up thing" << thing << ". The modbus RTU hardware resource is not available.";
            info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The modbus RTU hardware resource is not available"));
            return;
        }

        if (!hardwareManager()->modbusRtuResource()->hasModbusRtuMaster(modbusUuid)) {
            qCWarning(dcModbusCommander()) << "Cannot set up thing" << thing << ". The modbus RTU hardware resource" << modbusUuid.toString() << "does not exist any more. Reconfiguration required.";
            info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Configured modbus RTU master could not be found. Please reconfigure the client and assign a new valid modbus RTU master."));
            return;
        }

        ModbusRtuMaster *modbusMaster = hardwareManager()->modbusRtuResource()->getModbusRtuMaster(modbusUuid);
        qCDebug(dcModbusCommander()) << "Setting up" << thing << "using" << modbusMaster;
        m_modbusRtuMasters.insert(thing, modbusMaster);

        connect(modbusMaster, &ModbusRtuMaster::connectedChanged, thing, [=](bool connected){
            qCDebug(dcModbusCommander()) << "Modbus RTU client" << modbusMaster << "connected changed" << connected;
            thing->setStateValue(modbusRTUClientConnectedStateTypeId, connected);

            // Note: only set the connected state for the child things if disconnected.
            // The child things will be evaluated upon read requests if the slave is connected or not.
            if (!connected) {
                foreach (Thing *childThing, myThings()) {
                    if (childThing->parentId() == thing->id()) {
                        thing->setStateValue(m_connectedStateTypeId[childThing->thingClassId()], connected);
                    }
                }
            }
        });

        info->finish(Thing::ThingErrorNoError);
    } else if ((thing->thingClassId() == coilThingClassId)
               || (thing->thingClassId() == discreteInputThingClassId)
               || (thing->thingClassId() == holdingRegisterThingClassId)
               || (thing->thingClassId() == inputRegisterThingClassId)) {
        qCDebug(dcModbusCommander()) << "Setting up modbus register" << thing->name();
        info->finish(Thing::ThingErrorNoError);

    } else {
        Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
    }
}


void IntegrationPluginModbusCommander::postSetupThing(Thing *thing)
{
    qCDebug(dcModbusCommander()) << "Post setup thing" << thing->name();
    if (!m_refreshTimer) {
        int refreshTime = configValue(modbusCommanderPluginUpdateIntervalParamTypeId).toInt();
        qCDebug(dcModbusCommander()) << "Starting refresh timer with interval" << refreshTime << "s";
        m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(refreshTime);
        connect(m_refreshTimer, &PluginTimer::timeout, this, [this] {
            foreach (Thing *thing, myThings()) {
                if ((thing->thingClassId() == coilThingClassId) ||
                        (thing->thingClassId() == discreteInputThingClassId) ||
                        (thing->thingClassId() == holdingRegisterThingClassId) ||
                        (thing->thingClassId() == inputRegisterThingClassId)) {
                    readRegister(thing);
                }
            }
        });
    }

    if ((thing->thingClassId() == modbusRTUClientThingClassId) ||
            (thing->thingClassId() == modbusTCPClientThingClassId)) {
        thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true);
    } else if ((thing->thingClassId() == coilThingClassId) ||
               (thing->thingClassId() == discreteInputThingClassId) ||
               (thing->thingClassId() == holdingRegisterThingClassId) ||
               (thing->thingClassId() == inputRegisterThingClassId)) {
        readRegister(thing);
    } else {
        Q_ASSERT_X(false, "postSetupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
    }
}


void IntegrationPluginModbusCommander::executeAction(ThingActionInfo *info)
{
    Thing *thing = info->thing();
    Action action = info->action();

    if (thing->thingClassId() == coilThingClassId) {

        if (action.actionTypeId() == coilValueActionTypeId) {
            writeRegister(thing, info);
            return;
        } else {
            Q_ASSERT_X(false, "Execute action", QString("Unhandled action type id: %1").arg(action.actionTypeId().toString()).toUtf8());
        }
    } else if (thing->thingClassId() == holdingRegisterThingClassId) {

        if (action.actionTypeId() == holdingRegisterValueActionTypeId) {
            writeRegister(thing, info);
            return;
        } else {
            Q_ASSERT_X(false, "Execute action", QString("Unhandled action type id: %1").arg(action.actionTypeId().toString()).toUtf8());
        }
    } else {
        Q_ASSERT_X(false, "Execute action", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
    }
}


void IntegrationPluginModbusCommander::thingRemoved(Thing *thing)
{
    qCDebug(dcModbusCommander()) << "Removing thing" << thing->name();
    if (thing->thingClassId() == modbusTCPClientThingClassId) {
        m_modbusTCPMasters.take(thing)->deleteLater();
    } else if (thing->thingClassId() == modbusRTUClientThingClassId) {
        m_modbusRtuMasters.take(thing)->deleteLater();
    }

    if (myThings().empty()) {
        qCDebug(dcModbusCommander()) << "No more Modbus commander things, stopping timer";
        hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer);
        m_refreshTimer = nullptr;
    }
}

void IntegrationPluginModbusCommander::onPluginConfigurationChanged(const ParamTypeId &paramTypeId, const QVariant &value)
{
    // Check refresh schedule
    if (paramTypeId == modbusCommanderPluginUpdateIntervalParamTypeId) {;
        qCDebug(dcModbusCommander()) << "Update interval has changed to" << value.toUInt() << "s";
        if (m_refreshTimer) {
            uint refreshTime = value.toUInt();
            m_refreshTimer->stop();
            m_refreshTimer->startTimer(refreshTime);
        } else {
            qCWarning(dcModbusCommander()) << "Update interval changed but refresh timer is not initialized";
        }
    }
}

void IntegrationPluginModbusCommander::onConnectionStateChanged(bool status)
{
    auto modbus = sender();
    if (m_modbusTCPMasters.values().contains(static_cast<ModbusTcpMaster *>(modbus))) {
        Thing *thing = m_modbusTCPMasters.key(static_cast<ModbusTcpMaster *>(modbus));
        qCDebug(dcModbusCommander()) << "Connections state changed" << thing->name() << status;
        thing->setStateValue(modbusTCPClientConnectedStateTypeId, status);
    }
}

void IntegrationPluginModbusCommander::onRequestExecuted(QUuid requestId, bool success)
{
    if (m_asyncActions.contains(requestId)){
        ThingActionInfo *info = m_asyncActions.take(requestId);
        info->thing()->setStateValue(m_connectedStateTypeId.value(info->thing()->thingClassId()), success);
        if (success){
            info->finish(Thing::ThingErrorNoError);
        } else {
            info->finish(Thing::ThingErrorHardwareNotAvailable);
        }
    }

    if (m_readRequests.contains(requestId)){
        Thing *thing = m_readRequests.take(requestId);
        thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), success);
    }
}

void IntegrationPluginModbusCommander::onRequestError(QUuid requestId, const QString &error)
{
    if (m_asyncActions.contains(requestId)){
        ThingActionInfo *info = m_asyncActions.take(requestId);
        info->finish(Thing::ThingErrorHardwareNotAvailable, error);
        info->thing()->setStateValue(m_connectedStateTypeId.value(info->thing()->thingClassId()), false);
    }

    if (m_readRequests.contains(requestId)){
        Thing *thing = m_readRequests.take(requestId);
        thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), false);
    }
}

void IntegrationPluginModbusCommander::onReceivedCoil(quint32 slaveAddress, quint32 modbusRegister, const QVector<quint16> &values)
{
    auto modbus = sender();
    if (m_modbusTCPMasters.values().contains(static_cast<ModbusTcpMaster *>(modbus))) {
        Thing *parent = m_modbusTCPMasters.key(static_cast<ModbusTcpMaster *>(modbus));
        foreach (Thing *thing, myThings().filterByParentId(parent->id())) {
            if (thing->thingClassId() == coilThingClassId) {
                if ((thing->paramValue(m_slaveAddressParamTypeId.value(thing->thingClassId())) == slaveAddress)
                        && (thing->paramValue(m_registerAddressParamTypeId.value(thing->thingClassId())) == modbusRegister)) {
                    thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), values[0]);
                    thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true);
                    return;
                }
            }
        }
    }
}

void IntegrationPluginModbusCommander::onReceivedDiscreteInput(quint32 slaveAddress, quint32 modbusRegister, const QVector<quint16> &values)
{
    auto modbus = sender();

    if (m_modbusTCPMasters.values().contains(static_cast<ModbusTcpMaster *>(modbus))) {
        Thing *parent = m_modbusTCPMasters.key(static_cast<ModbusTcpMaster *>(modbus));
        foreach (Thing *thing, myThings().filterByParentId(parent->id())) {
            if (thing->thingClassId() == discreteInputThingClassId) {
                if ((thing->paramValue(m_slaveAddressParamTypeId.value(thing->thingClassId())) == slaveAddress)
                        && (thing->paramValue(m_registerAddressParamTypeId.value(thing->thingClassId())) == modbusRegister)) {
                    thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), values[0]);
                    thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true);
                    return;
                }
            }
        }
    }
}

void IntegrationPluginModbusCommander::onReceivedHoldingRegister(uint slaveAddress, uint modbusRegister, const QVector<quint16> &values)
{
    auto modbus = sender();

    if (m_modbusTCPMasters.values().contains(static_cast<ModbusTcpMaster *>(modbus))) {
        Thing *parent = m_modbusTCPMasters.key(static_cast<ModbusTcpMaster *>(modbus));
        foreach (Thing *thing, myThings().filterByParentId(parent->id())) {
            if (thing->thingClassId() == holdingRegisterThingClassId) {
                if ((thing->paramValue(m_slaveAddressParamTypeId.value(thing->thingClassId())) == slaveAddress)
                        && (thing->paramValue(m_registerAddressParamTypeId.value(thing->thingClassId())) == modbusRegister)) {
                    thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), values[0]);
                    thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true);
                    return;
                }
            }
        }
    }
}

void IntegrationPluginModbusCommander::onReceivedInputRegister(uint slaveAddress, uint modbusRegister, const QVector<quint16> &values)
{
    auto modbus = sender();

    if (m_modbusTCPMasters.values().contains(static_cast<ModbusTcpMaster *>(modbus))) {
        Thing *parent = m_modbusTCPMasters.key(static_cast<ModbusTcpMaster *>(modbus));
        foreach (Thing *thing, myThings().filterByParentId(parent->id())) {
            if (thing->thingClassId() == inputRegisterThingClassId) {
                if ((thing->paramValue(m_slaveAddressParamTypeId.value(thing->thingClassId())) == slaveAddress)
                        && (thing->paramValue(m_registerAddressParamTypeId.value(thing->thingClassId())) == modbusRegister)) {
                    thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), values[0]);
                    thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true);
                    return;
                }
            }
        }
    }
}

void IntegrationPluginModbusCommander::readRegister(Thing *thing)
{
    Thing *parent = myThings().findById(thing->parentId());
    if (!parent) {
        qCWarning(dcModbusCommander()) << "Could not find parent device" << thing->name();
        return;
    }

    uint registerAddress = thing->paramValue(m_registerAddressParamTypeId.value(thing->thingClassId())).toUInt();;
    uint slaveAddress = thing->paramValue(m_slaveAddressParamTypeId.value(thing->thingClassId())).toUInt();

    QUuid requestId;

    if (parent->thingClassId() == modbusTCPClientThingClassId) {
        ModbusTcpMaster *modbus = m_modbusTCPMasters.value(parent);
        if (!modbus)
            return;

        if (!modbus->connected())
            return; // Send requests only if the modbus interface is connected

        if (thing->thingClassId() == coilThingClassId) {
            requestId = modbus->readCoil(slaveAddress, registerAddress);
        } else if (thing->thingClassId() == discreteInputThingClassId) {
            requestId = modbus->readDiscreteInput(slaveAddress, registerAddress);
        } else if (thing->thingClassId() == holdingRegisterThingClassId) {
            requestId = modbus->readHoldingRegister(slaveAddress, registerAddress);
        } else if (thing->thingClassId() == inputRegisterThingClassId) {
            requestId = modbus->readInputRegister(slaveAddress, registerAddress);
        }
    } else if (parent->thingClassId() == modbusRTUClientThingClassId) {

        ModbusRtuMaster *modbusMaster = m_modbusRtuMasters.value(parent);
        if (!modbusMaster)
            return;

        if (!modbusMaster->connected())
            return; // Send requests only if the modbus interface is connected

        if (thing->thingClassId() == coilThingClassId) {
            ModbusRtuReply *reply = modbusMaster->readCoil(slaveAddress, registerAddress);
            connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){
                if (reply->error() != ModbusRtuReply::NoError) {
                    qCWarning(dcModbusCommander()) << "Failed to read coil from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress;
                    thing->setStateValue(m_connectedStateTypeId[thing->thingClassId()], false);
                    return;
                }

                if (!reply->result().isEmpty()) {
                    thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), reply->result().at(0));
                }
                thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true);
            });
        } else if (thing->thingClassId() == discreteInputThingClassId) {
            ModbusRtuReply *reply = modbusMaster->readDiscreteInput(slaveAddress, registerAddress);
            connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){
                if (reply->error() != ModbusRtuReply::NoError) {
                    qCWarning(dcModbusCommander()) << "Failed to read discrete input from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress;
                    thing->setStateValue(m_connectedStateTypeId[thing->thingClassId()], false);
                    return;
                }

                if (!reply->result().isEmpty()) {
                    thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), reply->result().at(0));
                }
                thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true);
            });
        } else if (thing->thingClassId() == holdingRegisterThingClassId) {
            ModbusRtuReply *reply = modbusMaster->readHoldingRegister(slaveAddress, registerAddress);
            connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){
                if (reply->error() != ModbusRtuReply::NoError) {
                    qCWarning(dcModbusCommander()) << "Failed to read holding register from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress;
                    thing->setStateValue(m_connectedStateTypeId[thing->thingClassId()], false);
                    return;
                }

                if (!reply->result().isEmpty()) {
                    thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), reply->result().at(0));
                }
                thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true);
            });
        } else if (thing->thingClassId() == inputRegisterThingClassId) {
            ModbusRtuReply *reply = modbusMaster->readInputRegister(slaveAddress, registerAddress);
            connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){
                if (reply->error() != ModbusRtuReply::NoError) {
                    qCWarning(dcModbusCommander()) << "Failed to read input register from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress;
                    thing->setStateValue(m_connectedStateTypeId[thing->thingClassId()], false);
                    return;
                }

                if (!reply->result().isEmpty()) {
                    thing->setStateValue(m_valueStateTypeId.value(thing->thingClassId()), reply->result().at(0));
                }
                thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), true);
            });
        }

        // Note: we don't want proceed with the method here, since we are not
        // working with the requestId any more on RTU
        return;
    }

    if (!requestId.isNull()) {
        m_readRequests.insert(requestId, thing);
        QTimer::singleShot(5000, this, [requestId, this] {m_readRequests.remove(requestId);});
    } else {
        // Request returned without an id
        thing->setStateValue(m_connectedStateTypeId.value(thing->thingClassId()), false);
    }
}

void IntegrationPluginModbusCommander::writeRegister(Thing *thing, ThingActionInfo *info)
{
    Thing *parent = myThings().findById(thing->parentId());
    if (!parent) {
        qCWarning(dcModbusCommander()) << "Could not find parent device" << thing->name();
        info->finish(Thing::ThingErrorHardwareNotAvailable);
        return;
    }

    uint registerAddress = thing->paramValue(m_registerAddressParamTypeId.value(thing->thingClassId())).toUInt();;
    uint slaveAddress = thing->paramValue(m_slaveAddressParamTypeId.value(thing->thingClassId())).toUInt();

    QUuid requestId;
    Action action = info->action();

    if (parent->thingClassId() == modbusTCPClientThingClassId) {
        ModbusTcpMaster *modbus = m_modbusTCPMasters.value(parent);
        if (!modbus) {
            qCWarning(dcModbusCommander()) << "Could not find modbus TCP master for" << thing;
            info->finish(Thing::ThingErrorHardwareNotAvailable);
            return;
        }

        if (thing->thingClassId() == coilThingClassId) {
            requestId = modbus->writeCoil(slaveAddress, registerAddress, action.param(coilValueActionValueParamTypeId).value().toBool());
        } else if (thing->thingClassId() == holdingRegisterThingClassId) {
            requestId = modbus->writeHoldingRegister(slaveAddress, registerAddress, action.param(holdingRegisterValueActionValueParamTypeId).value().toUInt());
        }

    } else if (parent->thingClassId() == modbusRTUClientThingClassId) {
        ModbusRtuMaster *modbusMaster = m_modbusRtuMasters.value(parent);
        if (!modbusMaster) {
            qCWarning(dcModbusCommander()) << "Could not find modbus RTU master for" << thing;
            info->finish(Thing::ThingErrorHardwareNotAvailable);
            return;
        }

        if (thing->thingClassId() == coilThingClassId) {
            QVector<quint16> values;
            values.append(static_cast<quint16>(action.param(coilValueActionValueParamTypeId).value().toBool()));

            ModbusRtuReply *reply = modbusMaster->writeCoils(slaveAddress, registerAddress, values);
            connect(info, &ThingActionInfo::aborted, reply, &ModbusRtuReply::deleteLater);
            connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){
                if (reply->error() != ModbusRtuReply::NoError) {
                    qCWarning(dcModbusCommander()) << "Failed to write coils from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress << values << reply->errorString();
                    info->finish(Thing::ThingErrorHardwareFailure);
                    return;
                }
                thing->setStateValue("value", action.param(coilValueActionValueParamTypeId).value().toBool());
                info->finish(Thing::ThingErrorNoError);
            });
        } else if (thing->thingClassId() == holdingRegisterThingClassId) {
            QVector<quint16> values;
            values.append(static_cast<quint16>(action.param(holdingRegisterValueActionValueParamTypeId).value().toUInt()));

            ModbusRtuReply *reply = modbusMaster->writeHoldingRegisters(slaveAddress, registerAddress, values);
            connect(info, &ThingActionInfo::aborted, reply, &ModbusRtuReply::deleteLater);
            connect(reply, &ModbusRtuReply::finished, modbusMaster, [=](){
                if (reply->error() != ModbusRtuReply::NoError) {
                    qCWarning(dcModbusCommander()) << "Failed to write holding registers from" << modbusMaster << "slave:" << slaveAddress << "register:" << registerAddress << values << reply->errorString();
                    info->finish(Thing::ThingErrorHardwareFailure);
                    return;
                }

                thing->setStateValue("value", action.param(holdingRegisterValueActionValueParamTypeId).value().toUInt());
                info->finish(Thing::ThingErrorNoError);
            });
        }

        // Note: we don't want proceed with the method here, since we are not
        // working with the requestId any more on RTU
        return;
    }

    if (requestId.toString().isNull()){
        info->finish(Thing::ThingErrorHardwareNotAvailable);
    } else {
        m_asyncActions.insert(requestId, info);
        connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
    }
}
