Dimming logic in Homeassistant for multiple touch input

Hi all, I’ve got problems!

let me try to explain…

First af all a disclosure, the hardware and the software of the original product are certified homekit and it works with a custom mesh ble network to obtain what I want to replicate in esphome/home assistant.
but giving that I phisically make everything i can get the devices before the certification and let them open with a more friendly firmware for homeassistant.
The why i want I it to work with esphome and home assistant, is because I think that is a little thank you from myself to the opensource comunity, I would like to give something back by giving enthusiast a way to program my hardware with a firmware that suits better their needs.

with all this out of the way let me try to explain what I’m trying to replicate.

the hardware has abunch of ws2812 for halo and buttons animations, it comes with 3 custom made touch sensor a passive buzzer ( I think that could be changed with aptic but I haven’t had time to mess with it yet) and its all driven by an esp32 that controls also 3 triac for programmable dimmng.

I’ve made everithing to work, and I’m having a bit of trouble to get the dimming function right as the original product…

I’ve found this touch dimmer logic that works great, for multiple dimmer on the same device, I’ve made some test ( I’ll provide the modifed file at the bottom) because what i wanted to obtain was something in the ball park of

  1. touch event is propagated to home assistant ( got it working easy)
  2. touch logic ( touch_dimmer.h/ numeric_value_controller.h) resides only in HA ( kinda…almost there… i dunno)
  3. numeric value in HA syncs with all the dimmer that are “subscribed” on all the other devices
    4)the value is used ( I would like almost realtime ) to change the value of the dimmer ( kinda works but really buggy and in a loop for latency)

this to obtain that I can map whatever touch of the device to whatever dim i want to control and also change the behaviour of the dimmer to act like a dumb switch for simple on-off. in this way I can also free the touch buttons to be alwais connected to just a light, i can use they touch event to trigger a scene or automation ,if I want to, without loosing a single dimmer.
the problem I’m having is that in theory it works but in the reality latency is getting in the way and the dimmer function start jumping all around .

I forgot, i’ve made the treeshold of the touch button to auto calibrate at statup and also available to automate in HA to mak ethe touch more responsive but able to read long press without changin the treeshold like other examples I’ve seen online… this is simple and it works amazingly with my hardware… kinda impressed by myself ( little tap on the shoulder :wink: ).

this is the code for Numeric_value_controller, if you create a number helper in HA ( in my case is the one named ha_dimmer_number) you can test part of the yaml with a simpre asp32 with touch configured ( a simple dev board works great)

#include "esphome/components/binary_sensor/automation.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/esp32_touch/esp32_touch.h"
#include "esphome/components/light/automation.h"
#include "esphome/components/script/script.h"
#include "esphome/core/application.h"
#include "esphome/core/automation.h"
#include "esphome/core/base_automation.h"
#include "esphome/core/log.h"

#include <string>

using namespace esphome;

using ScriptExecuteAction = script::ScriptExecuteAction<script::Script<>>;
using ScriptStopAction = script::ScriptStopAction<script::SingleScript<>>;

class NumericValueController {
  static constexpr uint64_t dimStartDelayMs = 350;
  static constexpr uint64_t dimPeriodMs = 10;
  static constexpr auto LOGTAG = "NumericValueController";

  std::string name;
  number::Number *haNumber;
  binary_sensor::BinarySensor *touchSensor;

  script::SingleScript<> *scriptDimStart;
  script::SingleScript<> *scriptDimStop;

  float dimming_dir = 0;
  float saved_value = 0.0f;

 public:
  NumericValueController(const std::string &name,
                         number::Number *haNumber,
                         binary_sensor::BinarySensor *touchSensor)
      : name(name), haNumber(haNumber), touchSensor(touchSensor)
  {
      createDimStartScript();
      createDimStopScript();
      setupBinarySensor();
  }


 private:
   void createDimStartScript()
   {
       auto *startDelayAction = new DelayAction<>();
       startDelayAction->set_delay(dimStartDelayMs + 50);
       App.register_component(startDelayAction);
       LambdaAction<> *toggleDimDirLambda = new LambdaAction<>([&]() -> void {
           const float cur_value = this->haNumber->state;
           if (this->dimming_dir == 0 || cur_value <= 0.0f) {
               this->dimming_dir = 1;
               // Start at 0%.
               this->haNumber->publish_state(0.0f);
           } else if (cur_value >= 100.0f) {
               this->dimming_dir = -1;
           } else {
               this->dimming_dir *= -1;
           }
       });
       auto *dimDelayAction = new DelayAction<>();
       dimDelayAction->set_delay(dimPeriodMs);
       App.register_component(dimDelayAction);
       LambdaAction<> *dimStepLambda = new LambdaAction<>([&]() -> void {
           const float step = this->dimming_dir * 1.0f;  // Dim step value
           const float cur_value = this->haNumber->state;
           const float new_value = clamp(cur_value + step, 0.0f, 100.0f);
           if (new_value != cur_value) {
               this->haNumber->publish_state(new_value);
               if (new_value!= 0) {
              this->saved_value = new_value;
               }
           }
           if (new_value <= 0.0f || new_value >= 99.99f) {
               this->scriptDimStop->execute();
           }
       });
       auto *whileCondition = new binary_sensor::BinarySensorCondition<>(touchSensor, true);
       auto *whileAction = new WhileAction<>(whileCondition);
       whileAction->add_then({dimDelayAction, dimStepLambda});
       scriptDimStart = new script::SingleScript<>();
       scriptDimStart->set_name(name + "__script_dimming_start");
       auto *dimStartAutom = new Automation<>(scriptDimStart);
       dimStartAutom->add_actions({startDelayAction, toggleDimDirLambda, whileAction});
   }


   void createDimStopScript() {
       auto *scriptStopAction = new ScriptStopAction(scriptDimStart);
       scriptDimStop = new script::SingleScript<>();
       scriptDimStop->set_name(name + "__script_dimming_stop");

       auto *dimStopAutom = new Automation<>(scriptDimStop);
       dimStopAutom->add_actions({scriptStopAction});
       LambdaAction<> *saveCurrentValue = new LambdaAction<>([this]() {
         if (this->haNumber->state!= 0) {
        this->saved_value = this->haNumber->state;
         }
       });
       dimStopAutom->add_actions({saveCurrentValue});
   }

   void toggle_value() {
       if (this->haNumber->state == 0.0f) {
           if (saved_value >0) {
             float new_value = saved_value;
             this->haNumber->publish_state(new_value);
           }
       } else {
           saved_value = this->haNumber->state;
           this->haNumber->publish_state(0.0f);
       }
   }

  void setupBinarySensor() {
      auto *pressTrigger = new binary_sensor::PressTrigger(touchSensor);
      auto *pressScriptAction = new ScriptExecuteAction(scriptDimStart);
      auto *pressAutomation = new Automation<>(pressTrigger);
      pressAutomation->add_actions({pressScriptAction});
      auto *releaseTrigger = new binary_sensor::ReleaseTrigger(touchSensor);
      auto *releaseScriptAction = new ScriptExecuteAction(scriptDimStop);
      auto *releaseAutomation = new Automation<>(releaseTrigger);
      releaseAutomation->add_actions({releaseScriptAction});
      auto *clickTrigger = new binary_sensor::ClickTrigger(touchSensor, 50, dimStartDelayMs);
      auto *clickAutomation = new Automation<>(clickTrigger);
      clickAutomation->add_actions({new LambdaAction<>([this]() { this->toggle_value(); })});
      LambdaAction<> *setDimDirectionLambda = new LambdaAction<>([this]() -> void {
          this->dimming_dir = this->haNumber->state > 50.0f ? -1 : 1;
      });
      clickAutomation->add_actions({setDimDirectionLambda});
  }

};

this is the configuration yaml for the actual device ( sorry for the messy code…) the only part needed to test it are the ones relative to the touch sensor, the number with homeassistant template and the template to calibrate the touch… all the other things are for the complete device , maybe you can add a dimmer if you have one lying around and try the function live… but as for where I’m standing now I get stuck at the value looping for the latence…

esphome:
  name: di-wioo
  friendly_name: DI-WiOO
  includes:
    #- touch-dimming.h
    - NumericValueController.h

  on_boot:
    priority: 500
    then:
      - delay: 3sec
      - script.execute: calibrate_threshold
      #- lambda: new NumericValueController("valore impostato da tasto 1", brt1, touch1);
      - lambda: new NumericValueController("valore impostato da tasto 2", ha_dimmer_number, touch2);
      #- lambda: new NumericValueController("valore impostato da tasto 3", brt3, touch3);
     # - rtttl.play : "Indiana:d=4,o=5,b=250:e,8p,8f,8g,8p,1c6,8p.,d,8p,8e,1f,p.,g,8p,8a,8b,8p,1f6,p,a,8p,8b,2c6,2d6,2e6,e,8p,8f,8g,8p,1c6,p,d6,8p,8e6,1f.6,g,8p,8g,e.6,8p,d6,8p,8g,e.6,8p,d6,8p,8g,f.6,8p,e6,8p,8d6,2c6"
esp32:
  board: esp32dev
  framework:
    type: arduino
logger:
api:
  encryption:
    key: "xxxxxxxx"

  services:
    - service: play_rtttl
      variables:
        song_str: string
      then:
        - rtttl.play:
            rtttl: !lambda 'return song_str;'
        - output.turn_off: buzzer_output
ota:
  - platform: esphome
    password: "xxxxxxxx"
wifi:
  ssid: xxxxx
  password: xxxxxx

  ap:
    ssid: "Hotspot configurazione DYWi-OO"
    password: "12345678"
captive_portal:
light:
  - platform: fastled_clockless
    id: led
    chipset: WS2811
    pin: GPIO22
    num_leds: 32
    rgb_order: GRB
    name: "LED"
    effects:
      !include effetti.yaml

  - platform: partition
    name: "Tasto 1"
    segments:
      - id: led
        from: 0
        to: 1
    effects:
      !include effetti.yaml

  - platform: partition
    name: "Tasto 2"
    segments:
      - id: led
        from: 2
        to: 3
    effects:
      !include effetti.yaml

  - platform: partition
    name: "Tasto 3"
    segments:
      - id: led
        from: 4
        to: 5
    effects:
      !include effetti.yaml

  - platform: partition
    name: "Halo"
    segments:
      - id: led
        from: 6
        to: 31
    effects:
      !include effetti.yaml

  - platform: monochromatic
    output: dimmer1
    id: light1
    name: Dimmer 1

  - platform: monochromatic
    output: dimmer2
    id: light2
    name: Dimmer 2

  - platform: monochromatic
    output: dimmer3
    id: light3
    name: Dimmer 3
binary_sensor:

  - platform: esp32_touch
    name: "Touch1"
    id: touch1
    pin: GPIO33
    threshold: 100
    filters:
      - delayed_on_off: 80ms

  - platform: esp32_touch
    name: "Touch2"
    id: touch2
    pin: GPIO27
    threshold: 100
    filters:
      - delayed_on_off: 80ms

  - platform: esp32_touch
    name: "Touch3"
    id: touch3
    pin: GPIO32
    threshold: 100
    filters:
      - delayed_on_off: 80ms

number:
  - platform: homeassistant
    id: ha_dimmer_number
    entity_id: input_number.lampadina_centrale
    on_value:
      then:
        - homeassistant.service:
            service: input_number.set_value
            data:
              entity_id: input_number.lampadina_centrale
              value: !lambda 'return x;'

web_server:
  port: 80

output:
   - platform: ac_dimmer
     id: dimmer1
     gate_pin:
       number: GPIO18
     zero_cross_pin:
       number: GPIO23
       allow_other_uses : true
       mode:
         input: true

   # Dimmer2 configuration
   - platform : ac_dimmer
     id: dimmer2
     gate_pin:
       number: GPIO19
     zero_cross_pin :
       number: GPIO23
       allow_other_uses : true
       mode:
         input: true

   # Dimmer3 configuration
   - platform: ac_dimmer
     id: dimmer3
     gate_pin:
       number: GPIO21
     zero_cross_pin:
       number: GPIO23
       allow_other_uses : true
       mode:
         input: true

   # Buzzer output configuration
   - platform: ledc
     pin: GPIO25
     id: buzzer_output
     inverted: False
     zero_means_zero: True

rtttl:
  output: buzzer_output
  on_finished_playback:
    - switch.turn_off: buzzer

switch:

  - platform: output
    name: 'Buzzer'
    id: buzzer
    output: buzzer_output
    on_turn_on:
    - rtttl.play: 'Looney:d=4,o=5,b=140:32p,c6,8f6,8e6,8d6,8c6,a.,8c6,8f6,8e6,8d6,8d#6,e.6,8e6,8e6,8c6,8d6,8c6,8e6,8c6,8d6,8a,8c6,8g,8a#,8a,8f'
    on_turn_off:
    - rtttl.stop

  - platform: template
    name: "Calibrate Touch Threshold"
    turn_on_action:
      - script.execute: calibrate_threshold
esp32_touch:
  setup_mode: False
script:
  - id: calibrate_threshold
    mode: single
    then:
      - lambda: |-
          float sum1 = 0.0;
          float sum2 = 0.0;
          float sum3 = 0.0;
          for (int i = 0; i < 30; i++) {
            sum1 += id(touch1).get_value();
            sum2 += id(touch2).get_value();
            sum3 += id(touch3).get_value();
          }
          float average1 = sum1 / 30.0;
          float average2 = sum2/ 30.0;
          float average3 = sum3 / 30.0;
          id(touch1).set_threshold(average1 - 50);
          id(touch2).set_threshold(average2 - 50);
          id(touch3).set_threshold(average3 - 50);

thank you for any help you can give me! and don’t spare comment like " do you know that this code could have been written better by an epylectic monkey having a seizure?"

I forgot that i would like at the end to have an excel file for the beginner user to compile and a python script that spits out the yaml needed for avery device… but i’m getting ahead…this is not part of the request…

Got it workin’ the entity is no more a a lighbulb, this makes me able to control the same number helper on HA from every touch sensors from multiple and different devices, this number helper is the value propagatet to every esphome device associated keepin’ them in sync and giving the user the options to create 1,2,3 ect way dimmer.

all the heavy lifting is done in the NumericValueController, and the value of the helper is used to control in realtime the nrightness level of a phisical dimmer in the device.

now I want to clean the code and give the option to configure the yaml in an easier way, I would like to have a flag to disable the dimming if the lighbulb connected is not dimmable…