{"id":437,"date":"2025-10-04T19:04:29","date_gmt":"2025-10-04T18:04:29","guid":{"rendered":"https:\/\/ca.rstenpresser.de\/blag\/?p=437"},"modified":"2025-10-04T19:04:29","modified_gmt":"2025-10-04T18:04:29","slug":"esphome-lights-with-multiple-outputs","status":"publish","type":"post","link":"https:\/\/ca.rstenpresser.de\/blag\/2025\/10\/esphome-lights-with-multiple-outputs\/","title":{"rendered":"esphome lights with multiple outputs"},"content":{"rendered":"\n<p>I have some weird DIY lights fixtures at home that are controlled over wifi. When I first build them, they used a <strong>micropython<\/strong> firmware that speaks MQTT. I added code so they get auto-discovered by home-assistant, but never bothered to fully implement and debug it. One side-effect was that I had to restart the lights as soon as the MQTT connection broke, e.g. when restarting the home-assistant container or network outages. So after I learned about <strong>esphome<\/strong>, it was kinda clear that I should just scrap my own python code and write a .yaml file for esphome instead.<\/p>\n\n\n\n<p>For most of the LED-Dimmers that was simple. There is a PCA9685 component in upstream esphome, as well as a BME280 sensor that most of my boards have as well. Sprinkle in a few GPIOs for LEDs and buttons and we were done.<\/p>\n\n\n\n<p>There was only one light fixture I did not migrate until today: my ceiling light. The PCB was bodged and not documented. It has four powerful LEDs that need cooling, so one GPIO controls the fan. Which itself is attached to a low-side-buck-converter. <\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"600\" height=\"250\" src=\"https:\/\/ca.rstenpresser.de\/wp-files\/2025\/10\/image.png\" alt=\"Schematic of a low-side-buck-converter.\n\nTaken from https:\/\/electronics.stackexchange.com\/questions\/330471\/low-side-n-mosfet-buck-converter\" class=\"wp-image-438\" srcset=\"https:\/\/ca.rstenpresser.de\/wp-files\/2025\/10\/image.png 600w, https:\/\/ca.rstenpresser.de\/wp-files\/2025\/10\/image-300x125.png 300w\" sizes=\"(max-width: 600px) 100vw, 600px\" \/><\/figure>\n\n\n\n<p>And to make things even more complicated, the four main LEDs all should be set to the same brightness. So multiple <strong>float-output<\/strong> need to be set at the same time. Unfortunately the esphome <strong>light<\/strong> component only allows one output. The solution for that is to add <strong>lambda<\/strong> code into the <strong>.yaml<\/strong> file.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>light:\n  - platform: monochromatic\n    icon: \"mdi:ceiling-light\"\n    name: \"Deckenlampe\"\n    output: 'pwm0_dummy'\n    gamma_correct: 1.0 # use 1.0 so we can get the 'real' brightnes by using the get_brightness() call\n    id: \"mlight\"\n    # we can get away with not implementing the 'on_turn_off' state, since we always turn on in the on_state call\n    on_turn_off:\n      - lambda: |-\n          id(light09).turn_off().perform();\n          id(light11).turn_off().perform();\n          id(light13).turn_off().perform();\n          id(light15).turn_off().perform();\n          float fan_v = 0;\n          id(fan_pwm).set_level(fan_v);\n          id(fan_speed_sensor).publish_state(fan_v * 100); \/\/ show 0..100% value\n    on_state:\n      - lambda: |-\n          \/\/ remote_values holds the values reported to the frontend.\n          \/\/ we cannot use current_values since that is about to update in the on_state change call\n          float br = id(mlight).remote_values.get_brightness();\n          \/\/ESP_LOGD(\"main\", \"===== got brightness %f\", br);\n          id(light09).turn_on().set_brightness(br).perform();\n          id(light11).turn_on().set_brightness(br).perform();\n          id(light13).turn_on().set_brightness(br).perform();\n          id(light15).turn_on().set_brightness(br).perform();\n          \/\/ now the fan control\n          \/\/ these values were checked experimentally, there is no deeper meaning\n          \/\/ with 80% there is some air movement, but the fans are not to loud\n          \/\/ and 51% does turn the fans on just slightly\n          float fan_v = 0;\n          if (br > 0.5) {\n            fan_v = 0.51; \/\/130\/255\n          }\n          if (br > 0.9) {\n            fan_v = 0.8;  \/\/200\/255\n          }\n          id(fan_pwm).set_level(fan_v);\n          id(fan_speed_sensor).publish_state(fan_v * 100); \/\/ show 0..100% value\n\n<\/code><\/pre>\n\n\n\n<p>The <strong>mlight<\/strong> entity is the only light that is exposed to the user. It has a 1.0 gamma, so its output brightness is the same as the setpoint the user selects in the home-assistant interface. It is mapped to a unused output and only serves as the dummy for controlling the 4 real outputs.<\/p>\n\n\n\n<p>Because esphome adds a transition by default, the trick is to copy the <strong>remote_values.get_brightness() <\/strong>value over to the other lights. Then do a lookup for the the fan speed and publish it as <strong>template sensor<\/strong>. If you use current_values instead, the lights will always lag behind the actual commanded setpoint, because the on_change handler is executed at the beginning of the transition.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>I have some weird DIY lights fixtures at home that are controlled over wifi. When I first build them, they used a micropython firmware that speaks MQTT. I added code so they get auto-discovered by home-assistant, but never bothered to fully implement and debug it. One side-effect was that I had to restart the lights &hellip; <a href=\"https:\/\/ca.rstenpresser.de\/blag\/2025\/10\/esphome-lights-with-multiple-outputs\/\" class=\"more-link\">Continue reading <span class=\"screen-reader-text\">esphome lights with multiple outputs<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"_links":{"self":[{"href":"https:\/\/ca.rstenpresser.de\/blag\/wp-json\/wp\/v2\/posts\/437"}],"collection":[{"href":"https:\/\/ca.rstenpresser.de\/blag\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ca.rstenpresser.de\/blag\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ca.rstenpresser.de\/blag\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ca.rstenpresser.de\/blag\/wp-json\/wp\/v2\/comments?post=437"}],"version-history":[{"count":2,"href":"https:\/\/ca.rstenpresser.de\/blag\/wp-json\/wp\/v2\/posts\/437\/revisions"}],"predecessor-version":[{"id":440,"href":"https:\/\/ca.rstenpresser.de\/blag\/wp-json\/wp\/v2\/posts\/437\/revisions\/440"}],"wp:attachment":[{"href":"https:\/\/ca.rstenpresser.de\/blag\/wp-json\/wp\/v2\/media?parent=437"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ca.rstenpresser.de\/blag\/wp-json\/wp\/v2\/categories?post=437"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ca.rstenpresser.de\/blag\/wp-json\/wp\/v2\/tags?post=437"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}