Intro
Teño varios sensores de temperatura e humidade Xiaomi Mijia BLE por aquí sen acabar de usalos de todo. Cando os merquei en AliExpress funcionaban perfectamente na aplicación estándar Xiaomi Home, con todo, tempo despois Xiaomi lanzou unha actualización para que xa non funcionasen coa aplicación europea estándar. Ou usabas a versión chinesa (aínda máis chea de lixo) ou simplemente non podías usar a súa parte “intelixente”. Nese momento, para min convertéronse en sensores non-BLE, xa que non podía usar as aplicacións chinesas porque outros dispositivos IoT de Xiaomi que teño son europeos e, polo tanto, non se levan ben coa versión chinesa da aplicación.
Repurposing
Como sabemos que os sensores realmente funcionan, para recoller os seus datos só se necesitaría un servidor e, por casualidade, non hai moito merquei un mini-pc para usalo como servidor, con Proxmox, que permite aloxar multiples servizos. Así foi como a idea chegou a bo porto: simplemente montar un contedor LXC con Docker en Proxmox para recoller os datos e, xa que estabamos, facer que os graficase e os mostrase de xeito intuitivo.
O Firmware Custom
Estes sensores Xiaomi Mijia BLE (especificamente o LYWSD03MMC que son os que teño eu) son un hardware excelente por un prezo relativamente baixo. Porén, de fábrica, teñen trampa, xa que cifran as súas transmisións Bluetooth para que só a aplicación oficial de Xiaomi poida ler os datos e, en xeral, non se levan ben con outros protocolos nin receptores.
Aínda que tecnicamente podes extraer unha “Bind Key” única para cada sensor e introducila nos teus scripts para descifrar os datos de Xiaomi sobre a marcha, é tedioso. O estándar é flashear os sensores cun firmware personalizado, capturar o sinal Bluetooth sen cifrar e lelo normalmente. Isto xera algúns problemas de seguridade, xa que estás transmitindo os teus propios datos abertamente pero, dada a natureza da información e a baixa potencia dos sensores, creo que é un detalle relativamente menor.
Para flashear os sensores non necesitas cables especiais nin programación avanzada, é todo moi cómodo. Só tes que ir a Telink BLE OTA Flasher. Aí podes conectarte ao sensor a través de Bluetooth. Para iso necesitarás Chrome e, moi probablemente (polo menos no meu caso foi así), ter activada a flag #enable-experimental-web-platform-features (pegando chrome://flags/#enable-experimental-web-platform-features en Chrome debería levarte ata ela). Desde aí, só tes que seguir os botóns e procesos da pantalla: rexistrar primeiro o sensor, iniciar sesión e despois flashear o firmware. Flashealo é tan sinxelo como facer clic nun botón e leva ao redor dun minuto. Despois diso, podes acceder a un sinfín de axustes que che permiten modificar o dispositivo para engadir funcionalidades, como cambiar o que se mostra na pantalla, ou mellorar a duración da batería reducindo o número de medicións, etc.
Os axustes
De todos os axustes dispoñibles hai dous que me resultaron relevantes: o “Advertising type” (Tipo de anuncio) e o “RF TX Power” (Potencia de transmisión RF).
Advertising
Este axuste determina exactamente como cada sensor empaqueta os seus datos (payload) cando os transmite por Bluetooth. Como mencionei, o firmware orixinal usa un formato propietario e cifrado, pero o firmware personalizado permíteche emitir noutros estándares, como ‘Custom (pvvx)’, ‘ATC1441’ ou ‘BTHome’. Como non quería meterme de cheo na ruta completa de Home Assistant (máis sobre isto despois), seleccionei o pvvx.
RF TX Power
Este axuste controla literalmente a forza de transmisión da radio Bluetooth do sensor, e representa un compromiso directo entre o alcance do sinal e a duración da batería. Polo tanto, se tes pensado colocar o sensor lonxe do receptor ou separado por paredes, o mellor será aumentala. No meu caso non foi tanto a distancia ou as paredes senón a calidade do receptor. O meu Mini-PC ten unha carcasa metálica e no seu interior están as antenas da tarxeta M.2 de Wifi e Bluetooth, está documentado que isto diminúe enormemente o seu rendemento. Mesmo despois de cambiar a orixinal por unha mellor (a que traía non se levaba ben con Linux), o alcance é abismal. Tampouco axuda que estea preto dunha parede e dunha torre de PC completamente metálica. Así que, para conseguir que o sensor máis afastado fose detectado de forma fiable, tiven que subir a potencia ata +10dB (xa veremos canto afecta iso á duración da batería).
O servidor
Como xa mencionei antes, o servidor é un mini PC que executa Proxmox. O meu obxectivo era usar un LXC de Docker con passthrough de Bluetooth e listo. A primeira opción sería usar Home Assistant, pero como non son un gran fanático da domótica nin do IoT para o caso, non lle vin moito sentido a seguir ese camiño, xa que acabaría enchendo o servidor de bloat. De aí naceu a idea de facer un LXC cun Docker caseiro.
Aprendendo cando non re-enviar
Conseguir que o Bluetooth funcionase dentro dun contedor Docker en Proxmox levou o seu proceso de ensaio e erro (maiormente erros). Antes sequera de tocar o Docker, o host non podía ler o adaptador BT. Resulta que Proxmox non sempre trae a pila de Bluetooth configurada por defecto, así que tiven que instalala. Despois diso, intentei (e fracasei) pasarlle o adaptador Bluetooth do host ao contedor Docker (configurando network_mode: "host", facendo o LXC privilexiado e mapeando o socket D-Bus directamente dentro do contedor).
Ao final, tras moitos problemas, decidín dar un paso atrás e simplificar. O passthrough de hardware dende un host Proxmox a un LXC, e logo tentar atravesar a capa de illamento de Docker dentro dese LXC, crea un castelo de naipes moi fráxil que seguro que acaba rompendo nalgún momento (intentar facer un passthrough de GPU con gráficos Radeon deixoume un pouco traumatizado). Así que, para simplificar e facelo máis robusto (e permitir que o stack da base de datos fose máis sinxelo), decidín usar o paso de mensaxes a través de MQTT. Deste xeito, en lugar de tentar meter o Bluetooth con calzador no LXC e no propio Docker, deixaríalle ese traballo ao host Proxmox (que detecta nativamente o hardware Bluetooth sen ningún mapeo nin historias).
# Configurando a ponte no Host
Para facer isto realidade, instalei TheengsGateway directamente no meu Proxmox, rápido e fácil. Este é capza de ler nativamente os datos Bluetooth dos sensores e, posteriormente, convérteos nuns payloads limpos en formato JSON que logo poden enviarse mediante MQTT ao LXC.
Claro que, incluso con este enfoque simplificado, nada funciona á primeira. Cando o iniciei, vinme inmediatamente saudado por un erro que indicaba que o escaneo pasivo en Linux require BlueZ 5.56 ou superior, compilado coa flag --experimental activada. Para arranxar isto, tiven que editar o servizo systemd de Bluetooth (/lib/systemd/system/bluetooth.service) e engadir manualmente --experimental á liña ExecStart. Tras unha rápida recarga do daemon e reinicio do servizo, xa podía ver sinais de vida: os payloads estaban sendo decodificados!
{"name": "ATC\_6EA90E", "id": "A4:C1:38:6E:A9:0E", "rssi": -59, "brand": "Xiaomi", "model": "TH Sensor", "model\_id": "LYWSD03MMC/MJWSD05MMC\_PVVX\_BTHOME", "type": "THB", "tempc": 23.82, "hum": 75.14, "batt": 95}
Para acabar ca parte do host, envolvín o comando TheengsGateway nun servizo systemd persistente (/etc/systemd/system/theengs-gateway.service) para que se iniciase automaticamente ao encender (e se reiniciase no caso de fallar). A parte do host estaba rematada1 , hora de traballar no LXC.
O Docker LXC
Como estamos deixando que o host de Proxmox se amañe coa parte do Bluetooth, o LXC non necesita ser privilexiado nin ter ningún passthrough de hardware. O stack dentro do LXC xestiónase integramente cun único ficheiro docker-compose.yml e dous pequenos ficheiros de configuración.
Para manter as cousas ordenadas, configurei un directorio de proxecto (/opt/stack/) coa seguinte estrutura:
-
./docker-compose.yml -
./mosquitto/config/mosquitto.conf -
./telegraf/telegraf.conf
Antes de executar o stack, debes crear os ficheiros de configuración para Mosquitto e Telegraf. Se non o fas, Docker moi amablemente creará directorios baleiros en lugar de ficheiros, e Mosquitto bloqueará todo o tráfico externo.
A Configuración de Mosquitto (./mosquitto/config/mosquitto.conf) indícalle ao broker que escoite polo porto 1883 e que permita que os nosos contedores internos se conecten sen contrasinal:
listener 1883
allow_anonymous true
persistence true
persistence_location /mosquitto/data/
A Configuración de Telegraf (./telegraf/telegraf.conf) xestiona a inxestión de datos desde MQTT a InfluxDB:
[agent]
interval = "10s"
[[inputs.mqtt_consumer]]
servers = ["tcp://mosquitto:1883"]
topics = ["home/#"]
data_format = "json"
json_string_fields = ["name", "id", "model", "brand"]
[[outputs.influxdb_v2]]
urls = ["http://influxdb:8086"]
token = "mytoken123"
organization = "home"
bucket = "sensors"
Finalmente, o docker-compose debería lucir máis ou menos así:
services:
mosquitto:
image: eclipse-mosquitto:2
container_name: mosquitto
ports:
- "1883:1883"
volumes:
- ./mosquitto/config:/mosquitto/config
- mosquitto-data:/mosquitto/data
restart: unless-stopped
influxdb:
image: influxdb:2.7
container_name: influxdb
ports:
- "8086:8086"
volumes:
- influxdb-data:/var/lib/influxdb2
environment:
- DOCKER_INFLUXDB_INIT_MODE=setup
- DOCKER_INFLUXDB_INIT_USERNAME=admin
- DOCKER_INFLUXDB_INIT_PASSWORD=adminpassword
- DOCKER_INFLUXDB_INIT_ORG=home
- DOCKER_INFLUXDB_INIT_BUCKET=sensors
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=mytoken123
restart: unless-stopped
telegraf:
image: telegraf:1.30
container_name: telegraf
volumes:
- ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro
depends_on:
- mosquitto
- influxdb
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
restart: unless-stopped
volumes:
mosquitto-data:
influxdb-data:
grafana-data:
Levantándoo
Unha vez que os tres ficheiros descritos anteriormente estean no seu sitio, podes levantar o stack cun sinxelo docker compose up -d. Os catro contedores deberían arrincar en segundo plano. Debido a que Mosquitto está configurado para permitir conexións anónimas internas e Telegraf está a escoitar o comodín home/#, a canalización de datos comeza a funcionar inmediatamente, aínda que dependendo da frecuencia de consulta dos sensores os datos poden tardar algúns segundos/minutos en enviarse.
Unha vez que confirmes que os datos se están enviando e recibindo correctamente, o que se pode comprobar con
docker exec -it mosquitto mosquitto_sub -t "home/#" -v
podemos pasar ao seguinte paso, axustar a visualización en Grafana.
Visualizando con Flux en Grafana
Incluso con varios sensores enviando datos a InfluxDB co mesmo tipo de medida, graficalos dun xeito que non resulte confuso só require unha pequena cantidade de código en Grafana.
O meu plan era ter todos os sensores nunha soa gráfica, codificados por cores, para poder ter unha lectura “at-a-glance” do estado da casa (respecto á temperatura e á humidade). InfluxDB fai o seguimento de cada sensor a través do seu topic MQTT, que inclúe os seus enderezos MAC, facendo que as gráficas predeterminadas sexan un pouco feas. Usei a linguaxe de consultas Flux de InfluxDB e a función map() para interceptar eses enderezos MAC e renomealos para obter gráficas fáciles de ler. Deste xeito, podemos renomear os sensores a algo máis fácil de entender:
from(bucket: "sensors")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r._measurement == "mqtt_consumer")
|> filter(fn: (r) => r._field == "tempc")
|> map(fn: (r) => ({r with topic:
if r.topic == "home/TheengsGateway/BTtoMQTT/A4C1386EA90E" then "Escritorio"
else if r.topic == "home/TheengsGateway/BTtoMQTT/A4C13840D65B" then "Salón"
else if r.topic == "home/TheengsGateway/BTtoMQTT/A4C1387F84D8" then "Habitación"
else r.topic
}))
|> keep(columns: ["_time", "_value", "topic"])
Engadir sensores extra só requiriría un else if adicional para cada un, así que é relativamente sinxelo.
Os resultados deberían parecerse a isto: