diff --git a/.gitignore b/.gitignore index 555e90e..440ece2 100644 --- a/.gitignore +++ b/.gitignore @@ -188,3 +188,5 @@ cython_debug/ # Built Visual Studio Code Extensions *.vsix +# Custom to this repo +buttonplus.yaml \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..e7e80e0 --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pyyaml = ">=6.0" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..82f483a --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,101 @@ +{ + "_meta": { + "hash": { + "sha256": "fe14101cf3766f9bbb7942a51a278b5dab6d277c4ac67ed1e09c2774ea4655a2" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "pyyaml": { + "hashes": [ + "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", + "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", + "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", + "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", + "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", + "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", + "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", + "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", + "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", + "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", + "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", + "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", + "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", + "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", + "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", + "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", + "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", + "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", + "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", + "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", + "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", + "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", + "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", + "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", + "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", + "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", + "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", + "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", + "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", + "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", + "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", + "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", + "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", + "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", + "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", + "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", + "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", + "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", + "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", + "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", + "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", + "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", + "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", + "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", + "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", + "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", + "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", + "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", + "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", + "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", + "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", + "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", + "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", + "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", + "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", + "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", + "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", + "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", + "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", + "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", + "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", + "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", + "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", + "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", + "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", + "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", + "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", + "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", + "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", + "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", + "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", + "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", + "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==6.0.3" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 3f6e7da..74b302f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ # buttonplus-esphome-generator -A python script that generates a full ESPHome config for the Button+ V2 \ No newline at end of file +A python script that generates a full ESPHome config for the Button+ V2 + +## Project uses pipenv +run `pipenv install` to setup the project the first time. +run `pipenv shell` after that to open the virtual environment. +You can then use `python + +## Config example +The configuration (handmade) on which this generator is based, can be found in the `esphome` folder. \ No newline at end of file diff --git a/esphome/3bar_1display/base.yaml b/esphome/3bar_1display/base.yaml new file mode 100644 index 0000000..9b4cb49 --- /dev/null +++ b/esphome/3bar_1display/base.yaml @@ -0,0 +1,304 @@ +packages: + base: !include ../common.yaml + scripts: !include scripts.yaml + events: !include events.yaml + +esphome: + on_boot: + priority: -100 + then: + - script.execute: setup_bar_displays + - script.execute: update_bar1_left + - script.execute: update_bar1_right + - script.execute: update_bar2_left + - script.execute: update_bar2_right + - script.execute: update_bar3_left + - script.execute: update_bar3_right + +mcp23008: + - id: 'base_J0' + i2c_id: buttons + address: 0x20 + - id: 'base_J1' + i2c_id: buttons + address: 0x21 + - id: 'base_J2' + i2c_id: buttons + address: 0x22 + - id: 'base_J3' + i2c_id: buttons + address: 0x23 + +binary_sensor: + ############################ + ### Buttons Main Display ### + ############################ + - platform: gpio + id: 'main_display_btn_left' + name: "Main Display button Left" + pin: + mcp23xxx: base_J0 + number: 6 + mode: INPUT_PULLUP + inverted: true + - platform: gpio + id: 'main_display_btn_right' + name: "Main Display button Right" + pin: + mcp23xxx: base_J0 + number: 2 + mode: INPUT_PULLUP + inverted: true + + ############################ + ### Buttons BAR module 1 ### + ############################ + - platform: gpio + id: 'bar1_btn_left' + name: "BAR 1 button Left" + pin: + mcp23xxx: base_J1 + number: 6 + mode: INPUT_PULLUP + inverted: true + - platform: gpio + id: 'bar1_btn_right' + name: "BAR 1 button Right" + pin: + mcp23xxx: base_J1 + number: 2 + mode: INPUT_PULLUP + inverted: true + + ############################ + ### Buttons BAR module 2 ### + ############################ + - platform: gpio + id: 'bar2_btn_left' + name: "BAR 2 button Left" + pin: + mcp23xxx: base_J2 + number: 6 + mode: INPUT_PULLUP + inverted: true + - platform: gpio + id: 'bar2_btn_right' + name: "BAR 2 button Right" + pin: + mcp23xxx: base_J2 + number: 2 + mode: INPUT_PULLUP + inverted: true + + ############################ + ### Buttons BAR module 3 ### + ############################ + - platform: gpio + id: 'bar3_btn_left' + name: "BAR 3 button Left" + pin: + mcp23xxx: base_J3 + number: 6 + mode: INPUT_PULLUP + inverted: true + - platform: gpio + id: 'bar3_btn_right' + name: "BAR 3 button Right" + pin: + mcp23xxx: base_J3 + number: 2 + mode: INPUT_PULLUP + inverted: true + +light: + - id: !extend neopixels + num_leds: 13 + ############################## + ### RGB LED's BAR module 1 ### + ############################## + - platform: partition + name: "BAR 1 Left RGB Front" + id: bar1_left_rgb_front + segments: + - id: neopixels + from: 0 + to: 0 + - platform: partition + name: "BAR 1 Left RGB Back" + id: bar1_left_rgb_back + segments: + - id: neopixels + from: 1 + to: 1 + - platform: partition + name: "BAR 1 Right RGB Front" + id: bar1_right_rgb_front + segments: + - id: neopixels + from: 2 + to: 2 + - platform: partition + name: "BAR 1 Right RGB Back" + id: bar1_right_rgb_back + segments: + - id: neopixels + from: 3 + to: 3 + ############################## + ### RGB LED's BAR module 2 ### + ############################## + - platform: partition + name: "BAR 2 Left RGB Front" + id: bar2_left_rgb_front + segments: + - id: neopixels + from: 4 + to: 4 + - platform: partition + name: "BAR 2 Left RGB Back" + id: bar2_left_rgb_back + segments: + - id: neopixels + from: 5 + to: 5 + - platform: partition + name: "BAR 2 Right RGB Front" + id: bar2_right_rgb_front + segments: + - id: neopixels + from: 6 + to: 6 + - platform: partition + name: "BAR 2 Right RGB Back" + id: bar2_right_rgb_back + segments: + - id: neopixels + from: 7 + to: 7 + ############################## + ### RGB LED's BAR module 3 ### + ############################## + - platform: partition + name: "BAR 3 Left RGB Front" + id: bar3_left_rgb_front + segments: + - id: neopixels + from: 8 + to: 8 + - platform: partition + name: "BAR 3 Left RGB Back" + id: bar3_left_rgb_back + segments: + - id: neopixels + from: 9 + to: 9 + - platform: partition + name: "BAR 3 Right RGB Front" + id: bar3_right_rgb_front + segments: + - id: neopixels + from: 10 + to: 10 + - platform: partition + name: "BAR 3 Right RGB Back" + id: bar3_right_rgb_back + segments: + - id: neopixels + from: 11 + to: 11 + +output: + ####################################### + ### CS pins to control BAR Displays ### + ####################################### + - id: cs_pin_bar1_left + platform: gpio + pin: + mcp23xxx: base_J1 + number: 5 + mode: + output: true + inverted: true + - id: cs_pin_bar1_right + platform: gpio + pin: + mcp23xxx: base_J1 + number: 1 + mode: + output: true + inverted: true + - id: cs_pin_bar2_left + platform: gpio + pin: + mcp23xxx: base_J2 + number: 5 + mode: + output: true + inverted: true + - id: cs_pin_bar2_right + platform: gpio + pin: + mcp23xxx: base_J2 + number: 1 + mode: + output: true + inverted: true + - id: cs_pin_bar3_left + platform: gpio + pin: + mcp23xxx: base_J3 + number: 5 + mode: + output: true + inverted: true + - id: cs_pin_bar3_right + platform: gpio + pin: + mcp23xxx: base_J3 + number: 1 + mode: + output: true + inverted: true + +display: + #################### + ### Main Display ### + #################### + - id: main_display + platform: ili9xxx + model: ili9341 + invert_colors: false + color_order: rgb + update_interval: 30s + dc_pin: + number: GPIO7 + allow_other_uses: true + cs_pin: + mcp23xxx: base_J0 + number: 5 + mode: + output: true + inverted: false + show_test_card: false + dimensions: + height: 240 + width: 320 + rotation: 180 + ################### + ### BAR modules ### + ################### + - id: bar_display + platform: ili9xxx + model: ST7735 + color_order: bgr + update_interval: never + dc_pin: + number: GPIO7 + allow_other_uses: true + invert_colors: false + show_test_card: false + auto_clear_enabled: false + dimensions: + height: 160 + width: 80 + offset_width: 24 diff --git a/esphome/3bar_1display/config.yaml b/esphome/3bar_1display/config.yaml new file mode 100644 index 0000000..1d0a229 --- /dev/null +++ b/esphome/3bar_1display/config.yaml @@ -0,0 +1,275 @@ +esphome: + name: buttonplus + friendly_name: ButtonPlus + +esp32: + variant: ESP32S3 + board: esp32-s3-devkitc1-n16r8 + flash_size: 16MB + framework: + type: esp-idf + +# Enable logging +logger: + level: DEBUG + +# Enable Home Assistant API +api: + encryption: + key: "" + +ota: + - platform: esphome + password: "" + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "Buttonplus" + password: "" + +packages: + remote_package_files: + files: + - path: 'base.yaml' + vars: + nr_of_pages: '3' + page_names: '{"Start", "Music", "Page 3"}' + - path: '../cards/music.yaml' + vars: + music_card_entity_id: 'media_player.spotifyplus_kennyboy44' + music_card_text_color: FFFFFF + music_card_icon_main_color: FF7F00 + music_card_icon_bar_color: FF7F00 + +font: + - file: "https://github.com/web-fonts/ttf/raw/refs/heads/master/bpg-ingiri-arial-webfont.ttf" + id: font_arial20 + size: 20 + bpp: 4 + - file: "https://torinak.com/font/7segment.ttf" + id: font_clock + size: 64 + bpp: 4 + - file: "https://torinak.com/font/7segment.ttf" + id: font_date + size: 24 + bpp: 4 + +image: + - file: mdi:lightbulb + id: lightbulb + resize: 80x80 + type: GRAYSCALE + - file: mdi:alert + id: alert + resize: 80x80 + type: GRAYSCALE + - file: mdi:chevron-right + id: icon_next_page + resize: 30x30 + type: GRAYSCALE + - file: mdi:chevron-left + id: icon_prev_page + resize: 30x30 + type: GRAYSCALE + - file: mdi:home-thermometer-outline + id: icon_temp_inside + resize: 40x40 + type: GRAYSCALE + - file: mdi:brightness-percent + id: icon_ambient_inside + resize: 40x40 + type: GRAYSCALE + +color: + - id: red + hex: FF0000 + - id: blue + hex: 0000FF + - id: yellow + hex: FFFF00 + - id: green + hex: 00FF00 + - id: orange + hex: FF7F00 + - id: white + hex: FFFFFF + - id: grey + hex: A6A6A6 + +display: + ######### + # MAIN ## + ######### + - id: !extend main_display + # data_rate: 40MHz + lambda: |- + id(all_pages_main).execute(); + + switch (id(actual_page)){ + case 0: + it.print(100, 150, id(font_arial20), "Hooray, it works!"); + break; + case 1: + id(music_card).execute(20, 125); + break; + case 2: + it.print(100, 150, id(font_arial20), "Last Page"); + break; + } + +script: + - id: all_pages_main + then: + - lambda: |- + //>>> prev/next icons next to butotns + id(main_display).image(0, 208, id(icon_prev_page), ImageAlign::TOP_LEFT, id(red)); + id(main_display).image(320, 208, id(icon_next_page), ImageAlign::TOP_RIGHT, id(red)); + // + //>>> Page name + id(main_display).print(100, 208, id(font_arial20), id(active_page_name).c_str()); + // + //>>> temperature inside: + id(main_display).image(36, 15, id(icon_temp_inside), ImageAlign::TOP_CENTER, id(red)); + id(main_display).printf(36, 80, id(font_arial20), id(white), TextAlign::TOP_CENTER , "%.1f°", id(buttonplus_temperature).state); + // + //>>> ambient light: + id(main_display).image(285, 15, id(icon_ambient_inside), ImageAlign::TOP_CENTER, id(red)); + id(main_display).printf(285, 80, id(font_arial20), id(white), TextAlign::TOP_CENTER , "%.1flux", id(buttonplus_ambientlight).state); + // + //>>> print time and date + id(main_display).strftime(160, 5, id(font_clock), TextAlign::TOP_CENTER, "%H:%M", id(datetime).now()); + id(main_display).strftime(160, 80, id(font_date), TextAlign::TOP_CENTER, "%d-%m-%Y", id(datetime).now()); + - id: !extend bar1_left_draw + then: + - lambda: |- + switch (id(actual_page)){ + case 0: + id(bar_display).image(80, 0, id(lightbulb), ImageAlign::TOP_CENTER, id(red)); + break; + case 1: + id(music_card_bar).execute("volume_down"); + break; + } + - id: !extend bar1_right_draw + then: + - lambda: |- + switch (id(actual_page)){ + case 0: + id(bar_display).image(80, 0, id(lightbulb), ImageAlign::TOP_CENTER, id(red)); + break; + case 1: + id(music_card_bar).execute("volume_up"); + break; + } + - id: !extend bar2_left_draw + then: + - lambda: |- + switch (id(actual_page)){ + case 0: + id(bar_display).image(80, 0, id(lightbulb), ImageAlign::TOP_CENTER, id(red)); + break; + case 1: + id(music_card_bar).execute("prev"); + break; + } + - id: !extend bar2_right_draw + then: + - lambda: |- + switch (id(actual_page)){ + case 0: + id(bar_display).image(80, 0, id(lightbulb), ImageAlign::TOP_CENTER, id(red)); + break; + case 1: + id(music_card_bar).execute("next"); + break; + } + - id: !extend bar3_left_draw + then: + - lambda: |- + switch (id(actual_page)){ + case 0: + id(bar_display).image(80, 0, id(lightbulb), ImageAlign::TOP_CENTER, id(red)); + break; + case 1: + id(music_card_bar).execute("power"); + break; + } + - id: !extend bar3_right_draw + then: + - lambda: |- + switch (id(actual_page)){ + case 0: + id(bar_display).image(80, 0, id(lightbulb), ImageAlign::TOP_CENTER, id(red)); + break; + case 1: + id(music_card_bar).execute("playpause"); + break; + } + +event: + - id: !extend bar1_btn_left_event + on_event: + then: + - lambda: |- + ESP_LOGD("main", "BAR1LEFT event %s triggered.", event_type.c_str()); + - lambda: |- + if (event_type == "click page 1"){ + id(music_card_vol_down).execute(); + ESP_LOGD("main", "VOL DOWN"); + } + - id: !extend bar1_btn_right_event + on_event: + then: + - lambda: |- + ESP_LOGD("main", "BAR1RIGHT event %s triggered.", event_type.c_str()); + - lambda: |- + if (event_type == "click page 1"){ + id(music_card_vol_up).execute(); + ESP_LOGD("main", "VOL UP"); + } + - id: !extend bar2_btn_left_event + on_event: + then: + - lambda: |- + ESP_LOGD("main", "BAR2LEFT event %s triggered.", event_type.c_str()); + - lambda: |- + if (event_type == "click page 1"){ + id(music_card_prev).execute(); + ESP_LOGD("main", "PREV"); + } + - id: !extend bar2_btn_right_event + on_event: + then: + - lambda: |- + ESP_LOGD("main", "BAR2RIGHT event %s triggered.", event_type.c_str()); + - lambda: |- + if (event_type == "click page 1"){ + id(music_card_next).execute(); + ESP_LOGD("main", "NEXT"); + } + - id: !extend bar3_btn_left_event + on_event: + then: + - lambda: |- + ESP_LOGD("main", "BAR3LEFT event %s triggered.", event_type.c_str()); + - lambda: |- + if (event_type == "click page 1"){ + id(music_card_power).execute(); + ESP_LOGD("main", "POWER"); + } + - id: !extend bar3_btn_right_event + on_event: + then: + - lambda: |- + ESP_LOGD("main", "BAR3RIGHT event %s triggered.", event_type.c_str()); + - lambda: |- + if (event_type == "click page 1"){ + id(music_card_playpause).execute(); + ESP_LOGD("main", "PLAY PAUSE"); + } + diff --git a/esphome/3bar_1display/events.yaml b/esphome/3bar_1display/events.yaml new file mode 100644 index 0000000..cede1e6 --- /dev/null +++ b/esphome/3bar_1display/events.yaml @@ -0,0 +1,549 @@ +## Events for 3 pages + +substitutions: + nr_of_pages: '3' + display_events: + - double_click page 0 + - triple_click page 0 + - hold page 0 + - double_click page 1 + - triple_click page 1 + - hold page 1 + - double_click page 2 + - triple_click page 2 + - hold page 2 + button_events: + - click page 0 + - double_click page 0 + - triple_click page 0 + - hold page 0 + - click page 1 + - double_click page 1 + - triple_click page 1 + - hold page 1 + - click page 2 + - double_click page 2 + - triple_click page 2 + - hold page 2 +script: +- id: trigger_with_feedback + parameters: + event_id: esphome::template_::TemplateEvent* + event_msg: std::string + led_id: light::LightState* + r: float + g: float + b: float + then: + - lambda: event_id->trigger(event_msg); + - if: + condition: + lambda: return led_id != nullptr; + then: + - lambda: |- + auto call = led_id->make_call(); + call.set_state(true); + call.set_rgb(r, g, b); + call.set_brightness(1.0); + call.set_flash_length(200); // Returns to previous state automatically + call.perform(); +event: +- platform: template + name: DISPLAY Button Left + id: display_btn_left_event + device_class: button + event_types: ${display_events} +- platform: template + name: DISPLAY Button Right + id: display_btn_right_event + device_class: button + event_types: ${display_events} +- platform: template + name: BAR1 Button Left + id: bar1_btn_left_event + device_class: button + event_types: ${button_events} +- platform: template + name: BAR1 Button Right + id: bar1_btn_right_event + device_class: button + event_types: ${button_events} +- platform: template + name: BAR2 Button Left + id: bar2_btn_left_event + device_class: button + event_types: ${button_events} +- platform: template + name: BAR2 Button Right + id: bar2_btn_right_event + device_class: button + event_types: ${button_events} +- platform: template + name: BAR3 Button Left + id: bar3_btn_left_event + device_class: button + event_types: ${button_events} +- platform: template + name: BAR3 Button Right + id: bar3_btn_right_event + device_class: button + event_types: ${button_events} +binary_sensor: +- id: !extend 'main_display_btn_left' + filters: + - delayed_off: 10ms + internal: true + on_multi_click: + - timing: + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: prev_page + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: display_btn_left_event + event_msg: !lambda "return \"double_click page \" + std::to_string(id(actual_page));" + led_id: !lambda "return nullptr;" + r: 0.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: display_btn_left_event + event_msg: !lambda "return \"triple_click page \" + std::to_string(id(actual_page));" + led_id: !lambda "return nullptr;" + r: 1.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at least 1s + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: display_btn_left_event + event_msg: !lambda "return \"hold page \" + std::to_string(id(actual_page));" + led_id: !lambda "return nullptr;" + r: 1.0 + g: 0.5 + b: 0.0 +- id: !extend 'main_display_btn_right' + filters: + - delayed_off: 10ms + internal: true + on_multi_click: + - timing: + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: next_page + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: display_btn_right_event + event_msg: !lambda "return \"double_click page \" + std::to_string(id(actual_page));" + led_id: !lambda "return nullptr;" + r: 0.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: display_btn_right_event + event_msg: !lambda "return \"triple_click page \" + std::to_string(id(actual_page));" + led_id: !lambda "return nullptr;" + r: 1.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at least 1s + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: display_btn_right_event + event_msg: !lambda "return \"hold page \" + std::to_string(id(actual_page));" + led_id: !lambda "return nullptr;" + r: 1.0 + g: 0.5 + b: 0.0 +- id: !extend 'bar1_btn_left' + filters: + - delayed_off: 10ms + internal: true + on_multi_click: + - timing: + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar1_btn_left_event + event_msg: !lambda "return \"click page \" + std::to_string(id(actual_page));" + led_id: bar1_left_rgb_front + r: 0.0 + g: 1.0 + b: 0.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar1_btn_left_event + event_msg: !lambda "return \"double_click page \" + std::to_string(id(actual_page));" + led_id: bar1_left_rgb_front + r: 0.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar1_btn_left_event + event_msg: !lambda "return \"triple_click page \" + std::to_string(id(actual_page));" + led_id: bar1_left_rgb_front + r: 1.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at least 1s + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar1_btn_left_event + event_msg: !lambda "return \"hold page \" + std::to_string(id(actual_page));" + led_id: bar1_left_rgb_front + r: 1.0 + g: 0.5 + b: 0.0 +- id: !extend 'bar1_btn_right' + filters: + - delayed_off: 10ms + internal: true + on_multi_click: + - timing: + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar1_btn_right_event + event_msg: !lambda "return \"click page \" + std::to_string(id(actual_page));" + led_id: bar1_right_rgb_front + r: 0.0 + g: 1.0 + b: 0.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar1_btn_right_event + event_msg: !lambda "return \"double_click page \" + std::to_string(id(actual_page));" + led_id: bar1_right_rgb_front + r: 0.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar1_btn_right_event + event_msg: !lambda "return \"triple_click page \" + std::to_string(id(actual_page));" + led_id: bar1_right_rgb_front + r: 1.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at least 1s + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar1_btn_right_event + event_msg: !lambda "return \"hold page \" + std::to_string(id(actual_page));" + led_id: bar1_right_rgb_front + r: 1.0 + g: 0.5 + b: 0.0 +- id: !extend 'bar2_btn_left' + filters: + - delayed_off: 10ms + internal: true + on_multi_click: + - timing: + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar2_btn_left_event + event_msg: !lambda "return \"click page \" + std::to_string(id(actual_page));" + led_id: bar2_left_rgb_front + r: 0.0 + g: 1.0 + b: 0.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar2_btn_left_event + event_msg: !lambda "return \"double_click page \" + std::to_string(id(actual_page));" + led_id: bar2_left_rgb_front + r: 0.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar2_btn_left_event + event_msg: !lambda "return \"triple_click page \" + std::to_string(id(actual_page));" + led_id: bar2_left_rgb_front + r: 1.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at least 1s + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar2_btn_left_event + event_msg: !lambda "return \"hold page \" + std::to_string(id(actual_page));" + led_id: bar2_left_rgb_front + r: 1.0 + g: 0.5 + b: 0.0 +- id: !extend 'bar2_btn_right' + filters: + - delayed_off: 10ms + internal: true + on_multi_click: + - timing: + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar2_btn_right_event + event_msg: !lambda "return \"click page \" + std::to_string(id(actual_page));" + led_id: bar2_right_rgb_front + r: 0.0 + g: 1.0 + b: 0.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar2_btn_right_event + event_msg: !lambda "return \"double_click page \" + std::to_string(id(actual_page));" + led_id: bar2_right_rgb_front + r: 0.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar2_btn_right_event + event_msg: !lambda "return \"triple_click page \" + std::to_string(id(actual_page));" + led_id: bar2_right_rgb_front + r: 1.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at least 1s + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar2_btn_right_event + event_msg: !lambda "return \"hold page \" + std::to_string(id(actual_page));" + led_id: bar2_right_rgb_front + r: 1.0 + g: 0.5 + b: 0.0 +- id: !extend 'bar3_btn_left' + filters: + - delayed_off: 10ms + internal: true + on_multi_click: + - timing: + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar3_btn_left_event + event_msg: !lambda "return \"click page \" + std::to_string(id(actual_page));" + led_id: bar3_left_rgb_front + r: 0.0 + g: 1.0 + b: 0.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar3_btn_left_event + event_msg: !lambda "return \"double_click page \" + std::to_string(id(actual_page));" + led_id: bar3_left_rgb_front + r: 0.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar3_btn_left_event + event_msg: !lambda "return \"triple_click page \" + std::to_string(id(actual_page));" + led_id: bar3_left_rgb_front + r: 1.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at least 1s + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar3_btn_left_event + event_msg: !lambda "return \"hold page \" + std::to_string(id(actual_page));" + led_id: bar3_left_rgb_front + r: 1.0 + g: 0.5 + b: 0.0 +- id: !extend 'bar3_btn_right' + filters: + - delayed_off: 10ms + internal: true + on_multi_click: + - timing: + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar3_btn_right_event + event_msg: !lambda "return \"click page \" + std::to_string(id(actual_page));" + led_id: bar3_right_rgb_front + r: 0.0 + g: 1.0 + b: 0.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar3_btn_right_event + event_msg: !lambda "return \"double_click page \" + std::to_string(id(actual_page));" + led_id: bar3_right_rgb_front + r: 0.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at most 0.2s + - ON for at most 300ms + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar3_btn_right_event + event_msg: !lambda "return \"triple_click page \" + std::to_string(id(actual_page));" + led_id: bar3_right_rgb_front + r: 1.0 + g: 0.0 + b: 1.0 + - timing: + - ON for at least 1s + - OFF for at least 0.2s + then: + - script.execute: + id: trigger_with_feedback + event_id: bar3_btn_right_event + event_msg: !lambda "return \"hold page \" + std::to_string(id(actual_page));" + led_id: bar3_right_rgb_front + r: 1.0 + g: 0.5 + b: 0.0 diff --git a/esphome/3bar_1display/scripts.yaml b/esphome/3bar_1display/scripts.yaml new file mode 100644 index 0000000..46c4620 --- /dev/null +++ b/esphome/3bar_1display/scripts.yaml @@ -0,0 +1,161 @@ + +script: +###################### +### update scripts ### +###################### + - id: update_bar1_left + then: + - lambda: |- + id(cs_pin_bar1_left).turn_on(); // << + id(cs_pin_bar1_right).turn_off(); + id(cs_pin_bar2_left).turn_off(); + id(cs_pin_bar2_right).turn_off(); + id(cs_pin_bar3_left).turn_off(); + id(cs_pin_bar3_right).turn_off(); + id(bar_display).clear(); + id(bar_display).set_rotation(display::DisplayRotation::DISPLAY_ROTATION_270_DEGREES); + id(bar1_left_draw).execute(); + id(bar_display).update(); + delay(1); + id(cs_pin_bar1_left).turn_off(); + - id: update_bar1_right + then: + - lambda: |- + id(cs_pin_bar1_left).turn_off(); + id(cs_pin_bar1_right).turn_on(); // << + id(cs_pin_bar2_left).turn_off(); + id(cs_pin_bar2_right).turn_off(); + id(cs_pin_bar3_left).turn_off(); + id(cs_pin_bar3_right).turn_off(); + id(bar_display).clear(); + id(bar_display).set_rotation(display::DisplayRotation::DISPLAY_ROTATION_90_DEGREES); + id(bar1_right_draw).execute(); + id(bar_display).update(); + delay(1); + id(cs_pin_bar1_right).turn_off(); + - id: update_bar2_left + then: + - lambda: |- + id(cs_pin_bar1_left).turn_off(); + id(cs_pin_bar1_right).turn_off(); + id(cs_pin_bar2_left).turn_on(); // << + id(cs_pin_bar2_right).turn_off(); + id(cs_pin_bar3_left).turn_off(); + id(cs_pin_bar3_right).turn_off(); + id(bar_display).clear(); + id(bar_display).set_rotation(display::DisplayRotation::DISPLAY_ROTATION_270_DEGREES); + id(bar2_left_draw).execute(); + id(bar_display).update(); + delay(1); + id(cs_pin_bar2_left).turn_off(); + - id: update_bar2_right + then: + - lambda: |- + id(cs_pin_bar1_left).turn_off(); + id(cs_pin_bar1_right).turn_off(); + id(cs_pin_bar2_left).turn_off(); + id(cs_pin_bar2_right).turn_on(); // << + id(cs_pin_bar3_left).turn_off(); + id(cs_pin_bar3_right).turn_off(); + id(bar_display).clear(); + id(bar_display).set_rotation(display::DisplayRotation::DISPLAY_ROTATION_90_DEGREES); + id(bar2_right_draw).execute(); + id(bar_display).update(); + delay(1); + id(cs_pin_bar2_right).turn_off(); + - id: update_bar3_left + then: + - lambda: |- + id(cs_pin_bar1_left).turn_off(); + id(cs_pin_bar1_right).turn_off(); + id(cs_pin_bar2_left).turn_off(); + id(cs_pin_bar2_right).turn_off(); + id(cs_pin_bar3_left).turn_on(); // << + id(cs_pin_bar3_right).turn_off(); + id(bar_display).clear(); + id(bar_display).set_rotation(display::DisplayRotation::DISPLAY_ROTATION_270_DEGREES); + id(bar3_left_draw).execute(); + id(bar_display).update(); + delay(1); + id(cs_pin_bar3_left).turn_off(); + - id: update_bar3_right + then: + - lambda: |- + id(cs_pin_bar1_left).turn_off(); + id(cs_pin_bar1_right).turn_off(); + id(cs_pin_bar2_left).turn_off(); + id(cs_pin_bar2_right).turn_off(); + id(cs_pin_bar3_left).turn_off(); + id(cs_pin_bar3_right).turn_on(); // << + id(bar_display).clear(); + id(bar_display).set_rotation(display::DisplayRotation::DISPLAY_ROTATION_90_DEGREES); + id(bar3_right_draw).execute(); + id(bar_display).update(); + delay(1); + id(cs_pin_bar3_right).turn_off(); +#################################### +### setup mini display's scripts ### +#################################### + - id: setup_bar_displays + then: + - lambda: |- + id(cs_pin_bar1_left).turn_on(); + id(bar_display).setup(); + delay(5); + id(cs_pin_bar1_left).turn_off(); + id(cs_pin_bar1_right).turn_on(); + id(bar_display).setup(); + delay(5); + id(cs_pin_bar1_right).turn_off(); + id(cs_pin_bar2_left).turn_on(); + id(bar_display).setup(); + delay(5); + id(cs_pin_bar2_left).turn_off(); + id(cs_pin_bar2_right).turn_on(); + id(bar_display).setup(); + delay(5); + id(cs_pin_bar2_right).turn_off(); + id(cs_pin_bar3_left).turn_on(); + id(bar_display).setup(); + delay(5); + id(cs_pin_bar3_left).turn_off(); + id(cs_pin_bar3_right).turn_on(); + id(bar_display).setup(); + delay(5); + id(cs_pin_bar3_right).turn_off(); +############################### +### pre-define draw scripts ### +############################### + - id: bar1_left_draw + then: + - id: bar1_right_draw + then: + - id: bar2_left_draw + then: + - id: bar2_right_draw + then: + - id: bar3_left_draw + then: + - id: bar3_right_draw + then: +###################### +### update scripts ### +###################### + - id: update_main + then: + - component.update: main_display + - id: update_all + then: + - component.update: main_display + - delay: 0.1s + - script.execute: update_bar1_left + - delay: 0.1s + - script.execute: update_bar1_right + - delay: 0.1s + - script.execute: update_bar2_left + - delay: 0.1s + - script.execute: update_bar2_right + - delay: 0.1s + - script.execute: update_bar3_left + - delay: 0.1s + - script.execute: update_bar3_right diff --git a/esphome/README.md b/esphome/README.md new file mode 100644 index 0000000..3db8508 --- /dev/null +++ b/esphome/README.md @@ -0,0 +1,7 @@ +This folder contains the example working configuration on which this generator is based. + +`common.yaml`: for all button+ configurations (max 3 bars, 1 display) +`config.yaml`: the base for ESPHome with board configuration +`base.yaml`: the base for the specific configuration with the GPIO definitions +`events.yaml`: all events for all buttons based on a provided number of pages +`scripts.yaml`: scripts to update the displays for the specific configuration \ No newline at end of file diff --git a/esphome/cards/music.md b/esphome/cards/music.md new file mode 100644 index 0000000..9d37415 --- /dev/null +++ b/esphome/cards/music.md @@ -0,0 +1,35 @@ +# ESPHome Button+ - Music card + +## Usage + +* Add the music card to the main display `lambda` and give it the position you want + ``` + id(music_card).execute(20, 125); + ``` + +* Add the icons buttons to the BAR's + + Select one of the folowing types: + * `volume_up` + * `volume_down` + * `next` + * `prev` + * `playpause` + * `power` + + ``` + id(music_card_bar).execute("volume_up"); + ``` + +* add actions to the buttons + Select the a matching action for the button: + * `music_card_vol_up` + * `music_card_vol_down` + * `music_card_next` + * `music_card_prev` + * `music_card_playpause` + * `music_card_power` + + ``` + id(music_card_vol_up).execute(); + ``` diff --git a/esphome/cards/music.yaml b/esphome/cards/music.yaml new file mode 100644 index 0000000..133a473 --- /dev/null +++ b/esphome/cards/music.yaml @@ -0,0 +1,200 @@ +image: + - file: mdi:music-box-outline + type: GRAYSCALE + id: icon_music_album + resize: 20x20 + - file: mdi:account-music + type: GRAYSCALE + id: icon_music_artist + resize: 20x20 + - file: mdi:music-note + type: GRAYSCALE + id: icon_music_songtitle + resize: 20x20 + - file: mdi:power-off + type: GRAYSCALE + id: icon_music_off + resize: 75x75 + - file: mdi:power-on + type: GRAYSCALE + id: icon_music_on + resize: 75x75 + - file: mdi:power + type: GRAYSCALE + id: icon_music_power + resize: 75x75 + - file: mdi:timer-sand-empty + type: GRAYSCALE + id: icon_music_idle + resize: 75x75 + - file: mdi:play-pause + type: GRAYSCALE + id: icon_music_playpause + resize: 75x75 + - file: mdi:play-box-outline + type: GRAYSCALE + id: icon_music_play_box + resize: 75x75 + - file: mdi:pause-box-outline + type: GRAYSCALE + id: icon_music_pause_box + resize: 75x75 + - file: mdi:power + type: GRAYSCALE + id: icon_music_standby + resize: 75x75 + - file: mdi:download-box-outline + type: GRAYSCALE + id: icon_music_buffering + resize: 75x75 + - file: mdi:volume-plus + type: GRAYSCALE + id: icon_music_vol_up + resize: 75x75 + - file: mdi:volume-minus + type: GRAYSCALE + id: icon_music_vol_down + resize: 75x75 + - file: mdi:skip-forward + type: GRAYSCALE + id: icon_music_next + resize: 75x75 + - file: mdi:skip-backward + type: GRAYSCALE + id: icon_music_prev + resize: 75x75 + - file: mdi:play + type: GRAYSCALE + id: icon_music_play + resize: 75x75 + - file: mdi:pause + type: GRAYSCALE + id: icon_music_pause + resize: 75x75 + +color: + - id: music_card_text_color + hex: ${music_card_text_color} + - id: music_card_icon_main_color + hex: ${music_card_icon_main_color} + - id: music_card_icon_bar_color + hex: ${music_card_icon_bar_color} + +text_sensor: + - platform: homeassistant + id: music_album + entity_id: ${music_card_entity_id} + attribute: media_album_name + on_value: + - delay: 500ms + - script.execute: update_main + - platform: homeassistant + id: music_artist + entity_id: ${music_card_entity_id} + attribute: media_artist + - platform: homeassistant + id: music_title + entity_id: ${music_card_entity_id} + attribute: media_title + on_value: + - delay: 500ms + - script.execute: update_main + - platform: homeassistant + id: music_playerstate + entity_id: ${music_card_entity_id} + on_value: + - script.execute: update_all + +script: + - id: music_card + parameters: + x: int + y: int + then: + - lambda: |- + // player state + if (strcmp(id(music_playerstate).state.c_str(), "off") == 0) { + id(main_display).image(x, 1+y, id(icon_music_off), ImageAlign::TOP_LEFT, id(music_card_icon_main_color)); + } else if (strcmp(id(music_playerstate).state.c_str(), "on") == 0) { + id(main_display).image(x, 1+y, id(icon_music_on), ImageAlign::TOP_LEFT, id(music_card_icon_main_color)); + } else if (strcmp(id(music_playerstate).state.c_str(), "idle") == 0) { + id(main_display).image(x, 1+y, id(icon_music_idle), ImageAlign::TOP_LEFT, id(music_card_icon_main_color)); + } else if (strcmp(id(music_playerstate).state.c_str(), "playing") == 0) { + id(main_display).image(x, 1+y, id(icon_music_play_box), ImageAlign::TOP_LEFT, id(music_card_icon_main_color)); + } else if (strcmp(id(music_playerstate).state.c_str(), "paused") == 0) { + id(main_display).image(x, 1+y, id(icon_music_pause_box), ImageAlign::TOP_LEFT, id(music_card_icon_main_color)); + } else if (strcmp(id(music_playerstate).state.c_str(), "standby") == 0) { + id(main_display).image(x, 1+y, id(icon_music_standby), ImageAlign::TOP_LEFT, id(music_card_icon_main_color)); + } else if (strcmp(id(music_playerstate).state.c_str(), "buffering") == 0) { + id(main_display).image(x, 1+y, id(icon_music_buffering), ImageAlign::TOP_LEFT, id(music_card_icon_main_color)); + } + // + // album + id(main_display).image(75+3+x, 4+y, id(icon_music_album), ImageAlign::TOP_LEFT, id(music_card_icon_main_color)); + id(main_display).print(75+25+x, 0+y, id(font_arial20), id(music_card_text_color), TextAlign::TOP_LEFT , id(music_album).state.c_str()); + // + // artist + id(main_display).image(75+3+x, 30+y, id(icon_music_artist), ImageAlign::TOP_LEFT, id(music_card_icon_main_color)); + id(main_display).print(75+25+x, 26+y, id(font_arial20), id(music_card_text_color), TextAlign::TOP_LEFT , id(music_artist).state.c_str()); + // + // song title + id(main_display).image(75+3+x, 56+y, id(icon_music_songtitle), ImageAlign::TOP_LEFT, id(music_card_icon_main_color)); + id(main_display).print(75+25+x, 52+y, id(font_arial20), id(music_card_text_color), TextAlign::TOP_LEFT , id(music_title).state.c_str()); + - id: music_card_bar + parameters: + type: string + then: + - lambda: |- + if (strcmp(type.c_str(), "volume_up") == 0) { // button "volume_up" + id(bar_display).image(80, 2, id(icon_music_vol_up), ImageAlign::TOP_CENTER, id(music_card_icon_bar_color)); + } else if (strcmp(type.c_str(), "volume_down") == 0) { // button "volume_down" + id(bar_display).image(80, 2, id(icon_music_vol_down), ImageAlign::TOP_CENTER, id(music_card_icon_bar_color)); + } else if (strcmp(type.c_str(), "next") == 0) { // button "next" + id(bar_display).image(80, 2, id(icon_music_next), ImageAlign::TOP_CENTER, id(music_card_icon_bar_color)); + } else if (strcmp(type.c_str(), "prev") == 0) { // button "prev" + id(bar_display).image(80, 2, id(icon_music_prev), ImageAlign::TOP_CENTER, id(music_card_icon_bar_color)); + } else if (strcmp(type.c_str(), "playpause") == 0) { // button "playpause" + if (strcmp(id(music_playerstate).state.c_str(), "playing") == 0) { + id(bar_display).image(80, 2, id(icon_music_pause), ImageAlign::TOP_CENTER, id(music_card_icon_bar_color)); + } else { + id(bar_display).image(80, 2, id(icon_music_play), ImageAlign::TOP_CENTER, id(music_card_icon_bar_color)); + } + } else if (strcmp(type.c_str(), "power") == 0) { // button "power" + id(bar_display).image(80, 2, id(icon_music_power), ImageAlign::TOP_CENTER, id(music_card_icon_bar_color)); + } + - id: music_card_vol_up + then: + - homeassistant.action: + action: media_player.volume_up + data: + entity_id: ${music_card_entity_id} + - id: music_card_vol_down + then: + - homeassistant.action: + action: media_player.volume_down + data: + entity_id: ${music_card_entity_id} + - id: music_card_next + then: + - homeassistant.action: + action: media_player.media_next_track + data: + entity_id: ${music_card_entity_id} + - id: music_card_prev + then: + - homeassistant.action: + action: media_player.media_previous_track + data: + entity_id: ${music_card_entity_id} + - id: music_card_playpause + then: + - homeassistant.action: + action: media_player.media_play_pause + data: + entity_id: ${music_card_entity_id} + - id: music_card_power + then: + - homeassistant.action: + action: media_player.toggle + data: + entity_id: ${music_card_entity_id} diff --git a/esphome/common.yaml b/esphome/common.yaml new file mode 100644 index 0000000..07037ef --- /dev/null +++ b/esphome/common.yaml @@ -0,0 +1,202 @@ +esp32: + flash_size: 16MB + +psram: + mode: octal + speed: 80MHz + +interval: + - interval: 1min + then: + - logger.log: "ESPHome for Button+ V2 v0.1.0" + +i2c: + # Devices at 0x20, 0x21, 0x22, 0x23 + - id: buttons + sda: GPIO1 + scl: GPIO2 + frequency: 50khz + + # Devices at 0x29 (ambient), 0x4A (temp) + - id: sensors + sda: GPIO47 + scl: GPIO48 + frequency: 50khz + +spi: + clk_pin: GPIO6 + mosi_pin: GPIO5 + +substitutions: + nr_of_pages: '"2"' + page_names: '{"Page 1", "Page 2"}' + +globals: + - id: nr_of_pages + type: int + initial_value: ${nr_of_pages} + - id: actual_page + type: int + initial_value: '0' + - id: page_names + type: std::vector + initial_value: ${page_names} + - id: active_page_name + type: std::string + initial_value: "" + +output: + - platform: ledc + pin: GPIO3 + id: display_main_backlight_pwm + inverted: True + max_power: 1.0 + frequency: 500Hz + - platform: ledc + pin: GPIO46 + id: displays_mini_backlight_pwm + inverted: True + max_power: 0.6 + frequency: 500Hz + +sensor: + - platform: sts3x + address: 0x4A + id: buttonplus_temperature + i2c_id: sensors + name: "Temperature" + update_interval: 10s + unit_of_measurement: "°C" + icon: "mdi:thermometer" + device_class: "temperature" + state_class: "measurement" + accuracy_decimals: 1 + - platform: ltr_als_ps + address: 0x29 + i2c_id: sensors + update_interval: 10s + type: ALS + auto_mode: false + gain: 48x + ambient_light: + name: "Ambient Light" + id: buttonplus_ambientlight + unit_of_measurement: "lx" + icon: "mdi:brightness-6" + device_class: "illuminance" + state_class: "measurement" + accuracy_decimals: 1 + infrared_counts: + name: "Infrared Counts" + id: buttonplus_infraredcounts + unit_of_measurement: "#" + icon: "mdi:brightness-5" + accuracy_decimals: 0 + full_spectrum_counts : + name: "Full Spectrum Counts" + id: buttonplus_fullspectrumcounts + unit_of_measurement: "#" + icon: "mdi:brightness-7" + accuracy_decimals: 0 + actual_gain: + name: "Actual Gain" + id: buttonplus_actualgain + icon: "mdi:multiplication" + accuracy_decimals: 0 + - platform: template + name: "Active Page ID" + icon: "mdi:order-numeric-ascending" + id: active_page_id_ha + update_interval: never + accuracy_decimals: 0 + +text_sensor: + - platform: template + name: "Active Page Name" + icon: "mdi:order-alphabetical-ascending" + id: active_page_name_ha + update_interval: never + +light: + - platform: esp32_rmt_led_strip + id: neopixels + rgb_order: RGB + chipset: ws2812 + pin: GPIO4 + num_leds: 17 + - platform: monochromatic + output: display_main_backlight_pwm + gamma_correct : 2.2 + name: "Main Display Backlight" + id: display_main_backlight + restore_mode: ALWAYS_ON + - platform: monochromatic + output: displays_mini_backlight_pwm + gamma_correct : 2.2 + name: "Bars Display Backlight" + id: displays_mini_backlight + restore_mode: ALWAYS_ON + +time: + - platform: homeassistant + id: datetime + +number: + ## Creates a number entity in Home Assistant + # It sets the menu of the Button Plus + - platform: template + name: "Menu Selector" + id: menuselector + optimistic: true + min_value: 0 + max_value: ${nr_of_pages | int - 1} + step: 1 + on_value: + then: + - lambda: |- + if(id(menuselector).state >= id(nr_of_pages)) { + id(actual_page) = id(nr_of_pages)-1; + } else { + id(actual_page) = id(menuselector).state; + } + - logger.log: "Changing page" + - script.execute: set_page + +script: +###################### +### paging scripts ### +###################### + - id: next_page + then: + - lambda: |- + id(actual_page) = id(actual_page) + 1; + if(id(actual_page) >= id(nr_of_pages)) { + id(actual_page) = 0; + } + - script.execute: update_page + - id: prev_page + then: + - lambda: |- + id(actual_page) = id(actual_page) - 1; + if(id(actual_page) < 0) { + id(actual_page) = id(nr_of_pages) - 1; + } + - script.execute: update_page + - id: set_page + then: + - lambda: |- + id(active_page_name) = id(page_names)[id(actual_page)]; + - sensor.template.publish: + id: active_page_id_ha + state: !lambda 'return id(actual_page);' + - text_sensor.template.publish: + id: active_page_name_ha + state: !lambda 'return id(active_page_name);' + - script.execute: update_all + - id: update_page + then: + - number.set: + id: menuselector + value: !lambda 'return id(actual_page);' + - script.execute: set_page + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..840d983 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "buttonplus-esphome-generator" +version = "0.1.0" +description = "Generate ESPHome YAML for Button+ V2 (ESP32-S3)" +requires-python = ">=3.12" \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/buttonplus_generator/__init__.py b/src/buttonplus_generator/__init__.py new file mode 100644 index 0000000..110510e --- /dev/null +++ b/src/buttonplus_generator/__init__.py @@ -0,0 +1,2 @@ +"""Button+ ESPHome generator package.""" +__all__ = ["generator", "snippets"] diff --git a/src/buttonplus_generator/generator.py b/src/buttonplus_generator/generator.py new file mode 100644 index 0000000..252d7e8 --- /dev/null +++ b/src/buttonplus_generator/generator.py @@ -0,0 +1,90 @@ +"""CLI tool to compose ESPHome YAML from snippets. + +Usage: python main.py --pages 3 --notifications --led --output buttonplus.yaml +""" +from __future__ import annotations + +import argparse +from typing import Any, Dict + +import yaml + +from buttonplus_generator import snippets +from pathlib import Path + + +def deep_merge(a: Dict[str, Any], b: Dict[str, Any]) -> Dict[str, Any]: + """Recursively merge b into a and return a new dict. + + Lists are concatenated; scalar values in b override a. + """ + result = dict(a) + for k, v in b.items(): + if k in result: + if isinstance(result[k], dict) and isinstance(v, dict): + result[k] = deep_merge(result[k], v) + elif isinstance(result[k], list) and isinstance(v, list): + result[k] = result[k] + v + else: + result[k] = v + else: + result[k] = v + return result + + +def compose_config(device_name: str, pages: int, notifications: bool, led: bool) -> Dict[str, Any]: + config = snippets.base_config(device_name) + config = deep_merge(config, snippets.pages(pages)) + config = deep_merge(config, snippets.notifications(notifications)) + config = deep_merge(config, snippets.led_control(led)) + return config + + +def compose_from_profile(profile: str, device_name: str | None = None) -> str: + """Load an existing esphome profile folder and return its raw YAML text. + + We intentionally return raw text because ESPHome config files use tags + like `!include` and `!secret` that PyYAML does not understand by + default. If `device_name` is provided we do a simple textual replace of + the `esphome.name` value. + """ + base = Path(__file__).resolve().parents[2] + profile_path = base / "esphome" / profile / "config.yaml" + if not profile_path.exists(): + raise FileNotFoundError(f"Profile config not found: {profile_path}") + text = profile_path.read_text(encoding="utf-8") + if device_name: + # Replace the first occurrence of `esphome:` block name line. + # This is a conservative textual substitution and avoids full YAML parsing. + import re + + def _repl(match: re.Match) -> str: + indent = match.group(1) + return f"{indent}name: {device_name}" + + # Look for a line like: (some spaces)name: that follows an `esphome:` line + text = re.sub(r"(^\s*)name:\s*.*$", _repl, text, count=1, flags=re.M) + return text + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description="Generate ESPHome YAML for Button+") + parser.add_argument("--device-name", default="buttonplus", help="ESPHome device name") + parser.add_argument("--pages", type=int, default=1, help="Number of pages") + parser.add_argument("--notifications", action="store_true", help="Enable notification features") + parser.add_argument("--led", action="store_true", help="Enable LED control") + parser.add_argument("--output", default="buttonplus.yaml", help="Output file path") + parser.add_argument("--profile", help="Use an existing esphome profile folder (e.g. 3bar_1display)") + args = parser.parse_args(argv) + + if args.profile: + text = compose_from_profile(args.profile, device_name=args.device_name) + with open(args.output, "w", encoding="utf-8") as fh: + fh.write(text) + else: + config = compose_config(args.device_name, args.pages, args.notifications, args.led) + with open(args.output, "w", encoding="utf-8") as fh: + yaml.safe_dump(config, fh, sort_keys=False) + + print(f"Wrote {args.output}") + return 0 diff --git a/src/buttonplus_generator/snippets.py b/src/buttonplus_generator/snippets.py new file mode 100644 index 0000000..206686a --- /dev/null +++ b/src/buttonplus_generator/snippets.py @@ -0,0 +1,61 @@ +"""Example snippet definitions for composing ESPHome YAML parts. + +Each function returns a Python dict representing part of the final YAML. +Keep snippets small and composable — the generator deep-merges them. +""" +from typing import Dict, Any +from utils import slugify + +def base_config(device_name: str = "buttonplus") -> Dict[str, Any]: + return { + "esphome": { + "name": slugify(device_name), + "friendly_name": device_name + }, + "esp32":{ + "variant": "ESP32S3", + "board": "esp32-s3-devkitc1-n16r8", + "flash_size": "16MB", + "framework": { + "type": "esp-idf" + } + }, + "wifi": { + "ssid": "!secret wifi_ssid", + "password": "!secret wifi_password", + }, + "logger": { + "level": "DEBUG" + }, + "api": { + "encryption": { + "key": "!secret encryption_key" + } + }, + "ota": [{ + "platform": "esphome", + "password": "!secret ota_password" + }], + } + + + +def notifications(enabled: bool) -> Dict[str, Any]: + if not enabled: + return {} + return { + "text_sensor": [ + { + "platform": "template", + "name": "notification_text", + "id": "notification_text", + } + ], + "binary_sensor": [ + { + "platform": "gpio", + "pin": "GPIO0", + "name": "notification_button", + } + ], + } diff --git a/src/buttonplus_generator/utils.py b/src/buttonplus_generator/utils.py new file mode 100644 index 0000000..289e2a9 --- /dev/null +++ b/src/buttonplus_generator/utils.py @@ -0,0 +1,15 @@ + +import re + + +def slugify(input : str) -> str: + s = input.lower() + s = re.sub(r'\s+', '_', s) + s = re.sub(r'[^a-z0-9_]+', '', s) + s = re.sub(r'_+', '_', s) + s = s.strip('_') + if not s: + s = 'buttonplus' + while len(s) > 1 and s[0].isdigit(): + s = s[1:] + return s \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..5aedd3f --- /dev/null +++ b/src/main.py @@ -0,0 +1,4 @@ +from buttonplus_generator import generator + +if __name__ == "__main__": + generator.main()