Implement config split (with AI help)

This commit is contained in:
2026-06-13 16:52:01 +02:00
parent 61bbb5b434
commit 6ba2e1419e
7 changed files with 2479 additions and 1473 deletions
+36 -21
View File
@@ -1,21 +1,42 @@
# buttonplus-esphome-generator # buttonplus-esphome-generator
A python script that generates a full ESPHome config for the Button+ V2 A python script that generates a full ESPHome config for the Button+ with V2 PCB
## Features ## Features
- Notification Action: `esphome.<name>_notification` - Notification Action: `esphome.<name>_notification`
- Shows a notification, which has to be dismissed - Shows a notification, which has to be dismissed
- Clear notification Action: `esphome.<name>_notification_clear`
- Toast Action: `esphome.<name>_toast` - Toast Action: `esphome.<name>_toast`
- Shows a quick toast message which dissapears after a timeout - Shows a quick toast message which dissapears after a timeout
- Perfect for confirming actions - Perfect for confirming actions
- Get info Action: `esphome.<name>_get_page_info` - Get info Action: `esphome.<name>_get_info`
- Shows data needed for the set_page_led action - Shows data needed for the set_page_led action
- Including the available effects, led names and page names
- Set Led per page Action: `esphome.<name>_set_page_led` - Set Led per page Action: `esphome.<name>_set_page_led`
- Allows you to set the LED for a specific page - Allows you to set the LED for a specific page
- So that you don't need to keep track of which page is active when setting a LED - So that you don't need to keep track of which page is active when setting a LED
- All fields are required, because optionality is not possible at this moment
- Effect can be set to None
- RGB and Brightness should be values between 0 and 255
- Reset all LEDS: `esphome.<name>_reset_all_page_leds`
- Every LED config for every page will be set back to off and clear all data
- Show web image: `esphome.<name>_show_web_jpeg_image`
- Must be a url to a JPG image!
- Will be resized to 320x240 pixels.
- Will disappear after the timeout and be removed from memory
- Will look absolutely horrendous
- Show HA image: `esphome.<name>_show_ha_jpeg_image`
- Only available if a Home Assistant `--token TOKEN` is set during generation
- Allows loading of images which require login from Home Assistant
- Sets the `Authorization: Bearer <token>` header
- All other things from the web image action
- Clear images before their timeout: `esphome.<name>_clear_image`
- Will clear both web or HA image immediatly and release the memory
- Generates pages and all buttons and events belonging to each page - Generates pages and all buttons and events belonging to each page
- Breathe and Blink effect for all LEDs - Breathe and Blink effect for all LEDs
- more to come.. - Confirmation LED pulse on button press
And all of that is split into two files for convenience. Everything you can and should edit or create manually in the top level file, while everything else is in the secondary file.
## Setup ## Setup
### Project uses pipenv (Optional) ### Project uses pipenv (Optional)
@@ -27,29 +48,23 @@ run `pipenv shell` after that to open the virtual environment.
You can then use `python src/main.py --help` to run the generator You can then use `python src/main.py --help` to run the generator
### Example run command ### Example run commands
`python src/main.py -v 1 -b 3 -p Start Music Lights Security -c single double hold` `python src/main.py -v 1 -b 3 -p Start Music Lights Security -c single double hold --token EXAMPLE_TOKEN --no-click-confirm`
`python src/main.py --device-name Generated_ButtonPlus --version 2 --bars 1 --pages Home Living --click-types single hold --no-split --output buttonplus`
## Config example ## 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 was initially based, can be found in the `esphome` folder. There is also an example of the output of this generator.
## Credits
Initially based on the configuration of [https://github.com/dixi83/ESPhome_ButtonPlus](https://github.com/dixi83/ESPhome_ButtonPlus), all initial effort for this was done by [dixi83](https://github.com/dixi83) and [balk77](https://github.com/balk77)
[More information (in Dutch) can be found here on Tweakers.net](https://gathering.tweakers.net/forum/list_messages/2270086)
## Todo ## Todo
### Ideas ### Ideas
- Show image service None
- Top level config only for drawing, everything else hidden
- back to using packages or includes probably
- with --no-split option
- two files
### Missing config ### Missing config
- Event lambda's (see config.yaml) None
- Implement --no-click-confirm
- Make effects optional (--no-effects)
- service: get led state
- led name
- page name
- page num
- service: get_page_info
- rename the service
- add the effect options
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -34,6 +34,7 @@ class IdSuffix(StrEnum):
CS_PIN = "cs_pin" CS_PIN = "cs_pin"
SCRIPT_UPDATE = "script_update" SCRIPT_UPDATE = "script_update"
SCRIPT_DRAW_USER = "script_draw_user"
SCRIPT_DRAW = "script_draw" SCRIPT_DRAW = "script_draw"
class ClickType(StrEnum): class ClickType(StrEnum):
@@ -86,6 +87,9 @@ TIMING_HOLD_CLICK = [
"ON for at least 1s", "ON for at least 1s",
"OFF for at least 0.2s" "OFF for at least 0.2s"
] ]
class YAMLInclude:
def __init__(self, value): self.value = value
class YAMLExtend: class YAMLExtend:
def __init__(self, value): self.value = value def __init__(self, value): self.value = value
@@ -94,4 +98,4 @@ class YAMLLambda:
def __init__(self, value): self.value = value def __init__(self, value): self.value = value
class YAMLSecret: class YAMLSecret:
def __init__(self, value): self.value = value def __init__(self, value): self.value = value
+81 -28
View File
@@ -8,16 +8,17 @@ from __future__ import annotations
import argparse import argparse
from typing import Any, Dict, List from typing import Any, Dict, List
import os
import yaml import yaml
from buttonplus_generator.constants import YAMLExtend, YAMLLambda, YAMLSecret, ClickType from buttonplus_generator.constants import YAMLExtend, YAMLInclude, YAMLLambda, YAMLSecret, ClickType
from buttonplus_generator.snippets.common import config_common from buttonplus_generator.snippets.common import config_common
from buttonplus_generator.snippets.bars import config_bars from buttonplus_generator.snippets.bars import config_bars
from buttonplus_generator.snippets.display import config_display from buttonplus_generator.snippets.display import config_display
from buttonplus_generator.snippets.events import config_events from buttonplus_generator.snippets.events import config_events
from buttonplus_generator.snippets.services import config_services from buttonplus_generator.snippets.services import config_services
from buttonplus_generator.snippets.leds import config_leds from buttonplus_generator.snippets.leds import config_leds
from buttonplus_generator.merger import deep_merge from buttonplus_generator.merger import deep_merge, deep_split
from buttonplus_generator.utils import list_to_click_type from buttonplus_generator.utils import list_to_click_type
def compose_config( def compose_config(
@@ -28,6 +29,7 @@ def compose_config(
click_types: List[ClickType], click_types: List[ClickType],
click_confirm: bool, click_confirm: bool,
services: bool, services: bool,
token: str,
effects: bool effects: bool
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@@ -44,7 +46,7 @@ def compose_config(
# Services, only available with display # Services, only available with display
if services: if services:
config = deep_merge(config, config_services(version=version)) config = deep_merge(config, config_services(version=version, token=token))
# Bars # Bars
config = deep_merge( config = deep_merge(
@@ -70,27 +72,31 @@ def compose_config(
def yaml_dumper(config: Dict[str, Any], fh): def yaml_dumper(config: Dict[str, Any], fh):
## Handle multiline strings ## Handle multiline strings
def represent_multistr(dumper, data): def represent_multistr(dumper, data: str):
"""configures yaml for dumping multiline strings """configures yaml for dumping multiline strings
Ref: https://stackoverflow.com/questions/8640959/how-can-i-control-what-scalar-form-pyyaml-uses-for-my-data 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 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.strip(), style="|")
return dumper.represent_scalar("tag:yaml.org,2002:str", data) return dumper.represent_scalar("tag:yaml.org,2002:str", data)
def represent_extend(dumper, data):
return dumper.represent_scalar("!extend", data.value)
def represent_lambda(dumper, data): def represent_lambda(dumper, data):
if len(data.value.splitlines()) > 1: # check for multiline string if len(data.value.splitlines()) > 1: # check for multiline string
return dumper.represent_scalar("!lambda", data.value, style="|") return dumper.represent_scalar("!lambda", data.value.strip(), style="|")
return dumper.represent_scalar("!lambda", data.value, style='"') return dumper.represent_scalar("!lambda", data.value, style='"')
def represent_include(dumper, data):
return dumper.represent_scalar("!include", data.value)
def represent_extend(dumper, data):
return dumper.represent_scalar("!extend", data.value)
def represent_secret(dumper, data): def represent_secret(dumper, data):
return dumper.represent_scalar("!secret", data.value, style="") return dumper.represent_scalar("!secret", data.value)
yaml.add_representer(str, represent_multistr) yaml.add_representer(str, represent_multistr)
yaml.add_representer(YAMLExtend, represent_extend) yaml.add_representer(YAMLExtend, represent_extend)
yaml.add_representer(YAMLInclude, represent_include)
yaml.add_representer(YAMLLambda, represent_lambda) yaml.add_representer(YAMLLambda, represent_lambda)
yaml.add_representer(YAMLSecret, represent_secret) yaml.add_representer(YAMLSecret, represent_secret)
@@ -152,6 +158,10 @@ def main(argv=None) -> int:
help="List click types buttons, choices are [single|double|triple|hold].", help="List click types buttons, choices are [single|double|triple|hold].",
metavar="TYPE", metavar="TYPE",
) )
parser.add_argument(
"--token",
help="Home Assistant token used for loading home assistant images, creates a second image service",
)
parser.add_argument( parser.add_argument(
"--no-click-confirm", "--no-click-confirm",
default=False, default=False,
@@ -171,7 +181,13 @@ def main(argv=None) -> int:
help="Don't add any effects to the LEDs.", help="Don't add any effects to the LEDs.",
) )
parser.add_argument( parser.add_argument(
"--output", "-o", default="buttonplus.yaml", help="Output file path" "--no-split",
default=False,
action="store_true",
help="Don't split the configuration in two files.",
)
parser.add_argument(
"--output", "-o", default="buttonplus", help="Output file path"
) )
args = parser.parse_args(argv) args = parser.parse_args(argv)
@@ -183,24 +199,61 @@ def main(argv=None) -> int:
click_confirm: bool = not args.no_click_confirm click_confirm: bool = not args.no_click_confirm
effects: bool = not args.no_effects effects: bool = not args.no_effects
services: bool = not args.no_services services: bool = not args.no_services
output: str = args.output split: bool = not args.no_split
token: str = args.token or ""
output: str = args.output.removesuffix(".yaml")
generate(
output, split, device_name, version, num_bars, pages, click_types, click_confirm, services, token, effects
)
return 0
def generate(
output: str,
split: bool,
device_name: str,
version: int,
num_bars: int,
pages: List[str],
click_types: List[ClickType],
click_confirm: bool,
services: bool,
token: str,
effects: bool):
config = compose_config( config = compose_config(
device_name, version, num_bars, pages, click_types, click_confirm, services, effects device_name, version, num_bars, pages, click_types, click_confirm, services, token, effects
) )
with open(output, "w", encoding="utf-8") as fh:
fh.writelines( user_file = output + ".yaml"
[ base_file = output + "_base.yaml"
"# Generated by ESPHome for Buttonplus\n",
"# Options: \n", files = {user_file: config}
f"# - device: {device_name}\n",
f"# - version: {version}\n", if split:
f"# - bars: {num_bars}\n", base, user = deep_split(config, os.path.basename(base_file))
f"# - pages: {", ".join(pages)}\n",
"\n", files = {
] base_file: base,
) user_file: user
yaml_dumper(config, fh) }
for yfile, yconf in files.items():
with open(yfile, "w", encoding="utf-8") as fh:
fh.writelines(
[
"# Generated by ESPHome for Buttonplus\n",
f"# File: {yfile}\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(yconf, fh)
print(f"Wrote {output}") print(f"Wrote {yfile}")
return 0
+265 -9
View File
@@ -1,10 +1,13 @@
from typing import Any, Dict, List import copy
import re
from typing import Any, Dict, List, Optional, Tuple
from buttonplus_generator.utils import display_id, bar_id
from buttonplus_generator.constants import IdSuffix, Side, YAMLExtend, YAMLInclude
def deep_merge(a: Dict[str, Any], b: Dict[str, Any]) -> Dict[str, Any]: def deep_merge(a: Dict[str, Any], b: Dict[str, Any]) -> Dict[str, Any]:
"""Recursively merge b into a and return a new dict. """Recursively merge b into a and return a new dict.
Lists are concatenated; scalar values in b override a. Scalar values in b overwrite a. Lists are merged using the list_merge function.
""" """
result = dict(a) result = dict(a)
for k, v in b.items(): for k, v in b.items():
@@ -15,20 +18,20 @@ def deep_merge(a: Dict[str, Any], b: Dict[str, Any]) -> Dict[str, Any]:
result[k] = deep_merge(result[k], v) result[k] = deep_merge(result[k], v)
# a[k] and b[k] are both a list: list_merge # a[k] and b[k] are both a list: list_merge
elif isinstance(result[k], list) and isinstance(v, list): elif isinstance(result[k], list) and isinstance(v, list):
result[k] = list_merge(result[k], v) result[k] = _list_merge(result[k], v)
else: else:
result[k] = v result[k] = v
else: else:
result[k] = v result[k] = v
return result return result
def list_merge(a: List[Any], b: List[Any]) -> List[Any]: def _list_merge(a: List[Any], b: List[Any]) -> List[Any]:
""" """
Merge two list together based on id's. This is similar to !extend in YAML. Merge two list together based on id's. This is similar to !extend in YAML.
Steps: Steps:
1. Check if both lists contain Dicts with an `id` key in them 1. Check if both lists contain Dicts with an `id` key in them
1a. Otherwise do a pre-append list merge (extend) 1a. Otherwise do a pre-append list merge (extend, see other function)
2. For every entry in a: 2. For every entry in a:
3. Store id in a list for later 3. Store id in a list for later
4. Find the same id in b, if not found add to result without merge 4. Find the same id in b, if not found add to result without merge
@@ -47,7 +50,7 @@ def list_merge(a: List[Any], b: List[Any]) -> List[Any]:
# If there are no id-based dicts in either list, just extend # If there are no id-based dicts in either list, just extend
if not (a_has_id and b_has_id): if not (a_has_id and b_has_id):
return pre_append_list_merge(a, b) return _pre_append_list_merge(a, b)
result: List[Any] = [] result: List[Any] = []
seen_ids = set() seen_ids = set()
@@ -103,7 +106,16 @@ def list_merge(a: List[Any], b: List[Any]) -> List[Any]:
# Return the merged list # Return the merged list
return result return result
def pre_append_list_merge(a: List[Any], b: List[Any]) -> List[Any]: def _pre_append_list_merge(a: List[Any], b: List[Any]) -> List[Any]:
"""
Merge two list together with optional pre-appending. This is step two
of the list merge. Checks if any list is requesting a pre-append (prepend key present),
in which case this function will process them. Otherwise just extend them together.
For every list element, if prepend=true, add it to the front. Otherwise add it to the back.
List order is preserved. Meaning that multiple prepend entries will be added to the front
in the same order that they were in the original list.
"""
# Detect whether both lists contain dicts with an 'prepend' key # Detect whether both lists contain dicts with an 'prepend' key
a_has_prepend = any(isinstance(x, dict) and "prepend" in x for x in a) a_has_prepend = any(isinstance(x, dict) and "prepend" in x for x in a)
@@ -135,4 +147,248 @@ def pre_append_list_merge(a: List[Any], b: List[Any]) -> List[Any]:
for item in b: for item in b:
do_pre_or_append(item) do_pre_or_append(item)
return list(prepend_list) + list(append_list) return list(prepend_list) + list(append_list)
### SPLITTER ###
SPLITOUT_KEYS = [
"esphome.name",
"esphome.friendly_name",
"wifi",
"logger",
"ota",
"api.encryption",
"color.[id=accent]",
f"script.[id={display_id(IdSuffix.SCRIPT_DRAW_USER)}]",
f"script.[id={bar_id(IdSuffix.SCRIPT_DRAW, 1, Side.LEFT)}]",
f"script.[id={bar_id(IdSuffix.SCRIPT_DRAW, 1, Side.RIGHT)}]",
f"script.[id={bar_id(IdSuffix.SCRIPT_DRAW, 2, Side.LEFT)}]",
f"script.[id={bar_id(IdSuffix.SCRIPT_DRAW, 2, Side.RIGHT)}]",
f"script.[id={bar_id(IdSuffix.SCRIPT_DRAW, 3, Side.LEFT)}]",
f"script.[id={bar_id(IdSuffix.SCRIPT_DRAW, 3, Side.RIGHT)}]",
f"event.[id={display_id(IdSuffix.EVENT_BUTTON, Side.LEFT)}].on_event",
f"event.[id={display_id(IdSuffix.EVENT_BUTTON, Side.RIGHT)}].on_event",
f"event.[id={bar_id(IdSuffix.EVENT_BUTTON, 1, Side.LEFT)}].on_event",
f"event.[id={bar_id(IdSuffix.EVENT_BUTTON, 1, Side.RIGHT)}].on_event",
f"event.[id={bar_id(IdSuffix.EVENT_BUTTON, 2, Side.LEFT)}].on_event",
f"event.[id={bar_id(IdSuffix.EVENT_BUTTON, 2, Side.RIGHT)}].on_event",
f"event.[id={bar_id(IdSuffix.EVENT_BUTTON, 3, Side.LEFT)}].on_event",
f"event.[id={bar_id(IdSuffix.EVENT_BUTTON, 3, Side.RIGHT)}].on_event",
]
def _parse_split_path(path: str) -> List[Any]:
"""Parse a split path string into tokens."""
tokens = []
for raw in re.findall(r"[^.\[\]]+|\[[^\]]+\]", path):
if raw.startswith('[') and raw.endswith(']'):
selector = raw[1:-1]
if '=' not in selector:
raise ValueError(f"Unsupported selector format: {path}")
key, value = selector.split('=', 1)
tokens.append(("filter", key, value.strip("\"'")))
else:
tokens.append(raw)
return tokens
def _find_target(container: Any, tokens: List[Any]) -> Optional[Tuple[Any, Any, Any, Any]]:
"""Find the target value described by tokens and return its parent context."""
current = container
for index, token in enumerate(tokens):
if isinstance(token, str):
if not isinstance(current, dict) or token not in current:
return None
if index == len(tokens) - 1:
return current, token, current[token], None
current = current[token]
else:
kind, key, value = token
if kind != "filter" or not isinstance(current, list):
return None
for item_index, item in enumerate(current):
if isinstance(item, dict) and item.get(key) == value:
if index == len(tokens) - 1:
return current, item_index, item, item
next_result = _find_target(item, tokens[index + 1:])
if next_result is not None:
return next_result[0], next_result[1], next_result[2], item
return None
return None
def _make_split_fragment(item: Dict[str, Any], tokens: List[Any]) -> Dict[str, Any]:
"""Create the fragment for a selector-based split path."""
selector_index = next((i for i, token in enumerate(tokens) if isinstance(token, tuple)), None)
if selector_index is None:
return copy.deepcopy(item)
if selector_index == len(tokens) - 1:
return copy.deepcopy(item)
fragment: Dict[str, Any] = {}
if isinstance(item, dict) and "id" in item:
fragment["id"] = YAMLExtend(item['id'])
current = fragment
for token in tokens[selector_index + 1:-1]:
if isinstance(token, str):
current[token] = {}
current = current[token]
last_token = tokens[-1]
if isinstance(last_token, str) and isinstance(item, dict) and last_token in item:
current[last_token] = copy.deepcopy(item[last_token])
return fragment
def deep_split(origin: Dict[str, Any], origin_filename: str, splitout: List[str] = SPLITOUT_KEYS) -> Tuple[Dict[str, Any], Dict[str, Any]]:
"""
Split a complete YAML dict into two files based on a list of keys that are requested to be split out.
Input:
origin: a yaml structure, consisting of nested dictionaries, lists, lists of dictionaries and scalar values
splitout: list of json-path like strings that declare which keys should be split out into a separate structure
Splitout-path specification:
<key-a>.<key-b> : key-a is a dictionary key, which contains a dictionary of which key-b is a key.
key-b and its value (which can be another nested structure), should me moved to <key-a>.<key-b> inside the output structure
assume <key-a>.<key-c> also exists, this should remain in the origin structure
<key-a>.[<key-b>=<value-b>]: key-a is a dictionary key, which contains a list of dictionaries,
of which we want the entry which has key-b with the value of value-b
there might also be other dictionaries in the list of key-a, but these must stay in the origin structure
the extracted dictionary should be placed in a list at the same path in the output structure, <key-a>.[]
<key-a>.[<key-b>=<value-b>].<key-c>: key-a is a dictionary key, which contains a list of dictionaries,
of which we want the entry which has key-b with the value of value-b
we only want to move the key-c entry of this dictionary in the list
the extracted element should be placed in a new list at the same path in the output structure, <key-a>.[<key-b>=<value-b>].<key-c>
Example:
paths =
list.[id=entry1]
list.[id=entry2].c
dict.e.g
move
origin = {
list: [
{
id: entry1
key1: moves_with_list_entry
key2: moves_with_list_entry
},
{
id: entry2
c: value_to_move
d: stays_here_value
}
],
dict: {
e: {
g: value_to_move
},
f: stays_here_value
},
move: value_to_move
}
expected_split = {
list: [
{
id: entry1
key1: moves_with_list_entry
key2: moves_with_list_entry
},
{
id: !extend entry2
c: value_to_move
}
],
dict: {
e: {
g: value_to_move
}
},
move: value_to_move
}
expected_origin = {
list: [
{
id: entry2
d: stays_here_value
}
],
dict: {
f: stays_here_value
}
}
Steps:
1. Parse the splitout keys into tokens
2. For every splitout key, find the entry in the origin dictionary.
2a. It is important to retain the entire parent path, as we want to reconstruct it in the new output
3. Create a new output dictionary where the entries are moved to
3a. Only move the leaf node value and copy it's parent structure
3b. Do not move any adjecant nodes, if there are no adjecant nodes it should be cleaned from the origin
3c. The moved leaf should not remain present in the origin, it has been moved out to the output
4. Ensure that the origin dictionary is still valid, after moving the node to the output structure
4a. If the leaf is a list element (<key-a>.[<key-b>=<value-b>]), remove it from the list. If this list is now empty, remove the list key
4b. If the leaf has a list as parent node at some point (<key-a>.[<key-b>=<value-b>].<key-c>), move this leaf only. Keep the rest of the node and structure intact.
4bb. Origin keeps the `id: <value>`, output gets `id: !extend <value>`.
4c. If the leaf is a dictionary or scalar (<key-a>.<key-b>), move it over entirely and remove it from origin
5. Return a tuple of the modified origin and the new output structure.
NOTE: This function and its subfunctions were generated by AI
"""
output: Dict[str, Any] = {"packages": [YAMLInclude(origin_filename)]}
for path in splitout:
tokens = _parse_split_path(path)
found = _find_target(origin, tokens)
if found is None:
continue
parent, key, value, selected_item = found
selector_index = next((i for i, token in enumerate(tokens) if isinstance(token, tuple)), None)
if isinstance(parent, dict):
if isinstance(key, int):
continue
if selector_index is not None and selector_index < len(tokens) - 1:
fragment = _make_split_fragment(copy.deepcopy(selected_item), tokens)
output.setdefault(tokens[0], []).append(fragment)
else:
current = output
for token in tokens[:-1]:
if isinstance(token, str):
current = current.setdefault(token, {})
current[tokens[-1]] = copy.deepcopy(value)
del parent[key]
continue
if isinstance(parent, list) and isinstance(key, int):
item = parent[key]
if selector_index is None or selector_index == len(tokens) - 1:
output.setdefault(tokens[0], []).append(copy.deepcopy(item))
parent.pop(key)
continue
fragment = _make_split_fragment(copy.deepcopy(item), tokens)
output.setdefault(tokens[0], []).append(fragment)
current = item
for token in tokens[selector_index + 1:-1]:
if isinstance(token, str) and isinstance(current, dict) and token in current:
current = current[token]
if isinstance(current, dict) and isinstance(tokens[-1], str):
current.pop(tokens[-1], None)
return origin, output
+21 -7
View File
@@ -169,31 +169,45 @@ def update_scripts(page_names: List[str]) -> Dict[str, Any]:
) )
generated_cpp = ( generated_cpp = (
"// All pages\n"
f"id({display_id(IdSuffix.DISPLAY)}).image(0, 235, id(icon_prev_page), ImageAlign::BOTTOM_LEFT, id(grey));\n" f"id({display_id(IdSuffix.DISPLAY)}).image(0, 235, id(icon_prev_page), ImageAlign::BOTTOM_LEFT, id(grey));\n"
f"id({display_id(IdSuffix.DISPLAY)}).image(320, 235, id(icon_next_page), ImageAlign::BOTTOM_RIGHT, id(grey));\n" f"id({display_id(IdSuffix.DISPLAY)}).image(320, 235, id(icon_next_page), ImageAlign::BOTTOM_RIGHT, id(grey));\n"
f"id({display_id(IdSuffix.DISPLAY)}).print(160, 235, id(font_arial20), id(white), TextAlign::BOTTOM_CENTER, id(active_page_name).c_str());\n" f"id({display_id(IdSuffix.DISPLAY)}).print(160, 235, id(font_arial20), id(white), TextAlign::BOTTOM_CENTER, id(active_page_name).c_str());\n"
"\n" )
f"id({display_id(IdSuffix.DISPLAY)}).strftime(160, 5, id(font_arial20), TextAlign::TOP_CENTER, \"%H:%M\", id(datetime).now());\n"
f"id({display_id(IdSuffix.DISPLAY)}).strftime(160, 80, id(font_arial20), TextAlign::TOP_CENTER, \"%d-%m-%Y\", id(datetime).now());\n" generated_cpp_user = (
"// All pages\n"
f"id({display_id(IdSuffix.DISPLAY)}).strftime(160, 5, id(font_arial20), TextAlign::TOP_CENTER, \"%H:%M\", id(datetime).now());\n"
f"id({display_id(IdSuffix.DISPLAY)}).strftime(160, 80, id(font_arial20), TextAlign::TOP_CENTER, \"%d-%m-%Y\", id(datetime).now());\n"
"\n" "\n"
"switch (id(active_page_nr)){\n" "switch (id(active_page_nr)){\n"
) )
for page_num in range(0, len(page_names)): for page_num in range(0, len(page_names)):
generated_cpp += ( generated_cpp_user += (
f" // Page {page_num} - {page_names[page_num]}\n" f" // Page {page_num} - {page_names[page_num]}\n"
f" case {page_num}:\n" f" case {page_num}:\n"
f" break;\n" f" break;\n"
) )
generated_cpp += "}\n" generated_cpp_user += "}\n"
display_dict["script"].append( display_dict["script"].append(
{ {
"id": display_id(IdSuffix.SCRIPT_DRAW), "id": display_id(IdSuffix.SCRIPT_DRAW),
"then": [ "then": [
{"lambda": generated_cpp} {"lambda": generated_cpp},
{
"script.execute": display_id(IdSuffix.SCRIPT_DRAW_USER)
}
],
}
)
display_dict["script"].append(
{
"id": display_id(IdSuffix.SCRIPT_DRAW_USER),
"then": [
{"lambda": generated_cpp_user},
], ],
} }
) )