Como aficionado a la domótica, utilizo mucho la iluminación inteligente. Pero cuando tuve que equipar todas las habitaciones de la casa con un controlador de luz, a veces incluso varios, me topé con un problema de presupuesto. Los controladores inteligentes comerciales son bastante caros. Así que decidí usar mi impresora 3D, añadir algunos componentes electrónicos y construir yo mismo un controlador a medida por una fracción del precio.

Quería crear un pequeño macro pad utilizando interruptores para teclado mecánico, una placa ESP32, un codificador rotatorio para ajustar el brillo y una pantalla OLED compacta para mostrar la escena de iluminación activa en ese momento. El codificador rotatorio también cuenta con un interruptor pulsador integrado – al pulsarlo se activa el modo de atenuación de la pantalla para ajustar el brillo de la pantalla OLED, y al pulsarlo de nuevo se vuelve al control de luz normal.

Para asegurarme de que no fuera simplemente otra caja de plástico aburrida, decidí combinar varios materiales Prusament Woodfill para crear bonitas combinaciones de colores que combinaran con la superficie de madera de mi escritorio. Tuve la oportunidad de probar una de las unidades beta de la Prusa CORE One+ INDX, así que aproveché sus capacidades para imprimir con varios materiales. Sin embargo, puedes imprimir fácilmente este proyecto en una Original Prusa XL, una impresora equipada con MMU3, o incluso una impresora estándar de un solo cabezal sin capacidad para imprimir con varios materiales. En ese caso, tendrás que cambiar manualmente los filamentos varias veces durante la impresión, pero es totalmente factible, aunque debes contar con que habrá más desperdicio. Gracias al uso de la INDX, acabé sin casi ningún desperdicio de filamento, lo cual fue una gran ventaja para el proyecto, ya que ahorré material que ahora puedo usar para imprimir más macropads u otras cosas.

Hardware

Para que este proyecto resulte asequible, todos los componentes electrónicos se pueden adquirir por unos pocos euros. Por supuesto, puedes prescindir de la pantalla y del codificador y limitarte solo a los interruptores, o incluso reducir el número de teclas.

Como cerebro del macropad, puedes utilizar prácticamente cualquier placa de desarrollo ESP32 o similar que cuente con suficientes entradas y conexión Wi-Fi. Las placas de alta calidad de marcas como Adafruit (como la compacta QT Py ESP32-S3 o la clásica Feather ESP32-S3) o SparkFun (la Thing Plus ESP32-S3) son opciones perfectas para esto. En mi caso, antes de comprar por Internet, rebusqué en mi interminable cajón de repuestos y encontré una placa similar, una Xiao ESP32-C3 de Seeed Studio, así que decidí quedarme con ella para este proyecto.

En cuanto a las teclas, me decidí por los interruptores mecánicos azules. Quizá sea una opción un poco minoritaria, y las teclas normales funcionarían perfectamente, pero los interruptores mecánicos me convencen por esa sensación y ese sonido tan satisfactorios al pulsarlos.

A continuación te ofrecemos un breve resumen de lo que necesitarás para la versión que se presenta en este artículo:

Componente Precio aproximado
ESP32 8.00$
Pantalla OLED de 0.91″ 4$
Codificador Rotativo KY-040 1.30$
Interruptores de teclado mecánico (x6) 3.50$
Filamento ~3.00$
Coste Total del Hardware ~19.80$

Impresión 3D y Personalización de Diseños

Ahora es el momento de preparar los modelos 3D para su impresión. Puedes crearlos tú mismo con un programa de modelado 3D como Fusion, o descargar los archivos ya preparados para este proyecto directamente desde Printables.

Importa los archivos a PrusaSlicer. Para conseguir un aspecto limpio y elegante, puedes imprimir el chasis en un solo color utilizando Prusament Woodfill Linden Light. Sin embargo, como la imaginación no tiene límites, también puedes crear variantes muy coloridas, perfectas para una habitación infantil, como un diseño inspirado en Los Vengadores. A continuación, añade modificadores SVG con símbolos sencillos a las teclas y al dial giratorio haciendo clic con el botón derecho del ratón sobre el modelo en PrusaSlicer y seleccionando Añadir modificador > SVG. Si quieres una guía más detallada sobre cómo aplicar modificadores SVG, echa un vistazo a nuestro Artículo sobre accesorios sensoriales DIY para jugar. Elegir un Prusament Chocolate Brown Woodfill más oscuro para los botones y utilizar el mismo Linden Light brillante para los símbolos crea un contraste muy bonito. Gracias a la impresión multimaterial en una impresora como la Prusa CORE One+ INDX o la Original Prusa XL, el resultado es increíblemente preciso, sin contaminación de colores y con un mínimo absoluto de residuos. La mejor forma de imprimir las teclas es con la cara hacia abajo sobre la cama. En la imagen de abajo, se han dado la vuelta para mostrar los diseños SVG. Una vez que todas las piezas estén impresas, es la hora de montarlo

En primer lugar, instala los interruptores del teclado mecánico simplemente empujándolos a través de las aberturas del chasis hasta que encajen con un clic. A continuación, instala la pantalla, el codificador giratorio y el ESP32, insertando su conector USB-C en la abertura preparada en el lateral del chasis de modo que quede orientado hacia fuera. Mientras que la pantalla y el codificador encajan con bastante firmeza por sí solos, fijar el ESP32 requiere un poco más de cuidado. He utilizado un sencillo espaciador impreso que solo hay que deslizar entre la pared del chasis y la placa para fijarlo en su sitio, asegurando que el ESP32 no se desplace hacia atrás al conectar el cable USB-C.

La configuración ocupa un total de once pines: seis interruptores, el codificador (2 pines para la rotación y 1 para el interruptor) y la pantalla I2C (2 pines para datos/reloj). Esto se ajusta perfectamente al número de entradas digitales disponibles en mi placa ESP32. A continuación se muestra el diagrama de cableado creado en Wokwi, un excelente simulador gratuito que se ejecuta directamente en el navegador. Wokwi también cuenta con un simulador integrado, por lo que puedes conectar todos los componentes y escribir código de prueba para asegurarte de que el cableado es correcto antes de montar la unidad física.

Ten cuidado al mirar la placa: la etiqueta física impresa en la PCB (D0-D10) casi siempre difiere de los registros internos (GPIO) que se utilizan en el código. Aquí tienes la asignación de hardware para mi ESP32:

5V / 5V0VCC (Línea de alimentación compartida para la pantalla OLED y el codificador rotatorio)

GNDGND (Tierra común para todos los componentes y los interruptores mecánicos)

D0 (GPIO Interno 2)Tecla 1

D1 (GPIO Interno 3)Tecla 2

D2 (GPIO Interno 4)Tecla 3

D3 (GPIO Interno 5)Tecla 4

D4 (GPIO Interno 6)OLED SDA (Línea de datos)

D5 (GPIO Interno 7)OLED SCL (Línea del reloj)

D6 (GPIO Interno 21)Codificador CLK (Pin Rotación A)

D7 (GPIO Interno 20)Codificador DT (Pin Rotación B)

D8 (GPIO Interno 8)Codificador SW (Botón integrado del codificador)

D9 (GPIO Interno 9)Tecla 5

D10 (GPIO Interno 10)Tecla 6

Para las pruebas iniciales, puedes conectar todo con conectores Dupont, como hice yo. Sin embargo, el espacio dentro del chasis es reducido y estos conectores no son lo suficientemente seguros para el uso diario. Para garantizar una fiabilidad del 100% y un montaje limpio, te recomiendo encarecidamente que sueldes todos los componentes.

Configuración del Software (ESPHome)

Ahora que ya está todo conectado, ¡es hora de ponerse de (vibe)coding😎! Conectar el ESP32 a un PC o un Mac es muy fácil. Solo tienes que conectarlo a tu ordenador con un cable USB-C, descargar las herramientas necesarias y cargar el código en la placa a través del terminal. En Mac, puedes usar Python y su paquete ESPHome, que se instala mediante el gestor de paquetes de Python pip con el comando pip3 install esphome.

Una vez completada la instalación, crea dos archivos YAML: keyboard.yaml y secrets.yaml. El archivo keyboard.yaml contiene todo el código para el ESP32, y el archivo secrets.yaml almacena de forma segura tus credenciales de Wi-Fi. También necesitarás un archivo de fuente. En este proyecto, he utilizado la fuente Nunito de la biblioteca Google Fonts. Puedes encontrarla aquí: https://fonts.google.com/specimen/Nunito. Descarga y extrae el archivo, coloca la fuente normal (Nunito-Regular.ttf) en la misma carpeta que el resto de archivos y cámbiale el nombre por font.ttf.

Nota: Este código ha sido escrito específicamente para la placa XIAO ESP32-C3 de Seeed Studio. Si utilizas una placa ESP32 diferente, tendrás que ajustar el parámetro de placa:y la asignación de pines GPIO para que se adapten a tu hardware específico. Si no estás seguro de cómo hacerlo, ¡no dudes en pedirle ayuda a tu asistente de IA favorito! 🙂

Aquí tienes el archivo completo keyboard.yaml. No te dejes intimidar por las largas secciones marcadas como lambda:. Estas se encargan de la lógica interna, como convertir los datos de brillo sin procesar en porcentajes (%) y dar formato al texto de la pantalla OLED.


esphome:
  name: keyboard
  on_boot:
    priority: -100
    then:
      - sensor.rotary_encoder.set_value:
          id: rotary_knob
          value: 10
      - sensor.template.publish:
          id: ha_knob
          state: 10
      - binary_sensor.template.publish:
          id: ha_service_mode
          state: false

esp32:
  board: seeed_xiao_esp32c3

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

captive_portal:

api:
  on_client_connected:
    - lambda: |-
        id(ha_service_mode).publish_state(id(service_mode));
        id(ha_active_key_sensor).publish_state(id(active_text));
  services:
    - service: set_display_text
      variables:
        new_text: string
      then:
        - lambda: |-
            id(active_text) = new_text;
            id(ha_active_key_sensor).publish_state(new_text);
            id(oled_display).update();

logger:
  baud_rate: 115200

i2c:
  sda: 6
  scl: 7

globals:
  - id: active_text
    type: std::string
    initial_value: '"OFF"'
  - id: service_mode
    type: bool
    initial_value: 'false'
  - id: saved_light_steps
    type: int
    initial_value: '10'
  - id: display_steps
    type: int
    initial_value: '10'

script:
  - id: block_feedback_script
    mode: restart
    then:
      - delay: 800ms

font:
  - file: "font.ttf"
    id: font_large
    size: 14
  - file: "font.ttf"
    id: font_sub
    size: 10

text_sensor:
  - platform: template
    name: "Active Key"
    id: ha_active_key_sensor

sensor:
  - platform: template
    name: "Rotary Knob"
    id: ha_knob

  - platform: rotary_encoder
    id: rotary_knob
    pin_a: 21
    pin_b: 20
    min_value: 0
    max_value: 10
    resolution: 2
    on_value:
      then:
        - lambda: |-
            if (isnan(x)) return;
            if (id(service_mode)) {
              if (x == 0) id(rotary_knob).set_value(1);
              id(oled_display).set_contrast(id(rotary_knob).state / 10.0);
            } else {
              if (x == 0 and id(ha_light_state).state) id(rotary_knob).set_value(1);
              id(block_feedback_script).execute();
              id(ha_knob).publish_state(id(rotary_knob).state);
            }
        - component.update: oled_display

  - platform: homeassistant
    id: ha_light_brightness
    entity_id: light.office
    attribute: brightness
    on_value:
      then:
        - lambda: |-
            if (not id(service_mode) and not id(block_feedback_script).is_running() and not isnan(x)) {
              int steps = round((x / 255.0) * 10.0);
              id(rotary_knob).set_value(steps);
              id(ha_knob).publish_state(steps);
            }
        - component.update: oled_display

binary_sensor:
  - platform: template
    name: "Service Mode"
    id: ha_service_mode

  - platform: status
    id: ha_connection_status

  - platform: homeassistant
    id: ha_light_state
    entity_id: light.office
    filters:
      - delayed_off: 500ms
    on_state:
      then:
        - lambda: |-
            if (not x) {
              if (not id(service_mode)) {
                id(rotary_knob).set_value(0);
                id(ha_knob).publish_state(0);
              } else {
                id(saved_light_steps) = 0;
              }
              id(active_text) = "OFF";
              id(ha_active_key_sensor).publish_state("OFF");
              id(oled_display).update();
            }

  - platform: gpio
    name: "Encoder Press"
    pin: {number: 8, mode: INPUT_PULLUP, inverted: true}
    on_press:
      then:
        - lambda: |-
            float current_state = isnan(id(rotary_knob).state) ? 10.0 : id(rotary_knob).state;
            if (not id(service_mode)) {
              id(saved_light_steps) = (int)current_state;
              id(service_mode) = true;
              id(rotary_knob).set_value(id(display_steps));
              id(ha_service_mode).publish_state(true);
            } else {
              id(display_steps) = (int)current_state;
              id(service_mode) = false;
              id(rotary_knob).set_value(id(saved_light_steps));
              id(ha_knob).publish_state(id(saved_light_steps));
              id(ha_service_mode).publish_state(false);
            }
        - component.update: oled_display

  - platform: gpio
    name: "Key 1"
    pin: {number: 2, mode: INPUT_PULLUP, inverted: true}
  - platform: gpio
    name: "Key 2"
    pin: {number: 3, mode: INPUT_PULLUP, inverted: true}
  - platform: gpio
    name: "Key 3"
    pin: {number: 4, mode: INPUT_PULLUP, inverted: true}
  - platform: gpio
    name: "Key 4"
    pin: {number: 5, mode: INPUT_PULLUP, inverted: true}
  - platform: gpio
    name: "Key 5"
    pin: {number: 9, mode: INPUT_PULLUP, inverted: true}
  - platform: gpio
    name: "Key 6"
    pin: {number: 10, mode: INPUT_PULLUP, inverted: true}

display:
  - platform: ssd1306_i2c
    id: oled_display
    model: "SSD1306 128x32"
    address: 0x3C
    rotation: 180
    lambda: |-
      if (id(service_mode)) {
        it.print(0, 0, id(font_large), "DISPLAY");
      } else {
        it.print(0, 0, id(font_large), id(active_text).c_str());
      }
      
      if (id(ha_connection_status).state) {
        it.print(82, 0, id(font_sub), "HA: OK");
      } else {
        it.print(82, 0, id(font_sub), "HA: --");
      }
      
      it.printf(0, 19, id(font_sub), "Int: %.0f%%", isnan(id(rotary_knob).state) ? 0.0 : id(rotary_knob).state * 10.0);
      
      if (wifi::global_wifi_component->is_connected()) {
        it.print(75, 19, id(font_sub), "Wi-Fi: OK");
      } else {
        it.print(75, 19, id(font_sub), "Wi-Fi: --");
      }

La contraseña de la red Wi-Fi se encuentra en un archivo independiente secrets.yaml. Créalo y edítelo con tus propias credenciales:

wifi_ssid: "my_wi-fi"
wifi_password: "my_secret_password"

Ahora puedes compilar y cargar el código en tu ESP32 con el comando python3 -m esphome run keyboard.yaml. No olvides que debes ejecutar este comando desde la carpeta en la que se encuentra tu archivo YAML. Cuando te lo solicite el terminal, escribe el número correspondiente al puerto USB al que estás conectado y pulsa Intro.

Si todo se ha cargado correctamente, deberías ver el registro de comunicación entre tu ordenador y el ESP32 en el terminal, y la pantalla debería encenderse.

Entre otras cosas, la pantalla también muestra información sobre el estado de la conexión Wi-Fi, que debería indicar «OK» al cabo de unos segundos. También verás una línea que dice «HA» en la pantalla, que indica el estado de la conexión con Home Assistant. No tienes que preocuparte por este estado por ahora. En cuanto configuremos todo en HA, también debería indicar OK.

Si ves los registros y la pantalla muestra la información, significa que has flasheado correctamente el ESP32 y puedes pasar a la configuración de Home Assistant.
Puedes dejar el ESP32 conectado a un puerto USB de tu ordenador o utilizar cualquier adaptador de pared USB estándar de 5 V para alimentar el macropad.

Configuración Home Assistant

1. Añadir el ESP32 a Home Assistant

Gracias a la integración nativa con ESPHome, Home Assistant debería reconocer tu nuevo macropad automáticamente.

  • Ve a Ajustes > Dispositivos & Servicios en el panel de control de Home Assistant.
  • Fíjate en la parte superior de la pestaña Integraciones. Deberías ver un dispositivo ESPHome recién detectado llamado keyboard.
  • Haz clic en Configurar y sigue las instrucciones que aparecen en pantalla para añadirlo a tu configuración.
  • Nota: Si no aparece automáticamente, haz clic en Añadir integración en la esquina inferior derecha, busca ESPHome e introduce la dirección IP local de tu ESP32.

Una vez añadido, Home Assistant carga automáticamente todas las teclas mecánicas, el codificador giratorio y el interruptor de modo de servicio.

2. Creación el Light Group Helper

Para que este proyecto resulte elegante y versátil, queremos evitar especificar directamente en el código modelos concretos de bombillas inteligentes. En su lugar, crea un ayudante Grupo de luces. De esta forma, si cambias las bombillas, solo tendrás que actualizar este grupo y tu automatización seguirá funcionando sin ningún problema.

  • Ve a Configuración > Dispositivos y Servicios y haz clic en la pestaña Asistentes situada en la parte superior.
  • Haz clic en + Crear Ssistente en la esquina inferior derecha.
  • Desplázate hacia abajo, selecciona Grupo y, a continuación, elige Grupo de luces.
  • Asigna al ayudante el nombre Office (esto generará automáticamente el ID de entidad necesario): light.office).
  • En Miembros, selecciona las luces inteligentes físicas que deseas que controle el macropad.
  • Haz clic en Enviar / Crear.

Con el Macropad conectado y nuestro grupo de luces listo, ya podemos integrarlo todo mediante nuestro script de automatización principal.

  • Ve a Configuración > Automatizaciones y escenas y haz clic en Crear automatización.
  • Selecciona Crear nueva automatización, luego haz clic en los tres puntos de la esquina superior derecha y elige Editar en YAML.
  • Borra cualquier código predeterminado, pega el script que aparece a continuación y haz clic en Guardar.

En esta guía, he asignado las seis teclas mecánicas para activar escenas de color sencillas (azul, verde, rojo, etc.), pero puedes personalizarlas para activar cualquier escena, ya sea sencilla o avanzada.


alias: Light Office - Master Control
description: Master Control for Macropad
triggers:
  - entity_id: binary_sensor.keyboard_key_1
    to: "on"
    id: press_k1
    trigger: state
  - entity_id: binary_sensor.keyboard_key_2
    to: "on"
    id: press_k2
    trigger: state
  - entity_id: binary_sensor.keyboard_key_3
    to: "on"
    id: press_k3
    trigger: state
  - entity_id: binary_sensor.keyboard_key_4
    to: "on"
    id: press_k4
    trigger: state
  - entity_id: binary_sensor.keyboard_key_5
    to: "on"
    id: press_k5
    trigger: state
  - entity_id: binary_sensor.keyboard_key_6
    to: "on"
    id: press_k6
    trigger: state
  - entity_id: sensor.keyboard_rotary_knob
    id: knob_rotation
    trigger: state
conditions:
  - condition: state
    entity_id: binary_sensor.keyboard_service_mode
    state: "off"
actions:
  - choose:
      - conditions:
          - condition: trigger
            id: knob_rotation
        sequence:
          - action: light.turn_on
            metadata: {}
            target:
              entity_id: light.office
            data:
              brightness_pct: "{{ (trigger.to_state.state | int(10)) * 10 }}"
      - conditions:
          - condition: trigger
            id: press_k1
        sequence:
          - choose:
              - conditions:
                  - condition: state
                    entity_id: sensor.keyboard_active_key
                    state: Blue
                sequence:
                  - action: light.turn_off
                    target:
                      entity_id: light.office
            default:
              - action: light.turn_on
                target:
                  entity_id: light.office
                data:
                  brightness_pct: 100
                  rgb_color:
                    - 4
                    - 51
                    - 255
              - action: esphome.keyboard_set_display_text
                data:
                  new_text: Blue
      - conditions:
          - condition: trigger
            id: press_k2
        sequence:
          - choose:
              - conditions:
                  - condition: state
                    entity_id: sensor.keyboard_active_key
                    state: Green
                sequence:
                  - action: light.turn_off
                    target:
                      entity_id: light.office
            default:
              - action: light.turn_on
                target:
                  entity_id: light.office
                data:
                  brightness_pct: 100
                  rgb_color:
                    - 0
                    - 249
                    - 0
              - action: esphome.keyboard_set_display_text
                data:
                  new_text: Green
      - conditions:
          - condition: trigger
            id: press_k3
        sequence:
          - choose:
              - conditions:
                  - condition: state
                    entity_id: sensor.keyboard_active_key
                    state: Red
                sequence:
                  - action: light.turn_off
                    target:
                      entity_id: light.office
            default:
              - action: light.turn_on
                target:
                  entity_id: light.office
                data:
                  brightness_pct: 100
                  rgb_color:
                    - 255
                    - 0
                    - 0
              - action: esphome.keyboard_set_display_text
                data:
                  new_text: Red
      - conditions:
          - condition: trigger
            id: press_k4
        sequence:
          - choose:
              - conditions:
                  - condition: state
                    entity_id: sensor.keyboard_active_key
                    state: Yellow
                sequence:
                  - action: light.turn_off
                    target:
                      entity_id: light.office
            default:
              - action: light.turn_on
                target:
                  entity_id: light.office
                data:
                  brightness_pct: 100
                  rgb_color:
                    - 255
                    - 255
                    - 0
              - action: esphome.keyboard_set_display_text
                data:
                  new_text: Yellow
      - conditions:
          - condition: trigger
            id: press_k5
        sequence:
          - choose:
              - conditions:
                  - condition: state
                    entity_id: sensor.keyboard_active_key
                    state: Purple
                sequence:
                  - action: light.turn_off
                    target:
                      entity_id: light.office
            default:
              - action: light.turn_on
                target:
                  entity_id: light.office
                data:
                  brightness_pct: 100
                  rgb_color:
                    - 255
                    - 0
                    - 255
              - action: esphome.keyboard_set_display_text
                data:
                  new_text: Purple
      - conditions:
          - condition: trigger
            id: press_k6
        sequence:
          - choose:
              - conditions:
                  - condition: state
                    entity_id: sensor.keyboard_active_key
                    state: White
                sequence:
                  - action: light.turn_off
                    target:
                      entity_id: light.office
            default:
              - action: light.turn_on
                target:
                  entity_id: light.office
                data:
                  brightness_pct: 100
                  rgb_color:
                    - 255
                    - 255
                    - 255
              - action: esphome.keyboard_set_display_text
                data:
                  new_text: White
mode: restart

Pruebas y Resumen

Una vez cargado todo, es hora de probar el macropad. Pulsa una tecla y la escena de iluminación debería cambiar inmediatamente. Gira el dial rotativo y la intensidad de la luz variará. La pantalla OLED también debería mostrar correctamente la escena seleccionada en ese momento.

Si todo funciona a la perfección, solo queda imprimir la cubierta inferior y encajarla en su sitio. Gracias a su diseño de ajuste por fricción, se mantiene firme sin necesidad de pegamento ni tornillos. Como mantuve mi versión de prueba cableada con voluminosos conectores Dupont, necesitaba un poco más de espacio libre en el interior, así que he fabricado una pieza inferior ligeramente más alta. Por el lado positivo, aproveché esto para mezclar de nuevo diferentes materiales Prusament Woodfill y crear una bonita franja de transición. Si decides soldar tus componentes, puedes reducir el grosor del archivo de la cubierta inferior para reducir significativamente la altura del macropad.

Aunque yo utilizo este macropad solo para controlar la iluminación, se puede configurar para cualquier función de hogar inteligente. Con Home Assistant encargándose de la lógica, con solo pulsar una tecla o girar el codificador se pueden ajustar las persianas, poner en marcha un robot aspirador o activar una automatización de Spotify para gastar una broma a tus invitados. ¡Las posibilidades son infinitas!

Y una vez que hayas creado tu primer proyecto de este tipo, te resultará difícil no buscar por toda la casa otros lugares en los que una impresora 3D y algunos componentes electrónicos puedan ser de ayuda.

Ten en cuenta que trabajar con Home Assistant y ESPHome implica manejar datos de red confidenciales. Aunque esta configuración funciona en mi entorno, tendrás que adaptar y ajustar el código para que se ajuste a la configuración específica de tu hogar inteligente. La configuración se comparte «tal cual» y sin ningún tipo de garantía. Asegúrate de comprobarlo todo minuciosamente y utiliza siempre la función de claves secretas de ESPHome para mantener a salvo tus credenciales privadas.

¡Felices impresiones!