Bicicleta Ergométrica Smart!

Minha esposa insistiu e comprei uma Bicicleta Ergométrica para nós. Neste vai e vem e pandemia, ajuda bastante. Nosso modelo é uma ACT! CLB 10 by Caloi. Mas tenho certeza que estas dicas funcionam para a maioria das ergométricas. E o melhor: não altera o sistema original delas. Veja a foto da minha:

É importante apenas que a bicicleta tenha algum marcador de velocidade. Estes marcadores, em geral, são apenas um sensor de proximidade eletromagnético (como os de abertura de porta/janela), que envia um pulso (fecha o circuito) sempre que um “giro” do pedal é completado. Desta forma, o computador de bordo da bicicleta apenas converte os “pulsos por minuto” (que ele recebe do sensor) em km/h e calcula também calorias, distância total, tempo de atividade física. Algumas bicicletas também registram os batimentos cardíacos (não a minha), mas acho desnecessário pois nosso Smart Watch faz isto (e não preciso ficar segurando na bicicleta para medir). Desta forma, este modelo mais simples me foi suficiente.

O que preciso?

  • Uma bicicleta ergométrica com contador de velocidade. Caso a sua não tenha, você pode tentar improvisar um sensor de porta (“Sensor Magnético de Porta”). A minha é uma Act CBL10, que custou 350 reais na OLX.
  • Um esp8266 ou algum semelhante, em que possa flashear com ESPHome. Não consegui fazer com qualidade usando Tasmota. Aqui, usei um NodeMCU v3 que comprei em promoção na Ali por menos de 10 reais.
  • Um carregador de celular antigo, para fornecer energia para o esp8266
  • Um fio comprido para alimentar a bicicleta + tomada macho
  • Dois fios sem emendas para conectar o carregador (em baixo) ao esp8266 (em cima). Usei os fios de uma fonte 12v antiga aqui, que tem de 1,5m. Para mim, suficiente. É importante que fosse sem emendas pois a bicicleta é toda de metal, e se minha esposa levasse um choque na bicicleta o projeto seria cancelado e todas as verbas cortadas kkkkk

Mãos à obra: o hardware

A primeira coisa que fiz foi desparafusar o computador de bordo da bicicleta para confirmar que o sensor de velocidade dela só tinha realmente dois fios. Acertei!

Coloquei um multímetro digital na opção de resistência (para testar a continuidade) e apenas “desencapei” os fios, sem cortar nada. Pedalei um pouco e… Voilà! Descobri que quando o pedal direito fica na posição “para baixo” o circuito “fecha” e o multímetro apita. Era o que eu precisava saber. Indica que tem por ali algum sensor magnético.

Então, fui para a segunda etapa: instalar um esp8266 dentro do painel da bicicleta. Neste caso, tenho sorte que a caixa dele é grande e o esp8266 cabe dentro sem complicações. Então, fiz a ligação dos dois fios com o meu ESP (sem cortar nada apenas adicionando a conexão, para manter o painel original da bicicleta). Veja minha ligação:

  • Fio preto/branco da bicicleta = ground do esp8266
  • Fio preto da bicicleta = d6 do esp8266

OBS: como este sensor também é monitorado pelo painel da bicicleta, ele já recebecia alguma voltagem (duas pilhas alimentam o sistema, então seria de no máximo 3v). Neste caso, caso você ligue “invertido” o esp8266 vai “travar” imediatamente. Aconteceu aqui. Bastou então inverter a polaridade.
OBS2: Não sou o “mestre” da eletrônica, então arrisquei a ligação apenas pois vi que o meu painel colocava menos de 2v, não oferecendo tanto risco ao ESP. Vale a pena verificar a voltagem de sua placa antes.

Por fim, passei pela haste da bicicleta o fio (o sem emenda) saindo do painel até a base da bicicleta, que é protegida pela carcaça de plástico. Desparafusei a base e achei facilmente um espaço para o carregador de celular. Assim, um buraco de parafuso (que não era usado deste modelo) para passar o fio para fora. O resultado foi este da foto:

Agora, eu tinha um ESP funcionando do painel da bicicleta, só “espiando” o sinal das pedaladas. Sem alterar o funcionamento original!

Configuração do software

O software que comanda o esp é o ESPHome (se não sabe, saiba mais aqui: ESPHome no seu Home Assistant). O código que usei para ele foi este:

esphome:
  name: bicicleta
  platform: ESP8266
  board: nodemcuv2

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:
  password: "0907d0d8878091222ae2323210a7b75"

wifi:
  ssid: "OliveiraIoT"
  password: "12345678"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Bicicleta Fallback Hotspot"
    password: "016GfgB23aNu"

captive_portal:

web_server:
  port: 80

sensor:
  - platform: pulse_counter
    pin: 12
    unit_of_measurement: 'km/h'
    update_interval: 5s
    name: 'Velocidade'
    filters:
      - multiply: 0.375  # foi o valor que me deixou mais próximo da velocidade real
    total:
      unit_of_measurement: 'km'
      name: 'Distância'
      filters:
        - multiply: 0.00625  # (1/1000 pulses per kWh) #tentei, mas não consegui usar este cara aqui. Não foi necessário

Sensores

Nos sensores, tive várias coisas para adaptar. E, como são 2 usuários (Ari e Mika), então vou colocar aqui tudo duplicado. Antes de tudo vou explicar o emaranhado:

  • A velocidade km/h chega corretamente do ESP.
  • Criei alguns auxiliares (inputs) para guardar quem é o usuário que está se exercitando agora, e aí determino se a velocidade dele é a mesma da bicicleta ou é zero (caso não seja esta pessoa que está pedalando agora.
  • Usei alguns “utility_meter” para registrar o total de km percorridos, baseado na velocidade. O valor obtido não ficou correto, usei um sensor auxiliar para corrigir o valor (coloquei um “2” no final do nome da entidade para diferenciar).
  • Converti os km pedalados em calorias (usei uma proporção parecida com a que a bicicleta usa originalmente). Mas você pode alternativamente calcular as calorias baseado no tempo de atividade, caso deseje.
  • Por fim, criei uma interface legal usando dois cards customizados (disponíveis para baixar no HACS):
    – mini-graph-card
    – multiple-entity-row

O Utility Meter:

utility_meter:
  bicicleta_km_ari_dia:
    source: sensor.bicicleta_velocidade_ari
    cycle: daily
  bicicleta_km_ari_semana:
    source: sensor.bicicleta_velocidade_ari
    cycle: weekly
  bicicleta_km_ari_mes:
    source: sensor.bicicleta_velocidade_ari
    cycle: monthly
  bicicleta_km_ari_ano:
    source: sensor.bicicleta_velocidade_ari
    cycle: yearly
  bicicleta_km_mika_dia:
    source: sensor.bicicleta_velocidade_mika
    cycle: daily
  bicicleta_km_mika_semana:
    source: sensor.bicicleta_velocidade_mika
    cycle: weekly
  bicicleta_km_mika_mes:
    source: sensor.bicicleta_velocidade_mika
    cycle: monthly
  bicicleta_km_mika_ano:
    source: sensor.bicicleta_velocidade_mika
    cycle: yearly

Sensor Binário para ver se há movimento na bicicleta:

- platform: template
  sensors:
    bicicleta_status:
      device_class: "motion"
      friendly_name: "Estado"
      icon_template: mdi:bicycle
      value_template: >-
        {% if (states('sensor.bicicleta_velocidade')|float > 0) %}
          true
        {% else %}
          false
        {% endif %}

Input Select. É importante que o último elemento da lista seja o “genérico”:

input_select:
  bicicleta_usuario:
    name: "Usuário"
    options:
      - Ari
      - Mika
      - Outro
    icon: mdi:account-box-multiple

Agora alguns sensores. Não coloquei os da minha esposa, mas seria só copiar e colar a parte dos específicos, renomeando onde tiver ari para mika:

###########################
## SENSORES DA BICICLETA
###########################


###########################
# Gerais:

- platform: template
  sensors:
    bicicleta_velocidade2: # correção da velocidade
      friendly_name: "Velocidade"
      unit_of_measurement: "km/h"
      icon_template: mdi:pulse
      value_template: >-
        {% if is_state("sensor.bicicleta_velocidade", "unavailable") %}
          0.0
        {% elif is_state("sensor.bicicleta_velocidade", "unknown") %}
          0.0
        {% else %}
          {{ states('sensor.bicicleta_velocidade')|float }}
        {%- endif %}
    bicicleta_status: #sensor de status personalizado
      friendly_name: "Estado"
      icon_template: mdi:bicycle
      value_template: >-
        {% if (states('sensor.bicicleta_velocidade2')|float > 0) %}
        Em movimento
        {% elif (states('sensor.bicicleta_velocidade2')|float == 0) %}
        Parado
        {% else %}
        Desconhecido
        {% endif %}
- platform: history_stats
  name: Tempo de Bicicleta Total Hoje
  entity_id: binary_sensor.bicicleta_status
  state: "on"
  type: time
  start: "{{ now().replace(hour=0, minute=0, second=0) }}"
  end: "{{ now() }}"


###########################
# Específicos do usuário
# (uma cópia destes para cada usuário da bicicleta)
		
- platform: template
  sensors:
    bicicleta_status_ari:
      friendly_name: "Estado"
      icon_template: mdi:bicycle
      value_template: >-
        {% if (states('sensor.bicicleta_velocidade2')|float > 0) and (states('input_select.bicicleta_usuario') == 'Ari') %}
        Em movimento
        {% else %}
        Parado
        {% endif %}
    bicicleta_velocidade_ari: #velocidade só de Ari
      friendly_name: "Velocidade"
      icon_template: mdi:gauge
      unit_of_measurement: "km"
      value_template: >-
        {% if (states('input_select.bicicleta_usuario') == 'Ari') %}
          {{ ( states('sensor.bicicleta_velocidade2')|float )|round(1) }}
        {% else %}
          0.0
        {% endif %}
    bicicleta_cal_ari_dia: #calorias. 35 calorias por km pedalado (meu calculo)
      friendly_name: "Calorias Diárias"
      unit_of_measurement: "cal"
      icon_template: mdi:water
      value_template: >-
        {{ states('sensor.bicicleta_km_ari_dia2')|float * 35 }}
    bicicleta_cal_ari_semana:
      friendly_name: "Calorias Semana"
      unit_of_measurement: "cal"
      icon_template: mdi:water
      value_template: >-
        {{ states('sensor.bicicleta_km_ari_semana2')|float * 35 }}
    bicicleta_cal_ari_mes:
      friendly_name: "Calorias Mês"
      unit_of_measurement: "cal"
      icon_template: mdi:water
      value_template: >-
        {{ states('sensor.bicicleta_km_ari_mes2')|float * 35 }}
    bicicleta_cal_ari_ano:
      friendly_name: "Calorias Ano"
      unit_of_measurement: "cal"
      icon_template: mdi:water
      value_template: >-
        {{ states('sensor.bicicleta_km_ari_ano2')|float * 35 }}
    bicicleta_km_ari_dia2: #correção da km total (utility meter), para poder ficar correto.
      friendly_name: "Km Hoje"
      icon_template: mdi:gauge
      unit_of_measurement: "km"
      value_template: >-
        {{ (states('sensor.bicicleta_km_ari_dia')|float / 90) |round(1) }}
    bicicleta_km_ari_semana2:
      friendly_name: "Km Semana"
      icon_template: mdi:gauge
      unit_of_measurement: "km"
      value_template: >-
        {{ (states('sensor.bicicleta_km_ari_semana')|float / 90) |round(1) }}
    bicicleta_km_ari_mes2:
      friendly_name: "Km Mês"
      icon_template: mdi:gauge
      unit_of_measurement: "km"
      value_template: >-
        {{ (states('sensor.bicicleta_km_ari_mes')|float / 90) |round(1) }}
    bicicleta_km_ari_ano2:
      friendly_name: "Km Ano"
      icon_template: mdi:gauge
      unit_of_measurement: "km"
      value_template: >-
        {{ (states('sensor.bicicleta_km_ari_ano')|float / 90) |round(1) }}
    tempo_de_bicicleta_total_hoje2: #alteração do tempo total, para ficar apenas em minutos
      friendly_name: "Tempo de Bicicleta Total"
      icon_template: mdi:timer
      unit_of_measurement: "m"
      value_template: >-
        {% set duration_hours = states('sensor.tempo_de_bicicleta_total_hoje')|float %}
        {% set duration = duration_hours * 60 %}
        {{ duration|int }}:{{ '{:02}'.format(((duration - duration|int) * 60) |int) }}
    tempo_de_bicicleta_ari_hoje2: #alteração do tempo total, para ficar apenas em minutos
      friendly_name: "Tempo de Bicicleta Ari"
      icon_template: mdi:timer
      unit_of_measurement: "m"
      value_template: >-
        {% set duration_hours = states('sensor.tempo_de_bicicleta_ari_hoje')|float %}
        {% set duration = duration_hours * 60 %}
        {{ duration|int }}:{{ '{:02}'.format(((duration - duration|int) * 60) |int) }}
    tempo_de_bicicleta_ari_ontem2:
      friendly_name: "Tempo de Bicicleta Ari"
      icon_template: mdi:timer
      unit_of_measurement: "m"
      value_template: >-
        {% set duration_hours = states('sensor.tempo_de_bicicleta_ari_ontem')|float %}
        {% set duration = duration_hours * 60 %}
        {{ duration|int }}:{{ '{:02}'.format(((duration - duration|int) * 60) |int) }}
    tempo_de_bicicleta_ari_anteontem2:
      friendly_name: "Tempo de Bicicleta Ari"
      icon_template: mdi:timer
      unit_of_measurement: "m"
      value_template: >-
        {% set duration_hours = states('sensor.tempo_de_bicicleta_ari_anteontem')|float %}
        {% set duration = duration_hours * 60 %}
        {{ duration|int }}:{{ '{:02}'.format(((duration - duration|int) * 60) |int) }}
    tempo_de_bicicleta_ari_semana2:
      friendly_name: "Tempo de Bicicleta Ari"
      icon_template: mdi:timer
      unit_of_measurement: "m"
      value_template: >-
        {% set duration_hours = states('sensor.tempo_de_bicicleta_ari_semana')|float %}
        {% set duration = duration_hours * 60 %}
        {{ duration|int }}:{{ '{:02}'.format(((duration - duration|int) * 60) |int) }}
- platform: history_stats #registra o tempo de uso, mas o formato não fica tão legal de usar
  name: Tempo de Bicicleta Ari Hoje
  entity_id: sensor.bicicleta_status_ari
  state: "Em movimento"
  type: time
  start: "{{ now().replace(hour=0, minute=0, second=0) }}"
  end: "{{ now() }}"
- platform: history_stats
  name: Tempo de Bicicleta Ari Ontem
  entity_id: sensor.bicicleta_status_ari
  state: "Em movimento"
  type: time
  end: "{{ now().replace(hour=0, minute=0, second=0) }}"
  duration:
    hours: 24
- platform: history_stats
  name: Tempo de Bicicleta Ari Anteontem
  entity_id: sensor.bicicleta_status_ari
  state: "Em movimento"
  type: time
  end: "{{ as_timestamp( now().replace(hour=0, minute=0, second=0) ) - 86400 }}"
  duration:
    hours: 24
- platform: history_stats
  name: Tempo de Bicicleta Ari Semana
  entity_id: sensor.bicicleta_status_ari
  state: "Em movimento"
  type: time
  start: "{{ as_timestamp( now().replace(hour=0, minute=0, second=0) ) - ((now().weekday()+1)%7) * 86400 }}"
  end: "{{ now() }}"

E, como isto fica? Fiz 3 cards, aqui vão eles:

type: entities
entities:
  - entity: input_select.bicicleta_usuario
  - entity: binary_sensor.bicicleta_status
    name: Estado de Movimento
state_color: true

O de escolha de usuários:

type: conditional
conditions:
  - entity: input_select.bicicleta_usuario
    state: Outro
card:
  type: vertical-stack
  cards:
    - type: picture
      image: /local/bicicleta-ari.jpg
      tap_action:
        action: call-service
        service: input_select.select_option
        service_data:
          option: Ari
        target:
          entity_id: input_select.bicicleta_usuario
      hold_action:
        action: none
    - type: picture
      image: /local/bicicleta-mika.jpg
      tap_action:
        action: call-service
        service: input_select.select_option
        service_data:
          option: Mika
        target:
          entity_id: input_select.bicicleta_usuario
      hold_action:
        action: none

E o grandão, com os cards (cortei o da minha esposa, por ser igual ao meu):

type: vertical-stack
cards:
  - type: conditional
    conditions:
      - entity: input_select.bicicleta_usuario
        state: Ari
    card:
      type: entities
      entities:
        - entity: sensor.bicicleta_velocidade_ari
        - entity: sensor.tempo_de_bicicleta_ari_hoje2
          name: Tempo hoje
        - entity: sensor.bicicleta_km_ari_dia2
          name: Distância Hoje
          icon: mdi:map-marker-distance
        - entity: sensor.bicicleta_cal_ari_dia
        - type: divider
        - type: divider
        - type: divider
        - entity: sensor.bicicleta_km_ari_ano2
          type: custom:multiple-entity-row
          state_header: Ano
          name: Distância Ari
          secondary_info:
            entity: sensor.bicicleta_km_ari_dia2
            name: 'Hoje: '
          entities:
            - entity: sensor.bicicleta_km_ari_semana2
              name: Semana
            - entity: sensor.bicicleta_km_ari_mes2
              name: Mês
        - entity: sensor.tempo_de_bicicleta_ari_semana2
          type: custom:multiple-entity-row
          state_header: Semana
          name: Tempo Ari
          unit: m
          secondary_info:
            entity: sensor.tempo_de_bicicleta_ari_hoje2
            name: 'Hoje: '
            unit: m
          entities:
            - entity: sensor.tempo_de_bicicleta_ari_ontem2
              name: Ontem
              unit: m
            - entity: sensor.tempo_de_bicicleta_ari_anteontem2
              name: Anteontem
              unit: m
        - entity: sensor.bicicleta_cal_ari_ano
          type: custom:multiple-entity-row
          state_header: Ano
          name: Calorias Ari
          secondary_info:
            entity: sensor.bicicleta_cal_ari_dia
            name: 'Hoje: '
          entities:
            - entity: sensor.bicicleta_cal_ari_semana
              name: Semana
            - entity: sensor.bicicleta_cal_ari_mes
              name: Mês
        - entity: sensor.bicicleta_km_mika_mes2
          name: Distância MIKA no mês
          icon: mdi:account-heart
      state_color: false
      header:
        type: picture
        image: /local/bicicleta-ari.jpg
        tap_action:
          action: call-service
          service: input_select.select_last
          service_data:
            entity_id: input_select.bicicleta_usuario
        hold_action:
          action: none
      footer:
        entities:
          - entity: sensor.bicicleta_velocidade2
            name: Velocidade
            color: green
        font_size: 85
        font_size_header: 22
        height: 60
        hours_to_show: 0.5
        index: 0
        line_width: 2
        name: Gráfico de Velocidade
        points_per_hour: 200
        show:
          fill: true
          icon_adaptive_color: true
          labels: true
          extrema: true
        type: custom:mini-graph-card
  - type: conditional
    conditions:
      - entity: input_select.bicicleta_usuario
        state: Outro
    card:
      type: entities
      entities:
        - entity: sensor.bicicleta_velocidade2
        - entity: sensor.tempo_de_bicicleta_total_hoje2
          name: Tempo geral hoje
      header:
        type: picture
        image: /local/bicicleta-outro.jpg
        tap_action:
          action: call-service
          service: input_select.select_last
          service_data:
            entity_id: input_select.bicicleta_usuario
        hold_action:
          action: none
      footer:
        entities:
          - entity: sensor.bicicleta_velocidade2
            name: Velocidade
            color: green
        font_size: 85
        font_size_header: 22
        height: 60
        hours_to_show: 0.5
        index: 0
        line_width: 2
        name: Gráfico de Velocidade
        points_per_hour: 200
        show:
          fill: true
          icon_adaptive_color: true
          labels: true
          extrema: true
        type: custom:mini-graph-card

card

Qualquer dúvida é só falar!

7 curtidas