305 lines
12 KiB
C++
305 lines
12 KiB
C++
// 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 "plugininfo.h"
|
|
#include "integrationpluginmypv.h"
|
|
|
|
#include <QUdpSocket>
|
|
#include <QHostAddress>
|
|
|
|
IntegrationPluginMyPv::IntegrationPluginMyPv()
|
|
{
|
|
}
|
|
|
|
|
|
void IntegrationPluginMyPv::discoverThings(ThingDiscoveryInfo *info)
|
|
{
|
|
if (info->thingClassId() == elwaThingClassId) {
|
|
QUdpSocket *searchSocket = new QUdpSocket(this);
|
|
|
|
// Note: This will fail, and it's not a problem, but it is required to force the socket to stick to IPv4...
|
|
searchSocket->bind(QHostAddress::AnyIPv4, 16124);
|
|
|
|
QByteArray discoveryString;
|
|
discoveryString.resize(19);
|
|
discoveryString.fill(0);
|
|
discoveryString.insert(0, QByteArray::fromHex("86d93efc"));
|
|
|
|
discoveryString.insert(4, "AC ELWA-E");
|
|
qCDebug(dcMypv()) << "Send datagram:" << discoveryString << "length: " << discoveryString.length();
|
|
qint64 len = searchSocket->writeDatagram(discoveryString, QHostAddress("255.255.255.255"), 16124);
|
|
if (len != discoveryString.length()) {
|
|
searchSocket->deleteLater();
|
|
info->finish(Thing::ThingErrorHardwareNotAvailable , tr("Error starting device discovery"));
|
|
return;
|
|
}
|
|
|
|
QTimer::singleShot(2000, this, [this, searchSocket, info](){
|
|
QList<ThingDescriptor> descriptorList;
|
|
while(searchSocket->hasPendingDatagrams()) {
|
|
char buffer[1024];
|
|
QHostAddress senderAddress;
|
|
int len = searchSocket->readDatagram(buffer, 1024, &senderAddress);
|
|
QByteArray data = QByteArray::fromRawData(buffer, len);
|
|
qCDebug(dcMypv()) << "Have datagram:" << data;
|
|
if (data.length() < 64) {
|
|
continue;
|
|
}
|
|
|
|
//Device Id AC•THOR = 0x4e84
|
|
//Device Id Power = 0x4e8e
|
|
//Device Id AC ELWA-E = 0x3efc
|
|
qCDebug(dcMypv()) << "device Id:" << data.mid(2, 2);
|
|
if (data.mid(2, 2) == QByteArray::fromHex("3efc")) {
|
|
qCDebug(dcMypv()) << "Found Device: AC ElWA-E";
|
|
} else if (data.mid(2, 2) == QByteArray::fromHex("0x4e8e")) {
|
|
qCDebug(dcMypv()) << "Found Device: Powermeter";
|
|
} else if (data.mid(2, 2) == QByteArray::fromHex("0x4e84")) {
|
|
qCDebug(dcMypv()) << "Found Device: AC Thor";
|
|
} else {
|
|
qCDebug(dcMypv()) << "Failed to parse discovery datagram from" << senderAddress << data;
|
|
continue;
|
|
}
|
|
|
|
ThingDescriptor thingDescriptors(info->thingClassId(), "AC ELWA-E", senderAddress.toString());
|
|
QByteArray serialNumber = data.mid(8, 16);
|
|
|
|
foreach (Thing *existingThing, myThings()) {
|
|
if (serialNumber == existingThing->paramValue(elwaThingSerialNumberParamTypeId).toString()) {
|
|
qCDebug(dcMypv()) << "Rediscovered device " << existingThing->name();
|
|
thingDescriptors.setThingId(existingThing->id());
|
|
break;
|
|
}
|
|
}
|
|
|
|
ParamList params;
|
|
params << Param(elwaThingIpAddressParamTypeId, senderAddress.toString());
|
|
params << Param(elwaThingSerialNumberParamTypeId, serialNumber);
|
|
thingDescriptors.setParams(params);
|
|
descriptorList << thingDescriptors;
|
|
}
|
|
info->addThingDescriptors(descriptorList);;
|
|
searchSocket->deleteLater();
|
|
info->finish(Thing::ThingErrorNoError);
|
|
});
|
|
} else {
|
|
Q_ASSERT_X(false, "discoverThings", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8());
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginMyPv::setupThing(ThingSetupInfo *info)
|
|
{
|
|
Thing *thing = info->thing();
|
|
|
|
if(thing->thingClassId() == elwaThingClassId) {
|
|
QHostAddress address = QHostAddress(thing->paramValue(elwaThingIpAddressParamTypeId).toString());
|
|
ModbusTcpMaster *modbusTcpMaster = new ModbusTcpMaster(address, 502, this);
|
|
connect(modbusTcpMaster, &ModbusTcpMaster::connectionStateChanged, this, &IntegrationPluginMyPv::onConnectionStateChanged);
|
|
connect(modbusTcpMaster, &ModbusTcpMaster::receivedHoldingRegister, this, &IntegrationPluginMyPv::onReceivedHoldingRegister);
|
|
connect(modbusTcpMaster, &ModbusTcpMaster::writeRequestExecuted, this, &IntegrationPluginMyPv::onWriteRequestExecuted);
|
|
connect(modbusTcpMaster, &ModbusTcpMaster::writeRequestError, this, &IntegrationPluginMyPv::onWriteRequestError);
|
|
|
|
m_modbusTcpMasters.insert(thing, modbusTcpMaster);
|
|
} else {
|
|
Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginMyPv::postSetupThing(Thing *thing)
|
|
{
|
|
if (!m_refreshTimer) {
|
|
m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(10);
|
|
connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginMyPv::onRefreshTimer);
|
|
}
|
|
|
|
if (thing->thingClassId() == elwaThingClassId) {
|
|
update(thing);
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginMyPv::thingRemoved(Thing *thing)
|
|
{
|
|
if (thing->thingClassId() == elwaThingClassId) {
|
|
ModbusTcpMaster *modbusTCPMaster = m_modbusTcpMasters.take(thing);
|
|
modbusTCPMaster->deleteLater();
|
|
}
|
|
|
|
if (myThings().isEmpty()) {
|
|
hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer);
|
|
m_refreshTimer = nullptr;
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginMyPv::executeAction(ThingActionInfo *info)
|
|
{
|
|
Thing *thing = info->thing();
|
|
Action action = info->action();
|
|
|
|
if (thing->thingClassId() == elwaThingClassId) {
|
|
|
|
ModbusTcpMaster *modbusTCPMaster = m_modbusTcpMasters.value(thing);
|
|
if (action.actionTypeId() == elwaHeatingPowerActionTypeId) {
|
|
int heatingPower = action.param(elwaHeatingPowerActionHeatingPowerParamTypeId).value().toInt();
|
|
QUuid requestId = modbusTCPMaster->writeHoldingRegister(0xff, ElwaModbusRegisters::Power, heatingPower);
|
|
if (requestId.isNull()) {
|
|
info->finish(Thing::ThingErrorHardwareNotAvailable);
|
|
} else {
|
|
m_asyncActions.insert(requestId, info);
|
|
connect(info, &ThingActionInfo::aborted, this, [this, requestId] {m_asyncActions.remove(requestId);});
|
|
}
|
|
} else if (action.actionTypeId() == elwaPowerActionTypeId) {
|
|
bool power = action.param(elwaHeatingPowerActionHeatingPowerParamTypeId).value().toBool();
|
|
if(power) {
|
|
QUuid requestId = modbusTCPMaster->writeHoldingRegister(0xff, ElwaModbusRegisters::ManuelStart, 1);
|
|
if (requestId.isNull()) {
|
|
info->finish(Thing::ThingErrorHardwareNotAvailable);
|
|
} else {
|
|
m_asyncActions.insert(requestId, info);
|
|
connect(info, &ThingActionInfo::aborted, this, [this, requestId] {m_asyncActions.remove(requestId);});
|
|
}
|
|
}
|
|
} else {
|
|
Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8());
|
|
}
|
|
} else {
|
|
Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginMyPv::onRefreshTimer()
|
|
{
|
|
foreach (Thing *thing, myThings()) {
|
|
update(thing);
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginMyPv::onConnectionStateChanged(bool status)
|
|
{
|
|
ModbusTcpMaster *modbusTcpMaster = static_cast<ModbusTcpMaster *>(sender());
|
|
Thing *thing = m_modbusTcpMasters.key(modbusTcpMaster);
|
|
if (!thing)
|
|
return;
|
|
thing->setStateValue(elwaConnectedStateTypeId, status);
|
|
}
|
|
|
|
void IntegrationPluginMyPv::onWriteRequestExecuted(QUuid requestId, bool success)
|
|
{
|
|
if (m_asyncActions.contains(requestId)) {
|
|
ThingActionInfo *info = m_asyncActions.value(requestId);
|
|
if (success) {
|
|
info->finish(Thing::ThingErrorNoError);
|
|
} else {
|
|
info->finish(Thing::ThingErrorHardwareNotAvailable);
|
|
}
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginMyPv::onWriteRequestError(QUuid requestId, const QString &error)
|
|
{
|
|
Q_UNUSED(requestId)
|
|
qCWarning(dcMypv()) << "Modbus error "<< error;
|
|
}
|
|
|
|
void IntegrationPluginMyPv::onReceivedHoldingRegister(quint32 slaveAddress, quint32 modbusRegister, const QVector<quint16> &value)
|
|
{
|
|
Q_UNUSED(slaveAddress)
|
|
ModbusTcpMaster *modbusTcpMaster = static_cast<ModbusTcpMaster *>(sender());
|
|
Thing *thing = m_modbusTcpMasters.key(modbusTcpMaster);
|
|
if (!thing)
|
|
return;
|
|
|
|
if(modbusRegister == ElwaModbusRegisters::Status) {
|
|
switch (ElwaStatus(value[0])) {
|
|
case ElwaStatus::Heating: {
|
|
thing->setStateValue(elwaStatusStateTypeId, "Heating");
|
|
thing->setStateValue(elwaPowerStateTypeId, true);
|
|
break;
|
|
}
|
|
case ElwaStatus::Standby:{
|
|
thing->setStateValue(elwaStatusStateTypeId, "Standby");
|
|
thing->setStateValue(elwaPowerStateTypeId, false);
|
|
break;
|
|
}
|
|
case ElwaStatus::Boosted:{
|
|
thing->setStateValue(elwaStatusStateTypeId, "Boosted");
|
|
thing->setStateValue(elwaPowerStateTypeId, true);
|
|
break;
|
|
}
|
|
case ElwaStatus::HeatFinished:{
|
|
thing->setStateValue(elwaStatusStateTypeId, "Heat finished");
|
|
thing->setStateValue(elwaPowerStateTypeId, false);
|
|
break;
|
|
}
|
|
case ElwaStatus::Setup:{
|
|
thing->setStateValue(elwaStatusStateTypeId, "Setup");
|
|
thing->setStateValue(elwaPowerStateTypeId, false);
|
|
break;
|
|
}
|
|
case ElwaStatus::ErrorOvertempFuseBlown:{
|
|
thing->setStateValue(elwaStatusStateTypeId, "Error Overtemp Fuse blown");
|
|
break;
|
|
}
|
|
case ElwaStatus::ErrorOvertempMeasured:{
|
|
thing->setStateValue(elwaStatusStateTypeId, "Error Overtemp measured");
|
|
break;
|
|
}
|
|
case ElwaStatus::ErrorOvertempElectronics:{
|
|
thing->setStateValue(elwaStatusStateTypeId, "Error Overtemp Electronics");
|
|
break;
|
|
}
|
|
case ElwaStatus::ErrorHardwareFault:{
|
|
thing->setStateValue(elwaStatusStateTypeId, "Error Hardware Fault");
|
|
break;
|
|
}
|
|
case ElwaStatus::ErrorTempSensor:{
|
|
thing->setStateValue(elwaStatusStateTypeId, "Error Temp Sensor");
|
|
break;
|
|
}
|
|
default:
|
|
thing->setStateValue(elwaStatusStateTypeId, "Unknown");
|
|
}
|
|
} else if(modbusRegister == ElwaModbusRegisters::WaterTemperature) {
|
|
thing->setStateValue(elwaTemperatureStateTypeId, value[0]/10.00);
|
|
} else if(modbusRegister == ElwaModbusRegisters::TargetWaterTemperature) {
|
|
thing->setStateValue(elwaTargetWaterTemperatureStateTypeId, value[0]/10.00);
|
|
} else if(modbusRegister == ElwaModbusRegisters::Power) {
|
|
thing->setStateValue(elwaHeatingPowerStateTypeId, value[0]);
|
|
} else {
|
|
qCWarning(dcMypv()) << "Received unhandled modbus register";
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginMyPv::update(Thing *thing)
|
|
{
|
|
if (thing->thingClassId() == elwaThingClassId) {
|
|
ModbusTcpMaster *modbusTCPMaster = m_modbusTcpMasters.value(thing);
|
|
|
|
modbusTCPMaster->readHoldingRegister(0xff, ElwaModbusRegisters::Status);
|
|
modbusTCPMaster->readHoldingRegister(0xff, ElwaModbusRegisters::WaterTemperature);
|
|
modbusTCPMaster->readHoldingRegister(0xff, ElwaModbusRegisters::TargetWaterTemperature);
|
|
modbusTCPMaster->readHoldingRegister(0xff, ElwaModbusRegisters::Power);
|
|
}
|
|
}
|
|
|