Skip to content

Commit d6e15b4

Browse files
committed
Fix keeping ZGP Proxy entries alive
Issue: dresden-elektronik#436 After pairing a ZGP device, proxy entries need to be refreshed periodically. The PR adds periodic sending of the Pair command for each device (every 15 minutes, like the Hue bridge does). For ZGP devices the following extra attributes are persistently stored in the database: * Frame counter * GPD device id, aka the ZGP device type * GPDKey An extra ResourceItem RStateGPDLastPair keeps track of the timestamp from the last Pair command sending with a monotonic timer value. **Important:** For already paired ZGP switches, it's required to receive the Commissioning command once in order to get this working. This can be done by simply pressing the respective channel button of the device for 10 seconds. The switch does **not** need to be paired again. The respective channel button can be found at: https://phoscon.de/en/support#pairing-friends-of-hue-switch
1 parent 0df9fc0 commit d6e15b4

File tree

6 files changed

+250
-23
lines changed

6 files changed

+250
-23
lines changed

database.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3168,6 +3168,15 @@ static int sqliteLoadAllSensorsCallback(void *user, int ncols, char **colval , c
31683168
return 0;
31693169
}
31703170

3171+
// ZGP switches
3172+
if (sensor.fingerPrint().profileId == GP_PROFILE_ID)
3173+
{
3174+
sensor.addItem(DataTypeString, RConfigGPDKey)->setIsPublic(false);
3175+
sensor.addItem(DataTypeUInt16, RConfigGPDDeviceId)->setIsPublic(false);
3176+
sensor.addItem(DataTypeUInt32, RStateGPDFrameCounter)->setIsPublic(false);
3177+
sensor.addItem(DataTypeUInt64, RStateGPDLastPair)->setIsPublic(false);
3178+
}
3179+
31713180
if (sensor.type().endsWith(QLatin1String("Switch")))
31723181
{
31733182
if (sensor.modelId().startsWith(QLatin1String("SML00"))) // Hue motion sensor
@@ -4062,6 +4071,14 @@ static int sqliteLoadAllSensorsCallback(void *user, int ncols, char **colval , c
40624071
}
40634072
}
40644073

4074+
{
4075+
ResourceItem *item = sensor.item(RStateGPDLastPair);
4076+
if (item)
4077+
{
4078+
item->setValue(0); // reset at startup
4079+
}
4080+
}
4081+
40654082
// check for older setups with multiple ZHASwitch sensors per device
40664083
if (sensor.manufacturer() == QLatin1String("ubisys") && sensor.type() == QLatin1String("ZHASwitch"))
40674084
{

de_web_plugin.cpp

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,6 +1464,14 @@ void DeRestPluginPrivate::gpProcessButtonEvent(const deCONZ::GpDataIndication &i
14641464
}
14651465
sensor->rx();
14661466

1467+
{
1468+
ResourceItem *frameCounter = sensor->item(RStateGPDFrameCounter);
1469+
if (frameCounter)
1470+
{
1471+
frameCounter->setValue(ind.frameCounter());
1472+
}
1473+
}
1474+
14671475
quint32 btn = ind.gpdCommandId();
14681476
if (sensor->modelId() == QLatin1String("FOHSWITCH"))
14691477
{
@@ -1593,6 +1601,7 @@ void DeRestPluginPrivate::gpProcessButtonEvent(const deCONZ::GpDataIndication &i
15931601
}
15941602

15951603
updateSensorEtag(sensor);
1604+
sensor->setNeedSaveDatabase(true);
15961605
sensor->updateStateTimestamp();
15971606
item->setValue(btn);
15981607
DBG_Printf(DBG_ZGP, "ZGP button %u %s\n", item->toNumber(), qPrintable(sensor->modelId()));
@@ -1906,6 +1915,8 @@ void DeRestPluginPrivate::gpDataIndication(const deCONZ::GpDataIndication &ind)
19061915
sensorNode.setNeedSaveDatabase(true);
19071916
sensors.push_back(sensorNode);
19081917

1918+
sensor = &sensors.back();
1919+
19091920
Event e(RSensors, REventAdded, sensorNode.id());
19101921
enqueueEvent(e);
19111922
queSaveDb(DB_SENSORS , DB_SHORT_SAVE_DELAY);
@@ -1920,9 +1931,36 @@ void DeRestPluginPrivate::gpDataIndication(const deCONZ::GpDataIndication &ind)
19201931
gpProcessButtonEvent(ind);
19211932
}
19221933
}
1923-
else
1934+
1935+
if (sensor) // add or update config attributes for known and new devices
19241936
{
1925-
DBG_Printf(DBG_INFO, "SensorNode %s already known\n", qPrintable(sensor->name()));
1937+
{
1938+
ResourceItem *item = sensor->addItem(DataTypeString, RConfigGPDKey);
1939+
item->setIsPublic(false);
1940+
unsigned char buf[GP_SECURITY_KEY_SIZE * 2 + 1];
1941+
DBG_HexToAscii(gpdKey.data(), gpdKey.size(), buf);
1942+
Q_ASSERT(buf[GP_SECURITY_KEY_SIZE * 2] == '\0');
1943+
item->setValue(QString(QLatin1String(reinterpret_cast<char*>(buf))));
1944+
}
1945+
1946+
{
1947+
ResourceItem *item = sensor->addItem(DataTypeUInt16, RConfigGPDDeviceId);
1948+
item->setIsPublic(false);
1949+
item->setValue(gpdDeviceId);
1950+
}
1951+
1952+
{
1953+
ResourceItem *item = sensor->addItem(DataTypeUInt32, RStateGPDFrameCounter);
1954+
item->setIsPublic(false);
1955+
item->setValue(gpdOutgoingCounter);
1956+
}
1957+
1958+
{
1959+
ResourceItem *item = sensor->addItem(DataTypeUInt64, RStateGPDLastPair);
1960+
item->setIsPublic(false);
1961+
item->setValue(deCONZ::steadyTimeRef().ref);
1962+
}
1963+
sensor->setNeedSaveDatabase(true);
19261964
}
19271965
}
19281966
break;
@@ -16312,6 +16350,14 @@ void DeRestPlugin::idleTimerFired()
1631216350
}
1631316351
}
1631416352

16353+
if (sensorNode->fingerPrint().profileId == GP_PROFILE_ID && d->searchSensorsState != DeRestPluginPrivate::SearchSensorsActive)
16354+
{
16355+
if (GP_SendPairingIfNeeded(sensorNode, d->apsCtrl, d->zclSeq + 1))
16356+
{
16357+
d->zclSeq++;
16358+
}
16359+
}
16360+
1631516361
if (sensorNode->modelId().startsWith(QLatin1String("FLS-NB"))) // sync names
1631616362
{
1631716363
LightNode *lightNode = d->getLightNodeForAddress(sensorNode->address());

green_power.cpp

Lines changed: 165 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,45 @@
1+
/*
2+
* Copyright (c) 2020-2021 dresden elektronik ingenieurtechnik gmbh.
3+
* All rights reserved.
4+
*
5+
* The software in this package is published under the terms of the BSD
6+
* style license a copy of which has been included with this distribution in
7+
* the LICENSE.txt file.
8+
*
9+
*/
10+
111
#include <QLibrary>
2-
#include <array>
12+
#include <QDataStream>
13+
314
#ifdef HAS_OPENSSL
4-
#include <openssl/aes.h>
5-
#include <openssl/evp.h>
15+
#define OPEN_SSL_VERSION_MIN 0x10100000L
16+
17+
#include <openssl/opensslv.h>
18+
#if OPENSSL_VERSION_NUMBER >= OPEN_SSL_VERSION_MIN
19+
#define HAS_RECENT_OPENSSL
20+
#endif
21+
#endif
22+
23+
#ifdef HAS_RECENT_OPENSSL
24+
#include <openssl/aes.h>
25+
#include <openssl/evp.h>
626
#endif
727
#include <string>
28+
#include "deconz/aps_controller.h"
29+
#include "deconz/dbg_trace.h"
30+
#include "deconz/green_power.h"
31+
#include "deconz/zcl.h"
832
#include "green_power.h"
33+
#include "resource.h"
34+
35+
#define GP_PAIR_INTERVAL_SECONDS (60 * 15)
936

1037
// this code is based on
1138
// https://github.com/Smanar/Zigbee_firmware/blob/master/Encryption.cpp
12-
#define OPEN_SSL_VERSION_MIN 0x10100000
1339

1440
#define AES_KEYLENGTH 128
1541
#define AES_BLOCK_SIZE 16
1642

17-
const unsigned char defaultTCLinkKey[] = { 0x5A, 0x69, 0x67, 0x42, 0x65, 0x65, 0x41, 0x6C, 0x6C, 0x69, 0x61, 0x6E, 0x63, 0x65, 0x30, 0x39 };
1843

1944
// From https://github.com/Koenkk/zigbee-herdsman/blob/master/src/controller/greenPower.ts
2045
/*!
@@ -23,7 +48,8 @@ GpKey_t GP_DecryptSecurityKey(quint32 sourceID, const GpKey_t &securityKey)
2348
{
2449
GpKey_t result = { 0 };
2550

26-
#ifdef HAS_OPENSSL
51+
#ifdef HAS_RECENT_OPENSSL
52+
const unsigned char defaultTCLinkKey[] = { 0x5A, 0x69, 0x67, 0x42, 0x65, 0x65, 0x41, 0x6C, 0x6C, 0x69, 0x61, 0x6E, 0x63, 0x65, 0x30, 0x39 };
2753

2854
unsigned char nonce[13]; // u8 source address, u32 frame counter, u8 security control
2955
unsigned char sourceIDInBytes[4];
@@ -53,7 +79,7 @@ GpKey_t GP_DecryptSecurityKey(quint32 sourceID, const GpKey_t &securityKey)
5379

5480
if (!libCrypto.load() || !libSsl.load())
5581
{
56-
DBG_Printf(DBG_INFO, "OpenSSl library for ZGP encryption not found\n");
82+
DBG_Printf(DBG_ZGP, "[ZGP] OpenSSl library for ZGP encryption not found\n");
5783
return result;
5884
}
5985

@@ -80,11 +106,11 @@ GpKey_t GP_DecryptSecurityKey(quint32 sourceID, const GpKey_t &securityKey)
80106
_EVP_CIPHER_CTX_free &&
81107
_EVP_aes_128_ccm)
82108
{
83-
DBG_Printf(DBG_INFO, "OpenSSl version 0x%08X loaded\n", openSslVersion);
109+
DBG_Printf(DBG_ZGP, "[ZGP] OpenSSl version 0x%08X loaded\n", openSslVersion);
84110
}
85111
else
86112
{
87-
DBG_Printf(DBG_INFO, "OpenSSl library version 0x%08X for ZGP encryption resolve symbols failed\n", openSslVersion);
113+
DBG_Printf(DBG_ZGP, "[ZGP] OpenSSl library version 0x%08X for ZGP encryption resolve symbols failed\n", openSslVersion);
88114
return result;
89115
}
90116

@@ -110,7 +136,10 @@ GpKey_t GP_DecryptSecurityKey(quint32 sourceID, const GpKey_t &securityKey)
110136

111137
std::copy(encryptBuf.begin(), encryptBuf.begin() + result.size(), result.begin());
112138

113-
#endif // HAS_OPENSSL
139+
#else
140+
Q_UNUSED(securityKey)
141+
DBG_Printf(DBG_ERROR, "[ZGP] failed to decrypt GPDKey for 0x%08X, OpenSSL is not available or too old\n", unsigned(sourceID));
142+
#endif // HAS_RECENT_OPENSSL
114143

115144
return result;
116145
}
@@ -161,11 +190,11 @@ bool GP_SendProxyCommissioningMode(deCONZ::ApsController *apsCtrl, quint8 zclSeq
161190
// broadcast
162191
if (apsCtrl->apsdeDataRequest(req) == deCONZ::Success)
163192
{
164-
DBG_Printf(DBG_INFO, "send GP proxy commissioning mode\n");
193+
DBG_Printf(DBG_ZGP, "[ZGP] send GP proxy commissioning mode\n");
165194
return true;
166195
}
167196

168-
DBG_Printf(DBG_INFO, "send GP proxy commissioning mode failed\n");
197+
DBG_Printf(DBG_ZGP, "[ZGP] send GP proxy commissioning mode failed\n");
169198
return false;
170199
}
171200

@@ -204,7 +233,7 @@ bool GP_SendPairing(quint32 gpdSrcId, quint16 sinkGroupId, quint8 deviceId, quin
204233
// 4: remove gpd
205234
// 5..6: communication mode
206235
// 7: gpd fixed
207-
quint8 options0 = 0xc8; // bits 0..7: add sink, enter commissioning mode, exit on window expire
236+
quint8 options0 = 0x48; // bits 0..7: add sink, enter commissioning mode, exit on window expire
208237

209238
// 0 / 8: gpd mac seq number capabilities
210239
// 1..2 / 9..10: security level
@@ -245,10 +274,131 @@ bool GP_SendPairing(quint32 gpdSrcId, quint16 sinkGroupId, quint8 deviceId, quin
245274
// broadcast
246275
if (apsCtrl->apsdeDataRequest(req) == deCONZ::Success)
247276
{
248-
DBG_Printf(DBG_INFO, "send GP pairing to 0x%04X\n", gppShortAddress);
277+
DBG_Printf(DBG_ZGP, "[ZGP] send GP pairing to 0x%04X\n", gppShortAddress);
278+
return true;
279+
}
280+
281+
DBG_Printf(DBG_ZGP, "[ZGP] send GP pairing to 0x%04X failed\n", gppShortAddress);
282+
return false;
283+
}
284+
285+
286+
// TODO remove TMP_ functions after alarm systems PR is merged, where these functions are in utils.h
287+
static bool TMP_isHexChar(char ch)
288+
{
289+
return ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'));
290+
}
291+
292+
static quint64 TMP_extAddressFromUniqueId(const QString &uniqueId)
293+
{
294+
quint64 result = 0;
295+
296+
if (uniqueId.size() < 23)
297+
{
298+
return result;
299+
}
300+
301+
// 28:6d:97:00:01:06:41:79-01-0500 31 characters
302+
int pos = 0;
303+
char buf[16 + 1];
304+
305+
for (auto ch : uniqueId)
306+
{
307+
if (ch != ':')
308+
{
309+
buf[pos] = ch.toLatin1();
310+
311+
if (!TMP_isHexChar(buf[pos]))
312+
{
313+
return result;
314+
}
315+
pos++;
316+
}
317+
318+
if (pos == 16)
319+
{
320+
buf[pos] = '\0';
321+
break;
322+
}
323+
}
324+
325+
if (pos == 16)
326+
{
327+
result = strtoull(buf, nullptr, 16);
328+
}
329+
330+
return result;
331+
}
332+
333+
/*! For already paired ZGP devices a Pair command needs to be send periodically every \c GP_PAIR_INTERVAL_SECONDS
334+
in order to keep ZGP Proxy entrys alive.
335+
336+
Each ZGP device keeps track of when the last Pair command was sent and the current device frame counter.
337+
*/
338+
bool GP_SendPairingIfNeeded(Resource *resource, deCONZ::ApsController *apsCtrl, quint8 zclSeqNo)
339+
{
340+
if (!resource || !apsCtrl)
341+
{
342+
return false;
343+
}
344+
345+
ResourceItem *gpdLastpair = resource->item(RStateGPDLastPair);
346+
if (!gpdLastpair)
347+
{
348+
return false;
349+
}
350+
351+
const deCONZ::SteadyTimeRef now = deCONZ::steadyTimeRef();
352+
353+
if (now - deCONZ::SteadyTimeRef{gpdLastpair->toNumber()} < deCONZ::TimeSeconds{GP_PAIR_INTERVAL_SECONDS})
354+
{
355+
return false;
356+
}
357+
358+
// the GPDKey must be known to send pair command
359+
ResourceItem *gpdKey = resource->item(RConfigGPDKey);
360+
361+
if (!gpdKey || gpdKey->toString().isEmpty())
362+
{
363+
return false;
364+
}
365+
366+
ResourceItem *frameCounter = resource->item(RStateGPDFrameCounter);
367+
ResourceItem *gpdDeviceId = resource->item(RConfigGPDDeviceId);
368+
ResourceItem *uniqueId = resource->item(RAttrUniqueId);
369+
370+
if (!gpdKey || !frameCounter || !gpdDeviceId || !uniqueId)
371+
{
372+
return false;
373+
}
374+
375+
auto srcGpdId = TMP_extAddressFromUniqueId(uniqueId->toString());
376+
377+
if (srcGpdId == 0 || srcGpdId > UINT32_MAX)
378+
{
379+
return false; // should not happen
380+
}
381+
382+
GpKey_t key;
383+
384+
{
385+
QByteArray arr = QByteArray::fromHex(gpdKey->toString().toLocal8Bit());
386+
DBG_Assert(arr.size() == int(key.size()));
387+
if (arr.size() != int(key.size()))
388+
{
389+
return false;
390+
}
391+
392+
memcpy(key.data(), arr.constData(), key.size());
393+
}
394+
395+
quint8 deviceId = gpdDeviceId->toNumber() & 0xFF;
396+
397+
if (GP_SendPairing(quint32(srcGpdId), GP_DEFAULT_PROXY_GROUP, deviceId, frameCounter->toNumber(), key, apsCtrl, zclSeqNo, deCONZ::BroadcastRouters))
398+
{
399+
gpdLastpair->setValue(now.ref);
249400
return true;
250401
}
251402

252-
DBG_Printf(DBG_INFO, "send GP pairing to 0x%04X failed\n", gppShortAddress);
253403
return false;
254404
}

0 commit comments

Comments
 (0)