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
Qualquer dúvida é só falar!