From ac0948a537d021f187aff5d00a3a886d8622b957 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Thu, 8 Jul 2021 01:38:28 +0200 Subject: [PATCH] Copmleted implementation for controlling front panel LEDs individually. Also added docs. --- CHANGELOG.md | 8 ++ README.md | 11 +- components/xiaomi_bslamp2/__init__.py | 1 + .../xiaomi_bslamp2/binary_sensor/__init__.py | 1 + components/xiaomi_bslamp2/front_panel_hal.h | 6 +- components/xiaomi_bslamp2/output/__init__.py | 11 ++ components/xiaomi_bslamp2/output/automation.h | 12 +++ components/xiaomi_bslamp2/output/output.h | 4 + doc/configuration.md | 102 +++++++++++++++++- doc/technical_details.md | 7 +- 10 files changed, 150 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a28d2d..53c7644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **Note**: This release requires ESPHome v1.19.0 or newer. +### Added +- It is now possible to address the LEDs in the front panel of the device individually. + There are 12 LEDs in total: the power button, the color button and 10 LEDs that are + used by the original firmware to represent the lamp's current brightness setting. + The `output` component for the lamp was updated to provide access to the individual LEDs. + Check out the [documentation guide](https://github.com/mmakaay/esphome-xiaomi_bslamp2/blob/main/doc/configuration.md) + for details on how to control the individual LEDs. + ### Changed - Made it possible to use lambdas with the `preset.activate` automation. This makes it possible to link the action to an api service, which exposes the preset functionality diff --git a/README.md b/README.md index 379046a..f32753f 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,16 @@ aspect of the lamp and to integrate the lamp in your Home Assistant setup. light mode is missing in the Home Assistant GUI. * **Since the components of the lamp are exposed as ESPHome components, you don't have to stick with - the lamp's original behavior**. You can hook up the lamp in your home automation as you see fit. + the lamp's original behavior.** You can hook up the lamp in your home automation as you see fit. Use the slider to control the volume of your audio set? Long press the power button to put your house in night mode? Use the illumination behind the slider bar to represent the progress of your sour dough bread bulk fermentation? Go ahead, make it so! :-) - + +* **All LEDs that are used for illumination of the front panel (power button, color button and + 10 LEDs for the brightness slider) can be controlled individually.** This means that you have + 12 LEDs in total to use as you see fit, instead of sticking with the behavior of the original + firmware. + * **Possibilities to extend the device's functionality through hardware mods.** There are [GPIO pins that are not in use](doc/technical_details.md#esp32-pinout). If "tinkerer" is your middle name, you can use those pins to come up with your own hardware hacks to extend the device's @@ -60,7 +65,7 @@ For those who have experience with flashing ESPHome onto devices: * [Why custom firmware?](doc/why_custom_firmware.md) * [Installation guide](doc/installation.md) -* [Configuration_guide](doc/configuration.md) +* [Configuration guide](doc/configuration.md) * [Flashing guide](doc/flashing.md) * [Technical details](doc/technical_details.md) * [Sponsoring](doc/sponsoring.md) diff --git a/components/xiaomi_bslamp2/__init__.py b/components/xiaomi_bslamp2/__init__.py index 9aa7eee..fcf4877 100644 --- a/components/xiaomi_bslamp2/__init__.py +++ b/components/xiaomi_bslamp2/__init__.py @@ -37,6 +37,7 @@ FrontPanelHAL = bslamp2_ns.class_("FrontPanelHAL", cg.Component, I2CDevice) FrontPanelLEDs = bslamp2_ns.enum("FrontPanelLEDs") FRONT_PANEL_LED_OPTIONS = { "NONE": FrontPanelLEDs.LED_NONE, + "ALL": FrontPanelLEDs.LED_ALL, "POWER": FrontPanelLEDs.LED_POWER, "COLOR": FrontPanelLEDs.LED_COLOR, "1": FrontPanelLEDs.LED_1, diff --git a/components/xiaomi_bslamp2/binary_sensor/__init__.py b/components/xiaomi_bslamp2/binary_sensor/__init__.py index 5a7549a..839f13a 100644 --- a/components/xiaomi_bslamp2/binary_sensor/__init__.py +++ b/components/xiaomi_bslamp2/binary_sensor/__init__.py @@ -33,6 +33,7 @@ def validate_binary_sensor(conf): if CONF_PART in conf and CONF_FOR in conf: raise cv.Invalid("Specify only one of [part] or [for]") if CONF_PART in conf and not CONF_FOR in conf: + # Backward compatibility. conf[CONF_FOR] = conf[CONF_PART] if CONF_FOR not in conf: raise cv.Invalid("'for' is a required option for [binary_sensor.xiaomi_bslamp2]") diff --git a/components/xiaomi_bslamp2/front_panel_hal.h b/components/xiaomi_bslamp2/front_panel_hal.h index 8fc9478..fe8662a 100644 --- a/components/xiaomi_bslamp2/front_panel_hal.h +++ b/components/xiaomi_bslamp2/front_panel_hal.h @@ -22,9 +22,9 @@ using EVENT = uint16_t; // LED_1 is the slider LED closest to the power button. // LED_10 is the one closest to the color button. enum FrontPanelLEDs { - LED_ALL = 32768 + 8192 + 1023, - LED_POWER = 32768, - LED_COLOR = 8192, + LED_ALL = 16384 + 4096 + 1023, + LED_POWER = 16384, + LED_COLOR = 4096, LED_1 = 512, LED_2 = 256, LED_3 = 128, diff --git a/components/xiaomi_bslamp2/output/__init__.py b/components/xiaomi_bslamp2/output/__init__.py index 04eafe4..ffa5e89 100644 --- a/components/xiaomi_bslamp2/output/__init__.py +++ b/components/xiaomi_bslamp2/output/__init__.py @@ -14,6 +14,7 @@ AUTO_LOAD = ["xiaomi_bslamp2"] XiaomiBslamp2FrontPanelOutput = bslamp2_ns.class_( "XiaomiBslamp2FrontPanelOutput", output.FloatOutput, cg.Component) SetLEDsAction = bslamp2_ns.class_("SetLEDsAction", automation.Action) +UpdateLEDsAction = bslamp2_ns.class_("UpdateLEDsAction", automation.Action) CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { @@ -37,6 +38,10 @@ def maybe_simple_leds_value(schema): return schema({ "leds": value }) return validator +FRONT_PANEL_SCHEMA = cv.Schema({ + cv.GenerateID(CONF_ID): cv.use_id(XiaomiBslamp2FrontPanelOutput), +}) + FRONT_PANEL_LED_SCHEMA = cv.Schema( maybe_simple_leds_value(cv.Schema({ cv.GenerateID(CONF_ID): cv.use_id(XiaomiBslamp2FrontPanelOutput), @@ -82,3 +87,9 @@ async def turn_off_leds_to_code(config, action_id, template_arg, args): cg.add(action_var.set_mode(0)) cg.add(action_var.set_leds(value)) return action_var + +@automation.register_action("front_panel.update_leds", UpdateLEDsAction, FRONT_PANEL_SCHEMA) +async def update_leds_to_code(config, action_id, template_arg, args): + output_var = await cg.get_variable(config[CONF_ID]) + action_var = cg.new_Pvariable(action_id, template_arg, output_var) + return action_var diff --git a/components/xiaomi_bslamp2/output/automation.h b/components/xiaomi_bslamp2/output/automation.h index 7913691..b826ad7 100644 --- a/components/xiaomi_bslamp2/output/automation.h +++ b/components/xiaomi_bslamp2/output/automation.h @@ -37,6 +37,18 @@ template class SetLEDsAction : public Action { XiaomiBslamp2FrontPanelOutput *parent_; }; +template class UpdateLEDsAction : public Action { + public: + explicit UpdateLEDsAction(XiaomiBslamp2FrontPanelOutput *parent) : parent_(parent) {} + + void play(Ts... x) override { + parent_->update_leds(); + } + + protected: + XiaomiBslamp2FrontPanelOutput *parent_; +}; + } // namespace bslamp2 } // namespace xiaomi } // namespace esphome diff --git a/components/xiaomi_bslamp2/output/output.h b/components/xiaomi_bslamp2/output/output.h index 10c79d0..58c8206 100644 --- a/components/xiaomi_bslamp2/output/output.h +++ b/components/xiaomi_bslamp2/output/output.h @@ -35,6 +35,10 @@ class XiaomiBslamp2FrontPanelOutput : public output::FloatOutput, public Compone front_panel_->turn_off_leds(leds); } + void update_leds() { + front_panel_->update_leds(); + } + protected: FrontPanelHAL *front_panel_; }; diff --git a/doc/configuration.md b/doc/configuration.md index 00edc6f..5c5aa31 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -129,7 +129,7 @@ It is possible to control the night light mode separately. An example of this ca [example.yaml](../example.yaml), in which holding the power button is bound to activating the night light. -### light.disco_on Action +### `light.disco_on` Action This action sets the state of the light immediately (i.e. without waiting for the next main loop iteration), without saving the state to memory and without publishing the state change. @@ -148,7 +148,7 @@ on_...: The possible configuration options for this Action are the same as those for the standard `light.turn_on` Action. -### light.disco_off Action +### `light.disco_off` Action This action turns off the disco mode by restoring the state of the lamp to the last known state from before using the disco mode. @@ -198,7 +198,7 @@ light: .. ``` -*Note: duplicate template names are ok, as long as they are within their own group. If you use +*Note: Duplicate template names are ok, as long as they are within their own group. If you use duplicate preset names within a single group, then the last preset will override the earlier one(s).* @@ -348,8 +348,9 @@ sensor: ## Component: output The (float) output component is linked to the front panel illumination + level indicator. Setting -this output to value 0.0 will turn off the frontpanel illumination. Other values, up to 1.0, will -turn on the illumination and will set the level indicator to the requested level (in 10 steps). +this output (using the standard `output.set_level` action) to value 0.0 will turn off the frontpanel +illumination. Other values, up to 1.0, will turn on the illumination and will set the level indicator +to the requested level (in 10 steps). ```yaml output: @@ -362,6 +363,97 @@ output: * **id** (**Required**, ID): The id to use for this output component. * All other options from [Output](https://esphome.io/components/output/index.html) +### Addressing the LEDs of the illumination individually + +While the standard `output.set_level` action emulates the front panel illumination behavior +of the original device firmware, it is also possible to control all of the LEDs for this +illumination individually, in case you need some different behavior, e.g. leaving the +power button on at night, so the user can easily find it in the dark. + +To address the LEDs, the following identifiers can be used in your YAML configuration: + +* `POWER` : The power button illumination. +* `COLOR` : The color button illumination. +* `1`, `2`, .., `10` : The 10 LEDs on the slider, where LED `1` is closest to the + power button and LED `10` is closest to the color button. +* `ALL` : represents all of the available LEDs +* `NONE` : represents none of the available LEDs + +#### `front_panel.set_leds` Action + +This action turns on the provided LEDs, all other LEDs are turned off. + +```yaml + on_...: + then: + - front_panel.set_leds: + leds: + - POWER + - COLOR + - 1 + - 2 + - 3 +``` + +The `leds:` key can also be omitted here, making the following action calls +equivalent to the one above. + +```yaml + on_...: + then: + - front_panel.set_leds: + - POWER + - COLOR + - 1 + - 2 + - 3 +``` + +and + +```yaml + on_...: + then: + - front_panel.set_leds: [ POWER, COLOR, 1, 2, 3 ] +``` + +If only one let is specified, you can also omit the list; + +```yaml + on_...: + then: + - front_panel.set_leds: POWER +``` + +#### `front_panel.turn_on_leds` Action + +This action turns on the provided LEDs, and leaves the rest of the LEDs as-is. +The LEDs to affect are specified in the same wat as above for `front_panel.set_leds`. + +#### `front_panel.turn_off_leds` Action + +This action turns off the provided LEDs, and leaves the rest of the LEDs as-is. +The LEDs to affect are specified in the same wat as above for `front_panel.set_leds`. + +#### `front_panel.update_leds` Action + +The previous actions only modify the required state for the front panel LEDs. +Updating the actual state of the LEDs is done when the main loop for the +output component is run by ESPHome. + +If you need the required state to be pushed to the LEDs immediately, regardless +the main loop, then this action can ben used to take care of this. + +*Note: In most situations, you will not need to use this action explicitly +to make the LEDs update. Only use it when you are sure that this is required.* + +```yaml + on_...: + then: + - front_panel.set_leds: POWER + - front_panel.update_leds: +``` + ## Component: text_sensor The text sensor component publishes changes in the active [light mode](#light-modes). Possible diff --git a/doc/technical_details.md b/doc/technical_details.md index d6ddf72..57fe949 100644 --- a/doc/technical_details.md +++ b/doc/technical_details.md @@ -170,8 +170,8 @@ The commands that are used by the original firmware are these: *Note: The `READY FOR EVENT` command is only used when a new event is provided by the front panel. Information about this command can be found in the next section.* -Further experimentation has uncovered that the LEDs of the front panel (power, color, level 1 - 10) -can be enabled individually. The original firmware does not use this, but I built support for it +Further experimentation has uncovered that the LEDs of the front panel can be controlled +individually. The original firmware does not use this feature, but I built support for it into the custom firmware, because it opens up some nice possibilities. How this works, is that the general format of the "set LEDs" command is: `02 03 XX XX 64 00 00`. @@ -193,6 +193,9 @@ constructed by bitwise OR-ing the following LED bit values: | LED 9 | 00001100 00000010 | | LED 10 | 00001100 00000001 | +LED 1 is the one closest to the power button. +LED 10 is the one closest to the color button. + **Reading events from the front panel** The types of events that can occur can be summarized as: