diff --git a/debian/control b/debian/control index 25d8750..bcffd6f 100644 --- a/debian/control +++ b/debian/control @@ -110,6 +110,21 @@ Description: nymea.io plugin for wallbe ev charging stations . This package will install the nymea.io plugin for wallbe +Package: nymea-plugin-webasto +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-modbus-translations +Description: nymea.io plugin for Webasto Live EV charging stations + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the nymea.io plugin for webasto + Package: nymea-plugins-modbus-translations Section: misc diff --git a/debian/nymea-plugin-webasto.install.in b/debian/nymea-plugin-webasto.install.in new file mode 100644 index 0000000..1eb7eaa --- /dev/null +++ b/debian/nymea-plugin-webasto.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginwebasto.so diff --git a/discovery/discovery.cpp b/discovery/discovery.cpp new file mode 100644 index 0000000..96c459c --- /dev/null +++ b/discovery/discovery.cpp @@ -0,0 +1,272 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "discovery.h" + +#include +#include +#include +#include +#include +#include +NYMEA_LOGGING_CATEGORY(dcDiscovery, "Discovery") + +Discovery::Discovery(QObject *parent) : QObject(parent) +{ + connect(&m_timeoutTimer, &QTimer::timeout, this, &Discovery::onTimeout); +} + +void Discovery::discoverHosts(int timeout) +{ + if (isRunning()) { + qCWarning(dcDiscovery()) << "Discovery already running. Cannot start twice."; + return; + } + m_timeoutTimer.start(timeout * 1000); + + foreach (const QString &target, getDefaultTargets()) { + QProcess *discoveryProcess = new QProcess(this); + m_discoveryProcesses.append(discoveryProcess); + connect(discoveryProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(discoveryFinished(int,QProcess::ExitStatus))); + + QStringList arguments; + arguments << "-oX" << "-" << "-n" << "-sn"; + arguments << target; + + qCDebug(dcDiscovery()) << "Scanning network:" << "nmap" << arguments.join(" "); + discoveryProcess->start(QStringLiteral("nmap"), arguments); + } +} + +void Discovery::abort() +{ + foreach (QProcess *discoveryProcess, m_discoveryProcesses) { + if (discoveryProcess->state() == QProcess::Running) { + qCDebug(dcDiscovery()) << "Kill running discovery process"; + discoveryProcess->terminate(); + discoveryProcess->waitForFinished(5000); + } + } + foreach (QProcess *p, m_pendingArpLookups.keys()) { + p->terminate(); + delete p; + } + m_pendingArpLookups.clear(); + m_pendingNameLookups.clear(); + qDeleteAll(m_scanResults); + m_scanResults.clear(); +} + +bool Discovery::isRunning() const +{ + return !m_discoveryProcesses.isEmpty() || !m_pendingArpLookups.isEmpty() || !m_pendingNameLookups.isEmpty(); +} + +void Discovery::discoveryFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + QProcess *discoveryProcess = static_cast(sender()); + + if (exitCode != 0 || exitStatus != QProcess::NormalExit) { + qCWarning(dcDiscovery()) << "Nmap error failed. Is nmap installed correctly?"; + m_discoveryProcesses.removeAll(discoveryProcess); + discoveryProcess->deleteLater(); + discoveryProcess = nullptr; + finishDiscovery(); + return; + } + + QByteArray data = discoveryProcess->readAll(); + m_discoveryProcesses.removeAll(discoveryProcess); + discoveryProcess->deleteLater(); + discoveryProcess = nullptr; + + QXmlStreamReader reader(data); + + int foundHosts = 0; + qCDebug(dcDiscovery()) << "nmap finished network discovery:"; + while (!reader.atEnd() && !reader.hasError()) { + QXmlStreamReader::TokenType token = reader.readNext(); + if(token == QXmlStreamReader::StartDocument) + continue; + + if(token == QXmlStreamReader::StartElement && reader.name() == "host") { + bool isUp = false; + QString address; + QString macAddress; + QString vendor; + while (!reader.atEnd() && !reader.hasError() && !(token == QXmlStreamReader::EndElement && reader.name() == "host")) { + token = reader.readNext(); + + if (reader.name() == "address") { + QString addr = reader.attributes().value("addr").toString(); + QString type = reader.attributes().value("addrtype").toString(); + if (type == "ipv4" && !addr.isEmpty()) { + address = addr; + } else if (type == "mac") { + macAddress = addr; + vendor = reader.attributes().value("vendor").toString(); + } + } + + if (reader.name() == "status") { + QString state = reader.attributes().value("state").toString(); + if (!state.isEmpty()) + isUp = state == "up"; + } + } + + if (isUp) { + foundHosts++; + qCDebug(dcDiscovery()) << " - host:" << address; + + Host *host = new Host(); + host->setAddress(address); + + if (!macAddress.isEmpty()) { + host->setMacAddress(macAddress); + } else { + QProcess *arpLookup = new QProcess(this); + m_pendingArpLookups.insert(arpLookup, host); + connect(arpLookup, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(arpLookupDone(int,QProcess::ExitStatus))); + arpLookup->start("arp", {"-vn"}); + } + + host->setHostName(vendor); + QHostInfo::lookupHost(address, this, SLOT(hostLookupDone(QHostInfo))); + m_pendingNameLookups.insert(address, host); + + m_scanResults.append(host); + } + } + } + + if (foundHosts == 0 && m_discoveryProcesses.isEmpty()) { + qCDebug(dcDiscovery()) << "Network scan successful but no hosts found in this network"; + finishDiscovery(); + } +} + +void Discovery::hostLookupDone(const QHostInfo &info) +{ + Host *host = m_pendingNameLookups.take(info.addresses().first().toString()); + if (!host) { + // Probably aborted... + return; + } + if (info.error() != QHostInfo::NoError) { + qWarning(dcDiscovery()) << "Host lookup failed:" << info.errorString(); + } + if (host->hostName().isEmpty() || info.hostName() != host->address()) { + host->setHostName(info.hostName()); + } + + finishDiscovery(); +} + +void Discovery::arpLookupDone(int exitCode, QProcess::ExitStatus exitStatus) +{ + QProcess *p = static_cast(sender()); + p->deleteLater(); + + Host *host = m_pendingArpLookups.take(p); + + if (exitCode != 0 || exitStatus != QProcess::NormalExit) { + qCWarning(dcDiscovery()) << "ARP lookup process failed for host" << host->address(); + finishDiscovery(); + return; + } + + QString data = QString::fromLatin1(p->readAll()); + foreach (QString line, data.split('\n')) { + line.replace(QRegExp("[ ]{1,}"), " "); + QStringList parts = line.split(" "); + if (parts.count() >= 3 && parts.first() == host->address() && parts.at(1) == "ether") { + host->setMacAddress(parts.at(2)); + break; + } + } + finishDiscovery(); +} + +void Discovery::onTimeout() +{ + qWarning(dcDiscovery()) << "Timeout hit. Stopping discovery"; + while (!m_discoveryProcesses.isEmpty()) { + QProcess *discoveryProcess = m_discoveryProcesses.takeFirst(); + disconnect(this, SLOT(discoveryFinished(int,QProcess::ExitStatus))); + discoveryProcess->terminate(); + delete discoveryProcess; + } + foreach (QProcess *p, m_pendingArpLookups.keys()) { + p->terminate(); + m_scanResults.removeAll(m_pendingArpLookups.value(p)); + delete p; + } + m_pendingArpLookups.clear(); + m_pendingNameLookups.clear(); + finishDiscovery(); +} + +QStringList Discovery::getDefaultTargets() +{ + QStringList targets; + foreach (const QHostAddress &interface, QNetworkInterface::allAddresses()) { + if (!interface.isLoopback() && interface.scopeId().isEmpty() && interface.protocol() == QAbstractSocket::IPv4Protocol) { + QPair pair = QHostAddress::parseSubnet(interface.toString() + "/24"); + QString newTarget = QString("%1/%2").arg(pair.first.toString()).arg(pair.second); + if (!targets.contains(newTarget)) { + targets.append(newTarget); + } + } + } + return targets; +} + +void Discovery::finishDiscovery() +{ + if (m_discoveryProcesses.count() > 0 || m_pendingNameLookups.count() > 0 || m_pendingArpLookups.count() > 0) { + // Still busy... + return; + } + + QList hosts; + foreach (Host *host, m_scanResults) { + if (!host->macAddress().isEmpty()) { + hosts.append(*host); + } + } + qDeleteAll(m_scanResults); + m_scanResults.clear(); + + qCDebug(dcDiscovery()) << "Found" << hosts.count() << "network devices"; + m_timeoutTimer.stop(); + emit finished(hosts); +} + diff --git a/discovery/discovery.h b/discovery/discovery.h new file mode 100644 index 0000000..02284c3 --- /dev/null +++ b/discovery/discovery.h @@ -0,0 +1,75 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef DISCOVERY_H +#define DISCOVERY_H + +#include +#include +#include +#include + +#include "host.h" + +class Discovery : public QObject +{ + Q_OBJECT +public: + explicit Discovery(QObject *parent = nullptr); + + void discoverHosts(int timeout); + void abort(); + bool isRunning() const; + +signals: + void finished(const QList &hosts); + +private: + QStringList getDefaultTargets(); + + void finishDiscovery(); + +private slots: + void discoveryFinished(int exitCode, QProcess::ExitStatus exitStatus); + void hostLookupDone(const QHostInfo &info); + void arpLookupDone(int exitCode, QProcess::ExitStatus exitStatus); + void onTimeout(); + +private: + QList m_discoveryProcesses; + QTimer m_timeoutTimer; + + QHash m_pendingArpLookups; + QHash m_pendingNameLookups; + QList m_scanResults; +}; + +#endif // DISCOVERY_H + diff --git a/discovery/host.cpp b/discovery/host.cpp new file mode 100644 index 0000000..f1e80a5 --- /dev/null +++ b/discovery/host.cpp @@ -0,0 +1,94 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "host.h" + +Host::Host() +{ + qRegisterMetaType(); + qRegisterMetaType >(); +} + +QString Host::macAddress() const +{ + return m_macAddress; +} + +void Host::setMacAddress(const QString &macAddress) +{ + m_macAddress = macAddress; +} + +QString Host::hostName() const +{ + return m_hostName; +} + +void Host::setHostName(const QString &hostName) +{ + m_hostName = hostName; +} + +QString Host::address() const +{ + return m_address; +} + +void Host::setAddress(const QString &address) +{ + m_address = address; +} + +void Host::seen() +{ + m_lastSeenTime = QDateTime::currentDateTime(); +} + +QDateTime Host::lastSeenTime() const +{ + return m_lastSeenTime; +} + +bool Host::reachable() const +{ + return m_reachable; +} + +void Host::setReachable(bool reachable) +{ + m_reachable = reachable; +} + +QDebug operator<<(QDebug dbg, const Host &host) +{ + dbg.nospace() << "Host(" << host.macAddress() << "," << host.hostName() << ", " << host.address() << ", " << (host.reachable() ? "up" : "down") << ")"; + return dbg.space(); +} + diff --git a/discovery/host.h b/discovery/host.h new file mode 100644 index 0000000..8fe28a2 --- /dev/null +++ b/discovery/host.h @@ -0,0 +1,70 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef HOST_H +#define HOST_H + +#include +#include +#include + +class Host +{ +public: + Host(); + + QString macAddress() const; + void setMacAddress(const QString &macAddress); + + QString hostName() const; + void setHostName(const QString &hostName); + + QString address() const; + void setAddress(const QString &address); + + void seen(); + QDateTime lastSeenTime() const; + + bool reachable() const; + void setReachable(bool reachable); + +private: + QString m_macAddress; + QString m_hostName; + QString m_address; + QDateTime m_lastSeenTime; + bool m_reachable; +}; +Q_DECLARE_METATYPE(Host) + +QDebug operator<<(QDebug dbg, const Host &host); + +#endif // HOST_H + diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro index c1261e8..694d2c8 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -7,6 +7,7 @@ PLUGIN_DIRS = \ sunspec \ unipi \ wallbe \ + webasto \ message(============================================) message("Qt version:" $$[QT_VERSION]) diff --git a/webasto/README.md b/webasto/README.md new file mode 100644 index 0000000..2d5677e --- /dev/null +++ b/webasto/README.md @@ -0,0 +1,17 @@ +# Webasto + +## Supported Things + +* AC Wallbox Live + +## Requirements + +* The packages 'nymea-plugin-webasto' must be installed. +* The modbus server must be enabled +* The setting 'Modbus Slave Register Address Set' must be set to 'TQ-DM100' +* The setting 'Modbus TCP Server Port Number' must be set to 502 + +## More + +https://dealers.webasto.com/Sections/Public/Documents.aspx?SectionId=6&CategoryId=9&ProductTypeId=66&ProductId=630&ShowResult=true + diff --git a/webasto/integrationpluginwebasto.cpp b/webasto/integrationpluginwebasto.cpp new file mode 100644 index 0000000..f7e05af --- /dev/null +++ b/webasto/integrationpluginwebasto.cpp @@ -0,0 +1,463 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "integrationpluginwebasto.h" +#include "plugininfo.h" + +#include "types/param.h" + +#include +#include +#include +#include + + +IntegrationPluginWebasto::IntegrationPluginWebasto() +{ +} + +void IntegrationPluginWebasto::init() +{ + m_discovery = new Discovery(this); + connect(m_discovery, &Discovery::finished, this, [this](const QList &hosts) { + + foreach (const Host &host, hosts) { + if (!host.hostName().contains("webasto", Qt::CaseSensitivity::CaseInsensitive)) + continue; + + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(liveWallboxThingMacAddressParamTypeId).toString().isEmpty()) { + //This device got probably manually setup, to enable auto rediscovery the MAC address needs to setup + if (existingThing->paramValue(liveWallboxThingIpAddressParamTypeId).toString() == host.address()) { + qCDebug(dcWebasto()) << "Wallbox MAC Address has been discovered" << existingThing->name() << host.macAddress(); + existingThing->setParamValue(liveWallboxThingMacAddressParamTypeId, host.macAddress()); + + } + } else if (existingThing->paramValue(liveWallboxThingMacAddressParamTypeId).toString() == host.macAddress()) { + if (existingThing->paramValue(liveWallboxThingIpAddressParamTypeId).toString() != host.address()) { + qCDebug(dcWebasto()) << "Wallbox IP Address has changed, from" << existingThing->paramValue(liveWallboxThingIpAddressParamTypeId).toString() << "to" << host.address(); + existingThing->setParamValue(liveWallboxThingIpAddressParamTypeId, host.address()); + + } else { + qCDebug(dcWebasto()) << "Wallbox" << existingThing->name() << "IP address has not changed" << host.address(); + } + break; + } + } + } + }); +} + +void IntegrationPluginWebasto::discoverThings(ThingDiscoveryInfo *info) +{ + qCDebug(dcWebasto()) << "Discover things"; + if (info->thingClassId() == liveWallboxThingClassId) { + m_discovery->discoverHosts(25); + connect(m_discovery, &Discovery::finished, info, [this, info] (const QList &hosts) { + + foreach (const Host &host, hosts) { + if (!host.hostName().contains("webasto", Qt::CaseSensitivity::CaseInsensitive)) + continue; + + qCDebug(dcWebasto()) << " - " << host.hostName() << host.address() << host.macAddress(); + ThingDescriptor descriptor(liveWallboxThingClassId, "Wallbox", host.address() + " (" + host.macAddress() + ")"); + + // Rediscovery + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(liveWallboxThingMacAddressParamTypeId).toString() == host.macAddress()) { + qCDebug(dcWebasto()) << " - Device is already added"; + descriptor.setThingId(existingThing->id()); + break; + } + } + ParamList params; + params << Param(liveWallboxThingMacAddressParamTypeId, host.macAddress()); + params << Param(liveWallboxThingIpAddressParamTypeId, host.address()); + descriptor.setParams(params); + info->addThingDescriptor(descriptor); + } + info->finish(Thing::ThingErrorNoError); + }); + } else { + Q_ASSERT_X(false, "discoverThings", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginWebasto::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcWebasto()) << "Setup thing" << thing->name(); + + if (thing->thingClassId() == liveWallboxThingClassId) { + + if (m_webastoConnections.contains(thing)) { + // Clean up after reconfiguration + m_webastoConnections.take(thing)->deleteLater(); + } + QHostAddress address = QHostAddress(thing->paramValue(liveWallboxThingIpAddressParamTypeId).toString()); + Webasto *webasto = new Webasto(address, 502, thing); + m_webastoConnections.insert(thing, webasto); + connect(webasto, &Webasto::destroyed, this, [thing, this] {m_webastoConnections.remove(thing);}); + connect(webasto, &Webasto::connectionStateChanged, this, &IntegrationPluginWebasto::onConnectionChanged); + connect(webasto, &Webasto::receivedRegister, this, &IntegrationPluginWebasto::onReceivedRegister); + connect(webasto, &Webasto::writeRequestError, this, &IntegrationPluginWebasto::onWriteRequestError); + connect(webasto, &Webasto::writeRequestExecuted, this, &IntegrationPluginWebasto::onWriteRequestExecuted); + if (!webasto->connectDevice()) { + qCWarning(dcWebasto()) << "Could not connect to device"; + info->finish(Thing::ThingErrorSetupFailed); + } + connect(webasto, &Webasto::connectionStateChanged, info, [info] (bool connected) { + if (connected) + info->finish(Thing::ThingErrorNoError); + }); + } else { + Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginWebasto::postSetupThing(Thing *thing) +{ + qCDebug(dcWebasto()) << "Post setup thing" << thing->name(); + if (!m_pluginTimer) { + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(1); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this] { + Q_FOREACH(Webasto *connection, m_webastoConnections) { + if (connection->connected()) + update(connection); + } + }); + } + + if (thing->thingClassId() == liveWallboxThingClassId) { + Webasto *connection = m_webastoConnections.value(thing); + if (!connection) { + qCWarning(dcWebasto()) << "Can't find connection to thing"; + } + update(connection); + + } else { + Q_ASSERT_X(false, "postSetupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginWebasto::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + Action action = info->action(); + + if (thing->thingClassId() == liveWallboxThingClassId) { + + Webasto *connection = m_webastoConnections.value(thing); + if (!connection) { + qCWarning(dcWebasto()) << "Can't find connection to thing"; + return info->finish(Thing::ThingErrorHardwareNotAvailable); + } + + if (action.actionTypeId() == liveWallboxPowerActionTypeId) { + bool enabled = action.paramValue(liveWallboxPowerActionPowerParamTypeId).toBool(); + thing->setStateValue(liveWallboxPowerActionTypeId, enabled); + int ampere = 0; + if (enabled) { + ampere = thing->stateValue(liveWallboxMaxChargingCurrentStateTypeId).toInt(); + } + QUuid requestId = connection->setChargeCurrent(ampere); + if (requestId.isNull()) { + info->finish(Thing::ThingErrorHardwareFailure); + } else { + m_asyncActions.insert(requestId, info); + } + } else if (action.actionTypeId() == liveWallboxMaxChargingCurrentActionTypeId) { + int ampere = action.paramValue(liveWallboxMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toInt(); + thing->setStateValue(liveWallboxMaxChargingCurrentStateTypeId, ampere); + QUuid requestId = connection->setChargeCurrent(ampere); + if (requestId.isNull()) { + info->finish(Thing::ThingErrorHardwareFailure); + } else { + m_asyncActions.insert(requestId, info); + } + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8()); + } + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginWebasto::thingRemoved(Thing *thing) +{ + qCDebug(dcWebasto()) << "Delete thing" << thing->name(); + if (thing->thingClassId() == liveWallboxThingClassId) { + } + + if (myThings().isEmpty()) { + //Stop timer + } +} + +void IntegrationPluginWebasto::update(Webasto *webasto) +{ + webasto->getRegister(Webasto::TqChargePointState); + webasto->getRegister(Webasto::TqCableState); + webasto->getRegister(Webasto::TqEVSEError); + + webasto->getRegister(Webasto::TqCurrentL1); + webasto->getRegister(Webasto::TqCurrentL2); + webasto->getRegister(Webasto::TqCurrentL3); + + webasto->getRegister(Webasto::TqActivePower, 2); + webasto->getRegister(Webasto::TqEnergyMeter, 2); + + webasto->getRegister(Webasto::TqMaxCurrent); + + webasto->getRegister(Webasto::TqChargedEnergy); + webasto->getRegister(Webasto::TqChargingTime, 2); + + webasto->getRegister(Webasto::TqUserId, 10); +} + +void IntegrationPluginWebasto::onConnectionChanged(bool connected) +{ + Webasto *connection = static_cast(sender()); + Thing *thing = m_webastoConnections.key(connection); + if (!thing) { + qCWarning(dcWebasto()) << "On connection changed, thing not found for connection"; + return; + } + thing->setStateValue(liveWallboxConnectedStateTypeId, connected); +} + +void IntegrationPluginWebasto::onWriteRequestExecuted(const QUuid &requestId, bool success) +{ + if (m_asyncActions.contains(requestId)) { + ThingActionInfo *info = m_asyncActions.take(requestId); + if (success) { + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + } +} + +void IntegrationPluginWebasto::onWriteRequestError(const QUuid &requestId, const QString &error) +{ + Q_UNUSED(requestId); + qCWarning(dcWebasto()) << "Write request error" << error; +} + +void IntegrationPluginWebasto::onReceivedRegister(Webasto::TqModbusRegister modbusRegister, const QVector &data) +{ + Webasto *connection = static_cast(sender()); + Thing *thing = m_webastoConnections.key(connection); + if (!thing) { + qCWarning(dcWebasto()) << "On basic information received, thing not found for connection"; + return; + } + if (thing->thingClassId() == liveWallboxThingClassId) { + switch (modbusRegister) { + case Webasto::TqChargePointState: + qCDebug(dcWebasto()) << " - Charge point state:" << Webasto::ChargePointState(data[0]); + switch (Webasto::ChargePointState(data[0])) { + case Webasto::ChargePointStateNoVehicleAttached: + thing->setStateValue(liveWallboxChargePointStateStateTypeId, "No vehicle attached"); + break; + case Webasto::ChargePointStateVehicleAttachedNoPermission: + thing->setStateValue(liveWallboxChargePointStateStateTypeId, "Vehicle attached, no permission"); + break; + case Webasto::ChargePointStateChargingAuthorized: + thing->setStateValue(liveWallboxChargePointStateStateTypeId, "Charging authorized"); + break; + case Webasto::ChargePointStateCharging: + thing->setStateValue(liveWallboxChargePointStateStateTypeId, "Charging"); + break; + case Webasto::ChargePointStateChargingPaused: + thing->setStateValue(liveWallboxChargePointStateStateTypeId, "Charging paused"); + break; + case Webasto::ChargePointStateChargeSuccessfulCarStillAttached: + thing->setStateValue(liveWallboxChargePointStateStateTypeId, "Charge successful (car still attached)"); + break; + case Webasto::ChargePointStateChargingStoppedByUserCarStillAttached: + thing->setStateValue(liveWallboxChargePointStateStateTypeId, "Charging stopped by user (car still attached)"); + break; + case Webasto::ChargePointStateChargingErrorCarStillAttached: + thing->setStateValue(liveWallboxChargePointStateStateTypeId, "Charging error (car still attached)"); + break; + case Webasto::ChargePointStateChargingStationReservedNorCarAttached: + thing->setStateValue(liveWallboxChargePointStateStateTypeId, "Charging station reserved (No car attached)"); + break; + case Webasto::ChargePointStateUserNotAuthorizedCarAttached: + thing->setStateValue(liveWallboxChargePointStateStateTypeId, "User not authorized (car attached)"); + break; + } + break; + case Webasto::TqChargeState: + qCDebug(dcWebasto()) << " - Charge state:" << data[0]; + break; + case Webasto::TqEVSEState: + qCDebug(dcWebasto()) << " - EVSE state:" << data[0]; + break; + case Webasto::TqCableState: + qCDebug(dcWebasto()) << " - Cable state:" << Webasto::CableState(data[0]); + switch (Webasto::CableState(data[0])) { + case Webasto::CableStateNoCableAttached: + thing->setStateValue(liveWallboxCableStateStateTypeId, "No cable attached"); + break; + case Webasto::CableStateCableAttachedNoCarAttached: + thing->setStateValue(liveWallboxCableStateStateTypeId, "Cable attached but no car attached)"); + break; + case Webasto::CableStateCableAttachedCarAttached: + thing->setStateValue(liveWallboxCableStateStateTypeId, "Cable attached and car attached"); + break; + case Webasto::CableStateCableAttachedCarAttachedLockActive: + thing->setStateValue(liveWallboxCableStateStateTypeId, "Cable attached, car attached and lock active"); + break; + } + break; + case Webasto::TqEVSEError: + qCDebug(dcWebasto()) << " - EVSE error:" << data[0]; + thing->setStateValue(liveWallboxErrorStateTypeId, data[0]); + break; + case Webasto::TqCurrentL1: + qCDebug(dcWebasto()) << " - Current L1:" << data[0]; + thing->setStateValue(liveWallboxCurrentPhase1StateTypeId, data[0]); + break; + case Webasto::TqCurrentL2: + qCDebug(dcWebasto()) << " - Current L2:" << data[0]; + thing->setStateValue(liveWallboxCurrentPhase2StateTypeId, data[0]); + break; + case Webasto::TqCurrentL3: + qCDebug(dcWebasto()) << " - Current L3:" << data[0]; + thing->setStateValue(liveWallboxCurrentPhase3StateTypeId, data[0]); + break; + case Webasto::TqActivePower: { + if (data.count() < 2) + return; + int power = (static_cast(data[0])<<16 | data[1]); + qCDebug(dcWebasto()) << " - Active power:" << power; + thing->setStateValue(liveWallboxPowerConsumptionStateTypeId, power); + } break; + case Webasto::TqEnergyMeter: { + if (data.count() < 2) + return; + int energy = (static_cast(data[0])<<16 | data[1]); + qCDebug(dcWebasto()) << " - Energy meter:" << energy << "Wh"; + thing->setStateValue(liveWallboxTotalEnergyConsumedStateTypeId, energy); + } break; + case Webasto::TqMaxCurrent: + qCDebug(dcWebasto()) << " - Max. Current" << data[0]; + thing->setStateValue(liveWallboxMaxPossibleChargingCurrentStateTypeId, data[0]); + break; + case Webasto::TqMinimumCurrentLimit: + qCDebug(dcWebasto()) << " - Min. Current" << data[0]; + break; + case Webasto::TqMaxCurrentFromEVSE: + qCDebug(dcWebasto()) << " - Max. Current EVSE" << data[0]; + break; + case Webasto::TqMaxCurrentFromCable: + qCDebug(dcWebasto()) << " - Max. Current Cable" << data[0]; + break; + case Webasto::TqMaxCurrentFromEV: + qCDebug(dcWebasto()) << " - Max. Current EV" << data[0]; + break; + case Webasto::TqUserPriority: + qCDebug(dcWebasto()) << " - User priority" << data[0]; + break; + case Webasto::TqEVBatteryState: + qCDebug(dcWebasto()) << " - Battery state" << data[0]; + break; + case Webasto::TqEVBatteryCapacity: { + if (data.count() < 2) + return; + uint batteryCapacity = (static_cast(data[0])<<16 | data[1]); + qCDebug(dcWebasto()) << " - Battery capacity" << batteryCapacity << "Wh"; + } break; + case Webasto::TqScheduleType: + qCDebug(dcWebasto()) << " - Schedule type" << data[0]; + break; + case Webasto::TqRequiredEnergy: { + if (data.count() < 2) + return; + uint requiredEnergy = (static_cast(data[0])<<16 | data[1]); + qCDebug(dcWebasto()) << " - Required energy" << requiredEnergy; + } break; + case Webasto::TqRequiredBatteryState: + qCDebug(dcWebasto()) << " - Required battery state" << data[0]; + break; + case Webasto::TqScheduledTime: + qCDebug(dcWebasto()) << " - Scheduled time" << data[0]; + break; + case Webasto::TqScheduledDate: + qCDebug(dcWebasto()) << " - Scheduled date" << data[0]; + break; + case Webasto::TqChargedEnergy: + qCDebug(dcWebasto()) << " - Charged energy" << data[0]; + thing->setStateValue(liveWallboxSessionEnergyStateTypeId, data[0]/1000.00); // Charged energy in kWh + break; + case Webasto::TqStartTime: + qCDebug(dcWebasto()) << " - Start time" << (static_cast(data[0])<<16 | data[1]); + break; + case Webasto::TqChargingTime: { + if (data.count() < 2) + return; + uint seconds = (static_cast(data[0])<<16 | data[1]); + qCDebug(dcWebasto()) << " - Charging time" << seconds << "s"; + thing->setStateValue(liveWallboxSessionTimeStateTypeId, seconds/60.00); // Charging time in minutes + } break; + case Webasto::TqEndTime: { + if (data.count() < 2) + return; + uint hour = ((static_cast(data[0])<<16 | data[1])&0xff0000)>>16; + uint minutes = ((static_cast(data[0])<<16 | data[1])&0x00ff00)>>8; + uint seconds= (static_cast(data[0])<<16 | data[1])&0x0000ff; + qCDebug(dcWebasto()) << " - End time" << hour << "h" << minutes << "m" << seconds << "s"; + } break; + case Webasto::TqUserId: { + if (data.count() < 10) + return; + QByteArray userID; + Q_FOREACH(quint16 i, data) { + userID.append(i>>16); + userID.append(i&0xff); + } + qCDebug(dcWebasto()) << " - User ID:" << userID; + } break; + case Webasto::TqSmartVehicleDetected: + qCDebug(dcWebasto()) << " - Smart vehicle detected:" << data[0]; + break; + case Webasto::TqSafeCurrent: + qCDebug(dcWebasto()) << " - Safe current:" << data[0]; + break; + case Webasto::TqComTimeout: + qCDebug(dcWebasto()) << " - Com timeout:" << data[0]; + break; + default: + break; + } + } +} diff --git a/webasto/integrationpluginwebasto.h b/webasto/integrationpluginwebasto.h new file mode 100644 index 0000000..b1d765b --- /dev/null +++ b/webasto/integrationpluginwebasto.h @@ -0,0 +1,77 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINWEBASTO_H +#define INTEGRATIONPLUGINWEBASTO_H + +#include "integrations/integrationplugin.h" +#include "plugintimer.h" +#include "webasto.h" +#include "../discovery/discovery.h" +#include "../discovery/host.h" +#include "../modbus/modbustcpmaster.h" + +#include +#include +#include + +class IntegrationPluginWebasto : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginwebasto.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginWebasto(); + void init() override; + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + void thingRemoved(Thing *thing) override; + +private: + Discovery *m_discovery = nullptr; + PluginTimer *m_pluginTimer = nullptr; + QHash m_webastoConnections; + QHash m_asyncActions; + + void update(Webasto *webasto); + +private slots: + void onConnectionChanged(bool connected); + void onWriteRequestExecuted(const QUuid &requestId, bool success); + void onWriteRequestError(const QUuid &requestId, const QString &error); + + void onReceivedRegister(Webasto::TqModbusRegister registerAddress, const QVector &data); +}; + +#endif // INTEGRATIONPLUGINWEBASTO_H diff --git a/webasto/integrationpluginwebasto.json b/webasto/integrationpluginwebasto.json new file mode 100644 index 0000000..824bb22 --- /dev/null +++ b/webasto/integrationpluginwebasto.json @@ -0,0 +1,195 @@ +{ + "id": "9fa369ab-c225-4447-9a23-f4911d9b056c", + "name": "Webasto", + "displayName": "webasto", + "vendors": [ + { + "id": "274f4453-6acf-4204-be21-379abbe3b5a7", + "name": "webasto", + "displayName": "Webasto", + "thingClasses": [ + { + "id": "48472124-3199-4827-990a-b72069bd5658", + "displayName": "Live Wallbox", + "name": "liveWallbox", + "createMethods": ["discovery"], + "interfaces": ["evcharger", "connectable"], + "paramTypes": [ + { + "id": "51fa3ea8-e819-46ca-b975-1bee6285441c", + "name": "ipAddress", + "displayName": "IP address", + "type": "QString", + "defaultValue": "0.0.0.0" + }, + { + "id": "4aa97965-fc1c-488a-92a6-848c214564bc", + "name": "macAddress", + "displayName": "MAC address", + "type": "QString", + "defaultValue": "", + "readOnly": true + } + ], + "stateTypes":[ + { + "id": "7e6ed2b4-aa8a-4bf6-b20b-84ecc6cc1508", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "name": "connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "b076353b-e911-444f-80ad-3f78c4075d1a", + "name": "chargePointState", + "displayName": "Charge point state", + "displayNameEvent": "Charge point state changed", + "type": "QString", + "possibleValues": [ + "No vehicle attached", + "Vehicle attached, no permission", + "Charging authorized", + "Charging", + "Charging paused", + "Charge successful (car still attached)", + "Charging stopped by user (car still attached)", + "Charging error (car still attached)", + "Charging station reserved (No car attached)", + "User not authorized (car attached)" + ], + "defaultValue": "No vehicle attached" + }, + { + "id": "a1a452f9-de93-4c31-b71b-c74264f85a3e", + "name": "cableState", + "displayName": "Cable state", + "displayNameEvent": "Cable state changed", + "type": "QString", + "possibleValues": [ + "No cable attached", + "Cable attached but no car attached)", + "Cable attached and car attached", + "Cable attached, car attached and lock active" + ], + "defaultValue": "No cable attached" + }, + { + "id": "3c054603-d933-4e30-a2cc-2177beaaffdb", + "name": "power", + "displayName": "Charging", + "type": "bool", + "defaultValue": false, + "displayNameAction": "Start charging", + "displayNameEvent": "Charging status changed", + "writable": true + }, + { + "id": "96ed77ce-c5cf-4981-8a72-b619f5702724", + "name": "maxChargingCurrent", + "displayName": "Charging current", + "displayNameAction": "Set charging current", + "displayNameEvent": "Charging current changed", + "type": "double", + "unit": "Ampere", + "minValue": 6.00, + "maxValue": 80.00, + "defaultValue": 6.00, + "writable": true + }, + { + "id": "2027fbb6-c9d2-4a75-bdd0-a3ad3785cdc6", + "name": "currentPhase1", + "displayName": "Current phase 1", + "displayNameEvent": "Current phase 1 changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00 + }, + { + "id": "1793f645-d7db-4e99-af92-3587aa3069f3", + "name": "currentPhase2", + "displayName": "Current phase 2", + "displayNameEvent": "Current phase 2 changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00 + }, + { + "id": "feb8c5da-91a7-45f9-acc3-c1b61478c3d2", + "name": "currentPhase3", + "displayName": "Current phase 3", + "displayNameEvent": "Current phase 3 changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0.00 + }, + { + "id": "b20a46ee-0f22-4096-a348-34e68e99e0be", + "name": "powerConsumption", + "displayName": "Power consumption", + "displayNameEvent": "Power consumtion changed", + "type": "double", + "unit": "KiloWatt", + "defaultValue": 0.00 + }, + { + "id": "80568c51-054c-4351-b9d2-e875fee4cc1f", + "name": "totalEnergyConsumed", + "displayName": "Total energy consumed", + "displayNameEvent": "Total energy consumption changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "87c70567-794e-4af2-916c-b34cf864afcf", + "name": "sessionTime", + "displayName": "Session time", + "displayNameEvent": "Session time changed", + "type": "int", + "unit": "Minutes", + "defaultValue": 0 + }, + { + "id": "b9b46920-55c1-4bfa-9200-acdc9c0a2471", + "name": "sessionEnergy", + "displayName": "Session energy", + "displayNameEvent": "Session energy changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "56d31fd1-5cfb-42dd-8181-e6b0d0ca9c8a", + "name": "error", + "displayName": "Error ", + "displayNameEvent": "Error changed", + "type": "int", + "defaultValue": 0 + }, + { + "id": "0e60b15d-2b0c-4672-960e-7c6ea67bf7ea", + "name": "maxPossibleChargingCurrent", + "displayName": "Maximum possible charging current", + "displayNameEvent": "Maximum possible charging current changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 6.00 + }, + { + "id": "48b62082-f286-433e-9cf8-2dcf6c0ea248", + "name": "userId", + "displayName": "User ID", + "displayNameEvent": "User ID changed", + "type": "QString", + "defaultValue": "" + } + ] + } + ] + } + ] +} + diff --git a/webasto/meta.json b/webasto/meta.json new file mode 100644 index 0000000..c226af8 --- /dev/null +++ b/webasto/meta.json @@ -0,0 +1,13 @@ +{ + "title": "webasto", + "tagline": "Integrates Webasto Live DC wallbox into nymea.", + "icon": "webasto.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "tool" + ] +} diff --git a/webasto/translations/9fa369ab-c225-4447-9a23-f4911d9b056c-en_US.ts b/webasto/translations/9fa369ab-c225-4447-9a23-f4911d9b056c-en_US.ts new file mode 100644 index 0000000..d8b5db2 --- /dev/null +++ b/webasto/translations/9fa369ab-c225-4447-9a23-f4911d9b056c-en_US.ts @@ -0,0 +1,280 @@ + + + + + Webasto + + + + Cable state + The name of the ParamType (ThingClass: liveWallbox, EventType: cableState, ID: {a1a452f9-de93-4c31-b71b-c74264f85a3e}) +---------- +The name of the StateType ({a1a452f9-de93-4c31-b71b-c74264f85a3e}) of ThingClass liveWallbox + + + + + Cable state changed + The name of the EventType ({a1a452f9-de93-4c31-b71b-c74264f85a3e}) of ThingClass liveWallbox + + + + + + Charge point state + The name of the ParamType (ThingClass: liveWallbox, EventType: chargePointState, ID: {b076353b-e911-444f-80ad-3f78c4075d1a}) +---------- +The name of the StateType ({b076353b-e911-444f-80ad-3f78c4075d1a}) of ThingClass liveWallbox + + + + + Charge point state changed + The name of the EventType ({b076353b-e911-444f-80ad-3f78c4075d1a}) of ThingClass liveWallbox + + + + + + + Charging + The name of the ParamType (ThingClass: liveWallbox, ActionType: power, ID: {3c054603-d933-4e30-a2cc-2177beaaffdb}) +---------- +The name of the ParamType (ThingClass: liveWallbox, EventType: power, ID: {3c054603-d933-4e30-a2cc-2177beaaffdb}) +---------- +The name of the StateType ({3c054603-d933-4e30-a2cc-2177beaaffdb}) of ThingClass liveWallbox + + + + + + + Charging current + The name of the ParamType (ThingClass: liveWallbox, ActionType: chargeCurrent, ID: {96ed77ce-c5cf-4981-8a72-b619f5702724}) +---------- +The name of the ParamType (ThingClass: liveWallbox, EventType: chargeCurrent, ID: {96ed77ce-c5cf-4981-8a72-b619f5702724}) +---------- +The name of the StateType ({96ed77ce-c5cf-4981-8a72-b619f5702724}) of ThingClass liveWallbox + + + + + Charging current changed + The name of the EventType ({96ed77ce-c5cf-4981-8a72-b619f5702724}) of ThingClass liveWallbox + + + + + Charging status changed + The name of the EventType ({3c054603-d933-4e30-a2cc-2177beaaffdb}) of ThingClass liveWallbox + + + + + + Connected + The name of the ParamType (ThingClass: liveWallbox, EventType: connected, ID: {7e6ed2b4-aa8a-4bf6-b20b-84ecc6cc1508}) +---------- +The name of the StateType ({7e6ed2b4-aa8a-4bf6-b20b-84ecc6cc1508}) of ThingClass liveWallbox + + + + + Connected changed + The name of the EventType ({7e6ed2b4-aa8a-4bf6-b20b-84ecc6cc1508}) of ThingClass liveWallbox + + + + + + Current phase 1 + The name of the ParamType (ThingClass: liveWallbox, EventType: currentPhase1, ID: {2027fbb6-c9d2-4a75-bdd0-a3ad3785cdc6}) +---------- +The name of the StateType ({2027fbb6-c9d2-4a75-bdd0-a3ad3785cdc6}) of ThingClass liveWallbox + + + + + Current phase 1 changed + The name of the EventType ({2027fbb6-c9d2-4a75-bdd0-a3ad3785cdc6}) of ThingClass liveWallbox + + + + + + Current phase 2 + The name of the ParamType (ThingClass: liveWallbox, EventType: currentPhase2, ID: {1793f645-d7db-4e99-af92-3587aa3069f3}) +---------- +The name of the StateType ({1793f645-d7db-4e99-af92-3587aa3069f3}) of ThingClass liveWallbox + + + + + Current phase 2 changed + The name of the EventType ({1793f645-d7db-4e99-af92-3587aa3069f3}) of ThingClass liveWallbox + + + + + + Current phase 3 + The name of the ParamType (ThingClass: liveWallbox, EventType: currentPhase3, ID: {feb8c5da-91a7-45f9-acc3-c1b61478c3d2}) +---------- +The name of the StateType ({feb8c5da-91a7-45f9-acc3-c1b61478c3d2}) of ThingClass liveWallbox + + + + + Current phase 3 changed + The name of the EventType ({feb8c5da-91a7-45f9-acc3-c1b61478c3d2}) of ThingClass liveWallbox + + + + + + Error + The name of the ParamType (ThingClass: liveWallbox, EventType: error, ID: {56d31fd1-5cfb-42dd-8181-e6b0d0ca9c8a}) +---------- +The name of the StateType ({56d31fd1-5cfb-42dd-8181-e6b0d0ca9c8a}) of ThingClass liveWallbox + + + + + Error changed + The name of the EventType ({56d31fd1-5cfb-42dd-8181-e6b0d0ca9c8a}) of ThingClass liveWallbox + + + + + IP address + The name of the ParamType (ThingClass: liveWallbox, Type: thing, ID: {51fa3ea8-e819-46ca-b975-1bee6285441c}) + + + + + Live Wallbox + The name of the ThingClass ({48472124-3199-4827-990a-b72069bd5658}) + + + + + MAC address + The name of the ParamType (ThingClass: liveWallbox, Type: thing, ID: {4aa97965-fc1c-488a-92a6-848c214564bc}) + + + + + + Maximum possible charging current + The name of the ParamType (ThingClass: liveWallbox, EventType: maxPossibleChargingCurrent, ID: {0e60b15d-2b0c-4672-960e-7c6ea67bf7ea}) +---------- +The name of the StateType ({0e60b15d-2b0c-4672-960e-7c6ea67bf7ea}) of ThingClass liveWallbox + + + + + Maximum possible charging current changed + The name of the EventType ({0e60b15d-2b0c-4672-960e-7c6ea67bf7ea}) of ThingClass liveWallbox + + + + + + Power consumption + The name of the ParamType (ThingClass: liveWallbox, EventType: powerConsumption, ID: {b20a46ee-0f22-4096-a348-34e68e99e0be}) +---------- +The name of the StateType ({b20a46ee-0f22-4096-a348-34e68e99e0be}) of ThingClass liveWallbox + + + + + Power consumtion changed + The name of the EventType ({b20a46ee-0f22-4096-a348-34e68e99e0be}) of ThingClass liveWallbox + + + + + + Session energy + The name of the ParamType (ThingClass: liveWallbox, EventType: sessionEnergy, ID: {b9b46920-55c1-4bfa-9200-acdc9c0a2471}) +---------- +The name of the StateType ({b9b46920-55c1-4bfa-9200-acdc9c0a2471}) of ThingClass liveWallbox + + + + + Session energy changed + The name of the EventType ({b9b46920-55c1-4bfa-9200-acdc9c0a2471}) of ThingClass liveWallbox + + + + + + Session time + The name of the ParamType (ThingClass: liveWallbox, EventType: sessionTime, ID: {87c70567-794e-4af2-916c-b34cf864afcf}) +---------- +The name of the StateType ({87c70567-794e-4af2-916c-b34cf864afcf}) of ThingClass liveWallbox + + + + + Session time changed + The name of the EventType ({87c70567-794e-4af2-916c-b34cf864afcf}) of ThingClass liveWallbox + + + + + Set charging current + The name of the ActionType ({96ed77ce-c5cf-4981-8a72-b619f5702724}) of ThingClass liveWallbox + + + + + Start charging + The name of the ActionType ({3c054603-d933-4e30-a2cc-2177beaaffdb}) of ThingClass liveWallbox + + + + + + Total energy consumed + The name of the ParamType (ThingClass: liveWallbox, EventType: totalEnergyConsumed, ID: {80568c51-054c-4351-b9d2-e875fee4cc1f}) +---------- +The name of the StateType ({80568c51-054c-4351-b9d2-e875fee4cc1f}) of ThingClass liveWallbox + + + + + Total energy consumption changed + The name of the EventType ({80568c51-054c-4351-b9d2-e875fee4cc1f}) of ThingClass liveWallbox + + + + + + User ID + The name of the ParamType (ThingClass: liveWallbox, EventType: userId, ID: {48b62082-f286-433e-9cf8-2dcf6c0ea248}) +---------- +The name of the StateType ({48b62082-f286-433e-9cf8-2dcf6c0ea248}) of ThingClass liveWallbox + + + + + User ID changed + The name of the EventType ({48b62082-f286-433e-9cf8-2dcf6c0ea248}) of ThingClass liveWallbox + + + + + Webasto + The name of the vendor ({274f4453-6acf-4204-be21-379abbe3b5a7}) + + + + + webasto + The name of the plugin Webasto ({9fa369ab-c225-4447-9a23-f4911d9b056c}) + + + + diff --git a/webasto/webasto.cpp b/webasto/webasto.cpp new file mode 100644 index 0000000..9c980ad --- /dev/null +++ b/webasto/webasto.cpp @@ -0,0 +1,141 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "webasto.h" +#include "extern-plugininfo.h" + +Webasto::Webasto(const QHostAddress &address, uint port, QObject *parent) : + QObject(parent) +{ + qCDebug(dcWebasto()) << "Webasto: Webasto connection created" << address.toString() << port; + m_modbusConnection = new ModbusTCPMaster(address, port, this); + m_modbusConnection->setNumberOfRetries(3); + m_modbusConnection->setTimeout(1000); + + connect(m_modbusConnection, &ModbusTCPMaster::receivedHoldingRegister, this, &Webasto::onReceivedHoldingRegister); + connect(m_modbusConnection, &ModbusTCPMaster::writeRequestExecuted, this, &Webasto::writeRequestExecuted); + connect(m_modbusConnection, &ModbusTCPMaster::writeRequestError, this, &Webasto::writeRequestError); + + m_lifeBitTimer = new QTimer(this); + m_lifeBitTimer->start(10000); + connect(m_lifeBitTimer, &QTimer::timeout, this, [this] { + setLiveBit(); + }); +} + +void Webasto::setAddress(const QHostAddress &address) +{ + qCDebug(dcWebasto()) << "Webasto: set address" << address; + m_modbusConnection->setHostAddress(address); +} + +QHostAddress Webasto::address() const +{ + return m_modbusConnection->hostAddress(); +} + +bool Webasto::connected() +{ + return m_modbusConnection->connected(); +} + +bool Webasto::connectDevice() +{ + return m_modbusConnection->connectDevice(); +} + +void Webasto::setLivebitInterval(uint seconds) +{ + qCDebug(dcWebasto()) << "Webasto: Live bit interval set to" << seconds << "[s]"; + m_lifeBitTimer->setInterval(seconds*1000); +} + +void Webasto::getRegister(Webasto::TqModbusRegister modbusRegister, uint length) +{ + qCDebug(dcWebasto()) << "Webasto: Get register" << modbusRegister << length; + if (length < 1 && length > 10) { + qCWarning(dcWebasto()) << "Invalide register length, allowed values [1,10]"; + return; + } + + m_modbusConnection->readHoldingRegister(m_unitId, modbusRegister, length); +} + +QUuid Webasto::setSafeCurrent(quint16 ampere) const +{ + return m_modbusConnection->writeHoldingRegister(m_unitId, TqSafeCurrent, ampere); +} + +QUuid Webasto::seComTimeout(quint16 seconds) const +{ + return m_modbusConnection->writeHoldingRegister(m_unitId, TqComTimeout, seconds); +} + +QUuid Webasto::setChargePower(quint32 watt) const +{ + QVector data; + data.append(watt>>16); + data.append(watt&0xff); + return m_modbusConnection->writeHoldingRegisters(m_unitId, TqChargePower, data); +} + +QUuid Webasto::setChargeCurrent(quint16 ampere) const +{ + return m_modbusConnection->writeHoldingRegister(m_unitId, TqChargeCurrent, ampere); +} + +void Webasto::setLiveBit() +{ + qCDebug(dcWebasto()) << "Webasto: Set live bit"; + m_modbusConnection->writeHoldingRegister(m_unitId, TqLifeBit, 0x0001); + if (m_awaitingLiveBitResponse) { + // Live bit response has not been received, setting connection as disconnected + // The live bit acts as heartbeat for both sides, client and server + if (m_connected) { + m_connected = false; + emit connectionStateChanged(false); + } + } else { + m_awaitingLiveBitResponse = true; + } +} + +void Webasto::onReceivedHoldingRegister(uint slaveAddress, uint modbusRegister, const QVector &values) +{ + Q_UNUSED(slaveAddress) + if (modbusRegister == TqLifeBit) { + m_awaitingLiveBitResponse = false; + if (!m_connected) { + m_connected = true; + emit connectionStateChanged(true); + } + } + emit receivedRegister(TqModbusRegister(modbusRegister), values); +} diff --git a/webasto/webasto.h b/webasto/webasto.h new file mode 100644 index 0000000..49a093f --- /dev/null +++ b/webasto/webasto.h @@ -0,0 +1,152 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef WEBASTO_H +#define WEBASTO_H + +#include +#include +#include +#include + +#include "../modbus/modbustcpmaster.h" + +class Webasto : public QObject +{ + Q_OBJECT +public: + enum ChargePointState { + ChargePointStateNoVehicleAttached = 0, + ChargePointStateVehicleAttachedNoPermission, + ChargePointStateChargingAuthorized, + ChargePointStateCharging, + ChargePointStateChargingPaused, + ChargePointStateChargeSuccessfulCarStillAttached, + ChargePointStateChargingStoppedByUserCarStillAttached, + ChargePointStateChargingErrorCarStillAttached, + ChargePointStateChargingStationReservedNorCarAttached, + ChargePointStateUserNotAuthorizedCarAttached + }; + Q_ENUM(ChargePointState) + + enum CableState { + CableStateNoCableAttached = 0, + CableStateCableAttachedNoCarAttached, + CableStateCableAttachedCarAttached, + CableStateCableAttachedCarAttachedLockActive + }; + Q_ENUM(CableState) + + enum EvseState { + EvseSateStarting = 0, + EvseSateRunning, + EvseSateError + }; + Q_ENUM(EvseState) + + enum TqModbusRegister { + TqChargePointState = 1000, // State of the charging device + TqChargeState = 1001, // Charging + TqEVSEState = 1002, // State of the charging station + TqCableState = 1004, // State of the charging cable + TqEVSEError = 1006, // Error code of the charging station + TqCurrentL1 = 1008, // Charging current L1 + TqCurrentL2 = 1010, // Charging current L2 + TqCurrentL3 = 1012, // Charging current L3 + TqActivePower = 1020, // Electric Power that can be changed to f.e. mechanical, chemical, thermic power + TqActivePowerL1 = 1024, // Active power L1 + TqActivePowerL2 = 1028, // Active power L2 + TqActivePowerL3 = 1032, // Active power L3 + TqEnergyMeter = 1036, // Meter reading of the charging station + TqMaxCurrent = 1100, // Maximal charging current UINT of the hardware (EVSE, cable, EV) + TqMinimumCurrentLimit = 1102, // Minimal charging current of the hardware (EVSE, cable, EV) + TqMaxCurrentFromEVSE = 1104, // Maximal charging current of the charging station + TqMaxCurrentFromCable = 1106, // Maximal charging current of the cable + TqMaxCurrentFromEV = 1108, // Maximal charging current of the EV + TqUserPriority = 1200, // Priorities of the user 0: not defined 1: high priority - 10: low priority + TqEVBatteryState = 1300, // Returns an estimate of the SoC + TqEVBatteryCapacity = 1302, // Returns an estimate of the EV Battery Capacity + TqScheduleType = 1400, // Type/information of traveling 0: energy that has to be charged, 1: Specification of the desired battery charge (Needs: state of the battery) + TqRequiredEnergy = 1402, // Desired energy + TqRequiredBatteryState = 1404, // Desired state of the battery + TqScheduledTime = 1408, // Departure time + TqScheduledDate = 1412, // Departure date + TqChargedEnergy = 1502, // Sum of charged energy for the current session + TqStartTime = 1504, // Start time of charging process + TqChargingTime = 1508, // Duration since beginning of charge + TqEndTime = 1512, // End time of charging process + TqUserId = 1600, // 24 Bytes long User ID (OCPP IdTag) from the current session + TqSmartVehicleDetected = 1620, //Returns 1 if an EV currently connected is a smart vehicle, or 0 if no EV connected or it is not a smart vehicle, + TqSafeCurrent = 2000, // Max. charge current under communication failure + TqComTimeout = 2002, // Communication timeout + TqChargePower = 5000, // Charge power + TqChargeCurrent = 5001, // Charge current + TqLifeBit = 6000 // Communication monitoring 0/1 Toggle-Bit EM writes 1, Live deletes it and puts it on 0. + }; + Q_ENUM(TqModbusRegister) + + explicit Webasto(const QHostAddress &address, uint port = 502, QObject *parent = nullptr); + + void setAddress(const QHostAddress &address); + QHostAddress address() const; + bool connected(); + bool connectDevice(); + + void setLivebitInterval(uint seconds); + + void getRegister(TqModbusRegister modbusRegister, uint length = 1); + + QUuid setSafeCurrent(quint16 ampere) const; + QUuid seComTimeout(quint16 seconds) const; + QUuid setChargePower(quint32 watt) const; + QUuid setChargeCurrent(quint16 ampere) const; + void setLiveBit(); + +private: + + ModbusTCPMaster *m_modbusConnection = nullptr; + QHostAddress m_address; + uint m_unitId = 255; + +private: + QTimer *m_lifeBitTimer = nullptr; + bool m_connected = false; + bool m_awaitingLiveBitResponse = false; +signals: + void connectionStateChanged(bool state); + void writeRequestExecuted(const QUuid &requestId, bool success); + void writeRequestError(const QUuid &requestId, const QString &error); + void receivedRegister(TqModbusRegister registerAddress, const QVector &data); + +private slots: + void onReceivedHoldingRegister(uint slaveAddress, uint modbusRegister, const QVector &values); +}; + +#endif // WEBASTO_H diff --git a/webasto/webasto.png b/webasto/webasto.png new file mode 100644 index 0000000..ea8e396 Binary files /dev/null and b/webasto/webasto.png differ diff --git a/webasto/webasto.pro b/webasto/webasto.pro new file mode 100644 index 0000000..3d94e5f --- /dev/null +++ b/webasto/webasto.pro @@ -0,0 +1,19 @@ +include(../plugins.pri) + +QT += \ + serialbus \ + network + +SOURCES += \ + integrationpluginwebasto.cpp \ + webasto.cpp \ + ../modbus/modbustcpmaster.cpp \ + ../discovery/discovery.cpp \ + ../discovery/host.cpp + +HEADERS += \ + integrationpluginwebasto.h \ + webasto.h \ + ../modbus/modbustcpmaster.h \ + ../discovery/discovery.h \ + ../discovery/host.h