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..e8029b1 --- /dev/null +++ b/webasto/integrationpluginwebasto.cpp @@ -0,0 +1,200 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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);}); + //TODO signal socket + //TODO emit setup finished + connect(webasto, &Webasto::connectionChanged, 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 (thing->thingClassId() == liveWallboxThingClassId) { + Webasto *connection = m_webastoConnections.value(thing); + if (!connection) { + qCWarning(dcWebasto()) << "Can't find connection to thing"; + } + + } 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) { + // Enable/Disable the charging process + } else if (action.actionTypeId() == liveWallboxChargeCurrentActionTypeId) { + + } 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::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); +} + diff --git a/webasto/integrationpluginwebasto.h b/webasto/integrationpluginwebasto.h new file mode 100644 index 0000000..6224c8d --- /dev/null +++ b/webasto/integrationpluginwebasto.h @@ -0,0 +1,69 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +#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; + QHash m_webastoConnections; + QHash m_asyncActions; + +private slots: + void onConnectionChanged(bool connected); +}; + +#endif // INTEGRATIONPLUGINWEBASTO_H diff --git a/webasto/integrationpluginwebasto.json b/webasto/integrationpluginwebasto.json new file mode 100644 index 0000000..3f964a5 --- /dev/null +++ b/webasto/integrationpluginwebasto.json @@ -0,0 +1,116 @@ +{ + "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": "537e01ac-9290-421d-a5cb-987d9e088941", + "name": "chargeTime", + "displayName": "Charging Time", + "unit": "Minutes", + "type": "int", + "defaultValue": 0, + "displayNameEvent": "Charging time changed" + }, + { + "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": "chargeCurrent", + "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 + } + ] + } + ] + } + ] +} + 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/webasto.cpp b/webasto/webasto.cpp new file mode 100644 index 0000000..339e73f --- /dev/null +++ b/webasto/webasto.cpp @@ -0,0 +1,86 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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) +{ + m_modbusConnection = new QModbusTcpClient(this); + m_modbusConnection->setConnectionParameter(QModbusDevice::NetworkPortParameter, port); + m_modbusConnection->setConnectionParameter(QModbusDevice::NetworkAddressParameter, address.toString()); + m_modbusConnection->setNumberOfRetries(3); + m_modbusConnection->setTimeout(1000); +} + +void Webasto::setAddress(const QHostAddress &address) +{ + qCDebug(dcWebasto()) << "Webasto: set address" << address; + m_modbusConnection->setConnectionParameter(QModbusDevice::NetworkAddressParameter, address.toString()); +} + +QHostAddress Webasto::address() const +{ + return QHostAddress(m_modbusConnection->connectionParameter(QModbusDevice::NetworkAddressParameter).toString()); +} + +bool Webasto::connected() +{ + return (m_modbusConnection->state() == QModbusTcpClient::State::ConnectedState); +} + +void Webasto::getBasicInformation() +{ + +} + +void Webasto::getUserId() +{ + +} + +void Webasto::getSessionInformation() +{ + +} + +void Webasto::setLiveBit() +{ + +} + +QUuid Webasto::writeHoldingRegister() +{ + QUuid request = QUuid::createUuid(); + + + return request; +} diff --git a/webasto/webasto.h b/webasto/webasto.h new file mode 100644 index 0000000..c64f8eb --- /dev/null +++ b/webasto/webasto.h @@ -0,0 +1,173 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 + +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 EvseSate { + EvseSateStarting = 0, + EvseSateRunning, + EvseSateError + }; + Q_ENUM(EvseSate) + + struct BasicInformation { + double currentL1; // [A] + double currentL2; // [A] + double currentL3; // [A] + double activePower; // [W] + uint activePowerL1; + uint activePowerL2; + uint activePowerL3; + double energyMeter; // [kWh] + }; + + struct CurrentLimitations { + double maxCurrent; + double minimumCurrentLimit; + double maxCurrentFromEvse; + double maxCurrentFromCable; + double maxCurrentFromEV; + }; + + explicit Webasto(const QHostAddress &address, uint port = 502, QObject *parent = nullptr); + + void setAddress(const QHostAddress &address); + QHostAddress address() const; + bool connected(); + +private: + enum TqModbusRegister { + // Get Basic Information polls Register 1000 to 1037 + 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 + // Get Current Limitatoins polls register 1100 to 1110 + 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 + // Get User Priority + TqUserPriority = 1200, // Priorities of the user 0: not defined 1: high priority - 10: low priority + // Get Battery state + TqEVBatteryState = 1300, // Returns an estimate of the SoC + TqEVBatteryCapacity = 1302, // Returns an estimate of the EV Battery Capacity + // Get Schedule polls register 1400 to 1414 + 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 + // Set session polls register 1500 to 15014 + 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 + // Get user id polls register 1600 to 1620 + 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, + // Get failsafe polls register 2000 to 2004 + TqSafeCurrent = 2000, // Max. charge current under communication failure + TqComTimeout = 2002, // Communication timeout + // Get Charge power polls register 5000 to 5002 + 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. + }; + + QModbusTcpClient *m_modbusConnection = nullptr; + QHostAddress m_address; + uint m_unitId = 255; + + void getBasicInformation(); + void getUserId(); + void getCurrentLimitations(); + void getSessionInformation(); + void getFailsafeSpecs(); + void getChargeCurrentAndPower(); + void getUserPriority(); + void getBatteryState(); + void setLiveBit(); + +private: + QTimer *m_lifeBitTimer = nullptr; + QUuid writeHoldingRegister(); + +signals: + void connectionChanged(bool connected); + void userIdReceived(const QByteArray &userId); + void currentLimitationsReceived(const CurrentLimitations &limitations); + void userPriorityReceived(uint userPriority); // 0 lowest - 10 highest +}; + +#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..a9abc65 --- /dev/null +++ b/webasto/webasto.pro @@ -0,0 +1,17 @@ +include(../plugins.pri) + +QT += \ + serialbus \ + network + +SOURCES += \ + integrationpluginwebasto.cpp \ + webasto.cpp \ + ../discovery/discovery.cpp \ + ../discovery/host.cpp + +HEADERS += \ + integrationpluginwebasto.h \ + webasto.h \ + ../discovery/discovery.h \ + ../discovery/host.h