M5Stack Dial - ESP32-S3 Smart Rotary Knob

It’s starting to look more and more that I will have to learn some c++, and the esphome codebase, to turn these ft3267 drivers into a custom esphome component to get the touchscreen working since the pull request isn’t getting much traction

Same in regards to the screen, it is doing something curious where it needs the entire (seemingly) esphome codebase and compile time takes forever. I started to pull it apart, but it is working so…kind of meh.

I know C but never made a component. Looking at the shell of them now, but it is less about knowing C than knowing the libraries and structures expected here.

Hey, have you managed to get the RFID sensor working to add the new card and make the current card work?

Hey, I tried, but it doesn’t work when I change something in my sensor. However, if I leave it out, it works. Strange, but I need to take a closer look. Just like I still need to figure out how to add a new page, like you did. Have you also been working with the RFID reader sensor or not yet?

I would avoid updating to the latest version of esphome for the time being if you can. It seems to make everything poorly behaved, and all of my S3 devices will only boot up I connect via a usb-c cable and connect the logger (via com ports).

Additionally, there are changes to the way the polling component of the display works, and the remote github repository hasn’t been updated.

Digging into it at the moment.

1 Like

Looks like esphome 2023.12.3 fixes the issues… partially. You just need to update the external component reference to my repository where I updated the gc9a01 component

external_components:
  - source: github://dgaust/esphome@gc9a01
    components: [ gc9a01 ]
1 Like

can not update this erro ?

Failed config

touchscreen.lilygo_t5_47: [source <unicode string>:91]
  
  Couldn't find any component that can be used for 'display::Display'. Are you missing a hub declaration?.
  platform: lilygo_t5_47
  interrupt_pin: 
    number: 14
    mode: 
      input: True
      output: False
      open_drain: False
      pullup: False
      pulldown: False
    inverted: False
    ignore_strapping_warning: False
    drive_strength: 20.0
  i2c_id: bus_internal
  id: my_touchscreen
  on_touch: 
    then: 
      - logger.log: 
          format: touched
          args: []
          tag: main
          level: DEBUG
  update_interval: 50ms
  address: 0x5A

Remove the whole code. It’s not relevant, wrong touchscreen.

This section is for the RFID reader Jarne.

rc522_i2c:
  i2c_id:  bus_internal
  address: 0x28
  on_tag:
    then:
      - rtttl.play: "success:d=24,o=5,b=100:c,g,b"
      - homeassistant.tag_scanned: !lambda 'return x;

When I scan a new NFC card, HA adds it to my tags list.

I think you are missing some punctuation. But besides that, I have not been able to get this to work. I am wondering why…the rc522 doesnt respond at all. Are you doing some other setup besides the i2c?

When you say doesn’t respond at all, what are you testing with?
It will only work with RFID cards and tags operating at 13.56MHz.

I2C setup -

i2c:
  - id: bus_internal
    sda: GPIO11
    scl: GPIO12
  - id: bus_porta
    sda: GPIO13
    scl: GPIO15
#  interrupt: 14

Setup for reader - rtttl wont work unless you have all the other components setup.
It can be commented out for testing.

rc522_i2c:
  i2c_id:  bus_internal
  address: 0x28
  on_tag:
    then:
      - rtttl.play: "success:d=24,o=5,b=100:c,g,b"
      - homeassistant.tag_scanned: !lambda 'return x;'

Output from logs on reader with device discovery on I2C and when a new card is scanned.

[10:00:24][C][logger:443]: Logger:
[10:00:24][C][logger:444]:   Level: DEBUG
[10:00:24][C][logger:445]:   Log Baud Rate: 115200
[10:00:24][C][logger:447]:   Hardware UART: USB_CDC
[10:00:25][C][i2c.arduino:053]: I2C Bus:
[10:00:25][C][i2c.arduino:054]:   SDA Pin: GPIO11
[10:00:25][C][i2c.arduino:055]:   SCL Pin: GPIO12
[10:00:25][C][i2c.arduino:056]:   Frequency: 50000 Hz
[10:00:25][C][i2c.arduino:059]:   Recovery: bus successfully recovered
[10:00:25][I][i2c.arduino:069]: Results from i2c bus scan:
[10:00:25][I][i2c.arduino:075]: Found i2c device at address 0x28
[10:00:25][I][i2c.arduino:075]: Found i2c device at address 0x38
[10:00:25][I][i2c.arduino:075]: Found i2c device at address 0x51
[10:00:25][C][i2c.arduino:053]: I2C Bus:
[10:00:25][C][i2c.arduino:054]:   SDA Pin: GPIO13
[10:00:25][C][i2c.arduino:055]:   SCL Pin: GPIO15
[10:00:25][C][i2c.arduino:056]:   Frequency: 50000 Hz
[10:00:25][C][i2c.arduino:059]:   Recovery: bus successfully recovered
[10:00:25][I][i2c.arduino:069]: Results from i2c bus scan:
[10:00:25][I][i2c.arduino:071]: Found no i2c devices!

[10:00:36][D][rtttl:051]: Playing song success
[10:00:36][D][rc522:263]: Found new tag '04-77-42-C4-08-43-70'

It is very random when it will respond. About 1 out of every 20 taps/waves it registers. I just had to keep trying to get it to work…but I wouldn’t say that means it is working. I wonder what tuning it needs…

Making some progress with a touch driver… not accurate at all, but it’s registering!

4 Likes

It looks like quite a bit of work is going into this m5Dial device.

I ordered a few to have a play with and am struggling to get it to install on the device.

It looks like things have changed with how a display registers now with the latest versions of ESPhome.

“ValueError: Component ID my_lcd was not declared to inherit from Component, or was registered twice. Please create a bug report with your configuration.”

From reading others have had a similar issue, but with existing devices that they were just wanting to update.
This was the example of what needed to be changed…


In the last release of ESPHome, some small changes were done to the display component, to make subcomponents leaner. With those changes, compilation breaks if a display is trying to register as a component itself (this is now done automatically when registering the display).

Also, DisplayBuffer now automatically inherits from PollingComponent, so there is no need to explicitly inherit from that.

This is probably a breaking change if someone uses a lower version of esphome after this is merged.


From the above example does anyone have any ideas how to actually fix the yaml that has been shared in the above posts?

Thanks heaps!

1 Like

I am facing the same issue:

INFO ESPHome 2023.12.5
INFO Reading configuration /config/esphome/m5dial1.yaml...
INFO Generating C++ source...
Traceback (most recent call last):
  File "/usr/local/bin/esphome", line 33, in <module>
    sys.exit(load_entry_point('esphome', 'console_scripts', 'esphome')())
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/__main__.py", line 1041, in main
    return run_esphome(sys.argv)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/__main__.py", line 1028, in run_esphome
    rc = POST_CONFIG_ACTIONS[args.command](args, config)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/__main__.py", line 458, in command_run
    exit_code = write_cpp(config)
                ^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/__main__.py", line 192, in write_cpp
    generate_cpp_contents(config)
  File "/esphome/esphome/__main__.py", line 204, in generate_cpp_contents
    CORE.flush_tasks()
  File "/esphome/esphome/core/__init__.py", line 679, in flush_tasks
    self.event_loop.flush_tasks()
  File "/esphome/esphome/coroutine.py", line 246, in flush_tasks
    next(task.iterator)
  File "/esphome/esphome/__main__.py", line 184, in wrapped
    await coro(conf)
  File "/data/external_components/c9069712/esphome/components/gc9a01/display.py", line 77, in to_code
    await setup_gc9a01(var, config)
  File "/data/external_components/c9069712/esphome/components/gc9a01/display.py", line 56, in setup_gc9a01
    await display.register_display(var, config)
  File "/esphome/esphome/components/display/__init__.py", line 119, in register_display
    await cg.register_component(var, config)
  File "/esphome/esphome/cpp_helpers.py", line 56, in register_component
    raise ValueError(
ValueError: Component ID my_lcd was not declared to inherit from Component, or was registered twice. Please create a bug report with your configuration.

using:

esphome:
  name: m5dial1
  friendly_name: m5dial1
  on_boot:
    priority: 100
    then:
      - logger.log: "Startup - Backlight on"
      - lambda: |-
          id(oledbacklight).set_level(0.7);

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

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "APIKEY"

ota:
  password: "otapassword"

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "M5Dial1 Fallback Hotspot"
    password: "HL2GnCGseKUb"

captive_portal:
    

globals:
  - id: screenwidth
    type: int
    restore_value: no
  - id: max_rotary_value
    type: int
    restore_value: no
    initial_value: '1'
  - id: min_rotary_value
    type: int
    restore_value: no
    initial_value: '100'

external_components:
  - source: github://the-smart-home-maker/esphome-4cello@gc9a01
    components: ["gc9a01"]

i2c:
  - id: bus_internal
    sda: GPIO11
    scl: GPIO12
  - id: bus_porta
    sda: 13
    scl: 15
#  interrupt: 14



image:
  - file: mdi:volume-low
    id: volume_notmute_80
    resize: 80x80


font:  
  - file: "gfonts://Roboto"
    id: roboto16
    size: 16

  - file: "gfonts://Roboto"
    id: roboto20
    size: 20
  
  - file: "gfonts://Roboto"
    id: roboto24
    size: 24


rc522_i2c:
  i2c_id:  bus_internal
  address: 0x28
  on_tag:
    then:
      - homeassistant.tag_scanned: !lambda 'return x;'

#touchscreen:
#  - platform: lilygo_t5_47
#    interrupt_pin: GPIO14
#    i2c_id: bus_internal
#    id: my_touchscreen
#    on_touch:
#      - logger.log: "touched"

spi:
  mosi_pin: GPIO5
  clk_pin: GPIO6

display:
- platform: gc9a01
  reset_pin: GPIO8
  id: my_lcd
  cs_pin: GPIO7
  dc_pin: GPIO4
  rotation: 90
  pages:
    - id: page1
      lambda: |-
        float screenheight = it.get_height();
        float screenwidth = it.get_width();
        float halfscreenheight = screenheight / 2;
        float halfscreenwidth = screenwidth /2;
        it.image(halfscreenwidth, halfscreenheight -55, volume_notmute_80, ImageAlign::TOP_CENTER);
        it.filled_circle(halfscreenwidth -12, 225, 3);
        it.circle(halfscreenwidth, 225, 3);
        it.circle(halfscreenwidth +12, 225, 3);

#  on_page_change:
#      - to: page1
#        then:
#          - sensor.rotary_encoder.set_value:
#              id: rotaryencoder
#              value: !lambda 'return id(workshop_volume).state * 100;'
#          - component.update: my_lcd



binary_sensor:
  - platform: gpio
    pin: GPIO42
    name: "BacklightButton"
 
sensor:
  - platform: rotary_encoder
    name: "Rotary Encoder"
    id: rotaryencoder
    max_value: 100
    min_value: 0
    pin_a: 
      number: GPIO40
      mode:
       input: true
       pullup: true
    pin_b: 
      number: GPIO41
      mode:
       input: true
       pullup: true
    accuracy_decimals: 1
    on_value:
      - lambda: |-
          id(oledbacklight).set_level(0.7);
    #  - component.update: my_lcd
    on_clockwise: 
      - display.page.show_next: my_lcd
      - component.update: my_lcd
    on_anticlockwise:
      - display.page.show_previous: my_lcd
      - component.update: my_lcd

   
light:
  - platform: monochromatic
    id: backlight
    name: "Backlight"
    output: oledbacklight
    default_transition_length: 250ms

output:
  - id: oledbacklight
    platform: ledc
    pin: GPIO9
    #pin: GPIO21
    max_power: 1
    min_power: 0

Was fixed here

2 Likes

hallo what you full code ? can you pleas deel this ?

My code is not going to help much Jarne as it’s custom to my setup, very much still a work in progress and there is a lot of unnecessary code for customizing for yourself.

I am using the Smart Knob for controlling media player volume, ventilation fan sped and dust extractor speed for my workshop. I have not completed the dust extractor page as I still need to integrate to my PLC.

The pages are changed by buttons on a keypad a 4 x 3 keypad installed beside the smart knob. I have automations in Home Assistant to revert the page back to volume control after 10 seconds so the vent fan or dust extraction can be changed via the smart knob, but then it reverts back to volume control.

Not sure if other people have tried the external I2C connectors, but I could not get it to work at all. I need to look at the schematics to see if it has internal pull ups.

esphome:
  name: workshop-rotary-test
  friendly_name: workshop-rotary-test
  on_boot:
    priority: 100
    then:
     - logger.log: "Startup - Backlight on"
     - lambda: |-
          id(oledbacklight).set_level(0.7);
          id(page1binary).publish_state(true);
          id(page2binary).publish_state(false);
          id(page3binary).publish_state(false);          
        # Formatted:
     - logger.log:
         format: "Vent fan Speed is %.1f"
         args: [ 'id(vent_fan_speed).state' ]

# Enable Home Assistant API
api:
  encryption:
    key: API_KEY

ota:
  password: OTA_PASSWORD

globals:
  - id: screenwidth
    type: int
    restore_value: no
  - id: max_rotary_value
    type: int
    restore_value: no
    initial_value: '1'
  - id: min_rotary_value
    type: int
    restore_value: no
    initial_value: '100'

external_components:
  - source: github://dgaust/esphome@gc9a01
    components: [ gc9a01 ]

i2c:
  - id: bus_internal
    sda: GPIO11
    scl: GPIO12
  - id: bus_porta
    sda: GPIO13
    scl: GPIO15
#  interrupt: 14

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

# Enable logging
logger:
  level: DEBUG

color:
  - id: my_red
    red: 100%
    green: 3%
    blue: 5%
  - id: my_green
    hex: 59981A
  - id: my_blue
    red: 3%
    green: 5%
    blue: 100%
  - id: my_yellow
    hex: FFFF00 
  - id: my_light_blue
    hex: 145DA0
  - id: my_light_red
    hex: fc6d6d
  - id: my_light_orange
    hex: FD7F20
  - id: my_light_yellow
    hex: B58B00

image:
  - file: mdi:volume-variant-off
    id: volume_mute
    resize: 40x40
  - file: mdi:volume-low
    id: volume_notmute
    resize: 40x40
  - file: mdi:fan
    id: fan_40
    resize: 40x40
  - file: mdi:weather-tornado
    id:  cyclone_icon_40
    resize: 40x40
  - file: mdi:weather-tornado
    id: cyclone_icon_80
    resize: 80x80
  - file: mdi:fan
    id: fan_80
    resize: 80x80
  - file: mdi:knob
    id: volumeknob
    resize: 80x80
  - file: mdi:volume-low
    id: volume_notmute_80
    resize: 80x80
  - file: mdi:volume-variant-off
    id: volume_mute_80
    resize: 80x80

font:
  - file: "fonts/arial.ttf"
    id: my_font
    size: 40
  
  - file: "gfonts://Roboto"
    id: roboto16
    size: 16

  - file: "gfonts://Roboto"
    id: roboto20
    size: 20
  
  - file: "gfonts://Roboto"
    id: roboto24
    size: 24

  - file: "gfonts://Roboto"
    id: roboto40
    size: 40

wifi:
  ssid: WIFI_SSID
  password: WIFI_PASSWRD


captive_portal:
  
rc522_i2c:
  i2c_id:  bus_internal
  address: 0x28
  on_tag:
    then:
      - rtttl.play: "success:d=24,o=5,b=100:c,g,b"
      - homeassistant.tag_scanned: !lambda 'return x;'

spi:
  mosi_pin: GPIO5
  clk_pin: GPIO6

display:
- platform: gc9a01
  reset_pin: GPIO8
  id: my_lcd
  cs_pin: GPIO7
  dc_pin: GPIO4
  rotation: 90
  pages:
    - id: page1
      lambda: |-
        float screenheight = it.get_height();
        float screenwidth = it.get_width();
        float halfscreenheight = screenheight / 2;
        float halfscreenwidth = screenwidth /2;
        if (id(mediaplayermute).state == "on")
        {
          it.image(halfscreenwidth, halfscreenwidth -55, volume_mute_80, ImageAlign::TOP_CENTER);
        }
        else
        {
          it.start_clipping(0, ((id(beam_volume).state * -1) * 300) + 155, 250, 250);
          it.filled_circle(halfscreenwidth, screenheight, 255, my_light_yellow);
          it.end_clipping();
          it.image(halfscreenwidth, halfscreenheight -55, volume_notmute_80, ImageAlign::TOP_CENTER);
        }
        it.printf(halfscreenwidth, halfscreenheight + 35, id(roboto40), TextAlign::TOP_CENTER, "%.0f%%", id(beam_volume).state * 100); 
        it.filled_circle(halfscreenwidth -12, 225, 3);
        it.circle(halfscreenwidth, 225, 3);
        it.circle(halfscreenwidth +12, 225, 3);
        id(page1binary).publish_state(true);
        id(page2binary).publish_state(false);
        id(page3binary).publish_state(false);   

    - id: page2
      lambda: |-
        float screenheight = it.get_height();
        float screenwidth = it.get_width();
        float halfscreenheight = screenheight / 2;
        float halfscreenwidth = screenwidth /2;
        it.image(halfscreenwidth, halfscreenheight - 55, fan_80, ImageAlign::TOP_CENTER);
        it.printf(halfscreenwidth, halfscreenheight + 35, id(roboto24), TextAlign::TOP_CENTER, "%.0f%%", id(vent_fan_speed).state); 
        it.circle(halfscreenwidth -12, 225, 3);
        it.filled_circle(halfscreenwidth, 225, 3);
        it.circle(halfscreenwidth +12, 225, 3);
        id(page1binary).publish_state(false);
        id(page2binary).publish_state(true);
        id(page3binary).publish_state(false);

    - id: page3
      lambda: |-
        float screenheight = it.get_height();
        float screenwidth = it.get_width();
        float halfscreenheight = screenheight / 2;
        float halfscreenwidth = screenwidth /2;
        it.image(halfscreenwidth, halfscreenheight - 55, cyclone_icon_80, ImageAlign::TOP_CENTER);
        it.printf(halfscreenwidth, halfscreenheight + 35, id(roboto24), TextAlign::TOP_CENTER, "%.0f%%", id(vent_fan_speed).state); 
        it.circle(halfscreenwidth -12, 225, 3);
        it.circle(halfscreenwidth, 225, 3);
        it.filled_circle(halfscreenwidth +12, 225, 3);
        id(page1binary).publish_state(false);
        id(page2binary).publish_state(false);
        id(page3binary).publish_state(true);
  on_page_change:
      - to: page1
        then:
          - sensor.rotary_encoder.set_value:
              id: rotaryencoder
              value: !lambda 'return id(beam_volume).state * 100;'
          - component.update: my_lcd

binary_sensor:
  - platform: gpio
    pin: GPIO42
    name: "BacklightButton"
    on_press:
        - if:
            condition:
              - display.is_displaying_page: page1
            then:
              if:
                condition:
                  lambda: return id(mediaplayermute).state == "off";
                then:
                  - logger.log: 
                      format: "The mediaplayer sensor reports value %s"
                      args: [ 'id(mediaplayermute).state' ]
                  - homeassistant.service:
                     service: media_player.volume_mute
                     data:
                      entity_id: media_player.workshop
                      is_volume_muted: 'true'
                else:
                  - logger.log: 
                      format: "The mediaplayer sensor reports value %s"
                      args: [ 'id(mediaplayermute).state' ]
                  - homeassistant.service:
                     service: media_player.volume_mute
                     data:
                      entity_id: media_player.workshop
                      is_volume_muted: 'false'

  - platform: homeassistant
    name: "keypad6"
    id: "fan_button"
    entity_id: binary_sensor.keypadkey_6
    on_state:
      then:
        - display.page.show: page2
        - component.update: my_lcd
  - platform: homeassistant
    name: "keypad9"
    id: "dusty_button"
    entity_id: binary_sensor.keypadkey_9
    on_state:
       then:
          - display.page.show: page3
          - component.update: my_lcd
  - platform: homeassistant
    name: "testbinary"
    id: "testbinary"
    entity_id: binary_sensor.test_binary
    on_state:
       then:
          - display.page.show: page2
          - component.update: my_lcd
          - delay: 10s
          - display.page.show: page1          

  - platform: template
    name: "Page 1 Active"
    id: page1binary

  - platform: template
    name: "Page 2 Active"
    id: page2binary

  - platform: template
    name: "Page 3 Active"
    id: page3binary

sensor:
  - platform: rotary_encoder
    name: "Rotary Encoder"
    id: rotaryencoder
    max_value: 100
    min_value: 0
    pin_a: 
      number: GPIO40
      mode:
       input: true
       pullup: true
    pin_b: 
      number: GPIO41
      mode:
       input: true
       pullup: true
    accuracy_decimals: 1
    on_clockwise: 
       - rtttl.play: quick_e:d=4,o=5,b=100:16e6

    on_anticlockwise:
       - rtttl.play: quick_d:d=4,o=5,b=100:16d6

  
  - platform: homeassistant
    name: "Media Volume"
    id: "beam_volume"
    entity_id: media_player.workshop
    attribute: volume_level
    
light:
  - platform: monochromatic
    id: backlight
    name: "Backlight"
    output: oledbacklight
    default_transition_length: 250ms

output:
  - id: oledbacklight
    platform: ledc
    pin: GPIO9
    #pin: GPIO21
    max_power: 1
    min_power: 0
    
  - platform: ledc
    id: buzzer_out
    pin:
      number: GPIO3

# Enable ringtone music support
rtttl:
  id: buzzer
  output: buzzer_out

text_sensor:
  - platform: homeassistant
    name: "Media Player Mute"
    id: "mediaplayermute"
    entity_id: media_player.workshop
    attribute: is_volume_muted
    on_value:
      then:
        - component.update: my_lcd
  - platform: homeassistant
    name: "Vent Fan Status"
    id: vent_fan
    entity_id: switch.vent_fan
    on_value:
      - component.update: my_lcd

  - platform: homeassistant
    name: "Vent Fan Speed"
    id: vent_fan_speed
    entity_id: input_number.vent_fan_speed
    on_value:
      - component.update: my_lcd
    
3 Likes

Great! your config helped me get going a lot. Thanks

Do you use an automation to actually change the volume? and if so why did you not map it directly to the media player volume?

Thanks heaps, i missed the new repo URL