/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, 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 "integrationpluginwallbe.h"
#include "plugininfo.h"
#include "network/networkdevicediscovery.h"
#include "types/param.h"
#include
#include
#include
#include
IntegrationPluginWallbe::IntegrationPluginWallbe()
{
}
void IntegrationPluginWallbe::init()
{
// FIXME: make use of the internal network discovery if the device gets unavailable. For now, commented out since it has not been used
// at the moment of changing this.
// m_discovery = new Discovery();
// connect(m_discovery, &Discovery::finished, this, [this](const QList &hosts) {
// foreach (const Host &host, hosts) {
// if (!host.vendor().contains("Phoenix", Qt::CaseSensitivity::CaseInsensitive))
// continue;
// Q_FOREACH(Thing *existingThing, myThings()) {
// if (existingThing->paramValue(wallbeEcoThingMacParamTypeId).toString().isEmpty()) {
// //This device got probably manually setup, to enable auto rediscovery the MAC address needs to setup
// if (existingThing->paramValue(wallbeEcoThingIpParamTypeId).toString() == host.address()) {
// qCDebug(dcWallbe()) << "Wallbe Wallbox MAC Address has been discovered" << existingThing->name() << host.macAddress();
// existingThing->setParamValue(wallbeEcoThingMacParamTypeId, host.macAddress());
// }
// } else if (existingThing->paramValue(wallbeEcoThingMacParamTypeId).toString() == host.macAddress()) {
// if (existingThing->paramValue(wallbeEcoThingIpParamTypeId).toString() != host.address()) {
// qCDebug(dcWallbe()) << "Wallbe Wallbox IP Address has changed, from" << existingThing->paramValue(wallbeEcoThingIpParamTypeId).toString() << "to" << host.address();
// existingThing->setParamValue(wallbeEcoThingIpParamTypeId, host.address());
// } else {
// qCDebug(dcWallbe()) << "Wallbe Wallbox" << existingThing->name() << "IP address has not changed" << host.address();
// }
// break;
// }
// }
// }
// });
}
void IntegrationPluginWallbe::discoverThings(ThingDiscoveryInfo *info)
{
if (info->thingClassId() == wallbeEcoThingClassId) {
if (!hardwareManager()->networkDeviceDiscovery()->available()) {
qCWarning(dcWallbe()) << "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;
}
qCDebug(dcWallbe()) << "Start Wallbe eco discovery";
NetworkDeviceDiscoveryReply *discoveryReply = hardwareManager()->networkDeviceDiscovery()->discover();
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
ThingDescriptors descriptors;
qCDebug(dcWallbe()) << "Discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "devices";
foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) {
qCDebug(dcWallbe()) << networkDeviceInfo;
if (!networkDeviceInfo.macAddressManufacturer().contains("Phoenix", Qt::CaseSensitivity::CaseInsensitive))
continue;
QString title;
if (networkDeviceInfo.hostName().isEmpty()) {
title += networkDeviceInfo.address().toString();
} else {
title += networkDeviceInfo.address().toString() + " (" + networkDeviceInfo.hostName() + ")";
}
QString description;
if (networkDeviceInfo.macAddressManufacturer().isEmpty()) {
description = networkDeviceInfo.macAddress();
} else {
description = networkDeviceInfo.macAddress() + " (" + networkDeviceInfo.macAddressManufacturer() + ")";
}
ThingDescriptor descriptor(wallbeEcoThingClassId, title, description);
// Check if we already have set up this device
Things existingThings = myThings().filterByParam(wallbeEcoThingIpParamTypeId, networkDeviceInfo.address().toString());
if (existingThings.count() == 1) {
qCDebug(dcWallbe()) << "This thing already exists in the system." << existingThings.first() << networkDeviceInfo;
descriptor.setThingId(existingThings.first()->id());
}
ParamList params;
params << Param(wallbeEcoThingIpParamTypeId, networkDeviceInfo.address().toString());
params << Param(wallbeEcoThingMacParamTypeId, networkDeviceInfo.macAddress());
descriptor.setParams(params);
info->addThingDescriptor(descriptor);
}
info->finish(Thing::ThingErrorNoError);
});
} else {
Q_ASSERT_X(false, "discoverThings", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8());
}
}
void IntegrationPluginWallbe::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
qCDebug(dcWallbe) << "Setting up a new device:" << thing->params();
if (thing->thingClassId() == wallbeEcoThingClassId) {
QHostAddress address(thing->paramValue(wallbeEcoThingIpParamTypeId).toString());
if (m_connections.contains(thing)) {
qCDebug(dcWallbe()) << "Setup after reconfiguration, cleaning up ...";
m_connections.take(thing)->deleteLater();
}
if (address.isNull()){
qCWarning(dcWallbe) << "IP address is not valid";
info->finish(Thing::ThingErrorSetupFailed, tr("Invalid IP address"));
return;
}
ModbusTCPMaster *modbusTcpMaster = new ModbusTCPMaster(address, 502, this);
connect(modbusTcpMaster, &ModbusTCPMaster::connectionStateChanged, this, &IntegrationPluginWallbe::onConnectionStateChanged);
connect(modbusTcpMaster, &ModbusTCPMaster::receivedCoil, this, &IntegrationPluginWallbe::onReceivedCoil);
connect(modbusTcpMaster, &ModbusTCPMaster::receivedInputRegister, this, &IntegrationPluginWallbe::onReceivedInputRegister);
connect(modbusTcpMaster, &ModbusTCPMaster::receivedHoldingRegister, this, &IntegrationPluginWallbe::onReceivedHoldingRegister);
connect(modbusTcpMaster, &ModbusTCPMaster::writeRequestExecuted, this, &IntegrationPluginWallbe::onWriteRequestExecuted);
connect(modbusTcpMaster, &ModbusTCPMaster::writeRequestError, this, &IntegrationPluginWallbe::onWriteRequestError);
connect(info, &ThingSetupInfo::aborted, modbusTcpMaster, &ModbusTCPMaster::deleteLater);
connect(modbusTcpMaster, &ModbusTCPMaster::connectionStateChanged, info, [this, info, modbusTcpMaster](bool connected) {
qCDebug(dcWallbe()) << "Modbus TCP connection changed, connected" << connected;
if(connected) {
m_connections.insert(info->thing(), modbusTcpMaster);
info->finish(Thing::ThingErrorNoError);
}
});
if (!modbusTcpMaster->connectDevice()) {
qCWarning(dcWallbe()) << "Could not connect device";
return info->finish(Thing::ThingErrorHardwareNotAvailable);
}
}
}
void IntegrationPluginWallbe::postSetupThing(Thing *thing)
{
qCDebug(dcWallbe()) << "Post setup thing" << thing->name();
if (thing->thingClassId() == wallbeEcoThingClassId){
if (!m_pluginTimer) {
qCDebug(dcWallbe()) << "Starting plugin timer";
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10);
connect(m_pluginTimer, &PluginTimer::timeout, this, [this] {
foreach(Thing *thing, m_connections.keys()) {
update(thing);
}
});
}
thing->setStateValue(wallbeEcoConnectedStateTypeId, true);
update(thing);
} else {
Q_ASSERT_X(false, "postSetupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
}
}
void IntegrationPluginWallbe::executeAction(ThingActionInfo *info)
{
Thing *thing = info->thing();
Action action = info->action();
ModbusTCPMaster *modbusTcpMaster = m_connections.value(thing);
if (!modbusTcpMaster) {
qCWarning(dcWallbe()) << "Modbus connection not available";
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
if (thing->thingClassId() == wallbeEcoThingClassId){
// check if this is the "set power" action
if (action.actionTypeId() == wallbeEcoPowerActionTypeId) {
// get the param value of the charging action
bool charging = action.param(wallbeEcoPowerActionPowerParamTypeId).value().toBool();
qCDebug(dcWallbe) << "Start Charging button" << thing->name() << "set power to" << charging;
QUuid requestId = modbusTcpMaster->writeCoil(m_slaveAddress, WallbeRegisterAddress::EnableCharging, charging);
m_asyncActions.insert(requestId, info);
connect(info, &ThingActionInfo::aborted, this, [this, requestId] {m_asyncActions.remove(requestId);});
} else if(action.actionTypeId() == wallbeEcoMaxChargingCurrentActionTypeId){
uint16_t current = action.param(wallbeEcoMaxChargingCurrentActionMaxChargingCurrentParamTypeId).value().toUInt();
qCDebug(dcWallbe) << "Charging power set to" << current;
QUuid requestId = modbusTcpMaster->writeHoldingRegister(m_slaveAddress, WallbeRegisterAddress::ChargingCurrent, current);
m_asyncActions.insert(requestId, info);
connect(info, &ThingActionInfo::aborted, this, [this, requestId] {m_asyncActions.remove(requestId);});
} else {
Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8());
}
} else {
Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
}
}
void IntegrationPluginWallbe::thingRemoved(Thing *thing)
{
if (thing->thingClassId() == wallbeEcoThingClassId) {
if (m_connections.contains(thing)) {
ModbusTCPMaster *modbusTcpMaster = m_connections.take(thing);
qCDebug(dcWallbe) << "Remove device" << thing->name();
modbusTcpMaster->deleteLater();
}
}
if (myThings().isEmpty()) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
m_pluginTimer = nullptr;
}
}
void IntegrationPluginWallbe::update(Thing *thing)
{
ModbusTCPMaster *modbusTCPMaster = m_connections.value(thing);
if(!modbusTCPMaster) {
qCWarning(dcWallbe()) << "Modbus TCP connection not found for" << thing->name();
return;
}
modbusTCPMaster->readInputRegister(m_slaveAddress, WallbeRegisterAddress::EVStatus, 1);
modbusTCPMaster->readInputRegister(m_slaveAddress, WallbeRegisterAddress::FirmwareVersion, 2);
modbusTCPMaster->readInputRegister(m_slaveAddress, WallbeRegisterAddress::ChargingTime, 2);
modbusTCPMaster->readHoldingRegister(m_slaveAddress, WallbeRegisterAddress::ChargingCurrent, 1);
modbusTCPMaster->readCoil(m_slaveAddress, WallbeRegisterAddress::EnableCharging, 1);
}
void IntegrationPluginWallbe::onConnectionStateChanged(bool status)
{
ModbusTCPMaster *modbusTCPMaster = static_cast(sender());
Thing *thing = m_connections.key(modbusTCPMaster);
if (!thing)
return;
thing->setStateValue(wallbeEcoConnectedStateTypeId, status);
}
void IntegrationPluginWallbe::onReceivedInputRegister(int slaveAddress, int modbusRegister, const QVector &value)
{
Q_UNUSED(slaveAddress)
ModbusTCPMaster *modbusTCPMaster = static_cast(sender());
Thing *thing = m_connections.key(modbusTCPMaster);
if (!thing)
return;
if (WallbeRegisterAddress(modbusRegister) == WallbeRegisterAddress::EVStatus) {
//EV state - 16 bit ASCII (8bit)
switch (value[0]) {
case 65:
thing->setStateValue(wallbeEcoEvStatusStateTypeId, "A - No car plugged in");
break;
case 66:
thing->setStateValue(wallbeEcoEvStatusStateTypeId, "B - Supply equipment not yet ready");
break;
case 67:
thing->setStateValue(wallbeEcoEvStatusStateTypeId, "C - Ready to charge");
break;
case 68:
thing->setStateValue(wallbeEcoEvStatusStateTypeId, "D - Ready to charge, ventilation needed");
break;
case 69:
thing->setStateValue(wallbeEcoEvStatusStateTypeId, "E - Short circuit detected");
break;
case 70:
thing->setStateValue(wallbeEcoEvStatusStateTypeId, "F - Supply equipment not available");
break;
default:
thing->setStateValue(wallbeEcoEvStatusStateTypeId, "F - Supply equipment not available");
}
} else if (WallbeRegisterAddress(modbusRegister) == WallbeRegisterAddress::ChargingTime) {
// Extract Input Register 102 - load time - 32bit integer
if (value.length() >= 2) {
int minutes = (((uint32_t)(value[1]<<16)|(uint32_t)(value[0]))/60); //Converts to minutes
qCDebug(dcWallbe()) << " - Charging time:" << minutes << "[min]";
thing->setStateValue(wallbeEcoChargeTimeStateTypeId, minutes);
}
} else if (WallbeRegisterAddress(modbusRegister) == WallbeRegisterAddress::FirmwareVersion) {
int firmware = (uint32_t)(value[1]<<16)|(uint32_t)(value[0]);
uint major = firmware/10000;
uint minor = (firmware%10000)/100;
uint patch = firmware%100;
QString firmwarestring = QString::number(major)+'.'+QString::number(minor)+'.'+QString::number(patch);
thing->setStateValue(wallbeEcoFirmwareVersionStateTypeId, firmwarestring);
}
}
void IntegrationPluginWallbe::onReceivedCoil(int slaveAddress, int modbusRegister, const QVector &value)
{
Q_UNUSED(slaveAddress)
ModbusTCPMaster *modbusTCPMaster = static_cast(sender());
Thing *thing = m_connections.key(modbusTCPMaster);
if (!thing)
return;
if (WallbeRegisterAddress(modbusRegister) == WallbeRegisterAddress::EnableCharging) {
qCDebug(dcWallbe()) << " - Enable charging:" << (value[0] != 0);
thing->setStateValue(wallbeEcoPowerStateTypeId, (value[0] != 0));
}
}
void IntegrationPluginWallbe::onReceivedHoldingRegister(int slaveAddress, int modbusRegister, const QVector &value)
{
Q_UNUSED(slaveAddress)
ModbusTCPMaster *modbusTCPMaster = static_cast(sender());
Thing *thing = m_connections.key(modbusTCPMaster);
if (!thing)
return;
switch (WallbeRegisterAddress(modbusRegister)) {
case WallbeRegisterAddress::ChargingCurrent: {
qCDebug(dcWallbe()) << " - Charging current:" << value[0] << "[A]";
thing->setStateValue(wallbeEcoMaxChargingCurrentStateTypeId, value[0]);
} break;
case WallbeRegisterAddress::ErrorCode: {
qCDebug(dcWallbe()) << "Received Error Code modbus register" << value[0];
break;
}
default:
break;
}
}
void IntegrationPluginWallbe::onWriteRequestExecuted(const QUuid &requestId, bool success)
{
if (m_asyncActions.contains(requestId)) {
qCDebug(dcWallbe()) << "Write request executed" << requestId << success;
ThingActionInfo *info = m_asyncActions.value(requestId);
if (success) {
info->finish(Thing::ThingErrorNoError);
} else {
info->finish(Thing::ThingErrorHardwareFailure);
}
}
}
void IntegrationPluginWallbe::onWriteRequestError(const QUuid &requestId, const QString &error)
{
Q_UNUSED(requestId)
qCWarning(dcWallbe()) << "Could not execute write request" << error;
}