M5Stack Dial - ESP32-S3 Smart Rotary Knob

How big are the images you’re including?

2 Likes

Not so many but maybe they were too big…
I just didn’t think a sec to this point.

It’s OK now, thank you… forced a resize to 120x120px, as this is just for testing.

  - id: img_startup
    file: m5dial_files/cs_smiley.png
    type: RGBA
    resize: 120x120 #added to save flash memory

image

So, after lot of digging in this post and espHome documentation: here’s my code.
Just a base for what i want to do, i only achieved the welcome page.

Code
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: !secret ip_m5stack_dial
    gateway: !secret wifi_gtw
    subnet: !secret wifi_sub
  ap:
    ssid: "m5stack-dial-fallback"
    password: !secret wifi_ap_password

ota:
  password: !secret ota_password

logger:
  level: DEBUG

api:
  encryption:
    key: !secret api_encryption_key

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  on_boot:
    then:
      - light.turn_on: backlight
      - delay: 3s
      - display.page.show: page_01
      - pcf8563.read_time: my_time
      
external_components:
  - source: github://dgaust/esphome@gc9a01
    components: [ gc9a01, ft3267 ]
    refresh: 0s

<<: !include m5dial_files/colors_and_fonts.yaml
<<: !include m5dial_files/images.yaml

spi: #required by display.ili9xxx
  mosi_pin: GPIO5
  clk_pin: GPIO6

i2c:
  - id: bus_internal #required by touchscreen.ft3267
    sda: GPIO11
    scl: GPIO12
    scan: false

rc522_i2c: # RFiD
  i2c_id:  bus_internal
  address: 0x28
  on_tag:
    then:
      - rtttl.play: 'two_short:d=4,o=5,b=100:16e6,16e6'
      - homeassistant.tag_scanned: !lambda 'return x;'

rtttl: # buzzer
  output: rtttl_out
  id: my_rtttl

output:
  - id: lcdbacklight # screen backlight
    platform: ledc
    pin: GPIO9
    min_power: 0
    max_power: 1
  - platform: ledc # buzzer
    pin: GPIO3
    id: rtttl_out

substitutions:
  name: tab-m5stack-dial
  friendly_name: M5Stack Dial
  lux_sensor_id: sensor.ip030_mmw_aqara_living_luminance
  alarm_panel_id: alarm_control_panel.alarme_maison
  gate_sensor_id: binary_sensor.tplt_gate_open_too_long
  windows_group_id: binary_sensor.windows_upstairs

# ======================================================================================
# ======================================================================================
# ================================================================================= time

time:
  - platform: pcf8563
    id: my_time
    address: 0x38
    update_interval: never # repeated synchronization is not necessary unless the external RTC is much more accurate than the internal clock
  - platform: homeassistant
    on_time_sync: # instead try to synchronize via network repeatedly ...
      then:
        - pcf8563.write_time: my_time # ... and update the RTC when the synchronization was successful
        - logger.log: "time synced"

# ======================================================================================
# ======================================================================================
# =============================================================================== lights

light:

  # ------------------------------------------------------------------ backlight
  - platform: monochromatic
    id: backlight
    name: "Backlight"
    output: lcdbacklight
    default_transition_length: 500ms

# ======================================================================================
# ======================================================================================
# ============================================================================== selects

select:
  - platform: template
    name: Page Selector
    id: page_selector
    options:
     - "page_off" # index 0
     - "page_00" # index 1
     - "page_01" # index 2
     - "page_a1" # index 3
     - "page_b1" # index 4
     - "page_c1" # index 5
     - "page_d1" # index 6
    initial_option: "page_00"
    optimistic: true
    on_value:
      - if:
          condition:
            - lambda: 'return id(page_selector).state == "page_off";'
          then:
            - light.turn_off: backlight
          else:
            - if:
                condition:
                  - light.is_off: backlight
                then:
                  - light.turn_on: backlight
                  - display.page.show: page_01
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_00";'
                then:
                  - display.page.show: page_00
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_01";'
                then:
                  - display.page.show: page_01
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_a1";'
                then:
                  - display.page.show: page_a1
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_b1";'
                then:
                  - display.page.show: page_b1
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_c1";'
                then:
                  - display.page.show: page_c1
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_d1";'
                then:
                  - display.page.show: page_d1

# ======================================================================================
# ======================================================================================
# ====================================================================== display + pages

touchscreen:
  platform: ft3267
  on_touch:
    then:
      - logger.log:
          format: 'Touch ID: %d at _____________________ (%d, %d)'
          args: [touch.id, touch.x, touch.y]
      - if:
          condition:
            - light.is_off: backlight
          then:
            - light.turn_on: backlight

display:
  - platform: ili9xxx
    model: gc9a01a
    reset_pin: GPIO8
    id: screen
    cs_pin: GPIO7
    dc_pin: GPIO4
    dimensions:
      height: 240
      width: 240
    on_page_change:
      - to: page_00
        then:
          - select.set:
              id: page_selector
              option: page_00
      - to: page_01
        then:
          - select.set:
              id: page_selector
              option: page_01
      - to: page_a1
        then:
          - select.set:
              id: page_selector
              option: page_a1
      - to: page_b1
        then:
          - select.set:
              id: page_selector
              option: page_b1
      - to: page_c1
        then:
          - select.set:
              id: page_selector
              option: page_c1
      - to: page_d1
        then:
          - select.set:
              id: page_selector
              option: page_d1
    pages:

      # _____________________________________________________ startup page
      - id: page_00
        lambda: |-
          it.image(120, 120, img_00, ImageAlign::CENTER);

      # ___________________________________________________ welcome page
      - id: page_01
        lambda: |-
          // variables =========================================================
          float screenheight = it.get_height();
          float screenwidth = it.get_width();
          float halfscreenheight = screenheight / 2;
          float halfscreenwidth = screenwidth /2;
          // background draws ==================================================
          it.filled_rectangle(0, 0, halfscreenwidth, halfscreenheight, pri_color);
          it.filled_rectangle(halfscreenwidth, halfscreenheight, 155, 155, pri_color);
          it.filled_rectangle(0, halfscreenheight, halfscreenwidth, 155, pri_color);
          it.filled_rectangle(halfscreenwidth, 0, 155, halfscreenheight, pri_color);     
          it.line(0, halfscreenheight, screenwidth, halfscreenheight, off_color);
          it.line(halfscreenwidth, 0, halfscreenwidth, screenheight, off_color);
          // icons =============================================================
          // alarm icon ----------------------------------------------------
          if (id(alarm_text).state == "armed_away") {
          it.image(65, 75, mdi_shield_lock, ImageAlign::CENTER, nova_color);
          } else if (id(alarm_text).state == "armed_night") {
          it.image(65, 75, mdi_shield_moon, ImageAlign::CENTER, nova_color);
          } else if (id(alarm_text).state == "armed_home") {
          it.image(65, 75, mdi_shield_account, ImageAlign::CENTER, blue_color);
          } else if (id(alarm_text).state == "arming") {
          it.image(65, 75, mdi_shield_sync, ImageAlign::CENTER, text_color);
          } else if (id(alarm_text).state == "pending") {
          it.image(65, 75, mdi_shield_lock_open, ImageAlign::CENTER, yellow_color);
          } else if (id(alarm_text).state == "triggered") {
          it.image(65, 75, mdi_shield_alert, ImageAlign::CENTER, red_color);
          } else {
          it.image(65, 75, mdi_shield_off, ImageAlign::CENTER, off_color);
          }
          // gate icon -----------------------------------------------------
          if (id(gate_text).state == "closed") {
          it.image(175, 75, mdi_gate_closed, ImageAlign::CENTER, text_color);
          } else if (id(gate_text).state == "open") {
          it.image(175, 75, mdi_gate_open, ImageAlign::CENTER, blue_color);
          } else {
          it.image(175, 75, mdi_gate_alert, ImageAlign::CENTER, yellow_color);
          }
          // thermostat icon -----------------------------------------------
          it.image(65, 165, mdi_thermostat, ImageAlign::CENTER, text_color);
          // other icon ----------------------------------------------------
          if (id(windows_text).state == "off") {
          it.image(175, 165, mdi_window_closed, ImageAlign::CENTER, text_color);
          } else {
          it.image(175, 165, mdi_window_open, ImageAlign::CENTER, blue_color);
          }
          // foreground draws ==================================================
          it.filled_regular_polygon(halfscreenwidth, halfscreenheight -220, 144, EDGES_OCTAGON, VARIATION_FLAT_TOP, sec_color);
          it.filled_regular_polygon(halfscreenwidth, halfscreenheight +220, 144, EDGES_OCTAGON, VARIATION_FLAT_TOP, sec_color);
          it.regular_polygon(halfscreenwidth, halfscreenheight -220, 145, EDGES_OCTAGON, VARIATION_FLAT_TOP, off_color);
          it.regular_polygon(halfscreenwidth, halfscreenheight +220, 145, EDGES_OCTAGON, VARIATION_FLAT_TOP, off_color);
          // top text draw =====================================================
          it.strftime(halfscreenwidth, 20, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "%d/%m %X", id(my_time).now());
          // bottom text draw ==================================================
          it.print(halfscreenwidth, screenheight -20, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "\U000F0046 ETEINDRE");

      # ___________________________________________________ alarm page
      - id: page_a1
        lambda: |-
          // variables ======================================================
          float screenheight = it.get_height();
          float screenwidth = it.get_width();
          float halfscreenheight = screenheight / 2;
          float halfscreenwidth = screenwidth /2;
          // text draw ==================================================
          it.print(halfscreenwidth, halfscreenheight, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "A1 PAGE");

      # ___________________________________________________ gate page
      - id: page_b1
        lambda: |-
          // variables ======================================================
          float screenheight = it.get_height();
          float screenwidth = it.get_width();
          float halfscreenheight = screenheight / 2;
          float halfscreenwidth = screenwidth /2;
          // text draw ==================================================
          it.print(halfscreenwidth, halfscreenheight, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "B1 PAGE");

      # ___________________________________________________ thermostat page
      - id: page_c1
        lambda: |-
          // variables ======================================================
          float screenheight = it.get_height();
          float screenwidth = it.get_width();
          float halfscreenheight = screenheight / 2;
          float halfscreenwidth = screenwidth /2;
          // text draw ==================================================
          it.print(halfscreenwidth, halfscreenheight, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "C1 PAGE");

      # ___________________________________________________ windows page
      - id: page_d1
        lambda: |-
          // variables ======================================================
          float screenheight = it.get_height();
          float screenwidth = it.get_width();
          float halfscreenheight = screenheight / 2;
          float halfscreenwidth = screenwidth /2;
          // text draw ==================================================
          it.print(halfscreenwidth, halfscreenheight, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "D1 PAGE");

# ======================================================================================
# ======================================================================================
# ============================================================================== sensors

sensor:

  # ----------------------------------------------------------------- lux sensor
  - platform: homeassistant
    entity_id: $lux_sensor_id
    name: "Lux Sensor"
    id: sensor_lux
    accuracy_decimals: 0
    on_value:
      - logger.log: 
          format: "$lux_sensor_id: %.0f"
          args: [ 'id(sensor_lux).state' ]
      - if:
          condition:
            - light.is_on: backlight
          then:
            - lambda: |- 
                float lux = id(sensor_lux).state;
                if (lux < 10) { 
                  id(lcdbacklight).set_level(0.1);
                }
                else if (lux < 40) {
                  id(lcdbacklight).set_level(0.3);
                }
                else if (lux < 80) {
                  id(lcdbacklight).set_level(0.5);
                }
                else {
                  id(lcdbacklight).set_level(0.7);
                }

  # ------------------------------------------------------------- rotary encoder
  - platform: rotary_encoder
    name: Rotary Encoder
    id: rotaryencoder
    resolution: 1
    pin_a:
      number: GPIO40
      mode:
       input: true
       pullup: true
    pin_b:
      number: GPIO41
      mode:
       input: true
       pullup: true
    accuracy_decimals: 0
    # accuracy_decimals: 1
    on_clockwise:
      - logger.log: "rotary_encoder : _____________________ turned_clockwise"
      - if:
          <<: &select_off
            condition:
              - lambda: 'return id(page_selector).state == "page_off";'
            then:
              - select.set:
                  id: page_selector
                  option: page_01
    on_anticlockwise:
      - logger.log: "rotary_encoder : _____________________ turned_ANTIclockwise"
      - if:
          <<: *select_off

# ======================================================================================
# ======================================================================================
# ======================================================================= binary sensors

binary_sensor:

  # --------------------------------------------------------------------- button
  - platform: gpio
    pin:
      number: GPIO42
      inverted: true
    name: M5 Button
    on_press:
      - logger.log: "button_pushed _____________________ button_pushed"
      - if:
          condition:
            - lambda: 'return id(page_selector).state == "page_00";'
          then:
            - select.set:
                id: page_selector
                option: page_off
          else:
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_01";'
                then:
                  - select.set:
                      id: page_selector
                      option: page_00
                else:
                  - select.set:
                      id: page_selector
                      option: page_01

# ================================================================= touch inputs
  # -------------------------------------------------------------- startup
  - platform: touchscreen
    id: touch_00
    internal: true
    x_min: 0
    x_max: 240
    y_min: 0
    y_max: 240
    page_id: page_00
    on_press:
      - logger.log: "touch_input OVER page_00 : _____________________ startup"
    on_release:
      - if:
          condition:
            - light.is_on: backlight
          then:
            - display.page.show: page_01

  # -------------------------------------------------------------- welcome
  - platform: touchscreen
    id: touch_01_tl
    internal: true
    x_min: 0
    x_max: 118
    y_min: 0
    y_max: 118
    page_id: page_01
    on_press:
      - logger.log: "touch_input OVER page_01 : _____________________ top_left"
    on_release:
      - if:
          condition:
            - light.is_on: backlight
          then:
            - display.page.show: page_a1
  - platform: touchscreen
    id: touch_01_tr
    internal: true
    x_min: 122
    x_max: 240
    y_min: 0
    y_max: 118
    page_id: page_01
    on_press:
      - logger.log: "touch_input OVER page_01 : _____________________ top_right"
    on_release:
      - if:
          condition:
            - light.is_on: backlight
          then:
            - display.page.show: page_b1
  - platform: touchscreen
    id: touch_01_bl
    internal: true
    x_min: 0
    x_max: 118
    y_min: 122
    y_max: 240
    page_id: page_01
    on_press:
      - logger.log: "touch_input OVER page_01 : _____________________ bottom_left"
    on_release:
      - if:
          condition:
            - light.is_on: backlight
          then:
            - display.page.show: page_c1
  - platform: touchscreen
    id: touch_01_br
    internal: true
    x_min: 122
    x_max: 240
    y_min: 122
    y_max: 240
    page_id: page_01
    on_press:
      - logger.log: "touch_input OVER page_01 : _____________________ bottom_right"
    on_release:
      - if:
          condition:
            - light.is_on: backlight
          then:
            - display.page.show: page_d1

# ======================================================================================
# ======================================================================================
# ============================================================================== numbers

number:
  - platform: template
    id: color_hue
    initial_value: 0
    min_value: 0
    max_value: 359.9
    step: 0.1
    optimistic: True
  - platform: template
    id: color_saturation
    initial_value: 0
    min_value: 0
    max_value: 100
    step: 0.1
    optimistic: True
  - platform: template
    id: dimmer_value
    initial_value: 0
    min_value: 0
    max_value: 1
    step: 0.001
    optimistic: True

# ======================================================================================
# ======================================================================================
# ============================================================================== scripts

script:
  - id: update_touch_script
    mode: restart
    parameters:
      x: int
      y: int
    then:
      - lambda: |-
          // adjust center or coordinate plane to (120, 120)
          float polar_x = x - 120;
          float polar_y = 120 - y; // Screen y is opposite of cartesian.
          // convert to polar coords
          float r = sqrt(pow(polar_x, 2) + pow(polar_y, 2));
          float theta = atan(polar_y / polar_x);
          if (polar_x < 0) {
            theta += M_PI;  // Adjust atan for quadrants 2 & 3.
          } else if (polar_y < 0) {
            theta += 2 * M_PI;  // Make quadrant 4 value positive.
          }
          // Update dimmer value.
          float min_theta = M_PI * 310 / 180;
          float max_theta = M_PI * 590 / 180;
          // Normalize the relative positioning of the values.
          float test_theta = theta;
          if (test_theta < min_theta) {
            test_theta += M_PI * 2;
          }
          ESP_LOGD("THETA", "%f < %f < %f", min_theta, test_theta, max_theta);

          // page interactions
          if (id(screen).get_active_page() == id(page_00)) {
            id(color_hue).publish_state(360 - theta / M_PI * 180);
            id(color_saturation).publish_state(min(r, (float)100));
          }
          if (id(screen).get_active_page() == id(page_01)) {
            id(color_hue).publish_state(360 - theta / M_PI * 180);
            id(color_saturation).publish_state(min(r, (float)100));
          }
      # - component.update: screen

# ======================================================================================
# ======================================================================================
# ==============================================================================  text sensors

text_sensor:
  - platform: homeassistant
    id: alarm_text
    entity_id: $alarm_panel_id
  - platform: homeassistant
    id: gate_text
    entity_id: $gate_sensor_id
    attribute: esphome_text_sensor
  - platform: homeassistant
    id: windows_text
    entity_id: $windows_group_id

I didn’t find the way to reduce those parts with lambdas… Any guess?
Select component

    on_value:
      - if:
          condition:
            - lambda: 'return id(page_selector).state == "page_off";'
          then:
            - light.turn_off: backlight
          else:
            - if:
                condition:
                  - light.is_off: backlight
                then:
                  - light.turn_on: backlight
                  - display.page.show: page_01
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_00";'
                then:
                  - display.page.show: page_00
            - if:
                condition:
                  - lambda: 'return id(page_selector)....... etc etc etc

Display component:

    on_page_change:
      - to: page_00
        then:
          - select.set:
              id: page_selector
              option: page_00
      - to: page_01
        then:
          - select.set:
              id: page_selector
              option: page_01
      - to: page_a1.......  etc etc etc
1 Like

In the video I shared a few posts up, the guy ‘Volos Projects’ has made not only a nice looking, but very responsive UI. I am not sure how he implemented it, but I am pretty sure he shares his code somewhere, maybe you could look at it and see?

This videos are only in German but the github repo is in english:
SmartHome-yourself/m5-dial-for-esphome
But has added touch switch between entities (swiping left and right) and switch between modes (swiping up and down)

https://www.youtube.com/watch?v=4dE7YONEYVk <= lights
https://www.youtube.com/watch?v=7TZfAWmYNzo <= update with more entities

Hello, has anyone already managed to operate the NFC tag via the M5Stack dial? I would like to perform this option with him. Thanks in advance