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:
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: