EQ-3: Add support for the Eqiva Bluetooth thermostat
parent
323d7415a9
commit
25a73fe059
|
|
@ -1 +1 @@
|
|||
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_deviceplugineq3.so
|
||||
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_deviceplugineq-3.so
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@
|
|||
#include "types/param.h"
|
||||
#include "plugininfo.h"
|
||||
|
||||
#include "eqivabluetooth.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
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<MaxCube *> &cubeList)
|
|||
emit devicesDiscovered(cubeDeviceClassId,retList);
|
||||
}
|
||||
|
||||
void DevicePluginEQ3::bluetoothDiscoveryDone(const QStringList results)
|
||||
{
|
||||
QList<DeviceDescriptor> 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){
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
#include "plugin/deviceplugin.h"
|
||||
#include "maxcubediscovery.h"
|
||||
#include "plugintimer.h"
|
||||
#include "eqivabluetooth.h"
|
||||
|
||||
#include <QHostAddress>
|
||||
|
||||
|
|
@ -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<Param> m_config;
|
||||
MaxCubeDiscovery *m_cubeDiscovery = nullptr;
|
||||
QHash<MaxCube *, Device *> m_cubes;
|
||||
|
||||
EqivaBluetoothDiscovery *m_eqivaBluetoothDiscovery = nullptr;
|
||||
QHash<Device*, EqivaBluetooth*> m_eqivaDevices;
|
||||
QHash<int, ActionId> 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<MaxCube *> &cubeList);
|
||||
void bluetoothDiscoveryDone(const QStringList results);
|
||||
void commandActionFinished(const bool &succeeded, const ActionId &actionId);
|
||||
|
||||
void wallThermostatFound();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,434 @@
|
|||
#include "eqivabluetooth.h"
|
||||
|
||||
#include "extern-plugininfo.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDateTime>
|
||||
|
||||
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<quint8>(4.5 * 2); // 4.5 degrees is off
|
||||
} else {
|
||||
stream << static_cast<quint8>(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<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)
|
||||
{
|
||||
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<BluetoothDiscoveryReply *>(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);
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
#ifndef EQIVABLUETOOTH_H
|
||||
#define EQIVABLUETOOTH_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#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<Command> 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
|
||||
Loading…
Reference in New Issue