From 28d37110bf3fb289a1b97571cc813d37bc460300 Mon Sep 17 00:00:00 2001 From: loosrob <79396812+loosrob@users.noreply.github.com> Date: Sun, 30 Apr 2023 21:44:05 +0200 Subject: [PATCH] added hue tap as 2 devices The Hue bridge creates 2 separate devices for Hue Tap Dial, one with the 4 buttons (similar to Switch V2) and one with the rotary dial. --- philipshue/hueremote.cpp | 8 +- philipshue/hueremote.h | 2 + philipshue/integrationpluginphilipshue.cpp | 144 ++++++++++++++++++- philipshue/integrationpluginphilipshue.h | 3 +- philipshue/integrationpluginphilipshue.json | 150 +++++++++++++++++++- 5 files changed, 300 insertions(+), 7 deletions(-) diff --git a/philipshue/hueremote.cpp b/philipshue/hueremote.cpp index 22aea704..8e171bba 100644 --- a/philipshue/hueremote.cpp +++ b/philipshue/hueremote.cpp @@ -61,20 +61,24 @@ void HueRemote::updateStates(const QVariantMap &statesMap, const QVariantMap &co QString lastUpdate = statesMap.value("lastupdated").toString(); int buttonCode = statesMap.value("buttonevent").toInt(); + int rotationCode = statesMap.value("expectedrotation").toInt(); - // If we never polled, just store lastUpdate/buttonCode and not emit a falsely button pressed event + // If we never polled, just store lastUpdate/buttonCode/rotationCode and not emit a falsely button pressed event if (m_lastUpdate.isEmpty() || m_lastButtonCode == -1) { m_lastUpdate = lastUpdate; m_lastButtonCode = buttonCode; + m_lastRotationCode = rotationCode; } - if (m_lastUpdate != lastUpdate || m_lastButtonCode != buttonCode) { + if (m_lastUpdate != lastUpdate || m_lastButtonCode != buttonCode || m_lastRotationCode != rotationCode) { m_lastUpdate = lastUpdate; m_lastButtonCode = buttonCode; + m_lastRotationCode = rotationCode; qCDebug(dcPhilipsHue) << "button pressed" << buttonCode; emit buttonPressed(buttonCode); + emit rotated(rotationCode); } } diff --git a/philipshue/hueremote.h b/philipshue/hueremote.h index 9f3d1253..9e5ea769 100644 --- a/philipshue/hueremote.h +++ b/philipshue/hueremote.h @@ -55,10 +55,12 @@ private: int m_battery; QString m_lastUpdate; int m_lastButtonCode = -1; + int m_lastRotationCode = 0; signals: void stateChanged(); void buttonPressed(int buttonCode); + void rotated(int rotationCode); }; diff --git a/philipshue/integrationpluginphilipshue.cpp b/philipshue/integrationpluginphilipshue.cpp index 4646e6fd..37efc1ce 100644 --- a/philipshue/integrationpluginphilipshue.cpp +++ b/philipshue/integrationpluginphilipshue.cpp @@ -515,6 +515,25 @@ void IntegrationPluginPhilipsHue::setupThing(ThingSetupInfo *info) return info->finish(Thing::ThingErrorNoError); } + // Hue Tap Dial + if (thing->thingClassId() == tapDialThingClassId) { + qCDebug(dcPhilipsHue) << "Setup Hue Tap Dial" << thing->params() << thing->thingClassId(); + + HueRemote *hueTapDial = new HueRemote(bridge, this); + + hueTapDial->setId(thing->paramValue(tapDialThingSensorIdParamTypeId).toInt()); + hueTapDial->setModelId(thing->paramValue(tapDialThingModelIdParamTypeId).toString()); + hueTapDial->setType(thing->paramValue(tapDialThingTypeParamTypeId).toString()); + hueTapDial->setUuid(thing->paramValue(tapDialThingUuidParamTypeId).toString()); + + connect(hueTapDial, &HueRemote::stateChanged, this, &IntegrationPluginPhilipsHue::remoteStateChanged); + connect(hueTapDial, &HueRemote::buttonPressed, this, &IntegrationPluginPhilipsHue::onRemoteButtonEvent); + connect(hueTapDial, &HueRemote::rotated, this, &IntegrationPluginPhilipsHue::onRemoteRotaryEvent); + + m_remotes.insert(hueTapDial, thing); + return info->finish(Thing::ThingErrorNoError); + } + // Hue tap if (thing->thingClassId() == tapThingClassId) { HueRemote *hueTap = new HueRemote(bridge, this); @@ -688,7 +707,7 @@ void IntegrationPluginPhilipsHue::thingRemoved(Thing *thing) light->deleteLater(); } - if (thing->thingClassId() == remoteThingClassId || thing->thingClassId() == dimmerSwitch2ThingClassId|| thing->thingClassId() == tapThingClassId || thing->thingClassId() == fohThingClassId || thing->thingClassId() == smartButtonThingClassId || thing->thingClassId() == wallSwitchThingClassId) { + if (thing->thingClassId() == remoteThingClassId || thing->thingClassId() == dimmerSwitch2ThingClassId || thing->thingClassId() == tapDialThingClassId || thing->thingClassId() == tapThingClassId || thing->thingClassId() == fohThingClassId || thing->thingClassId() == smartButtonThingClassId || thing->thingClassId() == wallSwitchThingClassId) { HueRemote *remote = m_remotes.key(thing); m_remotes.remove(remote); remote->deleteLater(); @@ -1209,6 +1228,10 @@ void IntegrationPluginPhilipsHue::remoteStateChanged() thing->setStateValue(dimmerSwitch2ConnectedStateTypeId, remote->reachable()); thing->setStateValue(dimmerSwitch2BatteryLevelStateTypeId, remote->battery()); thing->setStateValue(dimmerSwitch2BatteryCriticalStateTypeId, remote->battery() < 5); + } else if (thing->thingClassId() == tapDialThingClassId) { + thing->setStateValue(tapDialConnectedStateTypeId, remote->reachable()); + thing->setStateValue(tapDialBatteryLevelStateTypeId, remote->battery()); + thing->setStateValue(tapDialBatteryCriticalStateTypeId, remote->battery() < 5); } else if (thing->thingClassId() == tapThingClassId) { thing->setStateValue(tapConnectedStateTypeId, remote->reachable()); } else if (thing->thingClassId() == fohThingClassId) { @@ -1317,6 +1340,49 @@ void IntegrationPluginPhilipsHue::onRemoteButtonEvent(int buttonCode) // * codes ending in 0 (e.g. 1000) indicate start of long press // * codes ending in 3 (e.g. 1003) indicate end of long press // * codes ending in 1 (e.g. 1001) are sent during the long press + } else if (thing->thingClassId() == tapDialThingClassId) { + switch (buttonCode) { + case 1002: + param = Param(tapDialPressedEventButtonNameParamTypeId, "•"); + id = tapDialPressedEventTypeId; + break; + case 1001: + param = Param(tapDialLongPressedEventButtonNameParamTypeId, "•"); + id = tapDialLongPressedEventTypeId; + break; + case 2002: + param = Param(tapDialPressedEventButtonNameParamTypeId, "••"); + id = tapDialPressedEventTypeId; + break; + case 2001: + param = Param(tapDialLongPressedEventButtonNameParamTypeId, "••"); + id = tapDialLongPressedEventTypeId; + break; + case 3002: + param = Param(tapDialPressedEventButtonNameParamTypeId, "•••"); + id = tapDialPressedEventTypeId; + break; + case 3001: + param = Param(tapDialLongPressedEventButtonNameParamTypeId, "•••"); + id = tapDialLongPressedEventTypeId; + break; + case 4002: + param = Param(tapDialPressedEventButtonNameParamTypeId, "••••"); + id = tapDialPressedEventTypeId; + break; + case 4001: + param = Param(tapDialLongPressedEventButtonNameParamTypeId, "••••"); + id = tapDialLongPressedEventTypeId; + break; + default: + qCDebug(dcPhilipsHue()) << "Unhandled button code received from Hue Tap Dial:" << buttonCode << "Thing name:" << thing->name(); + return; + } + // codes ending in 2 (e.g. 1002) are short presses; + // for long presses the Dimmer Switch V2 sends 3 codes: + // * codes ending in 0 (e.g. 1000) indicate start of long press + // * codes ending in 3 (e.g. 1003) indicate end of long press --> not yet supported by this plugin, but e.g. LongPressEnded action could be added + // * codes ending in 1 (e.g. 1001) are sent during the long press --> probably for backwards compatibility with earlier version, and therefore not added to this plugin } else if (thing->thingClassId() == tapThingClassId) { switch (buttonCode) { case 34: @@ -1391,6 +1457,43 @@ void IntegrationPluginPhilipsHue::onRemoteButtonEvent(int buttonCode) emitEvent(Event(id, m_remotes.value(remote)->id(), ParamList() << param)); } +void IntegrationPluginPhilipsHue::onRemoteRotaryEvent(int rotationCode) +{ + HueRemote *remote = static_cast(sender()); + Thing *thing = m_remotes.value(remote); + if (!thing) { + qCWarning(dcPhilipsHue()) << "Received a button press event for a thing we don't know!"; + return; + } + + EventTypeId id; + Param param; + int currentLevel = thing->stateValue(tapDialLevelStateTypeId).toUInt(); + int stepSize = thing->setting(tapDialSettingsStepSizeParamTypeId).toUInt(); + int largeStepSize = thing->setting(tapDialSettingsLargeStepSizeParamTypeId).toUInt(); + + if (thing->thingClassId() == tapDialThingClassId) { + qCDebug(dcPhilipsHue()) << "Rotation code received from Hue Tap Dial:" << rotationCode << "Thing name:" << thing->name(); + if (rotationCode == 15) { + id = tapDialIncreaseEventTypeId; + thing->setStateValue(tapDialLevelStateTypeId, qMin(100, currentLevel + stepSize)); + } else if (rotationCode == -15) { + id = tapDialDecreaseEventTypeId; + thing->setStateValue(tapDialLevelStateTypeId, qMax(0, currentLevel - stepSize)); + } else if (rotationCode > 15) { + id = tapDialLargeIncreaseEventTypeId; + thing->setStateValue(tapDialLevelStateTypeId, qMin(100, currentLevel + largeStepSize)); + } else if (rotationCode < -15) { + id = tapDialLargeDecreaseEventTypeId; + thing->setStateValue(tapDialLevelStateTypeId, qMax(0, currentLevel - largeStepSize)); + } else { + qCDebug(dcPhilipsHue()) << "Unhandled rotation code received from Hue Tap Dial:" << rotationCode << "Thing name:" << thing->name(); + return; + } + } + emitEvent(Event(id, m_remotes.value(remote)->id())); +} + void IntegrationPluginPhilipsHue::onMotionSensorReachableChanged(bool reachable) { HueMotionSensor *sensor = static_cast(sender()); @@ -1557,7 +1660,7 @@ void IntegrationPluginPhilipsHue::processBridgeLightDiscoveryResponse(Thing *thi return; } - qCDebug(dcPhilipsHue()) << "Lights on bridge:" << qUtf8Printable(jsonDoc.toJson()); + // qCDebug(dcPhilipsHue()) << "Lights on bridge:" << qUtf8Printable(jsonDoc.toJson()); // Create Lights if not already added ThingDescriptors descriptors; @@ -1676,6 +1779,8 @@ void IntegrationPluginPhilipsHue::processBridgeSensorDiscoveryResponse(Thing *th return; } + // qCDebug(dcPhilipsHue()) << "Sensors on bridge:" << qUtf8Printable(jsonDoc.toJson()); + // Create sensors if not already added QVariantMap sensorsMap = jsonDoc.toVariant().toMap(); QHash motionSensors; @@ -1729,6 +1834,30 @@ void IntegrationPluginPhilipsHue::processBridgeSensorDiscoveryResponse(Thing *th emit autoThingsAppeared({descriptor}); qCDebug(dcPhilipsHue) << "Found new dimmer switch v2" << sensorMap.value("name").toString() << model; + // Tap Dial + } else if (model == "RDM002") { + if (sensorMap.value("type").toString() == "ZLLRelativeRotary") { + ThingDescriptor descriptor(tapDialThingClassId, sensorMap.value("name").toString(), "Philips Hue Tap Dial", thing->id()); + ParamList params; + params.append(Param(tapDialThingModelIdParamTypeId, model)); + params.append(Param(tapDialThingTypeParamTypeId, sensorMap.value("type").toString())); + params.append(Param(tapDialThingUuidParamTypeId, uuid)); + params.append(Param(tapDialThingSensorIdParamTypeId, sensorId)); + descriptor.setParams(params); + emit autoThingsAppeared({descriptor}); + qCDebug(dcPhilipsHue) << "Found new tap dial" << sensorMap.value("name").toString() << model; + } else if (sensorMap.value("type").toString() == "ZLLSwitch") { + ThingDescriptor descriptor(tapDialThingClassId, sensorMap.value("name").toString(), "Philips Hue Tap Dial", thing->id()); + ParamList params; + params.append(Param(tapDialThingModelIdParamTypeId, model)); + params.append(Param(tapDialThingTypeParamTypeId, sensorMap.value("type").toString())); + params.append(Param(tapDialThingUuidParamTypeId, uuid)); + params.append(Param(tapDialThingSensorIdParamTypeId, sensorId)); + descriptor.setParams(params); + emit autoThingsAppeared({descriptor}); + qCDebug(dcPhilipsHue) << "Found new tap dial" << sensorMap.value("name").toString() << model; + } + // Smart Button } else if (model == "ROM001") { ThingDescriptor descriptor(smartButtonThingClassId, sensorMap.value("name").toString(), "Philips Hue Smart Button", thing->id()); @@ -2130,6 +2259,8 @@ void IntegrationPluginPhilipsHue::bridgeReachableChanged(Thing *thing, bool reac m_remotes.value(remote)->setStateValue(remoteConnectedStateTypeId, false); } else if (m_remotes.value(remote)->thingClassId() == dimmerSwitch2ThingClassId) { m_remotes.value(remote)->setStateValue(dimmerSwitch2ConnectedStateTypeId, false); + } else if (m_remotes.value(remote)->thingClassId() == tapDialThingClassId) { + m_remotes.value(remote)->setStateValue(tapDialConnectedStateTypeId, false); } else if (m_remotes.value(remote)->thingClassId() == tapThingClassId) { m_remotes.value(remote)->setStateValue(tapConnectedStateTypeId, false); } else if (m_remotes.value(remote)->thingClassId() == fohThingClassId) { @@ -2211,6 +2342,13 @@ bool IntegrationPluginPhilipsHue::sensorAlreadyAdded(const QString &uuid) } } + // Hue tap dial + if (thing->thingClassId() == tapDialThingClassId) { + if (thing->paramValue(tapDialThingUuidParamTypeId).toString() == uuid) { + return true; + } + } + // Hue tap if (thing->thingClassId() == tapThingClassId) { if (thing->paramValue(tapThingUuidParamTypeId).toString() == uuid) { @@ -2281,4 +2419,4 @@ void IntegrationPluginPhilipsHue::abortRequests(QHash reply->abort(); } } -} +} \ No newline at end of file diff --git a/philipshue/integrationpluginphilipshue.h b/philipshue/integrationpluginphilipshue.h index 431aff5c..9e5003db 100644 --- a/philipshue/integrationpluginphilipshue.h +++ b/philipshue/integrationpluginphilipshue.h @@ -73,6 +73,7 @@ private slots: void lightStateChanged(); void remoteStateChanged(); void onRemoteButtonEvent(int buttonCode); + void onRemoteRotaryEvent(int rotationCode); // Motion sensor void onMotionSensorReachableChanged(bool reachable); @@ -155,4 +156,4 @@ private: void abortRequests(QHash requestList, Thing* thing); }; -#endif // INTEGRATIONPLUGINPHILIPSHUE_H +#endif // INTEGRATIONPLUGINPHILIPSHUE_H \ No newline at end of file diff --git a/philipshue/integrationpluginphilipshue.json b/philipshue/integrationpluginphilipshue.json index fc1c80a2..caeada94 100644 --- a/philipshue/integrationpluginphilipshue.json +++ b/philipshue/integrationpluginphilipshue.json @@ -673,6 +673,154 @@ } ] }, + { + "id": "58e579b4-f917-478c-9006-f1f8d4df2ded", + "name": "tapDial", + "displayName": "Hue Tap Dial", + "interfaces": ["longpressmultibutton", "battery", "wirelessconnectable"], + "createMethods": ["auto"], + "paramTypes": [ + { + "id": "de566aaa-2a5f-4e31-bce1-5924014fbd6a", + "name": "modelId", + "displayName": "model id", + "type" : "QString", + "readOnly": true + }, + { + "id": "65748463-dd3f-430d-871c-dd7ee4db0b1c", + "name": "type", + "displayName": "type", + "type" : "QString", + "readOnly": true + }, + { + "id": "e2365a3c-cdf3-4b1e-b908-e7642e467f20", + "name": "uuid", + "displayName": "uuid", + "type" : "QString", + "readOnly": true + }, + { + "id": "9263bef9-4dd9-4658-b798-cd39fcb70fee", + "name": "sensorId", + "displayName": "sensor id", + "type" : "int", + "readOnly": true + } + ], + "settingsTypes": [ + { + "id": "045f8a6a-e8de-4780-a4d3-60a986130977", + "name": "stepSize", + "displayName": "Step size", + "type": "uint", + "defaultValue": 5, + "minValue": 1, + "maxValue": 50 + }, + { + "id": "30342d23-b2c1-4969-85f1-1b0f58eeb8ce", + "name": "largeStepSize", + "displayName": "Large Step size", + "type": "uint", + "defaultValue": 10, + "minValue": 1, + "maxValue": 50 + } + ], + "stateTypes": [ + { + "id": "7c81af92-3643-4211-9d62-407cbc596619", + "name": "connected", + "displayName": "reachable", + "displayNameEvent": "reachable changed", + "defaultValue": false, + "type": "bool", + "cached": false + }, + { + "id": "9d0d190f-2898-4c9a-a423-ec7187bfcb5e", + "name": "batteryLevel", + "displayName": "battery", + "displayNameEvent": "battery changed", + "type": "int", + "unit": "Percentage", + "defaultValue": 0, + "minValue": 0, + "maxValue": 100 + }, + { + "id": "79efe477-2757-4457-bdfb-a3fbeb198e7a", + "name": "batteryCritical", + "displayName": "battery critical", + "displayNameEvent": "battery critical changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "23528402-b98f-49b8-90cf-7b68f251ad59", + "name": "level", + "displayName": "Level", + "displayNameEvent": "Level changed", + "unit": "Percentage", + "type": "uint", + "defaultValue": 0, + "minValue": 0, + "maxValue": 100 + } + ], + "eventTypes": [ + { + "id": "9ff8853f-b1fe-44ac-bcfe-193303397012", + "name": "pressed", + "displayName": "Button pressed", + "paramTypes": [ + { + "id": "73863657-b7fa-4c83-827c-6dec46278671", + "name": "buttonName", + "displayName": "Button name", + "type": "QString", + "allowedValues": ["•", "••", "•••", "••••"] + } + ] + }, + { + "id": "4c1bac9f-291e-4bd2-aa2e-c8ecc182c300", + "name": "longPressed", + "displayName": "Button longpress", + "paramTypes": [ + { + "id": "ce015b27-2ffb-4d8b-bb66-63013c4e2f52", + "name": "buttonName", + "displayName": "Button name", + "type": "QString", + "allowedValues": ["•", "••", "•••", "••••"] + } + ] + }, + { + "id": "11edf456-4484-45f3-8638-01162fb47609", + "name": "increase", + "displayName": "Increase" + }, + { + "id": "1b951b52-240d-437c-ae8d-c40298993acb", + "name": "decrease", + "displayName": "Decrease" + }, + { + "id": "8d8aa384-e408-4365-8381-ac763168788b", + "name": "largeIncrease", + "displayName": "Large Increase" + }, + { + "id": "d1415f84-c4cc-478d-89a4-bd7a5c9761e5", + "name": "largeDecrease", + "displayName": "Large Decrease" + } + ] + }, { "id": "1e34a056-9f37-4741-b249-a5eca7a4ab4e", "name": "smartButton", @@ -1310,4 +1458,4 @@ ] } ] -} +} \ No newline at end of file