Martin Jerry / Tessan Dimmer Automation Help [SOLVED]

I have some Martin Jerry / Tessan dimmer switches, which are Tuya based. I’ve successfully flashed Tasmota to it using tuya_convert, so all is good there.

I’d like to convert to ESPHome, which is brilliant stuff (hats off to OttoWinter!)

These switches use a ton of GPIOs: one for each button (power, up, and down), one for each of the LEDs that indicate the dimmer level, a relay (which controls the load), PWM for dimmer. Each of these needs to be managed independently. For instance, when you raise the dim level, you have to calculate and light the indicator lights yourself (e.g. all 4 lit for 100% dim, 2 lit for 50% dim level, etc).

I actually have it working, but am running into a few issues that maybe I get some guidance on:

  1. There is a relay that turns the load (goes to the light) completely on and off. Dimming is handled separately through PWM. You have to turn the relay (switch) on to get light. This means there are two items in Home Assistant: the switch (relay) and the light (which HA shows is dimmable). At the moment, I have the “switch” turn on the “light” in ESPHome using the “on_turn_on” action, since the switch (relay) has to be on for the light to illuminate.

  2. I’m keeping a global variable with the dim level (0-100), and using that to track the current dim level and to calculate how many LED indicator lights to light up. Right now the calcs are sloppy, as I copied the Tasmota “rule” setup for now. This works brilliantly when manually manipulating the switch (e.g. pushing the UP or DOWN buttons), but I can’t figure out how to update these when Home Assistant changes the dim level.

In essence, I’m trying to have the Home Assistant config be clean (one HA light that turns on and off and is dimmable) and have the LED indicator lights be accurate to the current dim level whether done manually at the switch or through Home Assistant. Would a template switch help? I’m not sure I fully understand them yet, frankly.

Here’s what I have, I know there are a ton of lambdas and I may be overthinking it… but I do love that I can drop into C code anytime.

Any help would be appreciated! Oh, and if you have similar switches, feel free to use this… it mostly works.

# Martin Jerry / Tessan In-Wall Dimmer

esphome:
  name: dimmer_master_bedroom
  platform: ESP8266
  board: esp01_1m

wifi:
  ssid: "#####"
  password: "####"

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

web_server:
  port: 80

# Lots of stuff to define
# Reference:  https://github.com/arendst/Sonoff-Tasmota/wiki/Martin-Jerry-MJ-SD01

# GLOBAL to hold current dimmer value in percentage
# Will need to divide by 100 in lambda when setting power
globals:
  - id: dimmerpct
    type: float
    restore_value: no
    initial_value: '100.0'

#------------------------------------------------------------------------------
# Initial GPIO Outputs
#------------------------------------------------------------------------------

# Indiciator LEDs for dimmer value
# We go from 1 to 4, with 1 being the lowest LED and 4 being the highest
output:
  - platform: gpio
    pin: GPIO14
    inverted: true
    id: level_led1
    
  - platform: gpio
    pin: GPIO12
    inverted: true
    id: level_led2
  
  - platform: gpio
    pin: GPIO5
    inverted: true
    id: level_led3
  
  - platform: gpio
    pin: GPIO3
    inverted: true
    id: level_led4

# PWM for dimmer control, to attach to main light
  - platform: esp8266_pwm
    pin: GPIO13
    frequency: 1000 Hz
    id: dimmer_pwm

#------------------------------------------------------------------------------
# Main relay - internal switch to turn the light on and off
#------------------------------------------------------------------------------
switch:
  - platform: gpio
    pin: 
      number: GPIO16
      inverted: true
    id: light_relay
    name: "Master Bedroom Lamp"
    on_turn_on:
      - light.turn_on:
          id: main_light
          brightness: !lambda 'return (id(dimmerpct) / 100.0);'
      - lambda: !lambda |-
          // Turn on indicators
          if (id(dimmerpct) > 19.0) { id(level_led1).turn_on(); }
          if (id(dimmerpct) < 20.0) { id(level_led1).turn_off(); }
          if (id(dimmerpct) > 39.0) { id(level_led2).turn_on(); }
          if (id(dimmerpct) < 40.0) { id(level_led2).turn_off(); }
          if (id(dimmerpct) > 59.0) { id(level_led3).turn_on(); }
          if (id(dimmerpct) < 60.0) { id(level_led3).turn_off(); }
          if (id(dimmerpct) > 79.0) { id(level_led4).turn_on(); }
          if (id(dimmerpct) < 80.0) { id(level_led4).turn_off(); }
    on_turn_off:
      - light.turn_off:
          id: main_light
          transition_length: 1000 ms
      - output.turn_off: level_led1
      - output.turn_off: level_led2
      - output.turn_off: level_led3
      - output.turn_off: level_led4


#------------------------------------------------------------------------------
# Main output light
#------------------------------------------------------------------------------

light:
  - platform: monochromatic
    name: "Master Bedroom Lamp - Light"
    id: main_light
    gamma_correct: '0.0'
    default_transition_length: 0 ms
    output: dimmer_pwm
    
#------------------------------------------------------------------------------
# Buttons - Up, Down, and the main power toggle
#------------------------------------------------------------------------------
binary_sensor:
  # UP button
  - platform: gpio
    pin: 
      number: GPIO0
      mode: INPUT_PULLUP
    internal: true
    id: button_up
    filters:
      - delayed_on: 10ms
    on_press:
      then:
        - lambda: !lambda |-
            if (id(light_relay).state) {
              if (id(dimmerpct) <= 90.0) {
                id(dimmerpct) += 10.0;
              };
              id(dimmer_pwm).set_level(id(dimmerpct)/100.0);
              // Update Indicators
              if (id(dimmerpct) > 19.0) { id(level_led1).turn_on(); }
              if (id(dimmerpct) < 20.0) { id(level_led1).turn_off(); }
              if (id(dimmerpct) > 39.0) { id(level_led2).turn_on(); }
              if (id(dimmerpct) < 40.0) { id(level_led2).turn_off(); }
              if (id(dimmerpct) > 59.0) { id(level_led3).turn_on(); }
              if (id(dimmerpct) < 60.0) { id(level_led3).turn_off(); }
              if (id(dimmerpct) > 79.0) { id(level_led4).turn_on(); }
              if (id(dimmerpct) < 80.0) { id(level_led4).turn_off(); }
            }
            
  # DOWN button
  - platform: gpio
    pin:
      number: GPIO1
      mode: INPUT_PULLUP
    internal: true
    id: button_down
    filters:
      - delayed_on: 10ms
    on_press:
      then:
        - lambda: !lambda |-
            if (id(light_relay).state) {
              if (id(dimmerpct) >= 11) {
                id(dimmerpct) -= 10.0;
              };
              id(dimmer_pwm).set_level(id(dimmerpct)/100.0);
              // Update Indicators
              if (id(dimmerpct) > 19.0) { id(level_led1).turn_on(); }
              if (id(dimmerpct) < 20.0) { id(level_led1).turn_off(); }
              if (id(dimmerpct) > 39.0) { id(level_led2).turn_on(); }
              if (id(dimmerpct) < 40.0) { id(level_led2).turn_off(); }
              if (id(dimmerpct) > 59.0) { id(level_led3).turn_on(); }
              if (id(dimmerpct) < 60.0) { id(level_led3).turn_off(); }
              if (id(dimmerpct) > 79.0) { id(level_led4).turn_on(); }
              if (id(dimmerpct) < 80.0) { id(level_led4).turn_off(); }
            }
            

  # Main Power Button
  - platform: gpio
    pin:
      number: GPIO15
      mode: INPUT_PULLUP
    internal: true
    id: button_main_power
    filters:
      - delayed_on: 10ms
    on_press:
      then:
        - switch.toggle: light_relay
7 Likes

After some careful analysis of OttoWinter’s C++ classes, I was able to figure this out.

They key was to use an interval (every 0.5 second) to keep the relay and dimmer in sync:

  1. If Home Assistant turns the light off, turn off the relay too.
  2. If Home Assistant adjust the dimmer, update the global “dimmerpct” variable -and- adjust the dimmer indicator LEDs on the dimmer to match the new dimmer value.

The interval process simply performs these checks and adjusts everything every 0.5s. The trickiest part was figuring out how to obtain the current brightness value from the light. Some research into OttoWinter’s API documentation showed the way:

auto lightval = id(main_light).get_current_values();
id(dimmerpct) = lightval.get_brightness() * 100;

In any event, here is the completed and somewhat prettied up version. Feel free to use it if you have one of these dimmers, just change the “substitutions” section accordingly.

# Martin Jerry / Tessan In-Wall Dimmer
#
# https://amzn.com/B07FXYSVR1
# https://amzn.com/B07K67D43J

# -----------------------------------------------------------------------------
# SUBSTITIONS - MODIFY TO YOUR LIKING
# -----------------------------------------------------------------------------

substitutions:
  devicename: dimmer_master_bedroom
  lightname: Master Bedroom Lamp
  ssid: your-ssid
  ssidpass: your-password

# -----------------------------------------------------------------------------
# TYPICAL SETUP
# -----------------------------------------------------------------------------
esphome:
  name: $devicename
  platform: ESP8266
  board: esp01_1m

wifi:
  ssid: $ssid
  password: $ssidpass

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

web_server:
  port: 80

# Lots of stuff to define
# Reference:  https://github.com/arendst/Sonoff-Tasmota/wiki/Martin-Jerry-MJ-SD01

# GLOBALS 
# 'dimmerpct' holds current dimmer value in percentage
# Will need to divide by 100 in lambdas when setting power (expects 0.0-1.0)
#
# 'criticalsection' acts a semaphore while setting the light manually
# so the async task (interval) that makes sure the relay/light are in sync doesn't
# stomp on the dimmer value or power settings when doing things at the switch.
#
globals:
  - id: dimmerpct
    type: float
    restore_value: no
    initial_value: '100.0'

  - id: criticalsection
    type: bool
    initial_value: 'false'

# Script that updates the dimmer level LEDs.
# We do this a lot, so I made it a script.
# You can call via script.execute or id(update_dimmer_led).execute(); in Lambda (not documented)

script:
  - id: update_dimmer_led
    then:
      - lambda: !lambda |-
          // Turn on LED dimmer indicators
          // A bit hacky but it works.
          if (id(dimmerpct) > 19.0) { id(level_led1).turn_on(); }
          if (id(dimmerpct) < 20.0) { id(level_led1).turn_off(); }
          if (id(dimmerpct) > 39.0) { id(level_led2).turn_on(); }
          if (id(dimmerpct) < 40.0) { id(level_led2).turn_off(); }
          if (id(dimmerpct) > 59.0) { id(level_led3).turn_on(); }
          if (id(dimmerpct) < 60.0) { id(level_led3).turn_off(); }
          if (id(dimmerpct) > 79.0) { id(level_led4).turn_on(); }
          if (id(dimmerpct) < 80.0) { id(level_led4).turn_off(); }

# ------------------------------------------------------------------------------
# GPIO Outputs
# ------------------------------------------------------------------------------

# Indiciator LEDs for dimmer level
# We go from 1 to 4, with 1 being the lowest LED and 4 being the highest
output:
  - platform: gpio
    pin: GPIO14
    inverted: true
    id: level_led1
    
  - platform: gpio
    pin: GPIO12
    inverted: true
    id: level_led2
  
  - platform: gpio
    pin: GPIO5
    inverted: true
    id: level_led3
  
  - platform: gpio
    pin: GPIO3
    inverted: true
    id: level_led4

# PWM for dimmer control, to attach to main light

  - platform: esp8266_pwm
    pin: GPIO13
    id: dimmer_pwm

#------------------------------------------------------------------------------
# Main relay - internal switch to turn the light on and off
# This is not exposed to Home Assistant, since changes within
# Home Assistant GUI/automations take place against the light, not this switch.
#------------------------------------------------------------------------------
switch:
  - platform: gpio
    pin: 
      number: GPIO16
      inverted: true
    id: light_relay
    internal: true
    on_turn_on:
      # When this switch is turned on, we "turn on" the light.
      - light.turn_on:
          id: main_light
          brightness: !lambda 'return (id(dimmerpct) / 100.0);'
      - script.execute: update_dimmer_led
      - delay: 2s
      - lambda: !lambda |-
            // Turn off the semaphore if we turned it on by hitting the
            // power button.
            id(criticalsection) = false;
    on_turn_off:
      - light.turn_off:
          id: main_light
          transition_length: 1000 ms
      - output.turn_off: level_led1
      - output.turn_off: level_led2
      - output.turn_off: level_led3
      - output.turn_off: level_led4
      - delay: 2s
      - lambda: !lambda |-
            // Turn off the semaphore if we turned it on by hitting the
            // power button.
            id(criticalsection) = false;

#------------------------------------------------------------------------------
# Main output light
# This is the only thing exposed to Home Assistant, so that you can dim and
# turn on/off the light through HA.
#------------------------------------------------------------------------------

light:
  - platform: monochromatic
    name: $lightname
    id: main_light
    gamma_correct: '0.0'
    # 0ms transition length is set because it appeared to slow down dimming
    # Could be wrong, so you can remove this if you like.
    default_transition_length: 0 ms
    output: dimmer_pwm
    
#------------------------------------------------------------------------------
# Buttons - Up, Down, and the main power toggle
#------------------------------------------------------------------------------
binary_sensor:
  # UP button on dimmer (to increases brightness)
  - platform: gpio
    pin: 
      number: GPIO0
      mode: INPUT_PULLUP
    internal: true
    id: button_up
    filters:
      - delayed_on: 10ms
    on_press:
      then:
        - lambda: !lambda |-
            // Only do this if the light is actually on
            if (id(light_relay).state) {
              if (id(dimmerpct) > 90.0) {
                id(dimmerpct) = 100;
              }
              else if (id(dimmerpct) <= 90.0) {
                id(dimmerpct) += 10.0;
              };
              // Set semaphore to keep the interval process from overwriting
              // the dimmerpct variable while the user is adjusting the dimmer manually
              id(criticalsection) = true;
              auto call = id(main_light).turn_on();
              call.set_brightness(id(dimmerpct)/100.0);
              call.set_transition_length(1);
              call.perform();
              // Update the LEDs
              id(update_dimmer_led).execute();
              // Clear semaphore
              id(criticalsection) = false;
            }
            
  # DOWN button
  - platform: gpio
    pin:
      number: GPIO1
      mode: INPUT_PULLUP
    internal: true
    id: button_down
    filters:
      - delayed_on: 10ms
    on_press:
      then:
        - lambda: !lambda |-
            if (id(light_relay).state) {
              if (id(dimmerpct) < 11) {
                id(dimmerpct) = 5;
              }
              else if (id(dimmerpct) >= 11) {
                id(dimmerpct) -= 10.0;
              };
              // Set semaphore to keep the interval from overwriting
              // while the user is adjusting the dimmer manually
              id(criticalsection) = true;
              auto call = id(main_light).turn_on();
              call.set_brightness(id(dimmerpct)/100.0);
              call.set_transition_length(1);
              call.perform();
              // Update Indicators
              id(update_dimmer_led).execute();
              // Clear semaphore
              id(criticalsection) = false;
            }
            

  # Main Power Button
  - platform: gpio
    pin:
      number: GPIO15
      mode: INPUT_PULLUP
    internal: true
    id: button_main_power
    filters:
      - delayed_on: 10ms
    on_press:
      then:
        - lambda: !lambda |-
            // Set critical section so the interval doesn't stomp on us while
            // the relay and lights are being turned off
            id(criticalsection) = true;
            id(light_relay).toggle();

# ------------------------------------------------------------------
# Intervals
# ------------------------------------------------------------------

interval:
  # Check every half second, only if semaphore is false
  - interval: 0.5s
    then:
      - lambda: !lambda |-
            // We only want to do this if criticalsection semaphore is false
            if (!id(criticalsection)) {
              auto lightval = id(main_light).get_current_values();
              // In HomeAssistant, we're controlling the light, not the relay
              // If you turn on or off the light in HA, we need to make sure
              // the relay matches too.
              //
              // Check if light is on but relay is not and fix
              if (lightval.is_on() && !id(light_relay).state) {
                id(light_relay).turn_on();
              }
              // ...and check if the light is off but the relay is on, and fix
              if (!lightval.is_on() && id(light_relay).state) {
                id(light_relay).turn_off();
              }
              // Get brightness, only if the light is on
              // A semaphore is set when being controlled physically, so we don't
              // overwrite this too soon.
              if (lightval.is_on() && !id(criticalsection)) {
                id(dimmerpct) = lightval.get_brightness() * 100;
                // Update Indicators
                id(update_dimmer_led).execute();
              }
            }
6 Likes

Thanks for posting this! I used some of your ideas as a base and came up with a config that I think is simpler, easier to read and includes some extra functionality… Will post it as soon as I can satisfactorily clean it up. Thanks again for posting your config!

1 Like

Nice! I’m glad you found it useful, and I’m sure there are tons of improvements… I know mine is a little hacky. I’m very new to ESPHome, and of course I start on a complex use case! :slight_smile:

I look forward to seeing your config so I can learn as well.

markm and mjoshd, thanks for sharing your work on this. I have been workign on the same switch in Esphome and struggling with the code. I really appreciate your work and sharing with the community. Look forward to seeing your code and hearing how things are working.

@medichugh , @markm
Sorry for the delay… had obligations. Finally had a chance to get it all prettied up so here is what I came up with. Much thanks to @markm for some of the ideas! :tada:

4 Likes

Very nice! Much cleaner than my whack-together! :slight_smile:

I wasn’t aware you could use the “power_supply” feature like this, so I learned something new (thanks!)

Looking at the docs, I saw “ATX power supply” and kinda skipped it, but now that I’m seeing how you used it, and knowing how an ATX power circuit works, it makes total sense to use that to tie the relay/light together. Also eliminates all the machinations with the semaphore to keep the interval from stomping on things when trying to keep the two in-sync. Awesome.

I’ll reflash mine tonight and give it a whirl. Thanks again for sharing!

agree with markm, didn’t understand the ATX power supply. Learned a ton from your sketch. Thanks again for sharing. I have a dimmer that I will upload it to this weekend. I’ll let you know how it goes. Thanks again for sharing.

1 Like

@mjoshd, this works great. However, my wife noticed that, in the dark, the dim indicator LEDs were all flickering. Not a big deal, but…

One minor tweak to the “interval” fixes it… move the ending curly brace for the “is it on” test to encompass the entire LED illumination logic. Verified this fixes it:

interval:
  - interval: 500ms
    # https://esphome.io/guides/automations.html#interval
    then:
      - lambda: |-
          auto dimmer_vals = id(dimmer).get_current_values();
          if (dimmer_vals.is_on()) {
            id(dimmer_lvl) = dimmer_vals.get_brightness();
            if (id(dimmer_lvl) > .19) { id(led2).turn_on(); }
            if (id(dimmer_lvl) < .20) { id(led2).turn_off(); }
            if (id(dimmer_lvl) > .39) { id(led3).turn_on(); }
            if (id(dimmer_lvl) < .40) { id(led3).turn_off(); }
            if (id(dimmer_lvl) > .59) { id(led4).turn_on(); }
            if (id(dimmer_lvl) < .60) { id(led4).turn_off(); }
            if (id(dimmer_lvl) > .79) { id(led5).turn_on(); }
            if (id(dimmer_lvl) < .80) { id(led5).turn_off(); }
          }
          if (!dimmer_vals.is_on()) {
            id(led2).turn_off();
            id(led3).turn_off();
            id(led4).turn_off();
            id(led5).turn_off();
          }

I’d be happy to do a pull request against your repo, but frankly, I’ve never done one. If you’re willing to put up with me learning the Github PR ropes…

@markm
That is very interesting… I’m not seeing the same behavior in my dimmer but I do like the change you proposed. It makes more sense to encapsulate the led on/off logic inside of the ‘if’.

You are welcome to use this for your first PR in starting to learn Github :smiley: it’s how I got started too!

@mjoshd, you can really only see it in the dark… and even then it is really subtle. Only the dimmer LEDs flicker, up to the last dimmer value. Cup you hands over the switch to make it dark, you’ll probably see it.

Not a big deal it all, but after looking at the logic, it was such an easy fix that I figured why not.

Thanks for the offer on the PR, I’m going to give it a shot!

Thanks @mjoshd, my first PR… I must have done it correctly! :slight_smile:

To anybody else finding this thread, the code at @mjoshd’s Github works really, really well. I have a few of these dimmers now (well, a few Martin Jerry and one Tessan) which were running Tasmota.

Although I like Tasmota, there was a fair amount of lag in operating the dimmer, especially when turning off the light (there was a 1-2 second delay before the light would actually shut off). It’s a little thing that triggered my OCD somehow and what prompted me to try flashing esphome.

The esphome version is SO much snappier. No more lag when shutting off the light, and overall the dimming/navigation at the dimmer itself is just peppy.

1 Like

I’d like to visualize the 3 button functions without firing up the actual relay so I can turn hue bulbs off and on and dim them from the switch.

Anybody have experience with that?

@hgelpke I would think that is doable, although I’ve never tried it and would require a fair amount of rework.

A few options come to mind:

  • Change the “on_press” for the up/down/power switch to call 'homeassistant.service" instead of flipping relays to control Hue via HA,

or,

  • Change the apply_dimming script to call 'homeassistant.service" instead of setting the PWM for the dimmer.

If you want the side LED’s to reference the dim level of the Hue bulbs and stay in sync, you’ll have to change the “interval” too, since it retrieves the current dimmer value from the internal PWM value and sets the LEDs accordingly. Perhaps you can retrieve it from HomeAssistant?. See https://esphome.io/components/sensor/homeassistant.html. You’d probably need a template sensor in Home Assistant to expose the current dimmer value as a separate entity you could reference from esphome.

I know that there is never a stupid question but here goes.
I have flashed a few sonoff devices but never a MJ dimmer switch.

Can you explain in a little detail how you “powered” the switch with it’s 4 leads or what tutorial you may have followed to get power to the device. You have the load, line and neutral but how was it setup.

I am used to working with power but do not want to “blow up” anything.

Thanks all for the assistance

I would cut the head off of an extension cord and then use wire nuts to connect neutral and either load or line to the cord. I can never remember which is which. If you get it wrong, it just won’t work. Nothing will explode. Just make sure you either tape or wire nut the remaining wire so there are no surprises if the relay gets switched. The use Tuya-convert to flash it. No need to open it up or solder anywires.

@carltonb here’s the process I used for the initial flashing away from the Tuya firwmare:

  • Cut the end off a 3-prong extension cord.
  • Wire the extension cord line, neutral, the ground to the MJ using wire nuts. Leave the load (brown) open (I capped it off with a wire nut). Plug in the extension cord to mains to power up the switch.
  • Use tuya-convert to convert it (I used an Rpi 3 for this). I initially loaded the Tasmota Basic firmware that came with tuya-convert, which I then upgraded to esphome via the Tasmota firmware upgrade process.

Thank you for the response.

Next question. Do I need to use Tuya or Tasmota to convert or can I go direct to ESPhome?

This world of hass is changing by the minute and not sure which system to use.

What tutorial did you use to get started with the PI.
Thanks

The dimmer, as it comes out of the box, is running Tuya firmware.

You can either solder leads to the chip for a serial (e.g. FTDI) connection, or you can use tuya-convert (https://github.com/ct-Open-Source/tuya-convert) to load a custom firmware over the air (no soldering needed). That custom firmware can either be Tasmota or an esphome compiled program… you just need a ‘.bin’ file to push to the dimmer.

tuya-convert comes with a basic Tasmota .bin file, so it’s relatively easy to flash that without having to go through the hassle of compiling an esphome binary, download it from the dashboard, coping it over to the Raspberry Pi, etc…

I simply flash the basic Tasmota right from the tuya-convert package, attach to the Tasmota temporary wireless network with my phone, browse to http://192.168.4.1, configure my wireless network (SSID and password) using the webpage, then use the “Firmware Upgrade” button to load my esphome .bin file directly once it’s attached to my WiFi.

The RPi3 is a great platform for tuya-convert since you need a hardware platform with both hard-wired Ethernet and WiFi… tuya-convert uses the RPI WiFi to setup in access-point mode that the MJ switch connects to in order to allow firmware flashing, and the Ethernet is used to communicate with the Pi (e.g. ssh). The tuya-convert link above describes the process pretty well, and in my case, it worked straight away following the steps.

If you’re asking on how to get started on the RPI from scratch, there are a ton of tutorials out there.

Hope this helps…