nymea-plugins/nuki/nuki.cpp

602 lines
20 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-plugins.
*
* nymea-plugins is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-plugins is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-plugins. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "nuki.h"
#include "extern-plugininfo.h"
#include <QBitArray>
#include <QtEndian>
#include <QByteArray>
#include <QDataStream>
#include <QTimer>
Nuki::Nuki(Thing *thing, BluetoothDevice *bluetoothDevice, QObject *parent) :
QObject(parent),
m_thing(thing),
m_bluetoothDevice(bluetoothDevice)
{
connect(m_bluetoothDevice, &BluetoothDevice::stateChanged, this, &Nuki::onBluetoothDeviceStateChanged);
onBluetoothDeviceStateChanged(m_bluetoothDevice->state());
}
Thing *Nuki::thing()
{
return m_thing;
}
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, ThingActionInfo *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<ThingActionInfo>(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;
foreach (BluetoothGattDescriptor *descriptor, characteristic->descriptors()) {
qCDebug(dcNuki()) << " " << descriptor;
}
}
}
}
void Nuki::readDeviceInformationCharacteristics()
{
qCDebug(dcNuki()) << "Start reading device information";
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
m_initUuidsToRead.append(QBluetoothUuid::CharacteristicType::SerialNumberString);
m_initUuidsToRead.append(QBluetoothUuid::CharacteristicType::HardwareRevisionString);
m_initUuidsToRead.append(QBluetoothUuid::CharacteristicType::FirmwareRevisionString);
m_deviceInformationService->readCharacteristic(QBluetoothUuid::CharacteristicType::SerialNumberString);
m_deviceInformationService->readCharacteristic(QBluetoothUuid::CharacteristicType::HardwareRevisionString);
m_deviceInformationService->readCharacteristic(QBluetoothUuid::CharacteristicType::FirmwareRevisionString);
#else
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);
#endif
}
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;
}
}
bool Nuki::enableNotificationsIndications(BluetoothGattCharacteristic *characteristic)
{
qCDebug(dcNuki()) << "Enable notifications on" << characteristic;
if (!characteristic->startNotifications()) {
qCDebug(dcNuki()) << "Failed to start notifications on" << characteristic;
return false;
}
return true;
}
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 thing information characteristic finished" << characteristic->chararcteristicName() << qUtf8Printable(value);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
if (characteristic->uuid() == QBluetoothUuid::CharacteristicType::SerialNumberString) {
m_serialNumber = QString::fromUtf8(value);
m_initUuidsToRead.removeOne(QBluetoothUuid::CharacteristicType::SerialNumberString);
} else if (characteristic->uuid() == QBluetoothUuid::CharacteristicType::HardwareRevisionString) {
m_hardwareRevision = QString::fromUtf8(value);
m_initUuidsToRead.removeOne(QBluetoothUuid::CharacteristicType::HardwareRevisionString);
} else if (characteristic->uuid() == QBluetoothUuid::CharacteristicType::FirmwareRevisionString) {
m_firmwareRevision = QString::fromUtf8(value);
m_initUuidsToRead.removeOne(QBluetoothUuid::CharacteristicType::FirmwareRevisionString);
}
#else
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);
}
#endif
if (m_initUuidsToRead.isEmpty()) {
// Initial read done. Make thing 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_thing)
return;
m_thing->setStateValue(nukiHardwareRevisionStateTypeId, m_hardwareRevision);
m_thing->setStateValue(nukiFirmwareRevisionStateTypeId, m_firmwareRevision);
m_thing->setStateValue(nukiBatteryCriticalStateTypeId, m_nukiController->batteryCritical());
switch (m_nukiController->nukiLockTrigger()) {
case NukiUtils::LockTriggerBluetooth:
m_thing->setStateValue(nukiTriggerStateTypeId, "Bluetooth");
break;
case NukiUtils::LockTriggerButton:
m_thing->setStateValue(nukiTriggerStateTypeId, "Button");
break;
case NukiUtils::LockTriggerManual:
m_thing->setStateValue(nukiTriggerStateTypeId, "Manual");
break;
default:
break;
}
switch (m_nukiController->nukiState()) {
case NukiUtils::NukiStateDoorMode:
m_thing->setStateValue(nukiModeStateTypeId, "Door");
break;
case NukiUtils::NukiStatePairingMode:
m_thing->setStateValue(nukiModeStateTypeId, "Pairing");
break;
case NukiUtils::NukiStateUninitialized:
m_thing->setStateValue(nukiModeStateTypeId, "Uninitialized");
break;
default:
break;
}
switch (m_nukiController->nukiLockState()) {
case NukiUtils::LockStateLocked:
m_thing->setStateValue(nukiStateStateTypeId, "locked");
m_thing->setStateValue(nukiStatusStateTypeId, "Ok");
break;
case NukiUtils::LockStateLocking:
m_thing->setStateValue(nukiStateStateTypeId, "locking");
m_thing->setStateValue(nukiStatusStateTypeId, "Ok");
break;
case NukiUtils::LockStateMotorBlocked:
m_thing->setStateValue(nukiStatusStateTypeId, "Motor blocked");
break;
case NukiUtils::LockStateUncalibrated:
m_thing->setStateValue(nukiStatusStateTypeId, "Uncalibrated");
break;
case NukiUtils::LockStateUndefined:
m_thing->setStateValue(nukiStatusStateTypeId, "Undefined");
break;
case NukiUtils::LockStateUnlatched:
m_thing->setStateValue(nukiStateStateTypeId, "unlatched");
m_thing->setStateValue(nukiStatusStateTypeId, "Ok");
break;
case NukiUtils::LockStateUnlatching:
m_thing->setStateValue(nukiStateStateTypeId, "unlatching");
m_thing->setStateValue(nukiStatusStateTypeId, "Ok");
break;
case NukiUtils::LockStateUnlockedLocknGoActive:
m_thing->setStateValue(nukiStatusStateTypeId, "unlocked");
break;
case NukiUtils::LockStateUnlocked:
m_thing->setStateValue(nukiStateStateTypeId, "unlocked");
m_thing->setStateValue(nukiStatusStateTypeId, "Ok");
break;
case NukiUtils::LockStateUnlocking:
m_thing->setStateValue(nukiStateStateTypeId, "unlocking");
m_thing->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 QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
if (!m_bluetoothDevice->hasService(QBluetoothUuid::ServiceClassUuid::DeviceInformation)) {
#else
if (!m_bluetoothDevice->hasService(QBluetoothUuid::DeviceInformation)) {
#endif
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
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
m_deviceInformationService = m_bluetoothDevice->getService(QBluetoothUuid::ServiceClassUuid::DeviceInformation);
#else
m_deviceInformationService = m_bluetoothDevice->getService(QBluetoothUuid::DeviceInformation);
#endif
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;
}
// Set key turner characteristics for data and user data
if (!m_keyturnerService->hasCharacteristic(keyturnerDataCharacteristicUuid())) {
qCWarning(dcNuki()) << "Could not find data characteristc on device" << m_bluetoothDevice;
return false;
}
// Enable notifications/indications
m_keyturnerUserDataCharacteristic = m_keyturnerService->getCharacteristic(keyturnerUserDataCharacteristicUuid());
if (!enableNotificationsIndications(m_keyturnerUserDataCharacteristic)) {
qCWarning(dcNuki()) << "Could not enable notifications/indications for user data characteristic.";
return false;
}
m_keyturnerDataCharacteristic = m_keyturnerService->getCharacteristic(keyturnerDataCharacteristicUuid());
if (!enableNotificationsIndications(m_keyturnerDataCharacteristic)) {
qCWarning(dcNuki()) << "Could not enable notifications/indications for key turner 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 (!enableNotificationsIndications(m_pairingDataCharacteristic)) {
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 ? Thing::ThingErrorNoError : Thing::ThingErrorHardwareFailure);
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_thing)
return;
m_thing->setStateValue(nukiConnectedStateTypeId, m_available);
}