From 25a73fe05953bacf1ea40a34d21cad6be2feee91 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 24 Jan 2019 02:00:22 +0100 Subject: [PATCH] EQ-3: Add support for the Eqiva Bluetooth thermostat --- debian/nymea-plugin-eq-3.install.in | 2 +- eq-3/deviceplugineq-3.cpp | 162 ++++++++++- eq-3/deviceplugineq-3.h | 9 + eq-3/deviceplugineq-3.json | 105 ++++++- eq-3/eq-3.pro | 10 +- eq-3/eqivabluetooth.cpp | 434 ++++++++++++++++++++++++++++ eq-3/eqivabluetooth.h | 118 ++++++++ 7 files changed, 825 insertions(+), 15 deletions(-) create mode 100644 eq-3/eqivabluetooth.cpp create mode 100644 eq-3/eqivabluetooth.h diff --git a/debian/nymea-plugin-eq-3.install.in b/debian/nymea-plugin-eq-3.install.in index a0021906..3b2c10fd 100644 --- a/debian/nymea-plugin-eq-3.install.in +++ b/debian/nymea-plugin-eq-3.install.in @@ -1 +1 @@ -usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_deviceplugineq3.so +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_deviceplugineq-3.so diff --git a/eq-3/deviceplugineq-3.cpp b/eq-3/deviceplugineq-3.cpp index 3dd2a723..be778ffd 100644 --- a/eq-3/deviceplugineq-3.cpp +++ b/eq-3/deviceplugineq-3.cpp @@ -69,6 +69,8 @@ #include "types/param.h" #include "plugininfo.h" +#include "eqivabluetooth.h" + #include DevicePluginEQ3::DevicePluginEQ3() @@ -83,20 +85,32 @@ DevicePluginEQ3::~DevicePluginEQ3() void DevicePluginEQ3::init() { + qCDebug(dcEQ3()) << "Initializing EQ-3 Plugin"; m_cubeDiscovery = new MaxCubeDiscovery(this); connect(m_cubeDiscovery, &MaxCubeDiscovery::cubesDetected, this, &DevicePluginEQ3::discoveryDone); m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); connect(m_pluginTimer, &PluginTimer::timeout, this, &DevicePluginEQ3::onPluginTimer); + + m_eqivaBluetoothDiscovery = new EqivaBluetoothDiscovery(hardwareManager()->bluetoothLowEnergyManager(), this); + connect(m_eqivaBluetoothDiscovery, &EqivaBluetoothDiscovery::finished, this, &DevicePluginEQ3::bluetoothDiscoveryDone); } DeviceManager::DeviceError DevicePluginEQ3::discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) { Q_UNUSED(params) + qCDebug(dcEQ3()) << "Discover devices called"; if(deviceClassId == cubeDeviceClassId){ m_cubeDiscovery->detectCubes(); return DeviceManager::DeviceErrorAsync; } + if (deviceClassId == eqivaBluetoothDeviceClassId) { + bool ret = m_eqivaBluetoothDiscovery->startDiscovery(); + if (!ret) { + return DeviceManager::DeviceErrorHardwareNotAvailable; + } + return DeviceManager::DeviceErrorAsync; + } return DeviceManager::DeviceErrorDeviceClassNotFound; } @@ -136,20 +150,110 @@ DeviceManager::DeviceSetupStatus DevicePluginEQ3::setupDevice(Device *device) device->setName("Max! Wall Thermostat (" + device->paramValue(wallThermostateDeviceSerialParamTypeId).toString() + ")"); } + if (device->deviceClassId() == eqivaBluetoothDeviceClassId) { + EqivaBluetooth *eqivaDevice = new EqivaBluetooth(hardwareManager()->bluetoothLowEnergyManager(), QBluetoothAddress(device->paramValue(eqivaBluetoothDeviceMacAddressParamTypeId).toString()), device->name(), this); + m_eqivaDevices.insert(device, eqivaDevice); + + connect(device, &Device::nameChanged, eqivaDevice, [device, eqivaDevice](){ + eqivaDevice->setName(device->name()); + }); + + // Connected state + device->setStateValue(eqivaBluetoothConnectedStateTypeId, eqivaDevice->available()); + connect(eqivaDevice, &EqivaBluetooth::availableChanged, device, [device, eqivaDevice](){ + device->setStateValue(eqivaBluetoothConnectedStateTypeId, eqivaDevice->available()); + }); + // Power state + device->setStateValue(eqivaBluetoothPowerStateTypeId, eqivaDevice->enabled()); + connect(eqivaDevice, &EqivaBluetooth::enabledChanged, device, [device, eqivaDevice](){ + device->setStateValue(eqivaBluetoothPowerStateTypeId, eqivaDevice->enabled()); + }); + // Boost state + device->setStateValue(eqivaBluetoothBoostStateTypeId, eqivaDevice->boostEnabled()); + connect(eqivaDevice, &EqivaBluetooth::boostEnabledChanged, device, [device, eqivaDevice](){ + device->setStateValue(eqivaBluetoothBoostStateTypeId, eqivaDevice->boostEnabled()); + }); + // Lock state + device->setStateValue(eqivaBluetoothLockStateTypeId, eqivaDevice->locked()); + connect(eqivaDevice, &EqivaBluetooth::lockedChanged, device, [device, eqivaDevice](){ + device->setStateValue(eqivaBluetoothLockStateTypeId, eqivaDevice->locked()); + }); + // Mode state + device->setStateValue(eqivaBluetoothModeStateTypeId, modeToString(eqivaDevice->mode())); + connect(eqivaDevice, &EqivaBluetooth::modeChanged, device, [this, device, eqivaDevice](){ + device->setStateValue(eqivaBluetoothModeStateTypeId, modeToString(eqivaDevice->mode())); + }); + // Target temp state + device->setStateValue(eqivaBluetoothTargetTemperatureStateTypeId, eqivaDevice->targetTemperature()); + connect(eqivaDevice, &EqivaBluetooth::targetTemperatureChanged, device, [device, eqivaDevice](){ + device->setStateValue(eqivaBluetoothTargetTemperatureStateTypeId, eqivaDevice->targetTemperature()); + }); + // Window open state + device->setStateValue(eqivaBluetoothWindowOpenStateTypeId, eqivaDevice->windowOpen()); + connect(eqivaDevice, &EqivaBluetooth::windowOpenChanged, device, [device, eqivaDevice](){ + device->setStateValue(eqivaBluetoothWindowOpenStateTypeId, eqivaDevice->windowOpen()); + }); + // Valve open state + device->setStateValue(eqivaBluetoothValveOpenStateTypeId, eqivaDevice->valveOpen()); + connect(eqivaDevice, &EqivaBluetooth::valveOpenChanged, device, [device, eqivaDevice](){ + device->setStateValue(eqivaBluetoothValveOpenStateTypeId, eqivaDevice->valveOpen()); + }); + + // Command handler + connect(eqivaDevice, &EqivaBluetooth::commandResult, this, [this](int commandId, bool success){ + if (m_commandMap.contains(commandId)) { + emit actionExecutionFinished(m_commandMap.take(commandId), success ? DeviceManager::DeviceErrorNoError : DeviceManager::DeviceErrorHardwareFailure); + } + }); + } + return DeviceManager::DeviceSetupStatusSuccess; } void DevicePluginEQ3::deviceRemoved(Device *device) { - if (!m_cubes.values().contains(device)) { - return; + if (device->deviceClassId() == cubeDeviceClassId) { + MaxCube *cube = m_cubes.key(device); + qCDebug(dcEQ3) << "Removing cube" << device->name() << cube->serialNumber(); + cube->disconnectFromCube(); + m_cubes.remove(cube); + cube->deleteLater(); } - MaxCube *cube = m_cubes.key(device); - cube->disconnectFromCube(); - qCDebug(dcEQ3) << "remove cube " << cube->serialNumber(); - m_cubes.remove(cube); - cube->deleteLater(); + if (device->deviceClassId() == eqivaBluetoothDeviceClassId) { + qCDebug(dcEQ3) << "Removing Eqiva device" << device->name(); + m_eqivaDevices.take(device)->deleteLater(); + } + +} + +QString DevicePluginEQ3::modeToString(EqivaBluetooth::Mode mode) +{ + switch (mode) { + case EqivaBluetooth::ModeAuto: + return "Auto"; + case EqivaBluetooth::ModeManual: + return "Manual"; + case EqivaBluetooth::ModeHoliday: + return "Holiday"; + } + Q_ASSERT_X(false, "ModeToString", "Unhandled mode"); + return QString(); +} + +EqivaBluetooth::Mode DevicePluginEQ3::stringToMode(const QString &string) +{ + if (string == "Holiday") { + return EqivaBluetooth::ModeHoliday; + } + if (string == "Manual") { + return EqivaBluetooth::ModeManual; + } + if (string == "Auto") { + return EqivaBluetooth::ModeAuto; + } + Q_ASSERT_X(false, "StringToMode", "Unhandled string:" + string.toUtf8()); + return EqivaBluetooth::ModeAuto; } DeviceManager::DeviceError DevicePluginEQ3::executeAction(Device *device, const Action &action) @@ -194,6 +298,24 @@ DeviceManager::DeviceError DevicePluginEQ3::executeAction(Device *device, const return DeviceManager::DeviceErrorAsync; } } + } else if (device->deviceClassId() == eqivaBluetoothDeviceClassId) { + int commandId; + if (action.actionTypeId() == eqivaBluetoothPowerActionTypeId) { + commandId = m_eqivaDevices.value(device)->setEnabled(action.param(eqivaBluetoothPowerActionPowerParamTypeId).value().toBool()); + } else if (action.actionTypeId() == eqivaBluetoothTargetTemperatureActionTypeId) { + commandId = m_eqivaDevices.value(device)->setTargetTemperature(action.param(eqivaBluetoothTargetTemperatureActionTargetTemperatureParamTypeId).value().toReal()); + } else if (action.actionTypeId() == eqivaBluetoothLockActionTypeId) { + commandId = m_eqivaDevices.value(device)->setLocked(action.param(eqivaBluetoothLockActionLockParamTypeId).value().toBool()); + } else if (action.actionTypeId() == eqivaBluetoothModeActionTypeId) { + commandId = m_eqivaDevices.value(device)->setMode(stringToMode(action.param(eqivaBluetoothModeActionModeParamTypeId).value().toString())); + } else if (action.actionTypeId() == eqivaBluetoothBoostActionTypeId) { + commandId = m_eqivaDevices.value(device)->setBoostEnabled(action.param(eqivaBluetoothBoostActionBoostParamTypeId).value().toBool()); + } else { + Q_ASSERT_X(false, "DevicePluginEQ3", "An action type has not been handled!"); + qCWarning(dcEQ3()) << "An action type has not been handled!"; + } + m_commandMap.insert(commandId, action.id()); + return DeviceManager::DeviceErrorAsync; } return DeviceManager::DeviceErrorActionTypeNotFound; @@ -216,7 +338,7 @@ void DevicePluginEQ3::cubeConnectionStatusChanged(const bool &connected) if (m_cubes.contains(cube)) { device = m_cubes.value(cube); device->setName("Max! Cube " + cube->serialNumber()); - device->setStateValue(cubeConnectionStateTypeId,true); + device->setStateValue(cubeConnectedStateTypeId,true); emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusSuccess); } }else{ @@ -224,7 +346,7 @@ void DevicePluginEQ3::cubeConnectionStatusChanged(const bool &connected) Device *device; if (m_cubes.contains(cube)){ device = m_cubes.value(cube); - device->setStateValue(cubeConnectionStateTypeId,false); + device->setStateValue(cubeConnectedStateTypeId,false); emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusFailure); } } @@ -258,6 +380,28 @@ void DevicePluginEQ3::discoveryDone(const QList &cubeList) emit devicesDiscovered(cubeDeviceClassId,retList); } +void DevicePluginEQ3::bluetoothDiscoveryDone(const QStringList results) +{ + QList deviceDescriptors; + qCDebug(dcEQ3()) << "Discovery finished"; + foreach (const QString &result, results) { + qCDebug(dcEQ3()) << "Discovered device" << result; + DeviceDescriptor descriptor(eqivaBluetoothDeviceClassId, "Eqiva Bluetooth Thermostat", result); + ParamList params; + params.append(Param(eqivaBluetoothDeviceMacAddressParamTypeId, result)); + descriptor.setParams(params); + foreach (Device* existingDevice, myDevices()) { + if (existingDevice->paramValue(eqivaBluetoothDeviceMacAddressParamTypeId).toString() == result) { + descriptor.setDeviceId(existingDevice->id()); + break; + } + } + deviceDescriptors.append(descriptor); + } + + emit devicesDiscovered(eqivaBluetoothDeviceClassId, deviceDescriptors); +} + void DevicePluginEQ3::commandActionFinished(const bool &succeeded, const ActionId &actionId) { if(succeeded){ diff --git a/eq-3/deviceplugineq-3.h b/eq-3/deviceplugineq-3.h index 664e8570..7f437b93 100644 --- a/eq-3/deviceplugineq-3.h +++ b/eq-3/deviceplugineq-3.h @@ -26,6 +26,7 @@ #include "plugin/deviceplugin.h" #include "maxcubediscovery.h" #include "plugintimer.h" +#include "eqivabluetooth.h" #include @@ -51,11 +52,18 @@ public: void deviceRemoved(Device *device) override; private: + QString modeToString(EqivaBluetooth::Mode mode); + EqivaBluetooth::Mode stringToMode(const QString &string); + PluginTimer *m_pluginTimer = nullptr; QList m_config; MaxCubeDiscovery *m_cubeDiscovery = nullptr; QHash m_cubes; + EqivaBluetoothDiscovery *m_eqivaBluetoothDiscovery = nullptr; + QHash m_eqivaDevices; + QHash m_commandMap; + public slots: DeviceManager::DeviceError executeAction(Device *device, const Action &action); @@ -63,6 +71,7 @@ private slots: void onPluginTimer(); void cubeConnectionStatusChanged(const bool &connected); void discoveryDone(const QList &cubeList); + void bluetoothDiscoveryDone(const QStringList results); void commandActionFinished(const bool &succeeded, const ActionId &actionId); void wallThermostatFound(); diff --git a/eq-3/deviceplugineq-3.json b/eq-3/deviceplugineq-3.json index bf5acd4d..68f8cea6 100644 --- a/eq-3/deviceplugineq-3.json +++ b/eq-3/deviceplugineq-3.json @@ -53,7 +53,7 @@ "stateTypes": [ { "id": "d0a9a369-cf8c-47c4-a12e-f2d076bf12fd", - "name": "connection", + "name": "connected", "displayName": "connected", "displayNameEvent": "connected changed", "type": "bool", @@ -577,6 +577,109 @@ "defaultValue": 0 } ] + }, + { + "id": "3c51327b-a823-4479-bd4b-f4ba64267ed8", + "name": "eqivaBluetooth", + "displayName": "Eqiva Bluetooth Smart Radiator Thermostat", + "interfaces": ["heating", "thermostat", "connectable"], + "createMethods": ["discovery"], + "setupMethod": "JustAdd", + "paramTypes": [ + { + "id": "56a77560-e1a3-44fa-8136-57fe5a8d1cbe", + "name": "macAddress", + "displayName": "MAC Address", + "type": "QString" + } + ], + "stateTypes": [ + { + "id": "e223666b-596f-42c0-90b9-1135a6f1c98e", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "cc5700f3-28b0-4653-b46d-770a99e6cea7", + "name": "power", + "displayName": "On", + "displayNameEvent": "On changed", + "displayNameAction": "Turn on/off", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "5e9035ed-317d-42ee-b7f4-2996c75ba939", + "name": "targetTemperature", + "displayName": "Target temperature", + "displayNameEvent": "Target temperature changed", + "displayNameAction": "Set target temperature", + "type": "double", + "defaultValue": 21, + "unit": "DegreeCelsius", + "minValue": 5, + "maxValue": 29.5, + "writable": true + }, + { + "id": "2d285ccf-6d94-4441-9a28-47373caadc5b", + "name": "lock", + "displayName": "Lock", + "displayNameEvent": "Lock changed", + "displayNameAction": "Set lock", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "36070993-6332-4f6f-9907-36756981cc25", + "name": "mode", + "displayName": "Mode", + "displayNameEvent": "Mode changed", + "displayNameAction": "Set mode", + "type": "QString", + "possibleValues": [ + "Auto", + "Manual", + "Holiday" + ], + "writable": true, + "defaultValue": "Auto" + }, + { + "id": "ccca3ccd-33d4-4187-b823-efa0f51a1859", + "name": "boost", + "displayName": "Boost", + "displayNameEvent": "Boost enabled changed", + "displayNameAction": "Enable/disable boost", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "dcacdacc-ee47-43b0-9fef-1fe423e4f355", + "name": "windowOpen", + "displayName": "Window open detected", + "displayNameEvent": "Window open changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "757f154f-30ce-4b9b-a009-b22777f96593", + "name": "valveOpen", + "displayName": "Valve open", + "displayNameEvent": "Valve open changed", + "type": "int", + "unit": "Percentage", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + } + ] } ] } diff --git a/eq-3/eq-3.pro b/eq-3/eq-3.pro index 117bded4..f7e8e9fc 100644 --- a/eq-3/eq-3.pro +++ b/eq-3/eq-3.pro @@ -1,8 +1,8 @@ include(../plugins.pri) -QT += network +QT += network bluetooth -TARGET = $$qtLibraryTarget(nymea_deviceplugineq3) +TARGET = $$qtLibraryTarget(nymea_deviceplugineq-3) SOURCES += \ deviceplugineq-3.cpp \ @@ -11,7 +11,8 @@ SOURCES += \ maxdevice.cpp \ room.cpp \ wallthermostat.cpp \ - radiatorthermostat.cpp + radiatorthermostat.cpp \ + eqivabluetooth.cpp HEADERS += \ deviceplugineq-3.h \ @@ -20,5 +21,6 @@ HEADERS += \ maxdevice.h \ room.h \ wallthermostat.h \ - radiatorthermostat.h + radiatorthermostat.h \ + eqivabluetooth.h diff --git a/eq-3/eqivabluetooth.cpp b/eq-3/eqivabluetooth.cpp new file mode 100644 index 00000000..135a67f3 --- /dev/null +++ b/eq-3/eqivabluetooth.cpp @@ -0,0 +1,434 @@ +#include "eqivabluetooth.h" + +#include "extern-plugininfo.h" + +#include +#include + +const QBluetoothUuid eqivaServiceUuid = QBluetoothUuid(QString("{3e135142-654f-9090-134a-a6ff5bb77046}")); +const QBluetoothUuid commandCharacteristicUuid = QBluetoothUuid(QString("3fa4585a-ce4a-3bad-db4b-b8df8179ea09")); +const QBluetoothUuid notificationCharacteristicUuid = QBluetoothUuid(QString("d0e8434d-cd29-0996-af41-6c90f4e0eb2a")); + +const QBluetoothUuid eqivaDeviceInfoServiceUuid = QBluetoothUuid(QString("9e5d1e47-5c13-43a0-8635-82ad38a1386f")); + +// 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 windowAndKeyOn = 0x30; + +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(30000); + m_refreshTimer.setSingleShot(true); + connect(&m_refreshTimer, &QTimer::timeout, this, &EqivaBluetooth::sendDate); +} + +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::enabled() const +{ + return m_enabled; +} + +int EqivaBluetooth::setEnabled(bool enabled) +{ + emit enabledChanged(); + return setTargetTemperature(enabled ? m_cachedTargetTemp : 4.5); +} + +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_enabled ? m_targetTemp : m_cachedTargetTemp; +} + +int EqivaBluetooth::setTargetTemperature(qreal targetTemperature) +{ + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + stream << commandSetTemp; + if (targetTemperature == 4.5) { + stream << static_cast(4.5 * 2); // 4.5 degrees is off + } else { + stream << static_cast(targetTemperature * 2); // Temperature *2 (device only supports .5 precision) + m_cachedTargetTemp = targetTemperature; + } + 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; +} + +void EqivaBluetooth::controllerStateChanged(const QLowEnergyController::ControllerState &state) +{ + qCDebug(dcEQ3()) << m_name << "Bluetooth device state changed:" << state; + if (state == QLowEnergyController::UnconnectedState) { + int delay = qMin(m_reconnectAttempt, 30); + qWarning(dcEQ3()) << m_name << "Eqiva device disconnected. Reconnecting in" << delay << "sec"; + m_available = false; + emit availableChanged(); + + if (m_currentCommand.id != -1) { + qCDebug(dcEQ3()) << m_name << "Putting command" << m_currentCommand.id << m_currentCommand.name << "back to queue"; + m_commandQueue.prepend(m_currentCommand); + m_currentCommand = Command(); + } + + QTimer::singleShot(delay * 1000, this, [this](){ + qCDebug(dcEQ3()) << m_name << "Trying to reconnect"; + m_reconnectAttempt++; + m_bluetoothDevice->connectDevice(); + }); + } + + if (state != QLowEnergyController::DiscoveredState) { + return; + } + +// qCDebug(dcEQ3()) << m_name << "Discovered: Service UUIDS:" << m_bluetoothDevice->serviceUuids(); + m_eqivaService = m_bluetoothDevice->controller()->createServiceObject(eqivaServiceUuid, this); +// m_eqivaService = m_bluetoothDevice->controller()->createServiceObject(QBluetoothUuid(QString("00001800-0000-1000-8000-00805f9b34fb")), this); +// m_eqivaService = m_bluetoothDevice->controller()->createServiceObject(QBluetoothUuid(QString("00001801-0000-1000-8000-00805f9b34fb")), this); +// m_eqivaService = m_bluetoothDevice->controller()->createServiceObject(QBluetoothUuid(QString("0000180a-0000-1000-8000-00805f9b34fb")), this); +// m_eqivaService = m_bluetoothDevice->controller()->createServiceObject(QBluetoothUuid(QString("9e5d1e47-5c13-43a0-8635-82ad38a1386f")), this); + connect(m_eqivaService, &QLowEnergyService::stateChanged, this, &EqivaBluetooth::serviceStateChanged); +// connect(m_eqivaService, &QLowEnergyService::characteristicRead, this, [](const QLowEnergyCharacteristic &info, const QByteArray &value){ +// qCDebug(dcEQ3()) << m_name << "Characteristic read:" << info.name() << info.uuid() << value.toHex(); +// }); + connect(m_eqivaService, &QLowEnergyService::characteristicWritten, this, [this](const QLowEnergyCharacteristic &info, const QByteArray &value){ + qCDebug(dcEQ3()) << m_name << "Characteristic written:" << info.name() << info.uuid() << value.toHex(); + emit commandResult(m_currentCommand.id, true); + m_currentCommand.id = -1; + processCommandQueue(); + }); + connect(m_eqivaService, &QLowEnergyService::characteristicChanged, this, &EqivaBluetooth::characteristicChanged); + m_eqivaService->discoverDetails(); + +} + +void EqivaBluetooth::serviceStateChanged(QLowEnergyService::ServiceState newState) +{ + qCDebug(dcEQ3()) << m_name << "Service state changed:" << newState; + if (newState != QLowEnergyService::ServiceDiscovered) { + return; + } + + m_available = true; + m_reconnectAttempt = 0; + emit availableChanged(); + +// // 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(); +// } +// } + + sendDate(); +} + +void EqivaBluetooth::characteristicChanged(const QLowEnergyCharacteristic &info, const QByteArray &value) +{ + qCDebug(dcEQ3()) << m_name << "Notification received" << info.uuid() << value.toHex(); + 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) || (lock == windowAndKeyOn); + m_windowOpen = (lock == windowLockOn) || (lock == windowAndKeyOn); + quint8 mode = (lockAndMode & 0x0F); + m_targetTemp = 1.0 * rawTemp / 2; + m_enabled = m_targetTemp >= 5; + if (m_targetTemp < 5) { + m_targetTemp = 5; + } + qCDebug(dcEQ3()) << m_name << "Status notification received: Enabled:" << m_enabled << "Temp:" << m_targetTemp << "Keylock:" << m_locked << "Window open:" << m_windowOpen << "Mode:" << mode << "Valve open:" << m_valveOpen << "Boost:" << m_boostEnabled; + + 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 enabledChanged(); + emit lockedChanged(); + emit boostEnabledChanged(); + emit modeChanged(); + emit windowOpenChanged(); + emit targetTemperatureChanged(); + emit valveOpenChanged(); + + 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 device:" << 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(now.date().year() - 2000); + stream << static_cast(now.date().month()); + stream << static_cast(now.date().day()); + stream << static_cast(now.time().hour()); + stream << static_cast(now.time().minute()); + stream << static_cast(now.time().second()); + + // Example: 03130117172315 -> 03YYMMDDHHMMSS + enqueue("SetDate", data); +} + +int EqivaBluetooth::enqueue(const QString &name, const QByteArray &data) +{ + static int nextId = 0; + + Command cmd; + cmd.name = name; + cmd.id = nextId++; + 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()) { + qCDebug(dcEQ3()) << m_name << "Command queue is empty. Nothing to do..."; + 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(); + 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(sender()); + reply->deleteLater(); + + if (reply->error() != BluetoothDiscoveryReply::BluetoothDiscoveryReplyErrorNoError) { + qCWarning(dcEQ3()) << "Bluetooth discovery error:" << reply->error(); + return; + } + + QStringList results; + + qCDebug(dcEQ3()) << "Discovery finished"; + foreach (const QBluetoothDeviceInfo &deviceInfo, reply->discoveredDevices()) { + qCDebug(dcEQ3()) << "Discovered device" << deviceInfo.name(); + if (deviceInfo.name().contains("CC-RT-BLE")) { + results.append(deviceInfo.address().toString()); + } + } + + emit finished(results); +} diff --git a/eq-3/eqivabluetooth.h b/eq-3/eqivabluetooth.h new file mode 100644 index 00000000..bc5f5dd8 --- /dev/null +++ b/eq-3/eqivabluetooth.h @@ -0,0 +1,118 @@ +#ifndef EQIVABLUETOOTH_H +#define EQIVABLUETOOTH_H + +#include + +#include "hardware/bluetoothlowenergy/bluetoothlowenergymanager.h" + + +class EqivaBluetooth : public QObject +{ + Q_OBJECT +public: + enum Mode { + ModeAuto, + ModeManual, + ModeHoliday + }; + explicit EqivaBluetooth(BluetoothLowEnergyManager *bluetoothManager, const QBluetoothAddress &hostAddress, const QString &name, QObject *parent = nullptr); + ~EqivaBluetooth(); + void setName(const QString &name); + + bool available() const; + + bool enabled() const; + int setEnabled(bool enabled); + + bool locked() const; + int setLocked(bool locked); + + bool boostEnabled() const; + int setBoostEnabled(bool enabled); + + qreal targetTemperature() const; + int setTargetTemperature(qreal targetTemperature); + + Mode mode() const; + int setMode(Mode mode); + + bool windowOpen() const; + + quint8 valveOpen() const; + +signals: + void availableChanged(); + void enabledChanged(); + void lockedChanged(); + void boostEnabledChanged(); + void modeChanged(); + void windowOpenChanged(); + void targetTemperatureChanged(); + void valveOpenChanged(); + + void commandResult(int id, bool result); + +private slots: + void controllerStateChanged(const QLowEnergyController::ControllerState &state); + void serviceStateChanged(QLowEnergyService::ServiceState newState); + void characteristicChanged(const QLowEnergyCharacteristic &info, const QByteArray &value); + + void writeCharacteristic(const QBluetoothUuid &characteristicUuid, const QByteArray &data); + + void sendDate(); + + // Name parameter used for debugging purposes + int enqueue(const QString &name, const QByteArray &data); + void processCommandQueue(); + +private: + BluetoothLowEnergyManager* m_bluetoothManager = nullptr; + BluetoothLowEnergyDevice* m_bluetoothDevice = nullptr; + QLowEnergyService *m_eqivaService = nullptr; + QTimer m_refreshTimer; + + QString m_name; + + bool m_available = false; + bool m_enabled = false; + bool m_locked = false; + bool m_boostEnabled = false; + qreal m_targetTemp = 0; + qreal m_cachedTargetTemp = 0; + Mode m_mode = ModeAuto; + bool m_windowOpen = false; + quint8 m_valveOpen = 0; + + int m_reconnectAttempt = 0; + + struct Command { + QString name; // For debug prints + QByteArray data; + int id = -1; + }; + QList m_commandQueue; + Command m_currentCommand; + + int m_nextCommandId = 0; +}; + +class EqivaBluetoothDiscovery: public QObject +{ + Q_OBJECT +public: + EqivaBluetoothDiscovery(BluetoothLowEnergyManager *bluetoothManager, QObject *parent = nullptr); + + bool startDiscovery(); + +private slots: + void deviceDiscoveryDone(); + +signals: + void finished(const QStringList &results); + +private: + BluetoothLowEnergyManager *m_bluetoothManager = nullptr; + +}; + +#endif // EQIVABLUETOOTH_H