Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document Plejd BLE #163

Open
SweVictor opened this issue Feb 11, 2021 · 20 comments
Open

Document Plejd BLE #163

SweVictor opened this issue Feb 11, 2021 · 20 comments
Labels
help wanted Extra attention is needed

Comments

@SweVictor
Copy link
Collaborator

SweVictor commented Feb 11, 2021

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!)

@klali
Copy link

klali commented Feb 11, 2021

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 klali/ha-plejd@de13334 @emilohman realised that this can be triggered as reads on one output at a time as well to get only specific device states.

@SweVictor
Copy link
Collaborator Author

SweVictor commented Feb 14, 2021

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?

@klali
Copy link

klali commented Feb 14, 2021

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 -> ?

@SweVictor
Copy link
Collaborator Author

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).

@klali
Copy link

klali commented Feb 16, 2021

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.

@SweVictor
Copy link
Collaborator Author

Thanks, clarified in the text!

@SweVictor
Copy link
Collaborator Author

SweVictor commented Feb 20, 2021

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 😉 )

@SweVictor SweVictor changed the title Document Plejd BLE and API Document Plejd BLE May 7, 2021
@faanskit
Copy link
Contributor

faanskit commented May 7, 2021

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`

@SweVictor
Copy link
Collaborator Author

Thanks, added in first post. Left to find out: Rotation of RTR-01 rotary encoder and what BLE commands that sends.

@faanskit
Copy link
Contributor

faanskit commented May 7, 2021

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.

@SweVictor
Copy link
Collaborator Author

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?

@faanskit
Copy link
Contributor

faanskit commented May 7, 2021

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"

@thomasloven
Copy link
Contributor

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 normal outputAddress too

I have tested this on one DWN-01, but it should work with DWN-02 and probably LED-75 too.

@SweVictor
Copy link
Collaborator Author

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)

@thomasloven
Copy link
Contributor

thomasloven commented Nov 12, 2023

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

IMG_6881

@SweVictor
Copy link
Collaborator Author

SweVictor commented Nov 12, 2023

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

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.

@thomasloven
Copy link
Contributor

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.

@SweVictor
Copy link
Collaborator Author

For DWN-01 you have to look into outputSettings.predefinedLoad.loadType which is DWN, and is dimmable despite outputSettings.dimCurve being NonDimmable.

@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 as d, 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 😄

@thomasloven
Copy link
Contributor

thomasloven commented Nov 19, 2023

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"
      },

@thomasloven
Copy link
Contributor

thomasloven commented Feb 20, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

4 participants