288 lines
12 KiB
C++
288 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.
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
#include "integrationpluginevbox.h"
|
|
#include "plugininfo.h"
|
|
#include "evboxport.h"
|
|
|
|
#include <QSerialPortInfo>
|
|
#include <QSerialPort>
|
|
#include <QDataStream>
|
|
|
|
#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);
|
|
}
|
|
}
|
|
|