diff --git a/debian/control b/debian/control index 596620f0..4bb9f9bc 100644 --- a/debian/control +++ b/debian/control @@ -14,6 +14,7 @@ Build-depends: libboblight-dev, qtbase5-dev, qtconnectivity5-dev, libow-dev, + libsodium-dev, Standards-Version: 3.9.3 @@ -453,6 +454,21 @@ Description: nymea.io plugin for networkdetector This package will install the nymea.io plugin for networkdetector +Package: nymea-plugin-nuki +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + libsodium23, + nymea-plugins-translations, +Description: nymea.io plugin for the nuki smart lock + 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 nuki devices + Package: nymea-plugin-onewire Architecture: any @@ -877,6 +893,7 @@ Depends: nymea-plugin-anel, nymea-plugin-texasinstruments, nymea-plugin-netatmo, nymea-plugin-networkdetector, + nymea-plugin-nuki, nymea-plugin-openuv, nymea-plugin-openweathermap, nymea-plugin-philipshue, diff --git a/debian/copyright b/debian/copyright index 268ef872..ae9f1c2c 100644 --- a/debian/copyright +++ b/debian/copyright @@ -54,6 +54,7 @@ Files: avahimonitor/* mailnotification/* netatmo/* networkdetector/* + nuki/* osdomotics/* senic/* udpcommander/* diff --git a/debian/nymea-plugin-nuki.install.in b/debian/nymea-plugin-nuki.install.in new file mode 100644 index 00000000..c0373c50 --- /dev/null +++ b/debian/nymea-plugin-nuki.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginnuki.so diff --git a/nuki/README.md b/nuki/README.md new file mode 100644 index 00000000..55891fa1 --- /dev/null +++ b/nuki/README.md @@ -0,0 +1,3 @@ +# nymea-plugin-nuki + +Device plugin for the [Nuki Smartlock](https://nuki.io/en/) Bluetooth API. diff --git a/nuki/bluez/bluetoothadapter.cpp b/nuki/bluez/bluetoothadapter.cpp new file mode 100644 index 00000000..75c9596f --- /dev/null +++ b/nuki/bluez/bluetoothadapter.cpp @@ -0,0 +1,416 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "bluetoothadapter.h" +#include "blueztypes.h" + +#include + +QString BluetoothAdapter::name() const +{ + return m_name; +} + +QString BluetoothAdapter::alias() const +{ + return m_alias; +} + +bool BluetoothAdapter::setAlias(const QString &alias) +{ + if (!m_adapterInterface->isValid()) + return false; + + return m_adapterInterface->setProperty("Alias", QVariant(alias)); +} + +QString BluetoothAdapter::address() const +{ + return m_address; +} + +QString BluetoothAdapter::modalias() const +{ + return m_modalias; +} + +bool BluetoothAdapter::discovering() const +{ + return m_discovering; +} + +bool BluetoothAdapter::discoverable() const +{ + return m_discoverable; +} + +bool BluetoothAdapter::setDiscoverable(const bool &discoverable) +{ + if (!m_adapterInterface->isValid()) + return false; + + return m_adapterInterface->setProperty("Discoverable", QVariant(discoverable)); +} + +uint BluetoothAdapter::discoverableTimeout() const +{ + return m_discoverableTimeout; +} + +bool BluetoothAdapter::setDiscoverableTimeout(const uint &seconds) +{ + if (!m_adapterInterface->isValid()) + return false; + + return m_adapterInterface->setProperty("DiscoverableTimeout", QVariant(seconds)); +} + +bool BluetoothAdapter::pairable() const +{ + return m_pairable; +} + +bool BluetoothAdapter::setPairable(const bool &pairable) +{ + if (!m_adapterInterface->isValid()) + return false; + + return m_adapterInterface->setProperty("Pairable", QVariant(pairable)); +} + +uint BluetoothAdapter::pairableTimeout() const +{ + return m_pairableTimeout; +} + +bool BluetoothAdapter::setPairableTimeout(const uint &seconds) +{ + if (!m_adapterInterface->isValid()) + return false; + + return m_adapterInterface->setProperty("PairableTimeout", QVariant(seconds)); +} + +uint BluetoothAdapter::adapterClass() const +{ + return m_adapterClass; +} + +QString BluetoothAdapter::adapterClassString() const +{ + QString adapterClassString; + switch (m_adapterClass) { + // TODO: get device type from class + default: + break; + } + return adapterClassString; +} + +bool BluetoothAdapter::powered() const +{ + return m_powered; +} + +bool BluetoothAdapter::setPower(const bool &power) +{ + if (!m_adapterInterface->isValid()) + return false; + + return m_adapterInterface->setProperty("Powered", QVariant(power)); +} + +QStringList BluetoothAdapter::uuids() const +{ + return m_uuids; +} + +QList BluetoothAdapter::devices() const +{ + return m_devices; +} + +bool BluetoothAdapter::hasDevice(const QBluetoothAddress &address) +{ + foreach (BluetoothDevice *device, m_devices) { + if (device->address() == address) { + return true; + } + } + + return false; +} + +BluetoothDevice *BluetoothAdapter::getDevice(const QBluetoothAddress &address) +{ + foreach (BluetoothDevice *device, m_devices) { + if (device->address() == address) { + return device; + } + } + + return nullptr; +} + +bool BluetoothAdapter::removeDevice(const QBluetoothAddress &address) +{ + foreach (BluetoothDevice *device, m_devices) { + if (device->address() == address) { + return removeDevice(device->m_path); + } + } + + return false; +} + +bool BluetoothAdapter::isValid() const +{ + return m_adapterInterface->isValid(); +} + +BluetoothAdapter::BluetoothAdapter(const QDBusObjectPath &path, const QVariantMap &properties, QObject *parent) : + QObject(parent), + m_path(path), + m_discovering(false), + m_discoverable(false), + m_discoverableTimeout(0), + m_pairable(false), + m_pairableTimeout(0), + m_adapterClass(0), + m_powered(false) +{ + // Check DBus connection + if (!QDBusConnection::systemBus().isConnected()) { + qCWarning(dcBluez()) << "System DBus not connected."; + return; + } + + m_adapterInterface = new QDBusInterface(orgBluez, m_path.path(), orgBluezAdapter1, QDBusConnection::systemBus(), this); + if (!m_adapterInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus adapter interface for" << m_path.path(); + return; + } + + QDBusConnection::systemBus().connect(orgBluez, m_path.path(), "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onPropertiesChanged(QString,QVariantMap,QStringList))); + + processProperties(properties); +} + +BluetoothAdapter::~BluetoothAdapter() +{ + +} + +void BluetoothAdapter::processProperties(const QVariantMap &properties) +{ + foreach (const QString &propertyName, properties.keys()) { + if (propertyName == "Name") { + m_name = properties.value(propertyName).toString(); + } else if (propertyName == "Alias") { + setAliasInternally(properties.value(propertyName).toString()); + } else if (propertyName == "Address") { + m_address = properties.value(propertyName).toString(); + } else if (propertyName == "Modalias") { + m_modalias = properties.value(propertyName).toString(); + } else if (propertyName == "Discovering") { + setDiscoveringInternally(properties.value(propertyName).toBool()); + } else if (propertyName == "Discoverable") { + setDiscoverableInernally(properties.value(propertyName).toBool()); + } else if (propertyName == "DiscoverableTimeout") { + setDiscoverableTimeoutInternally(properties.value(propertyName).toUInt()); + } else if (propertyName == "Pairable") { + setPairableInternally(properties.value(propertyName).toBool()); + } else if (propertyName == "PairableTimeout") { + setPairableTimeoutInternally(properties.value(propertyName).toUInt()); + } else if (propertyName == "Class") { + m_adapterClass = properties.value(propertyName).toUInt(); + } else if (propertyName == "Powered") { + setPoweredInternally(properties.value(propertyName).toBool()); + } else if (propertyName == "UUIDs") { + m_uuids = properties.value(propertyName).toStringList(); + } + } +} + +void BluetoothAdapter::addDeviceInternally(const QDBusObjectPath &path, const QVariantMap &properties) +{ + // Check if device already added + if (hasDevice(path)) + return; + + BluetoothDevice *device = new BluetoothDevice(path, properties, this); + m_devices.append(device); + + qCDebug(dcBluez()) << "[+]" << device; + + emit deviceAdded(device); +} + +void BluetoothAdapter::removeDeviceInternally(const QDBusObjectPath &path) +{ + foreach (BluetoothDevice *device, m_devices) { + if (device->m_path == path) { + m_devices.removeOne(device); + emit deviceRemoved(device); + device->deleteLater(); + } + } +} + +bool BluetoothAdapter::hasDevice(const QDBusObjectPath &path) +{ + foreach (BluetoothDevice *device, m_devices) { + if (device->m_path == path) + return true; + } + + return false; +} + +bool BluetoothAdapter::removeDevice(const QDBusObjectPath &path) +{ + if (!m_adapterInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus adapter interface for" << m_path.path(); + return false; + } + + qCDebug(dcBluez()) << "Remove and unpair device" << path.path(); + QDBusPendingCall removeCall = m_adapterInterface->asyncCall("RemoveDevice", QVariant::fromValue(path)); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(removeCall, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &BluetoothAdapter::onRemoveDeviceFinished); + return true; +} + +void BluetoothAdapter::setAliasInternally(const QString &alias) +{ + if (m_alias != alias) { + m_alias = alias; + emit aliasChanged(m_alias); + } +} + +void BluetoothAdapter::setDiscoveringInternally(const bool &discovering) +{ + if (m_discovering != discovering) { + m_discovering = discovering; + emit discoveringChanged(m_discovering); + } +} + +void BluetoothAdapter::setDiscoverableInernally(const bool &discoverable) +{ + if (m_discoverable != discoverable) { + m_discoverable = discoverable; + emit discoverableChanged(m_discoverable); + } +} + +void BluetoothAdapter::setDiscoverableTimeoutInternally(const uint &discoverableTimeout) +{ + if (m_discoverableTimeout != discoverableTimeout) { + m_discoverableTimeout = discoverableTimeout; + emit discoverableTimeoutChanged(m_discoverableTimeout); + } +} + +void BluetoothAdapter::setPairableInternally(const bool &pairable) +{ + if (m_pairable != pairable) { + m_pairable = pairable; + emit pairableChanged(m_pairable); + } +} + +void BluetoothAdapter::setPairableTimeoutInternally(const uint &pairableTimeout) +{ + if (m_pairableTimeout != pairableTimeout) { + m_pairableTimeout = pairableTimeout; + emit pairableTimeoutChanged(m_pairableTimeout); + } +} + +void BluetoothAdapter::setPoweredInternally(const bool &powered) +{ + if (m_powered != powered) { + m_powered = powered; + emit poweredChanged(m_powered); + } +} + +void BluetoothAdapter::onPropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties) +{ + if (interface != orgBluezAdapter1) + return; + + qCDebug(dcBluez()) << "BluetoothAdapter:" << m_name << m_address << "properties changed" << interface << changedProperties << invalidatedProperties; + processProperties(changedProperties); +} + +void BluetoothAdapter::onRemoveDeviceFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) + qCWarning(dcBluez()) << "Could not remove device" << m_address << reply.error().name() << reply.error().message(); + + call->deleteLater(); +} + +void BluetoothAdapter::startDiscovering() +{ + if (!m_adapterInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus adapter interface for" << m_path.path(); + return; + } + + if (discovering()) + return; + + QDBusMessage query = m_adapterInterface->call("StartDiscovery"); + if(query.type() != QDBusMessage::ReplyMessage) { + qCWarning(dcBluez()) << "Could not start discovery" << m_name << ":" << query.errorName() << query.errorMessage(); + return; + } +} + +void BluetoothAdapter::stopDiscovering() +{ + if (!m_adapterInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus adapter interface for" << m_path.path(); + return; + } + + QDBusMessage query = m_adapterInterface->call("StopDiscovery"); + if(query.type() != QDBusMessage::ReplyMessage) { + qCWarning(dcBluez()) << "Could not start discovery" << m_name << ":" << query.errorName() << query.errorMessage(); + return; + } +} + +QDebug operator<<(QDebug debug, BluetoothAdapter *adapter) +{ + debug.noquote().nospace() << "BluetoothAdapter(" << adapter->name() << ", " << adapter->address(); + debug.noquote().nospace() << ", powered: " << adapter->powered(); + debug.noquote().nospace() << ", pairable: " << adapter->pairable(); + debug.noquote().nospace() << ", visible: " << adapter->discoverable(); + debug.noquote().nospace() << ") "; + return debug; +} diff --git a/nuki/bluez/bluetoothadapter.h b/nuki/bluez/bluetoothadapter.h new file mode 100644 index 00000000..d5fa900e --- /dev/null +++ b/nuki/bluez/bluetoothadapter.h @@ -0,0 +1,152 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef BLUETOOTHADAPTER_H +#define BLUETOOTHADAPTER_H + +#include +#include +#include +#include +#include + +#include "bluetoothdevice.h" + +// Note: DBus documentation https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/adapter-api.txt + +class BluetoothManager; + +class BluetoothAdapter : public QObject +{ + Q_OBJECT + + friend class BluetoothManager; + +public: + // Properties + QString name() const; + + QString alias() const; + bool setAlias(const QString &alias); + + QString address() const; + QString modalias() const; + bool discovering() const; + + bool discoverable() const; + bool setDiscoverable(const bool &discoverable); + + uint discoverableTimeout() const; + bool setDiscoverableTimeout(const uint &seconds); + + bool pairable() const; + bool setPairable(const bool &pairable); + + uint pairableTimeout() const; + bool setPairableTimeout(const uint &seconds); + + uint adapterClass() const; + QString adapterClassString() const; + + bool powered() const; + bool setPower(const bool &power); + + QStringList uuids() const; + + QList devices() const; + bool hasDevice(const QBluetoothAddress &address); + BluetoothDevice *getDevice(const QBluetoothAddress &address); + bool removeDevice(const QBluetoothAddress &address); + + bool isValid() const; + +private: + // Note: only BluetoothManager can create adapter objects + explicit BluetoothAdapter(const QDBusObjectPath &path, const QVariantMap &properties, QObject *parent = 0); + ~BluetoothAdapter(); + + QDBusObjectPath m_path; + QDBusInterface *m_adapterInterface; + + QString m_name; + QString m_address; + + QString m_alias; + QString m_modalias; + bool m_discovering; + bool m_discoverable; + uint m_discoverableTimeout; + bool m_pairable; + uint m_pairableTimeout; + uint m_adapterClass; + bool m_powered; + QStringList m_uuids; + + QList m_devices; + + void processProperties(const QVariantMap &properties); + + // Methods called from BluetoothManager + void addDeviceInternally(const QDBusObjectPath &path, const QVariantMap &properties); + void removeDeviceInternally(const QDBusObjectPath &path); + + // Verification methods + bool hasDevice(const QDBusObjectPath &path); + + // DBus methods + bool removeDevice(const QDBusObjectPath &path); + + + void setAliasInternally(const QString &alias); + void setDiscoveringInternally(const bool &discovering); + void setDiscoverableInernally(const bool &discoverable); + void setDiscoverableTimeoutInternally(const uint &discoverableTimeout); + void setPairableInternally(const bool &pairable); + void setPairableTimeoutInternally(const uint &pairableTimeout); + void setPoweredInternally(const bool &powered); + +private slots: + void onPropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties); + void onRemoveDeviceFinished(QDBusPendingCallWatcher *call); + +signals: + void aliasChanged(const QString &alias); + void discoveringChanged(const bool &discovering); + void discoverableChanged(const bool &discoverable); + void discoverableTimeoutChanged(const uint &discoverableTimeout); + void pairableChanged(const bool &pairable); + void pairableTimeoutChanged(const uint &pairableTimeout); + void poweredChanged(const bool &powered); + + void deviceAdded(BluetoothDevice *device); + void deviceRemoved(BluetoothDevice *device); + +public slots: + void startDiscovering(); + void stopDiscovering(); + +}; + +QDebug operator<<(QDebug debug, BluetoothAdapter *adapter); + + +#endif // BLUETOOTHADAPTER_H diff --git a/nuki/bluez/bluetoothdevice.cpp b/nuki/bluez/bluetoothdevice.cpp new file mode 100644 index 00000000..871d03e8 --- /dev/null +++ b/nuki/bluez/bluetoothdevice.cpp @@ -0,0 +1,546 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "bluetoothdevice.h" + +#include + +BluetoothDevice::State BluetoothDevice::state() const +{ + return m_state; +} + +QString BluetoothDevice::name() const +{ + return m_name; +} + +QBluetoothAddress BluetoothDevice::address() const +{ + return m_address; +} + +QString BluetoothDevice::iconName() const +{ + return m_iconName; +} + +QBluetoothHostInfo BluetoothDevice::hostInfo() const +{ + return m_hostInfo; +} + +QString BluetoothDevice::alias() const +{ + return m_alias; +} + +bool BluetoothDevice::setAlias(const QString &alias) +{ + if (!m_deviceInterface->isValid()) + return false; + + return m_deviceInterface->setProperty("Alias", QVariant(alias)); +} + +QString BluetoothDevice::modalias() const +{ + return m_modalias; +} + +quint32 BluetoothDevice::deviceClass() const +{ + return m_deviceClass; +} + +quint16 BluetoothDevice::appearance() const +{ + return m_appearance; +} + +qint16 BluetoothDevice::rssi() const +{ + return m_rssi; +} + +qint16 BluetoothDevice::txPower() const +{ + return m_txPower; +} + +QList BluetoothDevice::uuids() const +{ + return m_uuids; +} + +bool BluetoothDevice::paired() const +{ + return m_paired; +} + +bool BluetoothDevice::connected() const +{ + return m_connected; +} + +bool BluetoothDevice::trusted() const +{ + return m_trusted; +} + +bool BluetoothDevice::setTrusted(const bool &trusted) +{ + if (!m_deviceInterface->isValid()) + return false; + + return m_deviceInterface->setProperty("Trusted", QVariant(trusted)); +} + +bool BluetoothDevice::blocked() const +{ + return m_blocked; +} + +bool BluetoothDevice::setBlocked(const bool &blocked) +{ + if (!m_deviceInterface->isValid()) + return false; + + return m_deviceInterface->setProperty("Blocked", QVariant(blocked)); +} + +bool BluetoothDevice::legacyPairing() const +{ + return m_legacyPairing; +} + +bool BluetoothDevice::servicesResolved() const +{ + return m_servicesResolved; +} + +QList BluetoothDevice::services() const +{ + return m_services; +} + +bool BluetoothDevice::hasService(const QBluetoothUuid &serviceUuid) +{ + foreach (BluetoothGattService *service, m_services) { + if (service->uuid() == serviceUuid) { + return true; + } + } + + return false; +} + +BluetoothGattService *BluetoothDevice::getService(const QBluetoothUuid &serviceUuid) +{ + foreach (BluetoothGattService *service, m_services) { + if (service->uuid() == serviceUuid) { + return service; + } + } + + return nullptr; +} + +BluetoothDevice::BluetoothDevice(const QDBusObjectPath &path, const QVariantMap &properties, QObject *parent) : + QObject(parent), + m_path(path), + m_state(Disconnected), + m_deviceClass(0), + m_appearance(0), + m_rssi(0), + m_txPower(0), + m_paired(false), + m_connected(false), + m_trusted(false), + m_blocked(false), + m_legacyPairing(false), + m_servicesResolved(false), + m_connectWatcher(nullptr), + m_disconnectWatcher(nullptr), + m_pairingWatcher(nullptr) +{ + // Check DBus connection + if (!QDBusConnection::systemBus().isConnected()) { + qCWarning(dcBluez()) << "System DBus not connected."; + return; + } + + m_deviceInterface = new QDBusInterface(orgBluez, m_path.path(), orgBluezDevice1, QDBusConnection::systemBus(), this); + if (!m_deviceInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus device interface for" << m_path.path(); + return; + } + + QDBusConnection::systemBus().connect(orgBluez, m_path.path(), "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onPropertiesChanged(QString,QVariantMap,QStringList))); + + processProperties(properties); + + // Set initial state + evaluateCurrentState(); +} + +BluetoothDevice::~BluetoothDevice() +{ + +} + +void BluetoothDevice::processProperties(const QVariantMap &properties) +{ + foreach (const QString &propertyName, properties.keys()) { + if (propertyName == "Name") { + m_name = properties.value(propertyName).toString(); + m_hostInfo.setName(m_name); + } else if (propertyName == "Address") { + m_address = QBluetoothAddress(properties.value(propertyName).toString()); + m_hostInfo.setAddress(m_address); + } else if (propertyName == "Icon") { + m_iconName = properties.value(propertyName).toString(); + } else if (propertyName == "Alias") { + setAliasInternally(properties.value(propertyName).toString()); + } else if (propertyName == "Modalias") { + m_modalias = properties.value(propertyName).toString(); + } else if (propertyName == "Class") { + m_deviceClass = properties.value(propertyName).toUInt(); + } else if (propertyName == "Appearance") { + m_appearance = properties.value(propertyName).toUInt(); + } else if (propertyName == "RSSI") { + setRssiInternally(properties.value(propertyName).toInt()); + } else if (propertyName == "TxPower") { + setTxPowerInternally(properties.value(propertyName).toInt()); + } else if (propertyName == "UUIDs") { + QStringList uuidStrings = properties.value(propertyName).toStringList(); + m_uuids.clear(); + foreach (const QString &uuidString, uuidStrings) { + QBluetoothUuid uuid = QBluetoothUuid(QUuid(uuidString)); + m_uuids.append(uuid); + } + } else if (propertyName == "Paired") { + setPairedInternally(properties.value(propertyName).toBool()); + } else if (propertyName == "Connected") { + setConnectedInternally(properties.value(propertyName).toBool()); + } else if (propertyName == "Trusted") { + setTrustedInternally(properties.value(propertyName).toBool()); + } else if (propertyName == "Blocked") { + setBlockedInternally(properties.value(propertyName).toBool()); + } else if (propertyName == "LegacyPairing") { + m_legacyPairing = properties.value(propertyName).toBool(); + } else if (propertyName == "ServicesResolved") { + setServicesResolvedInternally(properties.value(propertyName).toBool()); + } + } + +} + +void BluetoothDevice::evaluateCurrentState() +{ + if (!connected()) { + setStateInternally(Disconnected); + } else if (connected() && servicesResolved()) { + setStateInternally(Discovered); + } +} + +void BluetoothDevice::addServiceInternally(const QDBusObjectPath &path, const QVariantMap &properties) +{ + if (hasService(path)) + return; + + BluetoothGattService *service = new BluetoothGattService(path, properties, this); + m_services.append(service); + qCDebug(dcBluez()) << "[+]" << service; +} + +bool BluetoothDevice::hasService(const QDBusObjectPath &path) +{ + foreach (BluetoothGattService *service, m_services) { + if (service->m_path == path) { + return true; + } + } + + return false; +} + +BluetoothGattService *BluetoothDevice::getService(const QDBusObjectPath &path) +{ + foreach (BluetoothGattService *service, m_services) { + if (service->m_path == path) { + return service; + } + } + + return nullptr; +} + +void BluetoothDevice::setStateInternally(const BluetoothDevice::State &state) +{ + if (m_state != state) { + m_state = state; + emit stateChanged(m_state); + } +} + +void BluetoothDevice::setAliasInternally(const QString &alias) +{ + if (m_alias != alias) { + m_alias = alias; + emit aliasChanged(m_alias); + } +} + +void BluetoothDevice::setRssiInternally(const qint16 &rssi) +{ + if (m_rssi != rssi) { + m_rssi = rssi; + emit rssiChanged(m_rssi); + } +} + +void BluetoothDevice::setTxPowerInternally(const qint16 &txPower) +{ + if (m_txPower != txPower) { + m_txPower = txPower; + emit txPowerChanged(m_txPower); + } +} + +void BluetoothDevice::setPairedInternally(const bool &paired) +{ + if (m_paired != paired) { + m_paired = paired; + emit pairedChanged(m_paired); + + // Paired, if services not resolved + if (!m_servicesResolved) { + setStateInternally(Discovering); + } else { + setStateInternally(Discovered); + } + } +} + +void BluetoothDevice::setConnectedInternally(const bool &connected) +{ + if (m_connected != connected) { + m_connected = connected; + emit connectedChanged(m_connected); + + if (m_connected) { + setStateInternally(Connected); + + if (!m_servicesResolved) + setStateInternally(Discovering); + + } else { + setStateInternally(Disconnected); + } + } +} + +void BluetoothDevice::setTrustedInternally(const bool &trusted) +{ + if (m_trusted != trusted) { + m_trusted = trusted; + emit trustedChanged(m_trusted); + } +} + +void BluetoothDevice::setBlockedInternally(const bool &blocked) +{ + if (m_blocked != blocked) { + m_blocked = blocked; + emit blockedChanged(m_blocked); + } +} + +void BluetoothDevice::setServicesResolvedInternally(const bool &servicesResolved) +{ + if (m_servicesResolved != servicesResolved) { + m_servicesResolved = servicesResolved; + emit servicesResolvedChanged(m_servicesResolved); + + if (m_servicesResolved) + setStateInternally(Discovered); + + // Note: if unresolved, the device is about to disconnect + } +} + +void BluetoothDevice::onPropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties) +{ + if (interface != orgBluezDevice1) + return; + + qCDebug(dcBluez()) << "BluetoothDevice:" << m_name << m_address << "properties changed" << interface << changedProperties << invalidatedProperties; + processProperties(changedProperties); +} + +void BluetoothDevice::onConnectDeviceFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) { + setStateInternally(Disconnected); + qCWarning(dcBluez()) << "Could not connect device" << m_address.toString() << reply.error().name() << reply.error().message(); + } + + call->deleteLater(); + m_connectWatcher = nullptr; +} + +void BluetoothDevice::onDisconnectDeviceFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) + qCWarning(dcBluez()) << "Could not disconnect device" << m_address.toString() << reply.error().name() << reply.error().message(); + + evaluateCurrentState(); + + call->deleteLater(); + m_disconnectWatcher = nullptr; +} + +void BluetoothDevice::onPairingFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) + qCWarning(dcBluez()) << "Could not pair device" << m_address.toString() << reply.error().name() << reply.error().message(); + + evaluateCurrentState(); + + call->deleteLater(); + m_pairingWatcher = nullptr; +} + +void BluetoothDevice::onCancelPairingFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) + qCWarning(dcBluez()) << "Could not cancel pairing" << m_address.toString() << reply.error().name() << reply.error().message(); + + evaluateCurrentState(); + + call->deleteLater(); +} + +bool BluetoothDevice::connectDevice() +{ + if (!m_deviceInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus device interface for" << m_path.path(); + return false; + } + + if (connected() || state() == Connecting || m_connectWatcher) + return true; + + setStateInternally(Connecting); + + QDBusPendingCall connectingCall = m_deviceInterface->asyncCall("Connect"); + m_connectWatcher = new QDBusPendingCallWatcher(connectingCall, this); + connect(m_connectWatcher, &QDBusPendingCallWatcher::finished, this, &BluetoothDevice::onConnectDeviceFinished); + return true; +} + +bool BluetoothDevice::disconnectDevice() +{ + if (!m_deviceInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus device interface for" << m_path.path(); + return false; + } + + if (!connected() || state() == Disconnecting || m_disconnectWatcher) + return true; + + setStateInternally(Disconnecting); + + QDBusPendingCall disconnectingCall = m_deviceInterface->asyncCall("Disconnect"); + m_disconnectWatcher = new QDBusPendingCallWatcher(disconnectingCall, this); + connect(m_disconnectWatcher, &QDBusPendingCallWatcher::finished, this, &BluetoothDevice::onDisconnectDeviceFinished); + return true; +} + +bool BluetoothDevice::disconnectDeviceBlocking() +{ + if (!m_deviceInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus device interface for" << m_path.path(); + return false; + } + + if (!connected() || state() == Disconnecting) + return true; + + qCWarning(dcBluez()) << "Disconnecting blocking" << this; + QDBusPendingReply reply = m_deviceInterface->call("Disconnect"); + reply.waitForFinished(); + if(reply.isError()) { + qCWarning(dcBluez()) << reply.error(); + return false; + } + + return true; +} + +bool BluetoothDevice::requestPairing() +{ + if (!m_deviceInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus device interface for" << m_path.path(); + return false; + } + + if (paired() || state() == Pairing || m_pairingWatcher) + return true; + + setStateInternally(Pairing); + + QDBusPendingCall pairCall = m_deviceInterface->asyncCall("Pair"); + m_pairingWatcher = new QDBusPendingCallWatcher(pairCall, this); + connect(m_pairingWatcher, &QDBusPendingCallWatcher::finished, this, &BluetoothDevice::onPairingFinished); + return true; +} + +bool BluetoothDevice::cancelPairingRequest() +{ + if (!m_deviceInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus device interface for" << m_path.path(); + return false; + } + + if (!paired() || state() == Unpairing) + return true; + + setStateInternally(Unpairing); + + QDBusPendingCall cancelPairingCall = m_deviceInterface->asyncCall("CancelPairing"); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(cancelPairingCall, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &BluetoothDevice::onPairingFinished); + return true; +} + +QDebug operator<<(QDebug debug, BluetoothDevice *device) +{ + debug.noquote().nospace() << "BluetoothDevice(" << device->name() << ", " << device->address() << ") "; + return debug; +} diff --git a/nuki/bluez/bluetoothdevice.h b/nuki/bluez/bluetoothdevice.h new file mode 100644 index 00000000..dcb6d9ef --- /dev/null +++ b/nuki/bluez/bluetoothdevice.h @@ -0,0 +1,185 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef BLUETOOTHDEVICE_H +#define BLUETOOTHDEVICE_H + +#include +#include +#include +#include +#include +#include + +#include "blueztypes.h" +#include "bluetoothgattservice.h" + +// Note: DBus documentation https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt + +class BluetoothManager; +class BluetoothAdapter; + +class BluetoothDevice : public QObject +{ + Q_OBJECT + + friend class BluetoothManager; + friend class BluetoothAdapter; + +public: + enum State { + Connecting, + Connected, + Pairing, + Unpairing, + Discovering, + Discovered, + Disconnecting, + Disconnected + }; + Q_ENUM(State) + + State state() const; + + QString name() const; + QBluetoothAddress address() const; + QString iconName() const; + + QBluetoothHostInfo hostInfo() const; + + QString alias() const; + bool setAlias(const QString &alias); + + QString modalias() const; + quint32 deviceClass() const; + quint16 appearance() const; + qint16 rssi() const; + qint16 txPower() const; + QList uuids() const; + bool paired() const; + bool connected() const; + + bool trusted() const; + bool setTrusted(const bool &trusted); + + bool blocked() const; + bool setBlocked(const bool &blocked); + + bool legacyPairing() const; + bool servicesResolved() const; + + // Service methods + QList services() const; + bool hasService(const QBluetoothUuid &serviceUuid); + BluetoothGattService *getService(const QBluetoothUuid &serviceUuid); + +private: + explicit BluetoothDevice(const QDBusObjectPath &path, const QVariantMap &properties, QObject *parent = 0); + ~BluetoothDevice(); + + QDBusObjectPath m_path; + QDBusInterface *m_deviceInterface; + + QList m_services; + + State m_state; + + QString m_name; + QBluetoothAddress m_address; + QBluetoothHostInfo m_hostInfo; + QString m_iconName; + QString m_alias; + QString m_modalias; + quint32 m_deviceClass; + quint16 m_appearance; + qint16 m_rssi; + qint16 m_txPower; + + QList m_uuids; + bool m_paired; + bool m_connected; + bool m_trusted; + bool m_blocked; + bool m_legacyPairing; + bool m_servicesResolved; + + QDBusObjectPath m_adapterObjectPath; + + QDBusPendingCallWatcher *m_connectWatcher; + QDBusPendingCallWatcher *m_disconnectWatcher; + QDBusPendingCallWatcher *m_pairingWatcher; + + // TODO: org.bluez.Device1.ManufacturerData + // TODO: org.bluez.Device1.ServiceData + + void processProperties(const QVariantMap &properties); + + void evaluateCurrentState(); + + // Methods called from BluetoothManager + void addServiceInternally(const QDBusObjectPath &path, const QVariantMap &properties); + bool hasService(const QDBusObjectPath &path); + BluetoothGattService *getService(const QDBusObjectPath &path); + + void setStateInternally(const State &state); + + void setAliasInternally(const QString &alias); + void setRssiInternally(const qint16 &rssi); + void setTxPowerInternally(const qint16 &txPower); + void setPairedInternally(const bool &paired); + void setConnectedInternally(const bool &connected); + void setTrustedInternally(const bool &trusted); + void setBlockedInternally(const bool &blocked); + void setServicesResolvedInternally(const bool &servicesResolved); + +signals: + void stateChanged(const State &state); + + void aliasChanged(const QString &alias); + void rssiChanged(const qint16 &rssi); + void txPowerChanged(const qint16 &txPower); + void pairedChanged(const bool &paired); + void connectedChanged(const bool &connected); + void trustedChanged(const bool &trusted); + void blockedChanged(const bool &blocked); + void servicesResolvedChanged(const bool &servicesResolved); + +private slots: + void onPropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties); + + void onConnectDeviceFinished(QDBusPendingCallWatcher *call); + void onDisconnectDeviceFinished(QDBusPendingCallWatcher *call); + void onPairingFinished(QDBusPendingCallWatcher *call); + void onCancelPairingFinished(QDBusPendingCallWatcher *call); + +public slots: + bool connectDevice(); + bool disconnectDevice(); + bool disconnectDeviceBlocking(); + bool requestPairing(); + bool cancelPairingRequest(); +}; + +QDebug operator<<(QDebug debug, BluetoothDevice *device); + + +#endif // BLUETOOTHDEVICE_H diff --git a/nuki/bluez/bluetoothgattcharacteristic.cpp b/nuki/bluez/bluetoothgattcharacteristic.cpp new file mode 100644 index 00000000..20a48d69 --- /dev/null +++ b/nuki/bluez/bluetoothgattcharacteristic.cpp @@ -0,0 +1,368 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "bluetoothgattcharacteristic.h" + +#include + +QString BluetoothGattCharacteristic::chararcteristicName() const +{ + bool ok = false; + quint16 typeId = m_uuid.toUInt16(&ok); + + if (ok) { + QBluetoothUuid::CharacteristicType uuid = static_cast(typeId); + const QString name = QBluetoothUuid::characteristicToString(uuid); + if (!name.isEmpty()) + return name; + } + + return QString("Unknown Characteristic"); +} + +QBluetoothUuid BluetoothGattCharacteristic::uuid() const +{ + return m_uuid; +} + +bool BluetoothGattCharacteristic::notifying() const +{ + return m_notifying; +} + +BluetoothGattCharacteristic::Properties BluetoothGattCharacteristic::properties() const +{ + return m_properties; +} + +QByteArray BluetoothGattCharacteristic::value() const +{ + return m_value; +} + +QList BluetoothGattCharacteristic::descriptors() const +{ + return m_descriptors; +} + +BluetoothGattCharacteristic::BluetoothGattCharacteristic(const QDBusObjectPath &path, const QVariantMap &properties, QObject *parent) : + QObject(parent), + m_path(path), + m_notifying(false) +{ + m_characteristicInterface = new QDBusInterface(orgBluez, m_path.path(), orgBluezGattCharacteristic1, QDBusConnection::systemBus(), this); + if (!m_characteristicInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus characteristic interface for" << m_path.path(); + return; + } + + QDBusConnection::systemBus().connect(orgBluez, m_path.path(), "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onPropertiesChanged(QString,QVariantMap,QStringList))); + + processProperties(properties); +} + +void BluetoothGattCharacteristic::processProperties(const QVariantMap &properties) +{ + foreach (const QString &propertyName, properties.keys()) { + if (propertyName == "UUID") { + m_uuid = QBluetoothUuid(properties.value(propertyName).toString()); + } else if (propertyName == "Notifying") { + m_notifying = properties.value(propertyName).toBool(); + emit notifyingChanged(m_notifying); + } else if (propertyName == "Flags") { + m_properties = parsePropertyFlags(properties.value(propertyName).toStringList()); + } else if (propertyName == "Value") { + m_value = properties.value(propertyName).toByteArray(); + emit valueChanged(m_value); + } + } +} + +void BluetoothGattCharacteristic::addDescriptorInternally(const QDBusObjectPath &path, const QVariantMap &properties) +{ + if (hasDescriptor(path)) + return; + + BluetoothGattDescriptor *descriptor = new BluetoothGattDescriptor(path, properties, this); + m_descriptors.append(descriptor); + + qCDebug(dcBluez()) << "[+]" << descriptor; +} + +bool BluetoothGattCharacteristic::hasDescriptor(const QDBusObjectPath &path) +{ + foreach (BluetoothGattDescriptor *descriptor, m_descriptors) { + if (descriptor->m_path == path) { + return true; + } + } + + return false; +} + +BluetoothGattDescriptor *BluetoothGattCharacteristic::getDescriptor(const QDBusObjectPath &path) +{ + foreach (BluetoothGattDescriptor *descriptor, m_descriptors) { + if (descriptor->m_path == path) { + return descriptor; + } + } + + return nullptr; +} + +void BluetoothGattCharacteristic::setValueInternally(const QByteArray &value) +{ + if (m_value != value) { + m_value = value; + emit valueChanged(m_value); + } +} + +void BluetoothGattCharacteristic::setNotifyingInternally(const bool ¬ifying) +{ + if (m_notifying != notifying) { + m_notifying = notifying; + emit notifyingChanged(m_notifying); + } +} + +BluetoothGattCharacteristic::Properties BluetoothGattCharacteristic::parsePropertyFlags(const QStringList &characteristicProperties) +{ + Properties properties; + + foreach (const QString &propertyString, characteristicProperties) { + if (propertyString == "broadcast") { + properties |= Broadcasting; + } else if (propertyString == "read") { + properties |= Read; + } else if (propertyString == "write-without-response") { + properties |= WriteNoResponse; + } else if (propertyString == "write") { + properties |= Write; + } else if (propertyString == "notify") { + properties |= Notify; + } else if (propertyString == "indicate") { + properties |= Indicate; + } else if (propertyString == "authenticated-signed-writes") { + properties |= WriteAuthenticatedSigned; + } else if (propertyString == "reliable-write") { + properties |= ReliableWrite; + } else if (propertyString == "writable-auxiliaries") { + properties |= WritableAuxiliaries; + } else if (propertyString == "encrypt-read") { + properties |= EncryptRead; + } else if (propertyString == "encrypt-write") { + properties |= EncryptWrite; + } else if (propertyString == "encrypt-authenticated-read") { + properties |= EncryptAuthenticatedRead; + } else if (propertyString == "encrypt-authenticated-write") { + properties |= EncryptAuthenticatedWrite; + } else if (propertyString == "secure-read") { + properties |= SecureRead; + } else if (propertyString == "secure-write") { + properties |= Unknown; + } + } + + return properties; +} + +void BluetoothGattCharacteristic::onPropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties) +{ + if (interface != orgBluezGattCharacteristic1) + return; + + qCDebug(dcBluez()) << "BluetoothCharacteristic:" << m_uuid.toString() << "properties changed" << interface << changedProperties << invalidatedProperties; + processProperties(changedProperties); +} + +void BluetoothGattCharacteristic::onReadingFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) { + qCWarning(dcBluez()) << "Could not read characteristic" << m_uuid.toString() << reply.error().name() << reply.error().message(); + } else { + QByteArray value = reply.argumentAt<0>(); + qCDebug(dcBluez()) << "Async reading finished for" << m_uuid.toString() << value; + setValueInternally(value); + emit readingFinished(value); + } + + call->deleteLater(); +} + +void BluetoothGattCharacteristic::onWritingFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) { + qCWarning(dcBluez()) << "Could not write characteristic" << m_uuid.toString() << reply.error().name() << reply.error().message(); + } else { + QByteArray value = m_asyncWrites.take(call); + qCDebug(dcBluez()) << "Async characteristic writing finished for" << m_uuid.toString() << value; + emit writingFinished(value); + } + + call->deleteLater(); +} + +void BluetoothGattCharacteristic::onStartNotificationFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) + qCWarning(dcBluez()) << "Could not start notifications on characteristic" << m_uuid.toString() << reply.error().name() << reply.error().message(); + + call->deleteLater(); +} + +void BluetoothGattCharacteristic::onStopNotificationFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) + qCWarning(dcBluez()) << "Could not stop notifications on characteristic" << m_uuid.toString() << reply.error().name() << reply.error().message(); + + call->deleteLater(); +} + +bool BluetoothGattCharacteristic::readCharacteristic() +{ + if (!m_characteristicInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus characteristic interface for" << m_path.path(); + return false; + } + + QDBusPendingCall readingCall = m_characteristicInterface->asyncCall("ReadValue", QVariantMap()); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(readingCall, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &BluetoothGattCharacteristic::onReadingFinished); + return true; +} + +bool BluetoothGattCharacteristic::writeCharacteristic(const QByteArray &value) +{ + if (!m_characteristicInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus characteristic interface for" << m_path.path(); + return false; + } + + QDBusPendingCall writingCall = m_characteristicInterface->asyncCall("WriteValue", value, QVariantMap()); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(writingCall, this); + m_asyncWrites.insert(watcher, value); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &BluetoothGattCharacteristic::onWritingFinished); + return true; +} + +bool BluetoothGattCharacteristic::startNotifications() +{ + if (!m_characteristicInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus characteristic interface for" << m_path.path(); + return false; + } + + // If already notifying + if (notifying()) + return true; + + QDBusPendingCall startNotifyCall = m_characteristicInterface->asyncCall("StartNotify"); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(startNotifyCall, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &BluetoothGattCharacteristic::onStartNotificationFinished); + return true; +} + +bool BluetoothGattCharacteristic::stopNotifications() +{ + if (!m_characteristicInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus characteristic interface for" << m_path.path(); + return false; + } + + // If already stopped + if (!notifying()) + return true; + + QDBusPendingCall stopNotifyCall = m_characteristicInterface->asyncCall("StopNotify"); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(stopNotifyCall, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &BluetoothGattCharacteristic::onStopNotificationFinished); + return true; +} + +QDebug operator<<(QDebug debug, BluetoothGattCharacteristic *characteristic) +{ + debug.noquote().nospace() << "GattCharacteristic(" << characteristic->chararcteristicName(); + debug.noquote().nospace() << ", " << characteristic->uuid().toString(); + debug.noquote().nospace() << ", Properties: " << characteristic->properties(); + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::Unknown)) + debug.noquote().nospace() << " Unknown"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::Broadcasting)) + debug.noquote().nospace() << " B"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::Read)) + debug.noquote().nospace() << " R "; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::WriteNoResponse)) + debug.noquote().nospace() << " WNR"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::Write)) + debug.noquote().nospace() << " W"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::Notify)) + debug.noquote().nospace() << " N"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::Indicate)) + debug.noquote().nospace() << " I"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::WriteAuthenticatedSigned)) + debug.noquote().nospace() << " WAS"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::ReliableWrite)) + debug.noquote().nospace() << " RW"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::WritableAuxiliaries)) + debug.noquote().nospace() << " WA"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::EncryptWrite)) + debug.noquote().nospace() << " EW"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::EncryptRead)) + debug.noquote().nospace() << " ER"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::EncryptAuthenticatedRead)) + debug.noquote().nospace() << " EAR"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::EncryptAuthenticatedWrite)) + debug.noquote().nospace() << " EAW"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::SecureRead)) + debug.noquote().nospace() << " SR"; + + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::SecureWrite)) + debug.noquote().nospace() << " SW"; + + // If this is a notifying characteristic, inform if notifications are enabled + if (characteristic->properties().testFlag(BluetoothGattCharacteristic::Notify)) + debug.noquote().nospace() << ", Notify: " << (characteristic->notifying() ? "ON" : "OFF"); + + debug.noquote().nospace() << ", value:" << characteristic->value(); + debug.noquote().nospace() << ") "; + return debug; +} diff --git a/nuki/bluez/bluetoothgattcharacteristic.h b/nuki/bluez/bluetoothgattcharacteristic.h new file mode 100644 index 00000000..91de433e --- /dev/null +++ b/nuki/bluez/bluetoothgattcharacteristic.h @@ -0,0 +1,132 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef BLUETOOTHGATTCHARACTERISTIC_H +#define BLUETOOTHGATTCHARACTERISTIC_H + +#include +#include +#include +#include +#include +#include +#include + +#include "blueztypes.h" +#include "bluetoothgattdescriptor.h" + +// Note: DBus documentation https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/gatt-api.txt + +class BluetoothManager; +class BluetoothGattService; + +class BluetoothGattCharacteristic : public QObject +{ + Q_OBJECT + Q_FLAGS(Properties) + + friend class BluetoothManager; + friend class BluetoothGattService; + +public: + enum Property { + Unknown = 0x00, + Broadcasting = 0x01, + Read = 0x02, + WriteNoResponse = 0x04, + Write = 0x08, + Notify = 0x10, + Indicate = 0x20, + WriteAuthenticatedSigned = 0x40, + ReliableWrite = 0x80, + WritableAuxiliaries = 0x100, + EncryptRead = 0x200, + EncryptWrite = 0x400, + EncryptAuthenticatedRead = 0x800, + EncryptAuthenticatedWrite = 0x1000, + SecureRead = 0x2000, // Server only + SecureWrite = 0x4000, // Server only + }; + Q_DECLARE_FLAGS(Properties, Property) + + QString chararcteristicName() const; + QBluetoothUuid uuid() const; + bool notifying() const; + Properties properties() const; + QByteArray value() const; + QList descriptors() const; + + +private: + explicit BluetoothGattCharacteristic(const QDBusObjectPath &path, const QVariantMap &properties, QObject *parent = 0); + + QDBusObjectPath m_path; + QDBusInterface *m_characteristicInterface; + + QString m_characteristicName; + QBluetoothUuid m_uuid; + bool m_notifying; + Properties m_properties; + QByteArray m_value; + QList m_descriptors; + + QHash m_asyncWrites; + + void processProperties(const QVariantMap &properties); + + // Methods called from BluetoothManager + void addDescriptorInternally(const QDBusObjectPath &path, const QVariantMap &properties); + bool hasDescriptor(const QDBusObjectPath &path); + BluetoothGattDescriptor *getDescriptor(const QDBusObjectPath &path); + + void setValueInternally(const QByteArray &value); + void setNotifyingInternally(const bool ¬ifying); + + Properties parsePropertyFlags(const QStringList &characteristicProperties); + +signals: + void notifyingChanged(const bool ¬ifying); + void valueChanged(const QByteArray &value); + void readingFinished(const QByteArray &value); + void writingFinished(const QByteArray &value); + +private slots: + void onPropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties); + + void onReadingFinished(QDBusPendingCallWatcher *call); + void onWritingFinished(QDBusPendingCallWatcher *call); + void onStartNotificationFinished(QDBusPendingCallWatcher *call); + void onStopNotificationFinished(QDBusPendingCallWatcher *call); + +public slots: + bool readCharacteristic(); + bool writeCharacteristic(const QByteArray &value); + bool startNotifications(); + bool stopNotifications(); + +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(BluetoothGattCharacteristic::Properties) + +QDebug operator<<(QDebug debug, BluetoothGattCharacteristic *characteristic); + +#endif // BLUETOOTHGATTCHARACTERISTIC_H diff --git a/nuki/bluez/bluetoothgattdescriptor.cpp b/nuki/bluez/bluetoothgattdescriptor.cpp new file mode 100644 index 00000000..07688140 --- /dev/null +++ b/nuki/bluez/bluetoothgattdescriptor.cpp @@ -0,0 +1,219 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "bluetoothgattdescriptor.h" + +#include + +QString BluetoothGattDescriptor::name() const +{ + bool ok = false; + quint16 typeId = m_uuid.toUInt16(&ok); + + if (ok) { + QBluetoothUuid::DescriptorType type = static_cast(typeId); + const QString name = QBluetoothUuid::descriptorToString(type); + if (!name.isEmpty()) + return name; + } + + return QString("Unknown Descriptor"); +} + +QBluetoothUuid BluetoothGattDescriptor::uuid() const +{ + return m_uuid; +} + +QByteArray BluetoothGattDescriptor::value() const +{ + return m_value; +} + +BluetoothGattDescriptor::Properties BluetoothGattDescriptor::properties() const +{ + return m_properties; +} + +BluetoothGattDescriptor::BluetoothGattDescriptor(const QDBusObjectPath &path, const QVariantMap &properties, QObject *parent) : + QObject(parent), + m_path(path) +{ + m_descriptorInterface = new QDBusInterface(orgBluez, m_path.path(), orgBluezGattDescriptor1, QDBusConnection::systemBus(), this); + if (!m_descriptorInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus descriptor interface for" << m_path.path(); + return; + } + + QDBusConnection::systemBus().connect(orgBluez, m_path.path(), "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onPropertiesChanged(QString,QVariantMap,QStringList))); + + processProperties(properties); +} + +void BluetoothGattDescriptor::processProperties(const QVariantMap &properties) +{ + foreach (const QString &propertyName, properties.keys()) { + if (propertyName == "UUID") { + m_uuid = QBluetoothUuid(properties.value(propertyName).toString()); + } else if (propertyName == "Value") { + setValueInternally(properties.value(propertyName).toByteArray()); + } else if (propertyName == "Flags") { + m_properties = parsePropertyFlags(properties.value(propertyName).toStringList()); + } + } +} + +void BluetoothGattDescriptor::setValueInternally(const QByteArray &value) +{ + if (m_value != value) { + m_value = value; + emit valueChanged(m_value); + } +} + +BluetoothGattDescriptor::Properties BluetoothGattDescriptor::parsePropertyFlags(const QStringList &descriptorProperties) +{ + Properties properties; + + foreach (const QString &propertyString, descriptorProperties) { + if (propertyString == "read") { + properties |= Read; + } else if (propertyString == "write") { + properties |= Write; + } else if (propertyString == "encrypt-read") { + properties |= EncryptRead; + } else if (propertyString == "encrypt-write") { + properties |= EncryptWrite; + } else if (propertyString == "encrypt-authenticated-read") { + properties |= EncryptAuthenticatedRead; + } else if (propertyString == "encrypt-authenticated-write") { + properties |= EncryptAuthenticatedWrite; + } else if (propertyString == "secure-read") { + properties |= SecureRead; + } else if (propertyString == "secure-write") { + properties |= SecureWrite; + } + } + + return properties; +} + +void BluetoothGattDescriptor::onPropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties) +{ + if (interface != orgBluezGattDescriptor1) + return; + + qCDebug(dcBluez()) << "BluetoothDescriptor:" << m_uuid << "properties changed" << interface << changedProperties << invalidatedProperties; + processProperties(changedProperties); +} + +void BluetoothGattDescriptor::onReadingFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) { + qCWarning(dcBluez()) << "Could not read descriptor" << m_uuid.toString() << reply.error().name() << reply.error().message(); + } else { + QByteArray value = reply.argumentAt<0>(); + qCDebug(dcBluez()) << "Async descriptor reading finished for" << m_uuid.toString() << value; + setValueInternally(value); + emit readingFinished(value); + } + + call->deleteLater(); +} + +void BluetoothGattDescriptor::onWritingFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) { + qCWarning(dcBluez()) << "Could not write descriptor" << m_uuid.toString() << reply.error().name() << reply.error().message(); + } else { + QByteArray value = m_asyncWrites.take(call); + qCDebug(dcBluez()) << "Async descriptor writing finished for" << m_uuid.toString() << value; + emit writingFinished(value); + } + + call->deleteLater(); +} + +bool BluetoothGattDescriptor::readValue() +{ + if (!m_descriptorInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus characteristic interface for" << m_path.path(); + return false; + } + + QDBusPendingCall readingCall = m_descriptorInterface->asyncCall("ReadValue", QVariantMap()); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(readingCall, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &BluetoothGattDescriptor::onReadingFinished); + return true; +} + +bool BluetoothGattDescriptor::writeValue(const QByteArray &value) +{ + if (!m_descriptorInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus characteristic interface for" << m_path.path(); + return false; + } + + QDBusPendingCall writingCall = m_descriptorInterface->asyncCall("WriteValue", value, QVariantMap()); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(writingCall, this); + m_asyncWrites.insert(watcher, value); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &BluetoothGattDescriptor::onWritingFinished); + return true; +} + +QDebug operator<<(QDebug debug, BluetoothGattDescriptor *descriptor) +{ + debug.noquote().nospace() << "GattDescriptor(" << descriptor->name(); + debug.noquote().nospace() << ", " << descriptor->uuid().toString(); + debug.noquote().nospace() << ", Properties: "; + + if (descriptor->properties().testFlag(BluetoothGattDescriptor::Read)) + debug.noquote().nospace() << " R "; + + if (descriptor->properties().testFlag(BluetoothGattDescriptor::Write)) + debug.noquote().nospace() << " W"; + + if (descriptor->properties().testFlag(BluetoothGattDescriptor::EncryptRead)) + debug.noquote().nospace() << " ER"; + + if (descriptor->properties().testFlag(BluetoothGattDescriptor::EncryptWrite)) + debug.noquote().nospace() << " EW"; + + if (descriptor->properties().testFlag(BluetoothGattDescriptor::EncryptAuthenticatedRead)) + debug.noquote().nospace() << " EAR"; + + if (descriptor->properties().testFlag(BluetoothGattDescriptor::EncryptAuthenticatedWrite)) + debug.noquote().nospace() << " EAW"; + + + if (descriptor->properties().testFlag(BluetoothGattDescriptor::SecureRead)) + debug.noquote().nospace() << " SR"; + + if (descriptor->properties().testFlag(BluetoothGattDescriptor::SecureWrite)) + debug.noquote().nospace() << " SW"; + + debug.noquote().nospace() << ", value: " << descriptor->value(); + debug.noquote().nospace() << ") "; + return debug; +} diff --git a/nuki/bluez/bluetoothgattdescriptor.h b/nuki/bluez/bluetoothgattdescriptor.h new file mode 100644 index 00000000..f51a142d --- /dev/null +++ b/nuki/bluez/bluetoothgattdescriptor.h @@ -0,0 +1,104 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef BLUETOOTHGATTDESCRIPTOR_H +#define BLUETOOTHGATTDESCRIPTOR_H + +#include +#include +#include +#include +#include + +#include "blueztypes.h" + +// Note: DBus documentation https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/gatt-api.txt + +class BluetoothManager; +class BluetoothGattCharacteristic; + +class BluetoothGattDescriptor : public QObject +{ + Q_OBJECT + Q_FLAGS(Properties) + + friend class BluetoothManager; + friend class BluetoothGattCharacteristic; + +public: + enum Property { + Unknown = 0x00, + Read = 0x01, + Write = 0x02, + EncryptRead = 0x04, + EncryptWrite = 0x08, + EncryptAuthenticatedRead = 0x10, + EncryptAuthenticatedWrite = 0x20, + SecureRead = 0x40, // Server only + SecureWrite = 0x80 // Server only + }; + Q_DECLARE_FLAGS(Properties, Property) + + QString name() const; + QBluetoothUuid uuid() const; + QByteArray value() const; + Properties properties() const; + +private: + explicit BluetoothGattDescriptor(const QDBusObjectPath &path, const QVariantMap &properties, QObject *parent = 0); + + QDBusObjectPath m_path; + QDBusInterface *m_descriptorInterface; + + QBluetoothUuid m_uuid; + QByteArray m_value; + Properties m_properties; + QHash m_asyncWrites; + + void processProperties(const QVariantMap &properties); + + void setValueInternally(const QByteArray &value); + + Properties parsePropertyFlags(const QStringList &descriptorProperties); + +signals: + void valueChanged(const QByteArray &value); + void readingFinished(const QByteArray &value); + void writingFinished(const QByteArray &value); + +private slots: + void onPropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties); + + void onReadingFinished(QDBusPendingCallWatcher *call); + void onWritingFinished(QDBusPendingCallWatcher *call); + +public slots: + bool readValue(); + bool writeValue(const QByteArray &value); +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(BluetoothGattDescriptor::Properties) + +QDebug operator<<(QDebug debug, BluetoothGattDescriptor *descriptor); + + +#endif // BLUETOOTHGATTDESCRIPTOR_H diff --git a/nuki/bluez/bluetoothgattservice.cpp b/nuki/bluez/bluetoothgattservice.cpp new file mode 100644 index 00000000..5f46b9f4 --- /dev/null +++ b/nuki/bluez/bluetoothgattservice.cpp @@ -0,0 +1,168 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "bluetoothgattservice.h" + +QString BluetoothGattService::serviceName() const +{ + bool ok = false; + quint16 typeId = m_uuid.toUInt16(&ok); + + if (ok) { + QBluetoothUuid::ServiceClassUuid uuid = static_cast(typeId); + const QString name = QBluetoothUuid::serviceClassToString(uuid); + if (!name.isEmpty()) + return name; + } + + return QString("Unknown Service"); +} + +BluetoothGattService::Type BluetoothGattService::type() const +{ + return m_type; +} + +QBluetoothUuid BluetoothGattService::uuid() const +{ + return m_uuid; +} + +QList BluetoothGattService::characteristics() const +{ + return m_characteristics; +} + +bool BluetoothGattService::hasCharacteristic(const QBluetoothUuid &characteristicUuid) +{ + foreach (BluetoothGattCharacteristic *characteristic, m_characteristics) { + if (characteristic->uuid() == characteristicUuid) { + return true; + } + } + + return false; +} + +BluetoothGattCharacteristic *BluetoothGattService::getCharacteristic(const QBluetoothUuid &characteristicUuid) +{ + foreach (BluetoothGattCharacteristic *characteristic, m_characteristics) { + if (characteristic->uuid() == characteristicUuid) { + return characteristic; + } + } + + return nullptr; +} + +BluetoothGattService::BluetoothGattService(const QDBusObjectPath &path, const QVariantMap &properties, QObject *parent) : + QObject(parent), + m_path(path), + m_type(Primary), + m_discovered(false) +{ + processProperties(properties); +} + +void BluetoothGattService::processProperties(const QVariantMap &properties) +{ + foreach (const QString &propertyName, properties.keys()) { + if (propertyName == "Primary") { + m_type = (properties.value(propertyName).toBool() ? Primary : Secondary); + } else if (propertyName == "UUID") { + m_uuid = QBluetoothUuid(properties.value(propertyName).toString()); + } + } +} + +void BluetoothGattService::addCharacteristicInternally(const QDBusObjectPath &path, const QVariantMap &properties) +{ + if (hasCharacteristic(path)) + return; + + BluetoothGattCharacteristic *characteristic = new BluetoothGattCharacteristic(path, properties, this); + m_characteristics.append(characteristic); + + connect(characteristic, &BluetoothGattCharacteristic::readingFinished, this, &BluetoothGattService::onCharacteristicReadFinished); + connect(characteristic, &BluetoothGattCharacteristic::writingFinished, this, &BluetoothGattService::onCharacteristicReadFinished); + connect(characteristic, &BluetoothGattCharacteristic::valueChanged, this, &BluetoothGattService::onCharacteristicValueChanged); + + qCDebug(dcBluez()) << "[+]" << characteristic; +} + +bool BluetoothGattService::hasCharacteristic(const QDBusObjectPath &path) +{ + foreach (BluetoothGattCharacteristic *characteristic, m_characteristics) { + if (characteristic->m_path == path) { + return true; + } + } + + return false; +} + +BluetoothGattCharacteristic *BluetoothGattService::getCharacteristic(const QDBusObjectPath &path) +{ + foreach (BluetoothGattCharacteristic *characteristic, m_characteristics) { + if (characteristic->m_path == path) { + return characteristic; + } + } + + return nullptr; +} + +void BluetoothGattService::onCharacteristicReadFinished(const QByteArray &value) +{ + BluetoothGattCharacteristic *characteristic = static_cast(sender()); + emit characteristicReadFinished(characteristic, value); +} + +void BluetoothGattService::onCharacteristicWriteFinished(const QByteArray &value) +{ + BluetoothGattCharacteristic *characteristic = static_cast(sender()); + emit characteristicWriteFinished(characteristic, value); +} + +void BluetoothGattService::onCharacteristicValueChanged(const QByteArray &newValue) +{ + BluetoothGattCharacteristic *characteristic = static_cast(sender()); + emit characteristicChanged(characteristic, newValue); +} + +bool BluetoothGattService::readCharacteristic(const QBluetoothUuid &characteristicUuid) +{ + if (!hasCharacteristic(characteristicUuid)) + return false; + + return getCharacteristic(characteristicUuid)->readCharacteristic(); +} + +QDebug operator<<(QDebug debug, BluetoothGattService *service) +{ + debug.noquote().nospace() << "GattService(" << (service->type() == BluetoothGattService::Primary ? "Primary" : "Secondary"); + debug.noquote().nospace() << ", " << service->serviceName(); + debug.noquote().nospace() << ", " << service->uuid().toString(); + debug.noquote().nospace() << ") "; + + return debug; +} diff --git a/nuki/bluez/bluetoothgattservice.h b/nuki/bluez/bluetoothgattservice.h new file mode 100644 index 00000000..eefcfa05 --- /dev/null +++ b/nuki/bluez/bluetoothgattservice.h @@ -0,0 +1,96 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef BLUETOOTHGATTSERVICE_H +#define BLUETOOTHGATTSERVICE_H + +#include +#include + +#include "blueztypes.h" +#include "bluetoothgattcharacteristic.h" + +// Note: DBus documentation https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/gatt-api.txt + +class BluetoothManager; +class BluetoothDevice; + +class BluetoothGattService : public QObject +{ + Q_OBJECT + + friend class BluetoothManager; + friend class BluetoothDevice; + +public: + enum Type { + Primary, + Secondary + }; + Q_ENUM(Type) + + QString serviceName() const; + Type type() const; + QBluetoothUuid uuid() const; + + // Characteristic methods + QList characteristics() const; + bool hasCharacteristic(const QBluetoothUuid &characteristicUuid); + BluetoothGattCharacteristic *getCharacteristic(const QBluetoothUuid &characteristicUuid); + +private: + explicit BluetoothGattService(const QDBusObjectPath &path, const QVariantMap &properties, QObject *parent = 0); + + QDBusObjectPath m_path; + + Type m_type; + QBluetoothUuid m_uuid; + QList m_characteristics; + + bool m_discovered; + + void processProperties(const QVariantMap &properties); + + // Methods called from BluetoothManager + void addCharacteristicInternally(const QDBusObjectPath &path, const QVariantMap &properties); + bool hasCharacteristic(const QDBusObjectPath &path); + BluetoothGattCharacteristic *getCharacteristic(const QDBusObjectPath &path); + +private slots: + void onCharacteristicReadFinished(const QByteArray &value); + void onCharacteristicWriteFinished(const QByteArray &value); + void onCharacteristicValueChanged(const QByteArray &newValue); + +signals: + void characteristicReadFinished(BluetoothGattCharacteristic *characteristic, const QByteArray &value); + void characteristicWriteFinished(BluetoothGattCharacteristic *characteristic, const QByteArray &value); + void characteristicChanged(BluetoothGattCharacteristic *characteristic, const QByteArray &newValue); + +public slots: + bool readCharacteristic(const QBluetoothUuid &characteristicUuid); + +}; + +QDebug operator<<(QDebug debug, BluetoothGattService *service); + + +#endif // BLUETOOTHGATTSERVICE_H diff --git a/nuki/bluez/bluetoothmanager.cpp b/nuki/bluez/bluetoothmanager.cpp new file mode 100644 index 00000000..c0e72399 --- /dev/null +++ b/nuki/bluez/bluetoothmanager.cpp @@ -0,0 +1,306 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "bluetoothmanager.h" + +#include +#include +#include + +BluetoothManager::BluetoothManager(QObject *parent) : + QObject(parent), + m_available(false) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + // Check DBus connection + if (!QDBusConnection::systemBus().isConnected()) { + qCWarning(dcBluez()) << "System DBus not connected."; + return; + } + + // Get notification when bluez appears/disappears on DBus + m_serviceWatcher = new QDBusServiceWatcher(orgBluez, QDBusConnection::systemBus(), QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this); + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &BluetoothManager::serviceRegistered); + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &BluetoothManager::serviceUnregistered); + + m_objectManagerInterface = new QDBusInterface(orgBluez, "/", orgFreedesktopDBusObjectManager, QDBusConnection::systemBus(), this); + if (!m_objectManagerInterface->isValid()) { + qCWarning(dcBluez()) << "Invalid DBus ObjectManager interface."; + return; + } + + QDBusConnection::systemBus().connect(orgBluez, "/", orgFreedesktopDBusObjectManager, "InterfacesAdded", this, SLOT(onInterfaceAdded(QDBusObjectPath,InterfaceList))); + QDBusConnection::systemBus().connect(orgBluez, "/", orgFreedesktopDBusObjectManager, "InterfacesRemoved", this, SLOT(onInterfaceRemoved(QDBusObjectPath,QStringList))); + + init(); +} + +QList BluetoothManager::adapters() const +{ + return m_adapters; +} + +bool BluetoothManager::isAvailable() const +{ + return m_available; +} + +void BluetoothManager::init() +{ + // Get current object from org.bluez + QDBusMessage query = m_objectManagerInterface->call("GetManagedObjects"); + if(query.type() != QDBusMessage::ReplyMessage) { + qCWarning(dcBluez()) << "Could not initialize BluetoothManager:" << query.errorName() << query.errorMessage(); + return; + } + + const QDBusArgument &argument = query.arguments().at(0).value(); + ManagedObjectList objectList = qdbus_cast(argument); + processObjectList(objectList); + + if (!m_adapters.isEmpty()) + setAvailable(true); + + qCDebug(dcBluez()) << "BluetoothManager initialized successfully."; +} + +void BluetoothManager::clean() +{ + // Delete all adapter objects + // Note: devices, services and characteristic objects will be removed throug parent relation + foreach (BluetoothAdapter *adapter, m_adapters) { + m_adapters.removeOne(adapter); + emit adapterRemoved(adapter); + adapter->deleteLater(); + } + + m_adapters.clear(); + + setAvailable(false); +} + +void BluetoothManager::setAvailable(const bool &available) +{ + if (m_available != available) { + m_available = available; + emit availableChanged(m_available); + } +} + +void BluetoothManager::processObjectList(const ManagedObjectList &objectList) +{ + foreach (const QDBusObjectPath &objectPath, objectList.keys()) { + InterfaceList interfaceList = objectList.value(objectPath); + processInterfaceList(objectPath, interfaceList); + } +} + +void BluetoothManager::processInterfaceList(const QDBusObjectPath &objectPath, const InterfaceList &interfaceList) +{ + // Note: object hierarchy: first add adapters, than devices, services, characteristics and finally descriptors + + // Adapter interface + foreach (const QString &interface, interfaceList.keys()) { + if (interface == orgBluezAdapter1) { + QVariantMap properties = interfaceList.value(interface); + // Check if this adapter already added + if (!adapterAlreadyAdded(objectPath)) { + BluetoothAdapter *adapter = new BluetoothAdapter(objectPath, properties, this); + m_adapters.append(adapter); + emit adapterAdded(adapter); + qCDebug(dcBluez()) << "[+]" << adapter; + } + } + } + + // Device interface + foreach (const QString &interface, interfaceList.keys()) { + if (interface == orgBluezDevice1) { + QVariantMap properties = interfaceList.value(interface); + // Find adapter for this device and add the device internally + if (properties.contains("Adapter")) { + QDBusObjectPath adapterObjectPath = qvariant_cast(properties.value("Adapter")); + BluetoothAdapter *adapter = findAdapter(adapterObjectPath); + if (adapter) + adapter->addDeviceInternally(objectPath, properties); + + } + } + } + + // GATT Service interface + foreach (const QString &interface, interfaceList.keys()) { + if (interface == orgBluezGattService1) { + QVariantMap properties = interfaceList.value(interface); + // Find device for this service and add the service internally + if (properties.contains("Device")) { + QDBusObjectPath deviceObjectPath = qvariant_cast(properties.value("Device")); + BluetoothDevice *device = findDevice(deviceObjectPath); + if (device) + device->addServiceInternally(objectPath, properties); + + } + } + } + + // GATT Characteristic interface + foreach (const QString &interface, interfaceList.keys()) { + if (interface == orgBluezGattCharacteristic1) { + QVariantMap properties = interfaceList.value(interface); + // Find service for this characteristic + if (properties.contains("Service")) { + QDBusObjectPath serviceObjectPath = qvariant_cast(properties.value("Service")); + BluetoothGattService *service = findService(serviceObjectPath); + if (service) + service->addCharacteristicInternally(objectPath, properties); + + } + } + } + + // GATT Descriptor interface + foreach (const QString &interface, interfaceList.keys()) { + if (interface == orgBluezGattDescriptor1) { + QVariantMap properties = interfaceList.value(interface); + // Find characteristic for this desciptor + if (properties.contains("Characteristic")) { + QDBusObjectPath characterisitcObjectPath = qvariant_cast(properties.value("Characteristic")); + BluetoothGattCharacteristic *characteristic = findCharacteristic(characterisitcObjectPath); + if (characteristic) + characteristic->addDescriptorInternally(objectPath, properties); + + } + } + } + +} + +bool BluetoothManager::adapterAlreadyAdded(const QDBusObjectPath &objectPath) +{ + foreach (BluetoothAdapter *existingAdapter, m_adapters) { + if (existingAdapter->m_path == objectPath) { + return true; + } + } + + return false; +} + +BluetoothAdapter *BluetoothManager::findAdapter(const QDBusObjectPath &objectPath) +{ + foreach (BluetoothAdapter *adapter, m_adapters) { + if (adapter->m_path == objectPath) { + return adapter; + } + } + + return nullptr; +} + +BluetoothDevice *BluetoothManager::findDevice(const QDBusObjectPath &objectPath) +{ + foreach (BluetoothAdapter *adapter, m_adapters) { + foreach (BluetoothDevice *device, adapter->devices()) { + if (device->m_path == objectPath) { + return device; + } + } + } + + return nullptr; +} + +BluetoothGattService *BluetoothManager::findService(const QDBusObjectPath &objectPath) +{ + foreach (BluetoothAdapter *adapter, m_adapters) { + foreach (BluetoothDevice *device, adapter->devices()) { + if (device->hasService(objectPath)) { + return device->getService(objectPath); + } + } + } + + return nullptr; +} + +BluetoothGattCharacteristic *BluetoothManager::findCharacteristic(const QDBusObjectPath &objectPath) +{ + foreach (BluetoothAdapter *adapter, m_adapters) { + foreach (BluetoothDevice *device, adapter->devices()) { + foreach (BluetoothGattService *service, device->services()) { + if (service->hasCharacteristic(objectPath)) { + return service->getCharacteristic(objectPath); + } + } + } + } + + return nullptr; +} + +void BluetoothManager::serviceRegistered(const QString &serviceName) +{ + qCDebug(dcBluez()) << "BluetoothManager: service registered" << serviceName; + init(); +} + +void BluetoothManager::serviceUnregistered(const QString &serviceName) +{ + qCDebug(dcBluez()) << "BluetoothManager: service unregistered" << serviceName; + if (serviceName == orgBluez) + clean(); +} + +void BluetoothManager::onInterfaceAdded(const QDBusObjectPath &objectPath, const InterfaceList &interfaceList) +{ + //qCDebug(dcBluez()) << "Interface added" << objectPath.path(); + processInterfaceList(objectPath, interfaceList); +} + +void BluetoothManager::onInterfaceRemoved(const QDBusObjectPath &objectPath, const QStringList &interfaces) +{ + //qCDebug(dcBluez()) << "Interface removed" << objectPath.path() << interfaces; + + // Adapter removed + if (interfaces.contains(orgBluezAdapter1)) { + BluetoothAdapter *adapter = findAdapter(objectPath); + qCDebug(dcBluez()) << "[-]" << adapter; + if (adapter) { + m_adapters.removeOne(adapter); + emit adapterRemoved(adapter); + adapter->deleteLater(); + } + } + + // Device removed + if (interfaces.contains(orgBluezDevice1)) { + // Find adapter for this device + foreach (BluetoothAdapter *adapter, m_adapters) { + if (adapter->hasDevice(objectPath)) { + adapter->removeDeviceInternally(objectPath); + } + } + } + +} diff --git a/nuki/bluez/bluetoothmanager.h b/nuki/bluez/bluetoothmanager.h new file mode 100644 index 00000000..752a9ec6 --- /dev/null +++ b/nuki/bluez/bluetoothmanager.h @@ -0,0 +1,85 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef BLUETOOTHMANAGER_H +#define BLUETOOTHMANAGER_H + +#include +#include +#include +#include + +#include "blueztypes.h" +#include "bluetoothadapter.h" + +class BluetoothManager : public QObject +{ + Q_OBJECT +public: + explicit BluetoothManager(QObject *parent = 0); + + QList adapters() const; + + bool isAvailable() const; + +private: + QDBusInterface *m_objectManagerInterface; + QDBusServiceWatcher *m_serviceWatcher; + + QList m_adapters; + + bool m_available; + + void init(); + void clean(); + + void setAvailable(const bool &available); + + // DBus object helpers + void processObjectList(const ManagedObjectList &objectList); + void processInterfaceList(const QDBusObjectPath &objectPath, const InterfaceList &interfaceList); + + bool adapterAlreadyAdded(const QDBusObjectPath &objectPath); + + BluetoothAdapter *findAdapter(const QDBusObjectPath &objectPath); + BluetoothDevice *findDevice(const QDBusObjectPath &objectPath); + BluetoothGattService *findService(const QDBusObjectPath &objectPath); + BluetoothGattCharacteristic *findCharacteristic(const QDBusObjectPath &objectPath); + +signals: + void availableChanged(const bool &available); + + void adapterAdded(BluetoothAdapter *adapter); + void adapterRemoved(BluetoothAdapter *adapter); + +private slots: + void serviceRegistered(const QString &serviceName); + void serviceUnregistered(const QString &serviceName); + + void onInterfaceAdded(const QDBusObjectPath &objectPath, const InterfaceList &interfaceList); + void onInterfaceRemoved(const QDBusObjectPath &objectPath, const QStringList &interfaces); + +public slots: + +}; + +#endif // BLUETOOTHMANAGER_H diff --git a/nuki/bluez/blueztypes.cpp b/nuki/bluez/blueztypes.cpp new file mode 100644 index 00000000..8e103db3 --- /dev/null +++ b/nuki/bluez/blueztypes.cpp @@ -0,0 +1,32 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "blueztypes.h" + +Q_LOGGING_CATEGORY(dcBluez, "Bluez") + +Bluez::Bluez() +{ + +} + + diff --git a/nuki/bluez/blueztypes.h b/nuki/bluez/blueztypes.h new file mode 100644 index 00000000..0f846b0f --- /dev/null +++ b/nuki/bluez/blueztypes.h @@ -0,0 +1,103 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016-2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef BLUEZTYPES_H +#define BLUEZTYPES_H + +#include +#include +#include +#include +#include + +// Interfaces DBus +static const QString orgFreedesktopDBus = QStringLiteral("org.freedesktop.DBus"); +static const QString orgFreedesktopDBusObjectManager = QStringLiteral("org.freedesktop.DBus.ObjectManager"); + +// Interfaces Bluez +static const QString orgBluez = QStringLiteral("org.bluez"); +static const QString orgBluezAdapter1 = QStringLiteral("org.bluez.Adapter1"); +static const QString orgBluezDevice1 = QStringLiteral("org.bluez.Device1"); +static const QString orgBluezGattService1 = QStringLiteral("org.bluez.GattService1"); +static const QString orgBluezGattCharacteristic1 = QStringLiteral("org.bluez.GattCharacteristic1"); +static const QString orgBluezGattDescriptor1 = QStringLiteral("org.bluez.GattDescriptor1"); + +// DBus Object and interface types +typedef QMap InterfaceList; +Q_DECLARE_METATYPE(InterfaceList) + +typedef QMap ManagedObjectList; +Q_DECLARE_METATYPE(ManagedObjectList) + +typedef struct { + quint16 manufacturerId; + QByteArray data; +} ManufacturerData; + +Q_DECLARE_METATYPE(ManufacturerData) + +typedef QList ManufacturerDataList; +Q_DECLARE_METATYPE(ManufacturerDataList) + +// TODO: get naufacturer data +//QDBusArgument &operator<<(QDBusArgument &argument, const ManufacturerDataList &manufacturerDataList); + +// Logging cathegory +Q_DECLARE_LOGGING_CATEGORY(dcBluez) + +// Gadget class fot handling types +class Bluez { + + Q_GADGET + Q_ENUMS(Error) + +public: + enum Error { + NoError, + NotReady, + Failed, + Rejected, + Canceled, + InvalidArguments, + AlreadyExists, + DoesNotExist, + InProgress, + NotInProgress, + AlreadyConnected, + ConnectFailed, + NotConnected, + NotSupported, + NotAuthorized, + AuthenticationCanceled, + AuthenticationFailed, + AuthenticationRejected, + AuthenticationTimeout, + ConnectionAttemptFailed, + DBusError, + UnknownError + }; + Q_ENUM(Error) + + Bluez(); +}; + +#endif // BLUEZTYPES_H diff --git a/nuki/devicepluginnuki.cpp b/nuki/devicepluginnuki.cpp new file mode 100644 index 00000000..5c95848e --- /dev/null +++ b/nuki/devicepluginnuki.cpp @@ -0,0 +1,325 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 Bernhard Trinnes * + * Copyright (C) 2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "devicepluginnuki.h" +#include "devices/device.h" +#include "plugininfo.h" +#include "hardware/bluetoothlowenergy/bluetoothlowenergymanager.h" + +extern "C"{ +#include "sodium.h" +} + +DevicePluginNuki::DevicePluginNuki() +{ + +} + +DevicePluginNuki::~DevicePluginNuki() +{ + hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer); +} + +void DevicePluginNuki::init() +{ + // Read every hour the state of the lock + m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(3600); + connect(m_refreshTimer, &PluginTimer::timeout, this, &DevicePluginNuki::onRefreshTimeout); + + // Bluetooth manager for BTLE bluez handling + m_bluetoothManager = new BluetoothManager(this); + if (!m_bluetoothManager->isAvailable()) { + qCWarning(dcNuki()) << "Bluetooth not available"; + return; + } + + if (m_bluetoothManager->adapters().isEmpty()) { + qCWarning(dcNuki()) << "No bluetooth adapter found."; + return; + } + + m_bluetoothAdapter = m_bluetoothManager->adapters().first(); + m_bluetoothAdapter->setPower(true); + m_bluetoothAdapter->setDiscoverable(true); + m_bluetoothAdapter->setPairable(true); + + qCDebug(dcNuki()) << "Using bluetooth adapter" << m_bluetoothAdapter; + + if (sodium_init() < 0) { + qCCritical(dcNuki()) << "Could not initialize encryption library sodium"; + m_encrytionLibraryInitialized = false; + return; + } + + m_encrytionLibraryInitialized = true; + qCDebug(dcNuki()) << "Encryption library initialized successfully: libsodium" << sodium_version_string(); +} + +void DevicePluginNuki::setupDevice(DeviceSetupInfo *info) +{ + Device *device = info->device(); + qCDebug(dcNuki()) << "Setup device" << device->name() << device->params(); + + QBluetoothAddress address = QBluetoothAddress(device->params().paramValue(nukiDeviceMacParamTypeId).toString()); + if (bluetoothDeviceAlreadyAdded(address)) { + qCWarning(dcNuki()) << "Device already added."; + return info->finish(Device::DeviceErrorDeviceInUse, QT_TR_NOOP("Device is already in use.")); + } + + if (!m_bluetoothAdapter){ + qCWarning(dcNuki()) << "No bluetooth adapter available"; + return info->finish(Device::DeviceErrorHardwareNotAvailable, QT_TR_NOOP("Bluetooth is not available on this system.")); + } + + if (m_bluetoothAdapter->hasDevice(address)) { + Nuki *nuki = new Nuki(device, m_bluetoothAdapter->getDevice(address), this); + m_nukiDevices.insert(nuki, device); + } else { + qCWarning(dcNuki()) << "Could not find bluetooth device for setup" << address; + return info->finish(Device::DeviceErrorHardwareNotAvailable, QT_TR_NOOP("Bluetooth device not found.")); + } + + info->finish(Device::DeviceErrorNoError); +} + +void DevicePluginNuki::discoverDevices(DeviceDiscoveryInfo *info) +{ + + if (info->deviceClassId() != nukiDeviceClassId) + return info->finish(Device::DeviceErrorDeviceClassNotFound); + + if (!hardwareManager()->bluetoothLowEnergyManager()->enabled()) + return info->finish(Device::DeviceErrorHardwareNotAvailable, QT_TR_NOOP("Bluetooth is not available on this system.")); + + if (!m_bluetoothAdapter) + return info->finish(Device::DeviceErrorHardwareNotAvailable, QT_TR_NOOP("Bluetooth is not available on this system.")); + + m_bluetoothAdapter->setDiscoverable(true); + m_bluetoothAdapter->setPairable(true); + + qCDebug(dcNuki()) << "Start bluetooth discovery..."; + if (!m_bluetoothAdapter->discovering()) + m_bluetoothAdapter->startDiscovering(); + + QTimer::singleShot(5000, info, [this, info]() { onBluetoothDiscoveryFinished(info); }); + +} + +void DevicePluginNuki::startPairing(DevicePairingInfo *info) +{ + info->finish(Device::DeviceErrorNoError, QT_TR_NOOP("Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue.")); +} + +void DevicePluginNuki::confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret) +{ + Q_UNUSED(username) + Q_UNUSED(secret) + + qCDebug(dcNuki()) << "Pairing confirmed, assuming the pairing mode is active. Start authentication process"; + if (info->deviceClassId() != nukiDeviceClassId) { + qCWarning(dcNuki()) << "Invalid device class id"; + return info->finish(Device::DeviceErrorDeviceClassNotFound); + } + + if (m_asyncSetupNuki) { + qCWarning(dcNuki()) << "There is already an async setup for a nuki running."; + return info->finish(Device::DeviceErrorDeviceInUse); + } + + QBluetoothAddress address = QBluetoothAddress(info->params().paramValue(nukiDeviceMacParamTypeId).toString()); + if (!m_bluetoothAdapter->hasDevice(address)) { + qCWarning(dcNuki()) << "Could not find bluetooth device for" << address.toString(); + return info->finish(Device::DeviceErrorDeviceNotFound); + } + + BluetoothDevice *bluetoothDevice = m_bluetoothAdapter->getDevice(address); + + m_asyncSetupNuki = new Nuki(nullptr, bluetoothDevice, this); + connect(m_asyncSetupNuki, &Nuki::authenticationProcessFinished, this, &DevicePluginNuki::onNukiAuthenticationProcessFinished); + connect(m_asyncSetupNuki, &Nuki::availableChanged, this, &DevicePluginNuki::onAsyncSetupNukiAvailableChanged); + + m_asyncSetupNuki->startAuthenticationProcess(info->transactionId()); + m_pairingInfo = info; + connect(info, &DevicePairingInfo::destroyed, this, [this] { m_pairingInfo = nullptr; }); +} + +void DevicePluginNuki::postSetupDevice(Device *device) +{ + Nuki *nuki = m_nukiDevices.key(device); + nuki->refreshStates(); +} + +void DevicePluginNuki::executeAction(DeviceActionInfo *info) +{ + Device *device = info->device(); + Action action = info->action(); + + QPointer nuki = m_nukiDevices.key(device); + if (nuki.isNull()) { + qCWarning(dcNuki()) << "Could not execute action. There is no nuki object for this device"; + return info->finish(Device::DeviceErrorHardwareFailure); + } + + if (!hardwareManager()->bluetoothLowEnergyManager()->enabled()) { + qCWarning(dcNuki()) << "Could not execute action. There bluetooth hardware resource is disabled."; + return info->finish(Device::DeviceErrorHardwareNotAvailable); + } + + if (action.actionTypeId() == nukiCloseActionTypeId) { + if (!nuki->executeDeviceAction(Nuki::NukiActionLock, info)) { + return info->finish(Device::DeviceErrorDeviceInUse); + } + return; + } else if (action.actionTypeId() == nukiOpenActionTypeId) { + if (!nuki->executeDeviceAction(Nuki::NukiActionUnlock, info)) { + return info->finish(Device::DeviceErrorDeviceInUse); + } + return; + } else if (action.actionTypeId() == nukiUnlatchActionTypeId) { + if (!nuki->executeDeviceAction(Nuki::NukiActionUnlatch, info)) { + return info->finish(Device::DeviceErrorDeviceInUse); + } + return; + } else if (action.actionTypeId() == nukiRefreshActionTypeId) { + if (!nuki->executeDeviceAction(Nuki::NukiActionRefresh, info)) { + return info->finish(Device::DeviceErrorDeviceInUse); + } + return; + } + + info->finish(Device::DeviceErrorActionTypeNotFound); +} + +void DevicePluginNuki::deviceRemoved(Device *device) +{ + if (!m_nukiDevices.values().contains(device)) + return; + + Nuki *nuki = m_nukiDevices.key(device); + nuki->clearSettings(); + + // FIXME: deauthenticate nymea from nuki device + + qCDebug(dcNuki()) << "Delete pairing information from bluez" << nuki->bluetoothDevice(); + m_bluetoothAdapter->removeDevice(nuki->bluetoothDevice()->address()); + m_nukiDevices.remove(nuki); + nuki->deleteLater(); +} + +bool DevicePluginNuki::bluetoothDeviceAlreadyAdded(const QBluetoothAddress &address) +{ + foreach (Device *device, m_nukiDevices.values()) { + if (device->deviceClassId() == nukiDeviceClassId && device->paramValue(nukiDeviceMacParamTypeId).toString() == address.toString()) { + qCDebug(dcNuki()) << "Nuki with address" << address.toString() << "already added."; + return true; + } + } + + return false; +} + +void DevicePluginNuki::onRefreshTimeout() +{ + // Only reconnect if the hardware resource is enabled + if (hardwareManager()->bluetoothLowEnergyManager()->enabled()) { + foreach (Nuki *nuki, m_nukiDevices.keys()) { + nuki->refreshStates(); + } + } +} + +void DevicePluginNuki::onBluetoothEnabledChanged(const bool &enabled) +{ + qCDebug(dcNuki()) << "Bluetooth hardware resource" << (enabled ? "enabled" : "disabled"); + + // Disconnect all devices, autoconnect will not trigger until the resource is enabled again + foreach (Nuki *nuki, m_nukiDevices.keys()) { + if (!enabled) { + nuki->disconnectDevice(); + } else { + nuki->connectDevice(); + } + } +} + +void DevicePluginNuki::onBluetoothDiscoveryFinished(DeviceDiscoveryInfo *info) +{ + qCDebug(dcNuki()) << "Bluetooth discovery for nuki devices finished"; + m_bluetoothAdapter->stopDiscovering(); + + foreach (BluetoothDevice *device, m_bluetoothAdapter->devices()) { + if (!bluetoothDeviceAlreadyAdded(device->address()) && device->name().contains("Nuki")) { + DeviceDescriptor descriptor(nukiDeviceClassId, "Nuki", device->address().toString()); + + // Get serial number from name + QString serialNumber; + QStringList tokens = device->name().split("_"); + if (tokens.count() == 2) { + serialNumber = tokens.at(1); + } else { + qCWarning(dcNuki()) << "Could not read serial number from bluetooth device name" << device->name(); + } + + ParamList params; + params.append(Param(nukiDeviceNameParamTypeId, device->name())); + params.append(Param(nukiDeviceMacParamTypeId, device->address().toString())); + params.append(Param(nukiDeviceSerialNumberParamTypeId, serialNumber)); + descriptor.setParams(params); + info->addDeviceDescriptor(descriptor); + } + } + + info->finish(Device::DeviceErrorNoError); +} + +void DevicePluginNuki::onAsyncSetupNukiAvailableChanged(bool available) +{ + // Remove possibly running Nuki setup devices on disconnected + if (!available && m_asyncSetupNuki) { + qCDebug(dcNuki()) << "Delete the temporary pairing device"; + m_asyncSetupNuki->deleteLater(); + m_asyncSetupNuki = nullptr; + } +} + +void DevicePluginNuki::onNukiAuthenticationProcessFinished(const PairingTransactionId &pairingTransactionId, bool success) +{ + if (m_asyncSetupNuki) { + qCDebug(dcNuki()) << "Delete the temporary pairing device"; + m_asyncSetupNuki->deleteLater(); + m_asyncSetupNuki = nullptr; + } + + if (!m_pairingInfo) { + qCWarning(dcNuki()) << "Authentication process finished, but have not valid pairing translaction id"; + return; + } + + if (m_pairingInfo->transactionId() != pairingTransactionId) { + qCWarning(dcNuki()) << "Authentication process finished, but have not valid pairing translaction id"; + return; + } + + m_pairingInfo->finish(success ? Device::DeviceErrorNoError : Device::DeviceErrorHardwareFailure); + m_pairingInfo = nullptr; +} diff --git a/nuki/devicepluginnuki.h b/nuki/devicepluginnuki.h new file mode 100644 index 00000000..a7c6f20d --- /dev/null +++ b/nuki/devicepluginnuki.h @@ -0,0 +1,78 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 Bernhard Trinnes * + * Copyright (C) 2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef DEVICEPLUGINNUKI_H +#define DEVICEPLUGINNUKI_H + +#include "plugintimer.h" +#include "devices/deviceplugin.h" +#include "bluez/bluetoothmanager.h" + +#include "nuki.h" + +class DevicePluginNuki : public DevicePlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "guru.guh.DevicePlugin" FILE "devicepluginnuki.json") + Q_INTERFACES(DevicePlugin) + +public: + explicit DevicePluginNuki(); + ~DevicePluginNuki(); + + void init() override; + void setupDevice(DeviceSetupInfo *info) override; + void discoverDevices(DeviceDiscoveryInfo *info) override; + void startPairing(DevicePairingInfo *info) override; + void confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret) override; + void postSetupDevice(Device *device) override; + void executeAction(DeviceActionInfo *info) override; + void deviceRemoved(Device *device) override; + +private: + QHash m_nukiDevices; + PluginTimer *m_refreshTimer = nullptr; + + BluetoothManager *m_bluetoothManager = nullptr; + BluetoothAdapter *m_bluetoothAdapter = nullptr; + + Nuki *m_asyncSetupNuki = nullptr; + DevicePairingInfo *m_pairingInfo = nullptr; + + bool m_encrytionLibraryInitialized = false; + + bool bluetoothDeviceAlreadyAdded(const QBluetoothAddress &address); + +private slots: + void onRefreshTimeout(); + void onBluetoothEnabledChanged(const bool &enabled); + void onBluetoothDiscoveryFinished(DeviceDiscoveryInfo *info); + + void onAsyncSetupNukiAvailableChanged(bool available); + + void onNukiAuthenticationProcessFinished(const PairingTransactionId &pairingTransactionId, bool success); + +}; + +#endif // DEVICEPLUGINNUKI_H diff --git a/nuki/devicepluginnuki.json b/nuki/devicepluginnuki.json new file mode 100644 index 00000000..c4b9f54b --- /dev/null +++ b/nuki/devicepluginnuki.json @@ -0,0 +1,157 @@ +{ + "name": "Nuki", + "displayName": "Nuki", + "id": "e5806d75-a40e-4766-a272-5a3a8d3ed625", + "vendors": [ + { + "name": "nuki", + "displayName": "Nuki", + "id": "bf313b83-2ac5-4d22-bf0e-c13d3b4caf52", + "deviceClasses": [ + { + "id": "4a1cc5d9-9b44-4632-8db0-66d64efd4767", + "name": "nuki", + "displayName": "Smartlock", + "interfaces": [ "connectable", "battery", "smartlock" ], + "createMethods": [ "discovery" ], + "setupMethod": "pushButton", + "paramTypes": [ + { + "id": "2477abba-874b-4c48-b543-7b911ff215b3", + "name": "name", + "displayName": "Name", + "type": "QString", + "inputType": "TextLine" + }, + { + "id": "30976794-6066-4f72-8135-6d50499247a5", + "name": "mac", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress" + }, + { + "id": "ea51d911-f94a-4d2d-97fd-9f1d4c6519bf", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "inputType": "TextLine" + } + ], + "stateTypes": [ + { + "id": "d5fbd774-4e87-4c7c-8c92-f094352897f6", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "e9ffe14f-b71d-44cd-96c7-eb90fe243e13", + "name": "batteryCritical", + "displayName": "Battery critical", + "displayNameEvent": "Battery critical changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "a47dec8b-4df2-4acd-895b-bfeacbcb6f2e", + "name": "status", + "displayName": "Status", + "displayNameEvent": "Status changed", + "type": "QString", + "possibleValues": [ + "Ok", + "Uncalibrated", + "Motor blocked", + "Undefined" + ], + "defaultValue": "Undefined" + }, + { + "id": "07a09bd8-b342-4e33-92cd-0af21f8689fa", + "name": "state", + "displayName": "State", + "displayNameEvent": "State changed", + "type": "QString", + "possibleValues": [ + "locked", + "locking", + "unlocked", + "unlocking", + "unlatched", + "unlatching" + ], + "defaultValue": "locked" + }, + { + "id": "4e291e6a-2b90-4e6c-bad4-5400e6db4f05", + "name": "mode", + "displayName": "Mode", + "displayNameEvent": "Mode changed", + "type": "QString", + "possibleValues": [ + "Uninitialized", + "Pairing", + "Door" + ], + "defaultValue": "Uninitialized" + }, + { + "id": "b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a", + "name": "trigger", + "displayName": "Trigger", + "displayNameEvent": "Trigger changed", + "type": "QString", + "possibleValues": [ + "Bluetooth", + "Manual", + "Button" + ], + "defaultValue": "Bluetooth" + }, + { + "id": "b1b0eafb-e5f2-47d3-bd18-74a21129f9b5", + "name": "hardwareRevision", + "displayName": "Hardware revision", + "displayNameEvent": "Hardware revision changed", + "type": "QString", + "defaultValue": "-" + }, + { + "id": "9dc0115b-cdb4-472c-961b-b182f77a576f", + "name": "firmwareRevision", + "displayName": "Firmware revision", + "displayNameEvent": "Firmware revision changed", + "type": "QString", + "defaultValue": "-" + } + ], + "actionTypes": [ + { + "id": "55d25891-89b9-4a9f-a2b3-774eccf30183", + "name": "close", + "displayName": "Lock" + }, + { + "id": "76e96738-5336-4b9a-87f7-1822307b5a39", + "name": "open", + "displayName": "Unlock" + }, + { + "id": "45be8d24-17c3-422b-b264-381e673bb3c8", + "name": "unlatch", + "displayName": "Open door" + }, + { + "id": "7c9d5c5d-d8c1-424b-8620-daa3a757576b", + "name": "refresh", + "displayName": "Refresh" + } + ] + } + ] + } + ] +} diff --git a/nuki/docs/20170619-Bridge-API-v1.6.pdf b/nuki/docs/20170619-Bridge-API-v1.6.pdf new file mode 100644 index 00000000..2b9616c5 Binary files /dev/null and b/nuki/docs/20170619-Bridge-API-v1.6.pdf differ diff --git a/nuki/docs/20171120.Smartlock.API_.v1.10.pdf b/nuki/docs/20171120.Smartlock.API_.v1.10.pdf new file mode 100755 index 00000000..23db5ee6 Binary files /dev/null and b/nuki/docs/20171120.Smartlock.API_.v1.10.pdf differ diff --git a/nuki/nuki.cpp b/nuki/nuki.cpp new file mode 100644 index 00000000..eaf5e3c2 --- /dev/null +++ b/nuki/nuki.cpp @@ -0,0 +1,556 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 Bernhard Trinnes * + * Copyright (C) 2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "nuki.h" +#include "extern-plugininfo.h" + +#include +#include +#include +#include +#include + +static QBluetoothUuid initializationServiceUuid = QBluetoothUuid(QUuid("a92ee000-5501-11e4-916c-0800200c9a66")); +static QBluetoothUuid pairingServiceUuid = QBluetoothUuid(QUuid("a92ee100-5501-11e4-916c-0800200c9a66")); +static QBluetoothUuid pairingDataCharacteristicUuid = QBluetoothUuid(QUuid("a92ee101-5501-11e4-916c-0800200c9a66")); +static QBluetoothUuid keyturnerServiceUuid = QBluetoothUuid(QUuid("a92ee200-5501-11e4-916c-0800200c9a66")); +static QBluetoothUuid keyturnerDataCharacteristicUuid = QBluetoothUuid(QUuid("a92ee201-5501-11e4-916c-0800200c9a66")); +static QBluetoothUuid keyturnerUserDataCharacteristicUuid = QBluetoothUuid(QUuid("a92ee202-5501-11e4-916c-0800200c9a66")); + +Nuki::Nuki(Device *device, BluetoothDevice *bluetoothDevice, QObject *parent) : + QObject(parent), + m_device(device), + m_bluetoothDevice(bluetoothDevice) +{ + connect(m_bluetoothDevice, &BluetoothDevice::stateChanged, this, &Nuki::onBluetoothDeviceStateChanged); + onBluetoothDeviceStateChanged(m_bluetoothDevice->state()); +} + +Device *Nuki::device() +{ + return m_device; +} + +BluetoothDevice *Nuki::bluetoothDevice() +{ + return m_bluetoothDevice; +} + +bool Nuki::startAuthenticationProcess(const PairingTransactionId &pairingTransactionId) +{ + if (m_nukiAction != NukiActionNone) { + qCWarning(dcNuki()) << "Cannot start authentication process. Nuki is busy and already processing an action. Please retry again." << m_nukiAction; + return false; + } + + m_nukiAction = NukiActionAuthenticate; + m_pairingId = pairingTransactionId; + + if (m_available) { + executeCurrentAction(); + } else { + m_bluetoothDevice->connectDevice(); + } + + return true; +} + +bool Nuki::refreshStates() +{ + return executeNukiAction(NukiActionRefresh); +} + +bool Nuki::executeNukiAction(Nuki::NukiAction action) +{ + if (m_nukiAction != NukiActionNone) { + qCWarning(dcNuki()) << "Cannot execute Nuki action. Nuki is busy and already processing an action." << m_nukiAction; + return false; + } + + m_nukiAction = action; + + if (m_available) { + executeCurrentAction(); + } else { + m_bluetoothDevice->connectDevice(); + } + return true; +} + +bool Nuki::executeDeviceAction(Nuki::NukiAction action, DeviceActionInfo *actionInfo) +{ + if (m_nukiAction != NukiActionNone || !m_actionInfo.isNull()) { + qCWarning(dcNuki()) << "Nuki is busy and already processing an action. Please retry again." << m_nukiAction; + return false; + } + + m_actionInfo = QPointer(actionInfo); + m_nukiAction = action; + + if (m_available) { + executeCurrentAction(); + } else { + m_bluetoothDevice->connectDevice(); + } + return true; +} + +void Nuki::connectDevice() +{ + if (!m_bluetoothDevice) + return; + + m_bluetoothDevice->connectDevice(); +} + +void Nuki::disconnectDevice() +{ + if (!m_bluetoothDevice) + return; + + m_bluetoothDevice->disconnectDevice(); +} + +void Nuki::clearSettings() +{ + if (m_nukiAuthenticator) { + m_nukiAuthenticator->clearSettings(); + } +} + +void Nuki::printServices() +{ + foreach (BluetoothGattService *service, m_bluetoothDevice->services()) { + qCDebug(dcNuki()) << service; + foreach (BluetoothGattCharacteristic *characteristic, service->characteristics()) { + qCDebug(dcNuki()) << " " << characteristic->chararcteristicName() << characteristic->uuid().toString(); + foreach (BluetoothGattDescriptor *descriptor, characteristic->descriptors()) { + qCDebug(dcNuki()) << " " << descriptor->name() << descriptor->uuid().toString(); + } + } + } +} + +void Nuki::readDeviceInformationCharacteristics() +{ + m_initUuidsToRead.append(QBluetoothUuid::SerialNumberString); + m_initUuidsToRead.append(QBluetoothUuid::HardwareRevisionString); + m_initUuidsToRead.append(QBluetoothUuid::FirmwareRevisionString); + + m_deviceInformationService->readCharacteristic(QBluetoothUuid::SerialNumberString); + m_deviceInformationService->readCharacteristic(QBluetoothUuid::HardwareRevisionString); + m_deviceInformationService->readCharacteristic(QBluetoothUuid::FirmwareRevisionString); +} + +void Nuki::executeCurrentAction() +{ + qCDebug(dcNuki()) << "Executing" << m_nukiAction; + + switch (m_nukiAction) { + case NukiActionAuthenticate: + m_nukiAuthenticator->startAuthenticationProcess(); + break; + case NukiActionRefresh: + if (!m_nukiController->readLockState()) { + finishCurrentAction(false); + } + break; + case NukiActionLock: + if (!m_nukiController->lock()) { + finishCurrentAction(false); + } + break; + case NukiActionUnlock: + if (!m_nukiController->unlock()) { + finishCurrentAction(false); + } + break; + case NukiActionUnlatch: + if (!m_nukiController->unlatch()) { + finishCurrentAction(false); + } + break; + default: + break; + } +} + +void Nuki::onBluetoothDeviceStateChanged(const BluetoothDevice::State &state) +{ + qCDebug(dcNuki()) << m_bluetoothDevice << "state changed --> " << state; + switch (state) { + case BluetoothDevice::Connecting: + break; + case BluetoothDevice::Connected: + if (m_bluetoothDevice->servicesResolved()) { + // Services already discovered + if (!init()) { + qCWarning(dcNuki()) << "Could not initialze device" << m_bluetoothDevice; + m_bluetoothDevice->disconnectDevice(); + } else { + readDeviceInformationCharacteristics(); + } + } + break; + case BluetoothDevice::Pairing: + break; + case BluetoothDevice::Discovering: + break; + case BluetoothDevice::Discovered: + printServices(); + if (!init()) { + qCWarning(dcNuki()) << "Could not initialze device" << m_bluetoothDevice; + m_bluetoothDevice->disconnectDevice(); + } else { + readDeviceInformationCharacteristics(); + } + break; + case BluetoothDevice::Disconnecting: + setAvailable(false); + clean(); + break; + case BluetoothDevice::Disconnected: + setAvailable(false); + clean(); + break; + default: + break; + } +} + +void Nuki::onDeviceInfoCharacteristicReadFinished(BluetoothGattCharacteristic *characteristic, const QByteArray &value) +{ + qCDebug(dcNuki()) << "Read device information characteristic finished" << characteristic->chararcteristicName() << qUtf8Printable(value); + if (characteristic->uuid() == QBluetoothUuid::SerialNumberString) { + m_serialNumber = QString::fromUtf8(value); + m_initUuidsToRead.removeOne(QBluetoothUuid::SerialNumberString); + } else if (characteristic->uuid() == QBluetoothUuid::HardwareRevisionString) { + m_hardwareRevision = QString::fromUtf8(value); + m_initUuidsToRead.removeOne(QBluetoothUuid::HardwareRevisionString); + } else if (characteristic->uuid() == QBluetoothUuid::FirmwareRevisionString) { + m_firmwareRevision = QString::fromUtf8(value); + m_initUuidsToRead.removeOne(QBluetoothUuid::FirmwareRevisionString); + } + + if (m_initUuidsToRead.isEmpty()) { + // Initial read done. Make device available + setAvailable(true); + } +} + +void Nuki::onAuthenticationError(NukiUtils::ErrorCode error) +{ + qCWarning(dcNuki()) << "Authentication error occured" << error; + + if (m_pairingId.isNull()) + return; + + // If we have a pairing id + emit authenticationProcessFinished(m_pairingId, false); + m_pairingId = PairingTransactionId(); +} + +void Nuki::onAuthenticationFinished(bool success) +{ + qCDebug(dcNuki()) << "Authentication process finished" << (success ? "successfully." : "with error."); + + if (m_pairingId.isNull()) + return; + + // If we have a pairing id + emit authenticationProcessFinished(m_pairingId, success); + m_pairingId = PairingTransactionId(); +} + +void Nuki::onNukiReadStatesFinished(bool success) +{ + m_nukiAction = NukiActionNone; + + if (success) { + // Update states + onNukiStatesChanged(); + } + + // Check if this was an action call + if (m_actionInfo.isNull()) { + // Looks like this was a refresh call, lets disconnect to minimize the not reachable time for other apps + QTimer::singleShot(0, m_bluetoothDevice, &BluetoothDevice::disconnectDevice); + return; + } + + finishCurrentAction(true); +} + +void Nuki::onNukiStatesChanged() +{ + if (!m_device) + return; + + m_device->setStateValue(nukiHardwareRevisionStateTypeId, m_hardwareRevision); + m_device->setStateValue(nukiFirmwareRevisionStateTypeId, m_firmwareRevision); + m_device->setStateValue(nukiBatteryCriticalStateTypeId, m_nukiController->batteryCritical()); + + switch (m_nukiController->nukiLockTrigger()) { + case NukiUtils::LockTriggerBluetooth: + m_device->setStateValue(nukiTriggerStateTypeId, "Bluetooth"); + break; + case NukiUtils::LockTriggerButton: + m_device->setStateValue(nukiTriggerStateTypeId, "Button"); + break; + case NukiUtils::LockTriggerManual: + m_device->setStateValue(nukiTriggerStateTypeId, "Manual"); + break; + default: + break; + } + + switch (m_nukiController->nukiState()) { + case NukiUtils::NukiStateDoorMode: + m_device->setStateValue(nukiModeStateTypeId, "Door"); + break; + case NukiUtils::NukiStatePairingMode: + m_device->setStateValue(nukiModeStateTypeId, "Pairing"); + break; + case NukiUtils::NukiStateUninitialized: + m_device->setStateValue(nukiModeStateTypeId, "Uninitialized"); + break; + default: + break; + } + + switch (m_nukiController->nukiLockState()) { + case NukiUtils::LockStateLocked: + m_device->setStateValue(nukiStateStateTypeId, "locked"); + m_device->setStateValue(nukiStatusStateTypeId, "Ok"); + break; + case NukiUtils::LockStateLocking: + m_device->setStateValue(nukiStateStateTypeId, "locking"); + m_device->setStateValue(nukiStatusStateTypeId, "Ok"); + break; + case NukiUtils::LockStateMotorBlocked: + m_device->setStateValue(nukiStatusStateTypeId, "Motor blocked"); + break; + case NukiUtils::LockStateUncalibrated: + m_device->setStateValue(nukiStatusStateTypeId, "Uncalibrated"); + break; + case NukiUtils::LockStateUndefined: + m_device->setStateValue(nukiStatusStateTypeId, "Undefined"); + break; + case NukiUtils::LockStateUnlatched: + m_device->setStateValue(nukiStateStateTypeId, "unlatched"); + m_device->setStateValue(nukiStatusStateTypeId, "Ok"); + break; + case NukiUtils::LockStateUnlatching: + m_device->setStateValue(nukiStateStateTypeId, "unlatching"); + m_device->setStateValue(nukiStatusStateTypeId, "Ok"); + break; + case NukiUtils::LockStateUnlockedLocknGoActive: + m_device->setStateValue(nukiStatusStateTypeId, "unlocked"); + break; + case NukiUtils::LockStateUnlocked: + m_device->setStateValue(nukiStateStateTypeId, "unlocked"); + m_device->setStateValue(nukiStatusStateTypeId, "Ok"); + break; + case NukiUtils::LockStateUnlocking: + m_device->setStateValue(nukiStateStateTypeId, "unlocking"); + m_device->setStateValue(nukiStatusStateTypeId, "Ok"); + break; + default: + break; + } +} + +bool Nuki::init() +{ + if (!m_bluetoothDevice) + return false; + + qCDebug(dcNuki()) << "Init" << m_bluetoothDevice; + + // If not connected, connect + if (!m_bluetoothDevice->connected()) { + qCWarning(dcNuki()) << "Device is not connected" << m_bluetoothDevice; + return false; + } + + // If services not resolved yet, wait + if (!m_bluetoothDevice->servicesResolved()) { + qCWarning(dcNuki()) << "Device services not resolved yet" << m_bluetoothDevice; + return false; + } + + // Verify services + if (!m_bluetoothDevice->hasService(QBluetoothUuid::DeviceInformation)) { + qCWarning(dcNuki()) << "Could not find device information service on device" << m_bluetoothDevice; + return false; + } + + if (!m_bluetoothDevice->hasService(pairingServiceUuid)) { + qCWarning(dcNuki()) << "Could not find pairing service on device" << m_bluetoothDevice; + return false; + } + + if (!m_bluetoothDevice->hasService(keyturnerServiceUuid)) { + qCWarning(dcNuki()) << "Could not find key turner service on device" << m_bluetoothDevice; + return false; + } + + // Create service and characteristic objects + // Device information + m_deviceInformationService = m_bluetoothDevice->getService(QBluetoothUuid::DeviceInformation); + connect(m_deviceInformationService, &BluetoothGattService::characteristicReadFinished, this, &Nuki::onDeviceInfoCharacteristicReadFinished); + + // Keyturner service + m_keyturnerService = m_bluetoothDevice->getService(keyturnerServiceUuid); + if (!m_keyturnerService->hasCharacteristic(keyturnerUserDataCharacteristicUuid)) { + qCWarning(dcNuki()) << "Could not find user data characteristc on device" << m_bluetoothDevice; + return false; + } + if (!m_keyturnerService->hasCharacteristic(keyturnerDataCharacteristicUuid)) { + qCWarning(dcNuki()) << "Could not find data characteristc on device" << m_bluetoothDevice; + return false; + } + m_keyturnerUserDataCharacteristic = m_keyturnerService->getCharacteristic(keyturnerUserDataCharacteristicUuid); + if (!m_keyturnerUserDataCharacteristic->startNotifications()) { + qCWarning(dcNuki()) << "Could not enable notifications for user data characteristic."; + return false; + } + m_keyturnerDataCharacteristic = m_keyturnerService->getCharacteristic(keyturnerDataCharacteristicUuid); + if (!m_keyturnerDataCharacteristic->startNotifications()) { + qCWarning(dcNuki()) << "Could not enable notifications for data characteristic."; + return false; + } + + // Pairing service + m_pairingService = m_bluetoothDevice->getService(pairingServiceUuid); + if (!m_pairingService->hasCharacteristic(pairingDataCharacteristicUuid)) { + qCWarning(dcNuki()) << "Could not find pairing data characteristc on device" << m_bluetoothDevice; + return false; + } + m_pairingDataCharacteristic = m_pairingService->getCharacteristic(pairingDataCharacteristicUuid); + if (!m_pairingDataCharacteristic->startNotifications()) { + qCWarning(dcNuki()) << "Could not enable notifications for pairing characteristic."; + return false; + } + + // Create authenticator + if (m_nukiAuthenticator) { + delete m_nukiAuthenticator; + m_nukiAuthenticator = nullptr; + } + + m_nukiAuthenticator = new NukiAuthenticator(m_bluetoothDevice->hostInfo(), m_pairingDataCharacteristic, this); + connect(m_nukiAuthenticator, &NukiAuthenticator::errorOccured, this, &Nuki::onAuthenticationError); + connect(m_nukiAuthenticator, &NukiAuthenticator::authenticationProcessFinished, this, &Nuki::onAuthenticationFinished); + + // Create nuki handler for encrypted communication + if (m_nukiController) { + delete m_nukiController; + m_nukiController = nullptr; + } + + m_nukiController = new NukiController(m_nukiAuthenticator, m_keyturnerUserDataCharacteristic, this); + connect(m_nukiController, &NukiController::readNukiStatesFinished, this, &Nuki::onNukiReadStatesFinished); + connect(m_nukiController, &NukiController::lockFinished, this, &Nuki::finishCurrentAction); + connect(m_nukiController, &NukiController::unlockFinished, this, &Nuki::finishCurrentAction); + connect(m_nukiController, &NukiController::unlatchFinished, this, &Nuki::finishCurrentAction); + connect(m_nukiController, &NukiController::nukiStatesChanged, this, &Nuki::onNukiStatesChanged); + + return true; +} + +void Nuki::clean() +{ + // Reset properties + m_hardwareRevision = QString(); + m_serialNumber = QString(); + m_firmwareRevision = QString(); + m_initUuidsToRead.clear(); + + finishCurrentAction(false); + + // Forget all services and characteristics + if (m_deviceInformationService) { + disconnect(m_deviceInformationService, &BluetoothGattService::characteristicReadFinished, this, &Nuki::onDeviceInfoCharacteristicReadFinished); + m_deviceInformationService = nullptr; + } + + m_keyturnerService = nullptr; + m_keyturnerDataCharacteristic = nullptr; + m_keyturnerUserDataCharacteristic = nullptr; + + m_pairingService = nullptr; + m_pairingDataCharacteristic = nullptr; + + // Delete handler + if (m_nukiController) { + delete m_nukiController; + m_nukiController = nullptr; + } + + // Note: delete the authenticator after the handler + if (m_nukiAuthenticator) { + delete m_nukiAuthenticator; + m_nukiAuthenticator = nullptr; + } +} + +void Nuki::finishCurrentAction(bool success) +{ + m_nukiAction = NukiActionNone; + if (m_actionInfo.isNull()) + return; + + m_actionInfo->finish(success ? Device::DeviceErrorNoError : Device::DeviceErrorHardwareFailure); + m_actionInfo.clear(); +} + +void Nuki::setAvailable(bool available) +{ + if (m_available == available) + return; + + m_available = available; + emit availableChanged(m_available); + + qCDebug(dcNuki()) << "Bluetooth device" << m_bluetoothDevice->name() << "is now" << (m_available ? "available" : "unavailable"); + + if (m_available) { + executeCurrentAction(); + } else { + // Finish any running actions + finishCurrentAction(false); + + // Finish possible running pairing transations + if (!m_pairingId.isNull()) { + qCWarning(dcNuki()) << "Cancel authentication process because of disconnection."; + emit authenticationProcessFinished(m_pairingId, false); + m_pairingId = PairingTransactionId(); + } + } + + if (!m_device) + return; + + m_device->setStateValue(nukiConnectedStateTypeId, m_available); +} diff --git a/nuki/nuki.h b/nuki/nuki.h new file mode 100644 index 00000000..a4503c67 --- /dev/null +++ b/nuki/nuki.h @@ -0,0 +1,136 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 Bernhard Trinnes * + * Copyright (C) 2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef NUKI_H +#define NUKI_H + +#include +#include +#include +#include +#include + +#include "typeutils.h" +#include "devices/device.h" +#include "bluez/bluetoothdevice.h" +#include "devices/deviceactioninfo.h" + +#include "nukiutils.h" +#include "nukicontroller.h" +#include "nukiauthenticator.h" + +//#include "nacl-20110221/crypto_auth/" + +class Nuki : public QObject +{ + + Q_OBJECT + +public: + enum NukiAction { + NukiActionNone, + NukiActionAuthenticate, + NukiActionRefresh, + NukiActionLock, + NukiActionUnlock, + NukiActionUnlatch + }; + Q_ENUM(NukiAction) + + explicit Nuki(Device *device, BluetoothDevice *bluetoothDevice, QObject *parent = nullptr); + + Device *device(); + BluetoothDevice *bluetoothDevice(); + + bool startAuthenticationProcess(const PairingTransactionId &pairingTransactionId); + bool refreshStates(); + bool executeDeviceAction(NukiAction action, DeviceActionInfo *actionInfo); + + void connectDevice(); + void disconnectDevice(); + + void clearSettings(); + + +private: + Device *m_device = nullptr; + BluetoothDevice *m_bluetoothDevice = nullptr; + NukiController *m_nukiController = nullptr; + NukiAuthenticator *m_nukiAuthenticator = nullptr; + + BluetoothGattService *m_deviceInformationService = nullptr; + BluetoothGattService *m_initializationService = nullptr; + BluetoothGattService *m_pairingService = nullptr; + BluetoothGattService *m_keyturnerService = nullptr; + + BluetoothGattCharacteristic *m_pairingDataCharacteristic = nullptr; + BluetoothGattCharacteristic *m_keyturnerDataCharacteristic = nullptr; + BluetoothGattCharacteristic *m_keyturnerUserDataCharacteristic = nullptr; + + // Device information + QList m_initUuidsToRead; + QString m_serialNumber; + QString m_hardwareRevision; + QString m_firmwareRevision; + + bool m_available = false; + + NukiAction m_nukiAction = NukiActionNone; + QPointer m_actionInfo; + PairingTransactionId m_pairingId; + + bool init(); + void clean(); + bool executeNukiAction(NukiAction action); + + void setAvailable(bool available); + void printServices(); + void readDeviceInformationCharacteristics(); + + void executeCurrentAction(); + +signals: + void availableChanged(bool available); + void authenticationProcessFinished(const PairingTransactionId &pairingId, bool success); + void stateChanged(NukiUtils::LockState lockState); + void actionFinished(const ActionId &action, bool success); + +private slots: + // Bluetooth device + void onBluetoothDeviceStateChanged(const BluetoothDevice::State &state); + + // DeviceInfo service + void onDeviceInfoCharacteristicReadFinished(BluetoothGattCharacteristic *characteristic, const QByteArray &value); + + // Nuki Authenticator + void onAuthenticationError(NukiUtils::ErrorCode error); + void onAuthenticationFinished(bool success); + + // Nuki controller + void onNukiReadStatesFinished(bool success); + void finishCurrentAction(bool success); + void onNukiStatesChanged(); + +}; + +#endif // NUKI_H diff --git a/nuki/nuki.pro b/nuki/nuki.pro new file mode 100644 index 00000000..3e151aa9 --- /dev/null +++ b/nuki/nuki.pro @@ -0,0 +1,36 @@ +include(../plugins.pri) + +TARGET = $$qtLibraryTarget(nymea_devicepluginnuki) + +QT += bluetooth dbus + +# apt install libsodium-dev +LIBS += -lsodium + +HEADERS += \ + devicepluginnuki.h \ + nuki.h \ + bluez/blueztypes.h \ + bluez/bluetoothmanager.h \ + bluez/bluetoothadapter.h \ + bluez/bluetoothdevice.h \ + bluez/bluetoothgattservice.h \ + bluez/bluetoothgattcharacteristic.h \ + bluez/bluetoothgattdescriptor.h \ + nukiutils.h \ + nukiauthenticator.h \ + nukicontroller.h + +SOURCES += \ + devicepluginnuki.cpp \ + nuki.cpp \ + bluez/blueztypes.cpp \ + bluez/bluetoothmanager.cpp \ + bluez/bluetoothadapter.cpp \ + bluez/bluetoothdevice.cpp \ + bluez/bluetoothgattservice.cpp \ + bluez/bluetoothgattcharacteristic.cpp \ + bluez/bluetoothgattdescriptor.cpp \ + nukiutils.cpp \ + nukiauthenticator.cpp \ + nukicontroller.cpp diff --git a/nuki/nukiauthenticator.cpp b/nuki/nukiauthenticator.cpp new file mode 100644 index 00000000..26fe2853 --- /dev/null +++ b/nuki/nukiauthenticator.cpp @@ -0,0 +1,602 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "nukiauthenticator.h" + +#include "nukiutils.h" +#include "nymeasettings.h" +#include "extern-plugininfo.h" + +extern "C" { +#include "sodium.h" +#include "sodium/crypto_box.h" +#include "sodium/crypto_secretbox.h" +} + +#include +#include +#include + +NukiAuthenticator::NukiAuthenticator(const QBluetoothHostInfo &hostInfo, BluetoothGattCharacteristic *pairingCharacteristic, QObject *parent) : + QObject(parent), + m_hostInfo(hostInfo), + m_pairingCharacteristic(pairingCharacteristic) +{ + +#ifdef QT_DEBUG + // Enable full debug messages containing sensible data for debug builds + m_debug = true; +#endif + + // Check if we have authentication data for this device and set initial state + loadData(); + if (isValid()) { + setState(AuthenticationStateAuthenticated); + } else { + setState(AuthenticationStateUnauthenticated); + } + + connect(m_pairingCharacteristic, &BluetoothGattCharacteristic::valueChanged, this, &NukiAuthenticator::onPairingDataCharacteristicChanged); +} + +NukiUtils::ErrorCode NukiAuthenticator::error() const +{ + return m_error; +} + +NukiAuthenticator::AuthenticationState NukiAuthenticator::state() const +{ + return m_state; +} + +bool NukiAuthenticator::isValid() const +{ + return !m_privateKey.isEmpty() && + !m_publicKey.isEmpty() && + !m_publicKeyNuki.isEmpty() && + !m_authorizationId == 0 && + !m_authorizationIdRawData.isEmpty() && + !m_uuid.isEmpty(); +} + +void NukiAuthenticator::clearSettings() +{ + QSettings setting(NymeaSettings::settingsPath() + "/plugin-nuki.conf", QSettings::IniFormat); + setting.beginGroup(m_hostInfo.address().toString()); + setting.remove(""); + setting.endGroup(); + + qCDebug(dcNuki()) << "Settings cleared for" << m_hostInfo.address().toString() << "in" << setting.fileName(); +} + +void NukiAuthenticator::startAuthenticationProcess() +{ + setState(AuthenticationStateRequestPublicKey); +} + +quint32 NukiAuthenticator::authorizationId() const +{ + return m_authorizationId; +} + +QByteArray NukiAuthenticator::authorizationIdRawData() const +{ + return m_authorizationIdRawData; +} + +QByteArray NukiAuthenticator::encryptData(const QByteArray &data, const QByteArray &nonce) +{ + // Calculate shared key + qCDebug(dcNuki()) << "Authenticator: Encrypt data"; + Q_ASSERT_X(nonce.length() == crypto_box_NONCEBYTES, "data length", "The nonce does not have the correct length."); + + /* Note: https://download.libsodium.org/doc/public-key_cryptography/authenticated_encryption.html + * unsigned char *c The encrypted message (length of the data + crypto_box_MACBYTES) + * const unsigned char *m The message to encrypt + * unsigned long long mlen The length of the message to encrypt + * const unsigned char *n The nonce (must also sent unencrypted) + * const unsigned char *pk The public key of the Nuki + * const unsigned char *sk The private key of nymea for this Nuki + */ + + unsigned char encrypted[crypto_box_MACBYTES + data.length()]; + int result = crypto_box_easy(encrypted, + reinterpret_cast(data.data()), + static_cast(data.length()), + reinterpret_cast(nonce.data()), + reinterpret_cast(m_publicKeyNuki.data()), + reinterpret_cast(m_privateKey.data())); + + if (result < 0) { + qCWarning(dcNuki()) << "Could not encrypt data. Something went wrong"; + return QByteArray(); + } + + QByteArray encryptedData = QByteArray(reinterpret_cast(encrypted), crypto_box_MACBYTES + data.length()); + + if (m_debug) qCDebug(dcNuki()) << " Private key :" << NukiUtils::convertByteArrayToHexStringCompact(m_privateKey); + if (m_debug) qCDebug(dcNuki()) << " Public key :" << NukiUtils::convertByteArrayToHexStringCompact(m_publicKey); + if (m_debug) qCDebug(dcNuki()) << " Nuki public key :" << NukiUtils::convertByteArrayToHexStringCompact(m_publicKeyNuki); + if (m_debug) qCDebug(dcNuki()) << " Unencrypted data:" << NukiUtils::convertByteArrayToHexStringCompact(data); + if (m_debug) qCDebug(dcNuki()) << " Encrypted data :" << NukiUtils::convertByteArrayToHexStringCompact(encryptedData); + + return encryptedData; +} + +QByteArray NukiAuthenticator::decryptData(const QByteArray &data, const QByteArray &nonce) +{ + qCDebug(dcNuki()) << "Authenticator: Decrypt data"; + Q_ASSERT_X(nonce.length() == crypto_box_NONCEBYTES, "data length", "The nonce does not have the correct length."); + Q_ASSERT_X(static_cast(data.length()) >= crypto_box_MACBYTES, "data length", "The encrypted data is to short."); + + /* Note: https://download.libsodium.org/doc/public-key_cryptography/authenticated_encryption.html + * unsigned char *m The decrypted message result + * const unsigned char *c The message to decrypt / cyphertext (length of the encrypted data + crypto_box_MACBYTES) + * unsigned long long clen The length of the message to decrypt + * const unsigned char *n The nonce used while encryption (received in the unencrypted ADATA) + * const unsigned char *pk The public key of the Nuki + * const unsigned char *sk The private key of nymea for this Nuki + */ + + unsigned char decrypted[data.length() - crypto_box_MACBYTES]; + int result = crypto_box_open_easy(decrypted, + reinterpret_cast(data.data()), + static_cast(data.length()), + reinterpret_cast(nonce.data()), + reinterpret_cast(m_publicKeyNuki.data()), + reinterpret_cast(m_privateKey.data())); + + if (result < 0) { + qCWarning(dcNuki()) << "Could not decrypt data. Something went wrong"; + return QByteArray(); + } + + QByteArray decryptedData = QByteArray(reinterpret_cast(decrypted), data.length() - crypto_box_MACBYTES); + + if (m_debug) qCDebug(dcNuki()) << " Private key :" << NukiUtils::convertByteArrayToHexStringCompact(m_privateKey); + if (m_debug) qCDebug(dcNuki()) << " Public key :" << NukiUtils::convertByteArrayToHexStringCompact(m_publicKey); + if (m_debug) qCDebug(dcNuki()) << " Nuki public key :" << NukiUtils::convertByteArrayToHexStringCompact(m_publicKeyNuki); + if (m_debug) qCDebug(dcNuki()) << " Encrypted data :" << NukiUtils::convertByteArrayToHexStringCompact(data); + if (m_debug) qCDebug(dcNuki()) << " Decrypted data :" << NukiUtils::convertByteArrayToHexStringCompact(decryptedData); + + return decryptedData; +} + +QByteArray NukiAuthenticator::generateNonce(const int &length) const +{ + unsigned char nounce[length]; + randombytes_buf(nounce, length); + return QByteArray(reinterpret_cast(nounce), length); +} + +void NukiAuthenticator::setState(NukiAuthenticator::AuthenticationState state) +{ + if (m_state == state) + return; + + m_state = state; + emit stateChanged(m_state); + + qCDebug(dcNuki()) << m_state; + + switch (m_state) { + case AuthenticationStateUnauthenticated: + resetExpectedData(); + break; + case AuthenticationStateAuthenticated: + qCDebug(dcNuki()) << "Device" << m_hostInfo.address().toString() << "authenticated."; + if (m_debug) qCDebug(dcNuki()) << " Private key :" << NukiUtils::convertByteArrayToHexStringCompact(m_privateKey); + if (m_debug) qCDebug(dcNuki()) << " Public key :" << NukiUtils::convertByteArrayToHexStringCompact(m_publicKey); + if (m_debug) qCDebug(dcNuki()) << " Nuki public key :" << NukiUtils::convertByteArrayToHexStringCompact(m_publicKeyNuki); + if (m_debug) qCDebug(dcNuki()) << " Authorization ID:" << NukiUtils::convertByteArrayToHexStringCompact(m_authorizationIdRawData) << m_authorizationId; + break; + case AuthenticationStateRequestPublicKey: + resetExpectedData(NukiUtils::CommandPublicKey, 2); + requestPublicKey(); + break; + case AuthenticationStateGenerateKeyPair: + resetExpectedData(); + generateKeyPair(); + setState(AuthenticationStateSendPublicKey); + break; + case AuthenticationStateSendPublicKey: + resetExpectedData(NukiUtils::CommandChallenge, 2); + sendPublicKey(); + setState(AuthenticationStateReadChallenge); + break; + case AuthenticationStateReadChallenge: + resetExpectedData(NukiUtils::CommandChallenge, 2); + break; + case AuthenticationStateAutorization: + sendAuthorizationAuthenticator(); + setState(AuthenticationStateReadSecondChallenge); + break; + case AuthenticationStateReadSecondChallenge: + resetExpectedData(NukiUtils::CommandChallenge, 2); + break; + case AuthenticationStateAuthenticateData: + resetExpectedData(); + sendAuthenticateData(); + setState(AuthenticationStateAuthorizationId); + break; + case AuthenticationStateAuthorizationId: + resetExpectedData(NukiUtils::CommandAuthorizationId, 5); + break; + case AuthenticationStateAuthorizationIdConfirm: + resetExpectedData(); + sendAuthoizationIdConfirm(); + setState(AuthenticationStateStatus); + break; + case AuthenticationStateStatus: + resetExpectedData(NukiUtils::CommandStatus); + break; + case AuthenticationStateError: + resetExpectedData(); + emit errorOccured(m_error); + emit authenticationProcessFinished(false); + break; + default: + qCWarning(dcNuki()) << "Authenticator: Unknown state."; + break; + } +} + +void NukiAuthenticator::resetExpectedData(NukiUtils::Command command, int expectedCount) +{ + m_currentReceivingCommand = command; + m_currentReceivingCurrentCount = 0; + m_currentReceivingExpectedCount = expectedCount; + m_currentReceivingData.clear(); +} + +bool NukiAuthenticator::createAuthenticator(const QByteArray content) +{ + // Create shared key + qCDebug(dcNuki()) << "Authenticator: Calculate shared key"; + unsigned char sharedKey[crypto_box_BEFORENMBYTES]; + int result = crypto_box_beforenm(sharedKey, reinterpret_cast(m_publicKeyNuki.data()), reinterpret_cast(m_privateKey.data())); + if (result < 0) { + qCWarning(dcNuki()) << "Could not create shared key for autorization authenticator."; + return false; + } + + m_sharedKey = QByteArray(reinterpret_cast(sharedKey), crypto_box_BEFORENMBYTES); + Q_ASSERT_X(m_sharedKey.length() == 32, "data length", "The shared key does not have the correct length."); + + if (m_debug) qCDebug(dcNuki()) << "Authenticator: Calculate authenticator hash HMAC-SHA-256"; + if (m_debug) qCDebug(dcNuki()) << " Shared key :" << NukiUtils::convertByteArrayToHexStringCompact(m_sharedKey); + if (m_debug) qCDebug(dcNuki()) << " Nuki nonce :" << NukiUtils::convertByteArrayToHexStringCompact(m_nonceNuki); + + // Calculate authenticator hash input for HMAC-SHA-256 + qCDebug(dcNuki()) << "Authenticator: Calculate authenticator data"; + unsigned char authenticator[crypto_auth_hmacsha256_BYTES]; + result = crypto_auth_hmacsha256(authenticator, reinterpret_cast(content.data()), content.length(), reinterpret_cast(m_sharedKey.data())); + if (result < 0) { + qCWarning(dcNuki()) << "Could not create authenticator hash for autorization authenticator."; + return false; + } + + m_authenticator = QByteArray(reinterpret_cast(authenticator), crypto_auth_hmacsha256_BYTES); + if (m_debug) qCDebug(dcNuki()) << " Authenticator :" << NukiUtils::convertByteArrayToHexStringCompact(m_authenticator); + return true; +} + +void NukiAuthenticator::requestPublicKey() +{ + qCDebug(dcNuki()) << "Authenticator: Request public key fom Nuki"; + + QByteArray payload; + QDataStream stream(&payload, QIODevice::WriteOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream << static_cast(NukiUtils::CommandPublicKey); + QByteArray data = NukiUtils::createRequestMessageForUnencrypted(NukiUtils::CommandRequestData, payload); + if (m_debug) qCDebug(dcNuki()) << "-->" << NukiUtils::convertByteArrayToHexStringCompact(data); + m_pairingCharacteristic->writeCharacteristic(data); +} + +void NukiAuthenticator::sendPublicKey() +{ + qCDebug(dcNuki()) << "Authenticator: Send public key to Nuki"; + QByteArray data = NukiUtils::createRequestMessageForUnencrypted(NukiUtils::CommandPublicKey, m_publicKey); + if (m_debug) qCDebug(dcNuki()) << "-->" << NukiUtils::convertByteArrayToHexStringCompact(data); + m_pairingCharacteristic->writeCharacteristic(data); +} + +void NukiAuthenticator::generateKeyPair() +{ + qCDebug(dcNuki()) << "Generate key pair"; + unsigned char publicKey[crypto_box_PUBLICKEYBYTES]; + unsigned char secretKey[crypto_box_SECRETKEYBYTES]; + crypto_box_keypair(publicKey, secretKey); + m_publicKey = QByteArray(reinterpret_cast(publicKey), crypto_box_PUBLICKEYBYTES); + m_privateKey = QByteArray(reinterpret_cast(secretKey), crypto_box_SECRETKEYBYTES); + + if (m_debug) qCDebug(dcNuki()) << " Private key :" << NukiUtils::convertByteArrayToHexStringCompact(m_privateKey); + if (m_debug) qCDebug(dcNuki()) << " Public key :" << NukiUtils::convertByteArrayToHexStringCompact(m_publicKey); + if (m_debug) qCDebug(dcNuki()) << " Nuki public key :" << NukiUtils::convertByteArrayToHexStringCompact(m_publicKeyNuki); +} + +void NukiAuthenticator::sendAuthorizationAuthenticator() +{ + QByteArray valueR; + valueR.append(m_publicKey); + valueR.append(m_publicKeyNuki); + valueR.append(m_nonceNuki); + + // Create authenticator and store it into m_authenticator + if (!createAuthenticator(valueR)) { + qCWarning(dcNuki()) << "Could not create authenticator hash HMAC-SHA-256"; + setState(AuthenticationStateError); + } + + // Send the authenticator + qCDebug(dcNuki()) << "Authenticator: Send authorization authenticator to Nuki"; + QByteArray message = NukiUtils::createRequestMessageForUnencrypted(NukiUtils::CommandAuthorizationAuthenticator, m_authenticator); + if (m_debug) qCDebug(dcNuki()) << "-->" << NukiUtils::convertByteArrayToHexStringCompact(message); + m_pairingCharacteristic->writeCharacteristic(message); +} + +void NukiAuthenticator::sendAuthenticateData() +{ + // Calculate new nounce + m_nonce = generateNonce(); + + QByteArray content; + QDataStream stream(&content, QIODevice::WriteOnly); + // Note: 0x00 = App, 0x01 = Bridge, 0x02 = Fob + stream << static_cast(0x01); + // Note: app id (42) + stream << static_cast(0x002A); + + // Note: the name of the bridge in 32 bytes [ 0 0 0 ... n y m e a ] + QByteArray name = QByteArray(27, '\0').append(QByteArray("nymea")); + Q_ASSERT_X(name.count() == 32, "data length", "Name has not the correct length."); + + QByteArray valueR = content; + valueR.append(name); + valueR.append(m_nonce); + valueR.append(m_nonceNuki); + + if (m_debug) qCDebug(dcNuki()) << " Name :" << qUtf8Printable(name) << NukiUtils::convertByteArrayToHexStringCompact(name); + if (m_debug) qCDebug(dcNuki()) << " Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(m_nonce); + + // Create authenticator and store it into m_authenticator + if (!createAuthenticator(valueR)) { + qCWarning(dcNuki()) << "Could not create authenticator hash HMAC-SHA-256"; + setState(AuthenticationStateError); + } + + // Prepare message to send to Nuki + QByteArray data; + data.append(m_authenticator); + data.append(content); + data.append(name); + data.append(m_nonce); + + qCDebug(dcNuki()) << "Authenticator: Send authentication data to Nuki"; + QByteArray message = NukiUtils::createRequestMessageForUnencrypted(NukiUtils::CommandAuthorizationData, data); + if (m_debug) qCDebug(dcNuki()) << "-->" << NukiUtils::convertByteArrayToHexStringCompact(message); + m_pairingCharacteristic->writeCharacteristic(message); +} + +void NukiAuthenticator::sendAuthoizationIdConfirm() +{ + qCDebug(dcNuki()) << "Authenticator: Create data for authentication ID confirm"; + QByteArray valueR; + valueR.append(m_authorizationIdRawData); + valueR.append(m_nonceNuki); + + // Create authenticator and store it into m_authenticator + if (!createAuthenticator(valueR)) { + qCWarning(dcNuki()) << "Could not create authenticator hash HMAC-SHA-256"; + setState(AuthenticationStateError); + } + + // Calculate new nounce + m_nonce = generateNonce(); + + if (m_debug) qCDebug(dcNuki()) << " Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(m_nonce); + if (m_debug) qCDebug(dcNuki()) << " Nuki Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(m_nonceNuki); + if (m_debug) qCDebug(dcNuki()) << " Authorization ID:" << NukiUtils::convertByteArrayToHexStringCompact(m_authorizationIdRawData) << m_authorizationId; + + // Prepare message to send to Nuki + QByteArray data; + data.append(m_authenticator); + data.append(m_authorizationIdRawData); + + qCDebug(dcNuki()) << "Authenticator: Send authentication ID confirm to Nuki"; + QByteArray message = NukiUtils::createRequestMessageForUnencrypted(NukiUtils::CommandAuthorizationIdConfirmation, data); + if (m_debug) qCDebug(dcNuki()) << "-->" << NukiUtils::convertByteArrayToHexStringCompact(message); + m_pairingCharacteristic->writeCharacteristic(message); +} + +void NukiAuthenticator::saveData() +{ + QSettings setting(NymeaSettings::settingsPath() + "/plugin-nuki.conf", QSettings::IniFormat); + setting.beginGroup(m_hostInfo.address().toString()); + setting.setValue("privateKey", m_privateKey); + setting.setValue("publicKey", m_publicKey); + setting.setValue("publicKeyNuki", m_publicKeyNuki); + setting.setValue("authenticationIdRawData", m_authorizationIdRawData); + setting.setValue("authenticationId", m_authorizationId); + setting.setValue("uuid", m_uuid); + setting.endGroup(); + + qCDebug(dcNuki()) << "Authenticator: Settings saved to" << setting.fileName(); +} + +void NukiAuthenticator::loadData() +{ + QSettings setting(NymeaSettings::settingsPath() + "/plugin-nuki.conf", QSettings::IniFormat); + setting.beginGroup(m_hostInfo.address().toString()); + m_privateKey = setting.value("privateKey", QByteArray()).toByteArray(); + m_publicKey = setting.value("publicKey", QByteArray()).toByteArray(); + m_publicKeyNuki = setting.value("publicKeyNuki", QByteArray()).toByteArray(); + m_authorizationIdRawData = setting.value("authenticationIdRawData", QByteArray()).toByteArray(); + m_authorizationId = static_cast(setting.value("authenticationId", 0).toInt()); + m_uuid = setting.value("uuid", QByteArray()).toByteArray(); + setting.endGroup(); + + qCDebug(dcNuki()) << "Authenticator: Settings loaded from" << setting.fileName(); +} + +void NukiAuthenticator::onPairingDataCharacteristicChanged(const QByteArray &value) +{ + if (m_debug) qCDebug(dcNuki()) << "Authenticator data received: <--" << NukiUtils::convertByteArrayToHexStringCompact(value); + + // Process pairing characteristic data + QByteArray data = QByteArray(value); + QDataStream stream(&data, QIODevice::ReadOnly); + stream.setByteOrder(QDataStream::LittleEndian); + quint16 command; + stream >> command; + + // Check if we are collecting data for multi part notification + if (m_currentReceivingCurrentCount > 0) { + command = m_currentReceivingCommand; + } + + if (m_debug) qCDebug(dcNuki()) << static_cast(command); + + switch (command) { + case NukiUtils::CommandErrorReport: + quint8 error; + quint16 commandIdentifier; + quint16 crc; + stream >> error >> commandIdentifier >> crc; + if (!NukiUtils::validateMessageCrc(data)) { + qCWarning(dcNuki()) << "Invalid message"; + // FIXME: check what to do if crc is invalid + } + m_error = static_cast(error); + qCWarning(dcNuki()) << "Authenticator: Error for command" << static_cast(commandIdentifier) << m_error; + setState(AuthenticationStateError); + break; + case NukiUtils::CommandPublicKey: + m_currentReceivingCurrentCount++; + m_currentReceivingData.append(value); + if (m_currentReceivingCurrentCount == m_currentReceivingExpectedCount) { + if (!NukiUtils::validateMessageCrc(m_currentReceivingData)) { + qCWarning(dcNuki()) << "Invalid CRC CCITT value for public key message."; + // FIXME: check what to do if crc is invalid + } + + qCDebug(dcNuki()) << "Authenticator: Nuki public key message received" << (m_debug ? NukiUtils::convertByteArrayToHexStringCompact(m_currentReceivingData) : ""); + m_publicKeyNuki = m_currentReceivingData.mid(2, 32); + if (m_debug) qCDebug(dcNuki()) << "Authenticator: --> Nuki public key:" << NukiUtils::convertByteArrayToHexStringCompact(m_publicKeyNuki); + + setState(AuthenticationStateGenerateKeyPair); + } + break; + case NukiUtils::CommandChallenge: + m_currentReceivingCurrentCount++; + m_currentReceivingData.append(value); + if (m_currentReceivingCurrentCount == m_currentReceivingExpectedCount) { + qCDebug(dcNuki()) << "Authenticator: Nuki challenge message received" << (m_debug ? NukiUtils::convertByteArrayToHexStringCompact(m_currentReceivingData) : ""); + if (!NukiUtils::validateMessageCrc(m_currentReceivingData)) { + qCWarning(dcNuki()) << "Invalid CRC CCITT value for challenge message."; + // FIXME: check what to do if crc is invalid + } + + m_nonceNuki = m_currentReceivingData.mid(2, 32); + if (m_debug) qCDebug(dcNuki()) << "Authenticator: --> Nuki nonce:" << NukiUtils::convertByteArrayToHexStringCompact(m_nonceNuki); + // Check if this was from the first challenge read or the second + if (m_state == AuthenticationStateReadChallenge) { + setState(AuthenticationStateAutorization); + } else if (m_state == AuthenticationStateReadSecondChallenge) { + setState(AuthenticationStateAuthenticateData); + } else { + qCWarning(dcNuki()) << "Received a challenge without expecting one."; + setState(AuthenticationStateError); + } + } + break; + case NukiUtils::CommandAuthorizationId: + m_currentReceivingCurrentCount++; + m_currentReceivingData.append(value); + if (m_currentReceivingCurrentCount == m_currentReceivingExpectedCount) { + qCDebug(dcNuki()) << "Authenticator: Nuki authorization ID message received" << (m_debug ? NukiUtils::convertByteArrayToHexStringCompact(m_currentReceivingData) : ""); + + if (!NukiUtils::validateMessageCrc(m_currentReceivingData)) { + qCWarning(dcNuki()) << "Invalid CRC CCITT value for challenge message."; + // FIXME: check what to do if crc is invalid + } + + // Parse data + QByteArray message = m_currentReceivingData.mid(2, m_currentReceivingData.count() - 4); + QByteArray authenticator = message.left(32); + Q_ASSERT_X(authenticator.count() == 32, "data length", "Nuki nonce has not the correct length."); + + // Read authorization ID + m_authorizationIdRawData = message.mid(32, 4); + QDataStream stream(&m_authorizationIdRawData, QIODevice::ReadOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream >> m_authorizationId; + + m_uuid = message.mid(36, 16); + Q_ASSERT_X(m_uuid.count() == 16, "data length", "UUIS has not the correct length."); + + m_nonceNuki = message.mid(52, 32); + Q_ASSERT_X(m_nonceNuki.count() == 32, "data length", "Nuki nonce has not the correct length."); + + if (m_debug) qCDebug(dcNuki()) << " Full message :" << NukiUtils::convertByteArrayToHexStringCompact(message); + if (m_debug) qCDebug(dcNuki()) << " Authenticator :" << NukiUtils::convertByteArrayToHexStringCompact(authenticator); + if (m_debug) qCDebug(dcNuki()) << " Authorization ID:" << NukiUtils::convertByteArrayToHexStringCompact(m_authorizationIdRawData) << m_authorizationId; + if (m_debug) qCDebug(dcNuki()) << " UUID data :" << NukiUtils::convertByteArrayToHexStringCompact(m_uuid); + if (m_debug) qCDebug(dcNuki()) << " Nuki nonce :" << NukiUtils::convertByteArrayToHexStringCompact(m_nonceNuki); + + setState(AuthenticationStateAuthorizationIdConfirm); + } + break; + case NukiUtils::CommandStatus: { + quint8 status; + stream >> status; + + if (!NukiUtils::validateMessageCrc(data)) { + qCWarning(dcNuki()) << "Invalid message"; + // FIXME: check what to do if crc is invalid + } + + NukiUtils::StatusCode statusCode = static_cast(status); + if (m_debug) qCDebug(dcNuki()) << statusCode; + switch (statusCode) { + case NukiUtils::StatusCodeAccepted: + qCWarning(dcNuki()) << "The command was accepted, but not completed."; + setState(AuthenticationStateError); + break; + case NukiUtils::StatusCodeCompeted: + qCDebug(dcNuki()) << "Nuki authentication process finished successfully!"; + saveData(); + setState(AuthenticationStateAuthenticated); + emit authenticationProcessFinished(true); + break; + default: + break; + } + break; + } + default: + qCWarning(dcNuki()) << "Authenticator: Unhandled command identifier for parining charateristic" << NukiUtils::convertUint16ToHexString(command); + resetExpectedData(); + break; + } +} diff --git a/nuki/nukiauthenticator.h b/nuki/nukiauthenticator.h new file mode 100644 index 00000000..ab0eccc7 --- /dev/null +++ b/nuki/nukiauthenticator.h @@ -0,0 +1,132 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 Simon Stürz * + * * + * This file is part of guh. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef NUKIAUTHENTICATOR_H +#define NUKIAUTHENTICATOR_H + +#include +#include +#include + +#include "nukiutils.h" +#include "bluez/bluetoothgattcharacteristic.h" + +class NukiAuthenticator : public QObject +{ + Q_OBJECT +public: + enum AuthenticationState { + AuthenticationStateUnauthenticated, + AuthenticationStateAuthenticated, + AuthenticationStateRequestPublicKey, + AuthenticationStateGenerateKeyPair, + AuthenticationStateSendPublicKey, + AuthenticationStateReadChallenge, + AuthenticationStateAutorization, + AuthenticationStateReadSecondChallenge, + AuthenticationStateAuthenticateData, + AuthenticationStateAuthorizationId, + AuthenticationStateAuthorizationIdConfirm, + AuthenticationStateStatus, + AuthenticationStateError + }; + Q_ENUM(AuthenticationState) + + explicit NukiAuthenticator(const QBluetoothHostInfo &hostInfo, BluetoothGattCharacteristic *pairingCharacteristic, QObject *parent = nullptr); + + NukiUtils::ErrorCode error() const; + AuthenticationState state() const; + + // Returns true if authentication end encryption data are available + bool isValid() const; + + void clearSettings(); + void startAuthenticationProcess(); + + quint32 authorizationId() const; + QByteArray authorizationIdRawData() const; + + QByteArray encryptData(const QByteArray &data, const QByteArray &nonce); + QByteArray decryptData(const QByteArray &data, const QByteArray &nonce); + + // Generate 32 byte nonce data + QByteArray generateNonce(const int &length = 32) const; + +private: + QBluetoothHostInfo m_hostInfo; + BluetoothGattCharacteristic *m_pairingCharacteristic = nullptr; + AuthenticationState m_state = AuthenticationStateUnauthenticated; + NukiUtils::ErrorCode m_error = NukiUtils::ErrorCodeNoError; + + // For handling splited notifications + NukiUtils::Command m_currentReceivingCommand = NukiUtils::CommandRequestData; + QByteArray m_currentReceivingData; + int m_currentReceivingExpectedCount = 0; + int m_currentReceivingCurrentCount = 0; + + // For development debugging + bool m_debug = false; + + // Local data + QByteArray m_privateKey; + QByteArray m_publicKey; + QByteArray m_sharedKey; + QByteArray m_authenticator; + QByteArray m_nonce; + QByteArray m_uuid; + QByteArray m_authorizationIdRawData; + quint32 m_authorizationId = 0; + + // Nuki data + QByteArray m_publicKeyNuki; + QByteArray m_nonceNuki; + + // State machine + void setState(AuthenticationState state); + void resetExpectedData(NukiUtils::Command command = NukiUtils::CommandRequestData, int expectedCount = 1); + + // Helper methods + bool createAuthenticator(const QByteArray content); + + // State action methods + void requestPublicKey(); + void sendPublicKey(); + void generateKeyPair(); + void sendAuthorizationAuthenticator(); + void sendAuthenticateData(); + void sendAuthoizationIdConfirm(); + + // Storage + void saveData(); + void loadData(); + +signals: + void errorOccured(NukiUtils::ErrorCode error); + void stateChanged(AuthenticationState state); + void authenticationProcessFinished(bool success); + +private slots: + void onPairingDataCharacteristicChanged(const QByteArray &value); + +}; + +#endif // NUKIAUTHENTICATOR_H diff --git a/nuki/nukicontroller.cpp b/nuki/nukicontroller.cpp new file mode 100644 index 00000000..2bcd2b44 --- /dev/null +++ b/nuki/nukicontroller.cpp @@ -0,0 +1,524 @@ +#include "nukicontroller.h" +#include "extern-plugininfo.h" + +#include +#include + +extern "C" { +#include "sodium.h" +} + +NukiController::NukiController(NukiAuthenticator *nukiAuthenticator, BluetoothGattCharacteristic *userDataCharacteristic, QObject *parent) : + QObject(parent), + m_nukiAuthenticator(nukiAuthenticator), + m_userDataCharacteristic(userDataCharacteristic) +{ +#ifdef QT_DEBUG + // Enable full debug messages containing sensible data for debug builds + m_debug = true; +#endif + + connect(m_userDataCharacteristic, &BluetoothGattCharacteristic::valueChanged, this, &NukiController::onUserDataCharacteristicChanged); +} + +NukiUtils::NukiState NukiController::nukiState() const +{ + return m_nukiState; +} + +NukiUtils::LockState NukiController::nukiLockState() const +{ + return m_nukiLockState; +} + +NukiUtils::LockTrigger NukiController::nukiLockTrigger() const +{ + return m_nukiLockTrigger; +} + +bool NukiController::batteryCritical() const +{ + return m_batteryCritical; +} + +bool NukiController::readLockState() +{ + if (m_state != NukiControllerStateIdle) { + // TODO: maybe queue commands + qCWarning(dcNuki()) << "Controller: Could not read lock state, Nuki is currenty busy"; + return false; + } + + if (!m_nukiAuthenticator->isValid()) { + qCWarning(dcNuki()) << "Invalid authenticator. Please authenticate the device first."; + return false; + } + + setState(NukiControllerStateReadingLockStates); + return true; +} + +bool NukiController::lock() +{ + if (m_state != NukiControllerStateIdle) { + // TODO: maybe queue commands + qCWarning(dcNuki()) << "Controller: Could not lock, Nuki is currenty busy"; + return false; + } + + if (!m_nukiAuthenticator->isValid()) { + qCWarning(dcNuki()) << "Invalid authenticator. Please authenticate the device first."; + return false; + } + + setState(NukiControllerStateLockActionRequestChallange); + return true; +} + +bool NukiController::unlock() +{ + if (m_state != NukiControllerStateIdle) { + // TODO: maybe queue commands + qCWarning(dcNuki()) << "Controller: Could not lock, Nuki is currenty busy"; + return false; + } + + if (!m_nukiAuthenticator->isValid()) { + qCWarning(dcNuki()) << "Invalid authenticator. Please authenticate the device first."; + return false; + } + + setState(NukiControllerStateUnlockActionRequestChallange); + return true; +} + +bool NukiController::unlatch() +{ + if (m_state != NukiControllerStateIdle) { + // TODO: maybe queue commands + qCWarning(dcNuki()) << "Controller: Could not unlatch, Nuki is currenty busy"; + return false; + } + + if (!m_nukiAuthenticator->isValid()) { + qCWarning(dcNuki()) << "Invalid authenticator. Please authenticate the device first."; + return false; + } + + setState(NukiControllerStateUnlatchActionRequestChallange); + return true; +} + +void NukiController::setState(NukiController::NukiControllerState state) +{ + if (m_state == state) + return; + + m_state = state; + + qCDebug(dcNuki()) << m_state; + + switch (m_state) { + case NukiControllerStateIdle: + break; + case NukiControllerStateReadingLockStates: + sendReadLockStateRequest(); + break; + case NukiControllerStateLockActionRequestChallange: + sendRequestChallengeRequest(); + break; + case NukiControllerStateLockActionExecute: + sendLockActionRequest(NukiUtils::LockActionLock); + setState(NukiControllerStateLockActionAccepted); + break; + case NukiControllerStateLockActionAccepted: + break; + case NukiControllerStateUnlockActionRequestChallange: + sendRequestChallengeRequest(); + break; + case NukiControllerStateUnlockActionExecute: + sendLockActionRequest(NukiUtils::LockActionUnlock); + setState(NukiControllerStateUnlockActionAccepted); + break; + case NukiControllerStateUnlockActionAccepted: + break; + case NukiControllerStateUnlatchActionRequestChallange: + sendRequestChallengeRequest(); + break; + case NukiControllerStateUnlatchActionExecute: + sendLockActionRequest(NukiUtils::LockActionUnlatch); + setState(NukiControllerStateUnlatchActionAccepted); + break; + case NukiControllerStateUnlatchActionAccepted: + break; + default: + break; + } + + emit stateChanged(m_state); +} + +void NukiController::resetMessageBuffer() +{ + m_messageBuffer.clear(); + m_messageBufferNonce.clear(); + m_messageBufferIdentifier = 0; + m_messageBufferLength = 0; + m_messageBufferCounter = 0; +} + +void NukiController::processNukiStatesData(const QByteArray &data) +{ + quint8 nukiState = 0; + quint8 nukiLockState = 0; + quint8 nukiLockTrigger = 0; + quint16 year = 1970; + quint8 month = 1; + quint8 day = 1; + quint8 hour = 0; + quint8 minute = 0; + quint8 second = 0; + qint16 utcOffset = 0; + quint8 batteryCritical = 0; + + QByteArray payload = data; + QDataStream stream(&payload, QIODevice::ReadOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream >> nukiState >> nukiLockState >> nukiLockTrigger >> year >> month >> day >> hour >> minute >> second >> utcOffset >> batteryCritical; + + m_nukiState = static_cast(nukiState); + m_nukiLockState = static_cast(nukiLockState); + m_nukiLockTrigger = static_cast(nukiLockTrigger); + m_nukiDateTime = QDateTime(QDate(year, month, day), QTime(hour, minute, second)); + m_nukiUtcOffset = utcOffset; + m_batteryCritical = (batteryCritical == 0 ? false : true); + + if (m_debug) qCDebug(dcNuki()) << "--------------------:" << m_state; + if (m_debug) qCDebug(dcNuki()) << " Nuki state :" << m_nukiState; + if (m_debug) qCDebug(dcNuki()) << " Nuki lock state :" << m_nukiLockState; + if (m_debug) qCDebug(dcNuki()) << " Lock trigger :" << m_nukiLockTrigger; + if (m_debug) qCDebug(dcNuki()) << " Date time :" << m_nukiDateTime.toString("dd.MM.yyyy hh:mm:ss") << "UTC offset:" << m_nukiUtcOffset; + if (m_debug) qCDebug(dcNuki()) << " Battery critical:" << m_batteryCritical; + + qCDebug(dcNuki()) << "Nuki states refreshed."; + + emit nukiStatesChanged(); +} + +void NukiController::processNukiErrorReport(const QByteArray &data) +{ + qint8 errorCode; + quint16 nukiCommand; + + QByteArray payload = data; + QDataStream stream(&payload, QIODevice::ReadOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream >> errorCode >> nukiCommand; + + qCDebug(dcNuki()) << "Received error report" << static_cast(errorCode) << static_cast(nukiCommand); +} + +void NukiController::processUserDataNotification(const QByteArray nonce, quint32 authorizationIdentifier, const QByteArray &privateData) +{ + QByteArray decryptedMessage = m_nukiAuthenticator->decryptData(privateData, nonce); + + // Process decrypted data + if (!NukiUtils::validateMessageCrc(decryptedMessage)) { + qCWarning(dcNuki()) << "Controller: User notification data has invalid CRC CCITT value. Rejecting data."; + return; + } + + // We have the unencrypted and valid PDATA, let's see what this is + quint32 decryptedAuthenticationId = NukiUtils::convertByteArrayToUint32BigEndian(decryptedMessage.left(4)); + NukiUtils::Command command = static_cast(NukiUtils::convertByteArrayToUint16BigEndian(decryptedMessage.mid(4, 2))); + QByteArray payload = decryptedMessage.mid(6, decryptedMessage.length() - 8); + + qCDebug(dcNuki()) << "Controller: Processing notification" << command; + + if (m_debug) qCDebug(dcNuki()) << " Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(nonce); + if (m_debug) qCDebug(dcNuki()) << " Authorization ID:" << authorizationIdentifier; + if (m_debug) qCDebug(dcNuki()) << " Encrypted data :" << NukiUtils::convertByteArrayToHexStringCompact(privateData) << privateData.length(); + if (m_debug) qCDebug(dcNuki()) << " Decrypted data :" << NukiUtils::convertByteArrayToHexStringCompact(decryptedMessage) << decryptedMessage.length(); + if (m_debug) qCDebug(dcNuki()) << " Command :" << command; + if (m_debug) qCDebug(dcNuki()) << " Authorization ID:" << NukiUtils::convertByteArrayToHexStringCompact(decryptedMessage.left(4)) << decryptedAuthenticationId; + if (m_debug) qCDebug(dcNuki()) << " Payload :" << NukiUtils::convertByteArrayToHexStringCompact(payload); + + // Lets see if this was an expected + switch (m_state) { + case NukiControllerStateReadingLockStates: + // We are expecting the states notification + if (command == NukiUtils::CommandNukiStates) { + processNukiStatesData(payload); + emit readNukiStatesFinished(true); + setState(NukiControllerStateIdle); + return; + } + break; + case NukiControllerStateLockActionRequestChallange: + // We are expecting callange message + if (command == NukiUtils::CommandChallenge) { + m_nukiNonce = payload; + setState(NukiControllerStateLockActionExecute); + return; + } + break; + case NukiControllerStateLockActionAccepted: + // We are expecting the status + if (command == NukiUtils::CommandStatus) { + NukiUtils::StatusCode statusCode = static_cast((quint8)payload.at(0)); + qCDebug(dcNuki()) << "Controller:" << statusCode; + + switch (statusCode) { + case NukiUtils::StatusCodeAccepted: + // Lets wait for completed + break; + case NukiUtils::StatusCodeCompeted: + emit lockFinished(true); + setState(NukiControllerStateIdle); + break; + default: + break; + } + } + break; + case NukiControllerStateUnlockActionRequestChallange: + // We are expecting callenge message + if (command == NukiUtils::CommandChallenge) { + m_nukiNonce = payload; + setState(NukiControllerStateUnlockActionExecute); + return; + } + break; + case NukiControllerStateUnlockActionAccepted: + // We are expecting the status + if (command == NukiUtils::CommandStatus) { + NukiUtils::StatusCode statusCode = static_cast((quint8)payload.at(0)); + qCDebug(dcNuki()) << "Controller:" << statusCode; + + switch (statusCode) { + case NukiUtils::StatusCodeAccepted: + // Lets wait for completed + break; + case NukiUtils::StatusCodeCompeted: + emit unlockFinished(true); + setState(NukiControllerStateIdle); + break; + default: + break; + } + } + break; + case NukiControllerStateUnlatchActionRequestChallange: + // We are expecting callenge message + if (command == NukiUtils::CommandChallenge) { + m_nukiNonce = payload; + setState(NukiControllerStateUnlatchActionExecute); + return; + } + break; + case NukiControllerStateUnlatchActionAccepted: + // We are expecting the status + if (command == NukiUtils::CommandStatus) { + NukiUtils::StatusCode statusCode = static_cast((quint8)payload.at(0)); + qCDebug(dcNuki()) << "Controller:" << statusCode; + + switch (statusCode) { + case NukiUtils::StatusCodeAccepted: + // Lets wait for completed + break; + case NukiUtils::StatusCodeCompeted: + emit unlatchFinished(true); + setState(NukiControllerStateIdle); + break; + default: + break; + } + } + break; + default: + break; + } + + // Other notification + switch (command) { + case NukiUtils::CommandNukiStates: + processNukiStatesData(payload); + break; + case NukiUtils::CommandErrorReport: + processNukiErrorReport(payload); + break; + case NukiUtils::CommandStatus: { + NukiUtils::StatusCode statusCode = static_cast((quint8)payload.at(0)); + qCDebug(dcNuki()) << "Controller:" << statusCode; + break; + } + default: + qCWarning(dcNuki()) << "Controller: Received unhandled notification:" << command; + break; + } +} + +void NukiController::sendReadLockStateRequest() +{ + qCDebug(dcNuki()) << "Controller: Reading lock state"; + + // Create data for encryption + QByteArray payload; + QDataStream stream(&payload, QIODevice::WriteOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream << static_cast(NukiUtils::CommandNukiStates); + + // Create unencrypted PDATA + QByteArray unencryptedMessage = NukiUtils::createRequestMessageForUnencryptedForEncryption(m_nukiAuthenticator->authorizationId(), NukiUtils::CommandRequestData, payload); + + // Encrypt PDATA + QByteArray nonce = m_nukiAuthenticator->generateNonce(crypto_box_NONCEBYTES); + QByteArray encryptedMessage = m_nukiAuthenticator->encryptData(unencryptedMessage, nonce); + + // Create ADATA + QByteArray header; + header.append(nonce); + header.append(m_nukiAuthenticator->authorizationIdRawData()); + header.append(NukiUtils::converUint16ToByteArrayLittleEndian(static_cast(encryptedMessage.length()))); + + // Message ADATA + PDATA + QByteArray message; + message.append(header); + message.append(encryptedMessage); + + // Send data + qCDebug(dcNuki()) << "Controller: Sending read lock states request"; + if (m_debug) qCDebug(dcNuki()) << " Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(nonce); + if (m_debug) qCDebug(dcNuki()) << " Header :" << NukiUtils::convertByteArrayToHexStringCompact(header); + if (m_debug) qCDebug(dcNuki()) << "Controller: -->" << NukiUtils::convertByteArrayToHexStringCompact(message); + m_userDataCharacteristic->writeCharacteristic(message); +} + +void NukiController::sendRequestChallengeRequest() +{ + qCDebug(dcNuki()) << "Controller: Request challenge"; + + // Create data for encryption + QByteArray payload; + QDataStream stream(&payload, QIODevice::WriteOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream << static_cast(NukiUtils::CommandChallenge); + + // Create unencrypted PDATA + QByteArray unencryptedMessage = NukiUtils::createRequestMessageForUnencryptedForEncryption(m_nukiAuthenticator->authorizationId(), NukiUtils::CommandRequestData, payload); + + // Encrypt PDATA + QByteArray nonce = m_nukiAuthenticator->generateNonce(crypto_box_NONCEBYTES); + QByteArray encryptedMessage = m_nukiAuthenticator->encryptData(unencryptedMessage, nonce); + + // Create ADATA + QByteArray header; + header.append(nonce); + header.append(m_nukiAuthenticator->authorizationIdRawData()); + header.append(NukiUtils::converUint16ToByteArrayLittleEndian(static_cast(encryptedMessage.length()))); + + // Message ADATA + PDATA + QByteArray message; + message.append(header); + message.append(encryptedMessage); + + // Send data + qCDebug(dcNuki()) << "Controller: Sending challange request"; + if (m_debug) qCDebug(dcNuki()) << " Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(nonce); + if (m_debug) qCDebug(dcNuki()) << " Header :" << NukiUtils::convertByteArrayToHexStringCompact(header); + if (m_debug) qCDebug(dcNuki()) << "Controller: -->" << NukiUtils::convertByteArrayToHexStringCompact(message); + m_userDataCharacteristic->writeCharacteristic(message); +} + +void NukiController::sendLockActionRequest(NukiUtils::LockAction lockAction, quint8 flag) +{ + qCDebug(dcNuki()) << "Controller: Send lock request" << lockAction; + + QByteArray nonce = m_nukiAuthenticator->generateNonce(crypto_box_NONCEBYTES); + + // Create data for encryption + QByteArray payload; + QDataStream stream(&payload, QIODevice::WriteOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream << static_cast(lockAction); + stream << static_cast(m_nukiAuthenticator->authorizationId()); + stream << flag; + + for (int i = 0; i < m_nukiNonce.length(); i++) { + stream << static_cast(m_nukiNonce.at(i)); + } + + // Create unencrypted PDATA + QByteArray unencryptedMessage = NukiUtils::createRequestMessageForUnencryptedForEncryption(m_nukiAuthenticator->authorizationId(), NukiUtils::CommandLockAction, payload); + + // Encrypt PDATA + QByteArray encryptedMessage = m_nukiAuthenticator->encryptData(unencryptedMessage, nonce); + + // Create ADATA + QByteArray header; + header.append(nonce); + header.append(m_nukiAuthenticator->authorizationIdRawData()); + header.append(NukiUtils::converUint16ToByteArrayLittleEndian(static_cast(encryptedMessage.length()))); + + // Message ADATA + PDATA + QByteArray message; + message.append(header); + message.append(encryptedMessage); + + // Send data + qCDebug(dcNuki()) << "Controller: Sending lock request"; + if (m_debug) qCDebug(dcNuki()) << " Nonce :" << NukiUtils::convertByteArrayToHexStringCompact(nonce); + if (m_debug) qCDebug(dcNuki()) << " Header :" << NukiUtils::convertByteArrayToHexStringCompact(header); + if (m_debug) qCDebug(dcNuki()) << "Controller: -->" << NukiUtils::convertByteArrayToHexStringCompact(message); + + m_userDataCharacteristic->writeCharacteristic(message); +} + +void NukiController::onUserDataCharacteristicChanged(const QByteArray &value) +{ + if (m_debug) qCDebug(dcNuki()) << "Controller: Data received: <--" << NukiUtils::convertByteArrayToHexStringCompact(value); + + if (m_messageBufferCounter <= 0) { + // New data arrived + m_messageBuffer.append(value); + m_messageBufferCounter++; + } else { + // We are currently collecting + m_messageBuffer.append(value); + m_messageBufferCounter++; + + // In the second buffer message is the complete message length + if (m_messageBufferCounter == 2) { + if (m_messageBuffer.count() < 30) { + qCWarning(dcNuki()) << "Controller: Cannot understand message. Rejecting."; + resetMessageBuffer(); + return; + } + + // Parse message length + // ADATA: 24 byte nonce, 4 byte autorization, 2 byte encrypted message length + m_messageBufferAData = m_messageBuffer.left(30); + m_messageBufferPData = m_messageBuffer.right(m_messageBuffer.count() - 30); + + m_messageBufferNonce = m_messageBufferAData.left(24); + QByteArray messageInformation = m_messageBufferAData.right(6); + + QDataStream stream(&messageInformation, QIODevice::ReadOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream >> m_messageBufferIdentifier >> m_messageBufferLength; + if (m_messageBufferPData.count() == m_messageBufferLength) { + processUserDataNotification(m_messageBufferNonce, m_messageBufferIdentifier, m_messageBufferPData); + resetMessageBuffer(); + } + + } else { + // We already know the message length and are still collecting p data + m_messageBufferPData.append(value); + if (m_messageBufferPData.count() == m_messageBufferLength) { + // Message finished + processUserDataNotification(m_messageBufferNonce, m_messageBufferIdentifier, m_messageBufferPData); + resetMessageBuffer(); + } + } + } +} diff --git a/nuki/nukicontroller.h b/nuki/nukicontroller.h new file mode 100644 index 00000000..73d1786c --- /dev/null +++ b/nuki/nukicontroller.h @@ -0,0 +1,113 @@ +#ifndef NUKICONTROLLER_H +#define NUKICONTROLLER_H + +#include +#include +#include + +#include "nukiutils.h" +#include "nukiauthenticator.h" +#include "bluez/bluetoothgattcharacteristic.h" + +class NukiController : public QObject +{ + Q_OBJECT +public: + enum NukiControllerState { + NukiControllerStateIdle, + + // Read state + NukiControllerStateReadingLockStates, + + // Lock action + NukiControllerStateLockActionRequestChallange, + NukiControllerStateLockActionExecute, + NukiControllerStateLockActionAccepted, + + // Unlock action + NukiControllerStateUnlockActionRequestChallange, + NukiControllerStateUnlockActionExecute, + NukiControllerStateUnlockActionAccepted, + + // Unlatch action + NukiControllerStateUnlatchActionRequestChallange, + NukiControllerStateUnlatchActionExecute, + NukiControllerStateUnlatchActionAccepted, + + NukiControllerStateError + }; + Q_ENUM(NukiControllerState) + + explicit NukiController(NukiAuthenticator *nukiAuthenticator, BluetoothGattCharacteristic *userDataCharacteristic, QObject *parent = nullptr); + + // States + NukiUtils::NukiState nukiState() const; + NukiUtils::LockState nukiLockState() const; + NukiUtils::LockTrigger nukiLockTrigger() const; + QDateTime nukiDateTime() const; + int nukiUtcOffset() const; + bool batteryCritical() const; + + // Actions + bool readLockState(); + bool lock(); + bool unlock(); + bool unlatch(); + +private: + NukiAuthenticator *m_nukiAuthenticator = nullptr; + BluetoothGattCharacteristic *m_userDataCharacteristic = nullptr; + + NukiControllerState m_state = NukiControllerStateIdle; + + // Notification parsing helper + QByteArray m_messageBuffer; + QByteArray m_messageBufferAData; + QByteArray m_messageBufferPData; + QByteArray m_messageBufferNonce; + quint32 m_messageBufferIdentifier = 0; + quint16 m_messageBufferLength = 0; + int m_messageBufferCounter = 0; + + // For development debugging + bool m_debug = false; + + // Properties + NukiUtils::NukiState m_nukiState = NukiUtils::NukiStateUninitialized; + NukiUtils::LockState m_nukiLockState = NukiUtils::LockStateUndefined; + NukiUtils::LockTrigger m_nukiLockTrigger = NukiUtils::LockTriggerBluetooth; + QDateTime m_nukiDateTime; + int m_nukiUtcOffset = 0; + bool m_batteryCritical = false; + + QByteArray m_nukiNonce; + + // State machine helpers + void setState(NukiControllerState state); + void resetExpectedData(int expectedCount = 1); + void resetMessageBuffer(); + + // Data processors + void processNukiStatesData(const QByteArray &data); + void processNukiErrorReport(const QByteArray &data); + void processUserDataNotification(const QByteArray nonce, quint32 authorizationIdentifier, const QByteArray &privateData); + + // State action methods + void sendReadLockStateRequest(); + void sendRequestChallengeRequest(); + void sendLockActionRequest(NukiUtils::LockAction lockAction, quint8 flag = 0); + +signals: + void stateChanged(NukiControllerState state); + void readNukiStatesFinished(bool success); + void lockFinished(bool success); + void unlockFinished(bool success); + void unlatchFinished(bool success); + void nukiStatesChanged(); + +private slots: + void onUserDataCharacteristicChanged(const QByteArray &value); + +}; + +#endif // NUKICONTROLLER_H diff --git a/nuki/nukiutils.cpp b/nuki/nukiutils.cpp new file mode 100644 index 00000000..8296ed1c --- /dev/null +++ b/nuki/nukiutils.cpp @@ -0,0 +1,171 @@ +#include "nukiutils.h" +#include "extern-plugininfo.h" + +#include +#include + +QString NukiUtils::convertByteToHexString(const quint8 &byte) +{ + QString hexString(QStringLiteral("0x%1")); + hexString = hexString.arg(byte, 2, 16, QLatin1Char('0')); + return hexString.toStdString().data(); +} + +QString NukiUtils::convertByteArrayToHexString(const QByteArray &byteArray) +{ + QString hexString; + for (int i = 0; i < byteArray.count(); i++) { + hexString.append(convertByteToHexString(static_cast(byteArray.at(i)))); + if (i != byteArray.count() - 1) { + hexString.append(" "); + } + } + + return hexString.toStdString().data(); +} + +QString NukiUtils::convertByteArrayToHexStringCompact(const QByteArray &byteArray) +{ + QString hexString; + for (int i = 0; i < byteArray.length(); i++) { + hexString.append(QString("%1").arg(static_cast(byteArray.at(i)), 2, 16, QLatin1Char('0'))); + } + + return hexString; +} + +QString NukiUtils::convertUint16ToHexString(const quint16 &value) +{ + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + stream << value; + + return QString("0x%1").arg(convertByteArrayToHexString(data).remove(" ").remove("0x")); +} + +QByteArray NukiUtils::converUint32ToByteArrayLittleEndian(const quint32 &value) +{ + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream << value; + Q_ASSERT_X(data.length() == 4, "data converting", "Could not convert quint32 value to byte array (little endian)"); + return data; +} + +QByteArray NukiUtils::converUint16ToByteArrayLittleEndian(const quint16 &value) +{ + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream << value; + Q_ASSERT_X(data.length() == 2, "data converting", "Could not convert quint16 value to byte array (little endian)"); + return data; +} + +quint16 NukiUtils::convertByteArrayToUint16BigEndian(const QByteArray &littleEndianByteArray) +{ + Q_ASSERT_X(littleEndianByteArray.length() == 2, "data converting", "Could not convert byte array (little endian) to quint16 value. Invalid size of byte array."); + + quint16 value = 0; + QByteArray data(littleEndianByteArray); + QDataStream stream(&data, QIODevice::ReadOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream >> value; + return value; +} + +quint32 NukiUtils::convertByteArrayToUint32BigEndian(const QByteArray &littleEndianByteArray) +{ + Q_ASSERT_X(littleEndianByteArray.length() == 4, "data converting", "Could not convert byte array (little endian) to quint32 value. Invalid size of byte array."); + + quint32 value = 0; + QByteArray data(littleEndianByteArray); + QDataStream stream(&data, QIODevice::ReadOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream >> value; + return value; +} + +quint16 NukiUtils::calculateCrc(const QByteArray &data) +{ + quint16 crcValue = 0xffff; + quint16 polynom = 0x1021; + + for (int byte = 0; byte < data.length(); ++byte) { + crcValue ^= (static_cast(data.at(byte)) << 8); + for (quint8 bit = 8; bit > 0; --bit) { + if (crcValue & 0x8000) { + crcValue = (crcValue << 1) ^ polynom; + } else { + crcValue = (crcValue << 1); + } + } + } + + return crcValue; +} + +bool NukiUtils::validateMessageCrc(const QByteArray &message) +{ + quint16 crcValue = 0; + QByteArray crcValueRaw = message.right(2); + QDataStream stream(&crcValueRaw, QIODevice::ReadOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream >> crcValue; + + QByteArray content = message.left(message.count() - 2); + + quint16 calculatedCrcValue = calculateCrc(content); + if (crcValue != calculatedCrcValue) { + qCWarning(dcNuki()) << "CRC CCITT validation failed:" << crcValue << "!=" << calculatedCrcValue; + return false; + } + + return true; +} + +QByteArray NukiUtils::createRequestMessageForUnencrypted(NukiUtils::Command command, const QByteArray &payload) +{ + /* Note: build a message for unencrypted communication with paring service + * 2 Bytes: command identifier (LittleEndian) + * n Bytes: pyload (raw bytes) + * 2 Bytes: crc (LittleEndian) + */ + QByteArray message; + QDataStream stream(&message, QIODevice::WriteOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream << static_cast(command); + + // Write payload + for (int i = 0; i < payload.length(); i++) { + stream << static_cast(payload.at(i)); + } + + stream << NukiUtils::calculateCrc(message); + return message; +} + +QByteArray NukiUtils::createRequestMessageForUnencryptedForEncryption(quint32 authenticationId, NukiUtils::Command command, const QByteArray &payload) +{ + /* Note: build a message for encrypted communication with key turner service. This represents the unencrypted PDATA + * 4 Bytes: authentication ID (LittleEndian) + * 2 Bytes: command identifier (LittleEndian) + * n Bytes: pyxload (raw bytes) + * 2 Bytes: crc (LittleEndian) + */ + + QByteArray message; + QDataStream stream(&message, QIODevice::WriteOnly); + stream.setByteOrder(QDataStream::LittleEndian); + stream << authenticationId; + stream << static_cast(command); + + // Write payload + for (int i = 0; i < payload.length(); i++) { + stream << static_cast(payload.at(i)); + } + + stream << NukiUtils::calculateCrc(message); + return message; +} diff --git a/nuki/nukiutils.h b/nuki/nukiutils.h new file mode 100644 index 00000000..c14c576d --- /dev/null +++ b/nuki/nukiutils.h @@ -0,0 +1,167 @@ +#ifndef NUKIUTILS_H +#define NUKIUTILS_H + +#include +#include +#include +#include + +class NukiUtils +{ + Q_GADGET +public: + enum StatusCode { + StatusCodeCompeted = 0x00, + StatusCodeAccepted = 0x01 + }; + Q_ENUM(StatusCode) + + + enum ErrorCode { + ErrorCodeNoError = 0x00, + + // Pairing service error codes + ErrorCodeNotInPairingMode = 0x10, + ErrorCodeBadAuthenticator = 0x11, + ErrorCodeBadPairingParameter = 0x12, + ErrorCodeMaxUsersReached = 0x13, + + // Key turner service error codes + ErrorCodeNotAuthorized = 0x20, + ErrorCodeBadPin = 0x21, + ErrorCodeBadNonce = 0x22, + ErrorCodeBadParameter = 0x23, + ErrorCodeInvalidAuthId = 0x24, + ErrorCodeDisabled = 0x25, + ErrorCodeRemoteNotAllowed = 0x26, + ErrorCodeTimeNotAllowed = 0x27, + ErrorCodeTooManyPinAttempts = 0x28, + ErrorCodeAutoUnlockTooRecent = 0x40, + ErrorCodePositionUnknown = 0x41, + ErrorCodeMotorBlocked = 0x42, + ErrorCodeClutchFailure = 0x43, + ErrorCodeMotorTimeout = 0x44, + ErrorCodeBusy = 0x45, + + // General error codes + ErrorCodeBadCrc = 0xFD, + ErrorCodeBadLength = 0xFE, + ErrorCodeUnknown = 0xFF + }; + Q_ENUM(ErrorCode) + + + enum LockState { + LockStateUncalibrated = 0x00, + LockStateLocked = 0x01, + LockStateUnlocking = 0x02, + LockStateUnlocked = 0x03, + LockStateLocking = 0x04, + LockStateUnlatched = 0x05, + LockStateUnlockedLocknGoActive = 0x06, + LockStateUnlatching = 0x07, + LockStateMotorBlocked = 0xfe, + LockStateUndefined = 0xff + }; + Q_ENUM(LockState) + + + enum LockAction { + LockActionUnlock = 0x01, + LockActionLock = 0x02, + LockActionUnlatch = 0x03, + LockActionLockNGo = 0x04, + LockActionLockNGoWithUnlatch = 0x05, + LockActionFobAction1 = 0x81, + LockActionFobAction2 = 0x82, + LockActionFobAction3 = 0x83 + }; + Q_ENUM(LockAction) + + + enum LockTrigger { + LockTriggerBluetooth = 0x00, + LockTriggerManual = 0x01, + LockTriggerButton = 0x02 + }; + Q_ENUM(LockTrigger) + + + enum NukiState { + NukiStateUninitialized = 0x00, + NukiStatePairingMode = 0x01, + NukiStateDoorMode = 0x02 + }; + Q_ENUM(NukiState) + + + enum Command { + CommandRequestData = 0x0001, + CommandPublicKey = 0x0003, + CommandChallenge = 0x0004, + CommandAuthorizationAuthenticator = 0x0005, + CommandAuthorizationData = 0x0006, + CommandAuthorizationId = 0x0007, + CommandRemoveUserAuthorization = 0x0008, + CommandRequestAuthorizationEntries = 0x0009, + CommandAuthorizationEntry = 0x000A, + CommandAuthorizationDataInvite = 0x000B, + CommandNukiStates = 0x000C, + CommandLockAction = 0x000D, + CommandStatus = 0x000E, + CommandMostRecentCommand = 0x000F, + CommandOpeningsClosingsSummary = 0x0010, + CommandBatteryReport = 0x0011, + CommandErrorReport = 0x0012, + CommandSetConG = 0x0013, + CommandRequestConG = 0x0014, + CommandConG = 0x0015, + CommandSetSecurityPIN = 0x0019, + CommandRequestCalibration = 0x001A, + CommandRequestReboot = 0x001D, + CommandAuthorizationIdConfirmation = 0x001E, + CommandAuthorizationIdInvite = 0x001F, + CommandVerifySecurityPIN = 0x0020, + CommandUpdateTime = 0x0021, + CommandUpdateUserAuthorization = 0x0025, + CommandAuthorizationEntryCount = 0x0027, + CommandRequestDisconnect = 0x0030, + CommandRequestLogEntries = 0x0031, + CommandLogEntry = 0x0032, + CommandLogEntryCount = 0x0033, + CommandEnableLogging = 0x0034, + CommandSetAdvancedConG = 0x0035, + CommandRequestAdvancedConG = 0x0036, + CommandAdvancedConG = 0x0037, + CommandAddTimeControlEntry = 0x0039, + CommandTimeControlEntryId = 0x003A, + CommandRemoveTimeControlEntry = 0x003B, + CommandRequestTimeControlEntries = 0x003C, + CommandTimeControlEntryCount = 0x003D, + CommandTimeControlEntry = 0x003E, + CommandUpdateTimeControlEntry = 0x003F + }; + Q_ENUM(Command) + + // Data helpers + static QString convertByteToHexString(const quint8 &byte); + static QString convertByteArrayToHexString(const QByteArray &byteArray); + static QString convertByteArrayToHexStringCompact(const QByteArray &byteArray); + static QString convertUint16ToHexString(const quint16 &value); + + static QByteArray converUint32ToByteArrayLittleEndian(const quint32 &value); + static QByteArray converUint16ToByteArrayLittleEndian(const quint16 &value); + static quint16 convertByteArrayToUint16BigEndian(const QByteArray &littleEndianByteArray); + static quint32 convertByteArrayToUint32BigEndian(const QByteArray &littleEndianByteArray); + + // Crc calculation + static quint16 calculateCrc(const QByteArray &data); + static bool validateMessageCrc(const QByteArray &message); + + // Message helper + static QByteArray createRequestMessageForUnencrypted(NukiUtils::Command command, const QByteArray &payload); + static QByteArray createRequestMessageForUnencryptedForEncryption(quint32 authenticationId, NukiUtils::Command command, const QByteArray &payload); + +}; + +#endif // NUKIUTILS_H diff --git a/nuki/plugins.pri b/nuki/plugins.pri new file mode 100644 index 00000000..7826456c --- /dev/null +++ b/nuki/plugins.pri @@ -0,0 +1,63 @@ +TEMPLATE = lib +CONFIG += plugin + +QT += network bluetooth dbus + +QMAKE_CXXFLAGS += -Werror -std=c++11 -g +QMAKE_LFLAGS += -std=c++11 + +INCLUDEPATH += /usr/include/nymea +LIBS += -lnymea + +PLUGIN_PATH=/usr/lib/$$system('dpkg-architecture -q DEB_HOST_MULTIARCH')/nymea/plugins/ + +# Check if this is a snap build +snappy{ + INCLUDEPATH+=$$(SNAPCRAFT_STAGE)/usr/include/nymea +} + +# Make the device plugin json file visible in the Qt Creator +OTHER_FILES+=$$PWD/deviceplugin"$$TARGET".json + +# NOTE: if the code includes "plugininfo.h", it would fail if we only give it a compiler for $$OUT_PWD/plugininfo.h +# Let's add a dummy target with the plugininfo.h file without any path to allow the developer to just include it like that. + +# Create plugininfo file +plugininfo.target = $$OUT_PWD/plugininfo.h +plugininfo_dummy.target = plugininfo.h +plugininfo.depends = FORCE +plugininfo.commands = nymea-generateplugininfo --filetype i --jsonfile $$PWD/deviceplugin"$$TARGET".json --output plugininfo.h --builddir $$OUT_PWD +plugininfo_dummy.commands = $$plugininfo.commands +QMAKE_EXTRA_TARGETS += plugininfo plugininfo_dummy + +# Create extern-plugininfo file +extern_plugininfo.target = $$OUT_PWD/extern-plugininfo.h +extern_plugininfo_dummy.target = extern-plugininfo.h +extern_plugininfo.depends = FORCE +extern_plugininfo.commands = nymea-generateplugininfo --filetype e --jsonfile $$PWD/deviceplugin"$$TARGET".json --output extern-plugininfo.h --builddir $$OUT_PWD +extern_plugininfo_dummy.commands = $$extern_plugininfo.commands +QMAKE_EXTRA_TARGETS += extern_plugininfo extern_plugininfo_dummy + +# Install translation files +TRANSLATIONS *= $$files($${PWD}/translations/*ts, true) +lupdate.depends = FORCE +lupdate.depends += plugininfo +lupdate.commands = lupdate -recursive -no-obsolete -locations none $$PWD/"$$TARGET".pro; +QMAKE_EXTRA_TARGETS += lupdate + +# make lrelease to build .qm from .ts +lrelease.depends = FORCE +lrelease.commands += lrelease $$files($$PWD/translations/*.ts, true); +lrelease.commands += rsync -a $$PWD/translations/*.qm $$OUT_PWD/translations/; +QMAKE_EXTRA_TARGETS += lrelease + +translations.path = /usr/share/nymea/translations +translations.files = $$[QT_SOURCE_TREE]/translations/*.qm + +HEADERS += $$OUT_PWD/plugininfo.h \ + $$OUT_PWD/extern-plugininfo.h +DEPENDPATH += $$OUT_PWD + +# Install plugin +target.path = $$PLUGIN_PATH +INSTALLS += target translations diff --git a/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-cs.ts b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-cs.ts new file mode 100644 index 00000000..6f8f31bf --- /dev/null +++ b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-cs.ts @@ -0,0 +1,169 @@ + + + + + DevicePluginNuki + + Device is already in use. + + + + Bluetooth is not available on this system. + + + + Bluetooth device not found. + + + + Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue. + Stiskněte tlačítko Nuki na 5 sekund k aktivování režimu párování dříve než budete pokračovat. + + + + Nuki + + Nuki + The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52}) +---------- +The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625}) + + + + Smartlock + The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767}) + Smartlock + + + Name + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3}) + Název + + + MAC address + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5}) + MAC adresa + + + Serial number + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf}) + Sériové číslo + + + Connected changed + The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Připojeno změněno + + + Connected + The name of the ParamType (DeviceClass: nuki, EventType: connected, ID: {d5fbd774-4e87-4c7c-8c92-f094352897f6}) +---------- +The name of the StateType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Připojeno + + + Battery critical changed + The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Baterie kritický stav změněno + + + Battery critical + The name of the ParamType (DeviceClass: nuki, EventType: batteryCritical, ID: {e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) +---------- +The name of the StateType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Baterie kritický stav + + + Status changed + The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Status změněn + + + Status + The name of the ParamType (DeviceClass: nuki, EventType: status, ID: {a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) +---------- +The name of the StateType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Status + + + Mode changed + The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Režim změněn + + + Mode + The name of the ParamType (DeviceClass: nuki, EventType: mode, ID: {4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) +---------- +The name of the StateType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Režim + + + Trigger changed + The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Spoušť změněna + + + Trigger + The name of the ParamType (DeviceClass: nuki, EventType: trigger, ID: {b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) +---------- +The name of the StateType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Spoušť + + + Hardware revision changed + The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Revize hardwaru změněna + + + Hardware revision + The name of the ParamType (DeviceClass: nuki, EventType: hardwareRevision, ID: {b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) +---------- +The name of the StateType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Revize hardwaru + + + Firmware revision changed + The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Revize firmwaru změněna + + + Firmware revision + The name of the ParamType (DeviceClass: nuki, EventType: firmwareRevision, ID: {9dc0115b-cdb4-472c-961b-b182f77a576f}) +---------- +The name of the StateType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Revize firmwaru + + + Lock + The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki + Zamknout + + + Unlock + The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki + Odemknout + + + Refresh + The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki + Obnovit + + + State changed + The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Stav změněn + + + State + The name of the ParamType (DeviceClass: nuki, EventType: state, ID: {07a09bd8-b342-4e33-92cd-0af21f8689fa}) +---------- +The name of the StateType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Stav + + + Open door + The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki + Otevřít dveře + + + diff --git a/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-da.ts b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-da.ts new file mode 100644 index 00000000..e670822f --- /dev/null +++ b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-da.ts @@ -0,0 +1,169 @@ + + + + + DevicePluginNuki + + Device is already in use. + + + + Bluetooth is not available on this system. + + + + Bluetooth device not found. + + + + Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue. + Tryk på Nuki-knappen i fem sekunder for at aktivere parringstilstanden, før du fortsætter. + + + + Nuki + + Nuki + The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52}) +---------- +The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625}) + + + + Smartlock + The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767}) + Smartlock + + + Name + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3}) + Navn + + + MAC address + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5}) + mac-adresse + + + Serial number + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf}) + Serienummer + + + Connected changed + The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Forbindelse ændret + + + Connected + The name of the ParamType (DeviceClass: nuki, EventType: connected, ID: {d5fbd774-4e87-4c7c-8c92-f094352897f6}) +---------- +The name of the StateType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Forbundet + + + Battery critical changed + The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Batteri kritisk ændret + + + Battery critical + The name of the ParamType (DeviceClass: nuki, EventType: batteryCritical, ID: {e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) +---------- +The name of the StateType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Batteri kritisk + + + Status changed + The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Status ændret + + + Status + The name of the ParamType (DeviceClass: nuki, EventType: status, ID: {a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) +---------- +The name of the StateType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Status + + + Mode changed + The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modus ændret + + + Mode + The name of the ParamType (DeviceClass: nuki, EventType: mode, ID: {4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) +---------- +The name of the StateType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modus + + + Trigger changed + The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Udløser ændret + + + Trigger + The name of the ParamType (DeviceClass: nuki, EventType: trigger, ID: {b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) +---------- +The name of the StateType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Udløser + + + Hardware revision changed + The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + hardwarerevision ændret + + + Hardware revision + The name of the ParamType (DeviceClass: nuki, EventType: hardwareRevision, ID: {b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) +---------- +The name of the StateType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Hardwarerevision + + + Firmware revision changed + The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Firmwarerevision ændret + + + Firmware revision + The name of the ParamType (DeviceClass: nuki, EventType: firmwareRevision, ID: {9dc0115b-cdb4-472c-961b-b182f77a576f}) +---------- +The name of the StateType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Firmwarerevision + + + Lock + The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki + Lås + + + Unlock + The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki + Lås op + + + Refresh + The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki + Opdatér + + + State changed + The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Tilstand ændret + + + State + The name of the ParamType (DeviceClass: nuki, EventType: state, ID: {07a09bd8-b342-4e33-92cd-0af21f8689fa}) +---------- +The name of the StateType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Tilstand + + + Open door + The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki + Åbn dør + + + diff --git a/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-de.ts b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-de.ts new file mode 100644 index 00000000..f1ee49bc --- /dev/null +++ b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-de.ts @@ -0,0 +1,169 @@ + + + + + DevicePluginNuki + + Device is already in use. + + + + Bluetooth is not available on this system. + + + + Bluetooth device not found. + + + + Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue. + Bevor es weitergeht drücke bitte die Nuki-Taste für 5 Sekunden um den Pairing-Modus zu aktivieren. + + + + Nuki + + Nuki + The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52}) +---------- +The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625}) + Nuki + + + Smartlock + The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767}) + Smartlock + + + Name + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3}) + Name + + + MAC address + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5}) + MAC Adresse + + + Serial number + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf}) + Seriennummer + + + Connected changed + The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Verbunden geändert + + + Connected + The name of the ParamType (DeviceClass: nuki, EventType: connected, ID: {d5fbd774-4e87-4c7c-8c92-f094352897f6}) +---------- +The name of the StateType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Verbunden + + + Battery critical changed + The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Batterie Ladung kritisch geändert + + + Battery critical + The name of the ParamType (DeviceClass: nuki, EventType: batteryCritical, ID: {e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) +---------- +The name of the StateType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Batterie Ladung kritisch + + + Status changed + The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Status geändert + + + Status + The name of the ParamType (DeviceClass: nuki, EventType: status, ID: {a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) +---------- +The name of the StateType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Status + + + Mode changed + The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modus geändert + + + Mode + The name of the ParamType (DeviceClass: nuki, EventType: mode, ID: {4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) +---------- +The name of the StateType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modus + + + Trigger changed + The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Auslöser geändert + + + Trigger + The name of the ParamType (DeviceClass: nuki, EventType: trigger, ID: {b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) +---------- +The name of the StateType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Auslöser + + + Hardware revision changed + The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Hardware-Revision geändert + + + Hardware revision + The name of the ParamType (DeviceClass: nuki, EventType: hardwareRevision, ID: {b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) +---------- +The name of the StateType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Hardware-Revision + + + Firmware revision changed + The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Firmware-Version geändert + + + Firmware revision + The name of the ParamType (DeviceClass: nuki, EventType: firmwareRevision, ID: {9dc0115b-cdb4-472c-961b-b182f77a576f}) +---------- +The name of the StateType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Firmware-Version + + + Lock + The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki + Zusperren + + + Unlock + The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki + Aufsperren + + + Refresh + The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki + Aktualisieren + + + State changed + The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Status geändert + + + State + The name of the ParamType (DeviceClass: nuki, EventType: state, ID: {07a09bd8-b342-4e33-92cd-0af21f8689fa}) +---------- +The name of the StateType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Status + + + Open door + The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki + Tür öffnen + + + diff --git a/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-en_US.ts b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-en_US.ts new file mode 100644 index 00000000..7b08d112 --- /dev/null +++ b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-en_US.ts @@ -0,0 +1,169 @@ + + + + + DevicePluginNuki + + Device is already in use. + + + + Bluetooth is not available on this system. + + + + Bluetooth device not found. + + + + Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue. + + + + + Nuki + + Nuki + The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52}) +---------- +The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625}) + + + + Smartlock + The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767}) + + + + Name + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3}) + + + + MAC address + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5}) + + + + Serial number + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf}) + + + + Connected changed + The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + + + + Connected + The name of the ParamType (DeviceClass: nuki, EventType: connected, ID: {d5fbd774-4e87-4c7c-8c92-f094352897f6}) +---------- +The name of the StateType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + + + + Battery critical changed + The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + + + + Battery critical + The name of the ParamType (DeviceClass: nuki, EventType: batteryCritical, ID: {e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) +---------- +The name of the StateType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + + + + Status changed + The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + + + + Status + The name of the ParamType (DeviceClass: nuki, EventType: status, ID: {a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) +---------- +The name of the StateType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + + + + Mode changed + The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + + + + Mode + The name of the ParamType (DeviceClass: nuki, EventType: mode, ID: {4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) +---------- +The name of the StateType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + + + + Trigger changed + The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + + + + Trigger + The name of the ParamType (DeviceClass: nuki, EventType: trigger, ID: {b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) +---------- +The name of the StateType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + + + + Hardware revision changed + The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + + + + Hardware revision + The name of the ParamType (DeviceClass: nuki, EventType: hardwareRevision, ID: {b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) +---------- +The name of the StateType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + + + + Firmware revision changed + The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + + + + Firmware revision + The name of the ParamType (DeviceClass: nuki, EventType: firmwareRevision, ID: {9dc0115b-cdb4-472c-961b-b182f77a576f}) +---------- +The name of the StateType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + + + + Lock + The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki + + + + Unlock + The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki + + + + Refresh + The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki + + + + State changed + The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + + + + State + The name of the ParamType (DeviceClass: nuki, EventType: state, ID: {07a09bd8-b342-4e33-92cd-0af21f8689fa}) +---------- +The name of the StateType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + + + + Open door + The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki + + + + diff --git a/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-es.ts b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-es.ts new file mode 100644 index 00000000..00c3f6ed --- /dev/null +++ b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-es.ts @@ -0,0 +1,169 @@ + + + + + DevicePluginNuki + + Device is already in use. + + + + Bluetooth is not available on this system. + + + + Bluetooth device not found. + + + + Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue. + Pulse el botón Nuki durante 5 segundos para activar el modo de enlazado antes de continuar. + + + + Nuki + + Nuki + The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52}) +---------- +The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625}) + + + + Smartlock + The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767}) + Smartlock + + + Name + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3}) + Nombre + + + MAC address + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5}) + Dirección MAC + + + Serial number + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf}) + Número de serie + + + Connected changed + The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Conexión modificada + + + Connected + The name of the ParamType (DeviceClass: nuki, EventType: connected, ID: {d5fbd774-4e87-4c7c-8c92-f094352897f6}) +---------- +The name of the StateType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Conectado + + + Battery critical changed + The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Batería en estado crítico modificada + + + Battery critical + The name of the ParamType (DeviceClass: nuki, EventType: batteryCritical, ID: {e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) +---------- +The name of the StateType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Batería en estado crítico + + + Status changed + The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Estado modificado + + + Status + The name of the ParamType (DeviceClass: nuki, EventType: status, ID: {a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) +---------- +The name of the StateType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Estado + + + Mode changed + The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modo modificado + + + Mode + The name of the ParamType (DeviceClass: nuki, EventType: mode, ID: {4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) +---------- +The name of the StateType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modo + + + Trigger changed + The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Disparador modificado + + + Trigger + The name of the ParamType (DeviceClass: nuki, EventType: trigger, ID: {b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) +---------- +The name of the StateType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Disparador + + + Hardware revision changed + The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Revisión de hardware modificada + + + Hardware revision + The name of the ParamType (DeviceClass: nuki, EventType: hardwareRevision, ID: {b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) +---------- +The name of the StateType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Revisión de hardware + + + Firmware revision changed + The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Revisión de firmware modificada + + + Firmware revision + The name of the ParamType (DeviceClass: nuki, EventType: firmwareRevision, ID: {9dc0115b-cdb4-472c-961b-b182f77a576f}) +---------- +The name of the StateType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Revisión de firmware + + + Lock + The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki + Bloqueo + + + Unlock + The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki + Desbloqueo + + + Refresh + The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki + Actualizar + + + State changed + The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Estado modificado + + + State + The name of the ParamType (DeviceClass: nuki, EventType: state, ID: {07a09bd8-b342-4e33-92cd-0af21f8689fa}) +---------- +The name of the StateType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Estado + + + Open door + The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki + Abrir puerta + + + diff --git a/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-fr.ts b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-fr.ts new file mode 100644 index 00000000..5d8b8ad3 --- /dev/null +++ b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-fr.ts @@ -0,0 +1,169 @@ + + + + + DevicePluginNuki + + Device is already in use. + + + + Bluetooth is not available on this system. + + + + Bluetooth device not found. + + + + Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue. + Veuillez appuyer pendant 5 secondes sur le bouton Nuki afin d'activer le mode de couplage avant de continuer. + + + + Nuki + + Nuki + The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52}) +---------- +The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625}) + + + + Smartlock + The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767}) + Smartlock + + + Name + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3}) + Nom + + + MAC address + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5}) + Adresse MAC + + + Serial number + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf}) + Numéro de serie + + + Connected changed + The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Connexion modifiée + + + Connected + The name of the ParamType (DeviceClass: nuki, EventType: connected, ID: {d5fbd774-4e87-4c7c-8c92-f094352897f6}) +---------- +The name of the StateType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Connecté + + + Battery critical changed + The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Statut de batterie critique modifié + + + Battery critical + The name of the ParamType (DeviceClass: nuki, EventType: batteryCritical, ID: {e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) +---------- +The name of the StateType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Batterie critique + + + Status changed + The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Statut modifié + + + Status + The name of the ParamType (DeviceClass: nuki, EventType: status, ID: {a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) +---------- +The name of the StateType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Statut + + + Mode changed + The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Mode modifié + + + Mode + The name of the ParamType (DeviceClass: nuki, EventType: mode, ID: {4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) +---------- +The name of the StateType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Mode + + + Trigger changed + The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Déclencheur modifié + + + Trigger + The name of the ParamType (DeviceClass: nuki, EventType: trigger, ID: {b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) +---------- +The name of the StateType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Déclencheur + + + Hardware revision changed + The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Révision du matériel (hardware) modifiée + + + Hardware revision + The name of the ParamType (DeviceClass: nuki, EventType: hardwareRevision, ID: {b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) +---------- +The name of the StateType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Révision du matériel (hardware) + + + Firmware revision changed + The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Version du progiciel modifiée + + + Firmware revision + The name of the ParamType (DeviceClass: nuki, EventType: firmwareRevision, ID: {9dc0115b-cdb4-472c-961b-b182f77a576f}) +---------- +The name of the StateType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Version du progiciel + + + Lock + The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki + Verrouiller + + + Unlock + The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki + Déverrouiller + + + Refresh + The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki + Rafraîchir + + + State changed + The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Statut modifié + + + State + The name of the ParamType (DeviceClass: nuki, EventType: state, ID: {07a09bd8-b342-4e33-92cd-0af21f8689fa}) +---------- +The name of the StateType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Statut + + + Open door + The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki + Ouvrir la porte + + + diff --git a/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-it.ts b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-it.ts new file mode 100644 index 00000000..1be93aa7 --- /dev/null +++ b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-it.ts @@ -0,0 +1,169 @@ + + + + + DevicePluginNuki + + Device is already in use. + + + + Bluetooth is not available on this system. + + + + Bluetooth device not found. + + + + Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue. + Premi il pulsante Nuki per 5 secondi per attivare la sincronizzazione prima di continuare. + + + + Nuki + + Nuki + The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52}) +---------- +The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625}) + + + + Smartlock + The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767}) + Smartlock + + + Name + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3}) + Nome + + + MAC address + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5}) + Indirizzo MAC + + + Serial number + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf}) + Numero di serie + + + Connected changed + The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Connesso modificato + + + Connected + The name of the ParamType (DeviceClass: nuki, EventType: connected, ID: {d5fbd774-4e87-4c7c-8c92-f094352897f6}) +---------- +The name of the StateType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Connesso + + + Battery critical changed + The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Batteria critica modificata + + + Battery critical + The name of the ParamType (DeviceClass: nuki, EventType: batteryCritical, ID: {e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) +---------- +The name of the StateType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Batteria critica + + + Status changed + The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Stato modificato + + + Status + The name of the ParamType (DeviceClass: nuki, EventType: status, ID: {a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) +---------- +The name of the StateType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Stato + + + Mode changed + The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modalità modificata + + + Mode + The name of the ParamType (DeviceClass: nuki, EventType: mode, ID: {4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) +---------- +The name of the StateType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modalità + + + Trigger changed + The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Trigger modificato + + + Trigger + The name of the ParamType (DeviceClass: nuki, EventType: trigger, ID: {b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) +---------- +The name of the StateType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Trigger + + + Hardware revision changed + The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Revisione hardware modificata + + + Hardware revision + The name of the ParamType (DeviceClass: nuki, EventType: hardwareRevision, ID: {b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) +---------- +The name of the StateType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Revisione hardware + + + Firmware revision changed + The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Revisione firmware modificata + + + Firmware revision + The name of the ParamType (DeviceClass: nuki, EventType: firmwareRevision, ID: {9dc0115b-cdb4-472c-961b-b182f77a576f}) +---------- +The name of the StateType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Revisione firmware + + + Lock + The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki + Blocca + + + Unlock + The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki + Sblocca + + + Refresh + The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki + Aggiorna + + + State changed + The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Stato modificato + + + State + The name of the ParamType (DeviceClass: nuki, EventType: state, ID: {07a09bd8-b342-4e33-92cd-0af21f8689fa}) +---------- +The name of the StateType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Stato + + + Open door + The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki + Apri porta + + + diff --git a/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-nl.ts b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-nl.ts new file mode 100644 index 00000000..1225a68d --- /dev/null +++ b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-nl.ts @@ -0,0 +1,169 @@ + + + + + DevicePluginNuki + + Device is already in use. + + + + Bluetooth is not available on this system. + + + + Bluetooth device not found. + + + + Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue. + Houd, alvorens verder te gaan, de Nuki-knop 5 seconden ingedrukt om de koppelmodus te activeren. + + + + Nuki + + Nuki + The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52}) +---------- +The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625}) + + + + Smartlock + The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767}) + Smartlock + + + Name + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3}) + Naam + + + MAC address + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5}) + MAC address + + + Serial number + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf}) + Serienummer + + + Connected changed + The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Verbonden gewijzigd + + + Connected + The name of the ParamType (DeviceClass: nuki, EventType: connected, ID: {d5fbd774-4e87-4c7c-8c92-f094352897f6}) +---------- +The name of the StateType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Verbonden + + + Battery critical changed + The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Batterij kritiek gewijzigd + + + Battery critical + The name of the ParamType (DeviceClass: nuki, EventType: batteryCritical, ID: {e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) +---------- +The name of the StateType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Batterij kritiek + + + Status changed + The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Status gewijzigd + + + Status + The name of the ParamType (DeviceClass: nuki, EventType: status, ID: {a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) +---------- +The name of the StateType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Status + + + Mode changed + The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modus gewijzigd + + + Mode + The name of the ParamType (DeviceClass: nuki, EventType: mode, ID: {4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) +---------- +The name of the StateType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modus + + + Trigger changed + The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Trigger gewijzigd + + + Trigger + The name of the ParamType (DeviceClass: nuki, EventType: trigger, ID: {b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) +---------- +The name of the StateType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Trigger + + + Hardware revision changed + The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Hardware-revisie gewijzigd + + + Hardware revision + The name of the ParamType (DeviceClass: nuki, EventType: hardwareRevision, ID: {b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) +---------- +The name of the StateType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Hardware-revisie + + + Firmware revision changed + The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Firmware-revisie gewijzigd + + + Firmware revision + The name of the ParamType (DeviceClass: nuki, EventType: firmwareRevision, ID: {9dc0115b-cdb4-472c-961b-b182f77a576f}) +---------- +The name of the StateType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Firmware-revisie + + + Lock + The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki + Vergrendelen + + + Unlock + The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki + Ontgrendelen + + + Refresh + The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki + Vernieuwen + + + State changed + The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Status gewijzigd + + + State + The name of the ParamType (DeviceClass: nuki, EventType: state, ID: {07a09bd8-b342-4e33-92cd-0af21f8689fa}) +---------- +The name of the StateType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Status + + + Open door + The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki + Deur openen + + + diff --git a/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-pt.ts b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-pt.ts new file mode 100644 index 00000000..bb4995f4 --- /dev/null +++ b/nuki/translations/e5806d75-a40e-4766-a272-5a3a8d3ed625-pt.ts @@ -0,0 +1,169 @@ + + + + + DevicePluginNuki + + Device is already in use. + + + + Bluetooth is not available on this system. + + + + Bluetooth device not found. + + + + Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue. + Pressione o botão Nuki durante 5 segundos para ativar o modo de emparelhamento antes de continuar. + + + + Nuki + + Nuki + The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52}) +---------- +The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625}) + + + + Smartlock + The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767}) + Smartlock + + + Name + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3}) + Nome + + + MAC address + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5}) + Endereço MAC + + + Serial number + The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf}) + Número de série + + + Connected changed + The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Conectado alterado + + + Connected + The name of the ParamType (DeviceClass: nuki, EventType: connected, ID: {d5fbd774-4e87-4c7c-8c92-f094352897f6}) +---------- +The name of the StateType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki + Conectado + + + Battery critical changed + The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Bateria crítica alterada + + + Battery critical + The name of the ParamType (DeviceClass: nuki, EventType: batteryCritical, ID: {e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) +---------- +The name of the StateType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki + Bateria crítica + + + Status changed + The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Estado alterado + + + Status + The name of the ParamType (DeviceClass: nuki, EventType: status, ID: {a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) +---------- +The name of the StateType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki + Estado + + + Mode changed + The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modo alterado + + + Mode + The name of the ParamType (DeviceClass: nuki, EventType: mode, ID: {4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) +---------- +The name of the StateType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki + Modo + + + Trigger changed + The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Acionamento alterado + + + Trigger + The name of the ParamType (DeviceClass: nuki, EventType: trigger, ID: {b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) +---------- +The name of the StateType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki + Acionamento + + + Hardware revision changed + The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Revisão de hardware alterada + + + Hardware revision + The name of the ParamType (DeviceClass: nuki, EventType: hardwareRevision, ID: {b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) +---------- +The name of the StateType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki + Revisão de hardware + + + Firmware revision changed + The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Revisão de firmware alterada + + + Firmware revision + The name of the ParamType (DeviceClass: nuki, EventType: firmwareRevision, ID: {9dc0115b-cdb4-472c-961b-b182f77a576f}) +---------- +The name of the StateType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki + Revisão de firmware + + + Lock + The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki + Bloquear + + + Unlock + The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki + Desbloquear + + + Refresh + The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki + Atualizar + + + State changed + The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Estado alterado + + + State + The name of the ParamType (DeviceClass: nuki, EventType: state, ID: {07a09bd8-b342-4e33-92cd-0af21f8689fa}) +---------- +The name of the StateType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki + Estado + + + Open door + The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki + Abrir porta + + + diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 60a276e2..b24bdf29 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -30,6 +30,7 @@ PLUGIN_DIRS = \ mqttclient \ netatmo \ networkdetector \ + nuki \ onewire \ openuv \ openweathermap \