diff --git a/debian/changelog b/debian/changelog index a3156c4c..36b84298 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +etm-powersync-plugins (1.15.0+etm2) trixie; urgency=medium + + * Add powersync-plugin-keba: fork of the nymea KEBA plugin, patched for the + French P30 PV-Edition with socket shutter (KC-P30-ESS...). + * Provides/Replaces/Conflicts nymea-plugin-keba. + + -- ETM-Schurig SARL Sun, 31 May 2026 12:00:00 +0200 + etm-powersync-plugins (1.15.0+etm1) trixie; urgency=medium * Initial ETM packaging of the openmeteo integration plugin. diff --git a/debian/control b/debian/control index faa635d7..faa81caa 100644 --- a/debian/control +++ b/debian/control @@ -22,3 +22,16 @@ Description: PowerSync integration plugin for Open-Meteo weather data This package contains the nymea integration plugin to fetch weather and solar forecast data from the Open-Meteo online service, for use by the ETM PowerSync home energy management system. + +Package: powersync-plugin-keba +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Provides: nymea-plugin-keba +Replaces: nymea-plugin-keba +Conflicts: nymea-plugin-keba +Description: PowerSync integration plugin for KEBA KeContact wallboxes (ETM fork) + Fork of the nymea KEBA plugin, patched to recognise the French KC-P30 + PV-Edition with socket shutter (KC-P30-ESS...), wrongly rejected by the + upstream discovery. Uses the KEBA UDP protocol. diff --git a/debian/powersync-plugin-keba.install b/debian/powersync-plugin-keba.install new file mode 100644 index 00000000..6479ac8f --- /dev/null +++ b/debian/powersync-plugin-keba.install @@ -0,0 +1 @@ +usr/lib/*/nymea/plugins/libnymea_integrationpluginkeba.so diff --git a/debian/powersync-plugin-openmeteo.install b/debian/powersync-plugin-openmeteo.install new file mode 100644 index 00000000..19acbbb0 --- /dev/null +++ b/debian/powersync-plugin-openmeteo.install @@ -0,0 +1 @@ +usr/lib/*/nymea/plugins/libnymea_integrationpluginopenmeteo.so diff --git a/etm-powersync-plugins.pro b/etm-powersync-plugins.pro index a0fca119..a675310b 100644 --- a/etm-powersync-plugins.pro +++ b/etm-powersync-plugins.pro @@ -1,7 +1,8 @@ TEMPLATE = subdirs PLUGIN_DIRS = \ - openmeteo \ + keba \ + openmeteo # eastron (à finir : pas de code) # waveshare (à normaliser : noms incohérents) diff --git a/keba/README.md b/keba/README.md new file mode 100644 index 00000000..a667ad10 --- /dev/null +++ b/keba/README.md @@ -0,0 +1,31 @@ +# Keba Wallbox + +This plugin allows to control Keba KeContact EV-Charging stations. + +## Supported Things + +* KeContact Wallbox + * P20 (certain models) + * P30 + * c-series + * x-series + * PV Edition + * BMW (certain models) + * [Keba Deutschland Edition](https://a.storyblok.com/f/40131/x/fc59dc7bf7/datenblatt_deutschland_edition.pdf) (DE440) +(by March 2022) + +Please make sure that your model supports communication through the UDP protocol. +The [product overview](https://www.keba.com/download/x/21634787f7/kecontact-p30_productoverview_en.pdf) helps to verify about your models capabilities. + +## Requirements + +* nymea and the wallbox are required to be in the same network. +* UDP Port 7090 must not be blocked by a firewall or router. +* The package "nymea-plugin-keba" must be installed. +* KeContact P20 Charging station with network connection (LSA+ socket). Firmware version: `2.5` or higher. +* KeContact P30 Charging station or BMW wallbox. Firmware version `3.05` of higher. +* **Enabled UDP function with DIP-switch `DSW1.3 = ON`.** + +## More information + +https://www.keba.com/en/emobility/products/product-overview/product_overview diff --git a/keba/integrationpluginkeba.cpp b/keba/integrationpluginkeba.cpp new file mode 100644 index 00000000..2f7e9f76 --- /dev/null +++ b/keba/integrationpluginkeba.cpp @@ -0,0 +1,782 @@ +// 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 "integrationpluginkeba.h" +#include "kebaproductinfo.h" +#include "plugininfo.h" + +#include +#include +#include + +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()); + params << Param(m_addressParamTypeIds.value(discoveredThingClassId), result.networkDeviceInfo.thingParamValueAddress()); + 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() == 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"; + 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(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(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); + + 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(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(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(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; + } +} diff --git a/keba/integrationpluginkeba.h b/keba/integrationpluginkeba.h new file mode 100644 index 00000000..579cd024 --- /dev/null +++ b/keba/integrationpluginkeba.h @@ -0,0 +1,94 @@ +// 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 . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINKEBA_H +#define INTEGRATIONPLUGINKEBA_H + +#include +#include +#include + +#include "kecontact.h" +#include "kebadiscovery.h" +#include "kecontactdatalayer.h" + +#include +#include +#include +#include + +#include "extern-plugininfo.h" + +class IntegrationPluginKeba : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginkeba.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginKeba(); + + void init() override; + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + + void postSetupThing(Thing* thing) override; + void thingRemoved(Thing* thing) override; + + void executeAction(ThingActionInfo *info) override; + +private: + PluginTimer *m_updateTimer = nullptr; + KeContactDataLayer *m_kebaDataLayer = nullptr; + + QHash m_kebaDevices; + QHash m_monitors; + QHash m_lastSessionId; + QHash m_asyncActions; + KebaDiscovery *m_runningDiscovery = nullptr; + + QHash m_macAddressParamTypeIds; + QHash m_hostNameParamTypeIds; + QHash m_addressParamTypeIds; + QHash m_modelParamTypeIds; + QHash m_serialNumberParamTypeIds; + + void setupKeba(ThingSetupInfo *info, const QHostAddress &address); + + void setDeviceState(Thing *device, KeContact::State state); + void setDevicePlugState(Thing *device, KeContact::PlugState plugState); + + void refresh(Thing *thing, KeContact *keba); + +private slots: + void onCommandExecuted(QUuid requestId, bool success); + void onReportTwoReceived(const KeContact::ReportTwo &reportTwo); + void onReportThreeReceived(const KeContact::ReportThree &reportThree); + void onReport1XXReceived(int reportNumber, const KeContact::Report1XX &report); + void onBroadcastReceived(KeContact::BroadcastType type, const QVariant &content); +}; + +#endif // INTEGRATIONPLUGINKEBA_H diff --git a/keba/integrationpluginkeba.json b/keba/integrationpluginkeba.json new file mode 100644 index 00000000..77f9dddd --- /dev/null +++ b/keba/integrationpluginkeba.json @@ -0,0 +1,678 @@ +{ + "displayName": "Keba KeContact", + "name": "Keba", + "id": "9142b09f-30a9-43d0-9ede-2f8debe075ac", + "vendors": [ + { + "id": "f7cda40b-829a-4675-abaa-485697430f5f", + "displayName": "Keba", + "name": "keba", + "thingClasses": [ + { + "id": "900dacec-cae7-4a37-95ba-501846368ea2", + "name": "keba", + "displayName": "Keba KeContact", + "createMethods": ["discovery", "user"], + "interfaces": ["evcharger", "smartmeterconsumer", "connectable", "networkdevice"], + "paramTypes":[ + { + "id": "c2df921d-ff8b-411c-9b1d-04a437d7dfa6", + "name": "macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "", + "readOnly": true + }, + { + "id": "0cc79bb7-3162-432c-a7bc-45f9b5542fbd", + "name": "hostName", + "displayName": "Host name", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "" + }, + { + "id": "22f70789-33c2-4183-8bca-0d3108b1c80b", + "name": "address", + "displayName": "IP address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "" + }, + { + "id": "45255155-318b-4204-8ce6-2c106a56286d", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "", + "readOnly": true + }, + { + "id": "a996c698-4831-4977-8979-f76f78ac7da8", + "name": "model", + "displayName": "Product name", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "", + "readOnly": true + } + ], + "stateTypes": [ + { + "id": "ce813458-d7d8-4f40-9648-dba4c41e92f0", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "83ed0774-2a91-434d-b03c-d920d02f2981", + "name": "power", + "displayName": "Charging enabled", + "displayNameEvent": "Charging enabled changed", + "displayNameAction": "Set charging enabled", + "type": "bool", + "writable": true, + "defaultValue": false, + "suggestLogging": true + }, + { + "id": "e5631593-f486-47cb-9951-b7597d0b769b", + "name": "systemEnabled", + "displayName": "System enabled", + "displayNameEvent": "System enabled changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "6713b2e7-41b3-4596-a304-3065726bdbe4", + "name": "phaseCount", + "displayName": "Number of connected phases", + "displayNameEvent": "Number of connected phases changed", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 1 + }, + { + "id": "539e5602-6dd9-465d-9705-3bb59bcf8982", + "name": "activity", + "displayName": "Activity", + "displayNameEvent": "Activity changed", + "type": "QString", + "defaultValue": "-" + }, + { + "id": "3b4d29f3-3101-47ad-90fd-269b6348783b", + "name": "plugState", + "displayName": "Plug state", + "displayNameEvent": "Plug state changed", + "type": "QString", + "defaultValue": "-" + }, + { + "id": "6c227717-f420-4dcd-bd52-49973715603b", + "name": "pluggedIn", + "displayName": "Car plugged in", + "displayNameEvent": "Car plugged in changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "c9785626-2501-478d-8c18-c42ad5d9a269", + "name": "charging", + "displayName": "Charging", + "displayNameEvent": "Charging changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "593656f0-babf-4308-8767-68f34e10fb15", + "name": "maxChargingCurrent", + "displayName": "Maximal charging current", + "displayNameEvent": "Maximal charging current changed", + "displayNameAction": "Set maximal charging current", + "type": "double", + "unit": "Ampere", + "defaultValue": 6, + "minValue": 6, + "maxValue": 32, + "stepSize": 0.001, + "writable": true, + "suggestLogging": true + }, + { + "id": "3c7b83a0-0e42-47bf-9788-dde6aab5ceea", + "name": "maxChargingCurrentPercent", + "displayName": "Maximal charging current in percent", + "displayNameEvent": "Maximal charging current percentage changed", + "type": "uint", + "unit": "Percentage", + "defaultValue": 100, + "minValue": 0, + "maxValue": 100 + }, + { + "id": "33e2ed95-f01e-44db-8156-34d124a8ecc8", + "name": "maxChargingCurrentHardware", + "displayName": "Maximal hardware charging current", + "displayNameEvent": "Maximal hardware charging current changed", + "type": "uint", + "unit": "Ampere", + "defaultValue": 32, + "suggestLogging": true + }, + { + "id": "4a2d75d8-a3a0-4b40-9ca7-e8b6f11d0ef9", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "displayNameEvent": "Voltage phase A changed", + "type": "int", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "c8344ca5-21ac-4cd1-8f4b-e5ed202c5862", + "name": "voltagePhaseB", + "displayName": "Voltage phase B", + "displayNameEvent": "Voltage phase B changed", + "type": "int", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "5f01e86c-0943-4849-a01a-db441916ebd5", + "name": "voltagePhaseC", + "displayName": "Voltage phase C", + "displayNameEvent": "Voltage phase C changed", + "type": "int", + "unit": "Volt", + "defaultValue": 0, + "cached": false + }, + { + "id": "31ec17b0-11e3-4332-92b0-fea821cf024f", + "name": "currentPhaseA", + "displayName": "Current phase A", + "displayNameEvent": "Current phase A changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "suggestLogging": true, + "cached": false + }, + { + "id": "cdc7e10a-0d0a-4e93-ad2c-d34ffca45c97", + "name": "currentPhaseB", + "displayName": "Current phase B", + "displayNameEvent": "Current phase B changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "suggestLogging": true, + "cached": false + }, + { + "id": "da838dc8-85f0-4e55-b4b5-cb93a43b373d", + "name": "currentPhaseC", + "displayName": "Current phase C", + "displayNameEvent": "Current phase C changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00, + "suggestLogging": true, + "cached": false + }, + { + "id": "7af9e93b-099d-4d9d-a480-9c0f66aecd8b", + "name": "currentPower", + "displayName": "Power consumption", + "displayNameEvent": "Power consumtion changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0.00, + "suggestLogging": true, + "cached": false + }, + { + "id": "889c3c9a-96b4-4408-bd9a-d79e36ed9296", + "name": "powerFactor", + "displayName": "Power factor", + "displayNameEvent": "Power factor changed", + "type": "double", + "unit": "Percentage", + "defaultValue": 0.00, + "cached": false + }, + { + "id": "1d30ce60-2ea0-450f-817e-5c88f59ebfbf", + "name": "sessionId", + "displayName": "Session ID", + "displayNameEvent": "Session ID changed", + "type": "uint", + "defaultValue": "" + }, + { + "id": "a6f35ea0-aaea-438b-b818-6d161762611e", + "name": "sessionTime", + "displayName": "Session time", + "displayNameEvent": "Session time changed", + "type": "int", + "unit": "Minutes", + "defaultValue": 0, + "cached": false + }, + { + "id": "8e277efe-21ef-4536-bfc0-901b32d44d7c", + "name": "sessionEnergy", + "displayName": "Session energy", + "displayNameEvent": "Session energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0, + "suggestLogging": true, + "cached": false + }, + { + "id": "41e179b3-29a2-43ec-b537-023a527081e8", + "name": "totalEnergyConsumed", + "displayName": "Total energy consumed", + "displayNameEvent": "Total energy consumption changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0, + "suggestLogging": true + }, + { + "id": "96b2d176-6460-4109-8824-3af4679c6573", + "name": "outputX2", + "displayName": "Output X2", + "displayNameEvent": "Output X2 changed", + "displayNameAction": "Set output X2", + "type": "bool", + "writable": true, + "defaultValue": false + }, + { + "id": "ba600276-8b36-4404-b8ec-415245e5bc15", + "name": "input", + "displayName": "Input", + "displayNameEvent": "Input changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "3421ecf9-c95f-4dc1-ad0c-144e9b6ae056", + "name": "uptime", + "displayName": "Uptime", + "displayNameEvent": "Uptime changed", + "type": "int", + "unit": "Minutes", + "defaultValue": 0, + "cached": false + }, + { + "id": "b44bc948-1234-4f87-9a22-bfb6de09df4d", + "name": "error1", + "displayName": "Error 1", + "displayNameEvent": "Error 1 changed", + "type": "int", + "defaultValue": 0, + "cached": false + }, + { + "id": "afca201a-5213-43fe-bfec-cae6ce7509d2", + "name": "error2", + "displayName": "Error 2", + "displayNameEvent": "Error 2 changed", + "type": "int", + "defaultValue": 0, + "cached": false + }, + { + "id": "f1758c5c-2c02-41cb-93ec-b778a3c78d28", + "name": "failsafeMode", + "displayName": "Failsafe mode", + "displayNameEvent": "Failsafe mode changed", + "displayNameAction": "Set failsafe mode", + "writable": true, + "type": "bool", + "defaultValue": false + }, + { + "id": "e941ace5-fb7f-4dc2-b3f2-188233f4e934", + "name": "firmware", + "displayName": "Firmware", + "displayNameEvent": "Firmware changed", + "type": "QString", + "defaultValue": "" + }, + { + "id": "11501a4b-8b83-4b92-be3c-a714d507f158", + "name": "hostAddress", + "displayName": "Host address", + "displayNameEvent": "Host address changed", + "type": "QString", + "defaultValue": "http://127.0.0.1" + } + ], + "actionTypes": [ + { + "id": "158b1a8f-fde9-4191-bf42-4ece5fe582e6", + "name": "display", + "displayName": "Display", + "paramTypes": [ + { + "id": "4e69a761-f4f1-42d0-83db-380894a86ebc", + "name": "message", + "displayName": "Display message", + "type": "QString", + "defaultValue": "" + } + ] + } + ], + "eventTypes": [ + { + "id": "dac02c37-f051-481a-ae99-1de0885ef37a", + "name": "chargingSessionFinished", + "displayName": "Charging session finished", + "paramTypes": [ + { + "id": "33446eae-f2cc-4cf2-af29-b3a45e4b91c0", + "name": "id", + "displayName": "ID", + "type": "int", + "defaultValue": "0" + }, + { + "id": "60494d6f-853b-42b8-894e-108a52ed6feb", + "name": "duration", + "displayName": "Duration", + "type": "int", + "unit": "Minutes", + "defaultValue": 0 + }, + { + "id": "c8de58b6-b671-4fee-b552-d2c14a37a769", + "name": "energy", + "displayName": "Energy", + "type": "double", + "defaultValue": 0.00, + "unit": "KiloWattHour" + } + ] + } + ] + }, + { + "id": "c5bca9d2-2a17-40c4-8bb2-ba89783a6dd1", + "name": "kebaSimple", + "displayName": "KeConnect German Edition", + "createMethods": ["discovery", "user"], + "interfaces": ["evcharger", "connectable", "networkdevice"], + "paramTypes":[ + { + "id": "e438179a-5202-4106-a622-d9e10a74fed9", + "name": "macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "TextLine", + "defaultValue":"", + "readOnly": true + }, + { + "id": "96104d2b-2fd3-4f20-bc8c-7b873d0b0f9e", + "name": "hostName", + "displayName": "Host name", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "" + }, + { + "id": "a0c60511-4082-4109-9a9f-383076680c4f", + "name": "address", + "displayName": "IP address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "" + }, + { + "id": "6f732eb9-1711-4da0-a9a4-abcfa19f5e34", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "inputType": "TextLine", + "defaultValue":"", + "readOnly": true + }, + { + "id": "5e49d289-9e32-47a8-8b30-43cb949695c8", + "name": "model", + "displayName": "Product name", + "type": "QString", + "inputType": "TextLine", + "defaultValue":"", + "readOnly": true + } + ], + "settingsTypes": [ + { + "id": "42f32882-ae1c-455e-a044-3f589056a148", + "name": "phaseCount", + "displayName": "Phase count", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 3 + } + ], + "stateTypes": [ + { + "id": "995f2ccf-2082-434e-a46d-c506862e6d6a", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "63f84293-62aa-420d-bc0d-cc48618c6526", + "name": "power", + "displayName": "Charging enabled", + "displayNameEvent": "Charging enabled changed", + "displayNameAction": "Set charging enabled", + "type": "bool", + "writable": true, + "defaultValue": false, + "suggestLogging": true + }, + { + "id": "8ade4b68-e44e-425c-87ea-a35d176f337d", + "name": "systemEnabled", + "displayName": "System enabled", + "displayNameEvent": "System enabled changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "2473e39b-9641-4236-be56-e706d7797161", + "name": "phaseCount", + "displayName": "Number of connected phases", + "displayNameEvent": "Number of connected phases changed", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 3 + }, + { + "id": "955ffd64-42f6-4000-94c5-c7f862daa438", + "name": "activity", + "displayName": "Activity", + "displayNameEvent": "Activity changed", + "type": "QString", + "defaultValue": "-" + }, + { + "id": "82aa0d67-eea6-4a5e-b7ab-2848a4012490", + "name": "plugState", + "displayName": "Plug state", + "displayNameEvent": "Plug state changed", + "type": "QString", + "defaultValue": "-" + }, + { + "id": "faf68cc9-f014-4db5-94fa-0f10a0b85fb1", + "name": "pluggedIn", + "displayName": "Car plugged in", + "displayNameEvent": "Car plugged in changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "38affdf2-f62e-458c-b738-8db81aa13790", + "name": "charging", + "displayName": "Charging", + "displayNameEvent": "Charging changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "2a72ad9e-96bd-4281-afb7-ce4f5c6f5052", + "name": "maxChargingCurrent", + "displayName": "Maximal charging current", + "displayNameEvent": "Maximal charging current changed", + "displayNameAction": "Set maximal charging current", + "type": "double", + "unit": "Ampere", + "defaultValue": 6, + "minValue": 6, + "maxValue": 32, + "stepSize": 0.001, + "writable": true, + "suggestLogging": true + }, + { + "id": "33631b7f-a675-4625-8095-31e09e03a010", + "name": "maxChargingCurrentPercent", + "displayName": "Maximal charging current in percent", + "displayNameEvent": "Maximal charging current percentage changed", + "type": "uint", + "unit": "Percentage", + "defaultValue": 100, + "minValue": 0, + "maxValue": 100 + }, + { + "id": "f94a2381-28a8-478e-ac44-0902a5be8885", + "name": "maxChargingCurrentHardware", + "displayName": "Maximal hardware charging current", + "displayNameEvent": "Maximal hardware charging current changed", + "type": "uint", + "unit": "Ampere", + "defaultValue": 32, + "suggestLogging": true + }, + { + "id": "043ea799-4348-44f9-985d-bee2ba280957", + "name": "outputX2", + "displayName": "Output X2", + "displayNameEvent": "Output X2 changed", + "displayNameAction": "Set output X2", + "type": "bool", + "writable": true, + "defaultValue": false + }, + { + "id": "0ca0921d-5516-44fb-9483-242d9bb7a2d0", + "name": "input", + "displayName": "Input", + "displayNameEvent": "Input changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "2cffff03-63b2-468d-b2ef-a4741401d7c8", + "name": "uptime", + "displayName": "Uptime", + "displayNameEvent": "Uptime changed", + "type": "int", + "unit": "Minutes", + "defaultValue": 0, + "cached": false + }, + { + "id": "8380c340-84ee-4d62-84b0-7c5738ab66bc", + "name": "error1", + "displayName": "Error 1", + "displayNameEvent": "Error 1 changed", + "type": "int", + "defaultValue": 0, + "cached": false + }, + { + "id": "afe287a2-35e2-4762-a6bf-79d7c31d32ab", + "name": "error2", + "displayName": "Error 2", + "displayNameEvent": "Error 2 changed", + "type": "int", + "defaultValue": 0, + "cached": false + }, + { + "id": "bfad6a1a-40e0-4b32-9f42-09efd5a7e94c", + "name": "failsafeMode", + "displayName": "Failsafe mode", + "displayNameEvent": "Failsafe mode changed", + "displayNameAction": "Set failsafe mode", + "writable": true, + "type": "bool", + "defaultValue": false + }, + { + "id": "d473770e-c5b4-4845-8215-0dea304ea202", + "name": "firmware", + "displayName": "Firmware", + "displayNameEvent": "Firmware changed", + "type": "QString", + "defaultValue": "" + }, + { + "id": "eef1d569-f383-4980-8cb8-06ccc5069b0b", + "name": "hostAddress", + "displayName": "Host address", + "displayNameEvent": "Host address changed", + "type": "QString", + "defaultValue": "http://127.0.0.1" + } + ], + "actionTypes": [ + { + "id": "e756c842-bec5-42ee-a28b-280d48e834b1", + "name": "display", + "displayName": "Display", + "paramTypes": [ + { + "id": "ec14a880-0546-431c-ab4e-578d56ecbfb9", + "name": "message", + "displayName": "Display message", + "type": "QString", + "defaultValue": "" + } + ] + } + ], + "eventTypes": [ ] + } + ] + } + ] +} + diff --git a/keba/keba.pro b/keba/keba.pro new file mode 100644 index 00000000..af37ee02 --- /dev/null +++ b/keba/keba.pro @@ -0,0 +1,17 @@ +include(../plugins.pri) + +QT *= network + +SOURCES += \ + integrationpluginkeba.cpp \ + kebadiscovery.cpp \ + kebaproductinfo.cpp \ + kecontact.cpp \ + kecontactdatalayer.cpp + +HEADERS += \ + integrationpluginkeba.h \ + kebadiscovery.h \ + kebaproductinfo.h \ + kecontact.h \ + kecontactdatalayer.h diff --git a/keba/keba.svg b/keba/keba.svg new file mode 100644 index 00000000..78e6eda7 --- /dev/null +++ b/keba/keba.svg @@ -0,0 +1,115 @@ + + + \ No newline at end of file diff --git a/keba/kebadiscovery.cpp b/keba/kebadiscovery.cpp new file mode 100644 index 00000000..d9d5b7a2 --- /dev/null +++ b/keba/kebadiscovery.cpp @@ -0,0 +1,141 @@ +// 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 "kebadiscovery.h" +#include "kecontactdatalayer.h" +#include "extern-plugininfo.h" + +#include +#include + +KebaDiscovery::KebaDiscovery(KeContactDataLayer *kebaDataLayer, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) : + QObject(parent), + m_kebaDataLayer(kebaDataLayer), + m_networkDeviceDiscovery(networkDeviceDiscovery) +{ + // Timer for waiting if network devices responded to the "report 1 request" + m_responseTimer.setInterval(2000); + m_responseTimer.setSingleShot(true); + connect(&m_responseTimer, &QTimer::timeout, this, [=](){ + + // Fill in all network device infos we have + for (int i = 0; i < m_results.count(); i++) { + m_results[i].networkDeviceInfo = m_networkDeviceInfos.get(m_results.at(i).address); + } + + qCInfo(dcKeba()) << "Discovery: Finished successfully. Found" << m_results.count() << "Keba Wallbox"; + emit discoveryFinished(); + }); + + // Read data from the keba data layer and verify if it is a keba report + connect (m_kebaDataLayer, &KeContactDataLayer::datagramReceived, this, [=](const QHostAddress &address, const QByteArray &datagram){ + + // Try to convert the received data to a json document + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(datagram, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcKeba()) << "Discovery: Received data from the keba data link but failed to parse the data as JSON:" << datagram << ":" << error.errorString(); + return; + } + + // Verify JSON data + QVariantMap dataMap = jsonDoc.toVariant().toMap(); + if (!dataMap.contains("ID") || !dataMap.contains("Serial") || !dataMap.contains("Product") || !dataMap.contains("Firmware")) { + qCDebug(dcKeba()) << "Discovery: Received valid JSON data on data layer but they don't seem to be what we are listening for:" << qUtf8Printable(jsonDoc.toJson()); + return; + } + + if (dataMap.value("ID").toInt() != 1) { + qCDebug(dcKeba()) << "Discovery: Received valid Keba JSON data on data layer but this is not a report 1 we requested for:" << qUtf8Printable(jsonDoc.toJson()); + return; + } + + // We have received a report 1 datagram, let's add it to the result + KebaDiscoveryResult result; + result.address = address; + result.product = dataMap.value("Product").toString(); + result.serialNumber = dataMap.value("Serial").toString(); + result.firmwareVersion = dataMap.value("Firmware").toString(); + + bool alreadyDiscovered = false; + foreach (const KebaDiscoveryResult &r, m_results) { + if (r.serialNumber == result.serialNumber) { + alreadyDiscovered = true; + break; + } + } + + if (!alreadyDiscovered) { + m_results.append(result); + qCDebug(dcKeba()) << "Discovery: -->" << address.toString() << result.product << result.serialNumber << result.firmwareVersion; + } + }); +} + +KebaDiscovery::~KebaDiscovery() +{ + qCDebug(dcKeba()) << "Discovery: Destructing"; +} + +void KebaDiscovery::startDiscovery() +{ + // Clean up + cleanup(); + + qCInfo(dcKeba()) << "Discovery: Start searching for Keba wallboxes in the network..."; + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + + // Imedialty check any new device gets discovered + connect(discoveryReply, &NetworkDeviceDiscoveryReply::hostAddressDiscovered, this, &KebaDiscovery::sendReportRequest); + + // Check what might be left on finished + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(dcKeba()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices"; + m_networkDeviceInfos = discoveryReply->networkDeviceInfos(); + + qCDebug(dcKeba()) << "Discovery: Network discovery finished. Start finishing discovery..."; + m_responseTimer.start(); + }); +} + +QList KebaDiscovery::discoveryResults() const +{ + return m_results; +} + +void KebaDiscovery::sendReportRequest(const QHostAddress &address) +{ + m_verifiedAddresses.append(address); + m_kebaDataLayer->write(address, QByteArray("report 1\n")); +} + +void KebaDiscovery::cleanup() +{ + m_networkDeviceInfos.clear(); + m_verifiedAddresses.clear(); + m_results.clear(); +} + + diff --git a/keba/kebadiscovery.h b/keba/kebadiscovery.h new file mode 100644 index 00000000..bfe5a6b4 --- /dev/null +++ b/keba/kebadiscovery.h @@ -0,0 +1,73 @@ +// 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 . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef KEBADISCOVERY_H +#define KEBADISCOVERY_H + +#include +#include + +#include + +class KeContactDataLayer; +class NetworkDeviceDiscovery; + +class KebaDiscovery : public QObject +{ + Q_OBJECT +public: + typedef struct KebaDiscoveryResult { + QString product; + QString serialNumber; + QString firmwareVersion; + QHostAddress address; + NetworkDeviceInfo networkDeviceInfo; + } KebaDiscoveryResult; + + explicit KebaDiscovery(KeContactDataLayer *kebaDataLayer, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + ~KebaDiscovery(); + + void startDiscovery(); + + QList discoveryResults() const; + +signals: + void discoveryFinished(); + +private slots: + void sendReportRequest(const QHostAddress &address); + +private: + KeContactDataLayer *m_kebaDataLayer = nullptr; + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + QTimer m_responseTimer; + + NetworkDeviceInfos m_networkDeviceInfos; + QList m_verifiedAddresses; + QList m_results; + + void cleanup(); +}; + +#endif // KEBADISCOVERY_H diff --git a/keba/kebaproductinfo.cpp b/keba/kebaproductinfo.cpp new file mode 100644 index 00000000..f65aca8e --- /dev/null +++ b/keba/kebaproductinfo.cpp @@ -0,0 +1,294 @@ +// 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 "kebaproductinfo.h" +#include "extern-plugininfo.h" + +KebaProductInfo::KebaProductInfo(const QString &productString) : + m_productString(productString) +{ + // Examples + + // BMW-10-EC240522-E1R + // KC-P30-EC240122-E0R + // KC-P30-EC220112-000-DE + // KC-P30-EC2404B2-M0A-GE + // KC-P30-EC2204U2-E00-PV + + qCDebug(dcKeba()) << "Parsing product information from" << productString.count() << productString; + if (m_productString.count() < 19) { + qCWarning(dcKeba()) << "Invalid product information string size for" << productString << ". Cannot parse."; + m_isValid = false; + return; + } + + QStringList subStrings = productString.split('-'); + if (subStrings.count() < 4) { + qCWarning(dcKeba()) << "Invalid product information format" << subStrings << ". Cannot parse" << productString; + m_isValid = false; + return; + } + + // 1. Manufacturer + // 2. Model + // 3. Desciptor + // 4. Meter infos + + // Parse the product string according to Keba Product code definitions + m_manufacturer = subStrings.at(0); + if (m_manufacturer == "KC") { + m_manufacturer = "KeConnect"; + } + qCDebug(dcKeba()) << "Manufacturer:" << m_manufacturer; + m_model = subStrings.at(1); + qCDebug(dcKeba()) << "Model:" << m_model; + + QString descriptor = subStrings.at(2); // EC240522 + m_countryCode = descriptor.at(0); // E + qCDebug(dcKeba()) << "Country:" << m_countryCode; + + QChar connectorValue = descriptor.at(1); + if (connectorValue.toLower() == QChar('s')) { + m_connector = ConnectorSocket; + qCDebug(dcKeba()) << "Connector: Socket"; + } else if (connectorValue.toLower() == QChar('c')) { + m_connector = ConnectorCable; + qCDebug(dcKeba()) << "Connector: Cable"; + } else { + m_isValid = false; + return; + } + + QChar connectorTypeValue = descriptor.at(2); + if (connectorTypeValue.isDigit() && connectorTypeValue == QChar('1')) { + m_connectorType = Type1; + } else if (connectorTypeValue.isDigit() && connectorTypeValue == QChar('2')) { + m_connectorType = Type2; + } else if (connectorTypeValue.toLower() == QChar('s')) { + m_connectorType = Shutter; + } else { + m_isValid = false; + return; + } + + qCDebug(dcKeba()) << "Connector type:" << m_connectorType; + + QChar connectorCurrentValue = descriptor.at(3); + if (connectorCurrentValue == QChar('1')) { + m_current = Current13A; + } else if (connectorCurrentValue == QChar('2')) { + m_current = Current16A; + } else if (connectorCurrentValue == QChar('3')) { + m_current = Current20A; + } else if (connectorCurrentValue == QChar('4')) { + m_current = Current32A; + } else { + m_isValid = false; + return; + } + + qCDebug(dcKeba()) << "Current:" << m_current; + + // KC-P30-EC24 01 22-E0R + QString cableValue = descriptor.mid(4, 2); + if (cableValue == "00") { + m_cable = NoCable; + qCDebug(dcKeba()) << "Cable: No cable"; + } else if (cableValue == "01") { + m_cable = Cable4m; + qCDebug(dcKeba()) << "Cable: 4 meter"; + } else if (cableValue == "05") { + m_cable = Cable5m; + qCDebug(dcKeba()) << "Cable: 5 meter"; + } else if (cableValue == "04") { + m_cable = Cable6m; + qCDebug(dcKeba()) << "Cable: 6 meter"; + } else if (cableValue == "07") { + m_cable = Cable5p5m; + qCDebug(dcKeba()) << "Cable: 5.5 meter"; + } else { + m_isValid = false; + return; + } + + // KC-P30-EC2401 2 2-E0R + QChar seriesValue = descriptor.at(6); + if (seriesValue == QChar('0')) { + m_series = SeriesE; + qCDebug(dcKeba()) << "Series: E"; + } else if (seriesValue == QChar('1')) { + m_series = SeriesB; + qCDebug(dcKeba()) << "Series: B"; + } else if (seriesValue == QChar('2')) { + m_series = SeriesC; + qCDebug(dcKeba()) << "Series: C"; + } else if (seriesValue == QChar('3')) { + m_series = SeriesA; + qCDebug(dcKeba()) << "Series: A"; + } else if (seriesValue.toLower() == QChar('b')) { + m_series = SeriesXWlan; + qCDebug(dcKeba()) << "Series: X (Wlan)"; + } else if (seriesValue.toLower() == QChar('c')) { + m_series = SeriesXWlan3G; + qCDebug(dcKeba()) << "Series: X (Wlan + 3G)"; + } else if (seriesValue.toLower() == QChar('e')) { + m_series = SeriesXWlan4G; + qCDebug(dcKeba()) << "Series: X (Wlan + 4G)"; + } else if (seriesValue.toLower() == QChar('g')) { + m_series = SeriesX3G; + qCDebug(dcKeba()) << "Series: X (3G)"; + } else if (seriesValue.toLower() == QChar('h')) { + m_series = SeriesX4G; + qCDebug(dcKeba()) << "Series: X (4G)"; + } else if (seriesValue.toLower() == QChar('u')) { + m_series = SeriesSpecial; + qCDebug(dcKeba()) << "Series: Special" + m_productString.right(2); + } else { + qCWarning(dcKeba()) << "Series: Unknown" << productString << "value:" << seriesValue; + m_isValid = false; + return; + } + + // KC-P30-EC24012 2 -E0R + QChar phaseCountValue = descriptor.at(7); + if (phaseCountValue == QChar('1')) { + m_phaseCount = 1; + } else if (phaseCountValue == QChar('2')) { + m_phaseCount = 3; + } else { + m_isValid = false; + return; + } + + qCDebug(dcKeba()) << "Phases:" << m_phaseCount; + + // Meter infos + QString meterInfos = subStrings.at(3); + + QChar meterValue = meterInfos.at(0); + if (meterValue == QChar('0')) { + m_meter = NoMeter; + qCDebug(dcKeba()) << "Meter: No meter"; + } else if (meterValue.toLower() == QChar('e')) { + m_meter = MeterNotCalibrated; + qCDebug(dcKeba()) << "Meter: Not calibrated meter"; + } else if (meterValue.toLower() == QChar('m')) { + m_meter = MeterCalibrated; + qCDebug(dcKeba()) << "Meter: Calibrated meter"; + } else if (meterValue.toLower() == QChar('l')) { + m_meter = MeterCalibratedNationalCertified; + qCDebug(dcKeba()) << "Meter: Calibrated meter (national certified)"; + } else { + m_isValid = false; + return; + } + + QChar authValue = meterInfos.at(2); + if (authValue == QChar('0')) { + m_authorization = NoAuthorization; + qCDebug(dcKeba()) << "Authorization: No authorization"; + } else if (authValue.toLower() == QChar('r')) { + m_authorization = Rfid; + qCDebug(dcKeba()) << "Authorization: RFID"; + } else if (authValue.toLower() == QChar('k')) { + m_authorization = Key; + qCDebug(dcKeba()) << "Authorization: Key"; + } else { + // Note: we don't require this info, so we don't mark it as invalid. + qCDebug(dcKeba()) << "Authorization: Unknown" << authValue; + } + + m_germanEdition = m_productString.toUpper().endsWith("DE"); + qCDebug(dcKeba()) << "German Edition:" << m_germanEdition; +} + +bool KebaProductInfo::isValid() const +{ + return m_isValid; +} + +QString KebaProductInfo::productString() const +{ + return m_productString; +} + +QString KebaProductInfo::manufacturer() const +{ + return m_manufacturer; +} + +QString KebaProductInfo::model() const +{ + return m_model; +} + +QString KebaProductInfo::countryCode() const +{ + return m_countryCode; +} + +KebaProductInfo::Connector KebaProductInfo::connector() const +{ + return m_connector; +} + +KebaProductInfo::ConnectorType KebaProductInfo::connectorType() const +{ + return m_connectorType; +} + +KebaProductInfo::ConnectorCurrent KebaProductInfo::current() const +{ + return m_current; +} + +KebaProductInfo::Cable KebaProductInfo::cable() const +{ + return m_cable; +} + +KebaProductInfo::Series KebaProductInfo::series() const +{ + return m_series; +} + +int KebaProductInfo::phaseCount() const +{ + return m_phaseCount; +} + +KebaProductInfo::Meter KebaProductInfo::meter() const +{ + return m_meter; +} + +KebaProductInfo::Authorization KebaProductInfo::authorization() const +{ + return m_authorization; +} + +bool KebaProductInfo::germanEdition() const +{ + return m_germanEdition; +} diff --git a/keba/kebaproductinfo.h b/keba/kebaproductinfo.h new file mode 100644 index 00000000..aa75af0f --- /dev/null +++ b/keba/kebaproductinfo.h @@ -0,0 +1,132 @@ +// 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 . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef KEBAPRODUCTINFO_H +#define KEBAPRODUCTINFO_H + +#include +#include + +class KebaProductInfo +{ + Q_GADGET +public: + enum Connector { + ConnectorSocket, + ConnectorCable + }; + Q_ENUM(Connector) + + enum ConnectorType { + Type1, + Type2, + Shutter + }; + Q_ENUM(ConnectorType) + + enum ConnectorCurrent { + Current13A = 1, + Current16A = 2, + Current20A = 3, + Current32A = 4 + }; + Q_ENUM(ConnectorCurrent) + + enum Cable { + NoCable = 0, + Cable4m = 1, + Cable6m = 4, + Cable5m = 5, + Cable5p5m = 7 + }; + Q_ENUM(Cable) + + enum Series { + SeriesE, + SeriesB, + SeriesC, + SeriesA, + SeriesXWlan, + SeriesXWlan3G, + SeriesXWlan4G, + SeriesX3G, + SeriesX4G, + SeriesSpecial + }; + Q_ENUM(Series) + + enum Meter { + NoMeter, + MeterNotCalibrated, + MeterCalibrated, + MeterCalibratedNationalCertified + }; + Q_ENUM(Meter) + + enum Authorization { + NoAuthorization, + Rfid, + Key + }; + Q_ENUM(Authorization) + + KebaProductInfo(const QString &productString); + + bool isValid() const; + + QString productString() const; + + // Porperties in the string + QString manufacturer() const; // KC (KeConnect), BMW... + QString model() const; // P30 + QString countryCode() const; // E + Connector connector() const; // Socket / Cable + ConnectorType connectorType() const; // Type 1 / Type 2 + ConnectorCurrent current() const; // 13A, 16A ... + Cable cable() const; // 4m, 6m... + Series series() const; // x, c, a... + int phaseCount() const; // 1 or 3 + Meter meter() const; // No meter, Calibrated, ... + Authorization authorization() const; + bool germanEdition() const; + +private: + bool m_isValid = true; + + QString m_productString; + QString m_manufacturer; + QString m_model; + QString m_countryCode; + Connector m_connector; + ConnectorType m_connectorType; + ConnectorCurrent m_current; + Cable m_cable; + Series m_series; + int m_phaseCount; + Meter m_meter; + Authorization m_authorization; + bool m_germanEdition = false; +}; + +#endif // KEBAPRODUCTINFO_H diff --git a/keba/kecontact.cpp b/keba/kecontact.cpp new file mode 100644 index 00000000..3f866190 --- /dev/null +++ b/keba/kecontact.cpp @@ -0,0 +1,584 @@ +// 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 "kecontact.h" +#include "extern-plugininfo.h" + +#include +#include + +KeContact::KeContact(const QHostAddress &address, KeContactDataLayer *dataLayer, QObject *parent) : + QObject(parent), + m_dataLayer(dataLayer), + m_address(address) +{ + qCDebug(dcKeba()) << "Creating KeContact connection for address" << m_address; + m_requestTimeoutTimer = new QTimer(this); + m_requestTimeoutTimer->setSingleShot(true); + connect(m_requestTimeoutTimer, &QTimer::timeout, this, [this]() { + // This timer will be started when a request is sent and stopped or resetted when a response has been received + setReachable(false); + + if (m_currentRequest.isValid()) { + // Schedule pause timer to send next request + qCWarning(dcKeba()) << "Command timeouted" << m_currentRequest.command(); + emit commandExecuted(m_currentRequest.requestId(), false); + } + + // Timeout...send the next request right the way since at least 5 seconds passed since tha last command + m_currentRequest = KeContactRequest(); + sendNextCommand(); + }); + + m_pauseTimer = new QTimer(this); + m_pauseTimer->setSingleShot(true); + connect(m_pauseTimer, &QTimer::timeout, this, [this](){ + sendNextCommand(); + }); + + connect(m_dataLayer, &KeContactDataLayer::datagramReceived, this, &KeContact::onReceivedDatagram); +} + +KeContact::~KeContact() +{ + qCDebug(dcKeba()) << "Deleting KeContact connection for address" << m_address.toString(); +} + +QHostAddress KeContact::address() const +{ + return m_address; +} + +QUuid KeContact::start(const QByteArray &rfidToken, const QByteArray &rfidClassifier) +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return QUuid(); + } + + QByteArray datagram = "start " + rfidToken + " " + rfidClassifier; + KeContactRequest request(QUuid::createUuid(), datagram); + qCDebug(dcKeba()) << "Start: Datagram:" << datagram; + m_requestQueue.enqueue(request); + sendNextCommand(); + return request.requestId(); +} + +QUuid KeContact::stop(const QByteArray &rfidToken) +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return QUuid(); + } + + QByteArray datagram = "stop " + rfidToken; + KeContactRequest request(QUuid::createUuid(), datagram); + qCDebug(dcKeba()) << "Stop: Datagram:" << datagram; + m_requestQueue.enqueue(request); + sendNextCommand(); + return request.requestId(); +} + +void KeContact::setAddress(const QHostAddress &address) +{ + if (m_address == address) + return; + + qCDebug(dcKeba()) << "Updating Keba connection address from" << m_address.toString() << "to" << address.toString(); + m_address = address; +} + +bool KeContact::reachable() const +{ + return m_reachable; +} + +void KeContact::sendCommand(const QByteArray &command) +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return; + } + + m_dataLayer->write(m_address, command); + m_requestTimeoutTimer->start(5000); +} + +void KeContact::sendNextCommand() +{ + // No message left, we are done + if (m_requestQueue.isEmpty()) + return; + + // Still a request pending + if (m_currentRequest.isValid()) + return; + + m_currentRequest = m_requestQueue.dequeue(); + sendCommand(m_currentRequest.command()); +} + +void KeContact::setReachable(bool reachable) +{ + if (m_reachable == reachable) + return; + + if (reachable) { + qCDebug(dcKeba()) << "The keba wallbox on" << m_address.toString() << "is now reachable again."; + } else { + qCWarning(dcKeba()) << "The keba wallbox on" << m_address.toString() << "is not reachable any more."; + m_requestQueue.clear(); + m_currentRequest = KeContactRequest(); + } + + m_reachable = reachable; + emit reachableChanged(m_reachable); +} + +QUuid KeContact::enableOutput(bool state) +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return QUuid(); + } + + // Print information that we are executing now the update action; + QByteArray datagram; + if (state){ + datagram.append("ena 1"); + } else{ + datagram.append("ena 0"); + } + + KeContactRequest request(QUuid::createUuid(), datagram); + request.setDelayUntilNextCommand(2000); + qCDebug(dcKeba()) << "Enable output: Datagram:" << datagram; + m_requestQueue.enqueue(request); + sendNextCommand(); + return request.requestId(); +} + +QUuid KeContact::setMaxAmpere(int milliAmpere) +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return QUuid(); + } + + if (milliAmpere < 6000 || milliAmpere > 63000) { + qCWarning(dcKeba()) << "KeContact: Set max ampere, currtime mA out of range [6000, 63000]" << milliAmpere; + return QUuid(); + } + + // Print information that we are executing now the update action + qCDebug(dcKeba()) << "Update max current to : " << milliAmpere; + QString commandLine = QString("currtime %1 1").arg(milliAmpere); + QByteArray datagram = commandLine.toUtf8(); + KeContactRequest request(QUuid::createUuid(), datagram); + request.setDelayUntilNextCommand(1200); + qCDebug(dcKeba()) << "Set max charging amps: Datagram:" << datagram; + m_requestQueue.enqueue(request); + sendNextCommand(); + return request.requestId(); +} + +QUuid KeContact::setMaxAmpereGeneral(int milliAmpere) +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return QUuid(); + } + + if (milliAmpere < 6000 || milliAmpere > 63000) { + qCWarning(dcKeba()) << "KeContact: Set max ampere curr, mA out of range [6000, 63000]" << milliAmpere; + return QUuid(); + } + + // Print information that we are executing now the update action + qCDebug(dcKeba()) << "Update general max current to: " << milliAmpere; + QString commandLine = QString("curr %1").arg(milliAmpere); + QByteArray datagram = commandLine.toUtf8(); + KeContactRequest request(QUuid::createUuid(), datagram); + request.setDelayUntilNextCommand(1200); + m_requestQueue.enqueue(request); + sendNextCommand(); + return request.requestId(); +} + +QUuid KeContact::displayMessage(const QByteArray &message) +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return QUuid(); + } + + /* Text shown on the display. Maximum 23 ASCII characters can be used. 0 .. 23 characters + ~ == Σ + $ == blank + , == comma + */ + + qCDebug(dcKeba()) << "Set display message: " << message; + QByteArray datagram; + QByteArray modifiedMessage = message; + modifiedMessage.replace(" ", "$"); + if (modifiedMessage.size() > 23) { + modifiedMessage.resize(23); + } + datagram.append("display 0 0 0 0 " + modifiedMessage); + KeContactRequest request(QUuid::createUuid(), datagram); + qCDebug(dcKeba()) << "Display message: Datagram:" << datagram; + m_requestQueue.enqueue(request); + sendNextCommand(); + return request.requestId(); +} + +QUuid KeContact::chargeWithEnergyLimit(double energy) +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return QUuid(); + } + + QByteArray datagram; + datagram.append("setenergy " + QVariant(static_cast(energy*10000)).toByteArray()); + KeContactRequest request(QUuid::createUuid(), datagram); + qCDebug(dcKeba()) << "Charge with energy limit: Datagram: " << datagram; + m_requestQueue.enqueue(request); + sendNextCommand(); + return request.requestId(); +} + +QUuid KeContact::setFailsafe(int timeout, int current, bool save) +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return QUuid(); + } + + QByteArray data; + data.append("failsave"); + data.append(" "+QVariant(timeout).toByteArray()); + data.append(" "+QVariant(current).toByteArray()); + data.append((save ? " 1":" 0")); + KeContactRequest request(QUuid::createUuid(), data); + qCDebug(dcKeba()) << "Set failsafe mode: Datagram: " << data; + m_requestQueue.enqueue(request); + sendNextCommand(); + return request.requestId(); +} + +void KeContact::getDeviceInformation() +{ + QByteArray data; + data.append("i"); + KeContactRequest request(QUuid::createUuid(), data); + qCDebug(dcKeba()) << "Get device information: Datagram: " << data; + m_requestQueue.enqueue(request); + sendNextCommand(); +} + +void KeContact::getReport1() +{ + getReport(1); +} + +void KeContact::getReport2() +{ + getReport(2); +} + +void KeContact::getReport3() +{ + getReport(3); +} + +void KeContact::getReport1XX(int reportNumber) +{ + getReport(reportNumber); +} + +QUuid KeContact::setOutputX2(bool state) +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return QUuid(); + } + + + QByteArray datagram; + datagram.append("output " + QVariant((state ? 1 : 0)).toByteArray()); + + KeContactRequest request(QUuid::createUuid(), datagram); + qCDebug(dcKeba()) << "Set Output X2, state:" << state << "Datagram:" << datagram; + m_requestQueue.enqueue(request); + sendNextCommand(); + return request.requestId(); +} + +void KeContact::getReport(int reportNumber) +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return; + } + + QByteArray datagram; + datagram.append("report " + QVariant(reportNumber).toByteArray()); + + KeContactRequest request(QUuid::createUuid(), datagram); + qCDebug(dcKeba()) << "Get report" << reportNumber << "Datagram:" << datagram; + m_requestQueue.enqueue(request); + sendNextCommand(); +} + +QUuid KeContact::unlockCharger() +{ + if (!m_dataLayer) { + qCWarning(dcKeba()) << "UDP socket not initialized"; + setReachable(false); + return QUuid(); + } + + QByteArray datagram; + datagram.append("unlock"); + + KeContactRequest request(QUuid::createUuid(), datagram); + qCDebug(dcKeba()) << "Unlock charger: Datagram:" << datagram; + m_requestQueue.enqueue(request); + sendNextCommand(); + return request.requestId(); +} + +void KeContact::onReceivedDatagram(const QHostAddress &address, const QByteArray &datagram) +{ + // Make sure the datagram is for this keba + if (address != m_address) + return; + + if (datagram.contains("TCH-OK")){ + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + + //Command response has been received, now send the next command + m_requestTimeoutTimer->stop(); + + if (m_currentRequest.isValid()) { + if (datagram.contains("done")) { + qCDebug(dcKeba()) << "Command" << m_currentRequest.command() << "finished successfully"; + emit commandExecuted(m_currentRequest.requestId(), true); + } else { + qCWarning(dcKeba()) << "Command" << m_currentRequest.command() << "finished with error" << datagram; + emit commandExecuted(m_currentRequest.requestId(), false); + } + + // Schedule pause timer to send next request + m_pauseTimer->start(m_currentRequest.delayUntilNextCommand()); + m_currentRequest = KeContactRequest(); + } else { + //Probably the response has taken too long and the requestId has been already removed + qCWarning(dcKeba()) << "Received command OK response without pending request." << datagram; + } + } else if (datagram.left(8).contains("Firmware")){ + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + + // Command response has been received, now send the next command + m_requestTimeoutTimer->stop(); + if (m_currentRequest.isValid()) { + // Schedule pause timer to send next request + m_pauseTimer->start(m_currentRequest.delayUntilNextCommand()); + m_currentRequest = KeContactRequest(); + } + + qCDebug(dcKeba()) << "Firmware information received"; + QByteArrayList firmware = datagram.split(':'); + if (firmware.length() >= 2) { + emit deviceInformationReceived(firmware[1]); + } + } else { + //Command response has been received, now send the next command + m_requestTimeoutTimer->stop(); + if (m_currentRequest.isValid()) { + // Schedule pause timer to send next request + m_pauseTimer->start(m_currentRequest.delayUntilNextCommand()); + m_currentRequest = KeContactRequest(); + } + + // Convert the rawdata to a json document + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(datagram, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcKeba()) << "Failed to parse JSON data" << datagram << ":" << error.errorString(); + return; + } + + QVariantMap data = jsonDoc.toVariant().toMap(); + + if (data.contains("ID")) { + int id = data.value("ID").toInt(); + if (id == 1) { + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + + ReportOne reportOne; + //qCDebug(dcKeba()) << "Report 1 received"; + reportOne.product = data.value("Product").toString(); + reportOne.firmware = data.value("Firmware").toString(); + reportOne.serialNumber = data.value("Serial").toString(); + //"Backend:" + //"timeQ": 3 + //"DIP-Sw1": "0x22" + //"DIP-Sw2": + reportOne.dipSw1 = data.value("DIP-Sw1").toString().remove("0x").toUInt(nullptr, 16); + reportOne.dipSw2 = data.value("DIP-Sw2").toString().remove("0x").toUInt(nullptr, 16); + + if (data.contains("COM-module")) { + reportOne.comModule = (data.value("COM-module").toInt() == 1); + } else { + reportOne.comModule = false; + } + if (data.contains("Sec")) { + reportOne.comModule = data.value("Sec").toInt(); + } else { + reportOne.comModule = 0; + } + emit reportOneReceived(reportOne); + + } else if (id == 2) { + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + + ReportTwo reportTwo; + //qCDebug(dcKeba()) << "Report 2 received"; + int state = data.value("State").toInt(); + reportTwo.state = State(state); + reportTwo.error1 = data.value("Error1").toInt(); + reportTwo.error2 = data.value("Error2").toInt(); + reportTwo.plugState = PlugState(data.value("Plug").toInt()); + reportTwo.enableUser = data.value("Enable user").toBool(); + reportTwo.enableSys = data.value("Enable sys").toBool(); + reportTwo.maxCurrent = data.value("Max curr").toInt() / 1000.00; + reportTwo.maxCurrentPercentage = data.value("Max curr %").toInt() / 10.00; + reportTwo.currentHardwareLimitation = data.value("Curr HW").toInt() / 1000.00; + reportTwo.currentUser = data.value("Curr user").toInt() / 1000.00; + reportTwo.currTimer = data.value("Curr timer").toInt() / 1000.00; + reportTwo.timeoutCt = data.value("Tmo CT").toInt(); + reportTwo.currentFailsafe = data.value("Curr FS").toInt() / 1000.00; + reportTwo.timeoutFailsafe = data.value("Tmo FS").toInt(); + reportTwo.setEnergy = data.value("Setenergy").toInt() / 10000.00; + reportTwo.output = data.value("Output").toInt(); + reportTwo.input= data.value("Input").toInt(); + reportTwo.serialNumber = data.value("Serial").toString(); + reportTwo.seconds = data.value("Sec").toInt(); + // Not documented: + //"AuthON": 0 + //"Authreq": 0 + emit reportTwoReceived(reportTwo); + + } else if (id == 3) { + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + + ReportThree reportThree; + //qCDebug(dcKeba()) << "Report 3 received"; + reportThree.currentPhase1 = data.value("I1").toInt() / 1000.00; + reportThree.currentPhase2 = data.value("I2").toInt() / 1000.00; + reportThree.currentPhase3 = data.value("I3").toInt() / 1000.00; + reportThree.voltagePhase1 = data.value("U1").toInt(); + reportThree.voltagePhase2 = data.value("U2").toInt(); + reportThree.voltagePhase3 = data.value("U3").toInt(); + reportThree.power = data.value("P").toInt() / 1000.00; + reportThree.powerFactor = data.value("PF").toInt() / 10.00; + reportThree.energySession = data.value("E pres").toInt() / 10000.00; + reportThree.energyTotal = data.value("E total").toInt() / 10000.00; + reportThree.serialNumber = data.value("Serial").toString(); + reportThree.seconds = data.value("Sec").toInt(); + emit reportThreeReceived(reportThree); + } else if (id >= 100) { + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + + Report1XX report; + //qCDebug(dcKeba()) << "Report" << id << "received"; + report.sessionId = data.value("Session ID").toInt(); + report.currHW = data.value("Curr HW").toInt(); + report.startEnergy = data.value("E start").toInt() / 10000.00; + report.presentEnergy = data.value("E pres").toInt() / 10000.00; + report.startTime = data.value("started[s]").toInt(); + report.endTime = data.value("ended[s]").toInt(); + report.stopReason = data.value("reason").toInt(); + report.rfidTag = data.value("RFID tag").toByteArray(); + report.rfidClass = data.value("RFID class").toByteArray(); + report.serialNumber = data.value("Serial").toString(); + report.seconds = data.value("Sec").toInt(); + emit report1XXReceived(id, report); + } + } else { + // Broadcast message, lets see what we recognize + + if (data.contains("State")) { + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + emit broadcastReceived(BroadcastType::BroadcastTypeState, data.value("State")); + } + if (data.contains("Plug")) { + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + emit broadcastReceived(BroadcastType::BroadcastTypePlug, data.value("Plug")); + } + if (data.contains("Input")) { + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + emit broadcastReceived(BroadcastType::BroadcastTypeInput, data.value("Input")); + } + if (data.contains("Enable sys")) { + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + emit broadcastReceived(BroadcastType::BroadcastTypeEnableSys, data.value("Enable sys")); + } + if (data.contains("Max curr")) { + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + emit broadcastReceived(BroadcastType::BroadcastTypeMaxCurr, data.value("Max curr")); + } + if (data.contains("E pres")) { + // We received valid data from the address over the data link, so the wallbox must be reachable + setReachable(true); + emit broadcastReceived(BroadcastType::BroadcastTypeEPres, data.value("E pres")); + } + } + } +} diff --git a/keba/kecontact.h b/keba/kecontact.h new file mode 100644 index 00000000..39871067 --- /dev/null +++ b/keba/kecontact.h @@ -0,0 +1,239 @@ +// 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 . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef KECONTACT_H +#define KECONTACT_H + +#include +#include +#include +#include +#include +#include +#include + +#include "kecontactdatalayer.h" + +class KeContactRequest +{ +public: + KeContactRequest() = default; + KeContactRequest(const QUuid &requestId, const QByteArray &command) : m_requestId(requestId), m_command(command) { } + + QUuid requestId() const { return m_requestId; } + QByteArray command() const { return m_command; } + + uint delayUntilNextCommand() const { return m_delayUntilNextCommand; } + void setDelayUntilNextCommand(uint delayUntilNextCommand) { m_delayUntilNextCommand = delayUntilNextCommand; } + + bool isValid() { return !m_requestId.isNull() && !m_command.isEmpty(); } + +private: + QUuid m_requestId; + QByteArray m_command; + uint m_delayUntilNextCommand = 200; +}; + + +class KeContact : public QObject +{ + Q_OBJECT +public: + enum DipSwitchOne { + // Power settings + // DIP 6 7 8 (Bit 2, 1, 0) + // 0 0 0 : 10A + // 1 0 0 : 13A + // 0 1 0 : 16A + // 1 1 0 : 20A + // 0 0 1 : 25A + // 1 0 1 : 32A + DipSwitchOnePin8 = 0x01, + DipSwitchOnePin7 = 0x02, + DipSwitchOnePin6 = 0x04, + DipSwitchOnePin5 = 0x08, + DipSwitchOnePin4 = 0x10, + DipSwitchOneSmartHomeInterface = 0x20, // 3 + DipSwitchOneExternalInputX2 = 0x40, // 2 + DipSwitchOneExternalInputX1 = 0x80 // 1 + }; + Q_ENUM(DipSwitchOne) + Q_DECLARE_FLAGS(DipSwitchOneFlag, DipSwitchOne) + + enum State { + StateStarting = 0, + StateNotReady, + StateReady, + StateCharging, + StateError, + StateAuthorizationRejected + }; + Q_ENUM(State) + + enum PlugState { + PlugStateUnplugged = 0, + PlugStatePluggedOnChargingStation = 1, + PlugStatePluggedOnChargingStationAndPlugLocked = 3, + PlugStatePluggedOnChargingStationAndPluggedOnEV = 5, + PlugStatePluggedOnChargingStationAndPlugLockedAndPluggedOnEV = 7 + }; + Q_ENUM(PlugState) + + enum BroadcastType { + BroadcastTypeState = 0, + BroadcastTypePlug, + BroadcastTypeInput, + BroadcastTypeEnableSys, + BroadcastTypeMaxCurr, + BroadcastTypeEPres + }; + Q_ENUM(BroadcastType) + + struct ReportOne { + QString product; // Model name (variant + QString serialNumber; // Serial number + QString firmware; // Firmware version + bool comModule; // Communication module is installed (only P30) + int seconds; // Current system clock since restart of the charging station.(only P30) + quint8 dipSw1; // Dip Switch 1 flag + quint8 dipSw2; // Dip Switch 2 flag + }; + + struct ReportTwo { + State state; //Current state of the charging station + int error1; //Detail code for state 4; exceptions see FAQ on www.kecontact.com + int error2; //Detail code for state 4 exception #6 see FAQ on www.kecontact.com + PlugState plugState; //Current condition of the loading connection + bool enableSys; //Enable state for charging (contains Enable input, RFID, UDP,..). + bool enableUser; //Enable condition via UDP. + double maxCurrent; //Current preset value via Control pilot in ampere. + double maxCurrentPercentage; //Current preset value via Control pilot in 0,1% of the PWM value + double currentHardwareLimitation; //Highest possible charging current of the charging connection. Contains device maximum, DIP-switch setting, cable coding and temperature reduction. + double currentUser; //Current preset value of the user via UDP; A. + double currentFailsafe; //Current preset value for the Failsafe function. + int timeoutFailsafe; //Communication timeout before triggering the Failsafe function. + int currTimer; //Shows the current preset value of currtime. + int timeoutCt; //Shows the remaining time until the current value is accepted. + double setEnergy; //Shows the set energy limit + bool output; //State of the output X2. + bool input; //State of the potential free Enable input X1. When using the input, please pay attention to the information in the installation manual. + QString serialNumber; //Serial number + int seconds; //Current system clock since restart of the charging station. + }; + + struct ReportThree { + int voltagePhase1; //voltage in V + int voltagePhase2; //voltage in V + int voltagePhase3; //voltage in V + double currentPhase1; //current in A + double currentPhase2; //current in A + double currentPhase3; //current in A + double power; //Current power in W (Real Power). + double powerFactor; //Power factor (cosphi) + double energySession; //Power consumption of the current loading session in kWh; Reset with new loading session (state = 2). + double energyTotal; //Total power consumption (persistent) without current loading session kWh; Is summed up after each completed charging session (state = 0). + QString serialNumber; + int seconds; //Current system clock since restart of the charging station. + }; + + struct Report1XX { + int sessionId; // running session counter; not resettable" + double currHW; // maximum charging current of the cable and the charging station setting + double startEnergy; // total energy value at the beginning of the session" + double presentEnergy; // delivered energy until now (equal to E pres in report 3)" + int startTime; // system time when the session was started (seconds from reboot; + int endTime; // system time when the session has ended" + int stopReason; // reason for stopping the session (1 = vehicle unplug; 10 = Rfid token)" + QByteArray rfidTag; // RFID Token ID if session started with rfid + QByteArray rfidClass; // RFID classifier shows the defined color code + QString serialNumber; // serial number of the charging station" + int seconds; // current time when the report was generated + }; + + explicit KeContact(const QHostAddress &address, KeContactDataLayer *dataLayer, QObject *parent = nullptr); + ~KeContact(); + + QHostAddress address() const; + void setAddress(const QHostAddress &address); + + bool reachable() const; + + QUuid start(const QByteArray &rfidToken, const QByteArray &rfidClassifier); // Command “start” + QUuid stop(const QByteArray &rfidToken); // Command “stop” + + QUuid enableOutput(bool state); // Command “ena” + QUuid setMaxAmpere(int milliAmpere); // Command "currtime" + QUuid setMaxAmpereGeneral(int milliAmpere); // Command "curr" + QUuid unlockCharger(); // Command “unlock" + QUuid displayMessage(const QByteArray &message); // Command “display” + QUuid chargeWithEnergyLimit(double energy); // Command “setenergy” + QUuid setFailsafe(int timeout, int current, bool save); // Command “failsafe” + + void getDeviceInformation(); // Command “i” + void getReport1(); // Command “report” + void getReport2(); + void getReport3(); + void getReport1XX(int reportNumber = 100); // Command “report 1xx” + + // Command “currtime” + QUuid setOutputX2(bool state); // Command “output” + +private: + KeContactDataLayer *m_dataLayer = nullptr; + bool m_reachable = false; + + QHostAddress m_address; + + QTimer *m_requestTimeoutTimer = nullptr; + QTimer *m_pauseTimer = nullptr; + int m_serialNumber = 0; + + KeContactRequest m_currentRequest; + QQueue m_requestQueue; + + void getReport(int reportNumber); + + void sendCommand(const QByteArray &command); + void sendNextCommand(); + void setReachable(bool reachable); + +signals: + void reachableChanged(bool status); + void commandExecuted(QUuid requestId, bool success); + void deviceInformationReceived(const QString &firmware); + void reportOneReceived(const KeContact::ReportOne &reportOne); + void reportTwoReceived(const KeContact::ReportTwo &reportTwo); + void reportThreeReceived(const KeContact::ReportThree &reportThree); + void report1XXReceived(int reportNumber, const KeContact::Report1XX &report); + void broadcastReceived(KeContact::BroadcastType type, const QVariant &content); + +private slots: + void onReceivedDatagram(const QHostAddress &address, const QByteArray &datagram); + +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(KeContact::DipSwitchOneFlag); + +#endif // KECONTACT_H + diff --git a/keba/kecontactdatalayer.cpp b/keba/kecontactdatalayer.cpp new file mode 100644 index 00000000..d1127723 --- /dev/null +++ b/keba/kecontactdatalayer.cpp @@ -0,0 +1,95 @@ +// 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 "kecontactdatalayer.h" +#include "extern-plugininfo.h" + +KeContactDataLayer::KeContactDataLayer(QObject *parent) : QObject(parent) +{ + qCDebug(dcKeba()) << "KeContactDataLayer: Creating UDP socket"; + m_udpSocket = new QUdpSocket(this); + connect(m_udpSocket, &QUdpSocket::readyRead, this, &KeContactDataLayer::readPendingDatagrams); + connect(m_udpSocket, &QUdpSocket::stateChanged, this, &KeContactDataLayer::onSocketStateChanged); +#if QT_VERSION >= QT_VERSION_CHECK(5 , 15, 0) + connect(m_udpSocket, &QUdpSocket::errorOccurred, this, &KeContactDataLayer::onSocketError); +#else + connect(m_udpSocket, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(onSocketError(QAbstractSocket::SocketError))); +#endif +} + +KeContactDataLayer::~KeContactDataLayer() +{ + qCDebug(dcKeba()) << "KeContactDataLayer: Deleting UDP socket"; +} + +bool KeContactDataLayer::init() +{ + m_udpSocket->close(); + m_initialized = false; + + if (!m_udpSocket->bind(QHostAddress::AnyIPv4, m_port, QAbstractSocket::ShareAddress)) { + qCWarning(dcKeba()) << "KeContactDataLayer: Cannot bind to port" << m_port; + return false; + } + + m_initialized = true; + return true; +} + +bool KeContactDataLayer::initialized() const +{ + return m_initialized; +} + +void KeContactDataLayer::write(const QHostAddress &address, const QByteArray &data) +{ + qCDebug(dcKeba()) << "KeContactDataLayer: -->" << address.toString() << data; + m_udpSocket->writeDatagram(data, address, m_port); +} + +void KeContactDataLayer::readPendingDatagrams() +{ + QUdpSocket *socket= qobject_cast(sender()); + + QByteArray datagram; + QHostAddress senderAddress; + quint16 senderPort; + + while (socket->hasPendingDatagrams()) { + datagram.resize(socket->pendingDatagramSize()); + socket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort); + qCDebug(dcKeba()) << "KeContactDataLayer: <--" << senderAddress.toString() << datagram; + emit datagramReceived(senderAddress, datagram); + } +} + +void KeContactDataLayer::onSocketError(QAbstractSocket::SocketError error) +{ + qCWarning(dcKeba()) << "KeContactDataLayer: Socket error" << error; +} + +void KeContactDataLayer::onSocketStateChanged(QAbstractSocket::SocketState socketState) +{ + qCDebug(dcKeba()) << "KeContactDataLayer: Socket state changed" << socketState; +} diff --git a/keba/kecontactdatalayer.h b/keba/kecontactdatalayer.h new file mode 100644 index 00000000..7d80718c --- /dev/null +++ b/keba/kecontactdatalayer.h @@ -0,0 +1,58 @@ +// 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 . +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef KECONTACTDATALAYER_H +#define KECONTACTDATALAYER_H + +#include +#include + +class KeContactDataLayer : public QObject +{ + Q_OBJECT +public: + explicit KeContactDataLayer(QObject *parent = nullptr); + ~KeContactDataLayer(); + + bool init(); + bool initialized() const; + + void write(const QHostAddress &address, const QByteArray &data); + +private: + bool m_initialized = false; + int m_port = 7090; + QUdpSocket *m_udpSocket = nullptr; + +signals: + void datagramReceived(const QHostAddress &address, const QByteArray &data); + +private slots: + void readPendingDatagrams(); + void onSocketError(QAbstractSocket::SocketError error); + void onSocketStateChanged(QAbstractSocket::SocketState socketState); + +}; + +#endif // KECONTACTDATALAYER_H diff --git a/keba/meta.json b/keba/meta.json new file mode 100644 index 00000000..c3695b2b --- /dev/null +++ b/keba/meta.json @@ -0,0 +1,13 @@ +{ + "title": "Keba", + "tagline": "Control wallboxes made by Keba.", + "icon": "keba.svg", + "stability": "consumer", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "energy" + ] +} diff --git a/keba/translations/9142b09f-30a9-43d0-9ede-2f8debe075ac-de.ts b/keba/translations/9142b09f-30a9-43d0-9ede-2f8debe075ac-de.ts new file mode 100644 index 00000000..e64df1c0 --- /dev/null +++ b/keba/translations/9142b09f-30a9-43d0-9ede-2f8debe075ac-de.ts @@ -0,0 +1,430 @@ + + + + + IntegrationPluginKeba + + + The communication could not be established. + Die Kommunikation konnte nicht aufgebaut werden. + + + + The network discovery is not available. Please enter the IP address manually. + Das Durchsuchen des Netzwerks ist leider nicht möglich. Bitte geben Sie die IP Adresse manuell ein. + + + + Error opening network port. + Fehler beim Öffnen des Netzwerkports. + + + + Already configured for this IP address. + Es wurde bereits ein Gerät für diese IP Addresse eingerichtet. + + + + The required communication interface is not enabled on this keba. Please make sure the DIP switch 1.3 is switched on and try again. + Die notwendige Schnittstelle ist nicht eingeschaltet. Bitte stellen Sie sicher, dass DIP switch 1.3 eingeschaltet ist und versuchen Sie es erneut. + + + + This model does not support communication with smart devices. + Dieses Model unterstützt keine Kommunikation mit Smarten Geräten. + + + + Keba + + + + Activity + The name of the StateType ({955ffd64-42f6-4000-94c5-c7f862daa438}) of ThingClass kebaSimple +---------- +The name of the StateType ({539e5602-6dd9-465d-9705-3bb59bcf8982}) of ThingClass keba + Aktivität + + + + + Car plugged in + The name of the StateType ({faf68cc9-f014-4db5-94fa-0f10a0b85fb1}) of ThingClass kebaSimple +---------- +The name of the StateType ({6c227717-f420-4dcd-bd52-49973715603b}) of ThingClass keba + Auto angesteckt + + + + + Charging + The name of the StateType ({38affdf2-f62e-458c-b738-8db81aa13790}) of ThingClass kebaSimple +---------- +The name of the StateType ({c9785626-2501-478d-8c18-c42ad5d9a269}) of ThingClass keba + Lade + + + + + + + Charging enabled + The name of the ParamType (ThingClass: kebaSimple, ActionType: power, ID: {63f84293-62aa-420d-bc0d-cc48618c6526}) +---------- +The name of the StateType ({63f84293-62aa-420d-bc0d-cc48618c6526}) of ThingClass kebaSimple +---------- +The name of the ParamType (ThingClass: keba, ActionType: power, ID: {83ed0774-2a91-434d-b03c-d920d02f2981}) +---------- +The name of the StateType ({83ed0774-2a91-434d-b03c-d920d02f2981}) of ThingClass keba + Laden ermöglicht + + + + Charging session finished + The name of the EventType ({dac02c37-f051-481a-ae99-1de0885ef37a}) of ThingClass keba + Ladevorgang beendet + + + + + Connected + The name of the StateType ({995f2ccf-2082-434e-a46d-c506862e6d6a}) of ThingClass kebaSimple +---------- +The name of the StateType ({ce813458-d7d8-4f40-9648-dba4c41e92f0}) of ThingClass keba + Verbunden + + + + Current phase A + The name of the StateType ({31ec17b0-11e3-4332-92b0-fea821cf024f}) of ThingClass keba + Ladestrom Phase A + + + + Current phase B + The name of the StateType ({cdc7e10a-0d0a-4e93-ad2c-d34ffca45c97}) of ThingClass keba + Ladestrom Phase B + + + + Current phase C + The name of the StateType ({da838dc8-85f0-4e55-b4b5-cb93a43b373d}) of ThingClass keba + Ladestrom Phase C + + + + + Display + The name of the ActionType ({e756c842-bec5-42ee-a28b-280d48e834b1}) of ThingClass kebaSimple +---------- +The name of the ActionType ({158b1a8f-fde9-4191-bf42-4ece5fe582e6}) of ThingClass keba + Anzeige + + + + + Display message + The name of the ParamType (ThingClass: kebaSimple, ActionType: display, ID: {ec14a880-0546-431c-ab4e-578d56ecbfb9}) +---------- +The name of the ParamType (ThingClass: keba, ActionType: display, ID: {4e69a761-f4f1-42d0-83db-380894a86ebc}) + Nachricht + + + + Duration + The name of the ParamType (ThingClass: keba, EventType: chargingSessionFinished, ID: {60494d6f-853b-42b8-894e-108a52ed6feb}) + Dauer + + + + Energy + The name of the ParamType (ThingClass: keba, EventType: chargingSessionFinished, ID: {c8de58b6-b671-4fee-b552-d2c14a37a769}) + Energie + + + + + Error 1 + The name of the StateType ({8380c340-84ee-4d62-84b0-7c5738ab66bc}) of ThingClass kebaSimple +---------- +The name of the StateType ({b44bc948-1234-4f87-9a22-bfb6de09df4d}) of ThingClass keba + Error 1 + + + + KeConnect German Edition + The name of the ThingClass ({c5bca9d2-2a17-40c4-8bb2-ba89783a6dd1}) + KeConnect Deutschland Edition + + + + + Error 2 + The name of the StateType ({afe287a2-35e2-4762-a6bf-79d7c31d32ab}) of ThingClass kebaSimple +---------- +The name of the StateType ({afca201a-5213-43fe-bfec-cae6ce7509d2}) of ThingClass keba + Error 2 + + + + + + + Failsafe mode + The name of the ParamType (ThingClass: kebaSimple, ActionType: failsafeMode, ID: {bfad6a1a-40e0-4b32-9f42-09efd5a7e94c}) +---------- +The name of the StateType ({bfad6a1a-40e0-4b32-9f42-09efd5a7e94c}) of ThingClass kebaSimple +---------- +The name of the ParamType (ThingClass: keba, ActionType: failsafeMode, ID: {f1758c5c-2c02-41cb-93ec-b778a3c78d28}) +---------- +The name of the StateType ({f1758c5c-2c02-41cb-93ec-b778a3c78d28}) of ThingClass keba + Failsafe Module + + + + + Firmware + The name of the StateType ({d473770e-c5b4-4845-8215-0dea304ea202}) of ThingClass kebaSimple +---------- +The name of the StateType ({e941ace5-fb7f-4dc2-b3f2-188233f4e934}) of ThingClass keba + Firmware + + + + ID + The name of the ParamType (ThingClass: keba, EventType: chargingSessionFinished, ID: {33446eae-f2cc-4cf2-af29-b3a45e4b91c0}) + ID + + + + + IP address + The name of the ParamType (ThingClass: kebaSimple, Type: thing, ID: {8324cad1-0d9d-4e48-b472-8c22eb7a1057}) +---------- +The name of the ParamType (ThingClass: keba, Type: thing, ID: {730cd3d3-5f0e-4028-a8c2-ced7574f13f3}) + IP Adresse + + + + + Input + The name of the StateType ({0ca0921d-5516-44fb-9483-242d9bb7a2d0}) of ThingClass kebaSimple +---------- +The name of the StateType ({ba600276-8b36-4404-b8ec-415245e5bc15}) of ThingClass keba + Eingang + + + + Keba + The name of the vendor ({f7cda40b-829a-4675-abaa-485697430f5f}) + Keba + + + + + Keba KeContact + The name of the ThingClass ({900dacec-cae7-4a37-95ba-501846368ea2}) +---------- +The name of the plugin Keba ({9142b09f-30a9-43d0-9ede-2f8debe075ac}) + Keba KeContact + + + + + MAC address + The name of the ParamType (ThingClass: kebaSimple, Type: thing, ID: {e438179a-5202-4106-a622-d9e10a74fed9}) +---------- +The name of the ParamType (ThingClass: keba, Type: thing, ID: {c2df921d-ff8b-411c-9b1d-04a437d7dfa6}) + MAC Adresse + + + + + + + Maximal charging current + The name of the ParamType (ThingClass: kebaSimple, ActionType: maxChargingCurrent, ID: {2a72ad9e-96bd-4281-afb7-ce4f5c6f5052}) +---------- +The name of the StateType ({2a72ad9e-96bd-4281-afb7-ce4f5c6f5052}) of ThingClass kebaSimple +---------- +The name of the ParamType (ThingClass: keba, ActionType: maxChargingCurrent, ID: {593656f0-babf-4308-8767-68f34e10fb15}) +---------- +The name of the StateType ({593656f0-babf-4308-8767-68f34e10fb15}) of ThingClass keba + Maximaler Ladestrom + + + + + Maximal charging current in percent + The name of the StateType ({33631b7f-a675-4625-8095-31e09e03a010}) of ThingClass kebaSimple +---------- +The name of the StateType ({3c7b83a0-0e42-47bf-9788-dde6aab5ceea}) of ThingClass keba + Maximaler Ladestrom in Prozent + + + + + Maximal hardware charging current + The name of the StateType ({f94a2381-28a8-478e-ac44-0902a5be8885}) of ThingClass kebaSimple +---------- +The name of the StateType ({33e2ed95-f01e-44db-8156-34d124a8ecc8}) of ThingClass keba + Maximaler Ladestrom der Hardware + + + + Number of connected phases + The name of the StateType ({6713b2e7-41b3-4596-a304-3065726bdbe4}) of ThingClass keba + Anzahl angeschlossener Phasen + + + + Voltage phase A + The name of the StateType ({4a2d75d8-a3a0-4b40-9ca7-e8b6f11d0ef9}) of ThingClass keba + Spannung Phase A + + + + Voltage phase B + The name of the StateType ({c8344ca5-21ac-4cd1-8f4b-e5ed202c5862}) of ThingClass keba + Spannung Phase B + + + + Voltage phase C + The name of the StateType ({5f01e86c-0943-4849-a01a-db441916ebd5}) of ThingClass keba + Spannung Phase C + + + + + + + Output X2 + The name of the ParamType (ThingClass: kebaSimple, ActionType: outputX2, ID: {043ea799-4348-44f9-985d-bee2ba280957}) +---------- +The name of the StateType ({043ea799-4348-44f9-985d-bee2ba280957}) of ThingClass kebaSimple +---------- +The name of the ParamType (ThingClass: keba, ActionType: outputX2, ID: {96b2d176-6460-4109-8824-3af4679c6573}) +---------- +The name of the StateType ({96b2d176-6460-4109-8824-3af4679c6573}) of ThingClass keba + Ausgang X2 + + + + + Plug state + The name of the StateType ({82aa0d67-eea6-4a5e-b7ab-2848a4012490}) of ThingClass kebaSimple +---------- +The name of the StateType ({3b4d29f3-3101-47ad-90fd-269b6348783b}) of ThingClass keba + Stecker-Status + + + + Power consumption + The name of the StateType ({7af9e93b-099d-4d9d-a480-9c0f66aecd8b}) of ThingClass keba + Leistungsaufnahme + + + + Power factor + The name of the StateType ({889c3c9a-96b4-4408-bd9a-d79e36ed9296}) of ThingClass keba + Leistungsfaktor + + + + + Product name + The name of the ParamType (ThingClass: kebaSimple, Type: thing, ID: {5e49d289-9e32-47a8-8b30-43cb949695c8}) +---------- +The name of the ParamType (ThingClass: keba, Type: thing, ID: {a996c698-4831-4977-8979-f76f78ac7da8}) + Produktname + + + + + Serial number + The name of the ParamType (ThingClass: kebaSimple, Type: thing, ID: {6f732eb9-1711-4da0-a9a4-abcfa19f5e34}) +---------- +The name of the ParamType (ThingClass: keba, Type: thing, ID: {45255155-318b-4204-8ce6-2c106a56286d}) + Seriennummer + + + + Session ID + The name of the StateType ({1d30ce60-2ea0-450f-817e-5c88f59ebfbf}) of ThingClass keba + Session ID + + + + Session energy + The name of the StateType ({8e277efe-21ef-4536-bfc0-901b32d44d7c}) of ThingClass keba + Session Energie + + + + Session time + The name of the StateType ({a6f35ea0-aaea-438b-b818-6d161762611e}) of ThingClass keba + Sessiondauer + + + + + Set charging enabled + The name of the ActionType ({63f84293-62aa-420d-bc0d-cc48618c6526}) of ThingClass kebaSimple +---------- +The name of the ActionType ({83ed0774-2a91-434d-b03c-d920d02f2981}) of ThingClass keba + Ermölgliche Laden + + + + + Set failsafe mode + The name of the ActionType ({bfad6a1a-40e0-4b32-9f42-09efd5a7e94c}) of ThingClass kebaSimple +---------- +The name of the ActionType ({f1758c5c-2c02-41cb-93ec-b778a3c78d28}) of ThingClass keba + Setze Failsafe Modus + + + + + Set maximal charging current + The name of the ActionType ({2a72ad9e-96bd-4281-afb7-ce4f5c6f5052}) of ThingClass kebaSimple +---------- +The name of the ActionType ({593656f0-babf-4308-8767-68f34e10fb15}) of ThingClass keba + Setze maximaler Ladestrom + + + + + Set output X2 + The name of the ActionType ({043ea799-4348-44f9-985d-bee2ba280957}) of ThingClass kebaSimple +---------- +The name of the ActionType ({96b2d176-6460-4109-8824-3af4679c6573}) of ThingClass keba + Setze Ausgang X2 + + + + + System enabled + The name of the StateType ({8ade4b68-e44e-425c-87ea-a35d176f337d}) of ThingClass kebaSimple +---------- +The name of the StateType ({e5631593-f486-47cb-9951-b7597d0b769b}) of ThingClass keba + System eingeschaltet + + + + Total energy consumed + The name of the StateType ({41e179b3-29a2-43ec-b537-023a527081e8}) of ThingClass keba + Gesamter Energieverbrauch + + + + + Uptime + The name of the StateType ({2cffff03-63b2-468d-b2ef-a4741401d7c8}) of ThingClass kebaSimple +---------- +The name of the StateType ({3421ecf9-c95f-4dc1-ad0c-144e9b6ae056}) of ThingClass keba + Betriebszeit + + + diff --git a/keba/translations/9142b09f-30a9-43d0-9ede-2f8debe075ac-en_US.ts b/keba/translations/9142b09f-30a9-43d0-9ede-2f8debe075ac-en_US.ts new file mode 100644 index 00000000..4594a984 --- /dev/null +++ b/keba/translations/9142b09f-30a9-43d0-9ede-2f8debe075ac-en_US.ts @@ -0,0 +1,430 @@ + + + + + IntegrationPluginKeba + + + The communication could not be established. + + + + + The network discovery is not available. Please enter the IP address manually. + + + + + Error opening network port. + + + + + Already configured for this IP address. + + + + + The required communication interface is not enabled on this keba. Please make sure the DIP switch 1.3 is switched on and try again. + + + + + This model does not support communication with smart devices. + + + + + Keba + + + + Activity + The name of the StateType ({955ffd64-42f6-4000-94c5-c7f862daa438}) of ThingClass kebaSimple +---------- +The name of the StateType ({539e5602-6dd9-465d-9705-3bb59bcf8982}) of ThingClass keba + + + + + + Car plugged in + The name of the StateType ({faf68cc9-f014-4db5-94fa-0f10a0b85fb1}) of ThingClass kebaSimple +---------- +The name of the StateType ({6c227717-f420-4dcd-bd52-49973715603b}) of ThingClass keba + + + + + + Charging + The name of the StateType ({38affdf2-f62e-458c-b738-8db81aa13790}) of ThingClass kebaSimple +---------- +The name of the StateType ({c9785626-2501-478d-8c18-c42ad5d9a269}) of ThingClass keba + + + + + + + + Charging enabled + The name of the ParamType (ThingClass: kebaSimple, ActionType: power, ID: {63f84293-62aa-420d-bc0d-cc48618c6526}) +---------- +The name of the StateType ({63f84293-62aa-420d-bc0d-cc48618c6526}) of ThingClass kebaSimple +---------- +The name of the ParamType (ThingClass: keba, ActionType: power, ID: {83ed0774-2a91-434d-b03c-d920d02f2981}) +---------- +The name of the StateType ({83ed0774-2a91-434d-b03c-d920d02f2981}) of ThingClass keba + + + + + Charging session finished + The name of the EventType ({dac02c37-f051-481a-ae99-1de0885ef37a}) of ThingClass keba + + + + + + Connected + The name of the StateType ({995f2ccf-2082-434e-a46d-c506862e6d6a}) of ThingClass kebaSimple +---------- +The name of the StateType ({ce813458-d7d8-4f40-9648-dba4c41e92f0}) of ThingClass keba + + + + + Current phase A + The name of the StateType ({31ec17b0-11e3-4332-92b0-fea821cf024f}) of ThingClass keba + + + + + Current phase B + The name of the StateType ({cdc7e10a-0d0a-4e93-ad2c-d34ffca45c97}) of ThingClass keba + + + + + Current phase C + The name of the StateType ({da838dc8-85f0-4e55-b4b5-cb93a43b373d}) of ThingClass keba + + + + + + Display + The name of the ActionType ({e756c842-bec5-42ee-a28b-280d48e834b1}) of ThingClass kebaSimple +---------- +The name of the ActionType ({158b1a8f-fde9-4191-bf42-4ece5fe582e6}) of ThingClass keba + + + + + + Display message + The name of the ParamType (ThingClass: kebaSimple, ActionType: display, ID: {ec14a880-0546-431c-ab4e-578d56ecbfb9}) +---------- +The name of the ParamType (ThingClass: keba, ActionType: display, ID: {4e69a761-f4f1-42d0-83db-380894a86ebc}) + + + + + Duration + The name of the ParamType (ThingClass: keba, EventType: chargingSessionFinished, ID: {60494d6f-853b-42b8-894e-108a52ed6feb}) + + + + + Energy + The name of the ParamType (ThingClass: keba, EventType: chargingSessionFinished, ID: {c8de58b6-b671-4fee-b552-d2c14a37a769}) + + + + + + Error 1 + The name of the StateType ({8380c340-84ee-4d62-84b0-7c5738ab66bc}) of ThingClass kebaSimple +---------- +The name of the StateType ({b44bc948-1234-4f87-9a22-bfb6de09df4d}) of ThingClass keba + + + + + KeConnect German Edition + The name of the ThingClass ({c5bca9d2-2a17-40c4-8bb2-ba89783a6dd1}) + + + + + + Error 2 + The name of the StateType ({afe287a2-35e2-4762-a6bf-79d7c31d32ab}) of ThingClass kebaSimple +---------- +The name of the StateType ({afca201a-5213-43fe-bfec-cae6ce7509d2}) of ThingClass keba + + + + + + + + Failsafe mode + The name of the ParamType (ThingClass: kebaSimple, ActionType: failsafeMode, ID: {bfad6a1a-40e0-4b32-9f42-09efd5a7e94c}) +---------- +The name of the StateType ({bfad6a1a-40e0-4b32-9f42-09efd5a7e94c}) of ThingClass kebaSimple +---------- +The name of the ParamType (ThingClass: keba, ActionType: failsafeMode, ID: {f1758c5c-2c02-41cb-93ec-b778a3c78d28}) +---------- +The name of the StateType ({f1758c5c-2c02-41cb-93ec-b778a3c78d28}) of ThingClass keba + + + + + + Firmware + The name of the StateType ({d473770e-c5b4-4845-8215-0dea304ea202}) of ThingClass kebaSimple +---------- +The name of the StateType ({e941ace5-fb7f-4dc2-b3f2-188233f4e934}) of ThingClass keba + + + + + ID + The name of the ParamType (ThingClass: keba, EventType: chargingSessionFinished, ID: {33446eae-f2cc-4cf2-af29-b3a45e4b91c0}) + + + + + + IP address + The name of the ParamType (ThingClass: kebaSimple, Type: thing, ID: {8324cad1-0d9d-4e48-b472-8c22eb7a1057}) +---------- +The name of the ParamType (ThingClass: keba, Type: thing, ID: {730cd3d3-5f0e-4028-a8c2-ced7574f13f3}) + + + + + + Input + The name of the StateType ({0ca0921d-5516-44fb-9483-242d9bb7a2d0}) of ThingClass kebaSimple +---------- +The name of the StateType ({ba600276-8b36-4404-b8ec-415245e5bc15}) of ThingClass keba + + + + + Keba + The name of the vendor ({f7cda40b-829a-4675-abaa-485697430f5f}) + + + + + + Keba KeContact + The name of the ThingClass ({900dacec-cae7-4a37-95ba-501846368ea2}) +---------- +The name of the plugin Keba ({9142b09f-30a9-43d0-9ede-2f8debe075ac}) + + + + + + MAC address + The name of the ParamType (ThingClass: kebaSimple, Type: thing, ID: {e438179a-5202-4106-a622-d9e10a74fed9}) +---------- +The name of the ParamType (ThingClass: keba, Type: thing, ID: {c2df921d-ff8b-411c-9b1d-04a437d7dfa6}) + + + + + + + + Maximal charging current + The name of the ParamType (ThingClass: kebaSimple, ActionType: maxChargingCurrent, ID: {2a72ad9e-96bd-4281-afb7-ce4f5c6f5052}) +---------- +The name of the StateType ({2a72ad9e-96bd-4281-afb7-ce4f5c6f5052}) of ThingClass kebaSimple +---------- +The name of the ParamType (ThingClass: keba, ActionType: maxChargingCurrent, ID: {593656f0-babf-4308-8767-68f34e10fb15}) +---------- +The name of the StateType ({593656f0-babf-4308-8767-68f34e10fb15}) of ThingClass keba + + + + + + Maximal charging current in percent + The name of the StateType ({33631b7f-a675-4625-8095-31e09e03a010}) of ThingClass kebaSimple +---------- +The name of the StateType ({3c7b83a0-0e42-47bf-9788-dde6aab5ceea}) of ThingClass keba + + + + + + Maximal hardware charging current + The name of the StateType ({f94a2381-28a8-478e-ac44-0902a5be8885}) of ThingClass kebaSimple +---------- +The name of the StateType ({33e2ed95-f01e-44db-8156-34d124a8ecc8}) of ThingClass keba + + + + + Number of connected phases + The name of the StateType ({6713b2e7-41b3-4596-a304-3065726bdbe4}) of ThingClass keba + + + + + Voltage phase A + The name of the StateType ({4a2d75d8-a3a0-4b40-9ca7-e8b6f11d0ef9}) of ThingClass keba + + + + + Voltage phase B + The name of the StateType ({c8344ca5-21ac-4cd1-8f4b-e5ed202c5862}) of ThingClass keba + + + + + Voltage phase C + The name of the StateType ({5f01e86c-0943-4849-a01a-db441916ebd5}) of ThingClass keba + + + + + + + + Output X2 + The name of the ParamType (ThingClass: kebaSimple, ActionType: outputX2, ID: {043ea799-4348-44f9-985d-bee2ba280957}) +---------- +The name of the StateType ({043ea799-4348-44f9-985d-bee2ba280957}) of ThingClass kebaSimple +---------- +The name of the ParamType (ThingClass: keba, ActionType: outputX2, ID: {96b2d176-6460-4109-8824-3af4679c6573}) +---------- +The name of the StateType ({96b2d176-6460-4109-8824-3af4679c6573}) of ThingClass keba + + + + + + Plug state + The name of the StateType ({82aa0d67-eea6-4a5e-b7ab-2848a4012490}) of ThingClass kebaSimple +---------- +The name of the StateType ({3b4d29f3-3101-47ad-90fd-269b6348783b}) of ThingClass keba + + + + + Power consumption + The name of the StateType ({7af9e93b-099d-4d9d-a480-9c0f66aecd8b}) of ThingClass keba + + + + + Power factor + The name of the StateType ({889c3c9a-96b4-4408-bd9a-d79e36ed9296}) of ThingClass keba + + + + + + Product name + The name of the ParamType (ThingClass: kebaSimple, Type: thing, ID: {5e49d289-9e32-47a8-8b30-43cb949695c8}) +---------- +The name of the ParamType (ThingClass: keba, Type: thing, ID: {a996c698-4831-4977-8979-f76f78ac7da8}) + + + + + + Serial number + The name of the ParamType (ThingClass: kebaSimple, Type: thing, ID: {6f732eb9-1711-4da0-a9a4-abcfa19f5e34}) +---------- +The name of the ParamType (ThingClass: keba, Type: thing, ID: {45255155-318b-4204-8ce6-2c106a56286d}) + + + + + Session ID + The name of the StateType ({1d30ce60-2ea0-450f-817e-5c88f59ebfbf}) of ThingClass keba + + + + + Session energy + The name of the StateType ({8e277efe-21ef-4536-bfc0-901b32d44d7c}) of ThingClass keba + + + + + Session time + The name of the StateType ({a6f35ea0-aaea-438b-b818-6d161762611e}) of ThingClass keba + + + + + + Set charging enabled + The name of the ActionType ({63f84293-62aa-420d-bc0d-cc48618c6526}) of ThingClass kebaSimple +---------- +The name of the ActionType ({83ed0774-2a91-434d-b03c-d920d02f2981}) of ThingClass keba + + + + + + Set failsafe mode + The name of the ActionType ({bfad6a1a-40e0-4b32-9f42-09efd5a7e94c}) of ThingClass kebaSimple +---------- +The name of the ActionType ({f1758c5c-2c02-41cb-93ec-b778a3c78d28}) of ThingClass keba + + + + + + Set maximal charging current + The name of the ActionType ({2a72ad9e-96bd-4281-afb7-ce4f5c6f5052}) of ThingClass kebaSimple +---------- +The name of the ActionType ({593656f0-babf-4308-8767-68f34e10fb15}) of ThingClass keba + + + + + + Set output X2 + The name of the ActionType ({043ea799-4348-44f9-985d-bee2ba280957}) of ThingClass kebaSimple +---------- +The name of the ActionType ({96b2d176-6460-4109-8824-3af4679c6573}) of ThingClass keba + + + + + + System enabled + The name of the StateType ({8ade4b68-e44e-425c-87ea-a35d176f337d}) of ThingClass kebaSimple +---------- +The name of the StateType ({e5631593-f486-47cb-9951-b7597d0b769b}) of ThingClass keba + + + + + Total energy consumed + The name of the StateType ({41e179b3-29a2-43ec-b537-023a527081e8}) of ThingClass keba + + + + + + Uptime + The name of the StateType ({2cffff03-63b2-468d-b2ef-a4741401d7c8}) of ThingClass kebaSimple +---------- +The name of the StateType ({3421ecf9-c95f-4dc1-ad0c-144e9b6ae056}) of ThingClass keba + + + +