/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 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 #include #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; }