/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2022, 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 . * * 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 "integrationpluginmennekes.h" #include "plugininfo.h" #include "amtronecudiscovery.h" #include "amtronhcc3discovery.h" #include #include IntegrationPluginMennekes::IntegrationPluginMennekes() { } void IntegrationPluginMennekes::discoverThings(ThingDiscoveryInfo *info) { if (!hardwareManager()->networkDeviceDiscovery()->available()) { qCWarning(dcMennekes()) << "The network discovery is not available on this platform."; info->finish(Thing::ThingErrorUnsupportedFeature, QT_TR_NOOP("The network device discovery is not available.")); return; } if (info->thingClassId() == amtronECUThingClassId) { AmtronECUDiscovery *discovery = new AmtronECUDiscovery(hardwareManager()->networkDeviceDiscovery(), info); connect(discovery, &AmtronECUDiscovery::discoveryFinished, info, [=](){ foreach (const AmtronECUDiscovery::Result &result, discovery->discoveryResults()) { QString name = "AMTRON Charge Control/Professional"; QString description = result.model.isEmpty() ? result.networkDeviceInfo.address().toString() : result.model + " (" + result.networkDeviceInfo.address().toString() + ")"; if (result.model.startsWith("CC")) { name = "AMTRON Charge Control"; } else if (result.model.startsWith("P")) { name = "AMTRON Professional"; } ThingDescriptor descriptor(amtronECUThingClassId, name, description); qCDebug(dcMennekes()) << "Discovered:" << descriptor.title() << descriptor.description(); // Check if we already have set up this device Things existingThings = myThings().filterByParam(amtronECUThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); if (existingThings.count() == 1) { qCDebug(dcMennekes()) << "This wallbox already exists in the system:" << result.networkDeviceInfo; descriptor.setThingId(existingThings.first()->id()); } ParamList params; params << Param(amtronECUThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); // Note: if we discover also the port and modbusaddress, we must fill them in from the discovery here, for now everywhere the defaults... descriptor.setParams(params); info->addThingDescriptor(descriptor); } info->finish(Thing::ThingErrorNoError); }); discovery->startDiscovery(); } else if (info->thingClassId() == amtronHCC3ThingClassId) { AmtronHCC3Discovery *discovery = new AmtronHCC3Discovery(hardwareManager()->networkDeviceDiscovery(), info); connect(discovery, &AmtronHCC3Discovery::discoveryFinished, info, [=](){ foreach (const AmtronHCC3Discovery::AmtronDiscoveryResult &result, discovery->discoveryResults()) { ThingDescriptor descriptor(amtronHCC3ThingClassId, result.wallboxName, "Serial: " + result.serialNumber + " - " + result.networkDeviceInfo.address().toString()); qCDebug(dcMennekes()) << "Discovered:" << descriptor.title() << descriptor.description(); // Check if we already have set up this device Things existingThings = myThings().filterByParam(amtronHCC3ThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); if (existingThings.count() == 1) { qCDebug(dcMennekes()) << "This wallbox already exists in the system:" << result.networkDeviceInfo; descriptor.setThingId(existingThings.first()->id()); } ParamList params; params << Param(amtronHCC3ThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); // Note: if we discover also the port and modbusaddress, we must fill them in from the discovery here, for now everywhere the defaults... descriptor.setParams(params); info->addThingDescriptor(descriptor); } info->finish(Thing::ThingErrorNoError); }); discovery->startDiscovery(); } } void IntegrationPluginMennekes::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); qCDebug(dcMennekes()) << "Setup" << thing << thing->params(); if (thing->thingClassId() == amtronECUThingClassId) { if (m_amtronECUConnections.contains(thing)) { qCDebug(dcMennekes()) << "Reconfiguring existing thing" << thing->name(); m_amtronECUConnections.take(thing)->deleteLater(); if (m_monitors.contains(thing)) { hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); } } MacAddress macAddress = MacAddress(thing->paramValue(amtronECUThingMacAddressParamTypeId).toString()); if (!macAddress.isValid()) { qCWarning(dcMennekes()) << "The configured mac address is not valid" << thing->params(); info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure the thing.")); return; } NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress); m_monitors.insert(thing, monitor); QHostAddress address = monitor->networkDeviceInfo().address(); if (address.isNull()) { qCWarning(dcMennekes()) << "Cannot set up thing. The host address is not known yet..."; hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The host address is not known yet. Trying later again.")); return; } connect(info, &ThingSetupInfo::aborted, monitor, [=](){ if (m_monitors.contains(thing)) { qCDebug(dcMennekes()) << "Unregistering monitor because setup has been aborted."; hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); } }); if (monitor->reachable()) { setupAmtronECUConnection(info); } else { qCDebug(dcMennekes()) << "Waiting for the network monitor to get reachable before continue to set up the connection" << thing->name() << address.toString() << "..."; connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ if (reachable) { qCDebug(dcMennekes()) << "The monitor for thing setup" << thing->name() << "is now reachable. Continue setup..."; setupAmtronECUConnection(info); } }); } return; } if (info->thing()->thingClassId() == amtronHCC3ThingClassId) { if (m_amtronHCC3Connections.contains(thing)) { qCDebug(dcMennekes()) << "Reconfiguring existing thing" << thing->name(); m_amtronHCC3Connections.take(thing)->deleteLater(); if (m_monitors.contains(thing)) { hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); } } MacAddress macAddress = MacAddress(thing->paramValue(amtronHCC3ThingMacAddressParamTypeId).toString()); if (!macAddress.isValid()) { qCWarning(dcMennekes()) << "The configured mac address is not valid" << thing->params(); info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The MAC address is not known. Please reconfigure the thing.")); return; } NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress); m_monitors.insert(thing, monitor); QHostAddress address = monitor->networkDeviceInfo().address(); if (address.isNull()) { qCWarning(dcMennekes()) << "Cannot set up thing. The host address is not known yet..."; hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The host address is not known yet. Trying later again.")); return; } connect(info, &ThingSetupInfo::aborted, monitor, [=](){ if (m_monitors.contains(thing)) { qCDebug(dcMennekes()) << "Unregistering monitor because setup has been aborted."; hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); } }); if (monitor->reachable()) { setupAmtronHCC3Connection(info); } else { qCDebug(dcMennekes()) << "Waiting for the network monitor to get reachable before continue to set up the connection" << thing->name() << address.toString() << "..."; connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){ if (reachable) { qCDebug(dcMennekes()) << "The monitor for thing setup" << thing->name() << "is now reachable. Continue setup..."; setupAmtronHCC3Connection(info); } }); } return; } } void IntegrationPluginMennekes::postSetupThing(Thing *thing) { Q_UNUSED(thing) if (!m_pluginTimer) { qCDebug(dcMennekes()) << "Starting plugin timer..."; m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); connect(m_pluginTimer, &PluginTimer::timeout, this, [this] { foreach(AmtronECUModbusTcpConnection *connection, m_amtronECUConnections) { qCDebug(dcMennekes()) << "Updating connection" << connection->hostAddress().toString(); connection->update(); } foreach(AmtronHCC3ModbusTcpConnection *connection, m_amtronHCC3Connections) { qCDebug(dcMennekes()) << "Updating connection" << connection->hostAddress().toString(); connection->update(); } }); m_pluginTimer->start(); } } void IntegrationPluginMennekes::executeAction(ThingActionInfo *info) { if (info->thing()->thingClassId() == amtronECUThingClassId) { AmtronECUModbusTcpConnection *amtronECUConnection = m_amtronECUConnections.value(info->thing()); if (info->action().actionTypeId() == amtronECUPowerActionTypeId) { bool power = info->action().paramValue(amtronECUPowerActionPowerParamTypeId).toBool(); QModbusReply *reply = amtronECUConnection->setHemsCurrentLimit(power ? info->thing()->stateValue(amtronECUMaxChargingCurrentStateTypeId).toUInt() : 0); connect(reply, &QModbusReply::finished, info, [info, reply, power](){ if (reply->error() == QModbusDevice::NoError) { info->thing()->setStateValue(amtronECUPowerStateTypeId, power); info->finish(Thing::ThingErrorNoError); } else { qCWarning(dcMennekes()) << "Error setting cp availability:" << reply->error() << reply->errorString(); info->finish(Thing::ThingErrorHardwareFailure); } }); } if (info->action().actionTypeId() == amtronECUMaxChargingCurrentActionTypeId) { int maxChargingCurrent = info->action().paramValue(amtronECUMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toInt(); QModbusReply *reply = amtronECUConnection->setHemsCurrentLimit(maxChargingCurrent); connect(reply, &QModbusReply::finished, info, [info, reply, maxChargingCurrent](){ if (reply->error() == QModbusDevice::NoError) { info->thing()->setStateValue(amtronECUMaxChargingCurrentStateTypeId, maxChargingCurrent); info->finish(Thing::ThingErrorNoError); } else { info->finish(Thing::ThingErrorHardwareFailure); } }); } } } void IntegrationPluginMennekes::thingRemoved(Thing *thing) { if (thing->thingClassId() == amtronECUThingClassId && m_amtronECUConnections.contains(thing)) { AmtronECUModbusTcpConnection *connection = m_amtronECUConnections.take(thing); delete connection; } if (thing->thingClassId() == amtronHCC3ThingClassId && m_amtronHCC3Connections.contains(thing)) { AmtronHCC3ModbusTcpConnection *connection = m_amtronHCC3Connections.take(thing); delete connection; } // Unregister related hardware resources if (m_monitors.contains(thing)) hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); if (myThings().isEmpty() && m_pluginTimer) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); m_pluginTimer = nullptr; } } void IntegrationPluginMennekes::updateECUPhaseCount(Thing *thing) { AmtronECUModbusTcpConnection *amtronECUConnection = m_amtronECUConnections.value(thing); int phaseCount = 0; qCDebug(dcMennekes()) << "Phases: L1" << amtronECUConnection->meterCurrentL1() << "L2" << amtronECUConnection->meterCurrentL2() << "L3" << amtronECUConnection->meterCurrentL3(); // the current idles on some 5 - 10 mA when not charging... // We want to detect the phases we're actually charging on. Checking the current flow for that if it's > 500mA // If no phase is charging, let's count all phases that are not 0 instead (to determine how many phases are connected at the wallbox) if (amtronECUConnection->meterCurrentL1() > 500) { phaseCount++; } if (amtronECUConnection->meterCurrentL2() > 500) { phaseCount++; } if (amtronECUConnection->meterCurrentL3() > 500) { phaseCount++; } qCDebug(dcMennekes()) << "Actively charging phases:" << phaseCount; if (phaseCount == 0) { if (amtronECUConnection->meterCurrentL1() > 0) { phaseCount++; } if (amtronECUConnection->meterCurrentL2() > 0) { phaseCount++; } if (amtronECUConnection->meterCurrentL3() > 0) { phaseCount++; } qCDebug(dcMennekes()) << "Connected phases:" << phaseCount; } thing->setStateValue(amtronECUPhaseCountStateTypeId, phaseCount); } void IntegrationPluginMennekes::setupAmtronECUConnection(ThingSetupInfo *info) { Thing *thing = info->thing(); QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address(); qCDebug(dcMennekes()) << "Setting up amtron wallbox on" << address.toString(); AmtronECUModbusTcpConnection *amtronECUConnection = new AmtronECUModbusTcpConnection(address, 502, 0xff, this); connect(info, &ThingSetupInfo::aborted, amtronECUConnection, &ModbusTCPMaster::deleteLater); // Reconnect on monitor reachable changed NetworkDeviceMonitor *monitor = m_monitors.value(thing); connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ qCDebug(dcMennekes()) << "Network device monitor reachable changed for" << thing->name() << reachable; if (!thing->setupComplete()) return; if (reachable && !thing->stateValue("connected").toBool()) { amtronECUConnection->setHostAddress(monitor->networkDeviceInfo().address()); amtronECUConnection->connectDevice(); } else if (!reachable) { // Note: We disable autoreconnect explicitly and we will // connect the device once the monitor says it is reachable again amtronECUConnection->disconnectDevice(); } }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::reachableChanged, thing, [thing, amtronECUConnection](bool reachable){ qCDebug(dcMennekes()) << "Reachable changed to" << reachable << "for" << thing; if (reachable) { amtronECUConnection->initialize(); } else { thing->setStateValue(amtronECUConnectedStateTypeId, false); } }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::initializationFinished, thing, [=](bool success){ if (!thing->setupComplete()) return; if (success) { thing->setStateValue(amtronECUConnectedStateTypeId, true); } else { thing->setStateValue(amtronECUConnectedStateTypeId, false); // Try once to reconnect the device amtronECUConnection->reconnectDevice(); } }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::initializationFinished, info, [=](bool success){ if (!success) { qCWarning(dcMennekes()) << "Connection init finished with errors" << thing->name() << amtronECUConnection->hostAddress().toString(); hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor); amtronECUConnection->deleteLater(); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error communicating with the wallbox.")); return; } qCDebug(dcMennekes()) << "Connection init finished successfully" << amtronECUConnection; m_amtronECUConnections.insert(thing, amtronECUConnection); info->finish(Thing::ThingErrorNoError); thing->setStateValue(amtronECUConnectedStateTypeId, true); amtronECUConnection->update(); }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::updateFinished, thing, [this, amtronECUConnection, thing](){ qCDebug(dcMennekes()) << "Amtron ECU update finished:" << thing->name() << amtronECUConnection; updateECUPhaseCount(thing); }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::cpSignalStateChanged, thing, [thing](AmtronECUModbusTcpConnection::CPSignalState cpSignalState) { qCDebug(dcMennekes()) << "CP signal state changed" << cpSignalState; thing->setStateValue(amtronECUPluggedInStateTypeId, cpSignalState >= AmtronECUModbusTcpConnection::CPSignalStateB); }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::signalledCurrentChanged, thing, [](quint16 signalledCurrent) { qCDebug(dcMennekes()) << "Signalled current changed:" << signalledCurrent; }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::minCurrentLimitChanged, thing, [thing](quint16 minCurrentLimit) { qCDebug(dcMennekes()) << "min current limit changed:" << minCurrentLimit; thing->setStateMinValue(amtronECUMaxChargingCurrentStateTypeId, minCurrentLimit); }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::maxCurrentLimitChanged, thing, [thing](quint16 maxCurrentLimit) { qCDebug(dcMennekes()) << "max current limit changed:" << maxCurrentLimit; thing->setStateMaxValue(amtronECUMaxChargingCurrentStateTypeId, maxCurrentLimit); }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::hemsCurrentLimitChanged, thing, [thing](quint16 hemsCurrentLimit) { qCDebug(dcMennekes()) << "HEMS current limit changed:" << hemsCurrentLimit; if (hemsCurrentLimit == 0) { thing->setStateValue(amtronECUPowerStateTypeId, false); } else { thing->setStateValue(amtronECUPowerStateTypeId, true); thing->setStateValue(amtronECUMaxChargingCurrentStateTypeId, hemsCurrentLimit); } }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::meterTotoalEnergyChanged, thing, [thing](quint32 meterTotalEnergy) { qCDebug(dcMennekes()) << "meter total energy changed:" << meterTotalEnergy; thing->setStateValue(amtronECUTotalEnergyConsumedStateTypeId, qRound(meterTotalEnergy / 10.0) / 100.0); // rounded to 2 as it changes on every update }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::meterTotalPowerChanged, thing, [thing](quint32 meterTotalPower) { qCDebug(dcMennekes()) << "meter total power changed:" << meterTotalPower; thing->setStateValue(amtronECUCurrentPowerStateTypeId, meterTotalPower); thing->setStateValue(amtronECUChargingStateTypeId, meterTotalPower > 0); }); connect(amtronECUConnection, &AmtronECUModbusTcpConnection::chargedEnergyChanged, thing, [thing](quint32 chargedEnergy) { qCDebug(dcMennekes()) << "charged energy changed:" << chargedEnergy; thing->setStateValue(amtronECUSessionEnergyStateTypeId, qRound(chargedEnergy / 10.0) / 100.0); // rounded to 2 as it changes on every update }); amtronECUConnection->connectDevice(); } void IntegrationPluginMennekes::setupAmtronHCC3Connection(ThingSetupInfo *info) { Thing *thing = info->thing(); QHostAddress address = m_monitors.value(thing)->networkDeviceInfo().address(); qCDebug(dcMennekes()) << "Setting up amtron wallbox on" << address.toString(); AmtronHCC3ModbusTcpConnection *amtronHCC3Connection = new AmtronHCC3ModbusTcpConnection(address, 502, 0xff, this); connect(info, &ThingSetupInfo::aborted, amtronHCC3Connection, &ModbusTCPMaster::deleteLater); // Reconnect on monitor reachable changed NetworkDeviceMonitor *monitor = m_monitors.value(thing); connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){ qCDebug(dcMennekes()) << "Network device monitor reachable changed for" << thing->name() << reachable; if (!thing->setupComplete()) return; if (reachable && !thing->stateValue("connected").toBool()) { amtronHCC3Connection->setHostAddress(monitor->networkDeviceInfo().address()); amtronHCC3Connection->connectDevice(); } else if (!reachable) { // Note: We disable autoreconnect explicitly and we will // connect the device once the monitor says it is reachable again amtronHCC3Connection->disconnectDevice(); } }); connect(amtronHCC3Connection, &AmtronHCC3ModbusTcpConnection::reachableChanged, thing, [thing, amtronHCC3Connection](bool reachable){ qCDebug(dcMennekes()) << "Reachable changed to" << reachable << "for" << thing; if (reachable) { amtronHCC3Connection->initialize(); } else { thing->setStateValue(amtronHCC3ConnectedStateTypeId, false); } }); connect(amtronHCC3Connection, &AmtronHCC3ModbusTcpConnection::initializationFinished, thing, [=](bool success){ if (!thing->setupComplete()) return; if (success) { thing->setStateValue(amtronHCC3ConnectedStateTypeId, true); } else { thing->setStateValue(amtronHCC3ConnectedStateTypeId, false); // Try once to reconnect the device amtronHCC3Connection->reconnectDevice(); } }); connect(amtronHCC3Connection, &AmtronHCC3ModbusTcpConnection::initializationFinished, info, [=](bool success){ if (!success) { qCWarning(dcMennekes()) << "Connection init finished with errors" << thing->name() << amtronHCC3Connection->hostAddress().toString(); hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(monitor); amtronHCC3Connection->deleteLater(); info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error communicating with the wallbox.")); return; } qCDebug(dcMennekes()) << "Connection init finished successfully" << amtronHCC3Connection; m_amtronHCC3Connections.insert(thing, amtronHCC3Connection); info->finish(Thing::ThingErrorNoError); thing->setStateValue(amtronHCC3ConnectedStateTypeId, true); amtronHCC3Connection->update(); }); amtronHCC3Connection->connectDevice(); } bool IntegrationPluginMennekes::ensureAmtronECUVersion(AmtronECUModbusTcpConnection *connection, const QString &version) { QByteArray deviceVersion = QByteArray::fromHex(QByteArray::number(connection->firmwareVersion(), 16)); return deviceVersion >= version; }