Last active
December 28, 2025 02:26
-
-
Save JonBons/2e87ef6778ad32ef6cc107903c38237c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| esphome: | |
| name: m5stickplus2-tally-cam1 | |
| friendly_name: m5-tally-cam1 | |
| platformio_options: | |
| upload_speed: 115200 | |
| esp32: | |
| board: m5stick-c | |
| framework: | |
| type: esp-idf | |
| # Enable logging | |
| logger: | |
| # Enable Home Assistant API | |
| api: | |
| encryption: | |
| key: "HA_API_KEY" | |
| ota: | |
| - platform: esphome | |
| password: "OTA_KEY" | |
| wifi: | |
| ssid: !secret wifi_ssid | |
| password: !secret wifi_password | |
| # Enable fallback hotspot (captive portal) in case wifi connection fails | |
| ap: | |
| ssid: "m5-tally-cam1" | |
| password: "AP_PASS" | |
| # MQTT configuration | |
| mqtt: | |
| broker: !secret mqtt_broker | |
| port: !secret mqtt_port | |
| username: !secret mqtt_username | |
| password: !secret mqtt_password | |
| discovery: true # Auto-discover in Home Assistant | |
| on_message: | |
| # Subscribe to the JSON topic for full bus information (includes color) | |
| - topic: 'tallyarbiter/device/${device_id}/bus/${program_bus_id}' | |
| then: | |
| - lambda: |- | |
| // Parse JSON string manually to extract state and color | |
| std::string payload = x.c_str(); | |
| bool is_active = payload.find("\"state\":\"ON\"") != std::string::npos; | |
| // Extract busColor from JSON (look for "busColor":"#xxxxxx") | |
| std::string color = "#000000"; // default | |
| size_t color_pos = payload.find("\"busColor\":\""); | |
| if (color_pos != std::string::npos) { | |
| color_pos += 11; // length of "busColor":" | |
| size_t color_end = payload.find("\"", color_pos); | |
| if (color_end != std::string::npos) { | |
| color = payload.substr(color_pos, color_end - color_pos); | |
| } | |
| } | |
| id(program_tally).publish_state(is_active); | |
| if (is_active) { | |
| id(tally_state).publish_state("program"); | |
| id(tally_color).publish_state(color); | |
| } else { | |
| if (!id(preview_tally).state) { | |
| id(tally_state).publish_state("empty"); | |
| id(tally_color).publish_state("#000000"); | |
| } | |
| } | |
| id(m5display).update(); | |
| - topic: 'tallyarbiter/device/${device_id}/bus/${preview_bus_id}' | |
| then: | |
| - lambda: |- | |
| // Parse JSON string manually to extract state and color | |
| std::string payload = x.c_str(); | |
| bool is_active = payload.find("\"state\":\"ON\"") != std::string::npos; | |
| // Extract busColor from JSON | |
| std::string color = "#000000"; // default | |
| size_t color_pos = payload.find("\"busColor\":\""); | |
| if (color_pos != std::string::npos) { | |
| color_pos += 11; // length of "busColor":" | |
| size_t color_end = payload.find("\"", color_pos); | |
| if (color_end != std::string::npos) { | |
| color = payload.substr(color_pos, color_end - color_pos); | |
| } | |
| } | |
| id(preview_tally).publish_state(is_active); | |
| if (is_active) { | |
| if (!id(program_tally).state) { | |
| id(tally_state).publish_state("preview"); | |
| id(tally_color).publish_state(color); | |
| } | |
| } else { | |
| if (!id(program_tally).state) { | |
| id(tally_state).publish_state("empty"); | |
| id(tally_color).publish_state("#000000"); | |
| } | |
| } | |
| id(m5display).update(); | |
| - topic: 'tallyarbiter/device/${device_id}/status' | |
| then: | |
| - lambda: |- | |
| std::string payload = x.c_str(); | |
| id(mqtt_connected).publish_state(payload == "online"); | |
| # Web server for configuration | |
| web_server: | |
| port: 80 | |
| spi: | |
| clk_pin: GPIO13 | |
| mosi_pin: GPIO15 | |
| # Display configuration for M5StickC Plus2 (135x240) | |
| display: | |
| - platform: st7789v | |
| model: TTGO TDisplay 135x240 | |
| cs_pin: GPIO5 | |
| dc_pin: GPIO14 | |
| reset_pin: GPIO12 | |
| rotation: 270 | |
| width: 135 | |
| height: 240 | |
| offset_height: 40 | |
| offset_width: 52 | |
| update_interval: 500ms | |
| id: m5display | |
| lambda: |- | |
| // Background color from MQTT (hex string like "#3fe481") | |
| auto color_str = id(tally_color).state; | |
| Color bg_color = Color(0, 0, 0); // Black default | |
| // Parse hex color string to RGB | |
| if (color_str.length() == 7 && color_str[0] == '#') { | |
| // Convert hex string to integer | |
| uint32_t color_hex = 0; | |
| for (int i = 1; i < 7; i++) { | |
| char c = color_str[i]; | |
| uint8_t val = 0; | |
| if (c >= '0' && c <= '9') val = c - '0'; | |
| else if (c >= 'a' && c <= 'f') val = c - 'a' + 10; | |
| else if (c >= 'A' && c <= 'F') val = c - 'A' + 10; | |
| color_hex = (color_hex << 4) | val; | |
| } | |
| // Extract RGB components | |
| uint8_t r = (color_hex >> 16) & 0xFF; | |
| uint8_t g = (color_hex >> 8) & 0xFF; | |
| uint8_t b = color_hex & 0xFF; | |
| bg_color = Color(r, g, b); | |
| } else { | |
| // Fallback to state-based colors | |
| auto state_str = id(tally_state).state; | |
| if (state_str == "program") { | |
| bg_color = Color(255, 0, 0); // Red | |
| } else if (state_str == "preview") { | |
| bg_color = Color(0, 255, 0); // Green | |
| } | |
| } | |
| it.fill(bg_color); | |
| // Get current tally state for display text | |
| auto state = id(tally_state).state; | |
| Color white = Color(255, 255, 255); | |
| Color green = Color(0, 255, 0); | |
| Color red = Color(255, 0, 0); | |
| // Device name | |
| it.printf(10, 10, id(font_small), white, "Device:"); | |
| it.printf(10, 25, id(font_medium), white, "%s", id(device_name).state.c_str()); | |
| // Tally state | |
| int y_pos = 60; | |
| if (state == "program") { | |
| it.printf(10, y_pos, id(font_large), white, "PROGRAM"); | |
| } else if (state == "preview") { | |
| it.printf(10, y_pos, id(font_large), white, "PREVIEW"); | |
| } else { | |
| it.printf(10, y_pos, id(font_large), white, "OFF AIR"); | |
| } | |
| // Battery info | |
| int battery_y = 180; | |
| it.printf(10, battery_y, id(font_small), white, "Battery: %.0f%%", id(battery_level).state); | |
| // MQTT status | |
| int status_y = 220; | |
| if (id(mqtt_connected).state) { | |
| it.printf(10, status_y, id(font_small), green, "MQTT: Online"); | |
| } else { | |
| it.printf(10, status_y, id(font_small), red, "MQTT: Offline"); | |
| } | |
| # Fonts | |
| font: | |
| - file: "gfonts://Roboto" | |
| id: font_small | |
| size: 12 | |
| - file: "gfonts://Roboto" | |
| id: font_medium | |
| size: 16 | |
| - file: "gfonts://Roboto" | |
| id: font_large | |
| size: 32 | |
| # Buttons | |
| binary_sensor: | |
| - platform: gpio | |
| pin: | |
| number: GPIO37 | |
| inverted: true | |
| name: "Button A" | |
| id: button_a | |
| on_press: | |
| - logger.log: "Button A pressed" | |
| - lambda: |- | |
| id(m5display).update(); | |
| - platform: gpio | |
| pin: | |
| number: GPIO39 | |
| inverted: true | |
| name: "Button B" | |
| id: button_b | |
| on_press: | |
| - logger.log: "Button B pressed" | |
| - lambda: |- | |
| id(m5display).update(); | |
| # Tally state binary sensors | |
| - platform: template | |
| name: "Program Tally" | |
| id: program_tally | |
| device_class: running | |
| - platform: template | |
| name: "Preview Tally" | |
| id: preview_tally | |
| device_class: running | |
| - platform: template | |
| name: "MQTT Connected" | |
| id: mqtt_connected | |
| device_class: connectivity | |
| # Battery sensor | |
| sensor: | |
| - platform: adc | |
| pin: GPIO38 | |
| attenuation: 12db | |
| update_interval: 60s | |
| name: "Battery Voltage" | |
| id: battery_voltage | |
| unit_of_measurement: "V" | |
| filters: | |
| - multiply: 2.0 # Voltage divider on Plus2 | |
| - platform: template | |
| name: "Battery Level" | |
| id: battery_level | |
| unit_of_measurement: "%" | |
| accuracy_decimals: 0 | |
| update_interval: 60s | |
| lambda: |- | |
| float voltage = id(battery_voltage).state; | |
| // M5StickC Plus2 battery calculation | |
| // Typically 3.0V (empty) to 4.2V (full) | |
| float percentage = ((voltage - 3.0) / (4.2 - 3.0)) * 100.0; | |
| if (percentage > 100.0) return 100.0; | |
| if (percentage < 0.0) return 0.0; | |
| return percentage; | |
| # Text sensors | |
| text_sensor: | |
| - platform: template | |
| name: "Tally State" | |
| id: tally_state | |
| lambda: |- | |
| return {"empty"}; | |
| update_interval: 1s | |
| - platform: template | |
| name: "Device Name" | |
| id: device_name | |
| lambda: |- | |
| return {"${display_device_name}"}; | |
| update_interval: 60s | |
| - platform: template | |
| name: "Tally Color" | |
| id: tally_color | |
| lambda: |- | |
| return {"#000000"}; | |
| update_interval: 1s | |
| # Brightness control | |
| light: | |
| - platform: monochromatic | |
| name: "Screen Brightness" | |
| id: screen_brightness | |
| output: screen_brightness_output | |
| default_transition_length: 0s | |
| gamma_correct: 1.0 | |
| output: | |
| - platform: ledc | |
| pin: GPIO27 # Backlight pin on Plus2 | |
| inverted: false | |
| id: screen_brightness_output | |
| frequency: 1000Hz | |
| # Configuration substitutions | |
| substitutions: | |
| device_name: "m5-tally-cam1" # Used for WiFi AP name | |
| display_device_name: "Cam 1" # Device name displayed on screen | |
| device_id: "ef22f2f4" # Replace with your TallyArbiter device ID | |
| program_bus_id: "334e4eda" # Default Program bus ID | |
| preview_bus_id: "e393251c" # Default Preview bus ID |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment