Still no finished product, just showcasing the implementation
As I got one of those proprietary dutch bikes I wanted to implement it in a way in my homeassistant dashboard (Even the picture is generated from the API)
Presence detection (typycally i store it in the basement, where it can be charged etc.) so i added two lines to a BT-Proxy device (PoESP32/M5 Stack, but that could be anything else)
ESPHome-BT-Config
substitutions:
vanmoof_s5_mac: "12:34:56:C0:FF:EE"
# Bluetooth settings
esp32_ble_tracker:
scan_parameters:
duration: 10s # https://github.com/esphome/issues/issues/1408#issuecomment-696834035
ble_client:
- id: ble_vanmoof_s5
auto_connect: false
# No autoconnect, just a couple of checks: https://esphome.io/components/ble_client.html#ble-client-connect-action
mac_address: ${vanmoof_s5_mac}
on_connect:
then:
- lambda: |-
ESP_LOGD("BLE", "VanMoof S5 verbunden");
on_disconnect:
then:
- lambda: |-
ESP_LOGD("BLE", "VanMoof S5 getrennt");
bluetooth_proxy:
active: true # activate bluetooth proxy
binary_sensor:
- platform: ble_presence
id: ble_vanmoof_s5_presence
name: "BLE Presence Vanmoof S5"
icon: "mdi:bike"
mac_address: ${vanmoof_s5_mac}
So that results in not only the device to be triangulated (if used) by Bermuda BLE, but also to be detected in HA-Cards:
But now we will face the API in order to gain insights on the bike speed and odometer.
I assume that you want to fetch data for one bike only (so that could be simplified), if you got multiple bikes, you can do it more sophisticated with regex and search for the serialnumber.
Requirement: please add the vm-token as a secret. It can be generated using shell/powershell etc: generate vm-token
its just the username and password as base64:
printf â%s:%sâ â$usernameâ â$passwordâ | base64
configuration.yaml
rest:
- resource: "https://my.vanmoof.com/api/v8/authenticate"
#scan_interval: 28800 # 8h
scan_interval: 14400 # 4h
timeout: 30 # Reduzierter Timeout fĂźr bessere Reaktionszeit
method: "POST"
headers:
Accept: "application/json"
Authorization: !secret vm_token
Api-Key: "fcb38d47-f14b-30cf-843b-26283f6a5819"
sensor:
- name: "VM Auth" # Token for Bike API and App Token
unique_id: vanmoof_auth_token
force_update: true
value_template: "OK"
json_attributes:
- token
- refreshToken
- resource: "https://api.vanmoof-api.com/v8/getApplicationToken"
scan_interval: 7200 # Alle 2 Stunden
timeout: 30
method: "GET"
headers:
Accept: "application/json"
Authorization: "Bearer {{ state_attr('sensor.vm_auth', 'token') }}"
Api-Key: "fcb38d47-f14b-30cf-843b-26283f6a5819"
sensor:
- name: "VM Application Token" # Used for rides API
unique_id: vanmoof_app_token
force_update: true
value_template: "OK"
json_attributes:
- token
availability: >
{{ states.sensor.vm_auth.state == "OK" and
state_attr('sensor.vm_auth', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_auth.last_updated)) < 14400 }}
- resource: "https://my.vanmoof.com/api/v8/getCustomerData?includeBikeDetails"
scan_interval: 31536000 # 1Y
timeout: 30
method: "GET"
headers:
Accept: "application/json"
Authorization: "Bearer {{ state_attr('sensor.vm_auth', 'token') }}"
Api-Key: "fcb38d47-f14b-30cf-843b-26283f6a5819"
sensor:
- name: "VM API customer details"
unique_id: vanmoof_api_cust
value_template: "OK"
json_attributes:
- data
availability: >
{{ states.sensor.vm_auth.state == "OK" and
state_attr('sensor.vm_auth', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_auth.last_updated)) < 14400 }}
- name: "VM User UUID"
unique_id: vanmoof_data_uuid
value_template: "{{ state_attr('sensor.vm_api_customer_details', 'data')['uuid'] }}"
availability: >
{{ states.sensor.vm_auth.state == "OK" and
state_attr('sensor.vm_auth', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_auth.last_updated)) < 14400 }}
- name: "VM Bike0 ID"
unique_id: vanmoof_data_bikes_0_id
value_template: "{{ state_attr('sensor.vm_api_customer_details', 'data')['bikes'][0]['id'] }}"
availability: >
{{ states.sensor.vm_auth.state == "OK" and
state_attr('sensor.vm_auth', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_auth.last_updated)) < 14400 }}
- name: "VM Bike0 frame number"
unique_id: vanmoof_data_bikes_0_frame_number
value_template: "{{ state_attr('sensor.vm_api_customer_details', 'data')['bikes'][0]['frameNumber'] }}"
availability: >
{{ states.sensor.vm_auth.state == "OK" and
state_attr('sensor.vm_auth', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_auth.last_updated)) < 14400 }}
- resource_template: https://bikeapi.production.vanmoof.cloud/bikes/{{ states('sensor.vm_bike0_frame_number') }}/create_certificate
scan_interval: 604800 # 1W
timeout: 30
method: "POST"
payload: '{ "public_key" : "xxx }'
headers:
Accept: "*/*"
Authorization: "Bearer {{ state_attr('sensor.vm_application_token', 'token') }}"
Content-Type: "application/json"
sensor:
- name: "VM bike certificate"
unique_id: vanmoof_api_bike_cert
value_template: "OK"
json_attributes:
- created_at
- expiry
- certificate
availability: >
{{ states('sensor.vm_bike0_frame_number') is not none and
state_attr('sensor.vm_application_token', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_application_token.last_updated)) < 7200 }}
- resource_template: https://tenjin.vanmoof.com/api/v1/rides/{{ states('sensor.vm_user_uuid') }}/{{ states('sensor.vm_bike0_id') }}/weekly?limit=15
scan_interval: 14400 # 8h
timeout: 30
method: "GET"
headers:
Accept: "*/*"
Authorization: "Bearer {{ state_attr('sensor.vm_application_token', 'token') }}"
Api-Key: "fcb38d47-f14b-30cf-843b-26283f6a5819"
Time-Zone: "Europe/Berlin"
Accept-Language: "de_DE"
sensor:
- name: "VM ride stats"
unique_id: vanmoof_api_rides
icon: mdi:counter
value_template: "OK"
json_attributes:
- carousel
availability: >
{{ states('sensor.vm_user_uuid') is not none and
states('sensor.vm_bike0_id') is not none and
state_attr('sensor.vm_application_token', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_application_token.last_updated)) < 7200 }}
- name: "VM total rides"
unique_id: vanmoof_total_rides
value_template: "{{ state_attr('sensor.vm_ride_stats', 'carousel')['summary']['totalRides'] }}"
state_class: "total_increasing"
availability: >
{{ states.sensor.vm_application_token.state == "OK" and
state_attr('sensor.vm_application_token', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_application_token.last_updated)) < 7200 }}
- name: "VM total distance"
unique_id: vanmoof_total_distance
unit_of_measurement: 'km'
icon: mdi:map-marker-distance
value_template: "{{ state_attr('sensor.vm_ride_stats', 'carousel')['summary']['totalDistance'] }}"
state_class: "total_increasing"
availability: >
{{ states.sensor.vm_application_token.state == "OK" and
state_attr('sensor.vm_application_token', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_application_token.last_updated)) < 7200 }}
- name: "VM average speed"
unique_id: vanmoof_average_speed
unit_of_measurement: 'kmh'
icon: mdi:speedometer
value_template: "{{ state_attr('sensor.vm_ride_stats', 'carousel')['summary']['averageSpeed'] }}"
availability: >
{{ states.sensor.vm_application_token.state == "OK" and
state_attr('sensor.vm_application_token', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_application_token.last_updated)) < 7200 }}
- name: "VM average distance"
unique_id: vanmoof_average_distance
unit_of_measurement: 'km'
icon: mdi:map-marker-distance
value_template: "{{ state_attr('sensor.vm_ride_stats', 'carousel')['summary']['averageDistance'] }}"
availability: >
{{ states.sensor.vm_application_token.state == "OK" and
state_attr('sensor.vm_application_token', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_application_token.last_updated)) < 7200 }}
- name: "VM average duration"
unique_id: vanmoof_average_duration
unit_of_measurement: 'min'
icon: mdi:timer-check-outline
value_template: "{{ (state_attr('sensor.vm_ride_stats', 'carousel')['summary']['averageDuration'] | int / 60000) }}"
availability: >
{{ states.sensor.vm_application_token.state == "OK" and
state_attr('sensor.vm_application_token', 'token') is not none and
(as_timestamp(now()) - as_timestamp(states.sensor.vm_application_token.last_updated)) < 7200 }}
You can now fetch the bike/ride stats accordingly. The API is built with different authorization tokens.
- There is one bearer-token, which is provided after signin in using the base64-ed username/password
Authorization: "Bearer {{ state_attr('sensor.vm_auth', 'token') }}"
- Another one is the application token, used for the ride api, but also to generate a certificate for the bike
Authorization: "Bearer {{ state_attr('sensor.vm_application_token', 'token') }}"
I would suggest to fetch the tokens using automations. (If the bike is at home, there is no need to fetch the api more often, but if its outside, you might generate new statistics in the ride API.
This is still not a finished product. The battery might be fetched using ESPHome and the encryption key. Feel free to use this as an inspiration for further investigations.
I got into those API-Endpoints using mitm-proxy and my android/iphone devices.