Add nuki plugin

Revert copyright changes
This commit is contained in:
Simon Stürz 2020-01-10 12:18:05 +01:00 committed by Michael Zanetti
parent af1570e372
commit e838e0ddd2
43 changed files with 7516 additions and 0 deletions

17
debian/control vendored
View File

@ -14,6 +14,7 @@ Build-depends: libboblight-dev,
qtbase5-dev, qtbase5-dev,
qtconnectivity5-dev, qtconnectivity5-dev,
libow-dev, libow-dev,
libsodium-dev,
Standards-Version: 3.9.3 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 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 Package: nymea-plugin-onewire
Architecture: any Architecture: any
@ -877,6 +893,7 @@ Depends: nymea-plugin-anel,
nymea-plugin-texasinstruments, nymea-plugin-texasinstruments,
nymea-plugin-netatmo, nymea-plugin-netatmo,
nymea-plugin-networkdetector, nymea-plugin-networkdetector,
nymea-plugin-nuki,
nymea-plugin-openuv, nymea-plugin-openuv,
nymea-plugin-openweathermap, nymea-plugin-openweathermap,
nymea-plugin-philipshue, nymea-plugin-philipshue,

1
debian/copyright vendored
View File

@ -54,6 +54,7 @@ Files: avahimonitor/*
mailnotification/* mailnotification/*
netatmo/* netatmo/*
networkdetector/* networkdetector/*
nuki/*
osdomotics/* osdomotics/*
senic/* senic/*
udpcommander/* udpcommander/*

1
debian/nymea-plugin-nuki.install.in vendored Normal file
View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginnuki.so

3
nuki/README.md Normal file
View File

@ -0,0 +1,3 @@
# nymea-plugin-nuki
Device plugin for the [Nuki Smartlock](https://nuki.io/en/) Bluetooth API.

View File

@ -0,0 +1,416 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "bluetoothadapter.h"
#include "blueztypes.h"
#include <QDBusPendingReply>
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<BluetoothDevice *> 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<void> 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;
}

View File

@ -0,0 +1,152 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef BLUETOOTHADAPTER_H
#define BLUETOOTHADAPTER_H
#include <QObject>
#include <QDebug>
#include <QDBusInterface>
#include <QDBusConnection>
#include <QDBusObjectPath>
#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<BluetoothDevice *> 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<BluetoothDevice *> 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

View File

@ -0,0 +1,546 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "bluetoothdevice.h"
#include <QDBusPendingReply>
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<QBluetoothUuid> 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<BluetoothGattService *> 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<void> 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<void> 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<void> 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<void> 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<void> 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;
}

View File

@ -0,0 +1,185 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef BLUETOOTHDEVICE_H
#define BLUETOOTHDEVICE_H
#include <QObject>
#include <QDBusInterface>
#include <QDBusPendingCall>
#include <QBluetoothAddress>
#include <QBluetoothHostInfo>
#include <QDBusPendingCallWatcher>
#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<QBluetoothUuid> 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<BluetoothGattService *> 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<BluetoothGattService *> 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<QBluetoothUuid> 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

View File

@ -0,0 +1,368 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "bluetoothgattcharacteristic.h"
#include <QDBusReply>
QString BluetoothGattCharacteristic::chararcteristicName() const
{
bool ok = false;
quint16 typeId = m_uuid.toUInt16(&ok);
if (ok) {
QBluetoothUuid::CharacteristicType uuid = static_cast<QBluetoothUuid::CharacteristicType>(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<BluetoothGattDescriptor *> 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 &notifying)
{
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<QByteArray> 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<void> 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<void> 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<void> 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;
}

View File

@ -0,0 +1,132 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef BLUETOOTHGATTCHARACTERISTIC_H
#define BLUETOOTHGATTCHARACTERISTIC_H
#include <QHash>
#include <QFlag>
#include <QObject>
#include <QBluetoothUuid>
#include <QDBusInterface>
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
#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<BluetoothGattDescriptor *> 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<BluetoothGattDescriptor *> m_descriptors;
QHash<QDBusPendingCallWatcher *, QByteArray> 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 &notifying);
Properties parsePropertyFlags(const QStringList &characteristicProperties);
signals:
void notifyingChanged(const bool &notifying);
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

View File

@ -0,0 +1,219 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "bluetoothgattdescriptor.h"
#include <QDBusReply>
QString BluetoothGattDescriptor::name() const
{
bool ok = false;
quint16 typeId = m_uuid.toUInt16(&ok);
if (ok) {
QBluetoothUuid::DescriptorType type = static_cast<QBluetoothUuid::DescriptorType>(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<QByteArray> 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<void> 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;
}

View File

@ -0,0 +1,104 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef BLUETOOTHGATTDESCRIPTOR_H
#define BLUETOOTHGATTDESCRIPTOR_H
#include <QObject>
#include <QBluetoothUuid>
#include <QDBusInterface>
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
#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<QDBusPendingCallWatcher *, QByteArray> 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

View File

@ -0,0 +1,168 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "bluetoothgattservice.h"
QString BluetoothGattService::serviceName() const
{
bool ok = false;
quint16 typeId = m_uuid.toUInt16(&ok);
if (ok) {
QBluetoothUuid::ServiceClassUuid uuid = static_cast<QBluetoothUuid::ServiceClassUuid>(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<BluetoothGattCharacteristic *> 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<BluetoothGattCharacteristic *>(sender());
emit characteristicReadFinished(characteristic, value);
}
void BluetoothGattService::onCharacteristicWriteFinished(const QByteArray &value)
{
BluetoothGattCharacteristic *characteristic = static_cast<BluetoothGattCharacteristic *>(sender());
emit characteristicWriteFinished(characteristic, value);
}
void BluetoothGattService::onCharacteristicValueChanged(const QByteArray &newValue)
{
BluetoothGattCharacteristic *characteristic = static_cast<BluetoothGattCharacteristic *>(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;
}

View File

@ -0,0 +1,96 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef BLUETOOTHGATTSERVICE_H
#define BLUETOOTHGATTSERVICE_H
#include <QObject>
#include <QBluetoothUuid>
#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<BluetoothGattCharacteristic *> 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<BluetoothGattCharacteristic *> 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

View File

@ -0,0 +1,306 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "bluetoothmanager.h"
#include <QDBusObjectPath>
#include <QDBusArgument>
#include <QDBusMetaType>
BluetoothManager::BluetoothManager(QObject *parent) :
QObject(parent),
m_available(false)
{
qDBusRegisterMetaType<InterfaceList>();
qDBusRegisterMetaType<ManagedObjectList>();
// 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<BluetoothAdapter *> 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<QDBusArgument>();
ManagedObjectList objectList = qdbus_cast<ManagedObjectList>(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<QDBusObjectPath>(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<QDBusObjectPath>(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<QDBusObjectPath>(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<QDBusObjectPath>(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);
}
}
}
}

View File

@ -0,0 +1,85 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef BLUETOOTHMANAGER_H
#define BLUETOOTHMANAGER_H
#include <QObject>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusServiceWatcher>
#include "blueztypes.h"
#include "bluetoothadapter.h"
class BluetoothManager : public QObject
{
Q_OBJECT
public:
explicit BluetoothManager(QObject *parent = 0);
QList<BluetoothAdapter *> adapters() const;
bool isAvailable() const;
private:
QDBusInterface *m_objectManagerInterface;
QDBusServiceWatcher *m_serviceWatcher;
QList<BluetoothAdapter *> 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

32
nuki/bluez/blueztypes.cpp Normal file
View File

@ -0,0 +1,32 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "blueztypes.h"
Q_LOGGING_CATEGORY(dcBluez, "Bluez")
Bluez::Bluez()
{
}

103
nuki/bluez/blueztypes.h Normal file
View File

@ -0,0 +1,103 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2016-2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef BLUEZTYPES_H
#define BLUEZTYPES_H
#include <QDebug>
#include <QString>
#include <QDBusArgument>
#include <QDBusObjectPath>
#include <QLoggingCategory>
// 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<QString, QVariantMap> InterfaceList;
Q_DECLARE_METATYPE(InterfaceList)
typedef QMap<QDBusObjectPath, InterfaceList> ManagedObjectList;
Q_DECLARE_METATYPE(ManagedObjectList)
typedef struct {
quint16 manufacturerId;
QByteArray data;
} ManufacturerData;
Q_DECLARE_METATYPE(ManufacturerData)
typedef QList<ManufacturerData> 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

325
nuki/devicepluginnuki.cpp Normal file
View File

@ -0,0 +1,325 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Bernhard Trinnes <bernhard.trinnes@guh.io> *
* Copyright (C) 2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#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> 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;
}

78
nuki/devicepluginnuki.h Normal file
View File

@ -0,0 +1,78 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Bernhard Trinnes <benhard.trinnes@guh.io> *
* Copyright (C) 2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#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<Nuki *, Device *> 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

157
nuki/devicepluginnuki.json Normal file
View File

@ -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"
}
]
}
]
}
]
}

Binary file not shown.

Binary file not shown.

556
nuki/nuki.cpp Normal file
View File

@ -0,0 +1,556 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Bernhard Trinnes <bernhard.trinnes@guh.io> *
* Copyright (C) 2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "nuki.h"
#include "extern-plugininfo.h"
#include <QBitArray>
#include <QtEndian>
#include <QByteArray>
#include <QDataStream>
#include <QTimer>
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<DeviceActionInfo>(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);
}

136
nuki/nuki.h Normal file
View File

@ -0,0 +1,136 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Bernhard Trinnes <bernhard.trinnes@guh.io> *
* Copyright (C) 2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef NUKI_H
#define NUKI_H
#include <QObject>
#include <QBluetoothUuid>
#include <QByteArray>
#include <QUuid>
#include <QPointer>
#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<QBluetoothUuid> m_initUuidsToRead;
QString m_serialNumber;
QString m_hardwareRevision;
QString m_firmwareRevision;
bool m_available = false;
NukiAction m_nukiAction = NukiActionNone;
QPointer<DeviceActionInfo> 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

36
nuki/nuki.pro Normal file
View File

@ -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

602
nuki/nukiauthenticator.cpp Normal file
View File

@ -0,0 +1,602 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#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 <QtEndian>
#include <QSettings>
#include <QDataStream>
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<const unsigned char *>(data.data()),
static_cast<unsigned long long>(data.length()),
reinterpret_cast<const unsigned char *>(nonce.data()),
reinterpret_cast<const unsigned char *>(m_publicKeyNuki.data()),
reinterpret_cast<const unsigned char *>(m_privateKey.data()));
if (result < 0) {
qCWarning(dcNuki()) << "Could not encrypt data. Something went wrong";
return QByteArray();
}
QByteArray encryptedData = QByteArray(reinterpret_cast<const char*>(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<uint>(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<const unsigned char *>(data.data()),
static_cast<unsigned long long>(data.length()),
reinterpret_cast<const unsigned char *>(nonce.data()),
reinterpret_cast<const unsigned char *>(m_publicKeyNuki.data()),
reinterpret_cast<const unsigned char *>(m_privateKey.data()));
if (result < 0) {
qCWarning(dcNuki()) << "Could not decrypt data. Something went wrong";
return QByteArray();
}
QByteArray decryptedData = QByteArray(reinterpret_cast<const char*>(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<const char *>(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<const unsigned char *>(m_publicKeyNuki.data()), reinterpret_cast<const unsigned char *>(m_privateKey.data()));
if (result < 0) {
qCWarning(dcNuki()) << "Could not create shared key for autorization authenticator.";
return false;
}
m_sharedKey = QByteArray(reinterpret_cast<const char*>(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<const unsigned char *>(content.data()), content.length(), reinterpret_cast<const unsigned char *>(m_sharedKey.data()));
if (result < 0) {
qCWarning(dcNuki()) << "Could not create authenticator hash for autorization authenticator.";
return false;
}
m_authenticator = QByteArray(reinterpret_cast<const char*>(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<quint16>(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<const char *>(publicKey), crypto_box_PUBLICKEYBYTES);
m_privateKey = QByteArray(reinterpret_cast<const char *>(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<quint8>(0x01);
// Note: app id (42)
stream << static_cast<quint32>(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<quint32>(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<NukiUtils::Command>(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<NukiUtils::ErrorCode>(error);
qCWarning(dcNuki()) << "Authenticator: Error for command" << static_cast<NukiUtils::Command>(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<NukiUtils::StatusCode>(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;
}
}

132
nuki/nukiauthenticator.h Normal file
View File

@ -0,0 +1,132 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Simon Stürz <simon.stuerz@guh.io> *
* *
* 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 *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef NUKIAUTHENTICATOR_H
#define NUKIAUTHENTICATOR_H
#include <QObject>
#include <QByteArray>
#include <QBluetoothHostInfo>
#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

524
nuki/nukicontroller.cpp Normal file
View File

@ -0,0 +1,524 @@
#include "nukicontroller.h"
#include "extern-plugininfo.h"
#include <QByteArray>
#include <QDataStream>
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<NukiUtils::NukiState>(nukiState);
m_nukiLockState = static_cast<NukiUtils::LockState>(nukiLockState);
m_nukiLockTrigger = static_cast<NukiUtils::LockTrigger>(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<NukiUtils::ErrorCode>(errorCode) << static_cast<NukiUtils::Command>(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::Command>(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<NukiUtils::StatusCode>((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<NukiUtils::StatusCode>((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<NukiUtils::StatusCode>((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<NukiUtils::StatusCode>((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<quint16>(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<quint16>(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<quint16>(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<quint16>(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<quint8>(lockAction);
stream << static_cast<quint32>(m_nukiAuthenticator->authorizationId());
stream << flag;
for (int i = 0; i < m_nukiNonce.length(); i++) {
stream << static_cast<quint8>(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<quint16>(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();
}
}
}
}

113
nuki/nukicontroller.h Normal file
View File

@ -0,0 +1,113 @@
#ifndef NUKICONTROLLER_H
#define NUKICONTROLLER_H
#include <QObject>
#include <QDateTime>
#include <QBluetoothAddress>
#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

171
nuki/nukiutils.cpp Normal file
View File

@ -0,0 +1,171 @@
#include "nukiutils.h"
#include "extern-plugininfo.h"
#include <QtEndian>
#include <QDataStream>
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<quint8>(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<unsigned char>(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<quint8>(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<quint16>(command);
// Write payload
for (int i = 0; i < payload.length(); i++) {
stream << static_cast<quint8>(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<quint16>(command);
// Write payload
for (int i = 0; i < payload.length(); i++) {
stream << static_cast<quint8>(payload.at(i));
}
stream << NukiUtils::calculateCrc(message);
return message;
}

167
nuki/nukiutils.h Normal file
View File

@ -0,0 +1,167 @@
#ifndef NUKIUTILS_H
#define NUKIUTILS_H
#include <QObject>
#include <QString>
#include <QByteArray>
#include <QBitArray>
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

63
nuki/plugins.pri Normal file
View File

@ -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

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="cs">
<context>
<name>DevicePluginNuki</name>
<message>
<source>Device is already in use.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth is not available on this system.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth device not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue.</source>
<translation type="unfinished">Stiskněte tlačítko Nuki na 5 sekund k aktivování režimu párování dříve než budete pokračovat.</translation>
</message>
</context>
<context>
<name>Nuki</name>
<message>
<source>Nuki</source>
<extracomment>The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52})
----------
The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Smartlock</source>
<extracomment>The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767})</extracomment>
<translation>Smartlock</translation>
</message>
<message>
<source>Name</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3})</extracomment>
<translation>Název</translation>
</message>
<message>
<source>MAC address</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5})</extracomment>
<translation>MAC adresa</translation>
</message>
<message>
<source>Serial number</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf})</extracomment>
<translation>Sériové číslo</translation>
</message>
<message>
<source>Connected changed</source>
<extracomment>The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki</extracomment>
<translation>Připojeno změněno</translation>
</message>
<message>
<source>Connected</source>
<extracomment>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</extracomment>
<translation>Připojeno</translation>
</message>
<message>
<source>Battery critical changed</source>
<extracomment>The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki</extracomment>
<translation>Baterie kritický stav změněno</translation>
</message>
<message>
<source>Battery critical</source>
<extracomment>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</extracomment>
<translation>Baterie kritický stav</translation>
</message>
<message>
<source>Status changed</source>
<extracomment>The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki</extracomment>
<translation>Status změněn</translation>
</message>
<message>
<source>Status</source>
<extracomment>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</extracomment>
<translation>Status</translation>
</message>
<message>
<source>Mode changed</source>
<extracomment>The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki</extracomment>
<translation>Režim změněn</translation>
</message>
<message>
<source>Mode</source>
<extracomment>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</extracomment>
<translation>Režim</translation>
</message>
<message>
<source>Trigger changed</source>
<extracomment>The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki</extracomment>
<translation>Spoušť změněna</translation>
</message>
<message>
<source>Trigger</source>
<extracomment>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</extracomment>
<translation>Spoušť</translation>
</message>
<message>
<source>Hardware revision changed</source>
<extracomment>The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki</extracomment>
<translation>Revize hardwaru změněna</translation>
</message>
<message>
<source>Hardware revision</source>
<extracomment>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</extracomment>
<translation>Revize hardwaru</translation>
</message>
<message>
<source>Firmware revision changed</source>
<extracomment>The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki</extracomment>
<translation>Revize firmwaru změněna</translation>
</message>
<message>
<source>Firmware revision</source>
<extracomment>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</extracomment>
<translation>Revize firmwaru</translation>
</message>
<message>
<source>Lock</source>
<extracomment>The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki</extracomment>
<translation>Zamknout</translation>
</message>
<message>
<source>Unlock</source>
<extracomment>The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki</extracomment>
<translation>Odemknout</translation>
</message>
<message>
<source>Refresh</source>
<extracomment>The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki</extracomment>
<translation>Obnovit</translation>
</message>
<message>
<source>State changed</source>
<extracomment>The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki</extracomment>
<translation>Stav změněn</translation>
</message>
<message>
<source>State</source>
<extracomment>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</extracomment>
<translation>Stav</translation>
</message>
<message>
<source>Open door</source>
<extracomment>The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki</extracomment>
<translation>Otevřít dveře</translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="da">
<context>
<name>DevicePluginNuki</name>
<message>
<source>Device is already in use.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth is not available on this system.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth device not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue.</source>
<translation type="unfinished">Tryk Nuki-knappen i fem sekunder for at aktivere parringstilstanden, før du fortsætter.</translation>
</message>
</context>
<context>
<name>Nuki</name>
<message>
<source>Nuki</source>
<extracomment>The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52})
----------
The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Smartlock</source>
<extracomment>The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767})</extracomment>
<translation>Smartlock</translation>
</message>
<message>
<source>Name</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3})</extracomment>
<translation>Navn</translation>
</message>
<message>
<source>MAC address</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5})</extracomment>
<translation>mac-adresse</translation>
</message>
<message>
<source>Serial number</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf})</extracomment>
<translation>Serienummer</translation>
</message>
<message>
<source>Connected changed</source>
<extracomment>The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki</extracomment>
<translation>Forbindelse ændret</translation>
</message>
<message>
<source>Connected</source>
<extracomment>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</extracomment>
<translation>Forbundet</translation>
</message>
<message>
<source>Battery critical changed</source>
<extracomment>The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki</extracomment>
<translation>Batteri kritisk ændret</translation>
</message>
<message>
<source>Battery critical</source>
<extracomment>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</extracomment>
<translation>Batteri kritisk</translation>
</message>
<message>
<source>Status changed</source>
<extracomment>The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki</extracomment>
<translation>Status ændret</translation>
</message>
<message>
<source>Status</source>
<extracomment>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</extracomment>
<translation>Status</translation>
</message>
<message>
<source>Mode changed</source>
<extracomment>The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki</extracomment>
<translation>Modus ændret</translation>
</message>
<message>
<source>Mode</source>
<extracomment>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</extracomment>
<translation>Modus</translation>
</message>
<message>
<source>Trigger changed</source>
<extracomment>The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki</extracomment>
<translation>Udløser ændret</translation>
</message>
<message>
<source>Trigger</source>
<extracomment>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</extracomment>
<translation>Udløser</translation>
</message>
<message>
<source>Hardware revision changed</source>
<extracomment>The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki</extracomment>
<translation>hardwarerevision ændret</translation>
</message>
<message>
<source>Hardware revision</source>
<extracomment>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</extracomment>
<translation>Hardwarerevision</translation>
</message>
<message>
<source>Firmware revision changed</source>
<extracomment>The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki</extracomment>
<translation>Firmwarerevision ændret</translation>
</message>
<message>
<source>Firmware revision</source>
<extracomment>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</extracomment>
<translation>Firmwarerevision</translation>
</message>
<message>
<source>Lock</source>
<extracomment>The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki</extracomment>
<translation>Lås</translation>
</message>
<message>
<source>Unlock</source>
<extracomment>The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki</extracomment>
<translation>Lås op</translation>
</message>
<message>
<source>Refresh</source>
<extracomment>The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki</extracomment>
<translation>Opdatér</translation>
</message>
<message>
<source>State changed</source>
<extracomment>The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki</extracomment>
<translation>Tilstand ændret</translation>
</message>
<message>
<source>State</source>
<extracomment>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</extracomment>
<translation>Tilstand</translation>
</message>
<message>
<source>Open door</source>
<extracomment>The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki</extracomment>
<translation>Åbn dør</translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de">
<context>
<name>DevicePluginNuki</name>
<message>
<source>Device is already in use.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth is not available on this system.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth device not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue.</source>
<translation type="unfinished">Bevor es weitergeht drücke bitte die Nuki-Taste für 5 Sekunden um den Pairing-Modus zu aktivieren.</translation>
</message>
</context>
<context>
<name>Nuki</name>
<message>
<source>Nuki</source>
<extracomment>The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52})
----------
The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625})</extracomment>
<translation>Nuki</translation>
</message>
<message>
<source>Smartlock</source>
<extracomment>The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767})</extracomment>
<translation>Smartlock</translation>
</message>
<message>
<source>Name</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3})</extracomment>
<translation>Name</translation>
</message>
<message>
<source>MAC address</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5})</extracomment>
<translation>MAC Adresse</translation>
</message>
<message>
<source>Serial number</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf})</extracomment>
<translation>Seriennummer</translation>
</message>
<message>
<source>Connected changed</source>
<extracomment>The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki</extracomment>
<translation>Verbunden geändert</translation>
</message>
<message>
<source>Connected</source>
<extracomment>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</extracomment>
<translation>Verbunden</translation>
</message>
<message>
<source>Battery critical changed</source>
<extracomment>The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki</extracomment>
<translation>Batterie Ladung kritisch geändert</translation>
</message>
<message>
<source>Battery critical</source>
<extracomment>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</extracomment>
<translation>Batterie Ladung kritisch</translation>
</message>
<message>
<source>Status changed</source>
<extracomment>The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki</extracomment>
<translation>Status geändert</translation>
</message>
<message>
<source>Status</source>
<extracomment>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</extracomment>
<translation>Status</translation>
</message>
<message>
<source>Mode changed</source>
<extracomment>The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki</extracomment>
<translation>Modus geändert</translation>
</message>
<message>
<source>Mode</source>
<extracomment>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</extracomment>
<translation>Modus</translation>
</message>
<message>
<source>Trigger changed</source>
<extracomment>The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki</extracomment>
<translation>Auslöser geändert</translation>
</message>
<message>
<source>Trigger</source>
<extracomment>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</extracomment>
<translation>Auslöser</translation>
</message>
<message>
<source>Hardware revision changed</source>
<extracomment>The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki</extracomment>
<translation>Hardware-Revision geändert</translation>
</message>
<message>
<source>Hardware revision</source>
<extracomment>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</extracomment>
<translation>Hardware-Revision</translation>
</message>
<message>
<source>Firmware revision changed</source>
<extracomment>The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki</extracomment>
<translation>Firmware-Version geändert</translation>
</message>
<message>
<source>Firmware revision</source>
<extracomment>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</extracomment>
<translation>Firmware-Version</translation>
</message>
<message>
<source>Lock</source>
<extracomment>The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki</extracomment>
<translation>Zusperren</translation>
</message>
<message>
<source>Unlock</source>
<extracomment>The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki</extracomment>
<translation>Aufsperren</translation>
</message>
<message>
<source>Refresh</source>
<extracomment>The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki</extracomment>
<translation>Aktualisieren</translation>
</message>
<message>
<source>State changed</source>
<extracomment>The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki</extracomment>
<translation>Status geändert</translation>
</message>
<message>
<source>State</source>
<extracomment>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</extracomment>
<translation>Status</translation>
</message>
<message>
<source>Open door</source>
<extracomment>The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki</extracomment>
<translation>Tür öffnen</translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US">
<context>
<name>DevicePluginNuki</name>
<message>
<source>Device is already in use.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth is not available on this system.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth device not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Nuki</name>
<message>
<source>Nuki</source>
<extracomment>The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52})
----------
The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Smartlock</source>
<extracomment>The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Name</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>MAC address</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Serial number</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Connected changed</source>
<extracomment>The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Connected</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Battery critical changed</source>
<extracomment>The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Battery critical</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Status changed</source>
<extracomment>The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Status</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Mode changed</source>
<extracomment>The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Mode</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Trigger changed</source>
<extracomment>The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Trigger</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware revision changed</source>
<extracomment>The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware revision</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Firmware revision changed</source>
<extracomment>The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Firmware revision</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Lock</source>
<extracomment>The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unlock</source>
<extracomment>The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Refresh</source>
<extracomment>The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>State changed</source>
<extracomment>The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>State</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open door</source>
<extracomment>The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="es">
<context>
<name>DevicePluginNuki</name>
<message>
<source>Device is already in use.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth is not available on this system.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth device not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue.</source>
<translation type="unfinished">Pulse el botón Nuki durante 5 segundos para activar el modo de enlazado antes de continuar.</translation>
</message>
</context>
<context>
<name>Nuki</name>
<message>
<source>Nuki</source>
<extracomment>The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52})
----------
The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Smartlock</source>
<extracomment>The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767})</extracomment>
<translation>Smartlock</translation>
</message>
<message>
<source>Name</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3})</extracomment>
<translation>Nombre</translation>
</message>
<message>
<source>MAC address</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5})</extracomment>
<translation>Dirección MAC</translation>
</message>
<message>
<source>Serial number</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf})</extracomment>
<translation>Número de serie</translation>
</message>
<message>
<source>Connected changed</source>
<extracomment>The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki</extracomment>
<translation>Conexión modificada</translation>
</message>
<message>
<source>Connected</source>
<extracomment>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</extracomment>
<translation>Conectado</translation>
</message>
<message>
<source>Battery critical changed</source>
<extracomment>The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki</extracomment>
<translation>Batería en estado crítico modificada</translation>
</message>
<message>
<source>Battery critical</source>
<extracomment>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</extracomment>
<translation>Batería en estado crítico</translation>
</message>
<message>
<source>Status changed</source>
<extracomment>The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki</extracomment>
<translation>Estado modificado</translation>
</message>
<message>
<source>Status</source>
<extracomment>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</extracomment>
<translation>Estado</translation>
</message>
<message>
<source>Mode changed</source>
<extracomment>The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki</extracomment>
<translation>Modo modificado</translation>
</message>
<message>
<source>Mode</source>
<extracomment>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</extracomment>
<translation>Modo</translation>
</message>
<message>
<source>Trigger changed</source>
<extracomment>The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki</extracomment>
<translation>Disparador modificado</translation>
</message>
<message>
<source>Trigger</source>
<extracomment>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</extracomment>
<translation>Disparador</translation>
</message>
<message>
<source>Hardware revision changed</source>
<extracomment>The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki</extracomment>
<translation>Revisión de hardware modificada</translation>
</message>
<message>
<source>Hardware revision</source>
<extracomment>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</extracomment>
<translation>Revisión de hardware</translation>
</message>
<message>
<source>Firmware revision changed</source>
<extracomment>The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki</extracomment>
<translation>Revisión de firmware modificada</translation>
</message>
<message>
<source>Firmware revision</source>
<extracomment>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</extracomment>
<translation>Revisión de firmware</translation>
</message>
<message>
<source>Lock</source>
<extracomment>The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki</extracomment>
<translation>Bloqueo</translation>
</message>
<message>
<source>Unlock</source>
<extracomment>The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki</extracomment>
<translation>Desbloqueo</translation>
</message>
<message>
<source>Refresh</source>
<extracomment>The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki</extracomment>
<translation>Actualizar</translation>
</message>
<message>
<source>State changed</source>
<extracomment>The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki</extracomment>
<translation>Estado modificado</translation>
</message>
<message>
<source>State</source>
<extracomment>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</extracomment>
<translation>Estado</translation>
</message>
<message>
<source>Open door</source>
<extracomment>The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki</extracomment>
<translation>Abrir puerta</translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="fr">
<context>
<name>DevicePluginNuki</name>
<message>
<source>Device is already in use.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth is not available on this system.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth device not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue.</source>
<translation type="unfinished">Veuillez appuyer pendant 5 secondes sur le bouton Nuki afin d&apos;activer le mode de couplage avant de continuer.</translation>
</message>
</context>
<context>
<name>Nuki</name>
<message>
<source>Nuki</source>
<extracomment>The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52})
----------
The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Smartlock</source>
<extracomment>The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767})</extracomment>
<translation>Smartlock</translation>
</message>
<message>
<source>Name</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3})</extracomment>
<translation>Nom</translation>
</message>
<message>
<source>MAC address</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5})</extracomment>
<translation>Adresse MAC</translation>
</message>
<message>
<source>Serial number</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf})</extracomment>
<translation>Numéro de serie</translation>
</message>
<message>
<source>Connected changed</source>
<extracomment>The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki</extracomment>
<translation>Connexion modifiée</translation>
</message>
<message>
<source>Connected</source>
<extracomment>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</extracomment>
<translation>Connecté</translation>
</message>
<message>
<source>Battery critical changed</source>
<extracomment>The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki</extracomment>
<translation>Statut de batterie critique modifié</translation>
</message>
<message>
<source>Battery critical</source>
<extracomment>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</extracomment>
<translation>Batterie critique</translation>
</message>
<message>
<source>Status changed</source>
<extracomment>The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki</extracomment>
<translation>Statut modifié</translation>
</message>
<message>
<source>Status</source>
<extracomment>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</extracomment>
<translation>Statut</translation>
</message>
<message>
<source>Mode changed</source>
<extracomment>The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki</extracomment>
<translation>Mode modifié</translation>
</message>
<message>
<source>Mode</source>
<extracomment>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</extracomment>
<translation>Mode</translation>
</message>
<message>
<source>Trigger changed</source>
<extracomment>The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki</extracomment>
<translation>Déclencheur modifié</translation>
</message>
<message>
<source>Trigger</source>
<extracomment>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</extracomment>
<translation>Déclencheur</translation>
</message>
<message>
<source>Hardware revision changed</source>
<extracomment>The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki</extracomment>
<translation>Révision du matériel (hardware) modifiée</translation>
</message>
<message>
<source>Hardware revision</source>
<extracomment>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</extracomment>
<translation>Révision du matériel (hardware)</translation>
</message>
<message>
<source>Firmware revision changed</source>
<extracomment>The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki</extracomment>
<translation>Version du progiciel modifiée</translation>
</message>
<message>
<source>Firmware revision</source>
<extracomment>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</extracomment>
<translation>Version du progiciel</translation>
</message>
<message>
<source>Lock</source>
<extracomment>The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki</extracomment>
<translation>Verrouiller</translation>
</message>
<message>
<source>Unlock</source>
<extracomment>The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki</extracomment>
<translation>Déverrouiller</translation>
</message>
<message>
<source>Refresh</source>
<extracomment>The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki</extracomment>
<translation>Rafraîchir</translation>
</message>
<message>
<source>State changed</source>
<extracomment>The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki</extracomment>
<translation>Statut modifié</translation>
</message>
<message>
<source>State</source>
<extracomment>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</extracomment>
<translation>Statut</translation>
</message>
<message>
<source>Open door</source>
<extracomment>The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki</extracomment>
<translation>Ouvrir la porte</translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="it">
<context>
<name>DevicePluginNuki</name>
<message>
<source>Device is already in use.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth is not available on this system.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth device not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue.</source>
<translation type="unfinished">Premi il pulsante Nuki per 5 secondi per attivare la sincronizzazione prima di continuare.</translation>
</message>
</context>
<context>
<name>Nuki</name>
<message>
<source>Nuki</source>
<extracomment>The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52})
----------
The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Smartlock</source>
<extracomment>The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767})</extracomment>
<translation>Smartlock</translation>
</message>
<message>
<source>Name</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3})</extracomment>
<translation>Nome</translation>
</message>
<message>
<source>MAC address</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5})</extracomment>
<translation>Indirizzo MAC</translation>
</message>
<message>
<source>Serial number</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf})</extracomment>
<translation>Numero di serie</translation>
</message>
<message>
<source>Connected changed</source>
<extracomment>The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki</extracomment>
<translation>Connesso modificato</translation>
</message>
<message>
<source>Connected</source>
<extracomment>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</extracomment>
<translation>Connesso</translation>
</message>
<message>
<source>Battery critical changed</source>
<extracomment>The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki</extracomment>
<translation>Batteria critica modificata</translation>
</message>
<message>
<source>Battery critical</source>
<extracomment>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</extracomment>
<translation>Batteria critica</translation>
</message>
<message>
<source>Status changed</source>
<extracomment>The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki</extracomment>
<translation>Stato modificato</translation>
</message>
<message>
<source>Status</source>
<extracomment>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</extracomment>
<translation>Stato</translation>
</message>
<message>
<source>Mode changed</source>
<extracomment>The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki</extracomment>
<translation>Modalità modificata</translation>
</message>
<message>
<source>Mode</source>
<extracomment>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</extracomment>
<translation>Modalità</translation>
</message>
<message>
<source>Trigger changed</source>
<extracomment>The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki</extracomment>
<translation>Trigger modificato</translation>
</message>
<message>
<source>Trigger</source>
<extracomment>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</extracomment>
<translation>Trigger</translation>
</message>
<message>
<source>Hardware revision changed</source>
<extracomment>The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki</extracomment>
<translation>Revisione hardware modificata</translation>
</message>
<message>
<source>Hardware revision</source>
<extracomment>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</extracomment>
<translation>Revisione hardware</translation>
</message>
<message>
<source>Firmware revision changed</source>
<extracomment>The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki</extracomment>
<translation>Revisione firmware modificata</translation>
</message>
<message>
<source>Firmware revision</source>
<extracomment>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</extracomment>
<translation>Revisione firmware</translation>
</message>
<message>
<source>Lock</source>
<extracomment>The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki</extracomment>
<translation>Blocca</translation>
</message>
<message>
<source>Unlock</source>
<extracomment>The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki</extracomment>
<translation>Sblocca</translation>
</message>
<message>
<source>Refresh</source>
<extracomment>The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki</extracomment>
<translation>Aggiorna</translation>
</message>
<message>
<source>State changed</source>
<extracomment>The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki</extracomment>
<translation>Stato modificato</translation>
</message>
<message>
<source>State</source>
<extracomment>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</extracomment>
<translation>Stato</translation>
</message>
<message>
<source>Open door</source>
<extracomment>The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki</extracomment>
<translation>Apri porta</translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="nl">
<context>
<name>DevicePluginNuki</name>
<message>
<source>Device is already in use.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth is not available on this system.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth device not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue.</source>
<translation type="unfinished">Houd, alvorens verder te gaan, de Nuki-knop 5 seconden ingedrukt om de koppelmodus te activeren.</translation>
</message>
</context>
<context>
<name>Nuki</name>
<message>
<source>Nuki</source>
<extracomment>The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52})
----------
The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Smartlock</source>
<extracomment>The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767})</extracomment>
<translation>Smartlock</translation>
</message>
<message>
<source>Name</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3})</extracomment>
<translation>Naam</translation>
</message>
<message>
<source>MAC address</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5})</extracomment>
<translation>MAC address</translation>
</message>
<message>
<source>Serial number</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf})</extracomment>
<translation>Serienummer</translation>
</message>
<message>
<source>Connected changed</source>
<extracomment>The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki</extracomment>
<translation>Verbonden gewijzigd</translation>
</message>
<message>
<source>Connected</source>
<extracomment>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</extracomment>
<translation>Verbonden</translation>
</message>
<message>
<source>Battery critical changed</source>
<extracomment>The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki</extracomment>
<translation>Batterij kritiek gewijzigd</translation>
</message>
<message>
<source>Battery critical</source>
<extracomment>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</extracomment>
<translation>Batterij kritiek</translation>
</message>
<message>
<source>Status changed</source>
<extracomment>The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki</extracomment>
<translation>Status gewijzigd</translation>
</message>
<message>
<source>Status</source>
<extracomment>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</extracomment>
<translation>Status</translation>
</message>
<message>
<source>Mode changed</source>
<extracomment>The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki</extracomment>
<translation>Modus gewijzigd</translation>
</message>
<message>
<source>Mode</source>
<extracomment>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</extracomment>
<translation>Modus</translation>
</message>
<message>
<source>Trigger changed</source>
<extracomment>The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki</extracomment>
<translation>Trigger gewijzigd</translation>
</message>
<message>
<source>Trigger</source>
<extracomment>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</extracomment>
<translation>Trigger</translation>
</message>
<message>
<source>Hardware revision changed</source>
<extracomment>The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki</extracomment>
<translation>Hardware-revisie gewijzigd</translation>
</message>
<message>
<source>Hardware revision</source>
<extracomment>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</extracomment>
<translation>Hardware-revisie</translation>
</message>
<message>
<source>Firmware revision changed</source>
<extracomment>The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki</extracomment>
<translation>Firmware-revisie gewijzigd</translation>
</message>
<message>
<source>Firmware revision</source>
<extracomment>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</extracomment>
<translation>Firmware-revisie</translation>
</message>
<message>
<source>Lock</source>
<extracomment>The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki</extracomment>
<translation>Vergrendelen</translation>
</message>
<message>
<source>Unlock</source>
<extracomment>The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki</extracomment>
<translation>Ontgrendelen</translation>
</message>
<message>
<source>Refresh</source>
<extracomment>The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki</extracomment>
<translation>Vernieuwen</translation>
</message>
<message>
<source>State changed</source>
<extracomment>The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki</extracomment>
<translation>Status gewijzigd</translation>
</message>
<message>
<source>State</source>
<extracomment>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</extracomment>
<translation>Status</translation>
</message>
<message>
<source>Open door</source>
<extracomment>The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki</extracomment>
<translation>Deur openen</translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="pt">
<context>
<name>DevicePluginNuki</name>
<message>
<source>Device is already in use.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth is not available on this system.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bluetooth device not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Please press the Nuki button for 5 seconds in order to activate the pairing mode before you continue.</source>
<translation type="unfinished">Pressione o botão Nuki durante 5 segundos para ativar o modo de emparelhamento antes de continuar.</translation>
</message>
</context>
<context>
<name>Nuki</name>
<message>
<source>Nuki</source>
<extracomment>The name of the vendor ({bf313b83-2ac5-4d22-bf0e-c13d3b4caf52})
----------
The name of the plugin Nuki ({e5806d75-a40e-4766-a272-5a3a8d3ed625})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Smartlock</source>
<extracomment>The name of the DeviceClass ({4a1cc5d9-9b44-4632-8db0-66d64efd4767})</extracomment>
<translation>Smartlock</translation>
</message>
<message>
<source>Name</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {2477abba-874b-4c48-b543-7b911ff215b3})</extracomment>
<translation>Nome</translation>
</message>
<message>
<source>MAC address</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {30976794-6066-4f72-8135-6d50499247a5})</extracomment>
<translation>Endereço MAC</translation>
</message>
<message>
<source>Serial number</source>
<extracomment>The name of the ParamType (DeviceClass: nuki, Type: device, ID: {ea51d911-f94a-4d2d-97fd-9f1d4c6519bf})</extracomment>
<translation>Número de série</translation>
</message>
<message>
<source>Connected changed</source>
<extracomment>The name of the EventType ({d5fbd774-4e87-4c7c-8c92-f094352897f6}) of DeviceClass nuki</extracomment>
<translation>Conectado alterado</translation>
</message>
<message>
<source>Connected</source>
<extracomment>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</extracomment>
<translation>Conectado</translation>
</message>
<message>
<source>Battery critical changed</source>
<extracomment>The name of the EventType ({e9ffe14f-b71d-44cd-96c7-eb90fe243e13}) of DeviceClass nuki</extracomment>
<translation>Bateria crítica alterada</translation>
</message>
<message>
<source>Battery critical</source>
<extracomment>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</extracomment>
<translation>Bateria crítica</translation>
</message>
<message>
<source>Status changed</source>
<extracomment>The name of the EventType ({a47dec8b-4df2-4acd-895b-bfeacbcb6f2e}) of DeviceClass nuki</extracomment>
<translation>Estado alterado</translation>
</message>
<message>
<source>Status</source>
<extracomment>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</extracomment>
<translation>Estado</translation>
</message>
<message>
<source>Mode changed</source>
<extracomment>The name of the EventType ({4e291e6a-2b90-4e6c-bad4-5400e6db4f05}) of DeviceClass nuki</extracomment>
<translation>Modo alterado</translation>
</message>
<message>
<source>Mode</source>
<extracomment>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</extracomment>
<translation>Modo</translation>
</message>
<message>
<source>Trigger changed</source>
<extracomment>The name of the EventType ({b67bc7a6-2d6b-46e8-8e59-e4eab2e5292a}) of DeviceClass nuki</extracomment>
<translation>Acionamento alterado</translation>
</message>
<message>
<source>Trigger</source>
<extracomment>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</extracomment>
<translation>Acionamento</translation>
</message>
<message>
<source>Hardware revision changed</source>
<extracomment>The name of the EventType ({b1b0eafb-e5f2-47d3-bd18-74a21129f9b5}) of DeviceClass nuki</extracomment>
<translation>Revisão de hardware alterada</translation>
</message>
<message>
<source>Hardware revision</source>
<extracomment>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</extracomment>
<translation>Revisão de hardware</translation>
</message>
<message>
<source>Firmware revision changed</source>
<extracomment>The name of the EventType ({9dc0115b-cdb4-472c-961b-b182f77a576f}) of DeviceClass nuki</extracomment>
<translation>Revisão de firmware alterada</translation>
</message>
<message>
<source>Firmware revision</source>
<extracomment>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</extracomment>
<translation>Revisão de firmware</translation>
</message>
<message>
<source>Lock</source>
<extracomment>The name of the ActionType ({55d25891-89b9-4a9f-a2b3-774eccf30183}) of DeviceClass nuki</extracomment>
<translation>Bloquear</translation>
</message>
<message>
<source>Unlock</source>
<extracomment>The name of the ActionType ({76e96738-5336-4b9a-87f7-1822307b5a39}) of DeviceClass nuki</extracomment>
<translation>Desbloquear</translation>
</message>
<message>
<source>Refresh</source>
<extracomment>The name of the ActionType ({7c9d5c5d-d8c1-424b-8620-daa3a757576b}) of DeviceClass nuki</extracomment>
<translation>Atualizar</translation>
</message>
<message>
<source>State changed</source>
<extracomment>The name of the EventType ({07a09bd8-b342-4e33-92cd-0af21f8689fa}) of DeviceClass nuki</extracomment>
<translation>Estado alterado</translation>
</message>
<message>
<source>State</source>
<extracomment>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</extracomment>
<translation>Estado</translation>
</message>
<message>
<source>Open door</source>
<extracomment>The name of the ActionType ({45be8d24-17c3-422b-b264-381e673bb3c8}) of DeviceClass nuki</extracomment>
<translation>Abrir porta</translation>
</message>
</context>
</TS>

View File

@ -30,6 +30,7 @@ PLUGIN_DIRS = \
mqttclient \ mqttclient \
netatmo \ netatmo \
networkdetector \ networkdetector \
nuki \
onewire \ onewire \
openuv \ openuv \
openweathermap \ openweathermap \