Add base and display snippets, initial generation works

This commit is contained in:
2026-05-15 13:04:18 +02:00
parent 43e510b93d
commit dbda5a8daa
10 changed files with 937 additions and 134 deletions
+17 -1
View File
@@ -8,4 +8,20 @@ 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.
The configuration (handmade) on which this generator is based, can be found in the `esphome` folder.
# Todo
## Ideas
- Notification service: until dismissed, or with timeout
- LED effects: flash, breathe
- Top level config only for drawing, everything else hidden
- Allow adding of font and icons easily
## Missing config
- Scripts for updating displays (see scripts.yaml)
- Draw routines (see config.yaml)
- Event generation (triggers and stuff, see events.yaml)
- configurable timing and button events
- Always set display to left and right page
- Event lambda's (see config.yaml)
-2
View File
@@ -1,2 +0,0 @@
"""Button+ ESPHome generator package."""
__all__ = ["generator", "snippets"]
+12
View File
@@ -0,0 +1,12 @@
## GPIO Pins
GPIO_NEOPIXELS = "GPIO4" #Neopixel LED's
GPIO_SPI_MOSI = "GPIO5" #SPI MOSI
GPIO_SPI_CLK = "GPIO6" #SPI SCK
GPIO_SPI_REG = "GPIO7" #SPI Register select
GPIO_BACKLIGHT_MAIN = "GPIO3" #Backlight main display
GPIO_BACKLIGHT_MINI = "GPIO46" #Backlight mini displays
GPIO_I2C_BUTTONS_SDA = "GPIO1" #I2C SDA > Buttons
GPIO_I2C_BUTTONS_SCL = "GPIO2" #I2C SCL > Buttons
GPIO_I2C_SENSORS_SDA = "GPIO47" #I2C SDA > Temperature & Ambient Sensor
GPIO_I2C_SENSORS_SCL = "GPIO48" #I2S SCL > Temperature & Ambient Sensor
+86 -69
View File
@@ -2,89 +2,106 @@
Usage: python main.py --pages 3 --notifications --led --output buttonplus.yaml
"""
from __future__ import annotations
import argparse
from typing import Any, Dict
from typing import Any, Dict, List
import yaml
from buttonplus_generator import snippets
from pathlib import Path
from buttonplus_generator.snippets.common import config_common
from buttonplus_generator.snippets.display import config_display
from buttonplus_generator.merger import deep_merge
from buttonplus_generator.snippets.bars import config_bars
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))
def compose_config(device_name: str, version: int, num_bars: int, pages: List[str]) -> Dict[str, Any]:
config = config_common(device_name=device_name, version=version, page_names=pages, num_bars=num_bars)
if(num_bars < 4): # Always assume display if less than 4 bars
config = deep_merge(config, config_display(version=version, num_bars=num_bars))
config = deep_merge(config, config_bars(version=version, num_bars=num_bars))
return config
def yaml_dumper(config: Dict[str, Any], fh) -> str:
## Handle multiline strings
def str_presenter(dumper, data):
"""configures yaml for dumping multiline strings
Ref: https://stackoverflow.com/questions/8640959/how-can-i-control-what-scalar-form-pyyaml-uses-for-my-data
"""
if len(data.splitlines()) > 1: # check for multiline string
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
return dumper.represent_scalar('tag:yaml.org,2002:str', data)
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: <value> that follows an `esphome:` line
text = re.sub(r"(^\s*)name:\s*.*$", _repl, text, count=1, flags=re.M)
return text
yaml.add_representer(str, str_presenter)
yaml.dump(config, fh, sort_keys=False, encoding="utf-8", default_flow_style=False)
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)")
parser = argparse.ArgumentParser(
description="Generate ESPHome YAML for Button+ V2",
epilog="The version of the display and bars matters because the V1 Display does not have LEDs.\n\nMade by Kenneth van Ewijk (kennyboy55)",
)
parser.add_argument(
"--device-name",
"--name",
"-n",
default="buttonplus",
help="ESPHome device name",
)
parser.add_argument(
"--version",
"-v",
type=int,
choices=[1,2],
required=True,
help="The version of the display and bars. Only V2 PCB is supported.",
metavar="VER"
)
parser.add_argument(
"--bars",
"-b",
type=int,
required=True,
help="Number of bars attached, at least 1, at most 4. A display is assumed for up to three bars attached.",
metavar="NUM"
)
parser.add_argument(
"--pages",
"-p",
nargs="+",
required=True,
help="List of page names",
metavar="NAME"
)
parser.add_argument(
"--output", "-o", default="buttonplus.yaml", help="Output file path"
)
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)
device_name: str = args.device_name
version: int = args.version
num_bars: int = args.bars
pages: List[str] = args.pages
output: str = args.output
print(f"Wrote {args.output}")
config = compose_config(device_name, version, num_bars, pages)
with open(output, "w", encoding="utf-8") as fh:
fh.writelines(
[
"# Generated by ESPHome for Buttonplus\n",
"# Options: \n",
f"# - device: {device_name}\n",
f"# - version: {version}\n",
f"# - bars: {num_bars}\n",
f"# - pages: {", ".join(pages)}\n",
"\n",
]
)
yaml_dumper(config, fh)
print(f"Wrote {output}")
return 0
+20
View File
@@ -0,0 +1,20 @@
from typing import Any, Dict
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
-61
View File
@@ -1,61 +0,0 @@
"""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",
}
],
}
+306
View File
@@ -0,0 +1,306 @@
from typing import Dict, Any, List
from buttonplus_generator.constants import *
from buttonplus_generator.merger import deep_merge
def config_bars(
version: int, num_bars: int
) -> Dict[str, Any]:
config = bars_backlight()
config = deep_merge(config, bars_setup(num_bars=num_bars))
config = deep_merge(config, bars_buttons(num_bars=num_bars))
config = deep_merge(config, bars_lights(num_bars=num_bars, display_has_leds=(version == 2 and num_bars < 4)))
config = deep_merge(config, bars_display_output(num_bars=num_bars))
config = deep_merge(config, bars_display())
return config
def bars_backlight() -> Dict[str, Any]:
return {
"output": [
{
"platform": "ledc",
"pin": GPIO_BACKLIGHT_MINI,
"id": "displays_mini_backlight_pwm",
"inverted": True,
"max_power": 0.6,
"frequency": "500Hz",
}
],
"light": [
{
"platform": "monochromatic",
"output": "displays_mini_backlight_pwm",
"gamma_correct": 2.2,
"name": "Bars Display Backlight",
"id": "displays_mini_backlight",
"restore_mode": "ALWAYS_ON",
}
],
}
def bars_setup(num_bars: int = 3) -> Dict[str, Any]:
bar_dict = {"mcp23008": []}
# esphome.on_boot.then.[]
scripts = []
if num_bars < 1:
raise ValueError("At least one bar must be connected!")
# At least one bar in bottom slot
if num_bars > 0:
bar_dict["mcp23008"].append(
{"id": "base_J3", "i2c_id": "buttons", "address": 0x23}
)
scripts.extend(
[{"script.execute": "update_bar1_left"},
{"script.execute": "update_bar1_right"}]
)
# At least two bars, in bottom two slots
if num_bars > 1:
bar_dict["mcp23008"].append(
{"id": "base_J2", "i2c_id": "buttons", "address": 0x22}
)
scripts.extend(
[{"script.execute": "update_bar2_left"},
{"script.execute": "update_bar2_right"}]
)
# At least three bars, in bottom three slots
if num_bars > 2:
bar_dict["mcp23008"].append(
{"id": "base_J1", "i2c_id": "buttons", "address": 0x21}
)
scripts.extend(
[{"script.execute": "update_bar3_left"},
{"script.execute": "update_bar3_right"}]
)
# No display, only bars, also in top slot
if num_bars > 3:
bar_dict["mcp23008"].append(
{"id": "base_J0", "i2c_id": "buttons", "address": 0x20}
)
scripts.extend(
[{"script.execute": "update_bar4_left"},
{"script.execute": "update_bar4_right"}]
)
bar_dict["esphome"] = {"on_boot": {"then": scripts}}
return bar_dict
def bars_buttons(num_bars: int = 3) -> Dict[str, Any]:
bar_dict = {"binary_sensor": []}
if num_bars < 1:
raise ValueError("At least one bar must be connected!")
if num_bars == 1:
bar_dict["binary_sensor"].extend(_bar_button_entry(1, "base_J3"))
if num_bars == 2:
bar_dict["binary_sensor"].extend(_bar_button_entry(1, "base_J2"))
bar_dict["binary_sensor"].extend(_bar_button_entry(2, "base_J3"))
if num_bars == 3:
bar_dict["binary_sensor"].extend(_bar_button_entry(1, "base_J1"))
bar_dict["binary_sensor"].extend(_bar_button_entry(2, "base_J2"))
bar_dict["binary_sensor"].extend(_bar_button_entry(3, "base_J3"))
if num_bars == 4:
bar_dict["binary_sensor"].extend(_bar_button_entry(1, "base_J0"))
bar_dict["binary_sensor"].extend(_bar_button_entry(2, "base_J1"))
bar_dict["binary_sensor"].extend(_bar_button_entry(3, "base_J2"))
bar_dict["binary_sensor"].extend(_bar_button_entry(4, "base_J3"))
return bar_dict
def _bar_button_entry(bar_nr: int, base_id: str) -> List[Dict[str, Any]]:
return [
{
"platform": "gpio",
"id": f"bar{bar_nr}_btn_left",
"name": f"BAR {bar_nr} button Left",
"pin": {
"mcp23xxx": base_id,
"number": 6,
"mode": "INPUT_PULLUP",
"inverted": True,
},
},
{
"platform": "gpio",
"id": f"bar{bar_nr}_btn_right",
"name": f"BAR {bar_nr} button Right",
"pin": {
"mcp23xxx": base_id,
"number": 2,
"mode": "INPUT_PULLUP",
"inverted": True,
},
},
]
def bars_lights(num_bars: int, display_has_leds: bool = True) -> Dict[str, Any]:
bar_dict = {"light": []}
if num_bars < 1:
raise ValueError("At least one bar must be connected!")
display_offset = 4 if display_has_leds else 0
if num_bars > 0:
bar_dict["light"].extend(_bar_lights_entry(1, 0 + display_offset))
if num_bars > 1:
bar_dict["light"].extend(_bar_lights_entry(2, 4 + display_offset))
if num_bars > 2:
bar_dict["light"].extend(_bar_lights_entry(3, 8 + display_offset))
if num_bars > 3:
bar_dict["light"].extend(_bar_lights_entry(4, 12)) # no display
return bar_dict
def _bar_lights_entry(bar_nr: int, offset: int) -> List[Dict[str, Any]]:
return [
{
"platform": "partition",
"name": f"BAR {bar_nr} Left RGB Front",
"id": f"bar{bar_nr}_left_rgb_front",
"segments": [
{
"id": "neopixels",
"from": offset + 0,
"to": offset + 0
}
]
},
{
"platform": "partition",
"name": f"BAR {bar_nr} Left RGB Back",
"id": f"bar{bar_nr}_left_rgb_back",
"segments": [
{
"id": "neopixels",
"from": offset + 1,
"to": offset + 1
}
]
},
{
"platform": "partition",
"name": f"BAR {bar_nr} Right RGB Front",
"id": f"bar{bar_nr}_right_rgb_front",
"segments": [
{
"id": "neopixels",
"from": offset + 2,
"to": offset + 2
}
]
},
{
"platform": "partition",
"name": f"BAR {bar_nr} Right RGB Back",
"id": f"bar{bar_nr}_right_rgb_back",
"segments": [
{
"id": "neopixels",
"from": offset + 3,
"to": offset + 3
}
]
},
]
def bars_display_output(num_bars: int = 3) -> Dict[str, Any]:
bar_dict = {"output": []}
if num_bars < 1:
raise ValueError("At least one bar must be connected!")
if num_bars == 1:
bar_dict["output"].extend(_bar_display_output_entry(1, "base_J3"))
if num_bars == 2:
bar_dict["output"].extend(_bar_display_output_entry(1, "base_J2"))
bar_dict["output"].extend(_bar_display_output_entry(2, "base_J3"))
if num_bars == 3:
bar_dict["output"].extend(_bar_display_output_entry(1, "base_J1"))
bar_dict["output"].extend(_bar_display_output_entry(2, "base_J2"))
bar_dict["output"].extend(_bar_display_output_entry(3, "base_J3"))
if num_bars == 4:
bar_dict["output"].extend(_bar_display_output_entry(1, "base_J0"))
bar_dict["output"].extend(_bar_display_output_entry(2, "base_J1"))
bar_dict["output"].extend(_bar_display_output_entry(3, "base_J2"))
bar_dict["output"].extend(_bar_display_output_entry(4, "base_J3"))
return bar_dict
def _bar_display_output_entry(bar_nr: int, base_id: str) -> List[Dict[str, Any]]:
return [
{
"platform": "gpio",
"id": f"cs_pin_bar{bar_nr}_left",
"pin": {
"mcp23xxx": base_id,
"number": 5,
"mode": {
"output": True
},
"inverted": True,
},
},
{
"platform": "gpio",
"id": f"cs_pin_bar{bar_nr}_right",
"pin": {
"mcp23xxx": base_id,
"number": 1,
"mode": {
"output": True
},
"inverted": True,
},
},
]
def bars_display() -> Dict[str, Any]:
return {
"display": [
{
"id": "bar_display",
"platform": "ili9xxx",
"model": "ST7735",
"color_order": "bgr",
"update_interval": "never",
"dc_pin": {
"number": GPIO_SPI_REG,
"allow_other_uses": True
},
"invert_colors": False,
"show_test_card": False,
"auto_clear_enabled": False,
"dimensions": {
"height": 160,
"width": 80,
"offset_width": 24
}
}
],
}
+327
View File
@@ -0,0 +1,327 @@
from typing import Dict, Any, List
from buttonplus_generator.merger import deep_merge
from buttonplus_generator.utils import slugify, array_to_cpp_vector
from buttonplus_generator.constants import *
def config_common(
device_name: str, version: int, page_names: List[str], num_bars: int
) -> Dict[str, Any]:
config = base_config(device_name=device_name)
config = deep_merge(config, colors(accent_color="FF0000"))
config = deep_merge(config, font())
config = deep_merge(
config, pages(nr_of_pages=len(page_names), page_names=page_names)
)
config = deep_merge(config, time())
config = deep_merge(config, i2c())
config = deep_merge(config, spi())
config = deep_merge(
config, sensors(ambient_gain="auto", extra_ambient_sensors=False)
)
num_leds = 0
display_add = 0 if version == 1 else 4
if num_bars > 0:
num_leds = 5 + display_add
if num_bars > 1:
num_leds = 9 + display_add
if num_bars > 2:
num_leds = 13 + display_add
if num_bars > 3:
num_leds = 17 #no display
config = deep_merge(config, neopixels(num_leds=num_leds))
return config
def base_config(device_name: str) -> Dict[str, Any]:
return {
"esphome": {
"name": slugify(device_name),
"friendly_name": device_name,
"on_boot": {
"priority": -100,
"then": [{"script.execute": "setup_bar_displays"}],
},
},
"esp32": {
"variant": "ESP32S3",
"board": "esp32-s3-devkitc1-n16r8",
"flash_size": "16MB",
"framework": {"type": "esp-idf"},
},
"psram": {"mode": "octal", "speed": "80MHz"},
"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 colors(accent_color: str) -> Dict[str, Any]:
return {
"color": [
{"id": "accent", "hex": accent_color},
{"id": "white", "hex": "FFFFFF"},
{"id": "grey", "hex": "A6A6A6"},
],
}
def font() -> Dict[str, Any]:
return {
"font": [
{
"file": {
"url": "https://github.com/web-fonts/ttf/raw/refs/heads/master/bpg-ingiri-arial-webfont.ttf",
"type": "web",
},
"id": "font_arial20",
"size": 20,
"bpp": 4,
}
]
}
def pages(nr_of_pages: int, page_names: List[str]) -> Dict[str, Any]:
page_dict = {
"globals": [
{"id": "nr_of_pages", "type": "int", "initial_value": str(nr_of_pages)},
{
"id": "page_names",
"type": "std::vector<std::string>",
"initial_value": array_to_cpp_vector(page_names),
},
{"id": "active_page_nr", "type": "int", "initial_value": "0"},
{"id": "active_page_name", "type": "std::string", "initial_value": ""},
],
"sensors": [
{
"platform": "template",
"name": "Active Page Number",
"icon": "mdi:order-numeric-ascending",
"id": "active_page_nr_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",
}
],
}
if nr_of_pages > 1:
page_dict["number"] = [
{
"platform": "template",
"name": "Page Selector",
"id": "pageselector",
"optimistic": True,
"min_value": 0,
"max_value": nr_of_pages - 1,
"step": 1,
"on_value": {
"then": [
{
"lambda": "if(id(pageselector).state >= id(nr_of_pages)) {\n id(active_page_nr) = id(nr_of_pages)-1;\n} else {\n id(active_page_nr) = id(pageselector).state;\n}"
},
{"script.execute": "set_page"},
]
},
}
]
page_dict["script"] = [
{
"id": "next_page",
"then": [
{
"lambda": "id(active_page_nr) = id(active_page_nr) + 1;\nif(id(active_page_nr) >= id(nr_of_pages)) {\n id(active_page_nr) = 0;\n}"
},
{"script.execute": "update_page"},
],
},
{
"id": "prev_page",
"then": [
{
"lambda": "id(active_page_nr) = id(active_page_nr) - 1;\nif(id(active_page_nr) < 0) {\n id(active_page_nr) = id(nr_of_pages) - 1;\n}"
},
{"script.execute": "update_page"},
],
},
{
"id": "set_page",
"then": [
{
"lambda": "id(active_page_name) = id(page_names)[id(active_page_nr)];"
},
{
"sensor.template.publish": {
"id": "active_page_id_ha",
"state": "return id(active_page_nr);",
}
},
{
"text_sensor.template.publish": {
"id": "active_page_name_ha",
"state": "return id(active_page_name);",
}
},
{"script.execute": "update_all"},
],
},
{
"id": "update_page",
"then": [
{
"number.set": {
"id": "pageselector",
"value": "return id(active_page_nr);",
}
},
{"script.execute": "set_page"},
],
},
]
page_dict["image"] = [
{
"file": "mdi:chevron-right",
"id": "icon_next_page",
"resize": "30x30",
"type": "BINARY",
},
{
"file": "mdi:chevron-left",
"id": "icon_prev_page",
"resize": "30x30",
"type": "BINARY",
},
]
return page_dict
def time() -> Dict[str, Any]:
return {"time": [{"platform": "homeassistant", "id": "datetime"}]}
def i2c() -> Dict[str, Any]:
return {
"i2c": [
{
"id": "buttons",
"sda": GPIO_I2C_BUTTONS_SDA,
"scl": GPIO_I2C_BUTTONS_SCL,
"frequency": "50khz",
},
{
"id": "sensors",
"sda": GPIO_I2C_SENSORS_SDA,
"scl": GPIO_I2C_SENSORS_SCL,
"frequency": "50khz",
},
],
}
def spi() -> Dict[str, Any]:
return {
"spi": {"clk_pin": GPIO_SPI_CLK, "mosi_pin": GPIO_SPI_MOSI},
}
def neopixels(num_leds: int) -> Dict[str, Any]:
return {
"light": [
{
"platform": "esp32_rmt_led_strip",
"id": "neopixels",
"rgb_order": "RGB",
"chipset": "ws2812",
"pin": GPIO_NEOPIXELS,
"num_leds": num_leds,
},
]
}
def sensors(ambient_gain: str, extra_ambient_sensors: bool) -> Dict[str, Any]:
sensors_dict = {
"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": ambient_gain,
"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,
},
},
]
}
if extra_ambient_sensors:
sensors_dict["sensor"][2]["infrared_counts"] = {
"name": "Infrared Counts",
"id": "buttonplus_infraredcounts",
"unit_of_measurement": "#",
"icon": "mdi:brightness-5",
"accuracy_decimals": 0,
}
sensors_dict["sensor"][2]["full_spectrum_counts"] = {
"name": "Full Spectrum Counts",
"id": "buttonplus_fullspectrumcounts",
"unit_of_measurement": "#",
"icon": "mdi:brightness-7",
"accuracy_decimals": 0,
}
sensors_dict["sensor"][2]["actual_gain"] = {
"name": "Actual Gain",
"id": "buttonplus_actualgain",
"icon": "mdi:multiplication",
"accuracy_decimals": 0,
}
sensors_dict["sensor"][2]["actual_integration_time "] = {
"name": "Actual Integration Time",
"id": "buttonplus_actualintegration",
"icon": "mdi:timer-outline",
"accuracy_decimals": 0,
}
return sensors_dict
@@ -0,0 +1,152 @@
from typing import Dict, Any
from buttonplus_generator.constants import *
from buttonplus_generator.utils import get_display_id, get_display_address
from buttonplus_generator.merger import deep_merge
def config_display(
version: int, num_bars: int
) -> Dict[str, Any]:
config = display_backlight(num_bars=num_bars)
config = deep_merge(config, display_setup(num_bars=num_bars))
config = deep_merge(config, display_buttons(num_bars=num_bars))
config = deep_merge(config, display_lights(num_bars=num_bars, display_has_leds=(version == 2)))
return config
def display_backlight(num_bars: int) -> Dict[str, Any]:
return {
"output": [
{
"platform": "ledc",
"pin": GPIO_BACKLIGHT_MAIN,
"id": "display_main_backlight_pwm",
"inverted": True,
"max_power": 1,
"frequency": "500Hz",
}
],
"light": [
{
"platform": "monochromatic",
"output": "display_main_backlight_pwm",
"gamma_correct": 2.2,
"name": "Main Display Backlight",
"id": "display_main_backlight",
"restore_mode": "ALWAYS_ON",
}
],
"display": [
{
"id": "main_display",
"platform": "ili9xxx",
"model": "ili9341",
"invert_colors": False,
"color_order": "rgb",
"update_interval": "30s",
"dc_pin": {
"number": GPIO_SPI_REG,
"allow_other_uses": True
},
"cs_pin": {
"mcp23xxx": get_display_id(num_bars),
"number": 5,
"mode": {
"output": True
},
"inverted": False
},
"show_test_card": False,
"dimensions": {
"height": 240,
"width": 320
},
"rotation": 180
},
],
}
def display_setup(num_bars: int) -> Dict[str, Any]:
if num_bars > 3:
raise ValueError("At most three bars must be connected to use a display!")
return {
"mcp23008": [
{
"id": get_display_id(num_bars),
"i2c_id": "buttons",
"address": get_display_address(num_bars),
}
]
}
def display_buttons(num_bars: int) -> Dict[str, Any]:
if num_bars > 3:
raise ValueError("At most three bars must be connected to use a display!")
return {
"binary_sensor": [
{
"platform": "gpio",
"id": "main_display_btn_left",
"name": "Main Display button Left",
"pin": {
"mcp23xxx": get_display_id(num_bars),
"number": 6,
"mode": "INPUT_PULLUP",
"inverted": True,
},
},
{
"platform": "gpio",
"id": "main_display_btn_right",
"name": "Main Display button Right",
"pin": {
"mcp23xxx": get_display_id(num_bars),
"number": 2,
"mode": "INPUT_PULLUP",
"inverted": True,
},
},
]
}
def display_lights(num_bars: int, display_has_leds: bool = True) -> Dict[str, Any]:
if not display_has_leds:
return {}
if num_bars > 3:
raise ValueError("At most three bars must be connected to use a display!")
return {
"light": [
{
"platform": "partition",
"name": "Main Display Left RGB Front",
"id": "main_display_left_rgb_front",
"segments": [{"id": "neopixels", "from": 0, "to": 0}],
},
{
"platform": "partition",
"name": "Main Display Left RGB Back",
"id": "main_display_left_rgb_back",
"segments": [{"id": "neopixels", "from": 1, "to": 1}],
},
{
"platform": "partition",
"name": "Main Display Right RGB Front",
"id": "main_display_right_rgb_front",
"segments": [{"id": "neopixels", "from": 2, "to": 2}],
},
{
"platform": "partition",
"name": "Main Display Right RGB Back",
"id": "main_display_right_rgb_back",
"segments": [{"id": "neopixels", "from": 3, "to": 3}],
},
]
}
+17 -1
View File
@@ -1,5 +1,6 @@
import re
from typing import List
def slugify(input : str) -> str:
@@ -12,4 +13,19 @@ def slugify(input : str) -> str:
s = 'buttonplus'
while len(s) > 1 and s[0].isdigit():
s = s[1:]
return s
return s
def array_to_cpp_vector(string_array : List[str]) -> str:
return "{" + ', '.join(f'"{entry}"' for entry in string_array) + "}"
def get_display_id(num_bars : int) -> str:
if(num_bars < 1 or num_bars > 3):
raise ValueError("Can't have a display with this number of bars")
return "base_J" + str(3 - num_bars)
def get_display_address(num_bars : int) -> str:
if(num_bars < 1 or num_bars > 3):
raise ValueError("Can't have a display with this number of bars")
return 0x23 - num_bars