hassio-plejd
hassio-plejd copied to clipboard
Document Plejd BLE
Broken out of #130
Thought it could be a good idea to compile all known BLE commands and their structure. From this projects code base, mentioned PR and some light looking @klali's great work in https://github.com/klali/ha-plejd
API has been documented in typings .d.ts
files bound for the v0.8.0 release.
Below are my best initial guesses/compilations. Please write any mistakes or improvements in the comments!
BLE characteristics
UUID:s for light level, data, last data, auth and ping below. Used to listen to incoming messages and write messages
- PLEJD_LIGHTLEVEL_UUID = '31ba0003-6085-4726-be45-040c957391b5'
- PLEJD_DATA_UUID = '31ba0004-6085-4726-be45-040c957391b5'
- PLEJD_LAST_DATA_UUID = '31ba0005-6085-4726-be45-040c957391b5'
- PLEJD_AUTH_UUID = '31ba0009-6085-4726-be45-040c957391b5'
- PLEJD_PING_UUID = '31ba000a-6085-4726-be45-040c957391b5'
Outgoing messages
Request light level update
Posted to light level
characteristic. Value 0x01
(hex). Response sent back on same characteristic. Response format:
device id | state | unknown | dim | unknown |
---|---|---|---|---|
67 | 01 | XXXXXX | 0123 | XX |
- Dim is 2 bytes, encoded little-endian
General format of incoming messages
device id | command/read | command | data |
---|---|---|---|
01 | 0110 | 001b | 2aeb236001 |
67 | 0110 | 0098 | 013f5c00 |
02 | 0110 | 0021 | 09 |
00 | 0110 | 0016 | 00d90a |
List of fields below
BLE device Id
- Device 00 is a "broadcast" message used by Bluetooth buttons WPH-01 and WRT-01
- Device 01 is "broadcast" according to the "Time" section below
- Device 02 is in my installation always what's sent for scene updates
- Numeric 03-255 (?) = BLE device id
- Api device id is a long hex string, example
B82B1730526F
Command/request
-
0110
: Command (no response) -
0102
: Read (request response) -
0103
: Read?
Command
Commands are 2 bytes, so 00 should be included.
~~Is the 00
prefix actually part of the command? So: Is the command 001b
or 1b
? Does it matter which we parse?~~
-
0021
: Scene trigger -
0097
: State update -
0098
: Dim+state update ("DIM2" in this code) -
0016
: (Wireless) button pressed -
001b
: Time broadcast -
00c8
: Dim+state update ("DIM" in this code)
Data
Depending on command
Commands
Scene trigger
Command 0021
device id | request to respond? | command | Scene id |
---|---|---|---|
02 | 0110 | 0021 | 09 |
- Not sure how well this works / when this is triggered?
- Device id seems to be
02
always in my installation
State update
Command 0097
device id | request to respond? | command | state |
---|---|---|---|
28 | 0110 | 0097 | 00 |
5d | 0110 | 0097 | 01 |
State/dim update
Command 0098
/ 00c8
device id | request to respond? | command | state |
---|---|---|---|
28 | 0110 | 0097 | 00 |
5d | 0110 | 0097 | 01 |
device type | request to respond? | command | state | dim | data2? |
---|---|---|---|---|---|
5a | 0110 | 0098 | 00 | ffff | 00 |
13 | 0110 | 0098 | 01 | 7878 | 00 |
2d | 0110 | 0098 | 01 | c3f5 |
- dim is 2 bytes, encoded little-endian
- data2 seems to be set sometimes, in my system ===
00
so it can probably be discarded
(Wireless) button pressed
Command 0016
device id | request to respond? | command | state |
---|---|---|---|
29 | 0110 | 0016 | 01 dc0a |
5e | 0110 | 0016 | 03 d90a |
Unit type | Command | Data [0] | Button representation | Mqtt event sent to home assistant |
---|---|---|---|---|
WPH-01 | 0016 | 00 | Top left | button_short_press, button_1 |
WPH-01 | 0016 | 01 | Top right | button_short_press, button_2 |
WPH-01 | 0016 | 02 | Bottom left | button_short_press, button_3 |
WPH-01 | 0016 | 03 | Bottom right | button_short_press, button_4 |
WRT-01 | 0016 | 00 | Rotary button | button_short_press, button_1 |
Time
Command 001b
Using @emilohman's great explanation:
broadcast | don't respond | time command | the time | unknown |
---|---|---|---|---|
01 | 0110 | 001b | 2aeb2360 | 01 |
- Time is encoded little-endian, as a unix timestamp. So 2aeb2360 -> 1612966698 -> Wed Feb 10 15:18:18 2021
(btw, the above code is actually picked up by the lastest code restructure, but only logged with Command 001b seems to be some kind of often repeating ping/mesh data
- now I know better, thanks @emilohman!)
Some random thoughts since you pinged me..
Some data in the plejd API is little endian, for example the dim level is 16 bits little endian. The commands are two bytes (big endian..)
command types: 0110 = command 0102 = read
I can probably have other thoughts, but this is as structured as they are right now.
One other interesting revelation I had was that it's possible to query current state on the light_level uuid, I implemented that at https://github.com/klali/ha-plejd/commit/de1333460dcb67750a22357dc60064db012a61eb @emilohman realised that this can be triggered as reads on one output at a time as well to get only specific device states.
Thanks for that @klali! I actually looked just the other day at that specific commit you referenced, and realized that is something not implemented in this code-base. Haven't had time to look into what it actually does though. I tried my best based on just looking at your code to write down what it writes/decodes, might not be accurate.
The dim level is interesting that you mention. Is what you're saying Plejd actually have 2 byte dim level? So not 0-255 but rather 0-65535? We recently switched to parsing byte 8 (index 7) rather than byte 7 (index 6) but maybe we're then just decoding the most significant byte and discarding the rest?
Yes, dim is two bytes, little-endian. If you let the bytes swap places and decode it it will make sense. This isn't very useful from home-assistant since dim is only one byte there, but to reach highest dim levels you need to set it to ffff.
Yes, I didn't completely decode the lightlevel data, but it's reported one or two outputs at a time, with 10 bytes per output: 0 -> id 1 -> state 2-4 -> ? 5-6 -> dim (little-endian..) 7-9 -> ?
I see. I realize now that we actually do set the full two bytes when setting dim level (const brightnessVal = (brightness << 8) | brightness;
), we however only parse the most significant byte and represent the value internally as only one byte. As you write it might not be the most important thing for HA, but we could potentially use it for transitioning or something else (and it's of course good to know as much as possible about incoming messages).
And as I look at what you wrote about time, remember that it's unix time, so 32 bits (little-endian) since 1970-01-01 00:00, so: 2aeb2360 -> 1612966698 -> Wed Feb 10 15:18:18 2021
Several of the commands seem to have a trailing byte, I have no idea what that means.
Thanks, clarified in the text!
Have been doiing some testing. Interestingly enough it seems you can broadcast light commands to device id 00
to set all lights at once (without any delay). Which brings the question: anyone knows the difference between devices 00
, 01
and 02
, which all seem to be "special"? Time is using 01
, scenes in my installation seem to be using 02
.
Btw - thanks to the discussion here this repo now has a PR for time reading/updates as well as a better handle on dim levels and little-endian encoding, so thanks for that! (btw 2: Makes me think the command are actually 0x9700
little-endian rather than 0x0097
big-endian, not that it matters 😉 )
General format of incoming messages
device id | command/read | command | data |
---|---|---|---|
00 | 0110 | 0016 | 00 dc0a // 01 d90a // 02 d90a // 03 d90a |
BLE device Id Device 00 is a "broadcast" message used by Bluetooth buttons WPH-01 and WRT-01
Command/request 0110: Command (no response)
Command
0016:
Button pressed
Data
For command 0016
(Button pressed), first byte of data represents which button that has been pressed.
Unit | Command | Data [0] | Button representation | Event in home assistant |
---|---|---|---|---|
WPH-01 | 0016 | 00 | Top left | button_short_press, `button_1` |
WPH-01 | 0016 | 01 | Top right | button_short_press, `button_2` |
WPH-01 | 0016 | 02 | Bottom left | button_short_press, `button_3` |
WPH-01 | 0016 | 03 | Bottom right | button_short_press, `button_4` |
WRT-01 | 0016 | 00 | Rotary button | button_short_press, `button_1` |
Thanks, added in first post. Left to find out: Rotation of RTR-01 rotary encoder and what BLE commands that sends.
Rotation of RTR-01 rotary encoder and what BLE commands that sends.
RTR-01 is just another input physically/electrically attached to a Plejd Device like DIM-01, etc. RTR-01 have no Bluetooth and are not sending any BLE commands. It is the host of the RTR-01 that sends the BLE commands, and there is nothing Broadcast for RTR-01 at all that I have seen. Just like any other input on any device, nothing is broadcast at click.
When using RTR-01 towards another device than the host it must be configured towards that other device, just like any other input.
WPH-01 and WTR-01 are different from RTR-01 by being battery powered Bluetooth devices without loads.
Sadly the WRT-01 does not broadcast on rotation. Just like RTR-01 it will only send targeted commands/rotation/dimming after having been configured to a target device.
Sadly the WRT-01 does not broadcast on rotation. Just like RTR-01 it will only send targeted commands/rotation/dimming after having been configured to a target device.
Oh, I didn't realize that was the the case, shame. So - if no device is set as output it sends nothing? And if a device is connected it sends dim command as per usual?
Oh, I didn't realize that was the the case, shame. So - if no device is set as output it sends nothing?
Correct. It only broadcast on click.
And if a device is connected it sends dim command as per usual?
Correct.
For more info, see post from @vBrolin. "Nothing on turn"
Color temperature
Command 0420
device id | request to respond? | command | unknown | color temp |
---|---|---|---|---|
ID | 0110 | 0420 | 030111 | CC CC |
- the
unknown
bytes has always been 03 01 11 in what I have seen -
color temp
is the color temperature in Kelvin. Two bytes, encoded big-endian - The plejd app sends this command to the device id given in
rxAddress
in the cloud API site data, but it works if you send it to the normaloutputAddress
too
I have tested this on one DWN-01, but it should work with DWN-02 and probably LED-75 too.
Color temperature ...
Much appreciated, thanks! Looking at my site json response I don't have any rxAddress
. My site.version
is 1447
- maybe that version is the reason the site json looks different for different installations? Also gateway/not seems to affect things.
@thomasloven could you possibly post a (scrubbed of course) version of yours in some way? There seems to be some difference with DWN-01 (and presumably at least DWN-02 as well) don't register themselves as dimmable in the same way as other devices (#295)
No, that's right.
For DWN-01 you have to look into outputSettings.predefinedLoad.loadType
which is DWN
, and is dimmable despite outputSettings.dimCurve
being NonDimmable
. https://github.com/thomasloven/pyplejd/blob/91e7abfd9d44cddfa14abf919afa01cfca84bce3/pyplejd/cloud/init.py#L152-L158
Quite annoying, and it honestly seems like Pljed are just making up things as they go. As for example in the color temperature being BIG endian while the dim value is LITTLE...
Here's the site details for my test setup: https://gist.github.com/thomasloven/b53ae38ea2971c319618a848e02c0234
For DWN-01 you have to look into
outputSettings.predefinedLoad.loadType
which isDWN
, and is dimmable despiteoutputSettings.dimCurve
beingNonDimmable
. https://github.com/thomasloven/pyplejd/blob/91e7abfd9d44cddfa14abf919afa01cfca84bce3/pyplejd/cloud/init.py#L152-L158
Perfect, thanks for that!
just making up things as they go It absolutely feels like that! 😆 And a lot of redundant-looking info in the json as well, so quite hard to know what values to trust.
DWN-X are not guaranteed to be tunable, by the way. They can be set up to follow the astrotable for the color temperature in which case I believe they will reject any manual settings. See the lines below what I linked above.
For DWN-01 you have to look into
outputSettings.predefinedLoad.loadType
which isDWN
, and is dimmable despiteoutputSettings.dimCurve
beingNonDimmable
.
@thomasloven Looking through your site JSON a bit more carefully and comparing it to our code I note that for the DWN-01 there is a new traits
value.
We (in this repo) use traits
to set capabilities (dimmable mostly). We used to use loadType etc, but that gave us some issues. I'm thinking that the "new" 15
value might be dim + color temperature. That would then give us
NO_LOAD: 0, // 0b0001
NON_DIMMABLE: 9, // 0b1001
DIMMABLE: 11, // 0b1011
DIMMABLE_COLORTEMP: 15, // 0b1111
I added the binary equivalents above, which seem to be reasonable. abcd
would then mean:
-
a
Same asd
, probably signifies something different -
b
Supports color temperature -
c
Supports dimming -
d
Has some load defined
I've added this as a test to fix that DWN are currently not dimmable in the https://github.com/icanos/hassio-plejd/tree/feature/DWN-dimmable-fix branch.
Thoughts?
We would really need some more examples to know for sure, but I'm feeling lucky today 😄
Seems to make sense. I have site JSON from a user with some DWN-2 also, and they have traits either 15 or 9.
I'm not sure how it works, but I guess those can be grouped in some way.
The ones that have traits: 9
also have a property in plejdDevices
called isFellowshipFollower
set to true
and no predefinedLoad
.
Unfortunately they only had one in stock at elbutik.se when I ordered mine for testing, so I can't test the grouping...
{
"deviceId": "C3E245730751",
"siteId": "e6f7cddb-6582-4c39-b485-6982803b5f0f",
"title": "Downlights",
"traits": 15,
"hiddenFromRoomList": false,
"roomId": "fb9f0653-3ceb-45f2-bca8-50321c704cc8",
"createdAt": "2023-09-09T23:20:52.033Z",
"updatedAt": "2023-09-10T19:55:12.605Z",
"hiddenFromIntegrations": false,
"outputType": "LIGHT",
"ACL": {},
"objectId": "NJxc5qbS8B",
"__type": "Object",
"className": "Device"
},
{
"deviceId": "D2B437D3D6C3",
"siteId": "e6f7cddb-6582-4c39-b485-6982803b5f0f",
"title": "Downlights",
"traits": 9,
"hiddenFromRoomList": false,
"roomId": "fb9f0653-3ceb-45f2-bca8-50321c704cc8",
"createdAt": "2023-09-09T23:20:53.286Z",
"updatedAt": "2023-09-09T23:20:53.286Z",
"ACL": {},
"objectId": "63ggVxw5aC",
"__type": "Object",
"className": "Device"
},
{
"deviceId": "C3E245730751",
"siteId": ...,
"installer": {
...
},
"dirtyInstall": false,
"dirtyUpdate": false,
"dirtyClock": false,
"dirtySettings": false,
"hardwareId": "199",
"faceplateId": "0",
"faceplateUpdatedAt": "2023-09-09T23:20:52.029Z",
"firmware": {
...
},
"createdAt": "2023-09-09T23:20:52.033Z",
"updatedAt": "2023-09-10T19:53:43.192Z",
"isFellowshipFollower": false,
"coordinates": {
"__type": "GeoPoint",
"latitude": 68.6974329,
"longitude": 15.1949525
},
"predefinedLoad": {
"loadType": "DWN",
"descriptionKey": "DWNDescription",
"titleKey": "DWNTitle",
"predefinedLoadData": "{\n \"Order\":1,\n \"Min\":0.5,\n \"Max\":100,\n \"Start\":0.5,\n \"OutputSpeed\":0.25,\n \"ColorTemperature\":{\n \"behavior\":\"dimToWarm\",\n \"logFactor\":105,\n \"slewRate\":6554,\n \"minDimLevel\":25,\n \"maxDimLevel\":255,\n \"minTemperatureLimit\":2200,\n \"maxTemperatureLimit\":4000,\n \"minTemperature\":2200,\n \"maxTemperature\":3200\n },\n \"MinDimLevelMapping\":{\n \"0%\":15,\n \"0.1%\":19,\n \"0.2%\":23,\n \"0.3%\":29,\n \"0.4%\":35,\n \"0.5%\":44,\n \"0.6%\":76,\n \"0.7%\":130,\n \"0.8%\":222,\n \"0.9%\":382\n },\n \"OutputType\":\"LIGHT\",\n \"BootState\":\"UseLast\",\n \"UserDefined\":[\n \"ColorTemperature\"\n ],\n \"Settings\":[\n \"SimpleStart\",\n \"Max\",\n \"ColorTemperature\"\n ]\n}",
"createdAt": "2023-05-16T14:37:27.700Z",
"updatedAt": "2023-06-22T13:01:16.366Z",
"defaultDimCurve": {
"__type": "Pointer",
"className": "DimCurve",
"objectId": "xGBw2qRHoE"
},
"allowedDimCurves": {
"__type": "Relation",
"className": "DimCurve"
},
"ACL": {},
"objectId": "G9rgAQ8X6B",
"__type": "Object",
"className": "PredefinedLoad"
},
"diagnostics": "0000170000003200000000000000",
"ACL": {},
"objectId": "wpjCzRm0xz",
"__type": "Object",
"className": "PlejdDevice"
},
{
"deviceId": "D2B437D3D6C3",
"siteId": ...,
"installer": {
...
},
"dirtyInstall": true,
"dirtyUpdate": false,
"dirtyClock": false,
"dirtySettings": false,
"hardwareId": "199",
"faceplateId": "0",
"faceplateUpdatedAt": "2023-09-09T23:20:53.225Z",
"isFellowshipFollower": true,
"firmware": {
...
},
"createdAt": "2023-09-09T23:20:53.286Z",
"updatedAt": "2023-09-10T19:59:37.014Z",
"diagnostics": "0000150000003400000000000000",
"ACL": {},
"objectId": "D3HEfE6djd",
"__type": "Object",
"className": "PlejdDevice"
},
I got a WMS-01 motion sensor. It will send events on LASTDATA when motion is detected, whether or not it is paired to a light.
device id | request to respond? | command | unknown 1 | unknown 2 | light level |
---|---|---|---|---|---|
ID | 0110 | 0420 | 03031f | 0700b10f084616 | 02f0 |
command
is the same as for color temperature commands, but unknown 1
is different. That was 030111
for my DWN-01 but always 03031f
here.
unknown 2
I have no idea about. Once I saw it was 0f00b0...
but every other time it's been 0f00b1...
.
light level
seems to be the light level of the room in big endian encoding. With my strongest light I can nearly push it up to ffff
, but I don't know the range or the unit yet.
Edit: I just realized that part of
unknown 2
may be likely to be the battery voltage. This thing has a standard AA battery.
There are no events sent when no more motion is detected. The cooldown between detection events seems to be just over 30 seconds.
The sensitivity can be set in the app. I've been playing around with it a little bit, but can't see much difference in either behavior or in the siteData.
siteData.devices
:
{
"deviceId": "EE2FE8EBFE52",
"siteId": "<REDACTED>",
"title": "H\u00f6rn",
"traits": 0,
"hiddenFromRoomList": false,
"roomId": "40e2a007-9445-4e5b-821a-4e922ba8fd47",
"createdAt": "2024-02-20T19:52:25.976Z",
"updatedAt": "2024-02-20T19:52:25.976Z",
"ACL": {},
"objectId": "poSqxqgd8X",
"__type": "Object",
"className": "Device"
}
siteData.plejdDevices
:
{
"deviceId": "EE2FE8EBFE52",
"siteId": "<REDACTED>",
"installer": "<REDACTED>",
"dirtyInstall": false,
"dirtyUpdate": false,
"dirtyClock": false,
"dirtySettings": false,
"hardwareId": "70",
"faceplateId": "0",
"faceplateUpdatedAt": "2024-02-20T19:52:25.967Z",
"firmware": {
"notes": "WMS-01",
"data": {
"__type": "File",
"name": "e6bde80da922879ee5cf7d6d47960593_application.bin",
"url": "https://cloud.plejd.com/parse/files/zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak/e6bde80da922879ee5cf7d6d47960593_application.bin"
},
"metaData": {
"__type": "File",
"name": "9082ea4691359bee99f15aad763a4020_application.dat",
"url": "https://cloud.plejd.com/parse/files/zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak/9082ea4691359bee99f15aad763a4020_application.dat"
},
"version": "1.4.4",
"buildTime": 20231122114870,
"firmwareApi": "12",
"createdAt": "2023-11-24T09:17:18.850Z",
"updatedAt": "2023-11-24T09:17:18.850Z",
"ACL": {},
"objectId": "YP9uedNKVw",
"__type": "Object",
"className": "Firmware"
},
"createdAt": "2024-02-20T19:52:25.976Z",
"updatedAt": "2024-02-20T19:52:38.388Z",
"ACL": {},
"objectId": "C3xCAWEZTr",
"__type": "Object",
"className": "PlejdDevice"
}
siteData.inputSettings
:
{
"motionSensorData": {
"threshold": 50,
"blindTime": 15,
"windowTime": 0,
"pulseCounter": 0,
"requireZeroCrossing": true,
"useHpf04": false
},
"deviceId": "EE2FE8EBFE52",
"siteId": "<REDACTED>",
"input": 0,
"buttonType": "WirelessMotionSensor",
"dimSpeed": -1,
"doubleSidedDirectionButton": false,
"createdAt": "2024-02-20T19:52:26.120Z",
"updatedAt": "2024-02-20T19:58:49.184Z",
"ACL": {},
"objectId": "ud53ef9U0b",
"__type": "Object",
"className": "PlejdDeviceInputSetting"
}
siteData.motionSensors
(new section, list of objects):
{
"siteId": "<REDACTED>",
"deviceId": "EE2FE8EBFE52",
"input": 0,
"deviceParseId": "poSqxqgd8X",
"dirty": false,
"dirtyRemove": false,
"active": true,
"createdAt": "2024-02-20T19:53:04.706Z",
"updatedAt": "2024-02-20T19:53:04.706Z",
"ACL": {},
"objectId": "yXpj4rivza",
"__type": "Object",
"className": "MotionSensor"
}
It's also listed in siteData.inputAddress
and siteData.deviceAddress
as usual.