icanos / hassio-plejd

Hass.io add-on for Plejd home automation devices
Apache License 2.0
126 stars 37 forks source link

Document Plejd BLE #163

Open SweVictor opened 3 years ago

SweVictor commented 3 years ago

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

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

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

Command/request

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?

Data

Depending on command

Commands

Scene trigger

Command 0021

device id request to respond? command Scene id
02 0110 0021 09

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

(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

(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 commented 3 years ago

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.

SweVictor commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

Thanks, clarified in the text!

SweVictor commented 3 years ago

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

faanskit commented 3 years ago

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 commented 3 years ago

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

faanskit commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 9 months ago

Color temperature

Command 0420

device id request to respond? command unknown color temp
ID 0110 0420 030111 CC CC

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

SweVictor commented 9 months ago

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 commented 9 months ago

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 commented 9 months ago

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 commented 9 months ago

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 commented 9 months ago

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:

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 commented 9 months ago

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 commented 6 months ago

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.