Gateway Wifi com Zigbee e Bluetooth DIY (CC2531 - ESP32 - ESPHome)

Este é o meu primeiro tutorial, todos os comentários que o possam melhorar são muito bem vindos!
Estou consciente que não é um tutorial passo a passo, mas deverá ser mais ou menos simples de seguir para quem já tenha tido abordagem inicial ao ESPHome (com ESP8266 ou ESP32), Zigbee2mqtt e Docker.

1 - Introdução e lista de material

Com o confinamento “tive” de converter um anexo em escritório e fui automatizando algumas coisas aos poucos. Entretanto surgiu a necessidade de criar também uma pequena rede zigbee, que criei com recurso a um cc2531 (com antena) ligado a uma box android com Coreeelec e o zigbee2mqtt a correr em Docker, mas isto obrigava a ter a box 24h ligada. Também tinha um ESP32 a servir de BT gateway via ESPHome para sensor de temperatura da Xiaomi (lywsd03mmc).

Como fiquei com um CC2531 e um ESP32 sem uso, e inspirado no gateway zigbee via wifi (serial over tcp), comecei a investigar a possibilidade de construir um gateway com estes equipamentos e assim poder desactivar a box android. Encontrei vários artigos mas nenhum tutorial completo, e fui juntando as várias peças. No fim resultou num gateway wifi com zigbee e BT utilizando um ESP32 e CC2531, para já a funcionar de forma estável.

Lista de material:

  • ESP32 (no meu caso utilizei a versão ESP32-CAM)
  • CC2531 (com antena externa)
  • Fonte alimentação
  • Caixa projecto

Outros pressupostos:

  • Caso já estejam a utilizar o addon zigbee2mqtt do Homeassistant será necessário uma segunda instância do Zigbee2mqtt especifica para este gateway. (actualizado @11-4-2021)

  • Zigbee2mqtt e como flashar o CC2531. Tópico não abrangido no tutorial.

  • ESPHome e como flashar o ESP32 com ESPHome. Tópico não abrangido no tutorial

  • Ter integração mqtt no Homeassistant. Tópico não abrangido no tutorial

2 - Desenho

Abaixo esquema de ligações da solução:

3 - Preparação do CC2531

Para utilizar o CC2531 como wifi gateway via um ESP32 temos de utilizar as portas GPIO em vez do habitual USB e flashar com o firmware do cc2530.

Pelo que percebi o CC2531 é um CC2530 mas com ligação USB. Basicamente iremos seguir o referido no tutorial abaixo a nível de ligações:

https://www.zigbee2mqtt.io/information/connecting_cc2530.html

O primeiro passo é flashar o CC2531 mas com o firmware do cc2530 (CC2530_DEFAULT_20190608.zip), eu utilizei o seguinte firmware:

https://github.com/Koenkk/Z-Stack-firmware/blob/4ab0d66e28b10dba75bcc8f1f5a7d7af4fbabf6c/coordinator/Z-Stack_Home_1.2/bin/default/

Para flashar seguir um dos tutoriais abaixo, pessoalmente utilizo o método do ESP8266:

[https://www.zigbee2mqtt.io/information/flashing_the_cc2531.html](https://www.zigbee2mqtt.io/information/flashing_the_cc2531.html)

ou

https://www.zigbee2mqtt.io/information/alternative_flashing_methods.html

No final deste ponto ficamos com o CC2531 flashado com o firmware coordinator do CC2530 e pronto para ser ligado via serie.

4 - Preparação do ESP32 com ESPHome

Para utilizar o sensor de temperatura que tenho a única solução que conheço é o ESPHome com ESP32. Colocava-se o desafio de criar o “serial over tcp” com ESPHome para a ligar o CC2531, solução que encontrei na net e partilho (link abaixo).

Abaixo o código ESPHome yaml com que flashei o ESP32.

O código inicial utilizava a GPIO1 e GPIO3 (ver links abaixo) para a comunicação serial com o cc2531, mas esta ligação não permitia ter o logger do ESPHome em simultâneo por isso testei com a GPIO12 e GPIO13 e funcionou sem problemas e assim é possível manter o “logger”.

 substitutions:
  host_name: nome_esphome
  host_ip: ip_do_esp32

esphome:
  name: $host_name
  platform: ESP32
  board: esp32cam #no meu caso utilizei ESP32 podem tb usar ESPdev

  includes:
    - stream_server.h
    - stream_server.cpp

wifi:
  ssid: "a_vossa_rede_wifi"
  password: "a_vossa_pass_wifi"
  manual_ip:
    static_ip: $host_ip
    gateway: 192.168.1.1
    subnet: 255.255.255.0

  ap:
    ssid: $host_name
    password: 'a_vossa_pass_wifi'

captive_portal:

# Enable logger
logger:

# Enable Home Assistant API
api:
  password: 'pass_para_integrar_no_ha'

ota:

esp32_ble_tracker:

uart:
  id: uart_bus
  tx_pin: GPIO12
  rx_pin: GPIO13
  baud_rate: 115200
#initial pins tx GPIO1 and rx GPIO3. This had a incompatibilitie with logger. 

custom_component:
  - lambda: |-
      auto stream_server = new StreamServerComponent(id(uart_bus));
      return {stream_server};

sensor:
  - platform: xiaomi_lywsd03mmc
    mac_address: "00:00:00:00:00:00" # colocar o mac do sensor
    bindkey: "00000000000000000008127f24f1b6" #utilizar se necessário o bindkey
    temperature:
      name: "LYWSD03MMC nome do sensor temperature"
    humidity:
      name: "LYWSD03MMC nome do sensor humidity"
    battery_level:
      name: "LYWSD03MMC nome do sensor battery"

O código acima faz referência a 2 “includes”.
Esses ficheiros devem estar na mesma directoria do yaml acima quando da compilação e flash do ESP32. Abaixo o código dos 2 ficheiros:

stream_server.h

#pragma once

#include "esphome/core/component.h"

#include <memory>
#include <string>
#include <vector>
#include <Stream.h>
#include <AsyncTCP.h> //<ESPAsyncTCP.h>

class StreamServerComponent : public esphome::Component {
public:
    explicit StreamServerComponent(Stream *stream) : stream_{stream} {}

    void setup() override;
    void loop() override;
    void dump_config() override;
    void on_shutdown() override;

    void set_port(uint16_t port) { this->port_ = port; }

protected:
    void cleanup();
    void read();
    void write();

    struct Client {
        Client(AsyncClient *client, std::vector<uint8_t> &recv_buf);
        ~Client();

        AsyncClient *tcp_client{nullptr};
        std::string identifier{};
        bool disconnected{false};
    };
    
    float get_setup_priority() const override { return esphome::setup_priority::AFTER_WIFI; }
    Stream *stream_{nullptr};
    AsyncServer server_{0};
    uint16_t port_{6638};
    std::vector<uint8_t> recv_buf_{};
    std::vector<std::unique_ptr<Client>> clients_{};
};

stream_server.cpp

#include "stream_server.h"

#include "esphome/core/log.h"
#include "esphome/core/util.h"

static const char *TAG = "streamserver";

using namespace esphome;

void StreamServerComponent::setup() {
    ESP_LOGCONFIG(TAG, "Setting up stream server...");
    this->recv_buf_.reserve(128);

    this->server_ = AsyncServer(this->port_);
    this->server_.begin();
    this->server_.onClient([this](void *h, AsyncClient *tcpClient) {
        if(tcpClient == nullptr)
            return;

        this->clients_.push_back(std::unique_ptr<Client>(new Client(tcpClient, this->recv_buf_)));
    }, this);
}

void StreamServerComponent::loop() {
    this->cleanup();
    this->read();
    this->write();
}

void StreamServerComponent::cleanup() {
    auto discriminator = [](std::unique_ptr<Client> &client) { return !client->disconnected; };
    auto last_client = std::partition(this->clients_.begin(), this->clients_.end(), discriminator);
    for (auto it = last_client; it != this->clients_.end(); it++)
        ESP_LOGD(TAG, "Client %s disconnected", (*it)->identifier.c_str());

    this->clients_.erase(last_client, this->clients_.end());
}

void StreamServerComponent::read() {
    int len;
    while ((len = this->stream_->available()) > 0) {
        char buf[128];
        size_t read = this->stream_->readBytes(buf, min(len, 128));
        for (auto const& client : this->clients_)
            client->tcp_client->write(buf, read);
    }
}

void StreamServerComponent::write() {
    size_t len;
    while ((len = this->recv_buf_.size()) > 0) {
        this->stream_->write(this->recv_buf_.data(), len);
        this->recv_buf_.erase(this->recv_buf_.begin(), this->recv_buf_.begin() + len);
    }
}

void StreamServerComponent::dump_config() {
    ESP_LOGCONFIG(TAG, "Stream Server:");
    ESP_LOGCONFIG(TAG, "  Address: %s:%u", network_get_address().c_str(), this->port_);
}

void StreamServerComponent::on_shutdown() {
    for (auto &client : this->clients_)
        client->tcp_client->close(true);
}

StreamServerComponent::Client::Client(AsyncClient *client, std::vector<uint8_t> &recv_buf) :
        tcp_client{client}, identifier{client->remoteIP().toString().c_str()}, disconnected{false} {
    ESP_LOGD(TAG, "New client connected from %s", this->identifier.c_str());

    this->tcp_client->onError(     [this](void *h, AsyncClient *client, int8_t error)  { this->disconnected = true; });
    this->tcp_client->onDisconnect([this](void *h, AsyncClient *client)                { this->disconnected = true; });
    this->tcp_client->onTimeout(   [this](void *h, AsyncClient *client, uint32_t time) { this->disconnected = true; });

    this->tcp_client->onData([&](void *h, AsyncClient *client, void *data, size_t len) {
        if (len == 0 || data == nullptr)
            return;

        auto buf = static_cast<uint8_t *>(data);
        recv_buf.insert(recv_buf.end(), buf, buf + len);
    }, nullptr);
}

StreamServerComponent::Client::~Client() {
    delete this->tcp_client;
}

Nota - a porta com a ligação série é a 6638, podem alterar no ficheiro acima “stream_server.h”.

Nota 2 - podem criar mais sensores no vosso código ESPHome. Não testei os limites do ESP32 a nível de HW. Testei no entanto a integração da câmera e não funciona, mas pelo que pude investigar é um problema/limitação conhecido do ESP32-CAM com a utilização simultânea do componente câmera (wifi) e o BT.

5 - Configuração Zigbee2mqtt (actualizado @11-4-2021)

Como referido acima será necessário correrem uma instância do zigbee2mqtt. Se esta for a vossa primeira rede zigbee podem usar simplesmente o addon do homeassistant.

Instalam o addon e nas configurações incluam o código abaixo em vez da habitual ligação ao dispositivo USB (por exemplo: /dev/ttyACM0):

(...)
serial:
  port: 'tcp://ip_do_esp32:6638'
(...)

Caso esta seja a vossa segunda rede zigbee (segundo coordenador), que é o meu caso, existem pelo menos 2 hipóteses, podem optar pela que vos for mais favorável:

  • a) Utilizar uma instância docker que já tenham com outros containers

  • b) Utilizar o docker do hassos

5.a) Utilizar uma instância docker que já tenham

Por uma questão de simplicidade sugiro utilizar o container Portainer (container com UI para gerir o docker) e a sua funcionalidade de stack.

Abaixo esquema simplificado de como criar um stack.

Criar uma stack com o código abaixo e fazer deploy.

version: '2'
  services:
    zigbee2mqtt:
      container_name: zigbee2mqtt_office
      image: koenkk/zigbee2mqtt
      volumes:
        - /home/user/appdata/zigbee2mqtt_gw:/app/data
      restart: unless-stopped
      ports:
        - 8098:8099
      environment:
        - TZ=Europe/Lisbon

Correr a primeira vez o container e ver se inicia, ele deve criar o ficheiro de configuração standard no directório que definiram ‘/home/user/appdata/zigbee2mqtt_gw’.

Editem o ficheiro de configuração do zigbee2mqtt e incluam a seguinte configuração em vez da habitual ligação ao dispositivo USB (por exemplo: /dev/ttyACM0):

(...)
serial:
  port: 'tcp://ip_do_esp32:6638'
(...)

Nota - os (…) não é para incluir, indica apenas que existem outras configurações antes e depois.

Devem incluir as restantes configurações de acordo com o vosso sistema.

Como podem verificar não é necessário passar nenhum device para “dentro” do docker nem correr o container como “privileged”.

Após configurarem, devem (re-)iniciar o contentor e verificar os logs para ver se tudo está ok.

b) Utilizar o docker do hassos

O processo é semelhante ao descrito em 5.a) a única variação é que temos de criar um volume antes de fazer deploy à stack.

Caso não criem o volume quando iniciar o container vai dar erro de escrita indicando que não tem permissões de criar a directoria.

Começamos então por criar o volume, no meu caso criei volume “tipo samba”, pressupõe que já tenham o samba addon instalado.

Após a criação do volume devem criar o stack com o código abaixo e fazer deploy.

As única 2 diferenças em relação ao stack do ponto 5.a são:

a) agora em vez de indicarmos o caminho indicamos o volume criado “z2m_gateway”.

b) a porta externa para o user interface é alterada para 8098. Esta alteração permite que, caso tenham também o addon zigbee2mqtt instalado consigam aceder aos 2 UI, um na porta standard 8099 e o que estamos a criar na porta 8098:

version: '2'
  services:
    zigbee2mqtt:
      container_name: zigbee2mqtt_office
      image: koenkk/zigbee2mqtt
      volumes:
        - z2m_gateway:/app/data
      restart: unless-stopped
      ports:
        - 8098:8099
      environment:
        - TZ=Europe/Lisbon

Correr a primeira vez o container e ver se inicia, ele deve criar o ficheiro de configuração standard no directório samba que definiram como volume.

Editem o ficheiro de configuração do zigbee2mqtt e incluam a seguinte configuração em vez da habitual ligação ao dispositivo USB (por exemplo: /dev/ttyACM0):

(...)
serial:
  port: 'tcp://ip_do_esp32:6638'
(...)

Nota - os (…) não é para incluir, indica apenas que existem outras configurações antes e depois.

Devem incluir as restantes configurações de acordo com o vosso sistema.

Como podem verificar não é necessário passar nenhum device para “dentro” do docker nem correr o container como “privileged”.

Após configurarem, devem (re-)iniciar o contentor e verificar os logs para ver se tudo está ok.

6 - Integração com o homeassistant

No ESP32 acima criámos um sensor de temperatura/humidade via ESPHome. Para intergrar com o HA basta utilizar a integração do ESPHome:

Configuration>Integration>add integration>ESPHome>“ip_do_esp32”>"'pass_para_integrar_no_ha"

Relativamente aos devices zigbee devem aparecer automáticamente na integração mqtt como habitual.

7 - Conclusão e referências

No presente tutorial descrevo os passos que segui e que estão ajustados às minhas necessidades mas como podem ver as variações que este tutorial pode ter são muitas e não se esgotam na solução que acima descrevi.

Algumas fotos da solução final:


Nota - apesar da câmera não estar a funcionar optei por deixar a caixa preparada com a abertura da lente.

Espero que possam adaptar às vossas necessidades/soluções. Podem por exemplo utilizar um ESP8266 (em vez de ESP32, ficam sem o BT), podem integrar outros dispositivos ESPHome (ver possibilidade na página do ESPHome) etc etc.

Como nota final refiro que não desenvolvi nada, todos os créditos são dos autores dos links que passo a inumerar e que me ajudaram a colmatar a minha necessidade:

Olá, obrigado pela partilha.
Esqueceste de adicionar os esquemas (imagens) deduzo, poderás adicionar?
Obrigado

Podes verificar se já está visível? no meu PC eu consegui ver a imagem, provavelmente porque a imagem era um link interno da minha rede.

Sim já está ok obrigado, caso tenhas feito alguma caixa em 3d, podes disponibilizar também o link :slight_smile: mais uma vez obrigado pela partilha, bom trabalho e aparece no nosso discord :slight_smile:

A caixa encomendei da “China”, não tenho impressora 3d :grinning:
De vez em quando vou ao Discord, qualquer dia junto-me a um canal de voz :grinning:
obrigado!

1 Curtiu

Ponto 5 do tutorial revisto e actualizado @11-4-2021 para incluir a possibilidade de criação de segundo container docker no hassos (além do addon zigbee2mqtt standard) caso estejam a usar 2 redes zigbee como é o meu caso.

1 Curtiu

Copyright © 2017-2021. Todos os direitos reservados
CPHA.pt - info@cpha.pt


FAQ | Termos de Serviço/Regras | Política de Privacidade