├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── am43-flash.png ├── examples └── MQTTBlinds │ ├── MQTTBlinds.ino │ └── config.h ├── library.properties ├── platformio.ini └── src ├── AM43Client.cpp └── AM43Client.h /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.5.0] - 2021-02-11 4 | - Support NimBLE Bluetooth stack, possibly more stable. 5 | 6 | ## [0.4.0] - 2020-08-17 7 | - MQTT example adds new switch to enable/disable BLE connections. 8 | 9 | ## [0.3.1] - 2020-07-31 10 | - Fix light level 11 | 12 | ## [0.3.0] - 2020-07-17 13 | 14 | - Fixes to MQTT topic prefixes. 15 | - Wrap Arduino OTA around a #define. 16 | - Gracefully restrict connections to three devices. 17 | 18 | ## [0.2.0] - 2020-07-02 19 | 20 | - Support Home Assistant [auto-discovery](https://www.home-assistant.io/docs/mqtt/discovery/). 21 | - All MAC addresses in topic, etc are stripped of colons. 22 | - Tweak connection behaviour. 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Ben Buxton 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AM43 blind controller library for ESP32 2 | 3 | A library and example sketch that allows you to control AM43 style blind controllers 4 | using an ESP32 device's built-in Bluetooth radio. 5 | 6 | Included is an example sketch to perform control and monitoring via MQTT, including 7 | auto-discovery for Home Assistant. 8 | 9 | These blind and rollershade controllers are sold under various names, including 10 | Zemismart, A-OK and all use the "Blind Engine" app. 11 | 12 | Feel free to send PRs. 13 | 14 | Looking for native ESPHome support? [See this repo](https://github.com/buxtronix/esphome-am43) 15 | 16 | ## MQTTBlinds example overview 17 | 18 | This sketch will scan for and auto-connect to any AM43 devices in range, then provide 19 | MQTT topics to control and get status from them. 20 | 21 | For Home Assisant, auto-discovery is supported so connected AM43 devices will 22 | show up as entities. See below for simple setup. 23 | 24 | Note that the Arduino ESP32 integration only supports connecting to a maximum of 3 25 | bluetooth devices. See below if you have more. 26 | 27 | The following MQTT topis are published to: 28 | 29 | | Topic | Description | Values | 30 | | ----- | ----------- | ------ | 31 | | am43/<device>/available | Connection status of the blind controller | Either 'offline' or 'online' | 32 | | am43/<device>/position | The current blind position | between 0 and 100 | 33 | | am43/<device>/battery | The current battery level | between 0 and 100 | 34 | | am43/<device>/light | The current light level | between 0 and 100 | 35 | | am43/enabled | Whether the BLE client is enabled | Either "on" or "off" | 36 | | am43/LWT | MQTT connection status | Either 'Online' or 'Offline' | 37 | 38 | The following MQTT topics are subscribed to: 39 | 40 | | Topic | Description | Values | 41 | | ----- | ----------- | ------ | 42 | | am43/<device>/set | Set the blind position (would be deprecated in future release) | 'OPEN', 'STOP' or 'CLOSE' | 43 | | am43/<device>/set_position | Set the blind position or % position | 'OPEN', 'UP', 'STOP', 'CLOSE', 'DOWN' or a number between 0 and 100. | 44 | | am43/<device>/status | Get device status on demand (position/light/battery) | Ignored. | 45 | | am43/enable | Enable = BLE connections AlwaysOn mode, Disable = BLE OnDemand Connections mode | 'off' or 'on'. 46 | | am43/restart | Reboot this service | Ignored. 47 | | am43/cmnd/# | Only on bleOnDemand mode. This topic process commands with on demand connections. ex. am43/cmnd/<device>/set ; am43/cmnd/<device>/set_position | Depends on every command 48 | 49 | <device> is the bluetooth mac address of the device, eg 02:69:32:f0:c5:1d 50 | 51 | If you enable AM43_USE_NAME_FOR_TOPIC in config.h, then the device name configured is used 52 | in the topic instead of the mac. 53 | 54 | For the position set commands, you can use name 'all' to change all devices. 55 | 56 | ## Getting started 57 | 58 | Download this archive and unzip it to your Arduino installation's *libraries* 59 | folder, it should extract as its own sub-folder. Then (re)start the IDE. 60 | 61 | Open the example Sketch under *File -> Examples -> AM43Client -> MQTTBlinds* 62 | 63 | In the file tab *config.h*, configure your Wifi credentials and your MQTT server 64 | details. If your AM43 devices are not using the default pin (8888) also set it 65 | there. 66 | 67 | ### Components needed 68 | 69 | - ESP-32 --> v1.0.6 (https://github.com/espressif/arduino-esp32) 70 | - NimBLE-Arduino --> v1.2.0 (https://github.com/h2zero/NimBLE-Arduino) 71 | 72 | ### OnDemand Mode 73 | 74 | This mode works with on demand connections to AM43 devices, so when receives a command, 75 | it's start on demand connection to device, send the command requested and wait 60 seconds 76 | before disconnect from devices. 77 | 78 | To enable this mode, edit the config.h file and uncomment the line ``#define AM43_ONDEMAND`` 79 | 80 | Is strongly recommended, to reduce the update interval from 5000 to 20000, to have enough 81 | time to get all devices updates (battery/light/position). To change this, edit the 82 | AM43Client.h file and modify line from ``#define AM43_UPDATE_INTERVAL 30000`` to ``#define AM43_UPDATE_INTERVAL 5000``. 83 | 84 | ### Installation with NimBLE 85 | 86 | As of Version 0.5.0, the library uses the [NimBLE bluetooth stack](https://github.com/h2zero/NimBLE-Arduino/), 87 | rather than the legacy Arduino stack. This is signficantly smaller in both flash and 88 | RAM usage, so should increase stability. You can download the NimBLE library directly 89 | within the Arduino Library Manager (*Sketch->Include Library->Manage Libraries*). 90 | 91 | ### Legacy (non-NimBLE only) installation requirements 92 | 93 | It's recommended to use the NimBLE library, but if you are having issues with 94 | this, you can use the legacy stack (please also raise a bug). 95 | 96 | To have the AM43 library use the legacy BLE stack, edit the file *AM43Client.h* in your 97 | installation and comment out the line ```#define USE_NIMBLE``` near the top. 98 | 99 | The sketch takes up a lot of space on flash thanks to the use of Wifi and BLE - you 100 | will need to increase the available space by changing the board options in the 101 | Arduino IDE. Once you have selected your ESP32 board in *Tools*, select 102 | *Tools -> Partition Scheme -> Minimal SPIFFS* to enable the larger program space. 103 | 104 | #### Patch BLE library 105 | 106 | Whilst developing this, I found bugs in the ESP32 Arduino BLE libraries 107 | which cause significant instability issues. A patch will been submitted to 108 | the BLE maintainers, though if it's not yet in your release, you will need 109 | to make the following changes, which result in a massive stability gain: 110 | 111 | Find BLEClient.cpp in your installation, and make the following changes. 112 | 113 | ``` 114 | // Search for the following block, around line 180 and add the line. 115 | 116 | case ESP_GATTC_DISCONNECT_EVT: { 117 | if (evtParam->disconnect.conn_id != getConnId()) break; // <- ADD THIS LINE 118 | // If we receive a disconnect event, set the class flag that indicates that we are 119 | // no longer connected. 120 | m_isConnected = false; 121 | 122 | // Also two changes around line 238. 123 | 124 | case ESP_GATTC_CONNECT_EVT: { 125 | if (evtParam->connect.conn_id != getConnId()) break; // <- ADD THIS LINE 126 | BLEDevice::updatePeerDevice(this, true, m_appId); 127 | // ^^^^^^^ CHANGE THIS PARAMETER. 128 | 129 | ``` 130 | 131 | Next, find BLEScan.cpp, and add the following on line ~28 in the Constructor: 132 | 133 | ``` 134 | m_scan_params.scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE; 135 | ``` 136 | 137 | Finally, you need to find the file esp32-hal-bt.c and make the following changes: 138 | 139 | ``` 140 | // Change the mode to BLE only on the following line: 141 | 142 | bool btStart(){ 143 | esp_bt_controller_config_t cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); 144 | cfg.mode = ESP_BT_MODE_BLE; // <- ADD THIS LINE 145 | 146 | // Then around 10 lines below this, replace these lines: 147 | // if (esp_bt_controller_enable(BT_MODE)) { 148 | // log_e("BT Enable failed"); 149 | // return false; 150 | // } 151 | // with: 152 | auto err_p = esp_bt_controller_enable(ESP_BT_MODE_BLE); 153 | if (err_p) { 154 | log_e("BT Enable failed err=%d", err_p); 155 | return false; 156 | } 157 | ``` 158 | 159 | ## Testing 160 | 161 | Note that only one BLE client can connect to the AM43 at a time. So you cannot 162 | have multiple ESP controllers or have the device app (Blind Engine) running at 163 | the same time. 164 | 165 | You should perform initial setup of your AM43 devices with the native app 166 | before running this gateway. This includes setting the device name and motor 167 | direction. 168 | 169 | Lots of information is printed over the serial console. Connect to your ESP32 device 170 | at 115200 baud and there should be plenty of chatter. 171 | 172 | Once you have your device ready, you can monitor and control it using an MQTT 173 | client. For example, using mosquitto_sub, you can watch activity with: 174 | 175 | ``` 176 | $ mosquitto_sub -h -p -v -t am43/# 177 | am43/LWT Online 178 | am43/026932f2c41d/available online 179 | am43/026932f2c41d/position 0 180 | am43/026932f2c41d/battery 70 181 | am43/026932f2c41d/light 49 182 | am43/024d45f05b2e/available online 183 | am43/024d45f05b2e/battery 100 184 | am43/024d45f05b2e/position 0 185 | am43/024d45f05b2e/light 68 186 | ``` 187 | 188 | It's trivial to then control the shades similarly: 189 | 190 | ``` 191 | $ mosquitto_pub -h -p -t am43/026932f2c41d/set -m OPEN 192 | ``` 193 | 194 | You can also control all in unison: 195 | 196 | ``` 197 | $ mosquitto_pub -h -p -t am43/all/set -m CLOSE 198 | ``` 199 | 200 | ## Multiple devices 201 | 202 | If you have up to three AM43 devices connecting to the one ESP32, then it should 203 | just work out of the box. 204 | 205 | However if you have more devices, or just want to limit which AM43's are 206 | controlled by a given ESP32, then you'll need to set up as follows. 207 | 208 | ### Multiple ESP32 proxy devices 209 | 210 | This might be for a setup where you have AM43s in different parts of the house 211 | and you'd like to ensure each ESP32 only controls specific motor(s) such 212 | as the closest. 213 | 214 | For each ESP32, you should edit *config.h* and add the MAC address of the 215 | desired AM43(s) to DEVICE_ALLOWLIST. Separate each address by a comma. You 216 | can find the addresses either in the Arduino serial console or the Blind 217 | Engine app. Then that ESP will not connect to any other AM43. 218 | 219 | ### More than three AM43's 220 | 221 | Arduino BLE can only connect to at most three bluetooth devices (unless 222 | you custom compile it). If you have more devices you will need to have 223 | additional ESP32 proxies with this code, each setup with a separate allow 224 | list as above. 225 | 226 | ## Home Assistant configuration 227 | 228 | ### Native with ESPHome 229 | 230 | There is now a repo which allows you to integrate AM43 support directly 231 | into ESPHome without downloading or installing anything beyond your standard 232 | ESPHome installation. 233 | 234 | See [https://github.com/buxtronix/esphome-am43](https://github.com/buxtronix/esphome-am43) 235 | 236 | ### HA with auto-discovery 237 | 238 | Ref: [Home Assistant Auto Discovery](https://www.home-assistant.io/docs/mqtt/discovery/) 239 | 240 | If you have enabled auto-discovery in *config.h* as well as in Home Assistant, 241 | then once your blinds are setup and the sketch has connected, you will see them 242 | appear in the entity list under *Configuration->Entities* either by the device name 243 | or mac address. 244 | 245 | It's easy to add them to the Lovelace dashboard - create a new Entities Card and 246 | find the entities matching your device names (this is the name configured in the 247 | Blinds Engine app), or the MAC address. There will be three entities per AM43 248 | device - one cover and two sensors for the battery and light levels. 249 | 250 | There will also be a switch entity created, used to enable and disable the BLE 251 | connections. This can be useful to save battery power by only establishing the 252 | BLE connection when moving the cover. Note that it may take up to a minute to 253 | establish the connection so automations should add a delay between enabling the 254 | switch and sending a command. 255 | 256 | Example automation with only enabling BLE when changing the cover: 257 | 258 | ``` 259 | id: 'Sunrise blinds' 260 | trigger: 261 | - event: sunrise 262 | platform: sun 263 | action: 264 | - entity_id: switch.am43_ble 265 | service: switch.turn_on 266 | - delay: 00:01 267 | - entity_id: cover.bedroom 268 | service: cover.open_cover 269 | - delay: 00:02 270 | - entity_id: switch.am43_ble 271 | service: switch.turn_off 272 | ``` 273 | 274 | Check the *config.h* file for other related options. 275 | 276 | ### Without auto-discovery 277 | 278 | The MQTT topics are set to integrate natively with Home Assistant. Once both 279 | are talking to the MQTT server, add the following configuration for each 280 | blind: 281 | 282 | ``` 283 | cover: 284 | - platform: mqtt 285 | name: "Bedroom right" 286 | device_class: "shade" 287 | command_topic: "am43/026932f2c41d/set" 288 | position_topic: "am43/026932f2c41d/position" 289 | set_position_topic: "am43/026932f2c41d/set_position" 290 | availability_topic: "am43/026932f2c41d/available" 291 | # Devices dont always report 0, open might be 0, 1 or 2. 292 | position_open: 2 293 | # Devices dont always report 100, closed might be 99 or 100. 294 | position_closed: 99 295 | 296 | sensor: 297 | - platform: mqtt 298 | name: "Bedroom right blind battery" 299 | availability_topic: "am43/026932f2c41d/available" 300 | state_topic: "am43/026932f2c41d/battery" 301 | unit_of_measurement: "%" 302 | device_class: battery 303 | 304 | - platform: mqtt 305 | name: "Bedroom right blind light" 306 | state_topic: "am43/026932f2c41d/light" 307 | availability_topic: "am43/026932f2c41d/available" 308 | unit_of_measurement: "%" 309 | device_class: illuminance 310 | 311 | switch: 312 | - platform: mqtt 313 | name: "Bedroom BLE enable" 314 | command_topic: "am43/enable" 315 | state_topic: "am43/enabled" 316 | availability_topic: "am43/LWT" 317 | icon: "mdi:bluetooth" 318 | 319 | ``` 320 | 321 | ## OpenHAB v3 Configuration examples 322 | 323 | ### Things definition with OnDemand Mode support 324 | ``` 325 | Thing mqtt:topic:RollersController "Rollers Controller" (mqtt:broker:MQTTBroker) @ "am43-esp32-controller" [ availabilityTopic="am43/LWT", payloadNotAvailable="Offline",payloadAvailable="Online"] { 326 | Channels: 327 | Type string : reachable "Reachable" [ stateTopic="am43/LWT", transformationPatternOut="MAP:reachable.map" ] 328 | Type switch : allwaysOn "Allways ON mode" [ 329 | stateTopic="am43/enabled", 330 | commandTopic="am43/enable", 331 | ON="ON", 332 | OFF="OFF" 333 | ] 334 | 335 | Type string : roller1Reachable "roller1 Reachable" [ stateTopic="am43//available", transformationPatternOut="MAP:reachable.map"] 336 | Type rollershutter : roller1Control "roller1 Control" [ 337 | stateTopic="am43//position", 338 | commandTopic="am43/cmnd//set_position", 339 | UP="OPEN", 340 | DOWN="CLOSE", 341 | STOP="STOP" 342 | ] 343 | Type number : roller1Battery "roller1 Battery" [ stateTopic="am43//battery"] 344 | Type number : roller1Light "roller1 Light Sensor" [ stateTopic="am43//light"] 345 | 346 | Type string : roller2Reachable "roller2 Reachable" [ stateTopic="am43//available", transformationPatternOut="MAP:reachable.map"] 347 | Type rollershutter : roller2Control "roller2 Control" [ 348 | stateTopic="am43//position", 349 | commandTopic="am43/cmnd//set_position", 350 | UP="OPEN", 351 | DOWN="CLOSE", 352 | STOP="STOP" 353 | ] 354 | Type number : roller2Battery "roller2 Battery" [ stateTopic="am43//battery"] 355 | Type number : roller2Light "roller2 Light Sensor" [ stateTopic="am43//light"] 356 | } 357 | ``` 358 | 359 | ### Items definition 360 | ``` 361 | Group Equipment_Rollers "Rollers" (Location_Group) ["Blinds"] 362 | String ControllerState "Controller State : [%s]" (Equipment_Rollers) ["Status"] {channel="mqtt:topic:RollersController:reachable"} 363 | Switch ControllerBLEMode "AllwaysOn Mode" (Equipment_Rollers) [ "Switch" ] {channel="mqtt:topic:RollersController:allwaysOn"} 364 | 365 | String roller1State "roller1 State : [%s]" (Equipment_Rollers) ["Status"] {channel="mqtt:topic:RollersController:roller1Reachable"} 366 | Rollershutter roller1Position "roller1 Position [%d %%]" (Equipment_Rollers) [ "Setpoint" ] {channel="mqtt:topic:RollersController:roller1Control"} 367 | Number roller1Battery "roller1 Battery Level [%d %%]" (Equipment_Rollers) ["Measurement", "Energy"] {channel="mqtt:topic:RollersController:roller1Battery"} 368 | Number roller1LightLevel "roller1 Light Level [%d %%]" (Equipment_Rollers) ["Measurement", "Light"] {channel="mqtt:topic:RollersController:roller1Light"} 369 | 370 | String roller2State "roller2 State : [%s]" (Equipment_Rollers) ["Status"] {channel="mqtt:topic:RollersController:roller2Reachable"} 371 | Rollershutter roller2Position "roller2 Position [%d %%]" (Equipment_Rollers) [ "Setpoint" ] {channel="mqtt:topic:RollersController:roller2Control"} 372 | Number roller2Battery "roller2 Battery Level [%d %%]" (Equipment_Rollers) ["Measurement", "Energy"] {channel="mqtt:topic:RollersController:roller2Battery"} 373 | Number roller2LightLevel "roller2 Light Level [%d %%]" (Equipment_Rollers) ["Measurement", "Light"] {channel="mqtt:topic:RollersController:roller2Light"} 374 | 375 | ``` 376 | 377 | ### Transform definition 378 | 379 | reachable.map: 380 | ``` 381 | Online=ON 382 | Offline=OFF 383 | online=ON 384 | offline=OFF 385 | ``` 386 | 387 | ## Building with [PlatformIO](https://platformio.org/) 388 | 389 | Building with PlatformIO is simple, it will manage dependencies automatically. 390 | 391 | ### To build with PlatformIO 392 | 393 | 1. Copy examples/MQTTBlinds/MQTTBlinds.ino and config.h to the src/ directory. 394 | 395 | 2. Edit `config.h` 396 | 397 | 3. Compile and upload via USB 398 | ``` 399 | pio run -t upload 400 | ``` 401 | 402 | 4. Optionally monitor the serial port 403 | ``` 404 | pio run -t monitor 405 | ``` 406 | 407 | ## TODO 408 | 409 | - Consider more functionality such as device configuration. 410 | - Allow buttons on the ESP32 for control? 411 | - On-demand BLE connect to save AM43 device battery. 412 | 413 | ## Copyright 414 | 415 | Copyright 2020-2021, Ben Buxton. Licenced under the MIT licence, see LICENSE. 416 | -------------------------------------------------------------------------------- /am43-flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buxtronix/am43/667bd799e25a709fca24c5662dd59a801c85c491/am43-flash.png -------------------------------------------------------------------------------- /examples/MQTTBlinds/MQTTBlinds.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * A proxy that allows you to control AM43 style blind controllers via MQTT. 3 | * 4 | * It will scan for and auto-connect to any AM43 devices in range, then provide 5 | * MQTT topics to control and get status from them. 6 | * 7 | * The following MQTT topis are published to: 8 | * 9 | * - //available - Either 'offline' or 'online' 10 | * - //position - The current blind position, between 0 and 100 11 | * - //battery - The current battery level, between 0 and 100 12 | * - /rssi - The current RSSI reported by the device. 13 | * - /LWT - Either 'Online' or 'Offline', MQTT status of this service. 14 | * 15 | * The following MQTT topics are subscribed to: 16 | * 17 | * - //set - Set the blind to 'OPEN', 'STOP' or 'CLOSE' 18 | * - //set_position - Set the blind position 'OPEN', 'UP', 'STOP', 'CLOSE', 'DOWN' Or between 0 and 100. 19 | * - /restart - Reboot this service. 20 | * 21 | * is the bluetooth mac address of the device, eg 026932f0c51d 22 | * 23 | * For the position set commands, you can use name 'all' to change all devices. 24 | * 25 | * Arduino OTA update is supported. 26 | **/ 27 | 28 | #include "config.h" 29 | #ifdef ENABLE_ARDUINO_OTA 30 | #include 31 | #endif 32 | #include 33 | #include 34 | 35 | #include 36 | #include 37 | #ifdef USE_NIMBLE 38 | #include 39 | #else 40 | #include 41 | #endif 42 | 43 | #ifdef WDT_TIMEOUT 44 | #include 45 | #endif 46 | 47 | 48 | const char *ssid = WIFI_SSID; 49 | const char *password = WIFI_PASSWORD; 50 | const char *mqtt_server = MQTT_ADDRESS; 51 | const uint16_t mqtt_server_port = MQTT_PORT; 52 | const uint16_t am43Pin = AM43_PIN; 53 | const uint16_t am43BLEOnDemandScanTimeout = AM43_ONDEMAND_SCAN_TIMEOUT; 54 | 55 | WiFiClient espClient; 56 | PubSubClient pubSubClient(espClient); 57 | 58 | #ifdef USE_NIMBLE 59 | struct ble_npl_sem clientListSemaphore; 60 | #else 61 | FreeRTOS::Semaphore clientListSem = FreeRTOS::Semaphore("clients"); 62 | #endif 63 | 64 | void mqtt_callback(char* top, byte* pay, unsigned int length); 65 | 66 | unsigned long lastScan = 0; 67 | boolean scanning = false; 68 | 69 | #ifdef AM43_ONDEMAND 70 | boolean bleEnabled = false; // Controlled via MQTT. 71 | boolean bleOnDemand = true; 72 | #else 73 | boolean bleEnabled = true; // Controlled via MQTT. 74 | boolean bleOnDemand = false; 75 | #endif 76 | 77 | const uint16_t bleOnDemandTimeout = 60000; 78 | unsigned long onDemandTime = millis(); 79 | boolean bleOnDemandCommandQueued = false; 80 | String bleOnDemandAddress; 81 | String bleOnDemandCommand; 82 | String bleOnDemandCommandValue; 83 | BLEScan* pBLEScan; 84 | 85 | const char discTopicTmpl[] PROGMEM = "homeassistant/cover/%s/config"; 86 | const char discPayloadTmpl[] PROGMEM = " { \"cmd_t\": \"~/set\", \"pos_t\": \"~/position\"," 87 | "\"pos_open\": 2, \"pos_clsd\": 99, \"set_pos_t\": \"~/set_position\"," 88 | "\"avty_t\": \"~/available\", \"pl_avail\": \"online\", \"pl_not_avail\": \"offline\"," 89 | "\"dev_cla\": \"%s\", \"name\": \"%s\", \"uniq_id\": \"%s_am43_cover\", \"~\": \"%s/%s\"}"; 90 | 91 | const char discBattTopicTmpl[] PROGMEM = "homeassistant/sensor/%s_Battery/config"; 92 | const char discBattPayloadTmpl[] PROGMEM = " { \"device_class\": \"battery\"," 93 | "\"name\":\"%s Battery\", \"stat_t\": \"~/battery\", \"unit_of_meas\": \"%%\"," 94 | "\"avty_t\": \"~/available\", \"uniq_id\": \"%s_am43_battery\", \"~\": \"%s/%s\"}"; 95 | 96 | const char discLightTopicTmpl[] PROGMEM = "homeassistant/sensor/%s_Light/config"; 97 | const char discLightPayloadTmpl[] PROGMEM = " { \"device_class\": \"illuminance\"," 98 | "\"name\":\"%s Light\", \"stat_t\": \"~/light\", \"unit_of_meas\": \"%%\"," 99 | "\"avty_t\": \"~/available\", \"uniq_id\": \"%s_am43_light\", \"~\": \"%s/%s\"}"; 100 | 101 | const char discSwitchTopic[] PROGMEM = "homeassistant/switch/%s_Switch/config"; 102 | const char discSwitchPayload[] PROGMEM = " { \"icon\": \"mdi:bluetooth\"," 103 | "\"name\":\"%s Switch\", \"stat_t\": \"~/enabled\", \"cmd_t\": \"~/enable\"," 104 | "\"uniq_id\": \"%s_enabled\", \"~\": \"%s\"}"; 105 | 106 | class MyAM43Callbacks: public AM43Callbacks { 107 | public: 108 | AM43Client *client; 109 | WiFiClient wifiClient; 110 | PubSubClient *mqtt; 111 | unsigned long nextMqttAttempt; 112 | String rmtAddress; 113 | String deviceName; 114 | String mqttName; 115 | unsigned long lastBatteryMessage; 116 | ~MyAM43Callbacks() { 117 | delete this->client; 118 | delete this->mqtt; 119 | } 120 | 121 | String topic(char *t) { 122 | char top[64]; 123 | sprintf(top, "%s/%s/%s", MQTT_TOPIC_PREFIX, this->mqttName.c_str(), t); 124 | String ret = String(top); 125 | return ret; 126 | } 127 | 128 | void onPosition(uint8_t pos) { 129 | Serial.printf("[%s] Got position: %d\r\n", this->rmtAddress.c_str(), pos); 130 | this->mqtt->publish(topic("position").c_str(), String(pos).c_str(), false); 131 | } 132 | void onBatteryLevel(uint8_t level) { 133 | // Ignore overly frequent battery messages. AM43s are known to spam them at times. 134 | if (this->lastBatteryMessage == 0 || millis() - this->lastBatteryMessage > 30000) { 135 | Serial.printf("[%s] Got battery: %d\r\n", this->rmtAddress.c_str(), level); 136 | this->mqtt->publish(topic("battery").c_str(), String(level).c_str(), false); 137 | this->lastBatteryMessage = millis(); 138 | } 139 | } 140 | void onLightLevel(uint8_t level) { 141 | Serial.printf("[%s] Got light: %d\r\n", this->rmtAddress.c_str(), level); 142 | this->mqtt->publish(topic("light").c_str(), String(level).c_str(), false); 143 | } 144 | void onConnect(AM43Client *c) { 145 | this->mqtt = new PubSubClient(this->wifiClient); 146 | this->nextMqttAttempt = 0; 147 | this->mqtt->setServer(mqtt_server, mqtt_server_port); 148 | this->mqtt->setCallback(mqtt_callback); 149 | this->mqtt->setBufferSize(512); 150 | this->rmtAddress = String(c->m_Device->getAddress().toString().c_str()); 151 | this->rmtAddress.replace(":", ""); 152 | this->deviceName = String(client->m_Name.c_str()); 153 | #ifdef AM43_USE_NAME_FOR_TOPIC 154 | this->mqttName = this->deviceName; 155 | #else 156 | this->mqttName = this->rmtAddress; 157 | #endif 158 | lastScan = millis()-58000; // Trigger a new scan shortly after connection. 159 | Serial.printf("[%s] Connected\r\n", this->rmtAddress.c_str()); 160 | } 161 | void onDisconnect(AM43Client *c) { 162 | Serial.printf("[%s] Disconnected\r\n", this->rmtAddress.c_str()); 163 | if (this->mqtt != nullptr && this->mqtt->connected()) { 164 | // Publish offline availability as LWT is only for ungraceful disconnect. 165 | this->mqtt->publish(topic("available").c_str(), "offline", true); 166 | this->mqtt->loop(); 167 | this->mqtt->disconnect(); 168 | } 169 | delete this->mqtt; 170 | this->mqtt = nullptr; 171 | } 172 | 173 | void handle() { 174 | if (this->mqtt == nullptr) return; 175 | if (this->mqtt->connected()) { 176 | this->mqtt->loop(); 177 | return; 178 | } 179 | if (WiFi.status() != WL_CONNECTED || millis() < this->nextMqttAttempt) return; 180 | if (!this->mqtt->connect(topic("").c_str(), MQTT_USERNAME, MQTT_PASSWORD, topic("available").c_str(), 0, false, "offline")) { 181 | Serial.print("MQTT connect failed, rc="); 182 | Serial.print(this->mqtt->state()); 183 | Serial.println(" retrying in 5s."); 184 | this->nextMqttAttempt = millis() + 5000; 185 | return; 186 | } 187 | this->mqtt->publish(topic("available").c_str(), "online", true); 188 | this->mqtt->subscribe(topic("set").c_str()); 189 | this->mqtt->subscribe(topic("set_position").c_str()); 190 | this->mqtt->subscribe(topic("status").c_str()); 191 | 192 | #ifdef AM43_ENABLE_MQTT_DISCOVERY 193 | char discTopic[128]; 194 | char discPayload[300]; 195 | 196 | sprintf(discTopic, discTopicTmpl, this->mqttName.c_str()); 197 | sprintf(discPayload, discPayloadTmpl, AM43_MQTT_DEVICE_CLASS, this->deviceName.c_str(), this->rmtAddress.c_str(), MQTT_TOPIC_PREFIX, this->mqttName.c_str()); 198 | this->mqtt->publish(discTopic, discPayload, true); 199 | 200 | sprintf(discTopic, discBattTopicTmpl, this->mqttName.c_str()); 201 | sprintf(discPayload, discBattPayloadTmpl, this->deviceName.c_str(), this->rmtAddress.c_str(), MQTT_TOPIC_PREFIX, this->mqttName.c_str()); 202 | this->mqtt->publish(discTopic, discPayload, true); 203 | 204 | sprintf(discTopic, discLightTopicTmpl, this->mqttName.c_str()); 205 | sprintf(discPayload, discLightPayloadTmpl, this->deviceName.c_str(), this->rmtAddress.c_str(), MQTT_TOPIC_PREFIX, this->mqttName.c_str()); 206 | this->mqtt->publish(discTopic, discPayload, true); 207 | #endif 208 | this->mqtt->loop(); 209 | } 210 | }; 211 | 212 | std::map allClients; 213 | 214 | std::map getClients() { 215 | std::map cls; 216 | #ifdef USE_NIMBLE 217 | ble_npl_sem_pend(&clientListSemaphore, BLE_NPL_TIME_FOREVER); 218 | #else 219 | clientListSem.take("clientsAll"); 220 | #endif 221 | for (auto const& c : allClients) 222 | cls.insert({c.first, c.second}); 223 | #ifdef USE_NIMBLE 224 | ble_npl_sem_release(&clientListSemaphore); 225 | #else 226 | clientListSem.give(); 227 | #endif 228 | return cls; 229 | } 230 | 231 | 232 | static void notifyCallback(BLERemoteCharacteristic* rChar, uint8_t* pData, size_t length, bool isNotify) { 233 | auto cls = getClients(); 234 | for (auto const& c : cls) { 235 | if (c.second->client->m_Char == rChar) { 236 | c.second->client->myNotifyCallback(rChar, pData, length, isNotify); 237 | return; 238 | } 239 | } 240 | } 241 | 242 | 243 | void mqtt_callback(char* top, byte* pay, unsigned int length) { 244 | pay[length] = '\0'; 245 | String payload = String((char *)pay); 246 | String topic = String(top); 247 | Serial.printf("MQTT [%s]%d: %s\r\n", top, length, payload.c_str()); 248 | 249 | int i1, i2, i3; 250 | i1 = topic.indexOf('/'); 251 | i2 = topic.indexOf('/', i1+1); 252 | String address = topic.substring(i1+1, i2); 253 | String command = topic.substring(i2+1); 254 | if (address == "cmnd") { 255 | i3 = topic.indexOf('/', i2+1); 256 | address = topic.substring(i2+1, i3); 257 | command = topic.substring(i3+1); 258 | } 259 | boolean commandOnDemand = false; 260 | Serial.printf("Addr: %s Cmd: %s\r\n", address.c_str(), command.c_str()); 261 | payload.toLowerCase(); 262 | 263 | auto cls = getClients(); 264 | if (address == "restart") { 265 | for (auto const& c : cls) 266 | c.second->onDisconnect(c.second->client); 267 | delay(200); 268 | ESP.restart(); 269 | } 270 | 271 | if (address == "enable") { 272 | if (payload == "off") { 273 | Serial.println("Disabling BLE Clients"); 274 | bleEnabled = false; 275 | pubSubClient.publish(topPrefix("/enabled").c_str(), "OFF", true); 276 | #ifdef AM43_ONDEMAND 277 | bleOnDemand = true; 278 | #endif 279 | for (auto const& c : cls) { 280 | c.second->client->disconnectFromServer(); 281 | } 282 | } else if (payload == "on") { 283 | Serial.println("Enabling BLE Clients"); 284 | bleEnabled = true; 285 | pubSubClient.publish(topPrefix("/enabled").c_str(), "ON", true); 286 | lastScan = 0; 287 | #ifdef AM43_ONDEMAND 288 | bleOnDemand = false; 289 | #endif 290 | } 291 | } 292 | 293 | for (auto const& c : cls) { 294 | auto cl = c.second->client; 295 | if (c.second->mqttName == address || address == "all") { 296 | if (command == "set") { 297 | if (payload == "open") cl->open(); 298 | if (payload == "close") cl->close(); 299 | if (payload == "stop") cl->stop(); 300 | } 301 | if (command == "set_position") { 302 | if (payload == "open" || payload == "up") 303 | cl->open(); 304 | else if (payload == "close" || payload == "down") 305 | cl->close(); 306 | else if (payload == "stop") 307 | cl->stop(); 308 | else 309 | cl->setPosition(payload.toInt()); 310 | } 311 | if (command == "status") { 312 | cl->sendGetBatteryRequest(); 313 | cl->sendGetPositionRequest(); 314 | cl->sendGetLightRequest(); 315 | } 316 | } 317 | } 318 | 319 | if (command == "status" || command == "set" || command == "set_position" || command == "status") commandOnDemand = true; 320 | 321 | if (commandOnDemand && bleOnDemand && millis() - onDemandTime > bleOnDemandTimeout) { 322 | onDemandTime = millis(); 323 | onDemand_BLEScan(); 324 | if (command != "status") { 325 | bleOnDemandCommandQueued = true; 326 | bleOnDemandAddress = address; 327 | bleOnDemandCommand = command; 328 | bleOnDemandCommandValue = payload; 329 | } 330 | } 331 | 332 | } 333 | 334 | void onDemand_BLEScan() { 335 | Serial.println("Starting an OnDemand BLE Scan..."); 336 | scanning = true; 337 | lastScan = 0; 338 | pBLEScan->start(am43BLEOnDemandScanTimeout, bleScanComplete, false); 339 | } 340 | 341 | std::vector allowList; 342 | 343 | void parseAllowList() { 344 | std::string allowListStr = std::string(DEVICE_ALLOWLIST); 345 | if (allowListStr.length() == 0) return; 346 | size_t idx1 = 0; 347 | 348 | for(;;) { 349 | auto idx = allowListStr.find(',', idx1); 350 | if (idx == std::string::npos) break; 351 | allowList.push_back(BLEAddress(allowListStr.substr(idx1, idx-idx1))); 352 | idx1 = idx+1; 353 | } 354 | allowList.push_back(BLEAddress(allowListStr.substr(idx1))); 355 | Serial.println("AllowList contains the following device(s):"); 356 | for (auto dev : allowList) 357 | Serial.printf(" Mac: %s \n", dev.toString().c_str()); 358 | } 359 | 360 | bool isAllowed(BLEAddress address) { 361 | if (allowList.size() < 1) return true; 362 | for (auto a : allowList) { 363 | if (a.equals(address)) return true; 364 | } 365 | return false; 366 | } 367 | 368 | /** 369 | * Scan for BLE servers and find any that advertise the service we are looking for. 370 | */ 371 | class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { 372 | /** 373 | * Called for each advertising BLE server. 374 | */ 375 | #ifdef USE_NIMBLE 376 | void onResult(BLEAdvertisedDevice *advertisedDevice) { 377 | #else 378 | void onResult(BLEAdvertisedDevice advDevice) { 379 | BLEAdvertisedDevice *advertisedDevice = &advDevice; 380 | #endif 381 | if (advertisedDevice->getName().length() > 0) 382 | Serial.printf("BLE Advertised Device found: %s\r\n", advertisedDevice->toString().c_str()); 383 | 384 | // We have found a device, let us now see if it contains the service we are looking for. 385 | if (advertisedDevice->haveServiceUUID() && advertisedDevice->isAdvertisingService(serviceUUID)) { 386 | if (!bleEnabled && !bleOnDemand) { 387 | Serial.printf("BLE connections disabled, ignoring device\r\n"); 388 | return; 389 | } 390 | auto cls = getClients(); 391 | for (auto const& c : cls) { 392 | if (!c.first.compare(advertisedDevice->toString())) { 393 | Serial.printf("Ignoring advertising device %s, already present\r\n", advertisedDevice->toString().c_str()); 394 | return; 395 | } 396 | } 397 | if (!isAllowed(advertisedDevice->getAddress())) { 398 | Serial.printf("Ignoring device %s, not in allow list\r\n", advertisedDevice->toString().c_str()); 399 | return; 400 | } 401 | if (cls.size() >= BLE_MAX_CONN) { 402 | Serial.printf("ERROR: Already connected to %d devices, Arduino cannot connect to any more.\r\n", cls.size()); 403 | return; 404 | } 405 | #ifdef USE_NIMBLE 406 | //AM43Client* newClient = new AM43Client(advertisedDevice, am43Pin); 407 | AM43Client* newClient = new AM43Client(new BLEAdvertisedDevice(*advertisedDevice), am43Pin); 408 | #else 409 | AM43Client* newClient = new AM43Client(new BLEAdvertisedDevice(advDevice), am43Pin); 410 | #endif 411 | newClient->m_DoConnect = true; 412 | newClient->m_Name = advertisedDevice->getName(); 413 | MyAM43Callbacks *cbs = new MyAM43Callbacks(); 414 | cbs->client = newClient; 415 | cbs->lastBatteryMessage = 0; 416 | newClient->setClientCallbacks(cbs); 417 | #ifdef USE_NIMBLE 418 | ble_npl_sem_pend(&clientListSemaphore, BLE_NPL_TIME_FOREVER); 419 | #else 420 | clientListSem.take("clientInsert"); 421 | #endif 422 | allClients.insert({advertisedDevice->toString(), cbs}); 423 | #ifdef USE_NIMBLE 424 | ble_npl_sem_release(&clientListSemaphore); 425 | #else 426 | clientListSem.give(); 427 | #endif 428 | //pBLEScan->stop(); 429 | //scanning = false; 430 | } // Found our server 431 | } // onResult 432 | }; // MyAdvertisedDeviceCallbacks 433 | 434 | void bleScanComplete(BLEScanResults r) { 435 | Serial.println("BLE scan complete."); 436 | scanning = false; 437 | }; 438 | 439 | void initBLEScan() { 440 | // Retrieve a Scanner and set the callback we want to use to be informed when we 441 | // have detected a new device. Specify that we want active scanning and start the 442 | // scan to run for 5 seconds. 443 | scanning = true; 444 | pBLEScan = BLEDevice::getScan(); 445 | pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); 446 | pBLEScan->setInterval(150); 447 | pBLEScan->setWindow(99); 448 | pBLEScan->setActiveScan(true); 449 | pBLEScan->start(10, bleScanComplete, false); 450 | } 451 | 452 | unsigned int wifiDownSince = 0; 453 | 454 | void WiFiEvent(WiFiEvent_t event) { 455 | Serial.printf("[WiFi-event] event: %d\r\n", event); 456 | 457 | switch(event) { 458 | case SYSTEM_EVENT_STA_GOT_IP: 459 | Serial.println("WiFi connected"); 460 | Serial.print("IP address: "); 461 | Serial.println(WiFi.localIP()); 462 | break; 463 | case SYSTEM_EVENT_STA_DISCONNECTED: 464 | Serial.println("WiFi lost connection"); 465 | wifiDownSince = millis(); 466 | break; 467 | } 468 | } 469 | 470 | void setup_wifi() { 471 | WiFi.disconnect(true); 472 | delay(1000); 473 | Serial.printf("Wifi connecting to: %s ... \r\n", ssid); 474 | WiFi.onEvent(WiFiEvent); 475 | WiFi.mode(WIFI_STA); 476 | WiFi.begin(ssid, password); 477 | wifiDownSince = 0; 478 | } 479 | 480 | unsigned long nextMqttAttempt = 0; 481 | 482 | String topPrefix(const char *top) { 483 | String ret = String(MQTT_TOPIC_PREFIX) + top; 484 | return ret; 485 | } 486 | void reconnect_mqtt() { 487 | if (WiFi.status() == WL_CONNECTED && millis() > nextMqttAttempt) { 488 | Serial.print("Attempting MQTT connection...\r\n"); 489 | String mqttPrefix = String(MQTT_TOPIC_PREFIX); 490 | // Attempt to connect 491 | if (pubSubClient.connect(topPrefix("-gateway").c_str(), MQTT_USERNAME, MQTT_PASSWORD, topPrefix("/LWT").c_str(), 0, false, "Offline")) { 492 | Serial.println("connected"); 493 | // Once connected, publish an announcement... 494 | pubSubClient.publish(topPrefix("/LWT").c_str(), "Online", true); 495 | pubSubClient.subscribe(topPrefix("/restart").c_str()); 496 | pubSubClient.subscribe(topPrefix("/enable").c_str()); 497 | pubSubClient.subscribe(topPrefix("/all/set").c_str()); 498 | pubSubClient.subscribe(topPrefix("/all/set_position").c_str()); 499 | pubSubClient.subscribe(topPrefix("/all/status").c_str()); 500 | if (bleOnDemand) pubSubClient.subscribe(topPrefix("/cmnd/#").c_str()); 501 | pubSubClient.loop(); 502 | 503 | char discTopic[128]; 504 | char discPayload[300]; 505 | 506 | sprintf(discTopic, discSwitchTopic, MQTT_TOPIC_PREFIX); 507 | sprintf(discPayload, discSwitchPayload, MQTT_TOPIC_PREFIX, MQTT_TOPIC_PREFIX, MQTT_TOPIC_PREFIX); 508 | pubSubClient.publish(discTopic, discPayload, true); 509 | String msgEnabled = "ON"; 510 | if (!bleEnabled) msgEnabled = "OFF"; 511 | pubSubClient.publish(topPrefix("/enabled").c_str(), msgEnabled.c_str(), true); 512 | } else { 513 | Serial.print("failed, rc="); 514 | Serial.print(pubSubClient.state()); 515 | Serial.println(" try again in 5 seconds"); 516 | // Wait 5 seconds before retrying 517 | nextMqttAttempt = millis() + 5000; 518 | } 519 | } 520 | } 521 | 522 | bool otaUpdating = false; 523 | 524 | void setup() { 525 | Serial.begin(115200); 526 | Serial.println("Starting Arduino BLE Client application..."); 527 | #ifdef USE_NIMBLE 528 | Serial.println("Using NimBLE stack."); 529 | #else 530 | Serial.println("Using legacy stack."); 531 | #endif 532 | setup_wifi(); 533 | pubSubClient.setServer(mqtt_server, mqtt_server_port); 534 | pubSubClient.setCallback(mqtt_callback); 535 | 536 | parseAllowList(); 537 | #ifdef USE_NIMBLE 538 | ble_npl_sem_init(&clientListSemaphore, 1); 539 | #endif 540 | 541 | #ifdef ENABLE_ARDUINO_OTA 542 | ArduinoOTA 543 | .onStart([]() { 544 | String type; 545 | if (ArduinoOTA.getCommand() == U_FLASH) 546 | type = "sketch"; 547 | else // U_SPIFFS 548 | type = "filesystem"; 549 | 550 | // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() 551 | Serial.println("Start updating " + type); 552 | // Stop any active BLEScan during OTA - improves stability. 553 | otaUpdating = true; 554 | pBLEScan->stop(); 555 | }) 556 | .onEnd([]() { 557 | Serial.println("\nEnd"); 558 | pubSubClient.disconnect(); 559 | }) 560 | .onProgress([](unsigned int progress, unsigned int total) { 561 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 562 | }) 563 | .onError([](ota_error_t error) { 564 | Serial.printf("Error[%u]: ", error); 565 | if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); 566 | else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); 567 | else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); 568 | else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); 569 | else if (error == OTA_END_ERROR) Serial.println("End Failed"); 570 | otaUpdating = false; 571 | }); 572 | 573 | ArduinoOTA.begin(); 574 | #endif 575 | otaUpdating = false; 576 | esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); 577 | BLEDevice::init(""); 578 | initBLEScan(); 579 | #ifdef WDT_TIMEOUT 580 | Serial.println("Configuring WDT..."); 581 | esp_task_wdt_init(WDT_TIMEOUT, true); //enable panic so ESP32 restarts 582 | esp_task_wdt_add(NULL); //add current thread to WDT watch 583 | #endif 584 | } // End of setup. 585 | 586 | unsigned long lastAM43update = 0; 587 | 588 | // This is the Arduino main loop function. 589 | void loop() { 590 | 591 | if (WiFi.status() != WL_CONNECTED && wifiDownSince > 0 && millis()-wifiDownSince > 20000) { 592 | setup_wifi(); 593 | } 594 | if (!pubSubClient.connected()) { 595 | reconnect_mqtt(); 596 | } 597 | pubSubClient.loop(); 598 | 599 | if (millis()-lastAM43update > 500) { // Only process this every 500ms. 600 | std::vector removeList; // Clients will be added to this as they are disconnected. 601 | auto cls = getClients(); 602 | // Iterate through connected devices, perform any connect/update/etc actions. 603 | for (auto const &c : cls) { 604 | if (c.second->client->m_DoConnect && !scanning) { 605 | c.second->client->connectToServer(notifyCallback); 606 | if (bleOnDemandCommandQueued) c.second->client->setCommandQueued(true); 607 | break; // Connect takes some time, so break out to allow other processing. 608 | } 609 | if (c.second->client->m_Connected) { 610 | c.second->client->update(); 611 | c.second->handle(); 612 | if (c.second->client->m_CommandQueued && (bleOnDemandAddress == "all" || bleOnDemandAddress == c.second->mqttName)) { 613 | c.second->client->setCommandQueued(false); 614 | if (bleOnDemandCommand == "set") { 615 | if (bleOnDemandCommandValue == "open") c.second->client->open(); 616 | if (bleOnDemandCommandValue == "close") c.second->client->close(); 617 | if (bleOnDemandCommandValue == "stop") c.second->client->stop(); 618 | } 619 | if (bleOnDemandCommand == "set_position") { 620 | if (bleOnDemandCommandValue == "open" || bleOnDemandCommandValue == "up") 621 | c.second->client->open(); 622 | else if (bleOnDemandCommandValue == "close" || bleOnDemandCommandValue == "down") 623 | c.second->client->close(); 624 | else if (bleOnDemandCommandValue == "stop") 625 | c.second->client->stop(); 626 | else 627 | c.second->client->setPosition(bleOnDemandCommandValue.toInt()); 628 | } 629 | } 630 | if (bleOnDemand && millis() - onDemandTime > bleOnDemandTimeout) { 631 | bleOnDemandCommandQueued = false; 632 | c.second->client->disconnectFromServer(); 633 | } 634 | } 635 | if (c.second->client->m_Disconnected) removeList.push_back(c.first); 636 | } 637 | // Remove any clients that have been disconnected. 638 | #ifdef USE_NIMBLE 639 | ble_npl_sem_pend(&clientListSemaphore, BLE_NPL_TIME_FOREVER); 640 | #else 641 | clientListSem.take("clientRemove"); 642 | #endif 643 | for (auto i : removeList) { 644 | delete allClients[i]; 645 | allClients.erase(i); 646 | } 647 | #ifdef USE_NIMBLE 648 | ble_npl_sem_release(&clientListSemaphore); 649 | #else 650 | clientListSem.give(); 651 | #endif 652 | 653 | lastAM43update = millis(); 654 | } 655 | // Start a new scan every 60s. 656 | if (millis() - lastScan > 60000 && !otaUpdating && !scanning) { 657 | if (bleEnabled) { 658 | scanning = true; 659 | pBLEScan->start(10, bleScanComplete, false); 660 | } 661 | lastScan = millis(); 662 | Serial.printf("Up for %ds\r\n", millis()/1000); 663 | } 664 | 665 | #ifdef ENABLE_ARDUINO_OTA 666 | ArduinoOTA.handle(); 667 | #endif 668 | 669 | #ifdef WDT_TIMEOUT 670 | esp_task_wdt_reset(); 671 | #endif 672 | } // End of loop 673 | -------------------------------------------------------------------------------- /examples/MQTTBlinds/config.h: -------------------------------------------------------------------------------- 1 | // Your Wifi SSID. 2 | #define WIFI_SSID "wifi_ssid" 3 | 4 | // Wifi password. 5 | #define WIFI_PASSWORD "wifi_password" 6 | 7 | // MQTT server details. 8 | #define MQTT_ADDRESS "x.x.x.x" 9 | #define MQTT_PORT 1883 10 | #define MQTT_USERNAME "" 11 | #define MQTT_PASSWORD "" 12 | 13 | // WDT Timeout (in seconds) - comment out if not required 14 | #define WDT_TIMEOUT 6 15 | 16 | // Comment below to use the device mac address in the topic instead of the name. Ignored 17 | // if autodiscovery is enabled (will always use the mac address). 18 | #define AM43_USE_NAME_FOR_TOPIC 19 | 20 | // Prefix for MQTT topics, you can change this if you have multiple ESP devices, etc. 21 | #define MQTT_TOPIC_PREFIX "am43" 22 | 23 | // Enable MQTT auto-discovery for Home Assistant. 24 | #define AM43_ENABLE_MQTT_DISCOVERY 25 | 26 | // Device class to report to HomeAssistant. Must be one of the values at 27 | // https://www.home-assistant.io/integrations/cover/ 28 | // Currently don't support per-device class. 29 | #define AM43_MQTT_DEVICE_CLASS "shade" 30 | 31 | // PIN for the AM43 device (printed on it, default is 8888) 32 | #define AM43_PIN 8888 33 | 34 | // Enable this to use OnDemand feature : 35 | // This allows to maintain disconnected from BLE devices and, 36 | // Receive command from MQTT and connect and send commands to AM43 37 | // OnDemand. The devices commands needs to be sent to AM43/cmnd/MAC-ADDRESS/command 38 | // 39 | // Note: Commands can have a delay of 5-10s. 40 | // Config change recommended! : To improve status updates, 41 | // change AM43_UPDATE_INTERVAL to 5000 in AM43Clien.h file 42 | // ex. #define AM43_UPDATE_INTERVAL 5000 43 | // 44 | // To enable, uncomment the next line: 45 | //#define AM43_ONDEMAND 46 | 47 | // BLE Scan time out for OnDemand Mode. Default 3 seconds. 48 | // Increment this value if you have problems when using OnDemand Mode. 49 | // This represent the amount of seconds of BLE Scan executed in OnDemand Mode. 50 | #define AM43_ONDEMAND_SCAN_TIMEOUT 3 51 | 52 | // Comma separated list of MAC addresses to allow for control. Useful if you 53 | // have multiple ESP controllers. Leave empty to allow all devices. 54 | // 55 | // Note that the default Arduino SDK only allows connecting to three devices 56 | // maximum. For more devices, you should have multiple ESP32's, ensure they have 57 | // a different MQTT_TOPIC_PREFIX, and use the DEVICE_ALLOWLIST to control up 58 | // to 3 am43's per ESP. 59 | 60 | #define SHADE_1 "11:22:33:44:55:66" 61 | #define SHADE_2 "11:22:33:44:55:66" 62 | #define SHADE_3 "11:22:33:44:55:66" 63 | 64 | #define DEVICE_ALLOWLIST "" 65 | //#define DEVICE_ALLOWLIST SHADE_1 66 | //#define DEVICE_ALLOWLIST SHADE_1 "," SHADE_2 "," SHADE_3 67 | 68 | 69 | // Comment out below to disable the OTA feature, especially if you have 70 | // stability problems. 71 | #define ENABLE_ARDUINO_OTA 72 | 73 | // LEAVE BELOW UNTOUCHED 74 | #ifdef AM43_ENABLE_MQTT_DISCOVERY 75 | #undef AM43_USE_NAME_FOR_TOPIC 76 | #endif 77 | 78 | // Maximum number of connected devices. This should not be raised unless the Arduino 79 | // BLE stack is also modified as it has this limit also. 80 | #define BLE_MAX_CONN 3 81 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=AM43Client 2 | version=0.5.0 3 | author=Ben Buxton 4 | maintainer=Ben Buxton 5 | sentence=Library for controlling AM43 BLE blind motors. 6 | paragraph=Supports auto-discovery and multi-device. 7 | category=Device Control 8 | url=https://github.com/buxtronix/am43 9 | architecture=esp32 10 | includes=AM43Client.h 11 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Please visit documentation for options and examples 4 | ; https://docs.platformio.org/page/projectconf.html 5 | 6 | [platformio] 7 | lib_deps = 8 | h2zero/NimBLE-Arduino 9 | PubSubClient 10 | 11 | [env:esp32] 12 | platform = espressif32 13 | framework = arduino 14 | board = esp32dev 15 | monitor_speed = 115200 16 | 17 | -------------------------------------------------------------------------------- /src/AM43Client.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "AM43Client.h" 3 | #ifdef USE_NIMBLE 4 | #include "NimBLEDevice.h" 5 | #else 6 | #include "BLEDevice.h" 7 | #endif 8 | 9 | BLEUUID serviceUUID(AM43_SERVICE_UUID); 10 | BLEUUID charUUID(AM43_CHAR_UUID); 11 | 12 | uint8_t startPacket[5] = {0x00, 0xff, 0x00, 0x00, 0x9a}; 13 | std::vector startPkt(startPacket, startPacket + sizeof(startPacket)/sizeof(startPacket[0])); 14 | 15 | AM43Client::AM43Client(BLEAdvertisedDevice *d) 16 | :AM43Client(d, AM43_DEFAULT_PIN) 17 | {} 18 | 19 | AM43Client::AM43Client(BLEAdvertisedDevice *d, uint16_t pin) { 20 | this->m_Device = d; 21 | this->m_Connected = false; 22 | this->m_Disconnected = false; 23 | this->m_LoggedIn = false; 24 | this->m_Pin = pin; 25 | this->m_BatteryPercent = 0xff; 26 | this->m_ClientCallbacks = nullptr; 27 | this->m_CurrentQuery = 1; 28 | this->m_CommandQueued = false; 29 | } 30 | 31 | void AM43Client::onConnect(BLEClient* pclient) { 32 | this->m_Connected = true; 33 | if (this->m_ClientCallbacks != nullptr) 34 | this->m_ClientCallbacks->onConnect(this); 35 | } 36 | 37 | void AM43Client::onDisconnect(BLEClient* pclient) { 38 | this->m_Connected = false; 39 | this->m_Disconnected = true; 40 | if (this->m_ClientCallbacks != nullptr) 41 | this->m_ClientCallbacks->onDisconnect(this); 42 | } 43 | 44 | void AM43Client::setClientCallbacks(AM43Callbacks *callbacks) { 45 | this->m_ClientCallbacks = callbacks; 46 | } 47 | 48 | String AM43Client::deviceString() { 49 | return String(this->m_Device->getName().c_str()) + " " + String(this->m_Device->getAddress().toString().c_str()); 50 | } 51 | 52 | void AM43Client::myNotifyCallback( 53 | BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) { 54 | if (length < 1 || pData[0] != 0x9a) return; 55 | 56 | #if 0 57 | Serial.printf("%s: 0x", deviceString().c_str()); 58 | for (int i = 0; i < length ; i++) { 59 | if (pData[i]<0x10) Serial.print("0"); 60 | Serial.print(pData[i], HEX); 61 | } 62 | Serial.println(); 63 | #endif 64 | switch (pData[1]) { 65 | case AM43_COMMAND_GET_BATTERY: { 66 | this->m_BatteryPercent = pData[7]; 67 | if (this->m_ClientCallbacks != nullptr) 68 | this->m_ClientCallbacks->onBatteryLevel(this->m_BatteryPercent); 69 | break; 70 | } 71 | case AM43_COMMAND_SET_POSITION: { 72 | if (pData[3] != AM43_RESPONSE_ACK) { 73 | Serial.printf("[%s] Position command nack! (%d)\r\n", deviceString().c_str(), pData[3]); 74 | } 75 | break; 76 | } 77 | case AM43_NOTIFY_POSITION: { 78 | this->m_OpenLevel = pData[4]; 79 | if (this->m_ClientCallbacks != nullptr) 80 | this->m_ClientCallbacks->onPosition(this->m_OpenLevel); 81 | break; 82 | } 83 | case AM43_COMMAND_GET_POSITION: { 84 | /* 85 | * Bytes in this packet are: 86 | * 3: Configuration flags, bits are: 87 | * 1: direction 88 | * 2: operation mode 89 | * 3: top limit set 90 | * 4: bottom limit set 91 | * 5: has light sensor 92 | * 4: Speed setting 93 | * 5: Current position 94 | * 6,7: Shade length. 95 | * 8: Roller diameter. 96 | * 9: Roller type. 97 | */ 98 | this->m_OpenLevel = pData[5]; 99 | if (this->m_ClientCallbacks != nullptr) 100 | this->m_ClientCallbacks->onPosition(this->m_OpenLevel); 101 | break; 102 | } 103 | 104 | case AM43_COMMAND_GET_LIGHT: { 105 | this->m_LightLevel = ((float)pData[4]*100/9); 106 | if (this->m_ClientCallbacks != nullptr) 107 | this->m_ClientCallbacks->onLightLevel(this->m_LightLevel); 108 | break; 109 | } 110 | 111 | case AM43_COMMAND_LOGIN: { 112 | if (pData[3] == AM43_RESPONSE_ACK) { 113 | this->m_LoggedIn = true; 114 | Serial.printf("[%s] Pin ok\r\n", deviceString().c_str()); 115 | // Trigger a fetch in 1s; 116 | this->m_LastUpdate = millis() - AM43_UPDATE_INTERVAL + 1000; 117 | } else if (pData[3] == AM43_RESPONSE_NACK) { 118 | Serial.printf("[%s] Pin incorrect\r\n", deviceString().c_str()); 119 | this->m_LoggedIn = false; 120 | } 121 | break; 122 | } 123 | 124 | case AM43_COMMAND_MOVE: { 125 | if (pData[3] == AM43_RESPONSE_ACK) { 126 | Serial.printf("[%s] Move ok\r\n", deviceString().c_str()); 127 | break; 128 | } else if (pData[3] == AM43_RESPONSE_NACK) { 129 | Serial.printf("[%s] Move nack\r\n", deviceString().c_str()); 130 | break; 131 | } 132 | } 133 | 134 | case AM43_REPLY_UNKNOWN2: 135 | case AM43_REPLY_UNKNOWN1: { 136 | Serial.printf("[%s] Unknown reply: ", deviceString().c_str()); 137 | for (int i = 0; i < length ; i++) { 138 | if (pData[i]<0x10) Serial.print("0"); 139 | Serial.print(pData[i], HEX); 140 | } 141 | Serial.println(); 142 | break; 143 | } 144 | 145 | default: { 146 | Serial.printf("[%s] Unknown notify data for characteristic %s: 0x", deviceString().c_str(), pBLERemoteCharacteristic->getUUID().toString().c_str()); 147 | for (int i = 0; i < length ; i++) { 148 | if (pData[i]<0x10) Serial.print("0"); 149 | Serial.print(pData[i], HEX); 150 | } 151 | Serial.println(); 152 | } 153 | } 154 | } 155 | 156 | void AM43Client::sendGetBatteryRequest() { 157 | std::vector data{0x1}; 158 | this->sendCommand(AM43_COMMAND_GET_BATTERY, data); 159 | } 160 | 161 | void AM43Client::sendGetLightRequest() { 162 | std::vector data{0x1}; 163 | this->sendCommand(AM43_COMMAND_GET_LIGHT, data); 164 | } 165 | 166 | void AM43Client::sendGetPositionRequest() { 167 | std::vector data{0x1}; 168 | this->sendCommand(AM43_COMMAND_GET_POSITION, data); 169 | } 170 | 171 | void AM43Client::sendPin() { 172 | std::vector data{((uint16_t)this->m_Pin & 0xff00)>>8, ((uint16_t)this->m_Pin & 0xff)}; 173 | this->sendCommand(AM43_COMMAND_LOGIN, data); 174 | } 175 | 176 | void AM43Client::open() { 177 | std::vector data{0xDD}; 178 | this->sendCommand(AM43_COMMAND_MOVE, data); 179 | } 180 | 181 | void AM43Client::stop() { 182 | std::vector data{0xCC}; 183 | this->sendCommand(AM43_COMMAND_MOVE, data); 184 | } 185 | 186 | void AM43Client::close() { 187 | std::vector data{0xEE}; 188 | this->sendCommand(AM43_COMMAND_MOVE, data); 189 | } 190 | 191 | void AM43Client::setPosition(uint8_t pos) { 192 | std::vector data{pos}; 193 | this->sendCommand(AM43_COMMAND_SET_POSITION, data); 194 | } 195 | 196 | void AM43Client::setCommandQueued(boolean status) { 197 | this->m_CommandQueued = status; 198 | } 199 | 200 | void AM43Client::update() { 201 | if (millis() - this->m_LastUpdate > AM43_UPDATE_INTERVAL) { 202 | if (!this->m_LoggedIn) { 203 | this->sendPin(); 204 | return; 205 | } 206 | 207 | if (AM43_UPDATE_BATTERY == 1 && this->m_CurrentQuery == AM43_UPDATE_BATTERY) 208 | this->sendGetBatteryRequest(); 209 | else if (AM43_UPDATE_POSITION == 1 && this->m_CurrentQuery == AM43_UPDATE_BATTERY + AM43_UPDATE_POSITION) 210 | this->sendGetPositionRequest(); 211 | else if (AM43_UPDATE_BATTERY == 1 && this->m_CurrentQuery == AM43_UPDATE_BATTERY + AM43_UPDATE_POSITION + AM43_UPDATE_LIGHT) 212 | this->sendGetLightRequest(); 213 | 214 | this->m_CurrentQuery = this->m_CurrentQuery + 1 > AM43_UPDATE_BATTERY + AM43_UPDATE_POSITION + AM43_UPDATE_LIGHT ? 1 : this->m_CurrentQuery + 1; 215 | this->m_LastUpdate = millis(); 216 | } 217 | } 218 | 219 | byte checksum(std::vector data) { 220 | uint8_t checksum = 0; 221 | for (int i = 0; i < data.size() ; i++) 222 | checksum = checksum ^ data[i]; 223 | checksum = checksum ^ 0xff; 224 | return checksum; 225 | } 226 | 227 | void AM43Client::sendCommand(uint8_t command, std::vector data) { 228 | std::vector sendData; 229 | for (int i=0; i < startPkt.size(); i++) 230 | sendData.push_back(startPkt[i]); 231 | sendData.push_back(command); 232 | sendData.push_back((char)data.size()); 233 | sendData.insert(sendData.end(), data.begin(), data.end()); 234 | sendData.push_back(checksum(sendData)); 235 | Serial.printf("[%s] AM43 Send: ", deviceString().c_str()); 236 | for (int i = 0; i < sendData.size() ; i++) { 237 | if (sendData[i]<0x10) Serial.print("0"); 238 | Serial.print(sendData[i], HEX); 239 | } 240 | Serial.println(); 241 | m_Char->writeValue(&sendData[0], sendData.size(), false); 242 | } 243 | 244 | boolean AM43Client::connectToServer(notify_callback callback) { 245 | Serial.printf("Attempting to connect to: %s ", this->m_Device->getAddress().toString().c_str()); 246 | unsigned long connectStart = millis(); 247 | this->m_DoConnect = false; 248 | 249 | this->m_Client = BLEDevice::createClient(); 250 | this->m_Client->setClientCallbacks(this); 251 | 252 | this->m_Connected = false; 253 | // Connect to the remote BLE Server. 254 | Serial.print("..."); 255 | #ifdef USE_NIMBLE 256 | if (this->m_Client->connect(this->m_Device, false)) { 257 | #else 258 | if (this->m_Client->connect(this->m_Device)) { 259 | #endif 260 | Serial.println(" - Connected to server"); 261 | } else { 262 | Serial.println(" - Failed to connect."); 263 | this->m_Disconnected = true; 264 | this->m_Connected = false; 265 | return false; 266 | } 267 | #ifdef USE_NIMBLE 268 | this->m_Client->discoverAttributes(); 269 | #endif 270 | 271 | 272 | // Obtain a reference to the service we are after in the remote BLE server. 273 | BLERemoteService* pRemoteService = m_Client->getService(AM43_SERVICE_UUID); 274 | if (pRemoteService == nullptr) { 275 | Serial.print("Failed to find our service UUID: "); 276 | Serial.println(serviceUUID.toString().c_str()); 277 | this->m_Client->disconnect(); 278 | return false; 279 | } 280 | 281 | // Obtain a reference to the characteristic in the service of the remote BLE server. 282 | m_Char = pRemoteService->getCharacteristic(AM43_CHAR_UUID); 283 | if (m_Char == nullptr) { 284 | Serial.print("Failed to find our characteristic UUID: "); 285 | Serial.println(charUUID.toString().c_str()); 286 | this->m_Client->disconnect(); 287 | return false; 288 | } 289 | 290 | if(this->m_Char->canNotify()) 291 | this->m_Char->registerForNotify(callback); 292 | 293 | this->m_Connected = true; 294 | Serial.printf("Connect took %dms\r\n", millis()-connectStart); 295 | return true; 296 | } 297 | 298 | void AM43Client::disconnectFromServer() { 299 | this->m_Client->disconnect(); 300 | } 301 | -------------------------------------------------------------------------------- /src/AM43Client.h: -------------------------------------------------------------------------------- 1 | #ifndef AM43CLIENT_H_ 2 | #define AM43CLIENT_H_ 3 | 4 | // Comment out to use the legacy BLE stack instead. 5 | #define USE_NIMBLE 6 | 7 | #ifdef USE_NIMBLE 8 | #include "NimBLEDevice.h" 9 | #else 10 | #include "BLEDevice.h" 11 | #endif 12 | 13 | #define AM43_SERVICE_UUID "0000fe50-0000-1000-8000-00805f9b34fb" 14 | #define AM43_CHAR_UUID "0000fe51-0000-1000-8000-00805f9b34fb" 15 | 16 | #define AM43_COMMAND_MOVE 0x0A 17 | #define AM43_COMMAND_SET_POSITION 0x0D 18 | #define AM43_NOTIFY_POSITION 0xA1 19 | #define AM43_COMMAND_GET_BATTERY 0xA2 20 | #define AM43_COMMAND_GET_POSITION 0xA7 21 | #define AM43_REPLY_UNKNOWN1 0xA8 22 | #define AM43_REPLY_UNKNOWN2 0xA9 23 | #define AM43_COMMAND_GET_LIGHT 0xAA 24 | #define AM43_COMMAND_LOGIN 0x17 25 | #define AM43_RESPONSE_ACK 0x5A 26 | #define AM43_RESPONSE_NACK 0xA5 27 | #define AM43_DEFAULT_PIN 8888 28 | 29 | #define AM43_UPDATE_INTERVAL 30000 // Frequency to poll battery/position/light. 30 | #define AM43_UPDATE_BATTERY 1 // Update battery? 31 | #define AM43_UPDATE_POSITION 1 // Update position? 32 | #define AM43_UPDATE_LIGHT 1 // Update light? 33 | 34 | extern BLEUUID serviceUUID; 35 | extern BLEUUID charUUID; 36 | 37 | class AM43Callbacks; 38 | 39 | class AM43Client : public BLEClientCallbacks { 40 | public: 41 | AM43Client(BLEAdvertisedDevice*); 42 | AM43Client(BLEAdvertisedDevice *d, uint16_t pin); 43 | 44 | BLEAdvertisedDevice* m_Device; 45 | BLEClient* m_Client; 46 | BLERemoteCharacteristic* m_Char; 47 | std::string m_Name; 48 | // We are connected. 49 | boolean m_Connected; 50 | // Previously (attempted) connected, now disconnected. 51 | boolean m_Disconnected; 52 | // True to start a connection. 53 | boolean m_DoConnect; 54 | // We're logged in (correct pin) 55 | boolean m_LoggedIn; 56 | // OnDemand command flag 57 | boolean m_CommandQueued; 58 | 59 | // Latest battery level (percent) 60 | unsigned char m_BatteryPercent; 61 | // Latest closed amount (0=open, 100=closed) 62 | unsigned char m_OpenLevel; 63 | unsigned char m_LightLevel; 64 | int m_Rssi; 65 | 66 | void onConnect(BLEClient* pclient); 67 | void onDisconnect(BLEClient* pclient); 68 | 69 | void setClientCallbacks(AM43Callbacks *callbacks); 70 | void myNotifyCallback(BLERemoteCharacteristic*, uint8_t*, size_t, bool); 71 | 72 | boolean connectToServer(notify_callback); 73 | void disconnectFromServer(); 74 | 75 | void sendGetBatteryRequest(); 76 | void sendGetLightRequest(); 77 | void sendGetPositionRequest(); 78 | 79 | void update(); 80 | 81 | void open(); 82 | void stop(); 83 | void close(); 84 | void setPosition(uint8_t); 85 | void setCommandQueued(boolean); 86 | 87 | protected: 88 | void sendPin(); 89 | void sendCommand(uint8_t, std::vector); 90 | uint16_t m_Pin; 91 | AM43Callbacks *m_ClientCallbacks; 92 | unsigned long m_LastUpdate; 93 | String deviceString(); 94 | uint8_t m_CurrentQuery; 95 | }; 96 | 97 | 98 | class AM43Callbacks { 99 | public: 100 | virtual ~AM43Callbacks() {}; 101 | virtual void onPosition(uint8_t position) = 0; 102 | virtual void onBatteryLevel(uint8_t level) = 0; 103 | virtual void onLightLevel(uint8_t level) = 0; 104 | virtual void onConnect(AM43Client*) = 0; 105 | virtual void onDisconnect(AM43Client*) = 0; 106 | }; 107 | 108 | #endif 109 | --------------------------------------------------------------------------------