Building a integration for a Bluetooth Low Energy (BLE) adjustable bed

Hi there,
I have for a while been trying to make Home Assistant installed as Generic x86-64 communicate with my adjustable bed controller from Linak using a bluetooth adapter internally mounted in the computer.

To summarize this is where I am at now:

I have sniffed out the communications from the original android app “Bed Control” by Linak to the bed, and have figured it sends write requests to a attribute with UUID 99FA0002-338A-1024-8A49-009C0215F78A.
The values it sends are for example “9400” to toggle a light, “0a00” to lower the back, “0b00” to raise it and so on.
image

For anyone wondering: This was a great tutorial to sniff out the bluetooth traffic, combined with using the app “BLE scanner” to get the correct attribute.

I have managed to find the device and select the correct attribute in the Terminal app with the “bluetoothctl” command, but sending for example “write 9400” does not work.

Other BLE devices in my home show up in the integrations tab but a common denominator is that all of them have existing integrations already made for them

At this point I see two options:

  1. Write my own integration, however I am struggling to find the correct documentation needed to do this
  2. Figure out the correct format for the “write” command under the GATT menu in Terminal. I have unfortunately not been able to find any applicable or sufficiently detailed documentations to do this either.

In short I know what data to send and where to send it, but not how to send it.
Are there any of you out there that could help me or point me in the right direction?

I consider the best option here to be writing a integration that could be used as a template for “any” BLE device, although that does seem a bit unlikely to be doable.
I will of course update the thread if I happen to figure it out.
Thank you!

3 Likes

Nice tutorial and write up! I also am struggling to make a BLE integration (for a scale). I have asked on discord for some help as there is not a simple tutorial anywhere. Just gold-plated integrations which are hard to understand.

Sounds like you figured out enough to do a ESPHome integration. From the docs, it seems you can write a value to a specific characteristic.
https://esphome.io/components/ble_client.html

Thank you for the reply!

I forgot to mention I am doing this with a internal bluetooth card on a minicomputer from I believe Dell or HP and do not own a ESP32, although it does seem more and more likely that I will have to buy myself one.

As far as I have understood ESPHome is only for use with ESP32, or is it possible to use with a internally mounted bluetooth card on a x86-64 system?

Thanks

Correct, esphome is just for ESP32.

Hello

I too have a set of beds with Linak TD4 and it also looking into “hacking” the remote.

Have you tried sending the command with GATTTOOL, see the post:

I wil try with a old RPi 3B+ and see if I can make it work…

Hi!

I am unfortunately unable to use gatttool, as my installation is the generic x86-x64 one.
Or am I mistaken? Is it possible to install? Sudo commands do not work in the terminal at least.

Do you have a normal computer with bluetooth, not home assistant? Windows or linux? That is the easiest way to figure out to send BLE commands. You want to use the bleak library, as that is what HA uses for bluetooth control. You can look at my RD200 integration for some clues: https://github.com/jdeath/rd200v2 . I also have a simple python script there which shows how to interact with the device with the library. Making a python script is relatively easy, the hard part is getting into a HA integration.

Hi @Duus :slight_smile: This is great - I want to do exactly the same. It would be cool to have an esp32 controlling the bed :smiley: I have also sniffed “some” of the values that you point out. I can get them to work sending the commands with this IOS app ‎nRF Connect for Mobile i App Store

Since I started working on this, I were so lucky to get my hands on the official Wifi2lin module for connecting the bed to the Linak cloud service. I have it working now with Google assistant through the new Google SDK in Home Assistant (Formerly known as the Google Relay). It works most of the time, but us who mess around with HA wants to go local - so it would be cool to have an ESP32 as hub. I have been trying to make a BLE node as @jaaem points out, but I cannot get the EPS32 to connect to the bed :frowning: It would be awesome if someone could crack the nut - what about you @J-Lindvig :slight_smile:

I’m working on building BLE support into my smartbed-mqtt addon. GitHub - richardhopton/smartbed-mqtt

If you have a BLE bed then I can probably build “native” local support for it

1 Like

That would be amazing!
Let me know if you need more details than what i provided in the original post

Head on over to the discord linked in my github repo and we can work it out together.

Possibly my ESP32 setup can help you: How to setup ESPHome to control my Bluetooth controlled (Octocontrol) bed - ESPHome - Home Assistant Community (home-assistant.io)

Anyone got further with building a ble app for the HA?
I have my Linak bed 100% controled now, with height of head/legs and control… Even use a HACS thermostat to control legs/head from 0-100%. And i can use it with google… BUT it runs on my esp32, which is not good in switching between wifi and ble because it uses the same frequency…

1 Like

Is there a HACS integration that makes me able to control my Linak moters? I’m getting my new bed in 2 weeks, and would like if i cound use it in my HA setup.

How do you do that ?

Hi all

I managed to do the following with a Linak engined elevation bed. It works, but it is quite annoying, that I have to press press press the buttons to get the desired positions:) Anyone have a better approach?

esphome:
  name: linak-jacob-seng
  friendly_name: linak-jacob-seng

esp32:
  board: esp32dev
  framework:
    type: esp-idf

# Enable logging
logger:
  level: DEBUG

# Enable Home Assistant API
api:

ota:


wifi:
  ssid: SSID
  password: PASS
  fast_connect: true  

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "GW"
    password: "PASS"

captive_portal: 



esp32_ble_tracker:
  on_ble_advertise:
    - mac_address:
        - D2:3E:FE:30:27:F9
      then:
        - lambda: |-
            ESP_LOGD("ble_adv", "New BLE device");
            ESP_LOGD("ble_adv", "  address: %s", x.address_str().c_str());
            ESP_LOGD("ble_adv", "  name: %s", x.get_name().c_str());
            ESP_LOGD("ble_adv", "  Advertised service UUIDs:");
            for (auto uuid : x.get_service_uuids()) {
                ESP_LOGD("ble_adv", "    - %s", uuid.to_string().c_str());
            }
            ESP_LOGD("ble_adv", "  Advertised service data:");
            for (auto data : x.get_service_datas()) {
                ESP_LOGD("ble_adv", "    - %s: (length %i)", data.uuid.to_string().c_str(), data.data.size());
            }
            ESP_LOGD("ble_adv", "  Advertised manufacturer data:");
            for (auto data : x.get_manufacturer_datas()) {
                ESP_LOGD("ble_adv", "    - %s: (length %i)", data.uuid.to_string().c_str(), data.data.size());
            }    


binary_sensor:
  # Presence based on MAC address
  - platform: ble_presence
    mac_address: D2:3E:FE:30:27:F9
    name: "Jacob Seng Lys BLE Status"

sensor:
  # RSSI based on MAC address
  - platform: ble_rssi
    mac_address: D2:3E:FE:30:27:F9 
    name: "Jacob Seng Lys BLE RSSI"   





ble_client:
  - mac_address: D2:3E:FE:30:27:F9
    id: seng_1
    on_connect:
      then:
        - lambda: |-
            ESP_LOGD("ble_client_lambda", "Connected To Jacobs Bed"); 
    on_disconnect:
      then:
        - lambda: |-
            ESP_LOGD("ble_client_lambda", "Disconnected from Jacobs Bed");        






switch:
  - platform: template
    name: "Jacob Seng Lys"
    id: jacob_seng_light_switch
    optimistic: true
    turn_on_action: 
    - delay: 1s  
    - ble_client.ble_write:
        id: seng_1
        service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
        characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
        value: [0x92, 0x00]
    turn_off_action: 
    - delay: 1s  
    - ble_client.ble_write:
        id: seng_1
        service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
        characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
        value: [0x93, 0x00] 

  - platform: restart
    name: "Restart"  

  - platform: ble_client
    ble_client_id: seng_1
    name: "Enable BLE"     

button:

  - platform: template
    name: "Head up"
    id: head_up
    icon: "mdi:bed"
    on_press:
      - repeat:
          count: 10
          then:
            - ble_client.ble_write:
                id: seng_1
                service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
                characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
                value: [0x0B, 0x00]     

  - platform: template
    name: "Head down"
    id: head_down
    icon: "mdi:bed"
    on_press:
      - repeat:
          count: 10
          then:
            - ble_client.ble_write:
                id: seng_1
                service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
                characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
                value: [0x0A, 0x00]     

  - platform: template
    name: "Feet up"
    id: feet_up
    icon: "mdi:bed"
    on_press:
      - repeat:
          count: 10
          then:
            - ble_client.ble_write:
                id: seng_1
                service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
                characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
                value: [0x09, 0x00]     

  - platform: template
    name: "Feet down"
    id: feet_down
    icon: "mdi:bed"
    on_press:
      - repeat:
          count: 10
          then:
            - ble_client.ble_write:
                id: seng_1
                service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
                characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
                value: [0x08, 0x00]                                  

  - platform: template
    name: "Both up"
    id: both_up
    icon: "mdi:bed"
    on_press:
      - repeat:
          count: 10
          then:
            - ble_client.ble_write:
                id: seng_1
                service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
                characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
                value: [0x01, 0x00]                           

  - platform: template
    name: "Both down"
    id: both_down
    icon: "mdi:bed"
    on_press:
      - repeat:
          count: 10
          then:
            - ble_client.ble_write:
                id: seng_1
                service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
                characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
                value: [0x00, 0x00]

  - platform: template
    name: "Store Position"
    id: store
    icon: "mdi:bed"
    on_press:
      then:
        - ble_client.ble_write:
            id: seng_1
            service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
            characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
            value: [0x38] 

  - platform: template
    name: "Preset 1"
    id: preset_1
    icon: "mdi:bed"
    on_press:
      - repeat:
          count: 1
          then:
            - ble_client.ble_write:
                id: seng_1
                service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
                characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
                value: [0x0E, 0x00] 

some updated code

substitutions:
  device_name: 'linak-jacob-seng'
  friendly_name: 'Jacob Seng'
  device_description: "Bluetooth kontrol af elevationsseng"
  project_version: "1.0.0"
  log_level: DEBUG
  mac_address: "D2:3E:FE:30:27:F9" ## Change this to your beds unique mac-addresse 

packages:
  device_base: !include common/base.yaml

esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}

esp32:
  board: esp32dev
  framework:
    type: esp-idf 

globals:
  # To store the Linak Bed Connection Status
  - id: ble_client_connected
    type: bool
    initial_value: 'false'

esp32_ble_tracker:

ble_client:
  - mac_address: ${mac_address}
    id: seng_1
    on_connect:
      then:
        # Update the Linak Bed Connection Status
        - lambda: |-
            id(ble_client_connected) = true;
            ESP_LOGD("ble_client_lambda", "${friendly_name}");
        - delay: 5s
       # Update Linak Bed height sensors after bluetooth is connected
        - lambda: |-
            id(jacob_head_height).update();
            id(jacob_feet_height).update();             
    on_disconnect:
      then:
        - lambda: |-
            id(ble_client_connected) = false;
            ESP_LOGD("ble_client_lambda", "${friendly_name}");     
        
binary_sensor:
  # Presence based on MAC address
  - platform: ble_presence
    mac_address: ${mac_address}
    name: "${friendly_name} BLE Status"

  # Bed Bluetooth Connection Status
  - platform: template
    name: '${friendly_name} Connection'
    id: desk_connection
    lambda: 'return id(ble_client_connected);'    

sensor:
  # RSSI based on MAC address
  - platform: ble_rssi
    mac_address: ${mac_address} 
    name: "${friendly_name} BLE RSSI" 

  # Jacob Head Height Sensor
  - platform: ble_client
    type: characteristic
    ble_client_id: seng_1
    id: jacob_head_height
    name: '${friendly_name} hoved højde'
    service_uuid: '99fa0020-338a-1024-8a49-009c0215f78a'
    characteristic_uuid: '99fa0028-338a-1024-8a49-009c0215f78a'
    icon: 'mdi:arrow-up-down'
    unit_of_measurement: 'cm'
    accuracy_decimals: 1
    update_interval: never
    notify: true
    lambda: |-
      uint16_t raw_height = ((uint16_t)x[1] << 8) | x[0];
      unsigned short height_mm = raw_height /5;
 
      return (float) height_mm /5;
 
  # Jacob Feet Height Sensor
  - platform: ble_client
    type: characteristic
    ble_client_id: seng_1
    id: jacob_feet_height
    name: '${friendly_name} fod højde'
    service_uuid: '99fa0020-338a-1024-8a49-009c0215f78a'
    characteristic_uuid: '99fa0027-338a-1024-8a49-009c0215f78a'
    icon: 'mdi:arrow-up-down'
    unit_of_measurement: 'cm'
    accuracy_decimals: 1
    update_interval: never
    notify: true
    lambda: |-
      uint16_t raw_height = ((uint16_t)x[1] << 8) | x[0];
      unsigned short height_mm = raw_height / 6;
 
      return (float) height_mm / 5;  

  # Desk Speed Sensor
  - platform: ble_client
    type: characteristic
    ble_client_id: seng_1
    id: bed_speed
    name: 'Jacob seng Speed'
    service_uuid: '99fa0020-338a-1024-8a49-009c0215f78a'
    characteristic_uuid: '99fa0028-338a-1024-8a49-009c0215f78a'
    icon: 'mdi:speedometer'
    unit_of_measurement: 'cm/min' # I'm not sure this unit is correct
    accuracy_decimals: 0
    update_interval: never
    notify: true
    lambda: |-
      uint16_t raw_speed = ((uint16_t)x[3] << 8) | x[2];
      return raw_speed / 100;


### Lys under seng ####

switch:
  - platform: template
    name: "${friendly_name} Lys"
    id: light_bed
    optimistic: true
    turn_on_action: 
    - delay: 1s  
    - ble_client.ble_write:
        id: seng_1
        service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
        characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
        value: [0x92, 0x00]
    turn_off_action: 
    - delay: 1s  
    - ble_client.ble_write:
        id: seng_1
        service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
        characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
        value: [0x93, 0x00] 

### Lys under seng #### 

  - platform: ble_client
    ble_client_id: seng_1
    name: "${friendly_name} BLE" 

cover:

### Hoved + ben 

  - platform: template
    name: "${friendly_name} Both"
    id: both_actuators
    assumed_state: true
    icon: "mdi:bed"
    open_action:
      - script.execute: jacob_op
      - logger.log: "Repeat both actuators UP"
    stop_action:
      - script.execute: jacob_stop
      - logger.log: "Action stopped"     
    close_action:
      - script.execute: jacob_ned
      - logger.log: "Repeat both actuators DOWN"

### Hoved  

  - platform: template
    name: "${friendly_name} Head"
    id: head_actuator
    assumed_state: true
    icon: "mdi:bed"
    open_action:
      - script.execute: jacob_hoved_op
      - logger.log: "Repeat head actuator UP"
    stop_action:
      - script.execute: jacob_stop
      - logger.log: "Action stopped"      
    close_action:
      - script.execute: jacob_hoved_ned
      - logger.log: "Repeat head actuator DOWN"    

### Ben 

  - platform: template
    name: "${friendly_name} Legs"
    id: leg_actuator
    assumed_state: true
    icon: "mdi:bed"
    open_action:
      - script.execute: jacob_ben_op
      - logger.log: "Repeat legs actuator UP"
    stop_action:
      - script.execute: jacob_stop
      - logger.log: "Action stopped"      
    close_action:
      - script.execute: jacob_ben_ned
      - logger.log: "Repeat leg actuator DOWN"      
                                                            

button:

### Presets mv.   

  - platform: template
    name: "${friendly_name} Store Preset 1"
    id: store_preset_1
    icon: "mdi:file-account"
    on_press:
      then:
        - ble_client.ble_write:
            id: seng_1
            service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
            characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
            value: [0x38, 0x00] 
        - logger.log: "Preset 1 Saved"             

  - platform: template
    name: "${friendly_name} Store Preset 2"
    id: store_preset_2
    icon: "mdi:file-account"
    on_press:
      then:
        - ble_client.ble_write:
            id: seng_1
            service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
            characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
            value: [0x39, 0x00]
        - logger.log: "Preset 2 Saved"              

  - platform: template
    name: "${friendly_name} Store Preset 3"
    id: store_preset_3
    icon: "mdi:file-account"
    on_press:
      then:
        - ble_client.ble_write:
            id: seng_1
            service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
            characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
            value: [0x3A, 0x00] 
        - logger.log: "Preset 3 Saved"      

  - platform: template
    name: "${friendly_name} Store Preset 4"
    id: store_preset_4
    icon: "mdi:file-account"
    on_press:
      then:
        - ble_client.ble_write:
            id: seng_1
            service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
            characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
            value: [0x45, 0x00] 
        - logger.log: "Preset 4 Saved"                  

  - platform: template
    name: "${friendly_name} Run Preset 1"
    id: preset_1
    icon: "mdi:head-question-outline"
    on_press:
      - repeat:
          count: 100
          then:
            - ble_client.ble_write:
                id: seng_1
                service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
                characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
                value: [0x0E, 0x00] 
            - delay: 300ms
            - logger.log: "Run preset 1"     

  - platform: template
    name: "${friendly_name} Run Preset 2"
    id: preset_2
    icon: "mdi:head-question-outline"
    on_press:
      - repeat:
          count: 100
          then:
            - ble_client.ble_write:
                id: seng_1
                service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
                characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
                value: [0x0F, 0x00] 
            - delay: 300ms
            - logger.log: "Run preset 2"

  - platform: template
    name: "${friendly_name} Run Preset 3"
    id: preset_3
    icon: "mdi:head-question-outline"
    on_press:
      - repeat:
          count: 100
          then:
            - ble_client.ble_write:
                id: seng_1
                service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
                characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
                value: [0x0C, 0x00] 
            - delay: 300ms
            - logger.log: "Run preset 3" 

  - platform: template
    name: "${friendly_name} Run Preset 4"
    id: preset_4
    icon: "mdi:head-question-outline"
    on_press:
      - repeat:
          count: 100
          then:
            - ble_client.ble_write:
                id: seng_1
                service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
                characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
                value: [0x44, 0x00] 
            - delay: 300ms
            - logger.log: "Run preset 4" 

script:
  # Turn Head up and down (standalone)
  - id: jacob_hoved_op
    then:
      - ble_client.ble_write:
          id: seng_1
          service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
          characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
          value: [0x0b, 0x00]
      - delay: 0.05s  

  - id: jacob_hoved_ned
    then:
      - ble_client.ble_write:
          id: seng_1
          service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
          characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
          value: [0x0a, 0x00]
      - delay: 0.05s    

  # Turn legs up and down (standalone)
  - id: jacob_ben_op
    then:
      - ble_client.ble_write:
          id: seng_1
          service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
          characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
          value: [0x09, 0x00]
      - delay: 0.05s  

  - id: jacob_ben_ned
    then:
      - ble_client.ble_write:
          id: seng_1
          service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
          characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
          value: [0x08, 0x00]
      - delay: 0.05s

  # Turn legs and head up and down (both actuators)
  - id: jacob_op
    then:
      - ble_client.ble_write:
          id: seng_1
          service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
          characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
          value: [0x01, 0x00]
      - delay: 0.05s  

  - id: jacob_ned
    then:
      - ble_client.ble_write:
          id: seng_1
          service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
          characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
          value: [0x00, 0x00]
      - delay: 0.05s            
                 
  # Stop movement
  - id: jacob_stop
    then:
      - ble_client.ble_write:
          id: seng_1
          service_uuid: 99fa0001-338a-1024-8a49-009c0215f78a
          characteristic_uuid: 99fa0002-338a-1024-8a49-009c0215f78a
          value: [0xFF, 0x00] 
      - delay: 0.05s