diff --git a/components/ble_adv_controller/CUSTOM.md b/components/ble_adv_controller/CUSTOM.md index 7e9e7b0..0a00773 100644 --- a/components/ble_adv_controller/CUSTOM.md +++ b/components/ble_adv_controller/CUSTOM.md @@ -18,36 +18,153 @@ There are a few basics to know about android applications exchanging messages wi - if the same message was already processed, discard it - else process it * **Pairing** consists in having the phone (or controller) and the device agreeing on an identifier to be sent in all messages and that would indicate the device is the effective target of the message. This is done by sending a pairing message including an identifier generated by the app/controller to the device. When the device receives this message with an identifier it does not know it will just ... ignore it ... EXCEPT if it has been restarted (power off/on) less than 5 seconds ago, in this case it will accept the new identifier. This is the choosen way to have a specific device accepting a new identifer, and not all the devices in the roomm... -* Pairing with several controllers is possible, at least a remote and a phone, but it seems the phone apps and our apps may have to share the same identifier, which for now is not supported as the ID is generated differently. -* When the device is delivered to the end-user, it will no more change, meaning that the messages it receives to be controlled will have to be always the same. This particularly means that if an application is able to control it at a given time, any new version of this application in the future (new android version, bug fix, ...) will have to support the send of those exact same messages. - +* Pairing with several controllers is possible, at least a remote and a phone, but it seems the phone apps and our apps may have to share the same identifier. +* When the device is delivered to the end-user, it will no more change, meaning that the messages it receives to be controlled will have to be always the same. This particularly means that if an application is able to control it at a given time, any new version of this application in the future (new android version, bug fix, ...) will have to support the send of those exact same messages, and then the last version of the app will generate different variants corresponding to previous encoding at a given time. ## The messages The BLE Advertising messages are composed of different parts: -* The BLE Advertising Parameters (esp_ble_adv_params_t) which are always the same and part of the BLE standard -* The BLE Advertising data (usually 31 bytes) composed of: - * A Header (esp_ble_adv_data_t) also part of the BLE standard - * A **Manufacturer Data** section, usually composed of 26 bytes - -The goal of this component is to convert each entity action into this Manufacturer Data section (encoding) and emit it. Still this is not so simple as there are several applications using this methodology, and for each application different ways of encoding the data that evovled in the last years. +* The BLE Advertising Header and stack which are always the same and part of the BLE standard, the behaviour can be customized vi 'esp_ble_adv_params_t' +* The BLE Advertising data (max 31 bytes) composed of repeated data sections: + * 1 byte for the length of the section (length of the data + length of the type) + * 1 byte for the type of the section + * the data of the section + + Example: + * raw data: 02.01.02.1B.03.F9.08.49.13.F0.69.25.4E.31.51.BA.32.08.0A.24.CB.3B.7C.71.DC.8B.B8.97.08.D0.4C + * section 1: + * 02.01.02 + * => Length: 2, type: 01 (AD_FLAG), data: 02 + * section 2: + * 1B.03.F9.08.49.13.F0.69.25.4E.31.51.BA.32.08.0A.24.CB.3B.7C.71.DC.8B.B8.97.08.D0.4C + * => Length: 1B (27), type: 03 (UUIDs), data: F9.08.49.13.F0.69.25.4E.31.51.BA.32.08.0A.24.CB.3B.7C.71.DC.8B.B8.97.08.D0.4C (26 uint) + +The goal of this component is to convert each entity action into this raw data and emit it. Still this is not so simple as there are several applications using this methodology, and for each application different ways of encoding the data that evovled over the years. Supported Applications: * **Zhi jia**, includes several encoding variant: v0 (MSC16) / v1 (MSC26) / v2 (MSC26A) -* **FanLamp Pro** / **LampSmart Pro**, includes several encoding variant: v1a / v1b / v2 / v3 +* **FanLamp Pro**, includes variants v1 / v2 / v3 +* **LampSmart Pro**, includes variants: v1 / v3 +* Other (probably remotes), found in other repositories. Similar to what FanLamp Pro and Lamp Smart Pro generate but with small differences, variants v1a / v1b / v2 / v3. -To build the Manufacturer Data section corresponding to a command, the encoding is done as follow: -* Convert the command and its parameters into a base 26 bytes structure containing among other: - * A **type**: a 2 bytes code. No one found out yet the use of it, seems to be always 0x0100 but can be set to anything it seems - * An **index** / group_index: a 1 byte code allowing to specify a sub-identifier. Quite useless for this component as it is able to specify different ids with forced_id / controller id. +To build the Data section corresponding to a command, the encoding is done as follow: +* Convert the command and its parameters into a base structure containing among other: * A **command id**: a code based on one byte identifying a command. Different in between applications but usually common between variants of a same app. * Command **arguments**: 0 to 4 bytes containing parameters of the command (fan speed value, light brigthness, ...) * Add parameters from the controller part: + * A **type**: a 2 bytes code. No one found out yet the use of it, seems to be always 0x0100 but can be set to anything it seems + * An **index** / group_index: a 1 byte code allowing to specify a sub-identifier. * An **identifier**: a 2 or 4 bytes code generated and exchanged during pairing, identifying a device in a 'unique' way * A **transaction count**: a 1 byte code increased by 1 on each transaction. Allows the device to identify if a message was already read and processed, and not re-process it (guess) * Signing: compute an id based on a hard coded key allowing the device to be sure the message is coming only from the allowed app and would not be an interference from another message from another app (or a way to try to prevent smart people to reproduce the message...) * CRC computing: to be sure the message is complete and not corrupted * Whitening: to avoid the message to be mostly zeros +# Capturing Advertising messages +It can be usefull to capture the messages sent by a phone app or a remote already paired with the device you want to control, in order to extract some info such as the `identifier` for instance. + +This can be done quite 'easily' using the [ESPHome BLE Tracker](https://esphome.io/components/esp32_ble_tracker.html) component, and this config (you need to have at least one ble_adv_controller defined): + +``` +esp32_ble_tracker: + scan_parameters: + interval: 15ms + window: 15ms + on_ble_advertise: + then: + - lambda: 'ble_adv_static_handler->capture(x, true);' + +ble_adv_controller: + - id: my_controller + encoding: fanlamp_pro +``` + +This will generate DEBUG logs such as those ones each time a raw advertising message is received: +``` +[17:37:52][D][ble_adv_handler:297]: raw - 02.01.02.03.03.27.18.15.16.27.18.A8.01.51.3F.91.A2.00.E2.DC.38.AD.F0.64.03.07.00.00.00 (29) +``` + +It TRIES to capture EVERYTHING, meaning: +* if you have existing Bluetooth devices doing BLE Advertising, you will also capture the logs of those devices... +* it tries to capture as much as it can, but it can miss some of the messages, I would say it captures 75% of the messages + +Moreover, the phone app or the remotes are generating several advertising messages for a same command issued, for example the ***FanLamp Pro app is generating 6 distinct raw message for each action*** (2 commands for each variant with different AD Flag section...) + +For each message captured, it tries to decode it with each encoder available, and if one matches it produces the following: +``` +[16:08:56][D][ble_adv_handler:268]: raw - 02.01.01.1B.03.F0.08.30.80.B8.F7.E1.27.DB.F4.95.C1.65.7D.A4.9F.67.F6.B6.30.34.8B.53.2B.38.A2 (31) +[16:08:56][I][lampsmart_pro - v3:233]: Decoded OK - tx: 131, cmd: '0x11', Args: [0,0,0,0] +[16:08:56][I][ble_adv_handler:243]: config: +[16:08:56]ble_adv_controller: +[16:08:56] - id: my_controller_id +[16:08:56] encoding: lampsmart_pro +[16:08:56] variant: v3 +[16:08:56] forced_id: 0xB4555A3F +[16:08:56][D][lampsmart_pro - v3:103]: UUID: '0xB4555A3F', index: 0, tx: 131, cmd: '0x11', args: [0,0,0,0] +[16:08:56][D][ble_adv_handler:254]: enc - 02.01.19.1B.03.F0.08.30.80.B8.F7.E1.27.DB.F4.95.C1.65.7D.A4.9F.67.F6.B6.30.34.8B.53.2B.38.A2 (31) +[16:08:56][I][ble_adv_handler:256]: Decoded / Re-encoded with NO DIFF +``` + +The config loggued gives you what iall the info you would need to setup a copy of the remote / phone app you listen! + +STILL if you listen to your phone app, you will end up with more or less 6 configs (and 3 removing the dupe, one for each variant), so you will have to find the relevant one as the controlled device probably listen to only ONE of those variants... + +# Raw injection service +If you captured a raw advertising message emitted by a phone app or a remote, just define a dummy controller and you can re inject the message as such with the following HA service: +``` +esphome: _inject_raw_ +``` +Just put the raw hexa string in the `raw` parameters, accepted formats (leading 0x, trailing length, spaces and dots are removed automatically): +``` +0201021B03F9084913F069254E3151BA32080A24CB3B7C71DC8BB89708D04C +0x0201021B03F9084913F069254E3151BA32080A24CB3B7C71DC8BB89708D04C +02 01 02 1B 03 F9 08 49 13 F0 69 25 4E 31 51 BA 32 08 0A 24 CB 3B 7C 71 DC 8B B8 97 08 D0 4C +02.01.02.1B.03.F9.08.49.13.F0.69.25.4E.31.51.BA.32.08.0A.24.CB.3B.7C.71.DC.8B.B8.97.08.D0.4C +02.01.02.1B.03.F9.08.49.13.F0.69.25.4E.31.51.BA.32.08.0A.24.CB.3B.7C.71.DC.8B.B8.97.08.D0.4C (31) +``` +This will lead to the component re emitting the raw command as such, which can be useful if none of the encoding / variant is working for you, as you can still be able to build a template light in Home Assistant directly if you managed to capture the ON and OFF command, such as this one: +``` +light: + - platform: template + lights: + my_raw_light: + friendly_name: "My Raw Light" + turn_on: + service: esphome.my_device_inject_raw_my_controller + data: + raw: 02.01.02.1B.16.F0.08.10.00.DC.36.2F.22.9A.A0.0F.BE.FC.F9.68.C1.28.0C.1D.AD.09.DA.19.A9.35.23 + turn_off: + service: esphome.my_device_inject_raw_my_controller + data: + raw: 02.01.02.1B.16.F0.08.10.00.DF.59.DC.4B.A4.38.7A.C8.A8.B0.6D.F8.3F.FD.B7.A9.FC.7C.4A.C0.AA.7C +``` +The controller parameters forced_id / index / encoding / variant are ignored, but the `duration` and `max_duration` are available. The messages are also put in the sequencing queue and benefit from the centralized advertising of the component. + +# Raw decoding Service +If you captured a raw advertising message emitted by a phone app or a remote, you can try to have it decoded by the existing decoders available in the app, using the service: +``` +esphome: _raw_decode +``` +Just put the raw hexa string in the `raw` parameters, see previous section for the formats. +You will see the result of the decoding in the logs of the application, as such: +``` +[16:08:56][D][ble_adv_handler:268]: raw - 02.01.01.1B.03.F0.08.30.80.B8.F7.E1.27.DB.F4.95.C1.65.7D.A4.9F.67.F6.B6.30.34.8B.53.2B.38.A2 (31) +[16:08:56][I][lampsmart_pro - v3:233]: Decoded OK - tx: 131, cmd: '0x11', Args: [0,0,0,0] +[16:08:56][I][ble_adv_handler:243]: config: +[16:08:56]ble_adv_controller: +[16:08:56] - id: my_controller_id +[16:08:56] encoding: lampsmart_pro +[16:08:56] variant: v3 +[16:08:56] forced_id: 0xB4555A3F +[16:08:56][D][lampsmart_pro - v3:103]: UUID: '0xB4555A3F', index: 0, tx: 131, cmd: '0x11', args: [0,0,0,0] +[16:08:56][D][ble_adv_handler:254]: enc - 02.01.19.1B.03.F0.08.30.80.B8.F7.E1.27.DB.F4.95.C1.65.7D.A4.9F.67.F6.B6.30.34.8B.53.2B.38.A2 (31) +[16:08:56][I][ble_adv_handler:256]: Decoded / Re-encoded with NO DIFF +``` +* raw: the hexa string injected +* Decoded OK: the message was decoded by an encoder, here 'lampsmart_pro - v3'. Action Parameters are loggued. +* config: the config to setup for your controller to duplicate the source of the message. The defined controller will use the same identifier and will then be able to control the same device without any need to pair! +* enc: the hexa string as it would be re-encoded by the encoder from the parameters extracted for the controller and the Action parameters. +* the result of the comparison between what was injected and what was re encoded, to be sure the encoder would work OK! This comparison ignores the irrelevant differences in AD_Flag section (02.01.01 / 02.01.19). + # Custom Command Service if you are using 'api' component to communicate with HA, for each ble_adv_controller a HA service is available: * name of the service: @@ -70,7 +187,7 @@ For info here are the "known" commands already extracted from code and their cor * uses ZhiJia encoding, variant v2: encoding MSC26A. * Only arg0 used for known commands, BUT arg1 and arg2 available in message structure * **FanLamp v1**: - * for FanLamp Pro / SmartLamp Pro, with variant v1a and v1b using the same data structure. + * for FanLamp Pro / SmartLamp Pro, with variant v1, and Other for variant v1a and v1b using the same data structure. * arg0 and arg1 used for known commands, but arg2 available (and used by pair command btw...) * **FanLamp v2**: * for FanLamp Pro / SmartLamp Pro, with variant v2 and v3 (v3 is v2 plus a signing step) @@ -160,7 +277,7 @@ Example custom commands: * custom command parameters: {cmd: 222, arg0: 0, arg1: 0, arg2: 0, arg3: 0} -### FanLamp v1 (v1a and v1b) +### FanLamp v1 (and v1a / v1b) * Source code [HERE](https://gist.github.com/NicoIIT/527f21f7bbbd766b9844d5efbef86959) * Commands are calling function 'getMessage' with 3, 4 or 5 parameters most of the time: ``` diff --git a/components/ble_adv_controller/README.md b/components/ble_adv_controller/README.md index 16b2912..4a882f9 100644 --- a/components/ble_adv_controller/README.md +++ b/components/ble_adv_controller/README.md @@ -2,8 +2,9 @@ ## Goal and requirements The goal of this component is to build a hardware proxy [ESPHome based](https://esphome.io/) in between Home Assistant and Ceiling Fans and Lamps controlled using Bluetooth Low Energy(BLE) Advertising. If your Ceiling Fan or lamp is working with one of the following Android App, then you should be able to control it: -* LampSmart Pro or FanLamp Pro (tested against Marpou Ceiling Light / IRALAN Lamp and Fan / AQUBT Ceiling Fans) -* ZhiJia (tested against aftermarket LED drivers) +* LampSmart Pro +* FanLamp Pro +* ZhiJia This component is an [ESPHome external component](https://esphome.io/components/external_components.html). In order to use it you will need to have: * A basic knowledge of [ESPHome](https://esphome.io/). A good entry point is [here](https://esphome.io/guides/getting_started_hassio.html). @@ -35,7 +36,7 @@ The technical solution implemented by manufacturers to control those devices is 4. Add one or several light or fan entities to the configuration with the `ble_adv_controller` platform 5. Add a `pair` configuration button to ease the pairing action from HA 6. Install and flash the ESP32 device -7. Find the relevant `variant` and `duration` corresponding to your device thanks to [Dynamic configuration](#dynamic-configuration) +7. Find the relevant `variant` and `duration` corresponding to your device thanks to [Dynamic configuration](#dynamic-configuration) or the full setup by [setting without pairing](#setup-without-pairing) 8. Enjoy controlling your BLE light with Home Assistant! ## Known issues and not implemented or tested features @@ -92,11 +93,17 @@ button: ble_adv_controller: # A controller per device, or per remote in fact as it has the same role - id: my_controller - # encoding: could be any of 'zhijia', 'fanlamp_pro', 'smartlamp_pro' (the 2 last are the same) + # encoding: could be any of 'zhijia', 'fanlamp_pro', 'smartlamp_pro', 'other' or 'remote' + # 'smartlamp_pro', 'other' or 'remote' are very similar to 'fanlamp_pro' but slightly different + # if nothing is specified for them in what follows, consider them as 'fanlamp_pro' encoding: fanlamp_pro # variant: variant of the encoding # For ZhiJia: Can be v0 (MSC16), v1 (MSC26) or v2 (MSC26A), default is v2 - # For Fanlamp: Can be any of 'v1a', 'v1b', 'v2' or 'v3', depending on how old your lamp is... Default is 'v3' + # For Fanlamp: Can be any of 'v1', 'v2' or 'v3', depending on how old your lamp is... Default is 'v3' + # For LampSmart: Can be v1 or v3 + # For 'remote': can be v1 or v3 (only remotes we know..) + # For 'other': Can be any of v1a / v1b / v2 / v3, they are corresponding to legacy variants for 'lampsmart_pro' extracted from old version of this repo. + # Kept here for backward compatibility but should not be needed. # Can be configured dynamically in HA directly, device 'Configuration' section, "Encoding". variant: v3 # max_duration (default 3000, range 300 -> 10000): the maximum duration in ms during which the command is advertized. @@ -120,6 +127,9 @@ ble_adv_controller: # For ZhiJia, default to 0xC630B8 which was the value hard-coded in ble_adv_light component. Max 0xFFFFFF. # For FanLamp: default to 0, uses the hash id computed by esphome from the id/name of the controller forced_id: 0 + # index: a supplementary counter on the phone app to distinguish in between several devices + # only usefull if you want to copy the phone app setup + index: 0 # show_config (default true): shows the dynamic configuration in the device info page in Home Automation show_config: true @@ -190,6 +200,14 @@ It could be painful to find the correct variant or the correct duration by each Once you managed to define the relevant values (without the need to re flash each time!), you can save the values in the yaml config, and even hide the dynamic configuration with the option `show_config: false` +### Setup without pairing +Yes, it is possible! +If you already have a phone app or a remote already paired with the device to be controlled, then it means there is already an existing `identifier` (and possibly `index`) setup on the control device, so you just need to find it and setup your controller accordingly (`forced_id` and `index`). + +Check the [tech section](CUSTOM.md#capturing-advertising-messages) to know how to capture and log those parameters. + +If you already captured some traffic with app such as `nRF Connect` or `wireshark`, you can inject them directly using this [HA Service](CUSTOM.md#raw-injection-service). + ### Reverse Cold / Warm If this component works, but the cold and warm temperatures are reversed (that is, setting the temperature in Home Assistant to warm results in cold/blue light, and setting it to cold results in warm/yellow light), add a `reversed: true` line to your `ble_adv_controller` config. @@ -200,7 +218,9 @@ If the brightness or color temperature does not work for your Zhi Jia v1 or v2 l If the minimum brightness is too bright, and you know that your light can go darker - try changing the minimum brightness via the `min_brightness` configuration option (it takes a percentage) or directly via the dynamic configuration in HA `Min Brightness`. ### Saving state on ESP32 reboot -Fan and Light entities are inheriting properties from their ESPHome parent [Fan](https://esphome.io/components/fan/index.html) and [Light](https://esphome.io/components/light/index.html), in particular they implement the `restore_mode` which has default value `ALWAYS_OFF`. Just adding it to your config with value `RESTORE_DEFAULT_OFF` will have the Fan or Light remember its last state (ON/OFF, but also brightness, color temperature and fan speed). +Fan and Light entities are inheriting properties from their ESPHome parent [Fan](https://esphome.io/components/fan/index.html) and [Light](https://esphome.io/components/light/index.html), in particular they implement the `restore_mode` which has default value `ALWAYS_OFF` on ESPHome base lights and fans. + +The default has been forced to value `RESTORE_DEFAULT_OFF` on the fan and light entities of this component so that they could remember their last state (ON/OFF, but also brightness, color temperature and fan speed). You can still modify this value if it is not OK for you. ### Action on turn on/off Some devices perform some automatic actions when the main light or the fan are switched off, as for instance switch off the secondary light, or reset the Fan Direction or Oscillation status. @@ -246,6 +266,26 @@ Esphome is providing features to handle 'smooth' transitions. While they are not For instance, the Zhi Jia app is always sending at least 2 messages when the brightness or color temperature is updated and this can be achieved the same way by setting the light property 'default_transition_length' to the same value than 'duration', as per default 200ms. (NOT TESTED but may work and solve flickering issues) +### Warning in logs +You can have the following warnings in logs: +``` +[16:08:56][W][component:237]: Component 'xxxx' took a long time for an operation (56 ms). +[16:08:56][W][component:238]: Components should block for at most 30 ms. +``` +This is not an issue, it just means ESPHome considered it spent too much time in our component and that it should not be the case. It has no real impact but it means the Api / Wifi / BLE may not work properly or work slowly. +In fact this is mostly due to ESPHome itself as 99% of the time spent in our component is due to the logs... Each line of log needs 10ms to be processed ............... So 5 lines of log during a transaction and we are over the limit... + +In order to avoid this, once you have finalized your config and all is working OK, I recommend to [setup the log level to INFO](https://esphome.io/components/logger.html) instead of DEBUG (which is the default). + +### No encoder is working, help !!!!! +Two different cases here: +* You have successfully paired your device with one of the 3 referenced app at the top of this guide, but you cannot pair the controller you setup whereas you followed this guide. This is not normal, open an Issue on this git repo specifying your full config (anonymized), the phone App to whcich it is paired, the steps you followed and the corresponding DEBUG logs. +* Your device is not working with any of the phone app referenced (another one or a remote?), but you want to have it work with HA! Try to [capture the advertising messages](CUSTOM.md#capturing-advertising-messages) generated by your controlling app or remote. + * If nothing is captured, your device is not controlled by BLE advertising, and we cannot do anything for you. + * If something is captured and a config is extracted, then all is OK! + * If something is captured but no config is extracted but your are in a hurry, you can still build a HA Template light from the captured messages, using the [Raw injection service](CUSTOM.md#raw-injection-service) + * If something is captured but no config is extracted and you are not in a hurry, and you manage to control your device from another phone app, then open an Issue to have your phone app integrated to this component! + ## For the very tecki ones If you want to discover new features for your lamp and that you are able to understand the code of this component as well as the code of the applications that generate commands, you can try to send custom commands, details [here](CUSTOM.md). \ No newline at end of file diff --git a/components/ble_adv_controller/__init__.py b/components/ble_adv_controller/__init__.py index ae15a7e..bd3f421 100644 --- a/components/ble_adv_controller/__init__.py +++ b/components/ble_adv_controller/__init__.py @@ -32,32 +32,164 @@ BleAdvHandler = bleadvcontroller_ns.class_('BleAdvHandler', cg.Component) BleAdvEntity = bleadvcontroller_ns.class_('BleAdvEntity', cg.Component) -FanLampEncoder = bleadvcontroller_ns.class_('FanLampEncoder', BleAdvEncoder) -FanLampEncoder_ns = bleadvcontroller_ns.namespace('FanLampEncoder') -ZhijiaEncoder = bleadvcontroller_ns.class_('ZhijiaEncoder', BleAdvEncoder) -ZhijiaEncoder_ns = bleadvcontroller_ns.namespace('ZhijiaEncoder') +FanLampEncoderV1 = bleadvcontroller_ns.class_('FanLampEncoderV1') +FanLampEncoderV2 = bleadvcontroller_ns.class_('FanLampEncoderV2') +ZhijiaEncoderV0 = bleadvcontroller_ns.class_('ZhijiaEncoderV0') +ZhijiaEncoderV1 = bleadvcontroller_ns.class_('ZhijiaEncoderV1') +ZhijiaEncoderV2 = bleadvcontroller_ns.class_('ZhijiaEncoderV2') -FanLampVariant = bleadvcontroller_ns.enum("FanLampVariant") -CONTROLLER_FANLAMP_VARIANTS = { - "v3": FanLampVariant.VARIANT_3, - "v2": FanLampVariant.VARIANT_2, - "v1a": FanLampVariant.VARIANT_1A, - "v1b": FanLampVariant.VARIANT_1B, +BLE_ADV_ENCODERS = { + "fanlamp_pro" :{ + "variants": { + "v1": { + "class": FanLampEncoderV1, + "args": [ 0x83, False ], + "max_forced_id": 0xFFFFFF, + "ble_param": [ 0x19, 0x03 ], + "header": [0x77, 0xF8], + }, + "v2": { + "class": FanLampEncoderV2, + "args": [ [0x10, 0x80, 0x00], 0x0400, False ], + "ble_param": [ 0x19, 0x03 ], + "header": [0xF0, 0x08], + }, + "v3": { + "class": FanLampEncoderV2, + "args": [ [0x20, 0x80, 0x00], 0x0400, True ], + "ble_param": [ 0x19, 0x03 ], + "header": [0xF0, 0x08], + }, + "v1a": { + "legacy": True, + "msg": "please use 'other - v1a' for exact replacement, or 'fanlamp_pro' v1 / v2 / v3 if effectively using FanLamp Pro app", + }, + "v1b": { + "legacy": True, + "msg": "please use 'other - v1b' for exact replacement, or 'fanlamp_pro' v1 / v2 / v3 if effectively using FanLamp Pro app", + }, + }, + "default_variant": "v3", + "default_forced_id": 0, + }, + "lampsmart_pro": { + "variants": { + "v1": { + "class": FanLampEncoderV1, + "args": [ 0x81 ], + "max_forced_id": 0xFFFFFF, + "ble_param": [ 0x19, 0x03 ], + "header": [0x77, 0xF8], + }, + "v3": { + "class": FanLampEncoderV2, + "args": [ [0x30, 0x80, 0x00], 0x0100, True ], + "ble_param": [ 0x19, 0x03 ], + "header": [0xF0, 0x08], + }, + "v1a": { + "legacy": True, + "msg": "please use 'other - v1a' for exact replacement, or 'lampsmart_pro' v1 / v3 if effectively using LampSmart Pro app", + }, + "v1b": { + "legacy": True, + "msg": "please use 'other - v1b' for exact replacement, or 'lampsmart_pro' v1 / v3 if effectively using LampSmart Pro app", + }, + "v2": { + "legacy": True, + "msg": "please use 'lampsmart_pro - v3': exact replacement", + }, + }, + "default_variant": "v3", + "default_forced_id": 0, + }, + "zhijia": { + "variants": { + "v0": { + "class": ZhijiaEncoderV0, + "args": [], + "max_forced_id": 0xFFFF, + "ble_param": [ 0x1A, 0xFF ], + "header": [ 0xF9, 0x08, 0x49 ], + }, + "v1": { + "class": ZhijiaEncoderV1, + "args": [], + "max_forced_id": 0xFFFFFF, + "ble_param": [ 0x1A, 0xFF ], + "header": [ 0xF9, 0x08, 0x49 ], + }, + "v2": { + "class": ZhijiaEncoderV2, + "args": [], + "max_forced_id": 0xFFFFFF, + "ble_param": [ 0x1A, 0xFF ], + "header": [ 0x22, 0x9D ], + }, + }, + "default_variant": "v2", + "default_forced_id": 0xC630B8, + }, + "remote" : { + "variants": { + "v1": { + "class": FanLampEncoderV1, + "args": [ 0x83, False, True ], + "max_forced_id": 0xFFFFFF, + "ble_param": [ 0x00, 0xFF ], + "header":[0x56, 0x55, 0x18, 0x87, 0x52], + }, + "v3": { + "class": FanLampEncoderV2, + "args": [ [0x10, 0x00, 0x56], 0x0400, True ], + "ble_param": [ 0x02, 0x16 ], + "header": [0xF0, 0x08], + }, + }, + "default_variant": "v3", + "default_forced_id": 0, + }, +# legacy lampsmart_pro variants v1a / v1b / v2 / v3 +# None of them are actually matching what FanLamp Pro / LampSmart Pro apps are generating +# Maybe generated by some remotes, kept here for backward compatibility, with some raw sample + "other" : { + "variants": { + "v1b": { + "class": FanLampEncoderV1, + "args": [ 0x81, True, True, 0x55 ], + "max_forced_id": 0xFFFFFF, + "ble_param": [ 0x02, 0x16 ], + "header": [0xF9, 0x08], + # 02.01.02.1B.03.F9.08.49.13.F0.69.25.4E.31.51.BA.32.08.0A.24.CB.3B.7C.71.DC.8B.B8.97.08.D0.4C (31) + }, + "v1a": { + "class": FanLampEncoderV1, + "args": [ 0x81, True, True ], + "max_forced_id": 0xFFFFFF, + "ble_param": [ 0x02, 0x03 ], + "header": [0x77, 0xF8], + # 02.01.02.1B.03.77.F8.B6.5F.2B.5E.00.FC.31.51.50.CB.92.08.24.CB.BB.FC.14.C6.9E.B0.E9.EA.73.A4 (31) + }, + "v2": { + "class": FanLampEncoderV2, + "args": [ [0x10, 0x80, 0x00], 0x0100, False ], + "ble_param": [ 0x19, 0x16 ], + "header": [0xF0, 0x08], + # 02.01.02.1B.16.F0.08.10.80.0B.9B.DA.CF.BE.B3.DD.56.3B.E9.1C.FC.27.A9.3A.A5.38.2D.3F.D4.6A.50 (31) + }, + "v3": { + "class": FanLampEncoderV2, + "args": [ [0x10, 0x80, 0x00], 0x0100, True ], + "ble_param": [ 0x19, 0x16 ], + "header": [0xF0, 0x08], + # 02.01.02.1B.16.F0.08.10.80.33.BC.2E.B0.49.EA.58.76.C0.1D.99.5E.9C.D6.B8.0E.6E.14.2B.A5.30.A9 (31) + }, + }, + "default_variant": "v1b", + "default_forced_id": 0, + }, } -ZhijiaVariant = bleadvcontroller_ns.enum("ZhijiaVariant") -CONTROLLER_ZHIJIA_VARIANTS = { - "v0": ZhijiaVariant.VARIANT_V0, - "v1": ZhijiaVariant.VARIANT_V1, - "v2": ZhijiaVariant.VARIANT_V2, -} - -def validate_fanlamp_encoding(value): - accepted_encoding = ["fanlamp_pro", "lampsmart_pro"] - if value in accepted_encoding: - return "fanlamp_pro" - raise cv.Invalid("Invalid encoding: %s. accepted values: %s" % (value, accepted_encoding)) - ENTITY_BASE_CONFIG_SCHEMA = cv.Schema( { cv.Required(CONF_BLE_ADV_CONTROLLER_ID): cv.use_id(BleAdvController), @@ -72,26 +204,40 @@ def validate_fanlamp_encoding(value): cv.Optional(CONF_BLE_ADV_SEQ_DURATION, default=100): cv.All(cv.positive_int, cv.Range(min=0, max=150)), cv.Optional(CONF_REVERSED, default=False): cv.boolean, cv.Optional(CONF_BLE_ADV_SHOW_CONFIG, default=True): cv.boolean, + cv.Optional(CONF_INDEX, default=0): cv.All(cv.positive_int, cv.Range(min=0, max=255)), } ) +def validate_legacy_variant(config): + encoding = config[CONF_BLE_ADV_ENCODING] + variant = config[CONF_VARIANT] + pv = BLE_ADV_ENCODERS[ encoding ]["variants"][ variant ] + if pv.get("legacy", False): + raise cv.Invalid("DEPRECATED '%s - %s', %s" % (encoding, variant, pv["msg"])) + return config + +def validate_forced_id(config): + encoding = config[CONF_BLE_ADV_ENCODING] + variant = config[CONF_VARIANT] + forced_id = config[CONF_BLE_ADV_FORCED_ID] + params = BLE_ADV_ENCODERS[ encoding ] + max_forced_id = params["variants"][ variant ].get("max_forced_id", 0xFFFFFFFF) + if forced_id > max_forced_id : + raise cv.Invalid("Invalid 'forced_id' for %s - %s: %s. Maximum: 0x%X." % (encoding, variant, forced_id, max_forced_id)) + return config + CONFIG_SCHEMA = cv.All( cv.Any( - CONTROLLER_BASE_CONFIG.extend( - { - cv.Required(CONF_BLE_ADV_ENCODING): validate_fanlamp_encoding, - cv.Optional(CONF_VARIANT, default="v3"): cv.enum(CONTROLLER_FANLAMP_VARIANTS, lower=True), - cv.Optional(CONF_BLE_ADV_FORCED_ID, default=0): cv.hex_uint32_t, - } - ), - CONTROLLER_BASE_CONFIG.extend( + *[ CONTROLLER_BASE_CONFIG.extend( { - cv.Required(CONF_BLE_ADV_ENCODING): cv.one_of("zhijia"), - cv.Optional(CONF_VARIANT, default="v2"): cv.enum(CONTROLLER_ZHIJIA_VARIANTS, lower=True), - cv.Optional(CONF_BLE_ADV_FORCED_ID, default=0xC630B8): cv.All(cv.hex_uint32_t, cv.Range(min=0, max=0xFFFFFF)), + cv.Required(CONF_BLE_ADV_ENCODING): cv.one_of(encoding), + cv.Optional(CONF_VARIANT, default=params["default_variant"]): cv.one_of(*params["variants"].keys()), + cv.Optional(CONF_BLE_ADV_FORCED_ID, default=params["default_forced_id"]): cv.hex_uint32_t, } - ), + ) for encoding, params in BLE_ADV_ENCODERS.items() ] ), + validate_forced_id, + validate_legacy_variant, cv.only_on([PLATFORM_ESP32]), ) @@ -109,8 +255,14 @@ def get(cls): cls.handler = cg.new_Pvariable(hdl_id) cg.add(cls.handler.set_component_source("ble_adv_handler")) cg.add(cg.App.register_component(cls.handler)) - cg.add(ZhijiaEncoder_ns.register_encoders(cls.handler, "zhijia")) - cg.add(FanLampEncoder_ns.register_encoders(cls.handler, "fanlamp_pro")) + for encoding, params in BLE_ADV_ENCODERS.items(): + for variant, param_variant in params["variants"].items(): + if "class" in param_variant: + enc_id = ID("enc_%s_%s" % (encoding, variant), type=param_variant["class"]) + enc = cg.new_Pvariable(enc_id, encoding, variant, *param_variant["args"]) + cg.add(enc.set_ble_param(*param_variant["ble_param"])) + cg.add(enc.set_header(param_variant["header"])) + cg.add(cls.handler.add_encoder(enc)) return cls.handler async def to_code(config): @@ -121,7 +273,7 @@ async def to_code(config): await setup_entity(var, config) cg.add(var.set_handler(hdl)) cg.add(var.set_encoding_and_variant(config[CONF_BLE_ADV_ENCODING], config[CONF_VARIANT])) - cg.add(var.set_min_tx_duration(config[CONF_DURATION])) + cg.add(var.set_min_tx_duration(config[CONF_DURATION], 100, 500, 10)) cg.add(var.set_max_tx_duration(config[CONF_BLE_ADV_MAX_DURATION])) cg.add(var.set_seq_duration(config[CONF_BLE_ADV_SEQ_DURATION])) cg.add(var.set_reversed(config[CONF_REVERSED])) @@ -130,9 +282,5 @@ async def to_code(config): else: cg.add(var.set_forced_id(config[CONF_ID].id)) cg.add(var.set_show_config(config[CONF_BLE_ADV_SHOW_CONFIG])) - if config[CONF_BLE_ADV_SHOW_CONFIG]: - cg.add(cg.App.register_select(var.get_select_encoding())) - cg.add(cg.App.register_number(var.get_number_duration())) - diff --git a/components/ble_adv_controller/ble_adv_controller.cpp b/components/ble_adv_controller/ble_adv_controller.cpp index 0d690f0..0d1f497 100644 --- a/components/ble_adv_controller/ble_adv_controller.cpp +++ b/components/ble_adv_controller/ble_adv_controller.cpp @@ -1,29 +1,63 @@ #include "ble_adv_controller.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" namespace esphome { namespace bleadvcontroller { static const char *TAG = "ble_adv_controller"; -void BleAdvSelect::set_id(const char * name, const StringRef & parent_name) { - // Due to the use of sh... StringRef, we are forced to keep a ref on the built string... - this->ref_name_ = std::string(parent_name) + " - " + std::string(name); - this->set_object_id(this->ref_name_.c_str()); - this->set_name(this->ref_name_.c_str()); +void BleAdvSelect::control(const std::string &value) { + this->publish_state(value); + uint32_t hash_value = fnv1_hash(value); + this->rtc_.save(&hash_value); } -void BleAdvNumber::set_id(const char * name, const StringRef & parent_name) { - // Due to the use of sh... StringRef, we are forced to keep a ref on the built string... - this->ref_name_ = std::string(parent_name) + " - " + std::string(name); - this->set_object_id(this->ref_name_.c_str()); - this->set_name(this->ref_name_.c_str()); +void BleAdvSelect::sub_init() { + App.register_select(this); + this->rtc_ = global_preferences->make_preference< uint32_t >(this->get_object_id_hash()); + uint32_t restored; + if (this->rtc_.load(&restored)) { + for (auto & opt: this->traits.get_options()) { + if(fnv1_hash(opt) == restored) { + this->state = opt; + return; + } + } + } +} + +void BleAdvNumber::control(float value) { + this->publish_state(value); + this->rtc_.save(&value); } -void BleAdvController::set_encoding_and_variant(const std::string & encoding, uint8_t variant) { +void BleAdvNumber::sub_init() { + App.register_number(this); + this->rtc_ = global_preferences->make_preference< float >(this->get_object_id_hash()); + float restored; + if (this->rtc_.load(&restored)) { + this->state = restored; + } +} + +void BleAdvController::set_encoding_and_variant(const std::string & encoding, const std::string & variant) { this->select_encoding_.traits.set_options(this->handler_->get_ids(encoding)); - this->select_encoding_.state = this->handler_->get_encoder(encoding, variant).get_id(); + this->cur_encoder_ = this->handler_->get_encoder(encoding, variant); + this->select_encoding_.state = this->cur_encoder_->get_id(); + this->select_encoding_.add_on_state_callback(std::bind(&BleAdvController::refresh_encoder, this, std::placeholders::_1, std::placeholders::_2)); +} + +void BleAdvController::refresh_encoder(std::string id, size_t index) { + this->cur_encoder_ = this->handler_->get_encoder(id); +} + +void BleAdvController::set_min_tx_duration(int tx_duration, int min, int max, int step) { + this->number_duration_.traits.set_min_value(min); + this->number_duration_.traits.set_max_value(max); + this->number_duration_.traits.set_step(step); + this->number_duration_.state = tx_duration; } void BleAdvController::setup() { @@ -31,26 +65,18 @@ void BleAdvController::setup() { register_service(&BleAdvController::on_pair, "pair_" + this->get_object_id()); register_service(&BleAdvController::on_unpair, "unpair_" + this->get_object_id()); register_service(&BleAdvController::on_cmd, "cmd_" + this->get_object_id(), {"cmd", "arg0", "arg1", "arg2", "arg3"}); + register_service(&BleAdvController::on_raw_inject, "inject_raw_" + this->get_object_id(), {"raw"}); #endif - if (this->show_config_) { - // init select for encoding - this->select_encoding_.set_id("Encoding", this->get_name()); - this->select_encoding_.set_entity_category(EntityCategory::ENTITY_CATEGORY_CONFIG); - this->select_encoding_.publish_state(this->select_encoding_.state); - - // init number for duration - this->number_duration_.set_id("Duration", this->get_name()); - this->number_duration_.set_entity_category(EntityCategory::ENTITY_CATEGORY_CONFIG); - this->number_duration_.traits.set_min_value(100); - this->number_duration_.traits.set_max_value(500); - this->number_duration_.traits.set_step(10); - this->number_duration_.publish_state(this->number_duration_.state); + if (this->is_show_config()) { + this->select_encoding_.init("Encoding", this->get_name()); + this->number_duration_.init("Duration", this->get_name()); } } void BleAdvController::dump_config() { ESP_LOGCONFIG(TAG, "BleAdvController '%s'", this->get_object_id().c_str()); - ESP_LOGCONFIG(TAG, " Hash ID '%X'", this->forced_id_); + ESP_LOGCONFIG(TAG, " Hash ID '%X'", this->params_.id_); + ESP_LOGCONFIG(TAG, " Index '%d'", this->params_.index_); ESP_LOGCONFIG(TAG, " Transmission Min Duration: %d ms", this->get_min_tx_duration()); ESP_LOGCONFIG(TAG, " Transmission Max Duration: %d ms", this->max_tx_duration_); ESP_LOGCONFIG(TAG, " Transmission Sequencing Duration: %d ms", this->seq_duration_); @@ -70,30 +96,34 @@ void BleAdvController::on_unpair() { void BleAdvController::on_cmd(float cmd_type, float arg0, float arg1, float arg2, float arg3) { Command cmd(CommandType::CUSTOM); - cmd.args_[0] = (uint8_t)cmd_type; - cmd.args_[1] = (uint8_t)arg0; - cmd.args_[2] = (uint8_t)arg1; - cmd.args_[3] = (uint8_t)arg2; - cmd.args_[4] = (uint8_t)arg3; + cmd.cmd_ = (uint8_t)cmd_type; + cmd.args_[0] = (uint8_t)arg0; + cmd.args_[1] = (uint8_t)arg1; + cmd.args_[2] = (uint8_t)arg2; + cmd.args_[3] = (uint8_t)arg3; this->enqueue(cmd); } + +void BleAdvController::on_raw_inject(std::string raw) { + this->commands_.emplace_back(CommandType::CUSTOM); + this->commands_.back().params_.emplace_back(); + this->commands_.back().params_.back().from_hex_string(raw); +} #endif bool BleAdvController::enqueue(Command &cmd) { - // get the encoder from the currently selected one - BleAdvEncoder & encoder = this->get_encoder(); - - if (!encoder.is_supported(cmd)) { - ESP_LOGW(TAG, "Unsupported command received: %d. Aborted.", cmd.cmd_); + if (!this->cur_encoder_->is_supported(cmd)) { + ESP_LOGW(TAG, "Unsupported command received: %d. Aborted.", cmd.main_cmd_); return false; } - cmd.tx_count_ = this->tx_count_; - cmd.id_ = this->forced_id_; + // Reset tx count if near the limit + if (this->params_.tx_count_ > 120) { + this->params_.tx_count_ = 0; + } // Remove any previous command of the same type in the queue, if not used for several purposes - if (cmd.cmd_ != CommandType::FAN_ONOFF_SPEED // Used for ON / OFF / SPEED - && cmd.cmd_ != CommandType::CUSTOM) { + if (cmd.main_cmd_ != CommandType::CUSTOM) { uint8_t nb_rm = std::count_if(this->commands_.begin(), this->commands_.end(), [&](QueueItem& q){ return q.cmd_type_ == cmd.cmd_; }); if (nb_rm) { ESP_LOGD(TAG, "Removing %d previous pending commands", nb_rm); @@ -102,12 +132,9 @@ bool BleAdvController::enqueue(Command &cmd) { } // enqueue the new command and encode the buffer(s) - this->commands_.emplace_back(cmd.cmd_); - this->tx_count_ += encoder.get_adv_data(this->commands_.back().params_, cmd); - if (this->tx_count_ > 127) { - this->tx_count_ = 1; - } - + this->commands_.emplace_back(cmd.main_cmd_); + this->cur_encoder_->encode(this->commands_.back().params_, cmd, this->params_); + // setup seq duration for each packet bool use_seq_duration = (this->seq_duration_ > 0) && (this->seq_duration_ < this->get_min_tx_duration()); for (auto & param : this->commands_.back().params_) { @@ -123,10 +150,6 @@ void BleAdvController::loop() { // no on going command advertised by this controller, check if any to advertise if(!this->commands_.empty()) { QueueItem & item = this->commands_.front(); - for (auto & param : item.params_) { - ESP_LOGD(TAG, "%s - request start advertising: %s", this->get_object_id().c_str(), - esphome::format_hex_pretty(param.buf_, param.len_).c_str()); - } this->adv_id_ = this->handler_->add_to_advertiser(item.params_); this->adv_start_time_ = now; this->commands_.pop_front(); @@ -137,7 +160,6 @@ void BleAdvController::loop() { uint32_t duration = this->commands_.empty() ? this->max_tx_duration_ : this->number_duration_.state; if (now > this->adv_start_time_ + duration) { this->adv_start_time_ = 0; - ESP_LOGD(TAG, "%s - request stop advertising", this->get_object_id().c_str()); this->handler_->remove_from_advertiser(this->adv_id_); } } @@ -149,7 +171,7 @@ void BleAdvEntity::dump_config_base(const char * tag) { void BleAdvEntity::command(CommandType cmd_type, const std::vector &args) { Command cmd(cmd_type); - std::copy(args.begin(), args.end(), cmd.args_.begin()); + std::copy(args.begin(), args.end(), cmd.args_); this->get_parent()->enqueue(cmd); } diff --git a/components/ble_adv_controller/ble_adv_controller.h b/components/ble_adv_controller/ble_adv_controller.h index 0f3c62a..d0a2114 100644 --- a/components/ble_adv_controller/ble_adv_controller.h +++ b/components/ble_adv_controller/ble_adv_controller.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" #ifdef USE_API #include "esphome/components/api/custom_api_device.h" #endif @@ -15,30 +16,46 @@ namespace esphome { namespace bleadvcontroller { -/** - BleAdvSelect: basic implementation of 'Select' to handle configuration choice from HA directly - */ -class BleAdvSelect : public select::Select + +// Base class to define a dynamic Configuration +template < class BaseEntity > +class BleAdvDynConfig: public BaseEntity { public: - void control(const std::string &value) override { this->publish_state(value); } - void set_id(const char * name, const StringRef & parent_name); + void init(const char * name, const StringRef & parent_name) { + // Due to the use of sh... StringRef, we are forced to keep a ref on the built string... + this->ref_name_ = std::string(parent_name) + " - " + std::string(name); + this->set_object_id(this->ref_name_.c_str()); + this->set_name(this->ref_name_.c_str()); + this->set_entity_category(EntityCategory::ENTITY_CATEGORY_CONFIG); + this->sub_init(); + this->publish_state(this->state); + } + + // register to App and restore from config / saved data + virtual void sub_init() = 0; protected: std::string ref_name_; + ESPPreferenceObject rtc_{nullptr}; }; /** - BleAdvNumber: basic implementation of 'Number' to handle duration(s) choice from HA directly + BleAdvSelect: basic implementation of 'Select' to handle configuration choice from HA directly */ -class BleAdvNumber : public number::Number -{ -public: - void control(float value) override { this->publish_state(value); } - void set_id(const char * name, const StringRef & parent_name); +class BleAdvSelect: public BleAdvDynConfig < select::Select > { +protected: + void control(const std::string &value) override; + void sub_init() override; +}; +/** + BleAdvNumber: basic implementation of 'Number' to handle duration(s) choice from HA directly + */ +class BleAdvNumber: public BleAdvDynConfig < number::Number > { protected: - std::string ref_name_; + void control(float value) override; + void sub_init() override; }; /** @@ -58,28 +75,29 @@ class BleAdvController : public Component, public EntityBase void loop() override; virtual void dump_config() override; - void set_min_tx_duration(uint32_t tx_duration) { this->number_duration_.state = tx_duration; } + void set_min_tx_duration(int tx_duration, int min, int max, int step); uint32_t get_min_tx_duration() { return (uint32_t)this->number_duration_.state; } void set_max_tx_duration(uint32_t tx_duration) { this->max_tx_duration_ = tx_duration; } void set_seq_duration(uint32_t seq_duration) { this->seq_duration_ = seq_duration; } - void set_forced_id(uint32_t forced_id) { this->forced_id_ = forced_id; } - void set_forced_id(const std::string & str_id) { this->forced_id_ = fnv1_hash(str_id); } - void set_encoding_and_variant(const std::string & encoding, uint8_t variant); - select::Select * get_select_encoding() { return &(this->select_encoding_); } - number::Number * get_number_duration() { return &(this->number_duration_); } + void set_forced_id(uint32_t forced_id) { this->params_.id_ = forced_id; } + void set_forced_id(const std::string & str_id) { this->params_.id_ = fnv1_hash(str_id); } + void set_index(uint8_t index) { this->params_.index_ = index; } + void set_encoding_and_variant(const std::string & encoding, const std::string & variant); void set_reversed(bool reversed) { this->reversed_ = reversed; } bool is_reversed() const { return this->reversed_; } - bool is_supported(const Command &cmd) { return this->get_encoder().is_supported(cmd); } + bool is_supported(const Command &cmd) { return this->cur_encoder_->is_supported(cmd); } void set_show_config(bool show_config) { this->show_config_ = show_config; } + bool is_show_config() { return this->show_config_; } void set_handler(BleAdvHandler * handler) { this->handler_ = handler; } - BleAdvEncoder & get_encoder() { return this->handler_->get_encoder(this->select_encoding_.state); } + void refresh_encoder(std::string id, size_t index); #ifdef USE_API // Services void on_pair(); void on_unpair(); void on_cmd(float cmd, float arg0, float arg1, float arg2, float arg3); + void on_raw_inject(std::string raw); #endif bool enqueue(Command &cmd); @@ -89,11 +107,13 @@ class BleAdvController : public Component, public EntityBase uint32_t max_tx_duration_ = 3000; uint32_t seq_duration_ = 150; - uint32_t forced_id_ = 0; + ControllerParam_t params_; + bool reversed_; bool show_config_{false}; BleAdvSelect select_encoding_; + BleAdvEncoder * cur_encoder_{nullptr}; BleAdvNumber number_duration_; BleAdvHandler * handler_{nullptr}; @@ -110,7 +130,6 @@ class BleAdvController : public Component, public EntityBase std::list< QueueItem > commands_; // Being advertised data properties - uint8_t tx_count_ = 1; uint32_t adv_start_time_ = 0; uint16_t adv_id_ = 0; }; diff --git a/components/ble_adv_controller/ble_adv_handler.cpp b/components/ble_adv_controller/ble_adv_handler.cpp index 413f6b4..0c47fe5 100644 --- a/components/ble_adv_controller/ble_adv_handler.cpp +++ b/components/ble_adv_controller/ble_adv_handler.cpp @@ -2,45 +2,192 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" +#ifdef USE_ESP32_BLE_CLIENT +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#endif + namespace esphome { namespace bleadvcontroller { static const char *TAG = "ble_adv_handler"; -uint8_t BleAdvMultiEncoder::get_adv_data(std::vector< BleAdvParam > & params, Command &cmd) { +void BleAdvParam::from_raw(const uint8_t * buf, size_t len) { + // Copy the raw data as is, limiting to the max size of the buffer + this->len_ = std::min(MAX_PACKET_LEN, len); + std::copy(buf, buf + this->len_, this->buf_); + + // find the data / flag indexes in the buffer + size_t cur_len = 0; + while (cur_len < this->len_ - 2) { + size_t sub_len = this->buf_[cur_len]; + uint8_t type = this->buf_[cur_len + 1]; + if (type == ESP_BLE_AD_TYPE_FLAG) { + this->ad_flag_index_ = cur_len; + } + if ((type == ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE) + || (type == ESP_BLE_AD_TYPE_16SRV_CMPL) + || (type == ESP_BLE_AD_TYPE_SERVICE_DATA)){ + this->data_index_ = cur_len; + } + cur_len += (sub_len + 1); + } +} + +void BleAdvParam::from_hex_string(std::string & raw) { + // Clean-up input string + raw = raw.substr(0, raw.find('(')); + raw.erase(std::remove_if(raw.begin(), raw.end(), [&](char & c) { return c == '.' || c == ' '; }), raw.end()); + if (raw.substr(0,2) == "0x") { + raw = raw.substr(2); + } + + // convert to integers + uint8_t raw_int[MAX_PACKET_LEN]{0}; + uint8_t len = std::min(MAX_PACKET_LEN, raw.size()/2); + for (uint8_t i; i < len; ++i) { + raw_int[i] = stoi(raw.substr(2*i, 2), 0, 16); + } + this->from_raw(raw_int, len); +} + +void BleAdvParam::init_with_ble_param(uint8_t ad_flag, uint8_t data_type) { + if (ad_flag != 0x00) { + this->ad_flag_index_ = 0; + this->buf_[0] = 2; + this->buf_[1] = ESP_BLE_AD_TYPE_FLAG; + this->buf_[2] = ad_flag; + this->data_index_ = 3; + this->buf_[4] = data_type; + } else { + this->data_index_ = 0; + this->buf_[1] = data_type; + } +} + +void BleAdvParam::set_data_len(size_t len) { + this->buf_[this->data_index_] = len + 1; + this->len_ = len + 2 + (this->has_ad_flag() ? 3 : 0); +} + +bool BleAdvEncoder::is_supported(const Command &cmd) { + ControllerParam_t cont; + auto cmds = this->translate(cmd, cont); + return !cmds.empty(); +} + +bool BleAdvEncoder::decode(const BleAdvParam & param, Command &cmd, ControllerParam_t & cont) { + // Check global len and header to discard most of encoders + size_t len = param.get_data_len() - this->header_.size(); + const uint8_t * cbuf = param.get_const_data_buf(); + if (len != this->len_) return false; + if (!std::equal(this->header_.begin(), this->header_.end(), cbuf)) return false; + + // copy the data to be decoded, not to alter it for other decoders + uint8_t buf[MAX_PACKET_LEN]{0}; + std::copy(cbuf, cbuf + param.get_data_len(), buf); + return this->decode(buf + this->header_.size(), cmd, cont); +} + +void BleAdvEncoder::encode(std::vector< BleAdvParam > & params, Command &cmd, ControllerParam_t & cont) { + auto cmds = (cmd.main_cmd_ == CommandType::CUSTOM) ? std::vector< Command >({cmd}) : this->translate(cmd, cont); + for (auto & acmd: cmds) { + cont.tx_count_++; + + params.emplace_back(); + BleAdvParam & param = params.back(); + param.init_with_ble_param(this->ad_flag_, this->adv_data_type_); + std::copy(this->header_.begin(), this->header_.end(), param.get_data_buf()); + uint8_t * buf = param.get_data_buf() + this->header_.size(); + + ESP_LOGD(this->id_.c_str(), "UUID: '0x%X', index: %d, tx: %d, cmd: '0x%02X', args: [%d,%d,%d,%d]", + cont.id_, cont.index_, cont.tx_count_, cmd.cmd_, cmd.args_[0], cmd.args_[1], cmd.args_[2], cmd.args_[3]); + + this->encode(buf, acmd, cont); + param.set_data_len(this->len_ + this->header_.size()); + } +} + +void BleAdvEncoder::whiten(uint8_t *buf, size_t len, uint8_t seed) { + uint8_t r = seed; + for (size_t i=0; i < len; i++) { + uint8_t b = 0; + for (size_t j=0; j < 8; j++) { + r <<= 1; + if (r & 0x80) { + r ^= 0x11; + b |= 1 << j; + } + r &= 0x7F; + } + buf[i] ^= b; + } +} + +void BleAdvEncoder::reverse_all(uint8_t* buf, uint8_t len) { + for (size_t i = 0; i < len; ++i) { + uint8_t & x = buf[i]; + x = ((x & 0x55) << 1) | ((x & 0xAA) >> 1); + x = ((x & 0x33) << 2) | ((x & 0xCC) >> 2); + x = ((x & 0x0F) << 4) | ((x & 0xF0) >> 4); + } +} + +void BleAdvMultiEncoder::encode(std::vector< BleAdvParam > & params, Command &cmd, ControllerParam_t & cont) { uint8_t count = 0; - for(auto & encoder : this->encoder_ids_) { - count = std::max(encoder->get_adv_data(params, cmd), count); + for(auto & encoder : this->encoders_) { + ControllerParam_t c_cont = cont; // Copy to avoid increasing counts for the same command + encoder->encode(params, cmd, c_cont); + count = std::max(c_cont.tx_count_, count); } - return count; + cont.tx_count_ = count; } bool BleAdvMultiEncoder::is_supported(const Command &cmd) { bool is_supported = false; - for(auto & encoder : this->encoder_ids_) { + for(auto & encoder : this->encoders_) { is_supported |= encoder->is_supported(cmd); } return is_supported; } -BleAdvEncoder & BleAdvHandler::get_encoder(const std::string & id) { +void BleAdvHandler::setup() { +#ifdef USE_API + register_service(&BleAdvHandler::on_raw_decode, "raw_decode", {"raw"}); +#endif +} + +void BleAdvHandler::add_encoder(BleAdvEncoder * encoder) { + BleAdvMultiEncoder * enc_all = nullptr; + auto all_enc = std::find_if(this->encoders_.begin(), this->encoders_.end(), + [&](BleAdvEncoder * p){ return p->is_id(encoder->get_encoding(), "All"); }); + if (all_enc == this->encoders_.end()) { + enc_all = new BleAdvMultiEncoder(encoder->get_encoding()); + this->encoders_.push_back(enc_all); + } else { + enc_all = static_cast(*all_enc); + } + this->encoders_.push_back(encoder); + enc_all->add_encoder(encoder); +} + +BleAdvEncoder * BleAdvHandler::get_encoder(const std::string & id) { for(auto & encoder : this->encoders_) { if (encoder->is_id(id)) { - return *encoder; + return encoder; } } ESP_LOGE(TAG, "No Encoder with id: %s", id.c_str()); - return **(this->encoders_.begin()); + return nullptr; } -BleAdvEncoder & BleAdvHandler::get_encoder(const std::string & encoding, int variant) { +BleAdvEncoder * BleAdvHandler::get_encoder(const std::string & encoding, const std::string & variant) { for(auto & encoder : this->encoders_) { if (encoder->is_id(encoding, variant)) { - return *encoder; + return encoder; } } - ESP_LOGE(TAG, "No Encoder with encoding: %s and variant: %d", encoding.c_str(), variant); - return **(this->encoders_.begin()); + ESP_LOGE(TAG, "No Encoder with encoding: %s and variant: %s", encoding.c_str(), variant.c_str()); + return nullptr; } std::vector BleAdvHandler::get_ids(const std::string & encoding) { @@ -57,12 +204,15 @@ uint16_t BleAdvHandler::add_to_advertiser(std::vector< BleAdvParam > & params) { uint32_t msg_id = ++this->id_count; for (auto & param : params) { this->packets_.emplace_back(BleAdvProcess(msg_id, std::move(param))); + ESP_LOGD(TAG, "request start advertising - %d: %s", msg_id, + esphome::format_hex_pretty(param.get_full_buf(), param.get_full_len()).c_str()); } params.clear(); // As we moved the content, just to be sure no caller will re use it return this->id_count; } void BleAdvHandler::remove_from_advertiser(uint16_t msg_id) { + ESP_LOGD(TAG, "request stop advertising - %d", msg_id); for (auto & param : this->packets_) { if (param.id_ == msg_id) { param.to_be_removed_ = true; @@ -70,6 +220,88 @@ void BleAdvHandler::remove_from_advertiser(uint16_t msg_id) { } } +// try to identify the relevant encoder +bool BleAdvHandler::identify_param(const BleAdvParam & param, bool ignore_ble_param) { + for(auto & encoder : this->encoders_) { + if (!ignore_ble_param && !encoder->is_ble_param(param.get_ad_flag(), param.get_data_type())) { + continue; + } + ControllerParam_t cont; + Command cmd(CommandType::CUSTOM); + if(encoder->decode(param, cmd, cont)) { + ESP_LOGI(encoder->get_id().c_str(), "Decoded OK - tx: %d, cmd: '0x%02X', Args: [%d,%d,%d,%d]", + cont.tx_count_, cmd.cmd_, cmd.args_[0], cmd.args_[1], cmd.args_[2], cmd.args_[3]); + + std::string config_str = "config: \nble_adv_controller:"; + config_str += "\n - id: my_controller_id"; + config_str += "\n encoding: %s"; + config_str += "\n variant: %s"; + config_str += "\n forced_id: 0x%X"; + if (cont.index_ != 0) { + config_str += "\n index: %d"; + } + ESP_LOGI(TAG, config_str.c_str(), encoder->get_encoding().c_str(), encoder->get_variant().c_str(), cont.id_, cont.index_); + + // Re encoding with the same parameters to check if it gives the same output + std::vector< BleAdvParam > params; + cont.tx_count_--; // as the encoder will increase it automatically + if(cmd.cmd_ == 0x28) { + // Force recomputation of Args by translate function for PAIR command, as part of encoding + cmd.main_cmd_ = CommandType::PAIR; + } + encoder->encode(params, cmd, cont); + BleAdvParam & fparam = params.back(); + ESP_LOGD(TAG, "enc - %s", esphome::format_hex_pretty(fparam.get_full_buf(), fparam.get_full_len()).c_str()); + bool nodiff = std::equal(param.get_const_data_buf(), param.get_const_data_buf() + param.get_data_len(), fparam.get_data_buf()); + nodiff ? ESP_LOGI(TAG, "Decoded / Re-encoded with NO DIFF") : ESP_LOGE(TAG, "DIFF after Decode / Re-encode"); + + return true; + } + } + return false; +} + +#ifdef USE_API +void BleAdvHandler::on_raw_decode(std::string raw) { + BleAdvParam param; + param.from_hex_string(raw); + ESP_LOGD(TAG, "raw - %s", esphome::format_hex_pretty(param.get_full_buf(), param.get_full_len()).c_str()); + this->identify_param(param, true); +} +#endif + +#ifdef USE_ESP32_BLE_CLIENT +/* Basic class inheriting esp32_ble_tracker::ESPBTDevice in order to access + protected attribute 'scan_result_' containing raw advertisement +*/ +class HackESPBTDevice: public esp32_ble_tracker::ESPBTDevice { +public: + void get_raw_packet(BleAdvParam & param) const { + param.from_raw(this->scan_result_.ble_adv, this->scan_result_.adv_data_len); + } +}; + +void BleAdvHandler::capture(const esp32_ble_tracker::ESPBTDevice & device, bool ignore_ble_param, uint16_t rem_time) { + // Clean-up expired packets + this->listen_packets_.remove_if( [&](BleAdvParam & p){ return p.duration_ < millis(); } ); + + // Read raw advertised packets + BleAdvParam param; + const HackESPBTDevice * hack_device = reinterpret_cast< const HackESPBTDevice * >(&device); + hack_device->get_raw_packet(param); + if (!param.has_data()) return; + + // Check if not already received in the last 300s + auto idx = std::find(this->listen_packets_.begin(), this->listen_packets_.end(), param); + if (idx == this->listen_packets_.end()) { + ESP_LOGD(TAG, "raw - %s", esphome::format_hex_pretty(param.get_full_buf(), param.get_full_len()).c_str()); + param.duration_ = millis() + (uint32_t)rem_time * 1000; + this->identify_param(param, ignore_ble_param); + this->listen_packets_.emplace_back(std::move(param)); + } +} +#endif + void BleAdvHandler::loop() { if (this->adv_stop_time_ == 0) { // No packet is being advertised, process with clean-up IF already processed once and requested for removal @@ -77,33 +309,19 @@ void BleAdvHandler::loop() { // if packets to be advertised, advertise the front one if (!this->packets_.empty()) { BleAdvParam & packet = this->packets_.front().param_; - //ESP_LOGD(TAG, "Effective advertising start at %d: id %d - %s", millis(), this->packets_.front().id_, - // esphome::format_hex_pretty(packet.buf_, packet.len_).c_str()); - if (packet.len_ == MAX_PACKET_LEN) { - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ble_gap_config_adv_data_raw(packet.buf_, packet.len_)); - } else { - this->adv_data_.p_manufacturer_data = packet.buf_; - this->adv_data_.manufacturer_len = packet.len_; - ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ble_gap_config_adv_data(&(this->adv_data_))); - } + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ble_gap_config_adv_data_raw(packet.get_full_buf(), packet.get_full_len())); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ble_gap_start_advertising(&(this->adv_params_))); this->adv_stop_time_ = millis() + this->packets_.front().param_.duration_; this->packets_.front().processed_once_ = true; } } else { // Packet is being advertised, check if time to switch to next one in case: - // The advertise seq duration expired AND + // The advertise seq_duration expired AND // There is more than one packet to advertise OR the front packet was requested to be removed bool multi_packets = (this->packets_.size() > 1); bool front_to_be_removed = this->packets_.front().to_be_removed_; if ((millis() > this->adv_stop_time_) && (multi_packets || front_to_be_removed)) { - //ESP_LOGD(TAG, "Effective advertising stop at %d: id %d", millis(), this->packets_.front().id_); - //ESP_LOGD(TAG, "Nb Packets: %d", this->packets_.size() ); - //ESP_LOGD(TAG, "Front to be removed: %s", front_to_be_removed ? "Y":"N" ); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ble_gap_stop_advertising()); - delay(10); // Not sure it helps, but... - this->adv_data_.p_manufacturer_data = nullptr; - this->adv_data_.manufacturer_len = 0; this->adv_stop_time_ = 0; if (front_to_be_removed) { this->packets_.pop_front(); diff --git a/components/ble_adv_controller/ble_adv_handler.h b/components/ble_adv_controller/ble_adv_handler.h index 68de911..7cc19c7 100644 --- a/components/ble_adv_controller/ble_adv_handler.h +++ b/components/ble_adv_controller/ble_adv_handler.h @@ -1,12 +1,24 @@ #pragma once +#include "esphome/core/defines.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#ifdef USE_API +#include "esphome/components/api/custom_api_device.h" +#endif + #include #include #include namespace esphome { + +#ifdef USE_ESP32_BLE_CLIENT +namespace esp32_ble_tracker { + class ESPBTDevice; +} +#endif + namespace bleadvcontroller { enum CommandType { @@ -36,29 +48,58 @@ enum CommandType { class Command { public: - Command(CommandType cmd): cmd_(cmd) {} + Command(CommandType cmd = CommandType::NOCMD): main_cmd_(cmd) {} - CommandType cmd_; - std::vector args_{0,0,0,0,0}; + CommandType main_cmd_; + uint8_t cmd_{0}; + uint8_t args_[4]{0}; +}; - // Attributes from controller +/** + Controller Parameters + */ +struct ControllerParam_t { uint32_t id_ = 0; uint8_t tx_count_ = 0; + uint8_t index_ = 0; + uint16_t seed_ = 0; }; -const size_t MAX_PACKET_LEN = 31; +static constexpr size_t MAX_PACKET_LEN = 31; class BleAdvParam { public: BleAdvParam() {}; - uint8_t buf_[MAX_PACKET_LEN]{0}; - size_t len_{MAX_PACKET_LEN}; - uint32_t duration_{100}; - - // Only move operators to avoid data copy BleAdvParam(BleAdvParam&&) = default; BleAdvParam& operator=(BleAdvParam&&) = default; + + void from_raw(const uint8_t * buf, size_t len); + void from_hex_string(std::string & raw); + void init_with_ble_param(uint8_t ad_flag, uint8_t data_type); + + bool has_ad_flag() const { return this->ad_flag_index_ != MAX_PACKET_LEN; } + uint8_t get_ad_flag() const { return this->buf_[this->ad_flag_index_ + 2]; } + + bool has_data() const { return this->data_index_ != MAX_PACKET_LEN; } + void set_data_len(size_t len); + uint8_t get_data_len() const { return this->buf_[this->data_index_] - 1; } + uint8_t get_data_type() const { return this->buf_[this->data_index_ + 1]; } + uint8_t * get_data_buf() { return this->buf_ + this->data_index_ + 2; } + const uint8_t * get_const_data_buf() const { return this->buf_ + this->data_index_ + 2; } + + uint8_t * get_full_buf() { return this->buf_; } + uint8_t get_full_len() { return this->len_; } + + bool operator==(const BleAdvParam & comp) { return std::equal(comp.buf_, comp.buf_ + MAX_PACKET_LEN, this->buf_); } + + uint32_t duration_{100}; + +protected: + uint8_t buf_[MAX_PACKET_LEN]{0}; + size_t len_{0}; + size_t ad_flag_index_{MAX_PACKET_LEN}; + size_t data_index_{MAX_PACKET_LEN}; }; class BleAdvProcess @@ -82,23 +123,49 @@ class BleAdvProcess */ class BleAdvEncoder { public: - BleAdvEncoder(const std::string & id, const std::string & encoding, int variant): - id_(id), encoding_(encoding), variant_(variant) {} + BleAdvEncoder(const std::string & encoding, const std::string & variant): + id_(encoding + " - " + variant), encoding_(encoding), variant_(variant) {} const std::string & get_id() const { return this->id_; } + const std::string & get_encoding() const { return this->encoding_; } + const std::string & get_variant() const { return this->variant_; } bool is_id(const std::string & ref_id) const { return ref_id == this->id_; } - bool is_id(const std::string & encoding, int variant) const { return (encoding == this->encoding_) && (variant == this->variant_); } + bool is_id(const std::string & encoding, const std::string & variant) const { return (encoding == this->encoding_) && (variant == this->variant_); } bool is_encoding(const std::string & encoding) const { return (encoding == this->encoding_); } - virtual uint8_t get_adv_data(std::vector< BleAdvParam > & params, Command &cmd) = 0; - virtual bool is_supported(const Command &cmd) = 0; + void set_ble_param(uint8_t ad_flag, uint8_t adv_data_type){ this->ad_flag_ = ad_flag; this->adv_data_type_ = adv_data_type; } + bool is_ble_param(uint8_t ad_flag, uint8_t adv_data_type) { return this->ad_flag_ == ad_flag && this->adv_data_type_ == adv_data_type; } + void set_header(const std::vector< uint8_t > && header) { this->header_ = header; } + + virtual std::vector< Command > translate(const Command & cmd, const ControllerParam_t & cont) = 0; + virtual void encode(std::vector< BleAdvParam > & params, Command &cmd, ControllerParam_t & cont); + virtual bool is_supported(const Command &cmd) ; + virtual bool decode(const BleAdvParam & packet, Command &cmd, ControllerParam_t & cont); protected: + virtual bool decode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) { return false; }; + virtual void encode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) { }; + + // utils for encoding + void reverse_all(uint8_t* buf, uint8_t len); + void whiten(uint8_t *buf, size_t len, uint8_t seed); + + // encoder identifiers std::string id_; std::string encoding_; - int variant_ = 0; + std::string variant_; + + // BLE parameters + uint8_t ad_flag_{0x00}; + uint8_t adv_data_type_{ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE}; + + // Common parameters + std::vector< uint8_t > header_; + size_t len_{0}; }; +#define ENSURE_EQ(param1, param2, ...) if ((param1) != (param2)) { ESP_LOGD(this->id_.c_str(), __VA_ARGS__); return false; } + /** BleAdvMultiEncoder: Encode several messages at the same time with different encoders @@ -106,13 +173,17 @@ class BleAdvEncoder { class BleAdvMultiEncoder: public BleAdvEncoder { public: - BleAdvMultiEncoder(const std::string id, const std::string encoding): BleAdvEncoder(id, encoding, -1) {} - virtual uint8_t get_adv_data(std::vector< BleAdvParam > & params, Command &cmd) override; + BleAdvMultiEncoder(const std::string encoding): BleAdvEncoder(encoding, "All") {} + virtual void encode(std::vector< BleAdvParam > & params, Command &cmd, ControllerParam_t & cont) override; virtual bool is_supported(const Command &cmd) override; - void add_encoder(BleAdvEncoder * encoder_id) { this->encoder_ids_.push_back(encoder_id); } + void add_encoder(BleAdvEncoder * encoder) { this->encoders_.push_back(encoder); } + + // Not used + virtual std::vector< Command > translate(const Command & cmd, const ControllerParam_t & cont) { return std::vector< Command >(); }; + virtual bool decode(const BleAdvParam & packet, Command &cmd, ControllerParam_t & cont) override { return false; } protected: - std::vector< BleAdvEncoder * > encoder_ids_; + std::vector< BleAdvEncoder * > encoders_; }; /** @@ -122,21 +193,38 @@ class BleAdvMultiEncoder: public BleAdvEncoder with handling of prioritization and parallel send when possible */ class BleAdvHandler: public Component +#ifdef USE_API + , public api::CustomAPIDevice +#endif { public: - // Loop handling + // component handling + void setup() override; void loop() override; // Encoder registration and access - void add_encoder(BleAdvEncoder * encoder) { this->encoders_.push_back(encoder); } - BleAdvEncoder & get_encoder(const std::string & id); - BleAdvEncoder & get_encoder(const std::string & encoding, int variant); + void add_encoder(BleAdvEncoder * encoder); + BleAdvEncoder * get_encoder(const std::string & id); + BleAdvEncoder * get_encoder(const std::string & encoding, const std::string & variant); std::vector get_ids(const std::string & encoding); // Advertiser uint16_t add_to_advertiser(std::vector< BleAdvParam > & params); void remove_from_advertiser(uint16_t msg_id); + // identify which encoder is relevant for the param, decode and log Action and Controller parameters + bool identify_param(const BleAdvParam & param, bool ignore_ble_param); + + // Listener +#ifdef USE_ESP32_BLE_CLIENT + void capture(const esp32_ble_tracker::ESPBTDevice & device, bool ignore_ble_param = true, uint16_t rem_time = 60); +#endif + +#ifdef USE_API + // HA service to decode + void on_raw_decode(std::string raw); +#endif + protected: // ref to registered encoders std::vector< BleAdvEncoder * > encoders_; @@ -146,22 +234,6 @@ class BleAdvHandler: public Component uint16_t id_count = 1; uint32_t adv_stop_time_ = 0; - esp_ble_adv_data_t adv_data_ = { - .set_scan_rsp = false, - .include_name = false, - .include_txpower = false, - .min_interval = 0x0001, - .max_interval = 0x0004, - .appearance = 0x00, - .manufacturer_len = 0, - .p_manufacturer_data = nullptr, - .service_data_len = 0, - .p_service_data = nullptr, - .service_uuid_len = 0, - .p_service_uuid = nullptr, - .flag = (ESP_BLE_ADV_FLAG_LIMIT_DISC | ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_DMT_CONTROLLER_SPT), - }; - esp_ble_adv_params_t adv_params_ = { .adv_int_min = 0x20, .adv_int_max = 0x20, @@ -173,6 +245,8 @@ class BleAdvHandler: public Component .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, }; + // Packets already captured once + std::list< BleAdvParam > listen_packets_; }; } //namespace bleadvcontroller diff --git a/components/ble_adv_controller/fan/__init__.py b/components/ble_adv_controller/fan/__init__.py index 431c9c7..c13ed3c 100644 --- a/components/ble_adv_controller/fan/__init__.py +++ b/components/ble_adv_controller/fan/__init__.py @@ -4,6 +4,7 @@ from esphome.const import ( CONF_OUTPUT_ID, + CONF_RESTORE_MODE, ) from .. import ( @@ -28,6 +29,8 @@ cv.Optional(CONF_BLE_ADV_SPEED_COUNT, default=6): cv.one_of(0,3,6), cv.Optional(CONF_BLE_ADV_DIRECTION_SUPPORTED, default=True): cv.boolean, cv.Optional(CONF_BLE_ADV_OSCILLATION_SUPPORTED, default=False): cv.boolean, + # override default value for restore mode, to always restore as it was if possible + cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum(fan.RESTORE_MODES, upper=True, space="_"), } ).extend(ENTITY_BASE_CONFIG_SCHEMA), ) diff --git a/components/ble_adv_controller/fanlamp_pro.cpp b/components/ble_adv_controller/fanlamp_pro.cpp index 193861b..ca501aa 100644 --- a/components/ble_adv_controller/fanlamp_pro.cpp +++ b/components/ble_adv_controller/fanlamp_pro.cpp @@ -8,138 +8,9 @@ namespace esphome { namespace bleadvcontroller { -static const char *TAG = "fanlamp_pro"; - -#pragma pack(push, 1) -typedef struct { - uint8_t prefix[10]; - uint8_t packet_number; - uint16_t type; - uint32_t identifier; - uint8_t group_index; - uint16_t command; - uint8_t args[4]; - uint16_t sign; - uint8_t spare; - uint16_t rand; - uint16_t crc16; -} adv_data_v2_t; - -typedef struct { /* Advertising Data for version 1*/ - uint8_t prefix[8]; - uint8_t command; - uint16_t group_idx; - uint8_t channel1; - uint8_t channel2; - uint8_t channel3; - uint8_t tx_count; - uint8_t outs; - uint8_t src; - uint8_t r2; - uint16_t seed; - uint16_t crc16; -} adv_data_v1_t; -#pragma pack(pop) - -static uint8_t HEADERv1a[7] = {0x02, 0x01, 0x02, 0x1B, 0x03, 0x77, 0xF8}; -static uint8_t HEADERv1b[8] = {0x02, 0x01, 0x02, 0x1B, 0x03, 0xF9, 0x08, 0x49}; -static uint8_t PREFIXv1[8] = {0xAA, 0x98, 0x43, 0xAF, 0x0B, 0x46, 0x46, 0x46}; -static uint8_t PREFIXv2[10] = {0x02, 0x01, 0x02, 0x1B, 0x16, 0xF0, 0x08, 0x10, 0x80, 0x00}; - -static uint8_t XBOXES[128] = { - 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, - 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, - 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, - 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, - 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, - 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, - 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, - 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, - 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, - 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, - 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, - 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, - 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, - 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, - 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, - 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16 -}; - -uint16_t sign(uint8_t* buf, uint8_t tx_count, uint16_t seed) { - uint8_t sigkey[16] = {0, 0, 0, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16}; - - sigkey[0] = seed & 0xff; - sigkey[1] = (seed >> 8) & 0xff; - sigkey[2] = tx_count; - mbedtls_aes_context aes_ctx; - mbedtls_aes_init(&aes_ctx); - mbedtls_aes_setkey_enc(&aes_ctx, sigkey, sizeof(sigkey)*8); - uint8_t aes_in[16], aes_out[16]; - memcpy(aes_in, buf, 16); - mbedtls_aes_crypt_ecb(&aes_ctx, ESP_AES_ENCRYPT, aes_in, aes_out); - mbedtls_aes_free(&aes_ctx); - uint16_t sign = ((uint16_t*) aes_out)[0]; - return sign == 0 ? 0xffff : sign; -} - -void v2_whiten(uint8_t *buf, uint8_t size, uint8_t seed, uint8_t salt) { - for (uint8_t i = 0; i < size; ++i) { - buf[i] ^= XBOXES[((seed + i + 9) & 0x1f) + (salt & 0x3) * 0x20]; - buf[i] ^= seed; - } -} - -uint16_t v2_crc16_ccitt(uint8_t *src, uint8_t size, uint16_t crc16_result) { - for (uint8_t i = 0; i < size; ++i) { - crc16_result = crc16_result ^ (*(uint16_t*) &src[i]) << 8; - for (uint8_t j = 8; j != 0; --j) { - if ((crc16_result & 0x8000) == 0) { - crc16_result <<= 1; - } else { - crc16_result = crc16_result << 1 ^ 0x1021; - } - } - } - - return crc16_result; -} - -void v1_whiten(uint8_t *buf, size_t start, size_t len, uint8_t seed) { - uint8_t r = seed; - for (size_t i=0; i < start+len; i++) { - uint8_t b = 0; - for (size_t j=0; j < 8; j++) { - r <<= 1; - if (r & 0x80) { - r ^= 0x11; - b |= 1 << j; - } - r &= 0x7F; - } - if (i >= start) { - buf[i - start] ^= b; - } - //ESP_LOGD(TAG, "%0x", b); - } -} - -uint8_t reverse_byte(uint8_t x) { - - x = ((x & 0x55) << 1) | ((x & 0xAA) >> 1); - x = ((x & 0x33) << 2) | ((x & 0xCC) >> 2); - x = ((x & 0x0F) << 4) | ((x & 0xF0) >> 4); - return x; -} - -bool FanLampEncoder::is_supported(const Command &cmd) { - FanLampArgs cmd_real = translate_cmd(cmd); - return (cmd_real.cmd_ != 0); -} - -FanLampArgs FanLampEncoder::translate_cmd(const Command &cmd) { - FanLampArgs cmd_real; - bool isV2 = ((this->variant_ == VARIANT_2) || (this->variant_ == VARIANT_3)); - switch(cmd.cmd_) +std::vector< Command > FanLampEncoder::translate(const Command & cmd, const ControllerParam_t & cont) { + Command cmd_real(cmd.main_cmd_); + switch(cmd.main_cmd_) { case CommandType::PAIR: cmd_real.cmd_ = 0x28; @@ -147,13 +18,6 @@ FanLampArgs FanLampEncoder::translate_cmd(const Command &cmd) { case CommandType::UNPAIR: cmd_real.cmd_ = 0x45; break; - case CommandType::CUSTOM: - cmd_real.cmd_ = cmd.args_[0]; - cmd_real.args_[0] = cmd.args_[1]; - cmd_real.args_[1] = cmd.args_[2]; - cmd_real.args_[2] = cmd.args_[3]; - cmd_real.args_[3] = cmd.args_[4]; - break; case CommandType::LIGHT_ON: cmd_real.cmd_ = 0x10; break; @@ -162,13 +26,6 @@ FanLampArgs FanLampEncoder::translate_cmd(const Command &cmd) { break; case CommandType::LIGHT_WCOLOR: cmd_real.cmd_ = 0x21; - if (isV2) { - cmd_real.args_[2] = cmd.args_[0]; - cmd_real.args_[3] = cmd.args_[1]; - } else { - cmd_real.args_[0] = cmd.args_[0]; - cmd_real.args_[1] = cmd.args_[1]; - } break; case CommandType::LIGHT_SEC_ON: cmd_real.cmd_ = 0x12; @@ -177,164 +34,292 @@ FanLampArgs FanLampEncoder::translate_cmd(const Command &cmd) { cmd_real.cmd_ = 0x13; break; case CommandType::FAN_ONOFF_SPEED: - if(isV2) { - cmd_real.cmd_ = 0x31; - cmd_real.args_[1] = (cmd.args_[1] == 6) ? 0x20 : 0; // specific flag for 6 level - cmd_real.args_[2] = cmd.args_[0]; - } else { - cmd_real.cmd_ = (cmd.args_[1] == 6) ? 0x32 : 0x31; // use Fan Gear or Fan Level - cmd_real.args_[0] = cmd.args_[0]; - cmd_real.args_[1] = (cmd.args_[1] == 6) ? cmd.args_[1] : 0; - } + cmd_real.cmd_ = 0x31; break; case CommandType::FAN_DIR: cmd_real.cmd_ = 0x15; - if(isV2) { - cmd_real.args_[1] = !cmd.args_[0]; - } else { - cmd_real.args_[0] = !cmd.args_[0]; - } break; case CommandType::FAN_OSC: - if(isV2) { - cmd_real.cmd_ = 0x16; - cmd_real.args_[1] = cmd.args_[0]; - } + cmd_real.cmd_ = 0x16; break; case CommandType::NOCMD: default: break; } - return cmd_real; + std::vector< Command > cmds; + if(cmd_real.cmd_ != 0x00) { + cmds.emplace_back(cmd_real); + } + return cmds; } -void FanLampEncoder::build_packet_v1(uint8_t* buf, Command &cmd) { - uint16_t seed = this->get_seed(); - adv_data_v1_t *packet = (adv_data_v1_t*)buf; - std::copy(PREFIXv1, PREFIXv1 + sizeof(PREFIXv1), packet->prefix); - FanLampArgs cmd_real = this->translate_cmd(cmd); - packet->command = cmd_real.cmd_; - packet->group_idx = static_cast(cmd.id_ & 0xF0FF); - packet->tx_count = cmd.tx_count_; - packet->outs = 0; - packet->src = static_cast(seed ^ 1); - packet->r2 = static_cast(seed ^ 1); - packet->seed = htons(seed); - if (cmd.cmd_ == CommandType::PAIR) { - packet->channel1 = static_cast(packet->group_idx & 0xFF); - packet->channel2 = static_cast(packet->group_idx >> 8); - packet->channel3 = 0x81; - } else { - packet->channel1 = cmd_real.args_[0]; - packet->channel2 = cmd_real.args_[1]; - packet->channel3 = cmd_real.args_[2]; +uint16_t FanLampEncoder::get_seed(uint16_t forced_seed) { + return (forced_seed == 0) ? (uint16_t) rand() % 0xFFF5 : forced_seed; +} + +uint16_t FanLampEncoder::crc16(uint8_t* buf, size_t len, uint16_t seed) { + return esphome::crc16be(buf, len, seed); +} + +FanLampEncoderV1::FanLampEncoderV1(const std::string & encoding, const std::string & variant, uint8_t pair_arg3, + bool pair_arg_only_on_pair, bool xor1, uint8_t supp_prefix): + FanLampEncoder(encoding, variant, {0xAA, 0x98, 0x43, 0xAF, 0x0B, 0x46, 0x46, 0x46}), pair_arg3_(pair_arg3), pair_arg_only_on_pair_(pair_arg_only_on_pair), + with_crc2_(supp_prefix == 0x00), xor1_(xor1) { + if (supp_prefix != 0x00) this->prefix_.insert(this->prefix_.begin(), supp_prefix); + this->len_ = this->prefix_.size() + sizeof(data_map_t) + (this->with_crc2_ ? 2 : 1); +} + +std::vector< Command > FanLampEncoderV1::translate(const Command & cmd, const ControllerParam_t & cont) { + auto cmds = FanLampEncoder::translate(cmd, cont); + for (auto & cmd_real: cmds) { + switch(cmd_real.main_cmd_) + { + case CommandType::PAIR: + cmd_real.args_[0] = cont.id_ & 0xFF; + cmd_real.args_[1] = (cont.id_ >> 8) & 0xF0; + cmd_real.args_[2] = this->pair_arg3_; + break; + case CommandType::LIGHT_WCOLOR: + cmd_real.args_[0] = cmd.args_[0]; + cmd_real.args_[1] = cmd.args_[1]; + break; + case CommandType::FAN_ONOFF_SPEED: + cmd_real.cmd_ = (cmd.args_[1] == 6) ? 0x32 : 0x31; // use Fan Gear or Fan Level + cmd_real.args_[0] = cmd.args_[0]; + cmd_real.args_[1] = (cmd.args_[1] == 6) ? cmd.args_[1] : 0; + break; + case CommandType::FAN_DIR: + cmd_real.args_[0] = !cmd.args_[0]; + break; + case CommandType::FAN_OSC: + cmd_real.args_[0] = cmd.args_[0]; + break; + case CommandType::NOCMD: + default: + break; + } } - packet->crc16 = htons(v2_crc16_ccitt(buf + 8, 12, ~seed)); - - ESP_LOGD(TAG, "%s - ID: '0x%08X', tx: %d, Command: '0x%02X', Args: [%d,%d,%d]", this->id_.c_str(), cmd.id_, - packet->tx_count, packet->command, packet->channel1, packet->channel2, packet->channel3); + return cmds; } -void FanLampEncoder::build_packet_v1a(uint8_t* buf, Command &cmd) { +bool FanLampEncoderV1::decode(uint8_t* buf, Command &cmd, ControllerParam_t & cont){ + this->whiten(buf, this->len_, 0x6F); + this->reverse_all(buf, this->len_); + ENSURE_EQ(std::equal(this->prefix_.begin(), this->prefix_.end(), buf), true, "prefix KO"); - const size_t base = sizeof(HEADERv1a); - const size_t size = MAX_PACKET_LEN; - std::copy(HEADERv1a, HEADERv1a + base, buf); - - build_packet_v1(buf + base, cmd); - - uint16_t* crc16_2 = (uint16_t*) &buf[size-2]; - *crc16_2 = htons(v2_crc16_ccitt(buf + base + 8, 14, v2_crc16_ccitt(buf + base + 1, 5, 0xffff))); + std::string decoded = esphome::format_hex_pretty(buf, this->len_); + + uint8_t data_start = this->prefix_.size(); + data_map_t * data = (data_map_t *) (buf + data_start); + + // distinguish between lampsmart pro and fanlamp pro + if (data->command == 0x28 && this->pair_arg3_ != data->args[2]) return false; + if (data->command != 0x28 && !this->pair_arg_only_on_pair_ && data->args[2] != this->pair_arg3_) return false; + if (data->command != 0x28 && this->pair_arg_only_on_pair_ && data->args[2] != 0) return false; + + uint16_t seed = htons(data->seed); + uint8_t seed8 = static_cast(seed & 0xFF); + ENSURE_EQ(data->r2, this->xor1_ ? seed8 ^ 1 : seed8, "Decoded KO (r2) - %s", decoded.c_str()); + + uint16_t crc16 = htons(this->crc16((uint8_t*)(data), sizeof(data_map_t) - 2, ~seed)); + ENSURE_EQ(crc16, data->crc16, "Decoded KO (crc16) - %s", decoded.c_str()); - for (size_t i=base; i < size; i++) { - buf[i] = reverse_byte(buf[i]); + if (data->args[2] != 0) { + ENSURE_EQ(data->args[2], this->pair_arg3_, "Decoded KO (arg3) - %s", decoded.c_str()); } - - v1_whiten(buf + base, 8+base, size-base, 83); + + if (this->with_crc2_) { + uint16_t crc16_mac = this->crc16(buf + 1, 5, 0xffff); + uint16_t crc16_2 = htons(this->crc16(buf + data_start, sizeof(data_map_t), crc16_mac)); + uint16_t crc16_data_2 = *(uint16_t*) &buf[this->len_ - 2]; + ENSURE_EQ(crc16_data_2, crc16_2, "Decoded KO (crc16_2) - %s", decoded.c_str()); + } + + uint8_t rem_id = data->src ^ seed8; + + cmd.cmd_ = (CommandType) (data->command); + std::copy(data->args, data->args + sizeof(data->args), cmd.args_); + cont.tx_count_ = data->tx_count; + cont.index_ = (data->group_index & 0x0F00) >> 8; + cont.id_ = data->group_index + 256*256*rem_id; + cont.seed_ = seed; + return true; } -void FanLampEncoder::build_packet_v1b(uint8_t* buf, Command &cmd) { - const size_t base = sizeof(HEADERv1b); - const size_t size = MAX_PACKET_LEN; - std::copy(HEADERv1b, HEADERv1b + base, buf); +/* Code taken from FanLamp App + public static final byte[] PREAMBLE = {113, 15, 85}; // 0x71, 0x0F, 0x55 + public static final byte[] DEVICE_ADDRESS = {-104, 67, -81, 11, 70}; // 0x68, 0x43, 0xAF, 0x0B, 0x46 + + byte[] bArr2 = new byte[22]; + bArr2[0] = (LampConfig.DEVICE_ADDRESS[0] & 128) == 128 ? (byte) -86 : (byte) 85; // 0xAA : 0x55 + int i7 = 0; + while (i7 < Math.min(LampConfig.DEVICE_ADDRESS.length, 5)) { + int i8 = i7 + 1; + bArr2[i8] = LampConfig.DEVICE_ADDRESS[i7]; + i7 = i8; + } + bArr2[6] = bArr2[5]; + bArr2[7] = bArr2[5]; + ... + byte[] bArr3 = new byte[25]; + System.arraycopy(LampConfig.PREAMBLE, 0, bArr3, 0, LampConfig.PREAMBLE.length); + System.arraycopy(bArr2, 0, bArr3, 3, bArr2.length); +*/ + +void FanLampEncoderV1::encode(uint8_t* buf, Command &cmd_real, ControllerParam_t & cont) { + std::copy(this->prefix_.begin(), this->prefix_.end(), buf); + data_map_t *data = (data_map_t*)(buf + this->prefix_.size()); + + uint16_t seed = this->get_seed(cont.seed_); + uint8_t seed8 = static_cast(seed & 0xFF); + uint16_t cmd_id_trunc = static_cast(cont.id_ & 0xF0FF); - build_packet_v1(buf + base, cmd); + data->command = cmd_real.cmd_; + data->group_index = cmd_id_trunc + (((uint16_t)(cont.index_ & 0x0F)) << 8); + data->tx_count = cont.tx_count_; + data->outs = 0; + data->src = this->xor1_ ? seed8 ^ 1 : seed8 ^ ((cont.id_ >> 16) & 0xFF); + data->r2 = this->xor1_ ? seed8 ^ 1 : seed8; + data->seed = htons(seed); + data->args[0] = cmd_real.args_[0]; + data->args[1] = cmd_real.args_[1]; + data->args[2] = this->pair_arg_only_on_pair_ ? cmd_real.args_[2] : this->pair_arg3_; + data->crc16 = htons(this->crc16((uint8_t*)(data), sizeof(data_map_t) - 2, ~seed)); - buf[size-1] = 0xaa; - for (size_t i=base; i < size; i++) { - buf[i] = reverse_byte(buf[i]); + if (this->with_crc2_) { + uint16_t* crc16_2 = (uint16_t*) &buf[this->len_ - 2]; + uint16_t crc_mac = this->crc16(buf + 1, 5, 0xffff); + *crc16_2 = htons(this->crc16((uint8_t*)(data), sizeof(data_map_t), crc_mac)); + } else { + buf[this->len_ - 1] = 0xAA; } - v1_whiten(buf + base, 8+base, size-base, 83); + this->reverse_all(buf, this->len_); + this->whiten(buf, this->len_, 0x6F); } -void FanLampEncoder::build_packet_v2(uint8_t * buf, Command &cmd, bool with_sign) { - uint16_t seed = this->get_seed(); - adv_data_v2_t * packet = (adv_data_v2_t *) buf; - std::copy(PREFIXv2, PREFIXv2 + sizeof(PREFIXv2), packet->prefix); - FanLampArgs cmd_real = this->translate_cmd(cmd); - packet->packet_number = cmd.tx_count_; - packet->type = 0x0100; - packet->identifier = cmd.id_; - packet->command = cmd_real.cmd_; - std::copy(cmd_real.args_, cmd_real.args_ + sizeof(cmd_real.args_), packet->args); - packet->group_index = 0; - packet->rand = seed; - - ESP_LOGD(TAG, "%s - ID: '0x%08X', tx: %d, Command: '0x%02X', Args: [%d,%d,%d,%d]", this->id_.c_str(), cmd.id_, - packet->packet_number, packet->command, packet->args[0], packet->args[1], packet->args[2], packet->args[3]); - - if (with_sign) { - packet->sign = sign(buf + 8, packet->packet_number, seed); - } +FanLampEncoderV2::FanLampEncoderV2(const std::string & encoding, const std::string & variant, const std::vector && prefix, uint16_t device_type, bool with_sign): + FanLampEncoder(encoding, variant, prefix), device_type_(device_type), with_sign_(with_sign) { + this->len_ = this->prefix_.size() + sizeof(data_map_t); +} - v2_whiten(buf + 9, 18, (uint8_t) seed, 0); - packet->crc16 = v2_crc16_ccitt(buf + 7, 22, ~seed); +uint16_t FanLampEncoderV2::sign(uint8_t* buf, uint8_t tx_count, uint16_t seed) { + uint8_t sigkey[16] = {0, 0, 0, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16}; + + sigkey[0] = seed & 0xff; + sigkey[1] = (seed >> 8) & 0xff; + sigkey[2] = tx_count; + mbedtls_aes_context aes_ctx; + mbedtls_aes_init(&aes_ctx); + mbedtls_aes_setkey_enc(&aes_ctx, sigkey, sizeof(sigkey)*8); + uint8_t aes_in[16], aes_out[16]; + memcpy(aes_in, buf, 16); + mbedtls_aes_crypt_ecb(&aes_ctx, ESP_AES_ENCRYPT, aes_in, aes_out); + mbedtls_aes_free(&aes_ctx); + uint16_t sign = ((uint16_t*) aes_out)[0]; + return sign == 0 ? 0xffff : sign; } -uint16_t FanLampEncoder::get_seed() { - uint16_t seed = (uint16_t) rand() % 0xFFF5; - return seed; +void FanLampEncoderV2::whiten(uint8_t *buf, uint8_t size, uint8_t seed, uint8_t salt) { + static constexpr uint8_t XBOXES[128] = { + 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, + 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, + 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, + 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, + 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, + 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, + 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, + 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, + 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, + 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, + 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, + 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, + 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, + 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, + 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, + 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16 + }; + + for (uint8_t i = 0; i < size; ++i) { + buf[i] ^= XBOXES[((seed + i + 9) & 0x1f) + (salt & 0x3) * 0x20]; + buf[i] ^= seed; + } } -uint8_t FanLampEncoder::get_adv_data(std::vector< BleAdvParam > & params, Command &cmd) { - params.emplace_back(); - BleAdvParam & param = params.back(); - switch (this->variant_) { - case VARIANT_3: - this->build_packet_v2(param.buf_, cmd, true); - break; - case VARIANT_2: - this->build_packet_v2(param.buf_, cmd, false); - break; - case VARIANT_1A: - this->build_packet_v1a(param.buf_, cmd); - break; - case VARIANT_1B: - this->build_packet_v1b(param.buf_, cmd); - break; - default: - ESP_LOGW(TAG, "get_adv_data called with invalid variant %d", this->variant_); - break; +std::vector< Command > FanLampEncoderV2::translate(const Command & cmd, const ControllerParam_t & cont) { + auto cmds = FanLampEncoder::translate(cmd, cont); + for (auto & cmd_real: cmds) { + switch(cmd_real.main_cmd_) + { + case CommandType::LIGHT_WCOLOR: + cmd_real.args_[2] = cmd.args_[0]; + cmd_real.args_[3] = cmd.args_[1]; + break; + case CommandType::FAN_ONOFF_SPEED: + cmd_real.args_[1] = (cmd.args_[1] == 6) ? 0x20 : 0; // specific flag for 6 level + cmd_real.args_[2] = cmd.args_[0]; + break; + case CommandType::FAN_DIR: + cmd_real.args_[1] = !cmd.args_[0]; + break; + case CommandType::FAN_OSC: + cmd_real.args_[1] = cmd.args_[0]; + break; + case CommandType::NOCMD: + default: + break; + } + } + return cmds; +} + +bool FanLampEncoderV2::decode(uint8_t* buf, Command &cmd, ControllerParam_t & cont){ + data_map_t * data = (data_map_t *) (buf + this->prefix_.size()); + uint16_t crc16 = this->crc16(buf , this->len_ - 2, ~(data->seed)); + + this->whiten(buf + 2, this->len_ - 6, (uint8_t)(data->seed), 0); + if (!std::equal(this->prefix_.begin(), this->prefix_.end(), buf)) return false; + if (data->type != this->device_type_) return false; + if (this->with_sign_ && data->sign == 0x0000) return false; + if (!this->with_sign_ && data->sign != 0x0000) return false; + + std::string decoded = esphome::format_hex_pretty(buf, this->len_); + ENSURE_EQ(crc16, data->crc16, "Decoded KO (crc16) - %s", decoded.c_str()); + + if (this->with_sign_) { + ENSURE_EQ(this->sign(buf + 1, data->tx_count, data->seed), data->sign, "Decoded KO (sign) - %s", decoded.c_str()); } - return 1; + + cmd.cmd_ = (CommandType) (data->command); + std::copy(data->args, data->args + sizeof(data->args), cmd.args_); + cont.tx_count_ = data->tx_count; + cont.index_ = data->group_index; + cont.id_ = data->identifier; + cont.seed_ = data->seed; + + return true; } -void FanLampEncoder::register_encoders(BleAdvHandler * handler, const std::string & encoding) { - BleAdvMultiEncoder * fanlamp_all = new BleAdvMultiEncoder("FanLamp - All", encoding); - handler->add_encoder(fanlamp_all); - BleAdvEncoder * fanlamp_1a = new FanLampEncoder("FanLamp - v1a", encoding, FanLampVariant::VARIANT_1A); - handler->add_encoder(fanlamp_1a); - fanlamp_all->add_encoder(fanlamp_1a); - BleAdvEncoder * fanlamp_1b = new FanLampEncoder("FanLamp - v1b", encoding, FanLampVariant::VARIANT_1B); - handler->add_encoder(fanlamp_1b); - fanlamp_all->add_encoder(fanlamp_1b); - BleAdvEncoder * fanlamp_v2 = new FanLampEncoder("FanLamp - v2", encoding, FanLampVariant::VARIANT_2); - handler->add_encoder(fanlamp_v2); - fanlamp_all->add_encoder(fanlamp_v2); - BleAdvEncoder * fanlamp_v3 = new FanLampEncoder("FanLamp - v3", encoding, FanLampVariant::VARIANT_3); - handler->add_encoder(fanlamp_v3); - fanlamp_all->add_encoder(fanlamp_v3); +void FanLampEncoderV2::encode(uint8_t* buf, Command &cmd_real, ControllerParam_t & cont) { + std::copy(this->prefix_.begin(), this->prefix_.end(), buf); + data_map_t * data = (data_map_t *) (buf + this->prefix_.size()); + + uint16_t seed = this->get_seed(cont.seed_); + + data->tx_count = cont.tx_count_; + data->type = this->device_type_; + data->identifier = cont.id_; + data->command = cmd_real.cmd_; + std::copy(cmd_real.args_, cmd_real.args_ + sizeof(cmd_real.args_), data->args); + data->group_index = cont.index_; + data->seed = seed; + + if (this->with_sign_) { + data->sign = this->sign(buf + 1, data->tx_count, seed); + } + + this->whiten(buf + 2, this->len_ - 6, (uint8_t) seed); + data->crc16 = this->crc16(buf, this->len_ - 2, ~seed); } } // namespace bleadvcontroller diff --git a/components/ble_adv_controller/fanlamp_pro.h b/components/ble_adv_controller/fanlamp_pro.h index 38660ee..0d2f6d6 100644 --- a/components/ble_adv_controller/fanlamp_pro.h +++ b/components/ble_adv_controller/fanlamp_pro.h @@ -5,31 +5,78 @@ namespace esphome { namespace bleadvcontroller { -enum FanLampVariant : int {VARIANT_3, VARIANT_2, VARIANT_1A, VARIANT_1B}; +class FanLampEncoder: public BleAdvEncoder +{ +public: + FanLampEncoder(const std::string & encoding, const std::string & variant, const std::vector & prefix): + BleAdvEncoder(encoding, variant), prefix_(prefix) {} -typedef struct { - uint8_t cmd_{0}; - uint8_t args_[4]{0}; -} FanLampArgs; +protected: + virtual std::vector< Command > translate(const Command & cmd, const ControllerParam_t & cont) override; -class FanLampEncoder: public BleAdvEncoder + uint16_t get_seed(uint16_t forced_seed = 0); + uint16_t crc16(uint8_t* buf, size_t len, uint16_t seed); + + std::vector prefix_; +}; + +class FanLampEncoderV1: public FanLampEncoder { public: - FanLampEncoder(const std::string & name, const std::string & encoding, FanLampVariant variant): - BleAdvEncoder(name, encoding, variant) {} + FanLampEncoderV1(const std::string & encoding, const std::string & variant, + uint8_t pair_arg3, bool pair_arg_only_on_pair = true, bool xor1 = false, uint8_t supp_prefix = 0x00); - virtual bool is_supported(const Command &cmd) override; - virtual uint8_t get_adv_data(std::vector< BleAdvParam > & params, Command &cmd) override; - static void register_encoders(BleAdvHandler * handler, const std::string & encoding); +protected: + struct data_map_t { + uint8_t command; + uint16_t group_index; + uint8_t args[3]; + uint8_t tx_count; + uint8_t outs; + uint8_t src; + uint8_t r2; + uint16_t seed; + uint16_t crc16; + }__attribute__((packed, aligned(1))); + + virtual std::vector< Command > translate(const Command & cmd, const ControllerParam_t & cont) override; + virtual bool decode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) override; + virtual void encode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) override; + + uint8_t pair_arg3_; + bool pair_arg_only_on_pair_; + bool with_crc2_; + bool xor1_; +}; + +class FanLampEncoderV2: public FanLampEncoder +{ +public: + FanLampEncoderV2(const std::string & encoding, const std::string & variant, const std::vector && prefix, uint16_t device_type, bool with_sign); protected: - virtual FanLampArgs translate_cmd(const Command &cmd); - uint16_t get_seed(); + struct data_map_t { + uint8_t tx_count; + uint16_t type; + uint32_t identifier; + uint8_t group_index; + uint16_t command; + uint8_t args[4]; + uint16_t sign; + uint8_t spare; + uint16_t seed; + uint16_t crc16; + }__attribute__((packed, aligned(1))); + + virtual std::vector< Command > translate(const Command & cmd, const ControllerParam_t & cont) override; + virtual bool decode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) override; + virtual void encode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) override; + + uint16_t sign(uint8_t* buf, uint8_t tx_count, uint16_t seed); + void whiten(uint8_t *buf, uint8_t size, uint8_t seed, uint8_t salt = 0); - void build_packet_v1a(uint8_t* buf, Command &cmd); - void build_packet_v1b(uint8_t* buf, Command &cmd); - void build_packet_v1(uint8_t* buf, Command &cmd); - void build_packet_v2(uint8_t* buf, Command &cmd, bool with_sign); + uint16_t device_type_; + bool with_sign_; }; } //namespace bleadvcontroller diff --git a/components/ble_adv_controller/light/__init__.py b/components/ble_adv_controller/light/__init__.py index dab4940..9041b16 100644 --- a/components/ble_adv_controller/light/__init__.py +++ b/components/ble_adv_controller/light/__init__.py @@ -9,6 +9,7 @@ CONF_MIN_BRIGHTNESS, CONF_OUTPUT_ID, CONF_DEFAULT_TRANSITION_LENGTH, + CONF_RESTORE_MODE, ) from .. import ( @@ -38,6 +39,8 @@ cv.Optional(CONF_BLE_ADV_SPLIT_DIM_CCT, default=False): cv.boolean, # override default value of default_transition_length to 0s as mostly not supported by those lights cv.Optional(CONF_DEFAULT_TRANSITION_LENGTH, default="0s"): cv.positive_time_period_milliseconds, + # override default value for restore mode, to always restore as it was if possible + cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum(light.RESTORE_MODES, upper=True, space="_"), } ).extend(ENTITY_BASE_CONFIG_SCHEMA), light.RGB_LIGHT_SCHEMA.extend( @@ -57,10 +60,10 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await entity_base_code_gen(var, config) await light.register_light(var, config) - if not CONF_BLE_ADV_SECONDARY in config: - cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) - cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + if CONF_BLE_ADV_SECONDARY in config: + cg.add(var.set_traits()) + else: + cg.add(var.set_traits(config[CONF_COLD_WHITE_COLOR_TEMPERATURE], config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) cg.add(var.set_split_dim_cct(config[CONF_BLE_ADV_SPLIT_DIM_CCT])) - cg.add(var.set_min_brightness(config[CONF_MIN_BRIGHTNESS])) - cg.add(cg.App.register_number(var.get_number_min_brightness())) + cg.add(var.set_min_brightness(config[CONF_MIN_BRIGHTNESS] * 100, 0, 100, 1)) diff --git a/components/ble_adv_controller/light/ble_adv_light.cpp b/components/ble_adv_controller/light/ble_adv_light.cpp index f554ce7..929493d 100644 --- a/components/ble_adv_controller/light/ble_adv_light.cpp +++ b/components/ble_adv_controller/light/ble_adv_light.cpp @@ -10,32 +10,31 @@ float ensure_range(float f) { return (f > 1.0) ? 1.0 : ( (f < 0.0) ? 0.0 : f ); } -void BleAdvLight::setup() { - // init number for Min Brightness - this->number_min_brightness_.set_id("Min Brightness", this->get_name()); - this->number_min_brightness_.set_entity_category(EntityCategory::ENTITY_CATEGORY_CONFIG); - this->number_min_brightness_.traits.set_min_value(0); - this->number_min_brightness_.traits.set_max_value(100); - this->number_min_brightness_.traits.set_step(1); - this->number_min_brightness_.publish_state(this->number_min_brightness_.state); +void BleAdvLight::set_min_brightness(int min_brightness, int min, int max, int step) { + this->number_min_brightness_.traits.set_min_value(min); + this->number_min_brightness_.traits.set_max_value(max); + this->number_min_brightness_.traits.set_step(step); + this->number_min_brightness_.state = min_brightness; } -light::LightTraits BleAdvLight::get_traits() { - auto traits = light::LightTraits(); - - traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); - traits.set_min_mireds(this->cold_white_temperature_); - traits.set_max_mireds(this->warm_white_temperature_); +void BleAdvLight::set_traits(float cold_white_temperature, float warm_white_temperature) { + this->traits_.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); + this->traits_.set_min_mireds(cold_white_temperature); + this->traits_.set_max_mireds(warm_white_temperature); +} - return traits; +void BleAdvLight::setup() { + if (this->get_parent()->is_show_config()) { + this->number_min_brightness_.init("Min Brightness", this->get_name()); + } } void BleAdvLight::dump_config() { ESP_LOGCONFIG(TAG, "BleAdvLight"); BleAdvEntity::dump_config_base(TAG); ESP_LOGCONFIG(TAG, " Base Light '%s'", this->state_->get_name().c_str()); - ESP_LOGCONFIG(TAG, " Cold White Temperature: %f mireds", this->cold_white_temperature_); - ESP_LOGCONFIG(TAG, " Warm White Temperature: %f mireds", this->warm_white_temperature_); + ESP_LOGCONFIG(TAG, " Cold White Temperature: %f mireds", this->traits_.get_min_mireds()); + ESP_LOGCONFIG(TAG, " Warm White Temperature: %f mireds", this->traits_.get_max_mireds()); ESP_LOGCONFIG(TAG, " Constant Brightness: %s", this->constant_brightness_ ? "true" : "false"); ESP_LOGCONFIG(TAG, " Minimum Brightness: 0x%.2X", this->get_min_brightness()); } @@ -58,7 +57,7 @@ void BleAdvLight::write_state(light::LightState *state) { // Compute Corrected Brigtness / Warm Color Temperature (potentially reversed) as float: 0 -> 1 float updated_brf = ensure_range(this->get_min_brightness() + state->current_values.get_brightness() * (1.f - this->get_min_brightness())); - float updated_ctf = ensure_range((state->current_values.get_color_temperature() - this->cold_white_temperature_) / (this->warm_white_temperature_ - this->cold_white_temperature_)); + float updated_ctf = ensure_range((state->current_values.get_color_temperature() - this->traits_.get_min_mireds()) / (this->traits_.get_max_mireds() - this->traits_.get_min_mireds())); updated_ctf = this->get_parent()->is_reversed() ? 1.0 - updated_ctf : updated_ctf; // During transition(current / remote states are not the same), do not process change @@ -66,7 +65,7 @@ void BleAdvLight::write_state(light::LightState *state) { float br_diff = abs(this->brightness_ - updated_brf) * 100; float ct_diff = abs(this->warm_color_ - updated_ctf) * 100; bool is_last = (state->current_values == state->remote_values); - if (br_diff < 3 && ct_diff < 3 && !is_last) { + if ((br_diff < 3 && ct_diff < 3 && !is_last) || (is_last && br_diff == 0 && ct_diff == 0)) { return; } @@ -100,12 +99,6 @@ void BleAdvLight::write_state(light::LightState *state) { Secondary Light **********************/ -light::LightTraits BleAdvSecLight::get_traits() { - auto traits = light::LightTraits(); - traits.set_supported_color_modes({light::ColorMode::ON_OFF}); - return traits; -} - void BleAdvSecLight::dump_config() { ESP_LOGCONFIG(TAG, "BleAdvSecLight"); BleAdvEntity::dump_config_base(TAG); diff --git a/components/ble_adv_controller/light/ble_adv_light.h b/components/ble_adv_controller/light/ble_adv_light.h index f39378a..1cb7d19 100644 --- a/components/ble_adv_controller/light/ble_adv_light.h +++ b/components/ble_adv_controller/light/ble_adv_light.h @@ -12,24 +12,21 @@ class BleAdvLight : public light::LightOutput, public BleAdvEntity, public Entit void setup() override; void dump_config() override; - void set_cold_white_temperature(float cold_white_temperature) { this->cold_white_temperature_ = cold_white_temperature; } - void set_warm_white_temperature(float warm_white_temperature) { this->warm_white_temperature_ = warm_white_temperature; } + void set_traits(float cold_white_temperature, float warm_white_temperature); void set_constant_brightness(bool constant_brightness) { this->constant_brightness_ = constant_brightness; } - void set_min_brightness(float min_brightness) { this->number_min_brightness_.state = min_brightness * 100; } + void set_min_brightness(int min_brightness, int min, int max, int step); void set_split_dim_cct(bool split_dim_cct) { this->split_dim_cct_ = split_dim_cct; } - number::Number * get_number_min_brightness() { return &(this->number_min_brightness_); } float get_min_brightness() { return ((float)this->number_min_brightness_.state) / 100.0f; } void setup_state(light::LightState *state) override { this->state_ = state; }; void write_state(light::LightState *state) override; - light::LightTraits get_traits() override; + light::LightTraits get_traits() override { return this->traits_; } protected: light::LightState * state_{nullptr}; - float cold_white_temperature_; - float warm_white_temperature_; + light::LightTraits traits_; bool constant_brightness_; BleAdvNumber number_min_brightness_; bool split_dim_cct_; @@ -42,14 +39,16 @@ class BleAdvLight : public light::LightOutput, public BleAdvEntity, public Entit class BleAdvSecLight : public light::LightOutput, public BleAdvEntity, public EntityBase { public: + void set_traits() { this->traits_.set_supported_color_modes({light::ColorMode::ON_OFF}); }; void dump_config() override; void setup_state(light::LightState *state) override { this->state_ = state; }; void write_state(light::LightState *state) override; - light::LightTraits get_traits() override; + light::LightTraits get_traits() override { return this->traits_; }; protected: light::LightState * state_{nullptr}; + light::LightTraits traits_; }; } //namespace bleadvcontroller diff --git a/components/ble_adv_controller/zhijia.cpp b/components/ble_adv_controller/zhijia.cpp index e6c5fb0..7959b5d 100644 --- a/components/ble_adv_controller/zhijia.cpp +++ b/components/ble_adv_controller/zhijia.cpp @@ -4,701 +4,366 @@ namespace esphome { namespace bleadvcontroller { -static const char *TAG = "zhijia"; -static unsigned char MAC[] = {0x19, 0x01, 0x10, 0xAA}; - -/********************* -START OF MSC16_MSC26 encoder -**********************/ -namespace msc16_msc26 { - -// Reverse byte -uint8_t reverse_8(uint8_t d) -{ - uint8_t result = 0; - for (int k = 0; k < 8; k++) - { - result |= ((d >> k) & 1) << (7 - k); - } - return result; -} - -// Reverse 16-bit number -uint16_t reverse_16(uint16_t d) -{ - uint16_t result = 0; - for (int k = 0; k < 16; k++) - { - result |= ((d >> k) & 1) << (15 - k); - } - return result; -} - -// CRC-16 calculation -uint16_t crc16(uint8_t *addr, size_t addr_length, uint8_t *data, size_t data_length) -{ - uint16_t crc = 0xFFFF; - - for (int i = addr_length - 1; i >= 0; i--) - { - crc ^= addr[i] << 8; - for (int j = 0; j < 8; j++) - { - uint16_t tmp = crc << 1; - if (crc & 0x8000) - tmp ^= 0x1021; - crc = tmp; - } - } - - for (size_t i = 0; i < data_length; i++) - { - crc ^= reverse_8(data[i]) << 8; - for (int j = 0; j < 8; j++) - { - uint16_t tmp = crc << 1; - if (crc & 0x8000) - tmp ^= 0x1021; - crc = tmp; - } - } - - return (~reverse_16(crc)) & 0xFFFF; -} - -// Whitening initialization -void whitening_init(uint8_t val, int *ctx, size_t ctx_length) -{ - ctx[0] = 1; - ctx[1] = (val >> 5) & 1; - ctx[2] = (val >> 4) & 1; - ctx[3] = (val >> 3) & 1; - ctx[4] = (val >> 2) & 1; - ctx[5] = (val >> 1) & 1; - ctx[6] = val & 1; -} - -// Whitening encoding -void whitening_encode(uint8_t *data, size_t length, int *ctx) -{ - for (size_t i = 0; i < length; i++) - { - uint8_t varC = ctx[3]; - uint8_t var14 = ctx[5]; - uint8_t var18 = ctx[6]; - uint8_t var10 = ctx[4]; - uint8_t var8 = var14 ^ ctx[2]; - uint8_t var4 = var10 ^ ctx[1]; - uint8_t _var = var18 ^ varC; - uint8_t var0 = _var ^ ctx[0]; - - uint8_t c = data[i]; - data[i] = ((c & 0x80) ^ ((var8 ^ var18) << 7)) | - ((c & 0x40) ^ (var0 << 6)) | - ((c & 0x20) ^ (var4 << 5)) | - ((c & 0x10) ^ (var8 << 4)) | - ((c & 0x08) ^ (_var << 3)) | - ((c & 0x04) ^ (var10 << 2)) | - ((c & 0x02) ^ (var14 << 1)) | - ((c & 0x01) ^ (var18)); - - // Update context - ctx[2] = var4; - ctx[3] = var8; - ctx[4] = var8 ^ varC; - ctx[5] = var0 ^ var10; - ctx[6] = var4 ^ var14; - ctx[0] = var8 ^ var18; - ctx[1] = var0; - } -} - -// Adjusted to include length parameters for addr and data arrays -void get_rf_payload16(uint8_t *addr, size_t addr_length, uint8_t *data, size_t data_length, uint8_t *output) -{ - const size_t data_offset = 0x12; - const size_t inverse_offset = 0x0F; - const size_t result_data_size = data_offset + addr_length + data_length; - uint8_t *resultbuf = (uint8_t *)calloc(result_data_size + 2, sizeof(uint8_t)); - - // Hardcoded values - resultbuf[0x0F] = 0x71; - resultbuf[0x10] = 0x0F; - resultbuf[0x11] = 0x55; - - // Reverse copy the address - for (size_t i = 0; i < addr_length; i++) - { - resultbuf[data_offset + addr_length - i - 1] = addr[i]; - } - - // Copy data - memcpy(&resultbuf[data_offset + addr_length], data, data_length); - - // Reverse certain bytes - for (size_t i = inverse_offset; i < inverse_offset + addr_length + 3; i++) - { - resultbuf[i] = reverse_8(resultbuf[i]); - } - - // CRC - uint16_t crc = crc16(addr, addr_length, data, data_length); - resultbuf[result_data_size] = crc & 0xFF; - resultbuf[result_data_size + 1] = (crc >> 8) & 0xFF; - - // Whitening - int whiteningBLE[7]; - whitening_init(0x3F, whiteningBLE, 7); - whitening_encode(&resultbuf[data_offset], result_data_size + 2 - data_offset, whiteningBLE); - int whiteningContext[7]; - whitening_init(0x25, whiteningContext, 7); - whitening_encode(&resultbuf[2], result_data_size + 2 - 2, whiteningContext); - - // Returning the last 16 bytes of the payload - memcpy(output, &resultbuf[result_data_size - 16 + 2], 16); - - free(resultbuf); -} -void get_rf_payload26(uint8_t *addr, size_t addr_length, uint8_t *data, size_t data_length, uint8_t *output) -{ - const size_t data_offset = 0x12; - const size_t inverse_offset = 0x0F; - const size_t result_data_size = data_offset + addr_length + data_length; - uint8_t *resultbuf = (uint8_t *)calloc(result_data_size + 2, sizeof(uint8_t)); - - // Hardcoded values - resultbuf[0x0F] = 0x71; - resultbuf[0x10] = 0x0F; - resultbuf[0x11] = 0x55; - - // Reverse copy the address - for (size_t i = 0; i < addr_length; i++) - { - resultbuf[data_offset + addr_length - i - 1] = addr[i]; - } - - // Copy data - memcpy(&resultbuf[data_offset + addr_length], data, data_length); - - // Reverse certain bytes - for (size_t i = inverse_offset; i < inverse_offset + addr_length + 3; i++) - { - resultbuf[i] = reverse_8(resultbuf[i]); - } - - // CRC - uint16_t crc = crc16(addr, addr_length, data, data_length); - resultbuf[result_data_size] = crc & 0xFF; - resultbuf[result_data_size + 1] = (crc >> 8) & 0xFF; - - // Whitening - int whiteningContext[7]; - whitening_init(0x25, whiteningContext, 7); - - whitening_encode(&resultbuf[2], result_data_size + 2 - 2, whiteningContext); - - // Returning the last 26 bytes of the payload - // uint8_t* final_payload = (uint8_t*)malloc(26 * sizeof(uint8_t)); - memcpy(output, &resultbuf[result_data_size - 24], 26); - - free(resultbuf); -} - -void aes16Encrypt16(unsigned char *mac, unsigned char *uuid, unsigned char groupId, unsigned char cmd, unsigned char sn, unsigned char *data, - unsigned char *outputData) -{ - unsigned char key; - unsigned char txData[8]; - - txData[5] = data[2] ^ sn; - txData[6] = data[2] ^ *uuid; - txData[7] = *data ^ sn; - txData[0] = txData[5] ^ *uuid; - txData[1] = txData[5] ^ *data; - txData[2] = txData[5] ^ groupId; - txData[3] = txData[5] ^ data[1]; - txData[4] = txData[5] ^ cmd; - txData[5] = (txData[5] ^ uuid[1]) - 1; - get_rf_payload16(mac, 3, txData, 8, outputData); -} - -void aes26Encrypt(uint8_t *mac, uint8_t *uuid, uint8_t *uid, uint8_t groupId, uint8_t cmd, uint8_t sn, uint8_t *data, - uint8_t *outputData) - -{ - uint8_t bVar1; - uint8_t bVar2; - uint8_t bVar3; - uint8_t key3; - uint8_t key4; - uint8_t txData[17]; - - bVar1 = uuid[1]; - bVar2 = uid[2]; - bVar3 = uuid[2]; - txData[8] = bVar2 ^ bVar1 ^ bVar3; - txData[8] = ((txData[8] & 1) - 1) ^ txData[8]; - txData[0] = txData[8] ^ *data; - txData[2] = txData[8] ^ *uuid; - txData[3] = txData[8] ^ data[1]; - txData[4] = txData[8] ^ sn; - txData[12] = bVar1 ^ txData[2]; - txData[13] = bVar2 ^ txData[4]; - txData[5] = txData[8] ^ data[2]; - txData[6] = txData[8] ^ groupId; - txData[9] = txData[8] ^ cmd; - txData[7] = txData[8] ^ *uid; - txData[10] = txData[8] ^ uid[1]; - txData[15] = bVar3 ^ txData[9]; - txData[1] = txData[8] ^ - *data ^ *uuid ^ data[1] ^ sn ^ data[2] ^ groupId ^ *uid ^ cmd ^ uid[1] ^ bVar1 ^ bVar2 ^ bVar3; - txData[11] = txData[8]; - txData[14] = txData[7]; - txData[16] = txData[8]; - get_rf_payload26(mac, 4, &txData[0], 0x11, outputData); -} +static constexpr size_t MAC_LEN = 4; +static uint8_t MAC[MAC_LEN] = {0x19, 0x01, 0x10, 0xAA}; +static constexpr size_t UID_LEN = 3; +static uint8_t UID[UID_LEN] = {0x19, 0x01, 0x10}; -} -/********************* -END OF MSC16_MSC26 encoder -**********************/ - -/********************** -START OF MSC26A encoder -**********************/ -namespace msc26a { - -void stage1(const unsigned char *x0, const unsigned char w20, const unsigned char w21, - const unsigned char w22, const unsigned char *x23, const unsigned char *x24, - unsigned char *buffer) { - unsigned short w8 = 0xa6aa; - size_t xzr = 0; - unsigned char sp[0x100] = {0}; - unsigned short wzr = 0; - size_t x8 = 0xa6aa; - unsigned char w11, w13, w14, w15, w16, w17, w1, w12, w9, w10, w0, w3, w2, w4, - w6, w5; - - w8 = 0xa6aa; - // sp = sp - 0x30; - *(size_t *)(sp + 0x40) = wzr; - *(size_t *)(sp + 0x28) = x8; - w11 = *(unsigned char *)(x24); - w13 = *(unsigned char *)(x0 + 1); - w14 = *(unsigned char *)(x0 + 2); - w15 = *(unsigned char *)(x23); - w16 = w11 ^ w21; - w17 = w11 ^ w13; - w8 = *(unsigned char *)(x24 + 1); - w1 = w16 ^ w13; - w16 = w17 ^ w21; - w12 = *(unsigned char *)(x23 + 1); - w9 = *(unsigned char *)(x23 + 2); - w2 = w16 ^ w14; - w10 = *(unsigned char *)(x24 + 2); - w17 = w1 ^ w15; - w2 = w2 ^ w22; - w16 = w17 ^ w20; - w1 = w2 ^ w1; - w2 = w8 ^ w16; - w1 = w1 ^ w20; - w2 = w2 ^ w9; - w1 = w1 ^ w12; - w2 = w2 ^ w10; - w0 = *(unsigned char *)(x0); - w1 = w1 ^ w8; - w3 = w2 & 1; - w1 = w1 ^ w9; - w3 = w3 - 1; - w1 = w1 ^ w16; - w2 = w3 ^ w2; - w1 = w1 ^ w10; - w3 = *(unsigned char *)(sp + 0x35); - w1 = w0 ^ w1; - w0 = w2 ^ w0; - *(unsigned char *)(sp + 0x2a) = w0; - w0 = *(unsigned char *)(sp + 0x3a); - w11 = w2 ^ w11; - w13 = w2 ^ w13; - w4 = w2 ^ w21; - w15 = w2 ^ w15; - w6 = w2 ^ w20; - w12 = w2 ^ w12; - w3 = w2 ^ w3; - w14 = w2 ^ w14; - w5 = w2 ^ w22; - w17 = w2 ^ w17; - *(unsigned char *)(sp + 0x2c) = w11; - *(unsigned char *)(sp + 0x2d) = w13; - *(unsigned char *)(sp + 0x34) = w12; - *(unsigned char *)(sp + 0x35) = w3; - w8 = w8 ^ w11; - w9 = w9 ^ w4; - w11 = w16 ^ w15; - w10 = w10 ^ w6; - w12 = w0 ^ w3; - w13 = w2 ^ w1; - x0 = sp + 0x28; - *(size_t *)(sp + 0x10) = xzr; - *(size_t *)(sp + 8) = xzr; - *(unsigned char *)(sp + 0x2e) = w4; - *(unsigned char *)(sp + 0x2f) = w14; - *(unsigned char *)(sp + 0x30) = w5; - *(unsigned char *)(sp + 0x31) = w15; - *(unsigned char *)(sp + 0x32) = w17; - *(unsigned char *)(sp + 0x33) = w6; - *(unsigned char *)(sp + 0x36) = w8; - *(unsigned char *)(sp + 0x37) = w9; - *(unsigned char *)(sp + 0x38) = w11; - *(unsigned char *)(sp + 0x39) = w10; - *(unsigned char *)(sp + 0x3a) = w12; - *(unsigned char *)(sp + 0x2b) = w13; - *(unsigned short *)(sp + 0x20) = wzr; - - memcpy(buffer, (sp + 0x28), 0x18); +uint16_t ZhijiaEncoder::crc16(uint8_t* buf, size_t len, uint16_t seed) { + return esphome::crc16(buf, len, seed, 0x8408, true, true); } -unsigned char *stage2(unsigned char *d, unsigned int n, unsigned int md) { - int8_t cVar1; - uint8_t *pbVar2; - unsigned int in_w3 = 0xa6; - long lVar3; - unsigned int uVar4; - unsigned int uVar5; - - if (((unsigned int)md & 0xff) < (unsigned int)(uint8_t)n) { - lVar3 = (n & 0xff) - ((size_t)md & 0xff); - cVar1 = (int8_t)in_w3; - pbVar2 = d + ((size_t)md & 0xff); - while (1) { - if (cVar1 < '\0') { - in_w3 ^= 0x11; - *pbVar2 = *pbVar2 ^ 1; - } - uVar5 = (unsigned int)(int8_t)(in_w3 << 1); - if ((int)uVar5 < 0) { - uVar5 ^= 0x11; - *pbVar2 = *pbVar2 ^ 2; - } - uVar5 = (unsigned int)(int8_t)(uVar5 << 1); - if ((int)uVar5 < 0) { - uVar5 ^= 0x11; - *pbVar2 = *pbVar2 ^ 4; - } - uVar5 = (unsigned int)(int8_t)(uVar5 << 1); - if ((int)uVar5 < 0) { - uVar5 ^= 0x11; - *pbVar2 = *pbVar2 ^ 8; - } - uVar5 = (unsigned int)(int8_t)(uVar5 << 1); - if ((int)uVar5 < 0) { - uVar5 ^= 0x11; - *pbVar2 = *pbVar2 ^ 0x10; - } - uVar5 = (unsigned int)(int8_t)(uVar5 << 1); - if ((int)uVar5 < 0) { - uVar5 ^= 0x11; - *pbVar2 = *pbVar2 ^ 0x20; - } - uVar5 = (unsigned int)(int8_t)(uVar5 << 1); - if ((int)uVar5 < 0) { - uVar5 ^= 0x11; - *pbVar2 = *pbVar2 ^ 0x40; - } - uVar5 = (unsigned int)(int8_t)(uVar5 << 1); - if ((int)uVar5 < 0) { - uVar5 ^= 0x11; - uVar4 = *pbVar2 ^ 0xffffff80; - *pbVar2 = (uint8_t)uVar4; - } else { - uVar4 = (unsigned int)*pbVar2; - } - in_w3 = uVar5 << 1; - lVar3 += -1; - *pbVar2 = - (uint8_t)((uVar4 >> 7) & 1) | - (uint8_t)(((uVar4 >> 6 & 1) | - ((uVar4 >> 5 & 1) | - (((uVar4 & 0xff) >> 4 & 1) | - ((uVar4 >> 3 & 1) | - ((uVar4 & 2) | (uVar4 & 1) << 2 | ((uVar4 & 0xff) >> 2 & 1)) - << 1) - << 1) - << 1) - << 1) - << 1); - if (lVar3 == 0) - break; - cVar1 = (int8_t)in_w3; - pbVar2 = pbVar2 + 1; - } +// {0xAB, 0xCD, 0xEF} => 0xABCDEF +uint32_t ZhijiaEncoder::uuid_to_id(uint8_t * uuid, size_t len) { + uint32_t id = 0; + for (size_t i = 0; i < len; ++i) { + id |= uuid[len - i - 1] << (8*i); } - return d; + return id; } -void whitening_init(unsigned char value, unsigned int *seed) { - unsigned int *puVar1; - unsigned int uVar2; - - *seed = 1; - puVar1 = seed + 1; - for (uVar2 = 5; uVar2 != 0xffffffff; uVar2 -= 1) { - *puVar1 = value >> (uVar2 & 0xff) & 1; - puVar1++; +// 0xABCDEF => {0xAB, 0xCD, 0xEF} +void ZhijiaEncoder::id_to_uuid(uint8_t * uuid, uint32_t id, size_t len) { + for (size_t i = 0; i < len; ++i) { + uuid[len - i - 1] = (id >> (8*i)) & 0xFF; } - return; } -unsigned char invert_8(unsigned char param_1) { - unsigned int uVar1; - unsigned int uVar2; - unsigned int uVar3; - - uVar2 = 7; - uVar3 = 0; - for (uVar1 = 0; uVar1 != 8; uVar1 += 1) { - if ((1 << (uVar1 & 0xff) & param_1) != 0) { - uVar3 |= 1 << (uVar2 & 0xff); - } - uVar2 -= 1; - } - return uVar3 & 0xff; -} - -unsigned int whitening_output(unsigned int *param_1) { - unsigned int uVar1; - unsigned int uVar2; - unsigned int uVar3; - - uVar1 = param_1[1]; - uVar2 = param_1[2]; - uVar3 = param_1[3]; - param_1[1] = *param_1; - param_1[2] = uVar1; - param_1[3] = uVar2; - uVar1 = param_1[6]; - *param_1 = uVar1; - uVar2 = param_1[5]; - param_1[5] = param_1[4]; - param_1[6] = uVar2; - param_1[4] = uVar1 ^ uVar3; - return uVar1; -} - -void whitening_encode(unsigned char *buffer, int length, unsigned int *seed) { - unsigned char bVar1; - unsigned int uVar2; - unsigned int uVar3; - int iVar4; - int iVar5; - - for (iVar5 = 0; iVar5 < length; iVar5 += 1) { - bVar1 = *(unsigned char *)(buffer + iVar5); - iVar4 = 0; - for (uVar3 = 0; uVar3 != 8; uVar3 += 1) { - uVar2 = whitening_output(seed); - iVar4 += ((uVar2 ^ bVar1 >> (uVar3 & 0xff)) & 1) << (uVar3 & 0xff); - } - *(char *)(buffer + iVar5) = (char)iVar4; - } - return; -} - -void stage3(unsigned char *buffer, size_t length, unsigned char *dest) { - unsigned char whitened[39]; - unsigned char seed[28] = {0}; - - whitening_init(0x25, (unsigned int *)seed); - - for (int i = 0; i < length; ++i) { - whitened[13 + i] = invert_8(buffer[i]); - } - - whitening_encode(whitened, length + 0xd, (unsigned int *)seed); - - for (int i = 0; i < length; ++i) { - dest[i] = whitened[13 + i]; - } -} - -} -/********************* -END OF MSC26A encoder -**********************/ - -bool ZhijiaEncoder::is_supported(const Command &cmd) { - ZhijiaArgs_t cmd_real = translate_cmd(cmd); - return (cmd_real.cmd_ != 0); -} - -ZhijiaArgs_t ZhijiaEncoder::translate_cmd(const Command &cmd) { - ZhijiaArgs_t cmd_real; - bool isV0 = (this->variant_ == VARIANT_V0); - bool isV2 = (this->variant_ == VARIANT_V2); - switch(cmd.cmd_) +std::vector< Command > ZhijiaEncoderV0::translate(const Command & cmd, const ControllerParam_t & cont) { + Command cmd_real(cmd.main_cmd_); + switch(cmd.main_cmd_) { case CommandType::PAIR: - cmd_real.cmd_ = isV0 ? 0xB4 : 0xA2; // -76 : -94 + cmd_real.cmd_ = 0xB4; // -76 break; case CommandType::UNPAIR: - cmd_real.cmd_ = isV0 ? 0xB0 : 0xA3; // -80 : -93 - break; - case CommandType::CUSTOM: - cmd_real.cmd_ = cmd.args_[0]; - cmd_real.args_[0] = cmd.args_[1]; - cmd_real.args_[1] = cmd.args_[2]; - cmd_real.args_[2] = cmd.args_[3]; + cmd_real.cmd_ = 0xB0; // -80 break; case CommandType::LIGHT_ON: - cmd_real.cmd_ = isV0 ? 0xB3 : 0xA5; // -77 : -91 + cmd_real.cmd_ = 0xB3; // -77 break; case CommandType::LIGHT_OFF: - cmd_real.cmd_ = isV0 ? 0xB2 : 0xA6; // -78 : -90 - break; - case CommandType::LIGHT_WCOLOR: - if (isV0) { - cmd_real.cmd_ = 0; // no corresponding command found, maybe -92 ? - } else { - cmd_real.cmd_ = 0xA8; // -88 - cmd_real.args_[0] = (250 * (float)cmd.args_[1]) / 255; - cmd_real.args_[1] = (250 * (float)cmd.args_[0]) / 255; - } + cmd_real.cmd_ = 0xB2; // -78 break; case CommandType::LIGHT_DIM: - if(isV0) { + { cmd_real.cmd_ = 0xB5; // -75 // app software: int i in between 0 -> 1000 // (byte) ((0xFF0000 & i) >> 16), (byte) ((0x00FF00 & i) >> 8), (byte) (i & 0x0000FF) uint16_t argBy4 = (1000 * (float)cmd.args_[0]) / 255; // from 0..255 -> 0..1000 cmd_real.args_[1] = ((argBy4 & 0xFF00) >> 8); cmd_real.args_[2] = (argBy4 & 0x00FF); - } else { - cmd_real.cmd_ = 0xAD; // -83 - // app software: value in between 0 -> 250 - cmd_real.args_[0] = (250 * (float)cmd.args_[0]) / 255; } break; case CommandType::LIGHT_CCT: - if(isV0) { + { cmd_real.cmd_ = 0xB7; // -73 // app software: int i in between 0 -> 1000 // (byte) ((0xFF0000 & i) >> 16), (byte) ((0x00FF00 & i) >> 8), (byte) (i & 0x0000FF) uint16_t argBy4 = (1000 * (float)cmd.args_[0]) / 255; cmd_real.args_[1] = ((argBy4 & 0xFF00) >> 8); cmd_real.args_[2] = (argBy4 & 0x00FF); - } else { - cmd_real.cmd_ = 0xAE; // -82 - // app software: value in between 0 -> 250 - cmd_real.args_[0] = (250 * (float)cmd.args_[0]) / 255; } break; case CommandType::LIGHT_SEC_ON: - if(isV0) { - cmd_real.cmd_ = 0xA6; // -90 - cmd_real.args_[0] = 1; - } else { - cmd_real.cmd_ = 0xAF; // -81 - } + cmd_real.cmd_ = 0xA6; // -90 + cmd_real.args_[0] = 1; break; case CommandType::LIGHT_SEC_OFF: - if(isV0) { - cmd_real.cmd_ = 0xA6; // -90 - cmd_real.args_[0] = 2; - } else { - cmd_real.cmd_ = 0xB0; // -80 - } + cmd_real.cmd_ = 0xA6; // -90 + cmd_real.args_[0] = 2; break; - case CommandType::FAN_ON: - cmd_real.cmd_ = isV2 ? 0xD2 : 0; // -47 + default: break; - case CommandType::FAN_OFF: - cmd_real.cmd_ = isV2 ? 0xD3 : 0; // -46 + } + std::vector< Command > cmds; + if(cmd_real.cmd_ != 0x00) { + cmds.emplace_back(cmd_real); + } + return cmds; +} + +bool ZhijiaEncoderV0::decode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) { + this->whiten(buf, this->len_, 0x37); + this->whiten(buf, this->len_, 0x7F); + + data_map_t * data = (data_map_t *) buf; + uint16_t crc16 = this->crc16(buf, ADDR_LEN + TXDATA_LEN); + ENSURE_EQ(crc16, data->crc16, "Decoded KO (CRC)"); + + uint8_t addr[ADDR_LEN]; + this->reverse_all(buf, ADDR_LEN); + std::reverse_copy(data->addr, data->addr + ADDR_LEN, addr); + ENSURE_EQ(std::equal(addr, addr + ADDR_LEN, MAC), true, "Decoded KO (MAC)"); + + cont.tx_count_ = data->txdata[0] ^ data->txdata[6]; + cmd.args_[0] = cont.tx_count_ ^ data->txdata[7]; + uint8_t pivot = data->txdata[1] ^ cmd.args_[0]; + uint8_t uuid[UUID_LEN]; + uuid[0] = pivot ^ data->txdata[0]; + uuid[1] = pivot ^ data->txdata[5]; + cont.id_ = this->uuid_to_id(uuid, UUID_LEN); + cont.index_ = pivot ^ data->txdata[2]; + cmd.cmd_ = pivot ^ data->txdata[4]; + cmd.args_[1] = pivot ^ data->txdata[3]; + cmd.args_[2] = uuid[0] ^ data->txdata[6]; + + return true; +} + +void ZhijiaEncoderV0::encode(uint8_t* buf, Command &cmd_real, ControllerParam_t & cont) { + unsigned char uuid[UUID_LEN] = {0}; + this->id_to_uuid(uuid, cont.id_, UUID_LEN); + + data_map_t * data = (data_map_t *) buf; + std::reverse_copy(MAC, MAC + ADDR_LEN, data->addr); + this->reverse_all(data->addr, ADDR_LEN); + + uint8_t pivot = cmd_real.args_[2] ^ cont.tx_count_; + data->txdata[0] = pivot ^ uuid[0]; + data->txdata[1] = pivot ^ cmd_real.args_[0]; + data->txdata[2] = pivot ^ cont.index_; + data->txdata[3] = pivot ^ cmd_real.args_[1]; + data->txdata[4] = pivot ^ cmd_real.cmd_; + data->txdata[5] = pivot ^ uuid[1]; + data->txdata[6] = cmd_real.args_[2] ^ uuid[0]; + data->txdata[7] = cmd_real.args_[0] ^ cont.tx_count_; + + data->crc16 = this->crc16(buf, ADDR_LEN + TXDATA_LEN); + this->whiten(buf, this->len_, 0x7F); + this->whiten(buf, this->len_, 0x37); +} + +std::vector< Command > ZhijiaEncoderV1::translate(const Command & cmd, const ControllerParam_t & cont) { + Command cmd_real(cmd.main_cmd_); + switch(cmd.main_cmd_) + { + case CommandType::PAIR: + cmd_real.cmd_ = 0xA2; // -94 break; - case CommandType::FAN_SPEED: - { - uint8_t level = (cmd.args_[1] == 3) ? 2 * cmd.args_[0] : cmd.args_[0]; - cmd_real.cmd_ = isV2 ? 0xDB + level : 0; // -37 + speed(1..6) => -36 -> -31 - } + case CommandType::UNPAIR: + cmd_real.cmd_ = 0xA3; // -93 + break; + case CommandType::LIGHT_ON: + cmd_real.cmd_ = 0xA5; // -91 + break; + case CommandType::LIGHT_OFF: + cmd_real.cmd_ = 0xA6; // -90 + break; + case CommandType::LIGHT_WCOLOR: + cmd_real.cmd_ = 0xA8; // -88 + cmd_real.args_[0] = (250 * (float)cmd.args_[1]) / 255; + cmd_real.args_[1] = (250 * (float)cmd.args_[0]) / 255; + break; + case CommandType::LIGHT_DIM: + cmd_real.cmd_ = 0xAD; // -83 + // app software: value in between 0 -> 250 + cmd_real.args_[0] = (250 * (float)cmd.args_[0]) / 255; + break; + case CommandType::LIGHT_CCT: + cmd_real.cmd_ = 0xAE; // -82 + // app software: value in between 0 -> 250 + cmd_real.args_[0] = (250 * (float)cmd.args_[0]) / 255; + break; + case CommandType::LIGHT_SEC_ON: + cmd_real.cmd_ = 0xAF; // -81 + break; + case CommandType::LIGHT_SEC_OFF: + cmd_real.cmd_ = 0xB0; // -80 break; default: break; } - return cmd_real; + std::vector< Command > cmds; + if(cmd_real.cmd_ != 0x00) { + cmds.emplace_back(cmd_real); + } + return cmds; } -/* - private byte[] mMAC = {0x19, 0x01, 0x10}; - private byte[] mMAC1 = {0x19, 0x01, 0x10, 0xAA}; - private byte[] mUUID = {App.getAndroidId()[0], App.getAndroidId()[1]}; - private byte[] mUUID1 = App.getAndroidId(); - private byte[] mUID = {0x19, 0x01, 0x10}; - - private byte[] getAdvData(byte b, byte[] bArr) { - byte[] msc16 = MSC16.msc16(this.mMAC, this.mUUID, (byte) getGroupId(), b, (byte) getSN(), bArr); - Log.d(TAG, "Began: " + StringUtils.bytes2HexString(msc16)); - return msc16; - } - - private byte[] getAdvData1(byte b, byte[] bArr) { - byte[] msc26 = MSC26.msc26(this.mMAC1, this.mUUID1, this.mUID, (byte) getGroupId1(), b, (byte) getSN1(), bArr); - Log.d(TAG, "Began: " + StringUtils.bytes2HexString(msc26)); - return msc26; - } - - private byte[] getAdvData2(byte b, byte[] bArr) { - byte[] msc26 = MSC26A.msc26(this.mMAC1, this.mUUID1, this.mUID, (byte) getGroupId1(), b, (byte) getSN1(), bArr, this.mMAC1, this.mUUID1, this.mUID); - Log.d(TAG, "Began: " + StringUtils.bytes2HexString(msc26)); - return msc26; - } -*/ - -uint8_t ZhijiaEncoder::get_adv_data(std::vector< BleAdvParam > & params, Command &cmd) { - params.emplace_back(); - BleAdvParam & param = params.back(); - - ZhijiaArgs_t cmd_real = this->translate_cmd(cmd); - - unsigned char uuid[3] = {0}; - uuid[0] = (cmd.id_ & 0xFF0000) >> 16; - uuid[1] = (cmd.id_ & 0x00FF00) >> 8; - uuid[2] = (cmd.id_ & 0x0000FF); - ESP_LOGD(TAG, "%s - UUID: '0x%02X%02X%02X', tx: %d, Command: '0x%02X', Args: [%d,%d,%d]", this->id_.c_str(), - uuid[0], uuid[1], uuid[2], cmd.tx_count_, cmd_real.cmd_, cmd_real.args_[0], cmd_real.args_[1], cmd_real.args_[2]); - switch(this->variant_) +bool ZhijiaEncoderV1::decode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) { + this->whiten(buf, this->len_, 0x37); + + data_map_t * data = (data_map_t *) buf; + uint16_t crc16 = this->crc16(buf, ADDR_LEN + TXDATA_LEN); + ENSURE_EQ(crc16, data->crc16, "Decoded KO (CRC)"); + + uint8_t addr[ADDR_LEN]; + this->reverse_all(data->addr, ADDR_LEN); + std::reverse_copy(data->addr, data->addr + ADDR_LEN, addr); + ENSURE_EQ(std::equal(addr, addr + ADDR_LEN, addr), true, "Decoded KO (MAC)"); + + ENSURE_EQ(data->txdata[7], data->txdata[14], "Decoded KO (Dupe 7/14)"); + ENSURE_EQ(data->txdata[8], data->txdata[11], "Decoded KO (Dupe 8/11)"); + ENSURE_EQ(data->txdata[11], data->txdata[16], "Decoded KO (Dupe 11/16)"); + + uint8_t pivot = data->txdata[16]; + uint8_t uid[UID_LEN]; + uid[0] = data->txdata[7] ^ pivot; + uid[1] = data->txdata[10] ^ pivot; + uid[2] = data->txdata[4] ^ data->txdata[13]; + ENSURE_EQ(std::equal(uid, uid + UID_LEN, UID), true, "Decoded KO (UID)"); + + cmd.cmd_ = (CommandType) (data->txdata[9] ^ pivot); + cmd.args_[0] = data->txdata[0] ^ pivot; + cmd.args_[1] = data->txdata[3] ^ pivot; + cmd.args_[2] = data->txdata[5] ^ pivot; + cont.tx_count_ = data->txdata[4] ^ pivot; + cont.index_ = data->txdata[6] ^ pivot; + uint8_t uuid[UUID_LEN]; + uuid[0] = data->txdata[2] ^ pivot; + uuid[1] = data->txdata[2] ^ data->txdata[12]; + uuid[2] = data->txdata[9] ^ data->txdata[15]; + cont.id_ = this->uuid_to_id(uuid, UUID_LEN); + + uint8_t key = pivot ^ cmd.args_[0] ^ cmd.args_[1] ^ cmd.args_[2] ^ uid[0] ^ uid[1] ^ uid[2]; + key ^= uuid[0] ^ uuid[1] ^ uuid[2] ^ cont.tx_count_ ^ cont.index_ ^ cmd.cmd_; + ENSURE_EQ(key, data->txdata[1], "Decoded KO (Key)"); + + uint8_t re_pivot = uuid[1] ^ uuid[2] ^ uid[2]; + re_pivot ^= ((re_pivot & 1) - 1); + ENSURE_EQ(pivot, re_pivot, "Decoded KO (Pivot)"); + + return true; +} + +void ZhijiaEncoderV1::encode(uint8_t* buf, Command &cmd_real, ControllerParam_t & cont) { + unsigned char uuid[UUID_LEN] = {0}; + this->id_to_uuid(uuid, cont.id_, UUID_LEN); + + data_map_t * data = (data_map_t *) buf; + std::reverse_copy(MAC, MAC + ADDR_LEN, data->addr); + this->reverse_all(data->addr, ADDR_LEN); + + uint8_t pivot = uuid[1] ^ uuid[2] ^ UID[2]; + pivot ^= (pivot & 1) - 1; + + uint8_t key = cmd_real.args_[0] ^ cmd_real.args_[1] ^ cmd_real.args_[2]; + key ^= uuid[0] ^ uuid[1] ^ uuid[2] ^ cont.tx_count_ ^ cont.index_ ^ cmd_real.cmd_ ^ UID[0] ^ UID[1] ^ UID[2]; + + data->txdata[0] = pivot ^ cmd_real.args_[0]; + data->txdata[1] = pivot ^ key; + data->txdata[2] = pivot ^ uuid[0]; + data->txdata[3] = pivot ^ cmd_real.args_[1]; + data->txdata[4] = pivot ^ cont.tx_count_; + data->txdata[5] = pivot ^ cmd_real.args_[2]; + data->txdata[6] = pivot ^ cont.index_; + data->txdata[7] = pivot ^ UID[0]; + data->txdata[8] = pivot; + data->txdata[9] = pivot ^ cmd_real.cmd_; + data->txdata[10] = pivot ^ UID[1]; + data->txdata[11] = pivot; + data->txdata[12] = uuid[1] ^ data->txdata[2]; + data->txdata[13] = UID[2] ^ data->txdata[4]; + data->txdata[14] = data->txdata[7]; + data->txdata[15] = uuid[2] ^ data->txdata[9]; + data->txdata[16] = pivot; + + data->crc16 = this->crc16(buf, ADDR_LEN + TXDATA_LEN); + this->whiten(buf, this->len_, 0x37); +} + +std::vector< Command > ZhijiaEncoderV2::translate(const Command & cmd, const ControllerParam_t & cont) { + auto cmds = ZhijiaEncoderV1::translate(cmd, cont); + if (!cmds.empty()) return cmds; + + Command cmd_real(cmd.main_cmd_); + switch(cmd.main_cmd_) { - case VARIANT_V0: - msc16_msc26::aes16Encrypt16(MAC, uuid, 0xFF, cmd_real.cmd_, cmd.tx_count_, cmd_real.args_, param.buf_); - param.len_ = 16; + case CommandType::FAN_ON: + cmd_real.cmd_ = 0xD2; // -47 break; - case VARIANT_V1: - msc16_msc26::aes26Encrypt(MAC, uuid, MAC, 0x7F, cmd_real.cmd_, cmd.tx_count_, cmd_real.args_, param.buf_); - param.len_ = 26; + case CommandType::FAN_OFF: + cmd_real.cmd_ = 0xD3; // -46 break; - case VARIANT_V2: - msc26a::stage1(cmd_real.args_, cmd_real.cmd_, cmd.tx_count_, 0x7F, MAC, uuid, param.buf_); - msc26a::stage2(param.buf_, 0x18, 0x2); - msc26a::stage3(param.buf_, 0x1a, param.buf_); - param.len_ = 26; + case CommandType::FAN_SPEED: + // -37 + speed(1..6) => -36 -> -31 + cmd_real.cmd_ = 0xDB + ((cmd.args_[1] == 3) ? 2 * cmd.args_[0] : cmd.args_[0]); break; default: - ESP_LOGW(TAG, "get_adv_data called with invalid variant %d", this->variant_); break; } - return 1; + if(cmd_real.cmd_ != 0x00) { + cmds.emplace_back(cmd_real); + } + return cmds; } -void ZhijiaEncoder::register_encoders(BleAdvHandler * handler, const std::string & encoding) { - BleAdvMultiEncoder * zhijia_all = new BleAdvMultiEncoder("Zhi Jia - All", encoding); - handler->add_encoder(zhijia_all); - BleAdvEncoder * zhijia_v0 = new ZhijiaEncoder("Zhi Jia - v0", encoding, ZhijiaVariant::VARIANT_V0); - handler->add_encoder(zhijia_v0); - zhijia_all->add_encoder(zhijia_v0); - BleAdvEncoder * zhijia_v1 = new ZhijiaEncoder("Zhi Jia - v1", encoding, ZhijiaVariant::VARIANT_V1); - handler->add_encoder(zhijia_v1); - zhijia_all->add_encoder(zhijia_v1); - BleAdvEncoder * zhijia_v2 = new ZhijiaEncoder("Zhi Jia - v2", encoding, ZhijiaVariant::VARIANT_V2); - handler->add_encoder(zhijia_v2); - zhijia_all->add_encoder(zhijia_v2); +bool ZhijiaEncoderV2::decode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) { + this->whiten(buf, this->len_, 0x6F); + this->whiten(buf, this->len_ - 2, 0xD3); + + data_map_t * data = (data_map_t *) buf; + for (size_t i = 0; i < TXDATA_LEN; ++i) { + data->txdata[i] ^= data->pivot; + } + + cont.tx_count_ = data->txdata[4]; + cont.index_ = data->txdata[6]; + cmd.cmd_ = (CommandType) (data->txdata[9]); + uint8_t addr[ADDR_LEN]; + addr[0] = data->txdata[7]; + addr[1] = data->txdata[10]; + addr[2] = data->txdata[13] ^ cont.tx_count_; + cmd.args_[0] = data->txdata[0]; + cmd.args_[1] = data->txdata[3]; + cmd.args_[2] = data->txdata[5]; + uint8_t uuid[UUID_LEN]{0}; + uuid[0] = data->txdata[2]; + uuid[1] = data->txdata[12] ^ uuid[0]; + uuid[2] = data->txdata[15] ^ cmd.cmd_; + cont.id_ = this->uuid_to_id(uuid, UUID_LEN); + + ENSURE_EQ(std::equal(addr, addr + ADDR_LEN, MAC), true, "Decoded KO (MAC)"); + + uint8_t key = addr[0] ^ addr[1] ^ addr[2] ^ cont.index_ ^ cont.tx_count_ ^ cmd.args_[0] ^ cmd.args_[1] ^ cmd.args_[2] ^ uuid[0] ^ uuid[1] ^ uuid[2]; + ENSURE_EQ(key, data->txdata[1], "Decoded KO (Key)"); + + uint8_t re_pivot = uuid[0] ^ uuid[1] ^ uuid[2] ^ cont.tx_count_ ^ cmd.args_[1] ^ addr[0] ^ addr[2] ^ cmd.cmd_; + re_pivot = ((re_pivot & 1) - 1) ^ re_pivot; + ENSURE_EQ(data->pivot, re_pivot, "Decoded KO (Pivot)"); + + ENSURE_EQ(data->txdata[8], uuid[0] ^ cont.tx_count_ ^ cmd.args_[1] ^ addr[0], "Decoded KO (txdata 8)"); + ENSURE_EQ(data->txdata[11], 0x00, "Decoded KO (txdata 11)"); + ENSURE_EQ(data->txdata[14], uuid[0] ^ cont.tx_count_ ^ cmd.args_[1] ^ cmd.cmd_, "Decoded KO (txdata 14)"); + + return true; +} + +void ZhijiaEncoderV2::encode(uint8_t* buf, Command &cmd_real, ControllerParam_t & cont) { + unsigned char uuid[UUID_LEN] = {0}; + this->id_to_uuid(uuid, cont.id_, UUID_LEN); + + data_map_t * data = (data_map_t *) buf; + uint8_t key = MAC[0] ^ MAC[1] ^ MAC[2] ^ cont.index_ ^ cont.tx_count_; + key ^= cmd_real.args_[0] ^ cmd_real.args_[1] ^ cmd_real.args_[2] ^ uuid[0] ^ uuid[1] ^ uuid[2]; + + data->pivot = uuid[0] ^ uuid[1] ^ uuid[2] ^ cont.tx_count_ ^ cmd_real.args_[1] ^ MAC[0] ^ MAC[2] ^ cmd_real.cmd_; + data->pivot = ((data->pivot & 1) - 1) ^ data->pivot; + + data->txdata[0] = cmd_real.args_[0]; + data->txdata[1] = key; + data->txdata[2] = uuid[0]; + data->txdata[3] = cmd_real.args_[1]; + data->txdata[4] = cont.tx_count_; + data->txdata[5] = cmd_real.args_[2]; + data->txdata[6] = cont.index_; + data->txdata[7] = MAC[0]; + data->txdata[8] = uuid[0] ^ cont.tx_count_ ^ cmd_real.args_[1] ^ MAC[0]; + data->txdata[9] = cmd_real.cmd_; + data->txdata[10] = MAC[1]; + data->txdata[11] = 0x00; + data->txdata[12] = uuid[1] ^ uuid[0]; + data->txdata[13] = MAC[2] ^ cont.tx_count_; + data->txdata[14] = uuid[0] ^ cont.tx_count_ ^ cmd_real.args_[1] ^ cmd_real.cmd_; + data->txdata[15] = uuid[2] ^ cmd_real.cmd_; + + for (size_t i = 0; i < TXDATA_LEN; ++i) { + data->txdata[i] ^= data->pivot; + } + + this->whiten(buf, this->len_ - 2, 0xD3); + this->whiten(buf, this->len_, 0x6F); } } // namespace bleadvcontroller diff --git a/components/ble_adv_controller/zhijia.h b/components/ble_adv_controller/zhijia.h index db3b9b5..379a8cb 100644 --- a/components/ble_adv_controller/zhijia.h +++ b/components/ble_adv_controller/zhijia.h @@ -5,26 +5,81 @@ namespace esphome { namespace bleadvcontroller { -enum ZhijiaVariant : int {VARIANT_V0, VARIANT_V1, VARIANT_V2}; +class ZhijiaEncoder: public BleAdvEncoder +{ +public: + ZhijiaEncoder(const std::string & encoding, const std::string & variant): BleAdvEncoder(encoding, variant) {} + +protected: + uint16_t crc16(uint8_t* buf, size_t len, uint16_t seed = 0); + uint32_t uuid_to_id(uint8_t * uuid, size_t len); + void id_to_uuid(uint8_t * uuid, uint32_t id, size_t len); +}; -typedef struct { - uint8_t cmd_{0}; - uint8_t args_[3]{0}; -} ZhijiaArgs_t; +class ZhijiaEncoderV0: public ZhijiaEncoder +{ +public: + ZhijiaEncoderV0(const std::string & encoding, const std::string & variant): + ZhijiaEncoder(encoding, variant) { { this->len_ = sizeof(data_map_t); }} + +protected: + static constexpr size_t UUID_LEN = 2; + static constexpr size_t ADDR_LEN = 3; + static constexpr size_t TXDATA_LEN = 8; + struct data_map_t { + uint8_t addr[ADDR_LEN]; + uint8_t txdata[TXDATA_LEN]; + uint16_t crc16; + }__attribute__((packed, aligned(1))); -class ZhijiaEncoder: public BleAdvEncoder + virtual std::vector< Command > translate(const Command & cmd, const ControllerParam_t & cont) override; + virtual bool decode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) override; + virtual void encode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) override; +}; + +class ZhijiaEncoderV1: public ZhijiaEncoder { public: - ZhijiaEncoder(const std::string & name, const std::string & encoding, ZhijiaVariant variant): - BleAdvEncoder(name, encoding, variant) {} - virtual bool is_supported(const Command &cmd) override; - virtual uint8_t get_adv_data(std::vector< BleAdvParam > & params, Command &cmd) override; + ZhijiaEncoderV1(const std::string & encoding, const std::string & variant): + ZhijiaEncoder(encoding, variant) { this->len_ = sizeof(data_map_t); } - static void register_encoders(BleAdvHandler * handler, const std::string & encoding); +protected: + static constexpr size_t UUID_LEN = 3; + static constexpr size_t ADDR_LEN = 4; + static constexpr size_t TXDATA_LEN = 17; + struct data_map_t { + uint8_t addr[ADDR_LEN]; + uint8_t txdata[TXDATA_LEN]; + uint16_t crc16; + }__attribute__((packed, aligned(1))); + + virtual std::vector< Command > translate(const Command & cmd, const ControllerParam_t & cont) override; + virtual bool decode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) override; + virtual void encode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) override; +}; +class ZhijiaEncoderV2: public ZhijiaEncoderV1 +{ +public: + ZhijiaEncoderV2(const std::string & encoding, const std::string & variant): + ZhijiaEncoderV1(encoding, variant) { this->len_ = sizeof(data_map_t); } + protected: - virtual ZhijiaArgs_t translate_cmd(const Command &cmd); + static constexpr size_t UUID_LEN = 3; + static constexpr size_t ADDR_LEN = 3; + static constexpr size_t TXDATA_LEN = 16; + static constexpr size_t SPARE_LEN = 7; + struct data_map_t { + uint8_t txdata[TXDATA_LEN]; + uint8_t pivot; + uint8_t spare[SPARE_LEN]; + }__attribute__((packed, aligned(1))); + + virtual std::vector< Command > translate(const Command & cmd, const ControllerParam_t & cont) override; + virtual bool decode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) override; + virtual void encode(uint8_t* buf, Command &cmd, ControllerParam_t & cont) override; }; + } //namespace bleadvcontroller } //namespace esphome