When thingParamValueAddress() returns empty (because MAC is known and IP is tracked dynamically), fill the address param with the UDP source IP so the user can see it in the setup UI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
797 lines
37 KiB
C++
797 lines
37 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 "integrationpluginkeba.h"
|
|
#include "kebaproductinfo.h"
|
|
#include "plugininfo.h"
|
|
|
|
#include <QJsonDocument>
|
|
#include <QUdpSocket>
|
|
#include <QTimeZone>
|
|
|
|
IntegrationPluginKeba::IntegrationPluginKeba()
|
|
{
|
|
// KebaProductInfo bmw("BMW-10-EC240522-E1R");
|
|
// KebaProductInfo ke("KC-P30-EC240122-E0R");
|
|
// KebaProductInfo ge("KC-P30-EC220112-000-DE");
|
|
// KebaProductInfo n("KC-P30-EC2404B2-M0A-GE");
|
|
// KebaProductInfo pv("KC-P30-EC2204U2-E00-PV");
|
|
}
|
|
|
|
void IntegrationPluginKeba::init()
|
|
{
|
|
m_macAddressParamTypeIds.insert(kebaThingClassId, kebaThingMacAddressParamTypeId);
|
|
m_macAddressParamTypeIds.insert(kebaSimpleThingClassId, kebaSimpleThingMacAddressParamTypeId);
|
|
|
|
m_hostNameParamTypeIds.insert(kebaThingClassId, kebaThingHostNameParamTypeId);
|
|
m_hostNameParamTypeIds.insert(kebaSimpleThingClassId, kebaSimpleThingHostNameParamTypeId);
|
|
|
|
m_addressParamTypeIds.insert(kebaThingClassId, kebaThingAddressParamTypeId);
|
|
m_addressParamTypeIds.insert(kebaSimpleThingClassId, kebaSimpleThingAddressParamTypeId);
|
|
|
|
m_modelParamTypeIds.insert(kebaThingClassId, kebaThingModelParamTypeId);
|
|
m_modelParamTypeIds.insert(kebaSimpleThingClassId, kebaSimpleThingModelParamTypeId);
|
|
|
|
m_serialNumberParamTypeIds.insert(kebaThingClassId, kebaThingSerialNumberParamTypeId);
|
|
m_serialNumberParamTypeIds.insert(kebaSimpleThingClassId, kebaSimpleThingSerialNumberParamTypeId);
|
|
}
|
|
|
|
void IntegrationPluginKeba::discoverThings(ThingDiscoveryInfo *info)
|
|
{
|
|
// Init data layer if not already created
|
|
if (!m_kebaDataLayer){
|
|
qCDebug(dcKeba()) << "Creating new Keba data layer...";
|
|
m_kebaDataLayer= new KeContactDataLayer(this);
|
|
if (!m_kebaDataLayer->init()) {
|
|
m_kebaDataLayer->deleteLater();
|
|
m_kebaDataLayer = nullptr;
|
|
qCWarning(dcKeba()) << "Failed to create Keba data layer...";
|
|
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The communication could not be established."));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!hardwareManager()->networkDeviceDiscovery()->available()) {
|
|
qCWarning(dcKeba()) << "The network discovery does not seem to be available.";
|
|
info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The network discovery is not available. Please enter the IP address manually."));
|
|
return;
|
|
}
|
|
|
|
// Create a discovery with the info as parent for auto deleting the object once the discovery info is done
|
|
KebaDiscovery *discovery = new KebaDiscovery(m_kebaDataLayer, hardwareManager()->networkDeviceDiscovery(), info);
|
|
connect(discovery, &KebaDiscovery::discoveryFinished, info, [=](){
|
|
|
|
foreach (const KebaDiscovery::KebaDiscoveryResult &result, discovery->discoveryResults()) {
|
|
|
|
KebaProductInfo productInformation(result.product);
|
|
if (!productInformation.isValid()) {
|
|
qCWarning(dcKeba()) << "Discovered keba with invalid product information" << result.product;
|
|
continue;
|
|
}
|
|
|
|
ThingClassId discoveredThingClassId = kebaThingClassId;
|
|
// Check if this is a keba without meter (aka simple)
|
|
if (productInformation.meter() == KebaProductInfo::NoMeter) {
|
|
discoveredThingClassId = kebaSimpleThingClassId;
|
|
}
|
|
|
|
// Make sure we show only the result we searched for to prevent cross adding between normal and simple
|
|
if (discoveredThingClassId != info->thingClassId())
|
|
continue;
|
|
|
|
ThingDescriptor descriptor(discoveredThingClassId, productInformation.manufacturer() + " " + result.product, "Serial: " + result.serialNumber + " - " + result.networkDeviceInfo.address().toString());
|
|
qCDebug(dcKeba()) << "Discovered:" << descriptor.title() << descriptor.description();
|
|
|
|
// Check if we already have set up this device
|
|
Things existingThings = myThings().filterByParam(m_serialNumberParamTypeIds.value(discoveredThingClassId), result.serialNumber);
|
|
if (existingThings.count() == 1) {
|
|
qCDebug(dcKeba()) << "This keba already exists in the system!" << result.networkDeviceInfo;
|
|
descriptor.setThingId(existingThings.first()->id());
|
|
}
|
|
|
|
ParamList params;
|
|
params << Param(m_macAddressParamTypeIds.value(discoveredThingClassId), result.networkDeviceInfo.thingParamValueMacAddress());
|
|
params << Param(m_hostNameParamTypeIds.value(discoveredThingClassId), result.networkDeviceInfo.thingParamValueHostName());
|
|
// Fallback to direct IP when thingParamValueAddress() is empty (occurs when MAC is known and IP is tracked dynamically)
|
|
QString addressValue = result.networkDeviceInfo.thingParamValueAddress();
|
|
if (addressValue.isEmpty())
|
|
addressValue = result.address.toString();
|
|
params << Param(m_addressParamTypeIds.value(discoveredThingClassId), addressValue);
|
|
params << Param(m_modelParamTypeIds.value(discoveredThingClassId), result.product);
|
|
params << Param(m_serialNumberParamTypeIds.value(discoveredThingClassId), result.serialNumber);
|
|
descriptor.setParams(params);
|
|
info->addThingDescriptor(descriptor);
|
|
}
|
|
|
|
info->finish(Thing::ThingErrorNoError);
|
|
});
|
|
|
|
// Start the discovery process
|
|
discovery->startDiscovery();
|
|
}
|
|
|
|
void IntegrationPluginKeba::setupThing(ThingSetupInfo *info)
|
|
{
|
|
Thing *thing = info->thing();
|
|
|
|
// Handle reconfigure
|
|
if (myThings().contains(thing)) {
|
|
KeContact *keba = m_kebaDevices.take(thing->id());
|
|
if (keba) {
|
|
qCDebug(dcKeba()) << "Reconfigure" << thing->name() << thing->params();
|
|
delete keba;
|
|
}
|
|
|
|
if (m_monitors.contains(thing)) {
|
|
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
|
|
}
|
|
|
|
// Now continue with the normal setup..
|
|
}
|
|
|
|
qCDebug(dcKeba()) << "Setting up" << thing->name() << thing->params();
|
|
if (!m_kebaDataLayer){
|
|
qCDebug(dcKeba()) << "Creating new Keba data layer...";
|
|
m_kebaDataLayer = new KeContactDataLayer(this);
|
|
if (!m_kebaDataLayer->init()) {
|
|
m_kebaDataLayer->deleteLater();
|
|
m_kebaDataLayer = nullptr;
|
|
info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Error opening network port."));
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
// Create a monitor so we always get the correct IP in the network and see if the device is reachable without polling on our own
|
|
NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing);
|
|
// Make sure we have a valid mac address, otherwise no monitor and not auto searching is possible
|
|
if (!monitor) {
|
|
qCWarning(dcKeba()) << "Can not set up connection monitor with the given parameters:" << thing->params();
|
|
info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("Unable to set up the connection with this configuration. Please reconfigure the connection."));
|
|
return;
|
|
}
|
|
|
|
connect(monitor, &NetworkDeviceMonitor::reachableChanged, thing, [=](bool reachable){
|
|
// Only if the setup has been finished
|
|
KeContact *keba = m_kebaDevices.value(thing->id());
|
|
if (!keba)
|
|
return;
|
|
|
|
qCDebug(dcKeba()) << "Network device monitor for" << thing->name() << (reachable ? "is now reachable" : "is not reachable any more" );
|
|
if (reachable) {
|
|
// Update address and refresh
|
|
thing->setStateValue("hostAddress", monitor->networkDeviceInfo().address().toString());
|
|
keba->setAddress(monitor->networkDeviceInfo().address());
|
|
refresh(thing, keba);
|
|
}
|
|
});
|
|
|
|
// Continue with setup only if we know that the network device is reachable
|
|
m_monitors.insert(thing, monitor);
|
|
if (monitor->reachable()) {
|
|
setupKeba(info, monitor->networkDeviceInfo().address());
|
|
} else {
|
|
// otherwise wait until we reach the networkdevice before setting up the device
|
|
qCDebug(dcKeba()) << "Network device" << thing->name() << "is not reachable yet. Continue with the setup once reachable.";
|
|
connect(monitor, &NetworkDeviceMonitor::reachableChanged, info, [=](bool reachable){
|
|
if (reachable) {
|
|
qCDebug(dcKeba()) << "Network device" << thing->name() << "is now reachable. Continue with the setup...";
|
|
setupKeba(info, monitor->networkDeviceInfo().address());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginKeba::postSetupThing(Thing *thing)
|
|
{
|
|
qCDebug(dcKeba()) << "Post setup" << thing->name();
|
|
|
|
KeContact *keba = m_kebaDevices.value(thing->id());
|
|
if (!keba) {
|
|
qCWarning(dcKeba()) << "No Keba connection found for this thing while doing post setup.";
|
|
return;
|
|
}
|
|
|
|
refresh(thing, keba);
|
|
|
|
if (!m_updateTimer) {
|
|
m_updateTimer = hardwareManager()->pluginTimerManager()->registerTimer(10);
|
|
connect(m_updateTimer, &PluginTimer::timeout, this, [this]() {
|
|
foreach (const ThingId &thingId, m_kebaDevices.keys()) {
|
|
KeContact *keba = m_kebaDevices.value(thingId);
|
|
Thing *thing = myThings().findById(thingId);
|
|
if (!thing)
|
|
return;
|
|
|
|
if (!keba) {
|
|
qCWarning(dcKeba()) << "No Keba connection found for" << thing->name();
|
|
return;
|
|
}
|
|
|
|
refresh(thing, keba);
|
|
}
|
|
});
|
|
|
|
m_updateTimer->start();
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginKeba::thingRemoved(Thing *thing)
|
|
{
|
|
qCDebug(dcKeba()) << "Removing" << thing->name();
|
|
if (m_kebaDevices.contains(thing->id())) {
|
|
KeContact *keba = m_kebaDevices.take(thing->id());
|
|
keba->deleteLater();
|
|
}
|
|
|
|
if (m_monitors.contains(thing)) {
|
|
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
|
|
}
|
|
|
|
m_lastSessionId.remove(thing->id());
|
|
|
|
if (myThings().empty()) {
|
|
qCDebug(dcKeba()) << "Stopping plugin timers ...";
|
|
if (m_updateTimer) {
|
|
hardwareManager()->pluginTimerManager()->unregisterTimer(m_updateTimer);
|
|
m_updateTimer = nullptr;
|
|
}
|
|
|
|
qCDebug(dcKeba()) << "Closing keba data layer...";
|
|
m_kebaDataLayer->deleteLater();
|
|
m_kebaDataLayer= nullptr;
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginKeba::executeAction(ThingActionInfo *info)
|
|
{
|
|
Thing *thing = info->thing();
|
|
Action action = info->action();
|
|
|
|
KeContact *keba = m_kebaDevices.value(thing->id());
|
|
if (!keba) {
|
|
qCWarning(dcKeba()) << "Device not properly initialized, Keba object missing";
|
|
return info->finish(Thing::ThingErrorHardwareNotAvailable);
|
|
}
|
|
|
|
// Make sure keba is reachable
|
|
if (!keba->reachable()) {
|
|
qCWarning(dcKeba()) << "Failed to execute action. The keba seems not to be reachable" << thing;
|
|
info->finish(Thing::ThingErrorHardwareNotAvailable);
|
|
return;
|
|
}
|
|
|
|
QUuid requestId;
|
|
if (thing->thingClassId() == kebaThingClassId) {
|
|
if (action.actionTypeId() == kebaMaxChargingCurrentActionTypeId) {
|
|
int milliAmpere = qRound(action.paramValue(kebaMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble() * 1000);
|
|
requestId = keba->setMaxAmpereGeneral(milliAmpere);
|
|
} else if (action.actionTypeId() == kebaPowerActionTypeId) {
|
|
requestId = keba->enableOutput(action.param(kebaPowerActionTypeId).value().toBool());
|
|
} else if (action.actionTypeId() == kebaDisplayActionTypeId) {
|
|
requestId = keba->displayMessage(action.param(kebaDisplayActionMessageParamTypeId).value().toByteArray());
|
|
} else if (action.actionTypeId() == kebaOutputX2ActionTypeId) {
|
|
requestId = keba->setOutputX2(action.param(kebaOutputX2ActionOutputX2ParamTypeId).value().toBool());
|
|
} else if (action.actionTypeId() == kebaSetPhaseCountActionTypeId) {
|
|
int phaseCount = action.paramValue(kebaSetPhaseCountActionPhaseCountParamTypeId).toInt();
|
|
requestId = keba->setPhaseSwitch(phaseCount);
|
|
} else if (action.actionTypeId() == kebaFailsafeModeActionTypeId) {
|
|
int timeout = 0;
|
|
if (action.param(kebaFailsafeModeActionFailsafeModeParamTypeId).value().toBool()) {
|
|
timeout = 60;
|
|
}
|
|
requestId = keba->setFailsafe(timeout, 0, false);
|
|
} else {
|
|
qCWarning(dcKeba()) << "Unhandled ActionTypeId:" << action.actionTypeId();
|
|
return info->finish(Thing::ThingErrorActionTypeNotFound);
|
|
}
|
|
|
|
} else if (thing->thingClassId() == kebaSimpleThingClassId) {
|
|
if (action.actionTypeId() == kebaSimpleMaxChargingCurrentActionTypeId) {
|
|
int milliAmpere = qRound(action.paramValue(kebaSimpleMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble() * 1000);
|
|
requestId = keba->setMaxAmpereGeneral(milliAmpere);
|
|
} else if (action.actionTypeId() == kebaSimplePowerActionTypeId) {
|
|
requestId = keba->enableOutput(action.param(kebaSimplePowerActionTypeId).value().toBool());
|
|
} else if (action.actionTypeId() == kebaSimpleDisplayActionTypeId) {
|
|
requestId = keba->displayMessage(action.param(kebaSimpleDisplayActionMessageParamTypeId).value().toByteArray());
|
|
} else if (action.actionTypeId() == kebaSimpleOutputX2ActionTypeId) {
|
|
requestId = keba->setOutputX2(action.param(kebaSimpleOutputX2ActionOutputX2ParamTypeId).value().toBool());
|
|
} else if (action.actionTypeId() == kebaSimpleFailsafeModeActionTypeId) {
|
|
int timeout = 0;
|
|
if (action.param(kebaSimpleFailsafeModeActionFailsafeModeParamTypeId).value().toBool()) {
|
|
timeout = 60;
|
|
}
|
|
requestId = keba->setFailsafe(timeout, 0, false);
|
|
} else {
|
|
qCWarning(dcKeba()) << "Unhandled ActionTypeId:" << action.actionTypeId();
|
|
return info->finish(Thing::ThingErrorActionTypeNotFound);
|
|
}
|
|
}
|
|
|
|
|
|
// If the keba returns an invalid uuid, something went wrong
|
|
if (requestId.isNull()) {
|
|
info->finish(Thing::ThingErrorHardwareFailure);
|
|
return;
|
|
}
|
|
|
|
m_asyncActions.insert(requestId, info);
|
|
connect(info, &ThingActionInfo::aborted, this, [requestId, this]{ m_asyncActions.remove(requestId); });
|
|
}
|
|
|
|
void IntegrationPluginKeba::setupKeba(ThingSetupInfo *info, const QHostAddress &address)
|
|
{
|
|
Thing *thing = info->thing();
|
|
KeContact *keba = new KeContact(address, m_kebaDataLayer, this);
|
|
connect(keba, &KeContact::reachableChanged, thing, [=](bool reachable){
|
|
thing->setStateValue("connected", reachable);
|
|
if (!reachable) {
|
|
thing->setStateValue("voltagePhaseA", 0);
|
|
thing->setStateValue("voltagePhaseB", 0);
|
|
thing->setStateValue("voltagePhaseC", 0);
|
|
thing->setStateValue("currentPhaseA", 0);
|
|
thing->setStateValue("currentPhaseB", 0);
|
|
thing->setStateValue("currentPhaseC", 0);
|
|
thing->setStateValue("currentPower", 0);
|
|
thing->setStateValue("powerFactor", 0);
|
|
}
|
|
});
|
|
|
|
connect(keba, &KeContact::commandExecuted, this, &IntegrationPluginKeba::onCommandExecuted);
|
|
connect(keba, &KeContact::reportTwoReceived, this, &IntegrationPluginKeba::onReportTwoReceived);
|
|
connect(keba, &KeContact::reportThreeReceived, this, &IntegrationPluginKeba::onReportThreeReceived);
|
|
connect(keba, &KeContact::report1XXReceived, this, &IntegrationPluginKeba::onReport1XXReceived);
|
|
connect(keba, &KeContact::broadcastReceived, this, &IntegrationPluginKeba::onBroadcastReceived);
|
|
|
|
// Clean up if the setup fails
|
|
connect(info, &ThingSetupInfo::aborted, keba, &KeContact::deleteLater);
|
|
|
|
// Make sure we receive data from the keba and the DIP switches are configured correctly
|
|
connect(keba, &KeContact::reportOneReceived, info, [=] (const KeContact::ReportOne &report) {
|
|
Thing *thing = info->thing();
|
|
qCDebug(dcKeba()) << "Report one received for" << thing->name();
|
|
qCDebug(dcKeba()) << " - Firmware" << report.firmware;
|
|
qCDebug(dcKeba()) << " - Serial" << report.serialNumber;
|
|
qCDebug(dcKeba()) << " - Product" << report.product;
|
|
qCDebug(dcKeba()) << " - Uptime" << report.seconds / 60 << "[min]";
|
|
qCDebug(dcKeba()) << " - Com Module" << report.comModule;
|
|
qCDebug(dcKeba()) << " - DIP switch 1" << report.dipSw1;
|
|
qCDebug(dcKeba()) << " - DIP switch 2" << report.dipSw2;
|
|
|
|
KebaProductInfo productInformation(report.product);
|
|
|
|
if (thing->paramValue(m_serialNumberParamTypeIds.value(thing->thingClassId())).toString().isEmpty()) {
|
|
qCDebug(dcKeba()) << "Update serial number parameter for" << thing << "to" << report.serialNumber;
|
|
thing->setParamValue(m_serialNumberParamTypeIds.value(thing->thingClassId()), report.serialNumber);
|
|
}
|
|
|
|
if (thing->paramValue(m_modelParamTypeIds.value(thing->thingClassId())).toString().isEmpty()) {
|
|
qCDebug(dcKeba()) << "Update model parameter for" << thing << "to" << report.product;
|
|
thing->setParamValue(m_modelParamTypeIds.value(thing->thingClassId()), report.product);
|
|
}
|
|
|
|
// Verify the DIP switches and warn the user in case if wrong configuration
|
|
// For having UPD controll on the keba we need DIP Switch 1.3 enabled
|
|
KeContact::DipSwitchOneFlag dipSwOne(report.dipSw1);
|
|
qCDebug(dcKeba()) << dipSwOne;
|
|
if (!dipSwOne.testFlag(KeContact::DipSwitchOneSmartHomeInterface)) {
|
|
qCWarning(dcKeba()) << "Connected successfully to Keba but the DIP Switch for controlling it is not enabled.";
|
|
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The required communication interface is not enabled on this keba. Please make sure the DIP switch 1.3 is switched on and try again."));
|
|
return;
|
|
}
|
|
|
|
// Parse the product code and check if the model actually supports the UDP/Modbus communication
|
|
// Supported are:
|
|
// - The A series (german edition), no meter DE440 (green edition)
|
|
// - The B series (german edition), no meter DE440
|
|
// - All C series
|
|
// - All X series
|
|
|
|
if (productInformation.isValid()) {
|
|
bool supported = false;
|
|
qCDebug(dcKeba()) << "Product information are valid. Evaluating if model supports UDP/Modbus communication...";
|
|
|
|
switch (productInformation.series()) {
|
|
case KebaProductInfo::SeriesA:
|
|
if (productInformation.model() == "P30" && productInformation.germanEdition()) {
|
|
qCDebug(dcKeba()) << "The P30 A series german edition is supported (DE440 GREEN EDITION)";
|
|
supported = true;
|
|
}
|
|
break;
|
|
case KebaProductInfo::SeriesB:
|
|
if (productInformation.model() == "P30" && productInformation.germanEdition()) {
|
|
qCDebug(dcKeba()) << "The P30 B series german edition is supported (DE440)";
|
|
supported = true;
|
|
}
|
|
break;
|
|
case KebaProductInfo::SeriesC:
|
|
case KebaProductInfo::SeriesXWlan:
|
|
case KebaProductInfo::SeriesXWlan3G:
|
|
case KebaProductInfo::SeriesXWlan4G:
|
|
case KebaProductInfo::SeriesX3G:
|
|
case KebaProductInfo::SeriesX4G:
|
|
case KebaProductInfo::SeriesSpecial:
|
|
qCDebug(dcKeba()) << "The keba" << productInformation.series() << "is capable of communicating using UDP";
|
|
qCDebug(dcKeba()) << "Series Special detected:" << productInformation.productString().right(2) << "(PV-Edition ou autre variante spéciale)";
|
|
supported = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (!supported) {
|
|
qCWarning(dcKeba()) << "Connected successfully to Keba but this model" << productInformation.series() << "has no communication module.";
|
|
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("This model does not support communication with smart devices."));
|
|
return;
|
|
}
|
|
} else {
|
|
qCWarning(dcKeba()) << "Product information are not valid. Cannot determin if this model supports UDP/Modbus communication, assuming yes so let's try to init...";
|
|
}
|
|
|
|
m_kebaDevices.insert(thing->id(), keba);
|
|
info->finish(Thing::ThingErrorNoError);
|
|
qCDebug(dcKeba()) << "Setup finsihed successfully for" << thing << thing->params();
|
|
|
|
thing->setStateValue("connected", true);
|
|
thing->setStateValue("hostAddress", address.toString());
|
|
thing->setStateValue("firmware", report.firmware);
|
|
thing->setStateValue("uptime", report.seconds / 60);
|
|
|
|
if (thing->thingClassId() == kebaSimpleThingClassId) {
|
|
thing->setStateValue(kebaSimplePhaseCountStateTypeId, thing->setting(kebaSimpleThingClassId));
|
|
}
|
|
|
|
connect(thing, &Thing::settingChanged, thing, [thing](const ParamTypeId &settingsTypeId, const QVariant &value){
|
|
if (settingsTypeId == kebaSimpleSettingsPhaseCountParamTypeId) {
|
|
thing->setStateValue(kebaSimplePhaseCountStateTypeId, value);
|
|
}
|
|
});
|
|
});
|
|
|
|
keba->getReport1();
|
|
}
|
|
|
|
void IntegrationPluginKeba::onCommandExecuted(QUuid requestId, bool success)
|
|
{
|
|
if (m_asyncActions.contains(requestId)) {
|
|
KeContact *keba = static_cast<KeContact *>(sender());
|
|
Thing *thing = myThings().findById(m_kebaDevices.key(keba));
|
|
if (!thing) {
|
|
qCWarning(dcKeba()) << "On command executed: missing device object";
|
|
return;
|
|
}
|
|
|
|
ThingActionInfo *info = m_asyncActions.take(requestId);
|
|
if (success) {
|
|
qCDebug(dcKeba()) << "Action execution finished successfully. Request ID:" << requestId.toString();
|
|
|
|
if (thing->thingClassId() == kebaThingClassId) {
|
|
// Set the value to the state so we don't have to wait for the report 2 response
|
|
if (info->action().actionTypeId() == kebaMaxChargingCurrentActionTypeId) {
|
|
double value = info->action().paramValue(kebaMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble();
|
|
info->thing()->setStateValue("maxChargingCurrent", value);
|
|
} else if (info->action().actionTypeId() == kebaPowerActionTypeId) {
|
|
info->thing()->setStateValue("power", info->action().paramValue(kebaPowerActionTypeId).toBool());
|
|
}
|
|
} else if (thing->thingClassId() == kebaSimpleThingClassId) {
|
|
// Set the value to the state so we don't have to wait for the report 2 response
|
|
if (info->action().actionTypeId() == kebaSimpleMaxChargingCurrentActionTypeId) {
|
|
double value = info->action().paramValue(kebaSimpleMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble();
|
|
info->thing()->setStateValue("maxChargingCurrent", value);
|
|
} else if (info->action().actionTypeId() == kebaPowerActionTypeId) {
|
|
info->thing()->setStateValue("power", info->action().paramValue(kebaSimplePowerActionTypeId).toBool());
|
|
}
|
|
}
|
|
|
|
info->finish(Thing::ThingErrorNoError);
|
|
} else {
|
|
qCWarning(dcKeba()) << "Action execution finished with error. Request ID:" << requestId.toString();
|
|
info->finish(Thing::ThingErrorHardwareFailure);
|
|
}
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginKeba::setDeviceState(Thing *thing, KeContact::State state)
|
|
{
|
|
switch (state) {
|
|
case KeContact::StateStarting:
|
|
thing->setStateValue("activity", "Starting");
|
|
break;
|
|
case KeContact::StateNotReady:
|
|
thing->setStateValue("activity", "Not ready for charging");
|
|
break;
|
|
case KeContact::StateReady:
|
|
thing->setStateValue("activity", "Ready for charging");
|
|
break;
|
|
case KeContact::StateCharging:
|
|
thing->setStateValue("activity", "Charging");
|
|
break;
|
|
case KeContact::StateError:
|
|
thing->setStateValue("activity", "Error");
|
|
break;
|
|
case KeContact::StateAuthorizationRejected:
|
|
thing->setStateValue("activity", "Authorization rejected");
|
|
break;
|
|
}
|
|
|
|
thing->setStateValue("charging", state == KeContact::StateCharging);
|
|
}
|
|
|
|
void IntegrationPluginKeba::setDevicePlugState(Thing *thing, KeContact::PlugState plugState)
|
|
{
|
|
switch (plugState) {
|
|
case KeContact::PlugStateUnplugged:
|
|
thing->setStateValue("plugState", "Unplugged");
|
|
break;
|
|
case KeContact::PlugStatePluggedOnChargingStation:
|
|
thing->setStateValue("plugState", "Plugged in charging station");
|
|
break;
|
|
case KeContact::PlugStatePluggedOnChargingStationAndPluggedOnEV:
|
|
thing->setStateValue("plugState", "Plugged in on EV");
|
|
break;
|
|
case KeContact::PlugStatePluggedOnChargingStationAndPlugLocked:
|
|
thing->setStateValue("plugState", "Plugged in and locked");
|
|
break;
|
|
case KeContact::PlugStatePluggedOnChargingStationAndPlugLockedAndPluggedOnEV:
|
|
thing->setStateValue("plugState", "Plugged in on EV and locked");
|
|
break;
|
|
}
|
|
|
|
if (plugState >= 5) {
|
|
thing->setStateValue("pluggedIn", true);
|
|
} else {
|
|
thing->setStateValue("pluggedIn", false);
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginKeba::refresh(Thing *thing, KeContact *keba)
|
|
{
|
|
if (m_monitors.contains(thing) && !m_monitors.value(thing)->reachable())
|
|
return;
|
|
|
|
keba->getReport2();
|
|
// No valid information if no meter
|
|
if (thing->thingClassId() != kebaSimpleThingClassId) {
|
|
keba->getReport3();
|
|
}
|
|
|
|
if (thing->stateValue("activity").toString() == "Charging") {
|
|
keba->getReport1XX(100);
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginKeba::onReportTwoReceived(const KeContact::ReportTwo &reportTwo)
|
|
{
|
|
KeContact *keba = static_cast<KeContact *>(sender());
|
|
Thing *thing = myThings().findById(m_kebaDevices.key(keba));
|
|
if (!thing)
|
|
return;
|
|
|
|
qCDebug(dcKeba()) << "Report 2 received for" << thing->name() << "Serial number:" << thing->paramValue(m_serialNumberParamTypeIds.value(thing->thingClassId())).toString();
|
|
qCDebug(dcKeba()) << " - State:" << reportTwo.state;
|
|
qCDebug(dcKeba()) << " - Error 1:" << reportTwo.error1;
|
|
qCDebug(dcKeba()) << " - Error 2:" << reportTwo.error2;
|
|
qCDebug(dcKeba()) << " - Plug:" << reportTwo.plugState;
|
|
qCDebug(dcKeba()) << " - Enable sys:" << reportTwo.enableSys;
|
|
qCDebug(dcKeba()) << " - Enable user:" << reportTwo.enableUser;
|
|
qCDebug(dcKeba()) << " - Max curr:" << reportTwo.maxCurrent;
|
|
qCDebug(dcKeba()) << " - Max curr %:" << reportTwo.maxCurrentPercentage;
|
|
qCDebug(dcKeba()) << " - Curr HW:" << reportTwo.currentHardwareLimitation;
|
|
qCDebug(dcKeba()) << " - Curr User:" << reportTwo.currentUser;
|
|
qCDebug(dcKeba()) << " - Curr FS:" << reportTwo.currentFailsafe;
|
|
qCDebug(dcKeba()) << " - Tmo FS:" << reportTwo.timeoutFailsafe;
|
|
qCDebug(dcKeba()) << " - Curr timer:" << reportTwo.currTimer;
|
|
qCDebug(dcKeba()) << " - Timeout CT:" << reportTwo.timeoutCt;
|
|
qCDebug(dcKeba()) << " - Output:" << reportTwo.output;
|
|
qCDebug(dcKeba()) << " - Input:" << reportTwo.input;
|
|
qCDebug(dcKeba()) << " - Serial number:" << reportTwo.serialNumber;
|
|
qCDebug(dcKeba()) << " - Uptime:" << reportTwo.seconds / 60 << "[min]";
|
|
|
|
if (reportTwo.serialNumber == thing->paramValue(m_serialNumberParamTypeIds.value(thing->thingClassId())).toString()) {
|
|
setDeviceState(thing, reportTwo.state);
|
|
setDevicePlugState(thing, reportTwo.plugState);
|
|
|
|
thing->setStateValue("power", reportTwo.enableUser);
|
|
thing->setStateValue("error1", reportTwo.error1);
|
|
thing->setStateValue("error2", reportTwo.error2);
|
|
thing->setStateValue("systemEnabled", reportTwo.enableSys);
|
|
|
|
thing->setStateValue("maxChargingCurrent", reportTwo.currentUser);
|
|
thing->setStateValue("maxChargingCurrentPercent", reportTwo.maxCurrentPercentage);
|
|
thing->setStateValue("maxChargingCurrentHardware", reportTwo.currentHardwareLimitation);
|
|
|
|
// Set the state limits according to the hardware limits
|
|
if (reportTwo.currentHardwareLimitation > 0) {
|
|
thing->setStateMaxValue("maxChargingCurrent", reportTwo.currentHardwareLimitation);
|
|
} else {
|
|
// If we have no limit given, reset to the statetype limit
|
|
thing->setStateMaxValue("maxChargingCurrent", thing->thingClass().stateTypes().findByName("maxChargingCurrent").maxValue());
|
|
}
|
|
thing->setStateValue("outputX2", reportTwo.output);
|
|
thing->setStateValue("input", reportTwo.input);
|
|
|
|
// Phase switch state (PV-Edition / SeriesSpecial)
|
|
thing->setStateValue("phaseSwitchSource", reportTwo.x2PhaseSwitchSource);
|
|
thing->setStateValue("phaseSwitchActive", reportTwo.x2PhaseSwitchSource == 4);
|
|
// x2PhaseSwitch: 0 = 1 phase, 1 = 3 phases
|
|
thing->setStateValue("desiredPhaseCount", reportTwo.x2PhaseSwitch == 1 ? 3 : 1);
|
|
|
|
thing->setStateValue("uptime", reportTwo.seconds / 60);
|
|
} else {
|
|
qCWarning(dcKeba()) << "Received report but the serial number didn't match";
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginKeba::onReportThreeReceived(const KeContact::ReportThree &reportThree)
|
|
{
|
|
KeContact *keba = static_cast<KeContact *>(sender());
|
|
Thing *thing = myThings().findById(m_kebaDevices.key(keba));
|
|
if (!thing)
|
|
return;
|
|
|
|
qCDebug(dcKeba()) << "Report 3 received for" << thing->name() << "Serial number:" << thing->paramValue(m_serialNumberParamTypeIds.value(thing->thingClassId())).toString();
|
|
qCDebug(dcKeba()) << " - Current phase 1:" << reportThree.currentPhase1 << "[A]";
|
|
qCDebug(dcKeba()) << " - Current phase 2:" << reportThree.currentPhase2 << "[A]";
|
|
qCDebug(dcKeba()) << " - Current phase 3:" << reportThree.currentPhase3 << "[A]";
|
|
qCDebug(dcKeba()) << " - Voltage phase 1:" << reportThree.voltagePhase1 << "[V]";
|
|
qCDebug(dcKeba()) << " - Voltage phase 2:" << reportThree.voltagePhase2 << "[V]";
|
|
qCDebug(dcKeba()) << " - Voltage phase 3:" << reportThree.voltagePhase3 << "[V]";
|
|
qCDebug(dcKeba()) << " - Power consumption:" << reportThree.power << "[kW]";
|
|
qCDebug(dcKeba()) << " - Energy session" << reportThree.energySession << "[kWh]";
|
|
qCDebug(dcKeba()) << " - Energy total" << reportThree.energyTotal << "[kWh]";
|
|
qCDebug(dcKeba()) << " - Serial number" << reportThree.serialNumber;
|
|
qCDebug(dcKeba()) << " - Uptime" << reportThree.seconds / 60 << "[min]";
|
|
|
|
// Note: all these infos are from the meter...
|
|
if (thing->thingClassId() == kebaSimpleThingClassId) {
|
|
qCDebug(dcKeba()) << "Received report 3 from keba but this model has no meter. Ignoring report data...";
|
|
return;
|
|
}
|
|
|
|
if (reportThree.serialNumber == thing->paramValue(m_serialNumberParamTypeIds.value(thing->thingClassId())).toString()) {
|
|
thing->setStateValue("currentPhaseA", reportThree.currentPhase1);
|
|
thing->setStateValue("currentPhaseB", reportThree.currentPhase2);
|
|
thing->setStateValue("currentPhaseC", reportThree.currentPhase3);
|
|
thing->setStateValue("voltagePhaseA", reportThree.voltagePhase1);
|
|
thing->setStateValue("voltagePhaseB", reportThree.voltagePhase2);
|
|
thing->setStateValue("voltagePhaseC", reportThree.voltagePhase3);
|
|
thing->setStateValue("currentPower", reportThree.power);
|
|
thing->setStateValue("sessionEnergy", reportThree.energySession);
|
|
thing->setStateValue("powerFactor", reportThree.powerFactor);
|
|
thing->setStateValue("totalEnergyConsumed", reportThree.energyTotal);
|
|
|
|
// Check how many phases are actually charging, and update the phase count only if something happens on the phases (current or power)
|
|
if (!(reportThree.currentPhase1 == 0 && reportThree.currentPhase2 == 0 && reportThree.currentPhase3 == 0)) {
|
|
uint phaseCount = 0;
|
|
if (reportThree.currentPhase1 != 0)
|
|
phaseCount += 1;
|
|
|
|
if (reportThree.currentPhase2 != 0)
|
|
phaseCount += 1;
|
|
|
|
if (reportThree.currentPhase3 != 0)
|
|
phaseCount += 1;
|
|
|
|
thing->setStateValue("phaseCount", phaseCount);
|
|
}
|
|
} else {
|
|
qCWarning(dcKeba()) << "Received report but the serial number didn't match";
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginKeba::onReport1XXReceived(int reportNumber, const KeContact::Report1XX &report)
|
|
{
|
|
KeContact *keba = static_cast<KeContact *>(sender());
|
|
Thing *thing = myThings().findById(m_kebaDevices.key(keba));
|
|
if (!thing)
|
|
return;
|
|
|
|
qCDebug(dcKeba()) << "Report" << reportNumber << "received for" << thing->name() << "Serial number:" << thing->paramValue(m_serialNumberParamTypeIds.value(thing->thingClassId())).toString();
|
|
qCDebug(dcKeba()) << " - Session Id" << report.sessionId;
|
|
qCDebug(dcKeba()) << " - Curr HW" << report.currHW;
|
|
qCDebug(dcKeba()) << " - Energy start" << report.startEnergy;
|
|
qCDebug(dcKeba()) << " - Energy present" << report.presentEnergy;
|
|
qCDebug(dcKeba()) << " - Start time" << report.startTime << QDateTime::fromMSecsSinceEpoch(report.startTime * 1000).toString();
|
|
qCDebug(dcKeba()) << " - End time" << report.endTime;
|
|
qCDebug(dcKeba()) << " - Stop reason" << report.stopReason;
|
|
qCDebug(dcKeba()) << " - RFID Tag" << report.rfidTag;
|
|
qCDebug(dcKeba()) << " - RFID Class" << report.rfidClass;
|
|
qCDebug(dcKeba()) << " - Serial number" << report.serialNumber;
|
|
qCDebug(dcKeba()) << " - Uptime" << report.seconds;
|
|
|
|
// Note: all these infos are from the meter...
|
|
if (thing->thingClassId() == kebaSimpleThingClassId) {
|
|
qCDebug(dcKeba()) << "Received" << reportNumber << "from keba but this model has no meter. Ignoring report data...";
|
|
return;
|
|
}
|
|
|
|
if (reportNumber == 100) {
|
|
// Report 100 is the current charging session
|
|
if (report.endTime == 0) {
|
|
// if the charing session is finished the end time will be set
|
|
double duration = (report.seconds - report.startTime) / 60.00;
|
|
thing->setStateValue("sessionTime", duration);
|
|
} else {
|
|
// Charging session is finished and copied to Report 101
|
|
}
|
|
|
|
} else if (reportNumber == 101) {
|
|
// Report 101 is the lastest finished session
|
|
if (report.serialNumber == thing->paramValue(m_serialNumberParamTypeIds.value(thing->thingClassId())).toString()) {
|
|
if (!m_lastSessionId.contains(thing->id())) {
|
|
// This happens after reboot
|
|
m_lastSessionId.insert(thing->id(), report.sessionId);
|
|
} else {
|
|
if (m_lastSessionId.value(thing->id()) != report.sessionId) {
|
|
qCDebug(dcKeba()) << "New session id receivd";
|
|
Event event;
|
|
event.setEventTypeId(kebaChargingSessionFinishedEventTypeId);
|
|
event.setThingId(thing->id());
|
|
ParamList params;
|
|
params << Param(kebaChargingSessionFinishedEventEnergyParamTypeId, report.presentEnergy);
|
|
params << Param(kebaChargingSessionFinishedEventDurationParamTypeId, report.endTime);
|
|
params << Param(kebaChargingSessionFinishedEventIdParamTypeId);
|
|
event.setParams(params);
|
|
emit emitEvent(event);
|
|
}
|
|
}
|
|
} else {
|
|
qCWarning(dcKeba()) << "Received report but the serial number didn't match";
|
|
}
|
|
} else {
|
|
qCWarning(dcKeba()) << "Received unhandled report" << reportNumber;
|
|
}
|
|
}
|
|
|
|
void IntegrationPluginKeba::onBroadcastReceived(KeContact::BroadcastType type, const QVariant &content)
|
|
{
|
|
KeContact *keba = static_cast<KeContact *>(sender());
|
|
Thing *thing = myThings().findById(m_kebaDevices.key(keba));
|
|
if (!thing)
|
|
return;
|
|
|
|
qCDebug(dcKeba()) << "Broadcast received" << type << "value" << content;
|
|
|
|
switch (type) {
|
|
case KeContact::BroadcastTypePlug:
|
|
setDevicePlugState(thing, KeContact::PlugState(content.toInt()));
|
|
break;
|
|
case KeContact::BroadcastTypeInput:
|
|
thing->setStateValue("input", (content.toInt() == 1));
|
|
break;
|
|
case KeContact::BroadcastTypeEPres:
|
|
thing->setStateValue("sessionEnergy", content.toInt() / 10000.00);
|
|
break;
|
|
case KeContact::BroadcastTypeState:
|
|
setDeviceState(thing, KeContact::State(content.toInt()));
|
|
break;
|
|
case KeContact::BroadcastTypeMaxCurr:
|
|
//Current preset value via Control pilot in milliampere
|
|
break;
|
|
case KeContact::BroadcastTypeEnableSys:
|
|
thing->setStateValue("systemEnabled", (content.toInt() != 0));
|
|
break;
|
|
}
|
|
}
|