nymea-plugins/eq-3/eqivabluetooth.cpp

569 lines
22 KiB
C++
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "eqivabluetooth.h"
#include "extern-plugininfo.h"
#include <QDataStream>
#include <QDateTime>
const QBluetoothUuid eqivaCommandServiceUuid = QBluetoothUuid(QString("{3e135142-654f-9090-134a-a6ff5bb77046}"));
//I | EQ3: Service discovered: "Unknown Service" "{3e135142-654f-9090-134a-a6ff5bb77046}"
//I | EQ3: C: --> {3fa4585a-ce4a-3bad-db4b-b8df8179ea09} (Handle 0x411 Name: ): 0313091e0b2936000000000000000000  )6
//I | EQ3: C: --> {d0e8434d-cd29-0996-af41-6c90f4e0eb2a} (Handle 0x421 Name: ): 00000000000000000000000000000000
//I | EQ3: D: --> {00002902-0000-1000-8000-00805f9b34fb} (Handle 0x430 Name: Client Characteristic Configuration): 0100
const QBluetoothUuid eqivaGapServiceUuid = QBluetoothUuid(QString("00001800-0000-1000-8000-00805f9b34fb"));
//I | EQ3: Service discovered: "Generic Access" "{00001800-0000-1000-8000-00805f9b34fb}"
//I | EQ3: C: --> {00002a00-0000-1000-8000-00805f9b34fb} (Handle 0x111 Name: GAP Device Name): 43432d52542d424c45CC-RT-BLE
//I | EQ3: C: --> {00002a01-0000-1000-8000-00805f9b34fb} (Handle 0x121 Name: GAP Appearance): 0000
//I | EQ3: C: --> {00002a02-0000-1000-8000-00805f9b34fb} (Handle 0x131 Name: GAP Peripheral Privacy Flag): 00
//I | EQ3: C: --> {00002a03-0000-1000-8000-00805f9b34fb} (Handle 0x141 Name: GAP Reconnection Address):
//I | EQ3: C: --> {00002a04-0000-1000-8000-00805f9b34fb} (Handle 0x151 Name: GAP Peripheral Preferred Connection Parameters): 0000000000000000
const QBluetoothUuid eqivaGattServiceUuid = QBluetoothUuid(QString("00001801-0000-1000-8000-00805f9b34fb"));
//I | EQ3: Service discovered: "Generic Attribute" "{00001801-0000-1000-8000-00805f9b34fb}"
//I | EQ3: C: --> {00002a05-0000-1000-8000-00805f9b34fb} (Handle 0x211 Name: GATT Service Changed): 00000000
//I | EQ3: D: --> {00002902-0000-1000-8000-00805f9b34fb} (Handle 0x220 Name: Client Characteristic Configuration): 0200
const QBluetoothUuid eqivaDeviceInfoServiceUuid = QBluetoothUuid(QString("0000180a-0000-1000-8000-00805f9b34fb"));
//I | EQ3: Service discovered: "Device Information" "{0000180a-0000-1000-8000-00805f9b34fb}"
//I | EQ3: C: --> {00002a29-0000-1000-8000-00805f9b34fb} (Handle 0x311 Name: Manufacturer Name String): 65712d33eq-3
//I | EQ3: C: --> {00002a24-0000-1000-8000-00805f9b34fb} (Handle 0x321 Name: Model Number String): 43432d52542d424c45CC-RT-BLE
const QBluetoothUuid eqivaUnknownServiceUuid = QBluetoothUuid(QString("9e5d1e47-5c13-43a0-8635-82ad38a1386f"));
//I | EQ3: Service discovered: "Unknown Service" "{9e5d1e47-5c13-43a0-8635-82ad38a1386f}"
//I | EQ3: C: --> {e3dd50bf-f7a7-4e99-838e-570a086c666b} (Handle 0xff02 Name: ):
//I | EQ3: D: --> {00002902-0000-1000-8000-00805f9b34fb} (Handle 0xff03 Name: Client Characteristic Configuration): 0000
//I | EQ3: C: --> {92e86c7a-d961-4091-b74f-2409e72efe36} (Handle 0xff05 Name: ):
//I | EQ3: C: --> {347f7608-2e2d-47eb-913b-75d4edc4de3b} (Handle 0xff07 Name: ): 00100200
const QBluetoothUuid commandCharacteristicUuid = QBluetoothUuid(QString("3fa4585a-ce4a-3bad-db4b-b8df8179ea09"));
const QBluetoothUuid notificationCharacteristicUuid = QBluetoothUuid(QString("d0e8434d-cd29-0996-af41-6c90f4e0eb2a"));
// Protocol
// Commands:
const quint8 commandSetDate = 0x03;
const quint8 commandSetMode = 0x40;
const quint8 commandSetTemp = 0x41;
const quint8 commandBoost = 0x45;
const quint8 commandLock = 0x80;
// Notifications
const quint8 notifyHeader = 0x02;
const quint8 profileReadReply = 0x21;
const quint8 notifyStatus = 0x01;
const quint8 notifyProfile = 0x02;
// Values:
const quint8 valueOn = 0x01;
const quint8 valueOff = 0x00;
const quint8 modeAuto = 0x08;
const quint8 modeManual = 0x09;
const quint8 modeHoliday = 0x0A;
const quint8 modeBoost1 = 0x0C; // at the end it returns to automatic mode
const quint8 modeBoost2 = 0x0D; // at the end it returns to manual mode
const quint8 modeBoost3 = 0x0E; // at the end it returns to holiday mode
const quint8 lockOff = 0x00;
const quint8 windowLockOn = 0x10;
const quint8 keyLockOn = 0x20;
const quint8 batteryCriticalOn = 0x80;
const quint8 setModeAuto = 0x00;
const quint8 setModeManual = 0x40;
const quint8 setModeHoliday = 0x40; // Same as manual but with a time limit
EqivaBluetooth::EqivaBluetooth(BluetoothLowEnergyManager *bluetoothManager, const QBluetoothAddress &hostAddress, const QString &name, QObject *parent):
QObject(parent),
m_bluetoothManager(bluetoothManager),
m_name(name)
{
QBluetoothDeviceInfo deviceInfo = QBluetoothDeviceInfo(hostAddress, QString(), 0);
m_bluetoothDevice = m_bluetoothManager->registerDevice(deviceInfo, QLowEnergyController::PublicAddress);
connect(m_bluetoothDevice, &BluetoothLowEnergyDevice::stateChanged, this, &EqivaBluetooth::controllerStateChanged);
m_bluetoothDevice->connectDevice();
m_refreshTimer.setInterval(5000);
m_refreshTimer.setSingleShot(true);
connect(&m_refreshTimer, &QTimer::timeout, this, &EqivaBluetooth::sendDate);
m_reconnectTimer.setSingleShot(true);
connect(&m_reconnectTimer, &QTimer::timeout, this, [this](){
qCDebug(dcEQ3()) << m_name << "Trying to reconnect";
m_reconnectAttempt++;
m_bluetoothDevice->connectDevice();
});
m_commandTimeout.setInterval(3000);
m_commandTimeout.setSingleShot(true);
connect(&m_commandTimeout, &QTimer::timeout, this, [this](){
// Put current command back to the queue as it didn't succeed
qCWarning(dcEQ3()) << m_name << "Command timed out:" << m_currentCommand.id << m_currentCommand.name << "Putting command back to queue";
m_commandQueue.prepend(m_currentCommand);
m_currentCommand = Command();
// and reset the connection
if (m_bluetoothDevice->connected()) {
m_bluetoothDevice->disconnectDevice();
}
});
}
EqivaBluetooth::~EqivaBluetooth()
{
m_bluetoothManager->unregisterDevice(m_bluetoothDevice);
}
void EqivaBluetooth::setName(const QString &name)
{
m_name = name;
}
bool EqivaBluetooth::available() const
{
return m_available;
}
bool EqivaBluetooth::locked() const
{
return m_locked;
}
int EqivaBluetooth::setLocked(bool locked)
{
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
stream << commandLock;
stream << (locked ? valueOn : valueOff);
return enqueue("SetLocked", data);
}
bool EqivaBluetooth::boostEnabled() const
{
return m_boostEnabled;
}
int EqivaBluetooth::setBoostEnabled(bool enabled)
{
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
stream << commandBoost;
stream << (enabled ? valueOn : valueOff);
return enqueue("SetBoostEnabled", data);
}
qreal EqivaBluetooth::targetTemperature() const
{
return m_targetTemp;
}
int EqivaBluetooth::setTargetTemperature(qreal targetTemperature)
{
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
stream << commandSetTemp;
if (targetTemperature == 4.5) {
stream << static_cast<quint8>(4.5 * 2); // 4.5 degrees is off
} else {
stream << static_cast<quint8>(targetTemperature * 2); // Temperature *2 (thing only supports .5 precision)
}
return enqueue("SetTargetTemperature", data);
}
EqivaBluetooth::Mode EqivaBluetooth::mode() const
{
return m_mode;
}
int EqivaBluetooth::setMode(EqivaBluetooth::Mode mode)
{
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
stream << commandSetMode;
switch (mode) {
case ModeAuto:
stream << setModeAuto;
break;
case ModeManual:
stream << setModeManual;
break;
case ModeHoliday:
stream << setModeHoliday;
// Holiday mode would support adding a timestamp in the format:
// temperature*2 + 128, day, year, hour-and-minute, month
// Given we can't have params with states and in the nymea context this doesn't make too much sense anyways
// we're just gonna switch to manual mode here... Any coming-come automatism should be handled by nymea anyways.
break;
}
qCDebug(dcEQ3()) << m_name << "Setting mode to" << data.toHex();
return enqueue("SetMode", data);
}
bool EqivaBluetooth::windowOpen() const
{
return m_windowOpen;
}
quint8 EqivaBluetooth::valveOpen() const
{
return m_valveOpen;
}
bool EqivaBluetooth::batteryCritical() const
{
return m_batteryCritical;
}
void EqivaBluetooth::controllerStateChanged(const QLowEnergyController::ControllerState &state)
{
if (state == QLowEnergyController::ConnectingState) {
// Make sure the reconnect timer is stopped when we're starting a new attempt
m_reconnectTimer.stop();
return;
}
if (state == QLowEnergyController::UnconnectedState) {
int delay = qMin(m_reconnectAttempt, 30);
qWarning(dcEQ3()) << m_name << "Eqiva thing disconnected. Reconnecting in" << delay << "sec";
m_available = false;
emit availableChanged();
m_reconnectTimer.start(delay * 1000);
}
if (state != QLowEnergyController::DiscoveredState) {
return;
}
// DEBUG: Enabling this will read all the services and their characteristics and print them out.
// qCDebug(dcEQ3()) << m_name << "Discovered: Service UUIDS:" << m_bluetoothDevice->serviceUuids();
// QBluetoothUuid uuid = QBluetoothUuid(QString("{3e135142-654f-9090-134a-a6ff5bb77046}"));
//// QBluetoothUuid uuid = QBluetoothUuid(QString("00001800-0000-1000-8000-00805f9b34fb"));
//// QBluetoothUuid uuid = QBluetoothUuid(QString("00001801-0000-1000-8000-00805f9b34fb"));
//// QBluetoothUuid uuid = QBluetoothUuid(QString("0000180a-0000-1000-8000-00805f9b34fb")); // Manufacturer name and model name
//// QBluetoothUuid uuid = QBluetoothUuid(QString("9e5d1e47-5c13-43a0-8635-82ad38a1386f"));
// QLowEnergyService *service = m_bluetoothDevice->controller()->createServiceObject(uuid);
// service->discoverDetails();
// connect(service, &QLowEnergyService::stateChanged, this, [service](QLowEnergyService::ServiceState newState){
// if (newState != QLowEnergyService::ServiceDiscovered) {
// return;
// }
// qCDebug(dcEQ3()) << "Service discovered:" << service->serviceName() << service->serviceUuid();
// foreach (const QLowEnergyCharacteristic &characteristic, service->characteristics()) {
// qCDebug(dcEQ3()).nospace().noquote() << "C: --> " << characteristic.uuid().toString() << " (Handle 0x" << QString("%1").arg(characteristic.handle(), 0, 16) << " Name: " << characteristic.name() << "): 0x" << characteristic.value().toHex() << " " << qUtf8Printable(characteristic.value());
// foreach (const QLowEnergyDescriptor &descriptor, characteristic.descriptors()) {
// qCDebug(dcEQ3()).nospace().noquote() << "D: --> " << descriptor.uuid().toString() << " (Handle 0x" << QString("%2").arg(descriptor.handle(), 0, 16) << " Name: " << descriptor.name() << "): 0x" << descriptor.value().toHex() << " " << qUtf8Printable(characteristic.value());
// }
//// service->readCharacteristic(characteristic);
// }
// });
// return;
// DEBUG END
m_eqivaService = m_bluetoothDevice->controller()->createServiceObject(eqivaCommandServiceUuid, this);
if (!m_eqivaService) {
qCWarning(dcEQ3()) << "Failed to create Service Object for service" << eqivaCommandServiceUuid;
return;
}
connect(m_eqivaService, &QLowEnergyService::stateChanged, this, &EqivaBluetooth::serviceStateChanged);
connect(m_eqivaService, &QLowEnergyService::characteristicRead, this, [this](const QLowEnergyCharacteristic &info, const QByteArray &value){
qCDebug(dcEQ3()) << m_name << "Characteristic read:" << info.name() << info.uuid() << value.toHex();
QByteArray data(value);
QDataStream stream(&data, QIODevice::ReadOnly);
quint8 header;
stream >> header;
quint8 notificationType;
stream >> notificationType;
quint8 lockAndMode;
stream >> lockAndMode;
stream >> m_valveOpen;
quint8 undefined;
stream >> undefined;
quint8 rawTemp;
stream >> rawTemp;
qCDebug(dcEQ3()) << "**** header" << header << "type" << notificationType << "lock/mode" << lockAndMode << "valve:" << m_valveOpen << "temp" << rawTemp;
});
connect(m_eqivaService, &QLowEnergyService::characteristicWritten, this, [this](const QLowEnergyCharacteristic &info, const QByteArray &value){
Q_UNUSED(info) // We're only writing one...
Q_UNUSED(value)
qCDebug(dcEQ3()) << m_name << "Command sent:" << m_currentCommand.id << m_currentCommand.name;
m_commandTimeout.stop();
emit commandResult(m_currentCommand.id, true);
m_currentCommand.id = -1;
processCommandQueue();
// m_eqivaService->readCharacteristic(notificationCharacteristicUuid);
});
connect(m_eqivaService, &QLowEnergyService::descriptorWritten, this, [this](const QLowEnergyDescriptor &info, const QByteArray &value){
qCDebug(dcEQ3()) << m_name << "Descriptor written" << info.uuid() << value;
});
connect(m_eqivaService, &QLowEnergyService::characteristicChanged, this, &EqivaBluetooth::characteristicChanged);
qCDebug(dcEQ3()) << "Discovering service details";
m_eqivaService->discoverDetails();
}
void EqivaBluetooth::serviceStateChanged(QLowEnergyService::ServiceState newState)
{
if (newState != QLowEnergyService::ServiceDiscovered) {
return;
}
qCDebug(dcEQ3()) << m_name << "Service details discovered";
// DEBUG
// foreach (const QLowEnergyCharacteristic &characteristic, m_eqivaService->characteristics()) {
// qCDebug(dcEQ3()).nospace().noquote() << "C: --> " << characteristic.uuid().toString() << " (Handle 0x" << QString("%1").arg(characteristic.handle(), 0, 16) << " Name: " << characteristic.name() << "): " << characteristic.value().toHex() << characteristic.value();
// foreach (const QLowEnergyDescriptor &descriptor, characteristic.descriptors()) {
// qCDebug(dcEQ3()).nospace().noquote() << "D: --> " << descriptor.uuid().toString() << " (Handle 0x" << QString("%2").arg(descriptor.handle(), 0, 16) << " Name: " << descriptor.name() << "): " << descriptor.value().toHex() << characteristic.value();
// }
// }
m_available = true;
m_reconnectAttempt = 0;
emit availableChanged();
sendDate();
// Enable notifications
QLowEnergyCharacteristic characteristic = m_eqivaService->characteristic(notificationCharacteristicUuid);
QLowEnergyDescriptor notificationDescriptor = characteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);
m_eqivaService->writeDescriptor(notificationDescriptor, QByteArray::fromHex("0100"));
}
void EqivaBluetooth::characteristicChanged(const QLowEnergyCharacteristic &info, const QByteArray &value)
{
if (info.uuid() != notificationCharacteristicUuid) {
qCWarning(dcEQ3()) << m_name << "Received a notification from a characteristic we did't expect:" << info.uuid() << value.toHex();
return;
}
m_refreshTimer.start();
QByteArray data(value);
QDataStream stream(&data, QIODevice::ReadOnly);
quint8 header;
stream >> header;
if (header == notifyHeader) {
quint8 notificationType;
stream >> notificationType;
if (notificationType == notifyStatus) {
quint8 lockAndMode;
stream >> lockAndMode;
stream >> m_valveOpen;
quint8 undefined;
stream >> undefined;
quint8 rawTemp;
stream >> rawTemp;
quint8 lock = (lockAndMode & 0xF0);
m_locked = (lock & keyLockOn);
m_windowOpen = (lock & windowLockOn);
m_batteryCritical = (lock & batteryCriticalOn);
quint8 mode = (lockAndMode & 0x0F);
m_targetTemp = 1.0 * rawTemp / 2;
if (m_targetTemp < 5) {
m_targetTemp = 5;
}
qCDebug(dcEQ3()) << m_name << "Status notification received: Enabled:" << "Temp:" << m_targetTemp << "Keylock:" << m_locked << "Window open:" << m_windowOpen << "Mode:" << mode << "Valve open:" << m_valveOpen << "Boost:" << m_boostEnabled << "Battery critical" << m_batteryCritical;
m_boostEnabled = false;
switch (mode) {
case modeManual:
m_mode = ModeManual;
break;
case modeAuto:
m_mode = ModeAuto;
break;
case modeBoost1:
m_boostEnabled = true;
m_mode = ModeAuto;
break;
case modeBoost2:
m_boostEnabled = true;
m_mode = ModeManual;
break;
case modeBoost3:
m_boostEnabled = true;
m_mode = ModeHoliday;
break;
case modeHoliday:
m_mode = ModeHoliday;
break;
}
emit lockedChanged();
emit boostEnabledChanged();
emit modeChanged();
emit windowOpenChanged();
emit targetTemperatureChanged();
emit valveOpenChanged();
emit batteryCriticalChanged();
m_refreshTimer.start();
} else if (notificationType == notifyProfile) {
// Profile updates not implemented
} else {
qCWarning(dcEQ3()) << m_name << "Unknown notification type" << notificationType;
}
} else if (header == profileReadReply) {
// Profile read not implemented...
} else {
qCWarning(dcEQ3()) << m_name << "Unhandled notification from thing:" << value.toHex();
}
}
void EqivaBluetooth::writeCharacteristic(const QBluetoothUuid &characteristicUuid, const QByteArray &data)
{
QLowEnergyCharacteristic characteristic = m_eqivaService->characteristic(characteristicUuid);
m_eqivaService->writeCharacteristic(characteristic, data);
}
void EqivaBluetooth::sendDate()
{
QDateTime now = QDateTime::currentDateTime();
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
stream << commandSetDate;
stream << static_cast<quint8>(now.date().year() - 2000);
stream << static_cast<quint8>(now.date().month());
stream << static_cast<quint8>(now.date().day());
stream << static_cast<quint8>(now.time().hour());
stream << static_cast<quint8>(now.time().minute());
stream << static_cast<quint8>(now.time().second());
// Example: 03130117172315 -> 03YYMMDDHHMMSS
enqueue("SetDate", data);
}
int EqivaBluetooth::enqueue(const QString &name, const QByteArray &data)
{
Command cmd;
cmd.name = name;
cmd.id = m_nextCommandId++;
cmd.data = data;
m_commandQueue.append(cmd);
processCommandQueue();
return cmd.id;
}
void EqivaBluetooth::processCommandQueue()
{
if (m_currentCommand.id != -1) {
qCDebug(dcEQ3()) << m_name << "Busy sending a command" << m_currentCommand.id << m_currentCommand.name;
return;
}
if (m_commandQueue.isEmpty()) {
return;
}
if (!m_available) {
qCWarning(dcEQ3()) << m_name << "Not connected. Trying to reconnect before sending commands...";
m_bluetoothDevice->connectDevice();
return;
}
m_currentCommand = m_commandQueue.takeFirst();
m_commandTimeout.start();
qCDebug(dcEQ3()) << m_name << "Sending command" << m_currentCommand.id << m_currentCommand.name << m_currentCommand.data.toHex();
writeCharacteristic(commandCharacteristicUuid, m_currentCommand.data);
}
EqivaBluetoothDiscovery::EqivaBluetoothDiscovery(BluetoothLowEnergyManager *bluetoothManager, QObject *parent):
QObject(parent),
m_bluetoothManager(bluetoothManager)
{
}
bool EqivaBluetoothDiscovery::startDiscovery()
{
if (!m_bluetoothManager->available()) {
qCWarning(dcEQ3()) << "Bluetooth is not available.";
return false;
}
if (!m_bluetoothManager->enabled()) {
qCWarning(dcEQ3()) << "Bluetooth is not available.";
return false;
}
qCDebug(dcEQ3()) << "Starting Bluetooth discovery";
BluetoothDiscoveryReply *reply = m_bluetoothManager->discoverDevices();
connect(reply, &BluetoothDiscoveryReply::finished, this, &EqivaBluetoothDiscovery::deviceDiscoveryDone);
return true;
}
void EqivaBluetoothDiscovery::deviceDiscoveryDone()
{
BluetoothDiscoveryReply *reply = static_cast<BluetoothDiscoveryReply *>(sender());
reply->deleteLater();
QList<EqivaBluetoothDiscovery::DiscoveryResult> results;
if (reply->error() != BluetoothDiscoveryReply::BluetoothDiscoveryReplyErrorNoError) {
qCWarning(dcEQ3()) << "Bluetooth discovery error:" << reply->error();
emit finished(results);
return;
}
foreach (const auto &deviceInfo, reply->discoveredDevices()) {
qCDebug(dcEQ3()) << "Discovered Bluetooth device" << deviceInfo.first.name() << deviceInfo.first.address().toString();
if (deviceInfo.first.name().contains("CC-RT-BLE")) {
results.append({deviceInfo.first, deviceInfo.second});
}
}
emit finished(results);
}