// 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. * * nymea-plugins 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 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. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "integrationpluginevbox.h" #include "plugininfo.h" #include "evboxport.h" #include #include #include #define STX 0x02 #define ETX 0x03 IntegrationPluginEVBox::IntegrationPluginEVBox() { } IntegrationPluginEVBox::~IntegrationPluginEVBox() { } void IntegrationPluginEVBox::discoverThings(ThingDiscoveryInfo *info) { if (QSerialPortInfo::availablePorts().isEmpty()) { info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("No serial ports are available on this system. Please connect a RS485 adapter first.")); return; } int discoveryCount = 0; foreach(const QSerialPortInfo &portInfo, QSerialPortInfo::availablePorts()) { // Reusing existing ports as multiple boxes could be connected to a single adapter EVBoxPort *port = m_ports.value(portInfo.portName()); if (!port) { // But if we don't have one yet, create it just for the discovery and delete it when the discovery info ends port = new EVBoxPort(portInfo.portName(), info); if (!port->open()) { qCWarning(dcEVBox()) << "Unable to open serial port" << portInfo.portName() << "for discovery."; delete port; continue; } qCInfo(dcEVBox()) << "Serial port" << portInfo.portName() << "opened for discovery."; } else { qCDebug(dcEVBox()) << "Discovering on already open serial port:" << portInfo.portName(); } discoveryCount++; port->sendCommand(EVBoxPort::Command68, 10, 1); connect(port, &EVBoxPort::responseReceived, info, [=](EVBoxPort::Command command, const QString &serial){ if (command != EVBoxPort::Command68) { return; } ThingDescriptor thingDescriptor(info->thingClassId(), thingClass(info->thingClassId()).displayName(), serial); ParamTypeId serialPortParamTypeId = thingClass(info->thingClassId()).paramTypes().findByName("serialPort").id(); ParamTypeId serialNumberParamTypeId = thingClass(info->thingClassId()).paramTypes().findByName("serialNumber").id(); ParamList params{ {serialPortParamTypeId, portInfo.portName()}, {serialNumberParamTypeId, serial} }; thingDescriptor.setParams(params); Thing *existingThing = myThings().findByParams(params); if (existingThing) { thingDescriptor.setThingId(existingThing->id()); } if (!info->property("foundSerials").toStringList().contains(serial)) { qCInfo(dcEVBox()) << "Adding descriptor for port" << portInfo.portName() << "Serial:" << serial << "Existing:" << (existingThing != nullptr ? "yes" : "no"); info->addThingDescriptor(thingDescriptor); info->setProperty("foundSerials", QStringList{serial} + info->property("foundSerials").toStringList()); } else { qCInfo(dcEVBox()) << "Discarding duplicate descriptor for port" << portInfo.portName() << "Serial:" << serial; } }); } if (discoveryCount == 0) { info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to open the RS485 port. Please make sure the RS485 adapter is connected properly and not in use by anything else.")); return; } QTimer::singleShot(3000, info, [info](){ info->finish(Thing::ThingErrorNoError); }); } void IntegrationPluginEVBox::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); QString portName = thing->paramValue("serialPort").toString(); QString serialNumber = thing->paramValue("serialNumber").toString(); // Opening the port, sharing with others if already opened. EVBoxPort *port = m_ports.value(portName); if (!port) { qCInfo(dcEVBox()) << "Port" << portName << "not open yet. Opening."; port = new EVBoxPort(portName, this); if (!port->open()) { qCWarning(dcEVBox()) << "Unable to open port" << portName << "for EVBox" << serialNumber; delete port; info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to open the RS485 port. Please make sure the RS485 adapter is connected properly.")); return; } m_ports.insert(portName, port); } // Setup routine: Try to set the max charging current to 6A and see if we get a valid answer port->sendCommand(EVBoxPort::Command68, 60, 6, serialNumber); connect(port, &EVBoxPort::closed, info, [info](){ info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The EVBox has closed the connection.")); }); connect(port, &EVBoxPort::responseReceived, info, [info, serialNumber](EVBoxPort::Command /*command*/, const QString &serial){ if (serial == serialNumber) { info->finish(Thing::ThingErrorNoError); } }); QTimer::singleShot(5000, info, [info](){ info->finish(Thing::ThingErrorTimeout, QT_TR_NOOP("The EVBox is not responding.")); }); // And connect the signals to the thing for when the setup went well connect(port, &EVBoxPort::closed, thing, [thing, portName](){ qCInfo(dcEVBox()) << "Port" << portName << "closed. Marking thing as offline:" << thing->name(); thing->setStateValue("connected", false); }); connect(port, &EVBoxPort::opened, thing, [portName](){ qCInfo(dcEVBox()) << "Port" << portName << "opened."; }); connect(port, &EVBoxPort::shortResponseReceived, thing, [this, thing, serialNumber](EVBoxPort::Command /*command*/, const QString &serial){ if (serial != serialNumber) { return; } thing->setStateValue("connected", true); finishPendingAction(thing); m_waitingForResponses[thing] = false; }); connect(port, &EVBoxPort::responseReceived, thing, [this, thing, serialNumber](EVBoxPort::Command /*command*/, const QString &serial, quint16 minChargingCurrent, quint16 maxChargingCurrent, quint16 chargingCurrentL1, quint16 chargingCurrentL2, quint16 chargingCurrentL3, quint32 totalEnergyConsumed){ if (serial != serialNumber) { return; } thing->setStateValue("connected", true); finishPendingAction(thing); m_waitingForResponses[thing] = false; thing->setStateMinMaxValues("maxChargingCurrent", minChargingCurrent / 10, maxChargingCurrent / 10); if (thing->thingClassId() == elviMidThingClassId) { double currentPower = (chargingCurrentL1 + chargingCurrentL2 + chargingCurrentL3) * 23; thing->setStateValue("currentPower", currentPower); thing->setStateValue("totalEnergyConsumed", totalEnergyConsumed / 1000.0); thing->setStateValue("charging", currentPower > 0); int phaseCount = 0; if (chargingCurrentL1 > 0) { phaseCount++; } if (chargingCurrentL2 > 0) { phaseCount++; } if (chargingCurrentL3 > 0) { phaseCount++; } // If all phases are on 0, we aren't charging and don't know how many phases are available... // Only updating the count if we actually do know that at least one is charging. if (phaseCount > 0) { thing->setStateValue("phaseCount", phaseCount); } } else { thing->setStateValue("charging", thing->stateValue("maxChargingCurrent").toUInt() > 0 && thing->stateValue("power").toBool()); thing->setStateValue("phaseCount", thing->setting("phaseCount").toUInt()); } }); } void IntegrationPluginEVBox::postSetupThing(Thing */*thing*/) { if (!m_timer) { m_timer = hardwareManager()->pluginTimerManager()->registerTimer(5); connect(m_timer, &PluginTimer::timeout, this, [this](){ foreach (Thing *t, myThings()) { QString portName = t->paramValue("serialPort").toString(); QString serial = t->paramValue("serialNumber").toString(); if (m_waitingForResponses.value(t)) { qCInfo(dcEVBox()) << "Wallbox" << t->name() << "did not respond to last command. Marking offline."; t->setStateValue("connected", false); } EVBoxPort *port = m_ports.value(portName); if (port->isOpen()) { quint16 maxChargingCurrent = 0; if (t->stateValue("power").toBool()) { maxChargingCurrent = t->stateValue("maxChargingCurrent").toUInt(); } port->sendCommand(EVBoxPort::Command68, 60, maxChargingCurrent, serial); m_waitingForResponses[t] = true; } } }); } } void IntegrationPluginEVBox::thingRemoved(Thing *thing) { QString portName = thing->paramValue("serialPort").toString(); ParamTypeId serialPortParamTypeId = thing->thingClass().paramTypes().findByName("serialPort").id(); if (myThings().filterByParam(serialPortParamTypeId, portName).isEmpty()) { qCInfo(dcEVBox()).nospace() << "No more EVBox devices using port " << portName << ". Destroying port."; delete m_ports.take(portName); } if (myThings().isEmpty()) { hardwareManager()->pluginTimerManager()->unregisterTimer(m_timer); m_timer = nullptr; } } void IntegrationPluginEVBox::executeAction(ThingActionInfo *info) { Thing *thing = info->thing(); QString portName = thing->paramValue("serialPort").toString(); QString serial = thing->paramValue("serialNumber").toString(); EVBoxPort *port = m_ports.value(portName); qCDebug(dcEVBox()) << "Executing action" << info->action().actionTypeId().toString(); ActionType actionType = thing->thingClass().actionTypes().findById(info->action().actionTypeId()); if (actionType.name() == "power") { bool power = info->action().paramValue(actionType.id()).toBool(); quint16 maxChargingCurrent = thing->stateValue("maxChargingCurrent").toUInt(); port->sendCommand(EVBoxPort::Command68, 60, power ? maxChargingCurrent : 0, serial); } else if (actionType.name() == "maxChargingCurrent") { int maxChargingCurrent = qRound(info->action().paramValue(actionType.id()).toDouble()); port->sendCommand(EVBoxPort::Command68, 60, maxChargingCurrent, serial); } m_pendingActions[thing].append(info); connect(info, &ThingActionInfo::aborted, this, [=](){ m_pendingActions[thing].removeAll(info); }); } void IntegrationPluginEVBox::finishPendingAction(Thing *thing) { if (!m_pendingActions.value(thing).isEmpty()) { ThingActionInfo *info = m_pendingActions[thing].takeFirst(); qCDebug(dcEVBox()) << "Finishing action:" << info->action().actionTypeId().toString(); ActionType actionType = thing->thingClass().actionTypes().findById(info->action().actionTypeId()); if (actionType.name() == "power") { thing->setStateValue("power", info->action().paramValue(actionType.id())); } else if (actionType.name() == "maxChargingCurrent") { thing->setStateValue("maxChargingCurrent", qRound(info->action().paramValue(actionType.id()).toDouble())); } info->finish(Thing::ThingErrorNoError); } }