Room presence using amazfit via notify and esp32s

Here’s what it looks like on my frontend:


image

I tried using room-assistant and it worked pretty well for the last two years until one of my pi 3’s wifi module broke.

I am using room presence to decide whether to turn off the devices in the room if there are no motion for x amount of time and room presence is equal to 0.

Here’s what I am using:
• Amazfit’s GTR 2e / GTS2 / BIP S (Mi band will also work)
• Tasker or HA companion app on Android
• Notify for amazfit (need the intents for heartbeat/notWearing) - this is to determine if the watch is on hand
• ESP 32s for each room

Steps:

  1. Configure amazfit on notify app.
    a. Enable amazfit discoverable
    b. Enable tasker integration for the intents
    c. Configure heart monitor:
			i. interval - 5 mins
			ii. Heart monitor mode - use notify and amazfit device
  1. Configure tasker or companion app to catch the following intents:
    (com.mc.miband.tasker.notWearing & com.mc.miband.heartRateGot)

a. On HA companion app (recommended so it will work with public/private URL)
Go to settings > companion app> manage sensors> Last update trigger
Add the two intents

b. On Tasker (you will need MQTT plugin + external&internal MQTT server):

			i. Create intent received profile for the two intents above.
			ii. Point the profile to execute tasks that passes the intent's value e.g.
				1) Heartrate
					a) Topic: user1/watch/hr
					b) Payload: %value
				2) NotWearing
					a) Topic: user1/watch/onhand
					b) Payload: false

c. Configure mqtt sensor:

  - name: "user1_watch_heartrate"
    unique_id: user1_watch_heartrate
    state_topic: "user1/watch/hr"
    value_template: "{{ value|int(0)}}"
    unit_of_measurement: "bps"
    state_class: measurement

d. Configure mqtt binary sensor:

    - name: "user1_watch_onhand"
      unique_id: user1_watch_onhand
      state_topic: "user1/watch/onhand"
      payload_on: "true"
      payload_off: "false"

e. Automations:
i. watch_onhand_true:

  		  alias: watch_onhand_true
  		  id: watch_onhand_true
  		  mode: parallel
  		  trigger:
  		    - platform: state
  		      entity_id: 'sensor.user1_watch_heartrate'
  		      id: user1
  		  condition:
  		   condition: or
  		   conditions:
  		    - condition: and
  		      conditions:
  		        - condition: state
  		          entity_id: binary_sensor.user1_watch_onhand
  		          state: 'off'
  		        - condition: trigger
  		          id: user1
  		  action:
  		      - choose:
  		          - conditions:
  		                - condition: trigger
  		                  id: user1
  		            sequence:
  		                - service: mqtt.publish
  		                  data:
  		                    topic: "user1/watch/onhand"
  		                    payload: "yes"
  		                    retain: true

ii. watch_onhand_false:

  		  alias: 'watch_onhand_false'
  		  id: watch_onhand_false
  		  mode: parallel
  		  max: 5
  		  trigger:
  		    - platform: event
  		      event_type: android.intent_received
  		      event_data:
  		        intent: com.mc.watch.tasker.notWearing
  		        device_id: <16 character of companion app's device id that you can get from events>listen to events> android.intent_received
  		      id: user1
  		    - platform: numeric_state
  		      entity_id: sensor.user1_watch_heartrate
  		      below: 1
  		      for: 00:10:00
  		      id: user1_hr
  		
  		  action:
  		      - choose:
  		          - conditions:
  		                - condition: or
  		                  conditions:
  		                    - condition: template
  		                      value_template: "{{trigger.id in ('user1','user1_hr')}}"
  		
  		            sequence:
  		                - service: mqtt.publish
  		                  data:
  		                    topic: "user1/watch/onhand"
  		                    payload: "false"
  		                    retain: true

iii. watch_heartrate:

  		  alias: 'watch_heartrate'
  		  id: watch_heartrate
  		  mode: parallel
  		  max: 5
  		  trigger:
  		    - platform: event
  		      event_type: android.intent_received
  		      event_data:
  		        intent: com.mc.watch.heartRateGot
  		        device_id: c44efa41bb151d0e
  		      id: user1
  		  action:
  		      - choose:
  		          - conditions:
  		                - condition: trigger
  		                  id: user1
  		            sequence:
  		                - service: mqtt.publish
  		                  data:
  		                    topic: "user1/watch/hr"
  		                    payload: >-
  		                     {{trigger.event.data.value}}
  		                    retain: true
  1. ESP32s

a. Per each room / esp32, Configure the input_number for RSSI strength at 1meter:

esphome_measuredpower_rp_livingroom:
1) Min: -99
2) Max: -1
3) Unit of measurement: dB

b. Templates:
notes: name of ble rssi entity is rp__user1watch

i. persons_livingroom:

  	    persons_livingroom:
  	       unit_of_measurement: person/s
  	       value_template: >
  	        {% set room = 'livingroom' %}
  	        {% set ns = namespace(total=0,notonhand=0) %}
  	        {% for state in states['sensor'] %}
  	            {% if state.entity_id.startswith('sensor.rp_') and state.state==room %}
  	                {% set ns.total = ns.total + 1 %}
  	            {% endif %}
  	        {% endfor %}
  	        {% if states.binary_sensor.user1_watch_onhand.state=='off' and states.sensor.rp_user1.state==room %} {% set ns.notonhand = ns.notonhand + 1 %} {% endif %}
  	        {% if states.binary_sensor.user2_watch_onhand.state=='off' and states.sensor.rp_user2.state==room %} {% set ns.notonhand = ns.notonhand + 1 %} {% endif %}
  	        {% if states.binary_sensor.user3_watch_onhand.state=='off' and states.sensor.rp_user3.state==room %} {% set ns.notonhand = ns.notonhand + 1 %} {% endif %}
  	        {{ ns.total - ns.notonhand}}

ii. rp_user1 (note: you can set the farthest distance of the room between your device and esp32 on the ns namespace as input_number too)

  		    rp_user1:
  		      value_template: >
  		        {% set device = 'user1watch' %}
  		        {% set ns = namespace(kitchen=5,livingroom=8,entertainmentroom=6.5,laundry=5,masterbedroom=6,home=0,rp=[], min=[]) %}
  		        {% for state in states['sensor'] %}
  		            {% if state.entity_id.startswith('sensor.rp_') and state.entity_id.endswith(device+'_distance')  %}
  		                {% set room = state.entity_id|replace('sensor.rp_','')|replace('_'+device+'_distance','') %}
  		                {% if room=='kitchen' and state.state|float(20) <= ns.kitchen %}
  		                    {% set ns.rp=ns.rp + [state.entity_id] %}
  		                    {% set ns.min=ns.min + [state.state|float(20)] %}
  		                {% elif room=='kitchen' and state.state|float(20) > ns.kitchen and state.state!='unknown' and state.state!='unavailable' %}
  		                    {% set ns.home=ns.home|int(0)+1 %}
  		                {% elif room=='livingroom' and state.state|float(20) <= ns.livingroom %}
  		                    {% set ns.rp=ns.rp + [state.entity_id] %}
  		                    {% set ns.min=ns.min + [state.state|float(20)] %}
  		                {% elif room=='livingroom' and state.state|float(20) > ns.livingroom and state.state!='unknown' and state.state!='unavailable'%}
  		                    {% set ns.home=ns.home|int(0)+1 %}
  		                {% elif room=='entertainmentroom' and state.state|float(20) <= ns.entertainmentroom %}
  		                    {% set ns.rp=ns.rp + [state.entity_id] %}
  		                    {% set ns.min=ns.min + [state.state|float(20)] %}
  		                {% elif room=='entertainmentroom' and state.state|float(20) > ns.entertainmentroom and state.state!='unknown' and state.state!='unavailable' %}
  		                    {% set ns.home=ns.home|int(0)+1 %}
  		                {% elif room=='laundry' and state.state|float(20) <= ns.laundry %}
  		                    {% set ns.rp=ns.rp + [state.entity_id] %}
  		                    {% set ns.min=ns.min + [state.state|float(20)] %}
  		                {% elif room=='laundry' and state.state|float(20) > ns.laundry and state.state!='unknown' and state.state!='unavailable' %}
  		                    {% set ns.home=ns.home|int(0)+1 %}
  		                {% elif room=='masterbedroom' and state.state|float(20) <= ns.masterbedroom %}
  		                    {% set ns.rp=ns.rp + [state.entity_id] %}
  		                    {% set ns.min=ns.min + [state.state|float(20)] %}
  		                {% elif room=='masterbedroom' and state.state|float(20) > ns.masterbedroom and state.state!='unknown' and state.state!='unavailable' %}
  		                    {% set ns.home=ns.home|int(0)+1 %}
  		                {% endif %}
  		            {% endif %}
  		        {% endfor %}
  		        {% if ns.min==[] and ns.home!=0 %} home
  		        {% elif ns.min==[] and ns.home==0 %} not_home
  		        {% else %}
  		          {% set min_d = min(ns.min) %}
  		          {% set min_di = ns.min.index(min_d) %}
  		          {{ ns.rp[min_di]|replace('sensor.rp_','')|replace('_'+device+'_distance','')}}
  		        {% endif %}

c. ESP32 config:
notes:
1) I’ve splitted this so I just need to configure common_sensor_bletracker.yaml if I wanted to change filters on all esp32 devices.
2) You also need to create components/just_Esp_bt.h and copy secrets.yaml to common/
3) Regarding filters, I tested sliding_moving_average and mean but the update is very slow. Throttling average gives me somewhat accurate and it shows unknown if the device is not within esp32 bluetooth range

i. rp_livingroom.yaml:

  	substitutions:
  	  board: featheresp32
  	  device_name: rp_livingroom
  	  friendly_name: rp_livingroom
  	
  	esphome:
  	  name: ${device_name}
  	  platform: ESP32
  	  board: ${board}
  	  includes:
  	    - components/just_esp_bt.h # contains: #include <esp_bt.h>
  	
  	packages:
  	  wifi: !include common/wifi.yaml
  	  esphome: !include common/esphome_esp32bletracker.yaml
  	  bletracker: !include common/sensor_bletracker.yaml

ii. common/esphome_esp32bletracker.yaml:

  	web_server:
  	  port: 80
  	  auth:
  	    username: user1
  	    password: !secret web_pw
  	    
  	# Enable logging
  	logger:
  	  baud_rate: 0
  	  # level: VERY_VERBOSE
  	
  	# Enable Home Assistant API
  	api:
  	  password: !secret esphome
  	  encryption: 
  	   key: !secret encrypt
  	ota:
  	  password: !secret esphome
  	  on_begin:
  	    then:
  	      - logger.log: "OTA: disabling bluetooth ..."
  	      - lambda: '(void)esp_bt_controller_disable();'
  	      - lambda: '(void)esp_bt_controller_deinit();'
  	  on_error:
  	    then:
  	      - logger.log: "OTA error: Waiting 180s before forced reboot"
  	      - delay: 180 sec
  	      - logger.log: "OTA error: Rebooting now"
  	      - lambda: 'App.safe_reboot();'
  	      
  	esp32_ble_tracker:
  	  scan_parameters:
  	    interval: 320ms
  	    window: 160ms
  	    active: true
  	    
  	dashboard_import:
  	  package_import_url: github://esphome/bluetooth-proxies/esp32-generic.yaml@main
  	    
  	bluetooth_proxy:
  	
  	button:
  	  - platform: safe_mode
  	    name: ${device_name}_safemode_boot
  	    entity_category: diagnostic
  	
  	switch:
  	  - platform: restart
  	    name: '${friendly_name} REBOOT'
  	    entity_category: diagnostic

iii. common/sensor_bletracker.yaml

  	sensor: 
  	  - platform: homeassistant
  	    entity_id: input_number.esphome_measuredpower_${device_name}
  	    id: esphome_measuredpower_${device_name}
  	  - platform: ble_rssi
  	    mac_address: xx:xx:xx:xx:xx:xx
  	    name: ${device_name}_user1watch
  	    id: ${device_name}_user1watch
  	    filters:
  	      - throttle_average: 120s
  	    on_value:
  	      then:
  	        - component.update: ${device_name}_user1watch_distance
  	  - platform: template
  	    name: ${device_name}_user1watch_distance
  	    id: ${device_name}_user1watch_distance
  	    lambda: |-
  	      return pow(10, ((id(esphome_measuredpower_${device_name}).state - (id(${device_name}_user1watch).state)) / (10*2.4)));
  	    update_interval: 60s
  	    unit_of_measurement: 'm'
  	
  	  - platform: uptime
  	    name: ${friendly_name} uptime
  	    filters:
  	      - lambda: return x / 60;
  	    unit_of_measurement: "min"
3 Likes

I would like to read battery state of “Amazfit 2 mini” without use of Zepp app (I don’t use BT on phone to conserve battery).
Tested ESPhome esp32_ble_tracker in active mode but received no data.
Do I need some special decoder to be able to read the watch data?
Or it’s simply impossible without BT connection to Zepp@phone?

OK, during the weekend I have found a way to read battery level via BT.
In case someone needs such battery sensor here is the code needed for the ESPHome BTproxy device (replace zeroes with your BT MAC address):

globals:
  - id: global_amazfit_battery_level
    type: float
    initial_value: '0'
    restore_value: no

esp32_ble_tracker:
  scan_parameters:
    #duration: 60s
    interval: 320ms
    window: 60ms
    continuous: true
    active: true

bluetooth_proxy:
  active: true

# BLE devices needed for sensors
ble_client:
  - mac_address: 00:00:00:00:00:00 # change to your MAC
    id: amazfit_gts2mini
    auto_connect: true
    on_connect:
      then:
        - lambda: |-
            ESP_LOGD("ble_client_lambda", "Connected to BLE device");
    on_passkey_request:
      then:
        - logger.log: "Authenticating with passkey..."
        - ble_client.passkey_reply:
            id: amazfit_gts2mini
            passkey: 1234

sensor:
    # BLE devices service sensors
  - platform: ble_client
    id: amazfit_battery_level
    name: "Amazfit Battery level"
    internal: true
    type: characteristic
    ble_client_id: amazfit_gts2mini
    service_uuid: '180f' # Battery Service
    characteristic_uuid: '2a19'  # Battery Level
    notify: true
    #update_interval: 30s
    device_class: battery
    accuracy_decimals: 0
    unit_of_measurement: '%'
    on_value:
      then:
      - lambda: |-
          if ( (x <= 100) and (x >= 1) ) id(global_amazfit_battery_level) = x;          

  - platform: ble_client
    id: amazfit_heart_rate
    name: "Amazfit Heart rate"
    internal: true
    type: characteristic
    ble_client_id: amazfit_gts2mini
    service_uuid: '180d'  # Heart Rate Service
    characteristic_uuid: '2a37'  # Heart Rate
    #descriptor_uuid: '2902'
    notify: true
    update_interval: 30s
    icon: 'mdi:heart'
    #accuracy_decimals: 0
    #unit_of_measurement: 'bpm'
    lambda: |- #https://koen.vervloesem.eu/blog/connecting-to-bluetooth-low-energy-devices-in-esphome/
      uint16_t heart_rate_measurement = x[1];
      if (x[0] & 1) {
          heart_rate_measurement += (x[2] << 8);
      }
      return (float)heart_rate_measurement;

  - platform: template
    id: amazfit_battery
    name: "Amazfit Battery"
    internal: false
    device_class: battery
    accuracy_decimals: 0
    unit_of_measurement: '%'
    lambda: |-
      return id(global_amazfit_battery_level);

Heart rate sensor doesn’t work for me yet (enabled in the watches), maybe Amazfit doesn’t use the standard data format for transmitting…not sure.