# #!/usr/bin/env python3 # """ # generate.py — Chore Tracker Code Generator # ========================================== # Reads chores_config.yaml and generates: # • chore-tracker-esphome.yaml (flash to ESP32 via ESPHome) # • chore-tracker-ha.yaml (Home Assistant config) # • chore-tracker-dashboard.yaml (Lovelace dashboard) # Target hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480) # Home screen behaviour: # - Each kid's button has a RED outline while any chores remain undone # - Button turns SOLID GREEN when ALL chores are complete # - Resets to red automatically at midnight # Each kid must define their own chores list in chores_config.yaml. # Usage: # python3 generate.py # python3 generate.py --config my_other_config.yaml # python3 generate.py --out ./output_dir # """ # import argparse # import sys # from pathlib import Path # try: # import yaml # except ImportError: # print("PyYAML not found. Install it with: pip install pyyaml") # sys.exit(1) # # ───────────────────────────────────────────────────────────────────────────── # # Helpers # # ───────────────────────────────────────────────────────────────────────────── # # Screen padding — space kept clear around all edges (pixels) # PAD = 12 # def slug(name: str) -> str: # return name.lower().replace(" ", "_").replace("-", "_") # def kid_slug(kid: dict) -> str: # return slug(kid["name"]) # def eid(kid: dict, chore: dict) -> str: # return f"{kid_slug(kid)}_{slug(chore['name'])}" # def ha_switch(kid: dict, chore: dict) -> str: # return f"switch.chore_tracker_{eid(kid, chore)}" # # mdi:checkbox-marked-circle-outline — safe default if icon is omitted # DEFAULT_ICON = "\U000F0133" # def get_chores(kid: dict) -> list: # chores = kid["chores"] # for c in chores: # if "icon" not in c: # c["icon"] = DEFAULT_ICON # return chores # # ───────────────────────────────────────────────────────────────────────────── # # ESPHome generator # # ───────────────────────────────────────────────────────────────────────────── # def gen_esphome(cfg: dict) -> str: # s = cfg["settings"] # kids = cfg["kids"] # # ── Switches (one per chore per kid) ────────────────────────────────────── # switch_blocks = [] # for kid in kids: # chores = get_chores(kid) # ks = kid_slug(kid) # switch_blocks.append(f"\n # ── {kid['name']}'s chores ──────────────────────────\n") # for chore in chores: # e = eid(kid, chore) # switch_blocks.append(f"""\ # - platform: template # name: "{kid['name']} - {chore['name']}" # id: {e} # icon: mdi:checkbox-marked-circle # optimistic: true # restore_mode: RESTORE_DEFAULT_OFF # on_turn_on: # then: # - script.execute: update_{ks}_ui # - homeassistant.service: # service: switch.turn_on # data: # entity_id: switch.chore_tracker_{e} # on_turn_off: # then: # - script.execute: update_{ks}_ui # - homeassistant.service: # service: switch.turn_off # data: # entity_id: switch.chore_tracker_{e} # """) # # ── Sensors (chores-done count only — no stars) ──────────────────────── # sensor_blocks = [] # for kid in kids: # chores = get_chores(kid) # ks = kid_slug(kid) # done_lines = "\n".join( # f" if (id({eid(kid, c)}).state) done++;" for c in chores # ) # sensor_blocks.append(f"""\ # - platform: template # name: "{kid['name']} Chores Done" # id: {ks}_chores_done # accuracy_decimals: 0 # update_interval: 2s # lambda: |- # int done = 0; # {done_lines} # return done; # # Diagnostic sensors # - platform: wifi_signal # name: "${device_name} WiFi Signal" # update_interval: 60s # - platform: uptime # name: "${device_name} Uptime" # update_interval: 60s # """) # # ── Midnight reset calls ────────────────────────────────────────────────── # reset_calls = "\n".join( # f" id(reset_{kid_slug(k)}_chores).execute();" # for k in kids # ) # lvgl_pages = _gen_lvgl_pages(kids) # scripts = _gen_scripts(kids) # # Collect all unique MDI glyphs used (avatars + chore icons) # all_glyphs = sorted(set( # [k["avatar"] for k in kids] + # [c["icon"] for k in kids for c in get_chores(k)] # )) # glyphs_str = "".join(all_glyphs) # return f"""\ # ################################################################################ # # chore-tracker-esphome.yaml (AUTO-GENERATED — edit chores_config.yaml) # # Kids: {", ".join(k["name"] for k in kids)} # # # # HOME SCREEN BEHAVIOUR: # # Red outline = chores incomplete # # Solid green = all chores done # # Resets to red automatically at midnight # # # # Hardware: Waveshare ESP32-S3-Touch-LCD-7 (800x480) # ################################################################################ # substitutions: # device_name: {s["device_name"]} # friendly_name: "{s["friendly_name"]}" # esphome: # name: {s["device_name"]} # friendly_name: "{s["friendly_name"]}" # on_boot: # priority: -10 # then: # - light.turn_on: backlight # - lvgl.page.show: page_home # esp32: # board: esp32-s3-devkitc-1 # framework: # type: esp-idf # psram: # mode: octal # speed: 80MHz # wifi: # ssid: {s["wifi_ssid"]} # password: {s["wifi_password"]} # manual_ip: # static_ip: {s["static_ip"]} # gateway: {s["gateway"]} # subnet: {s["subnet"]} # ap: # ssid: "{s['friendly_name']} Hotspot" # password: "choretracker" # # Prevents aggressive reconnection attempts # power_save_mode: none # Use 'light' for battery devices # # Slower but more reliable connection # fast_connect: true # # Handle connection failures gracefully # on_connect: # - logger.log: "Wi-Fi connected!" # on_disconnect: # - logger.log: "Wi-Fi disconnected!" # mdns: # disabled: true # safe_mode: # disabled: true # logger: # level: INFO # api: # encryption: # key: {s["api_key"]} # reboot_timeout: 0s # ota: # - platform: esphome # password: {s["ota_password"]} # web_server: # port: 8080 # # ── I2C ─────────────────────────────────────────────────────────────────────── # i2c: # sda: 8 # scl: 9 # # ── CH422G IO Expander (controls LCD reset, touch reset, backlight) ─────────── # ch422g: # - id: ch422g_hub # # ── Display ─────────────────────────────────────────────────────────────────── # display: # - platform: mipi_rgb # model: ESP32-S3-TOUCH-LCD-7-800X480 # id: main_display # update_interval: never # auto_clear_enabled: false # reset_pin: # ch422g: ch422g_hub # number: 3 # mode: # output: true # # ── Touchscreen ─────────────────────────────────────────────────────────────── # touchscreen: # platform: gt911 # id: touch # update_interval: 120ms # reset_pin: # ch422g: ch422g_hub # number: 1 # mode: # output: true # # ── Backlight ───────────────────────────────────────────────────────────────── # output: # - platform: template # id: lcd_backlight_out # type: binary # write_action: # - if: # condition: # lambda: return state; # then: # - switch.turn_on: lcd_backlight_raw # else: # - switch.turn_off: lcd_backlight_raw # light: # - platform: binary # name: "Display Backlight" # output: lcd_backlight_out # id: backlight # # ── Midnight reset ──────────────────────────────────────────────────────────── # time: # - platform: homeassistant # id: ha_time # on_time: # - hours: 0 # minutes: 0 # seconds: 0 # then: # - lambda: |- # {reset_calls} # - lvgl.page.show: page_home # # ── Fonts (place files in /config/esphome/fonts/) ──────────────────────────── # font: # - file: "fonts/Nunito-Black.ttf" # id: font_title # size: 40 # - file: "fonts/Nunito-ExtraBold.ttf" # id: font_name # size: 30 # - file: "fonts/Nunito-Bold.ttf" # id: font_med # size: 22 # - file: "fonts/Nunito-SemiBold.ttf" # id: font_small # size: 17 # - file: "fonts/Nunito-SemiBold.ttf" # id: font_tiny # size: 13 # # MDI icon font — used for chore icons and kid avatars # - file: "fonts/materialdesignicons-webfont.ttf" # id: font_mdi_large # size: 48 # bpp: 4 # glyphs: {glyphs_str} # - file: "fonts/materialdesignicons-webfont.ttf" # id: font_mdi_small # size: 32 # bpp: 4 # glyphs: {glyphs_str} # # ── Switches — backlight raw + one per chore per kid ───────────────────────── # switch: # - platform: gpio # id: lcd_backlight_raw # name: "LCD Backlight Raw" # restore_mode: ALWAYS_ON # pin: # ch422g: ch422g_hub # number: 2 # mode: # output: true # {"".join(switch_blocks)} # # ── Sensors — reported to HA ───────────────────────────────────────────────── # sensor: # {"".join(sensor_blocks)} # # ── Text Sensors — reported to HA ───────────────────────────────────────────────── # text_sensor: # - platform: wifi_info # ip_address: # name: "${device_name} IP Address" # ssid: # name: "${device_name} Connected SSID" # bssid: # name: "${device_name} BSSID" # # Watchdog to auto-reboot if things go wrong # # Note: interval fires on first tick (t≈0), so guard with millis() to ensure # # the device has actually been running for 5+ minutes before triggering. # interval: # - interval: 5min # then: # - if: # condition: # and: # - lambda: 'return millis() > 300000;' # >5 min uptime # - not: # wifi.connected: # then: # - logger.log: "Wi-Fi not connected for 5 min, rebooting..." # - delay: 10s # - lambda: 'App.safe_reboot();' # # ── Scripts — reset + UI update ─────────────────────────────────────────────── # script: # {scripts} # # ── LVGL UI ─────────────────────────────────────────────────────────────────── # lvgl: # displays: # - main_display # touchscreens: # - touch # theme: # button: # radius: 20 # border_width: 0 # pages: # {lvgl_pages} # """ # # ───────────────────────────────────────────────────────────────────────────── # # LVGL page builder # # ───────────────────────────────────────────────────────────────────────────── # def _gen_lvgl_pages(kids: list) -> str: # pages = [] # # ── HOME PAGE ───────────────────────────────────────────────────────────── # n = len(kids) # usable_w = 800 - PAD * 2 # btn_w = min(200, max(140, (usable_w - 40 - 20 * (n - 1)) // n)) # total_w = btn_w * n + 20 * (n - 1) # start_x = PAD + (usable_w - total_w) // 2 # btn_y = PAD + 118 # btn_h = 210 # reset_all_calls = "\n".join( # f" - script.execute: reset_{kid_slug(k)}_chores" # for k in kids # ) # home_btns = "" # for i, kid in enumerate(kids): # x = start_x + i * (btn_w + 20) # ks = kid_slug(kid) # home_btns += f"""\ # - button: # id: home_btn_{ks} # x: {x} # y: {btn_y} # width: {btn_w} # height: {btn_h} # bg_color: 0xFF4757 # bg_opa: COVER # border_width: 0 # radius: 24 # on_click: # then: # - lvgl.page.show: page_{ks} # widgets: # - label: # align: CENTER # y: -45 # text: "{kid['avatar']}" # text_font: font_mdi_large # - label: # align: CENTER # y: 22 # text: "{kid['name']}" # text_font: font_name # text_color: 0xFFFFFF # - label: # id: home_status_{ks} # align: CENTER # y: 66 # text: "not done" # text_font: font_tiny # text_color: 0xFFFFFF # """ # pages.append(f"""\ # - id: page_home # bg_color: 0xFFF9F0 # widgets: # - label: # x: 0 # y: 36 # width: {800 - PAD * 2} # align: TOP_MID # text: "Chore Tracker" # text_font: font_title # text_color: 0x2D3436 # - label: # x: 0 # y: 90 # width: {800 - PAD * 2} # align: TOP_MID # text: "Tap a name to check off chores" # text_font: font_small # text_color: 0xB2BEC3 # - button: # x: 640 # y: 428 # width: 148 # height: 40 # bg_color: 0xFF4757 # radius: 12 # on_click: # then: # {reset_all_calls} # widgets: # - label: # align: CENTER # text: "Reset All" # text_color: 0xFFFFFF # text_font: font_tiny # {home_btns}""") # # ── PER-KID CHORE PAGES ─────────────────────────────────────────────────── # for kid in kids: # chores = get_chores(kid) # ks = kid_slug(kid) # n_chores = len(chores) # cols = 3 # rows = (n_chores + cols - 1) // cols # sidebar_w = 172 # gap = 10 # content_w = 800 - sidebar_w - gap - 12 # right pad # card_w = (content_w - gap * (cols - 1)) // cols # card_h = min(138, (480 - PAD - gap * (rows + 1)) // rows) # card_widgets = "" # for idx, chore in enumerate(chores): # row = idx // cols # col = idx % cols # x = sidebar_w + gap + col * (card_w + gap) # y = 12 + row * (card_h + gap) # e = eid(kid, chore) # icon_y = -(card_h // 4) # label_y = card_h // 8 # card_widgets += f"""\ # - button: # id: card_{e} # x: {x} # y: {y} # width: {card_w} # height: {card_h} # bg_color: 0xFFFFFF # border_color: 0xEEEEEE # border_width: 2 # radius: 20 # shadow_color: 0xCCCCCC # shadow_width: 4 # shadow_ofs_y: 3 # on_click: # then: # - switch.toggle: {e} # widgets: # - label: # id: check_{e} # align: TOP_RIGHT # x: -10 # y: 8 # text: "" # text_font: font_med # - label: # align: CENTER # y: {icon_y} # text: "{chore['icon']}" # text_font: font_mdi_small # text_color: 0x{kid['color']} # - label: # align: CENTER # y: {label_y} # text: "{chore['name']}" # text_font: font_small # text_color: 0x2D3436 # """ # pages.append(f"""\ # - id: page_{ks} # bg_color: 0xFFF9F0 # widgets: # # ── Sidebar ────────────────────────────────────────────────────────── # - obj: # x: 0 # y: 0 # width: {sidebar_w} # height: 480 # bg_color: 0x{kid['color']} # border_width: 0 # radius: 0 # pad_all: 0 # widgets: # - label: # x: 0 # y: 22 # width: {sidebar_w} # align: TOP_MID # text: "{kid['avatar']}" # text_font: font_mdi_large # - label: # x: 0 # y: 76 # width: {sidebar_w} # align: TOP_MID # text: "{kid['name']}" # text_font: font_name # text_color: 0xFFFFFF # - bar: # id: progress_bar_{ks} # x: 14 # y: 132 # width: {sidebar_w - 28} # height: 16 # bg_color: 0x{kid['color_dark']} # indicator: # bg_color: 0xFFFFFF # value: 0 # min_value: 0 # max_value: {n_chores} # - label: # id: progress_label_{ks} # x: 0 # y: 156 # width: {sidebar_w} # align: TOP_MID # text: "0 / {n_chores}" # text_font: font_small # text_color: 0xDDEEFF # - label: # id: all_done_label_{ks} # x: 0 # y: 192 # width: {sidebar_w} # align: TOP_MID # text: "" # text_font: font_small # text_color: 0xFFFFFF # - button: # x: 14 # y: {480 - PAD - 44 - 6 - 44} # width: {sidebar_w - 28} # height: 44 # bg_color: 0xFF4757 # radius: 14 # on_click: # then: # - script.execute: reset_{ks}_chores # widgets: # - label: # align: CENTER # text: "Reset" # text_color: 0xFFFFFF # text_font: font_small # - button: # x: 14 # y: {480 - PAD - 44} # width: {sidebar_w - 28} # height: 44 # bg_color: 0x{kid['color_dark']} # radius: 14 # on_click: # then: # - lvgl.page.show: page_home # widgets: # - label: # align: CENTER # text: "Home" # text_color: 0xFFFFFF # text_font: font_small # # ── Chore cards ─────────────────────────────────────────────────────── # {card_widgets}""") # return "\n".join(pages) # # ───────────────────────────────────────────────────────────────────────────── # # Script builder # # ───────────────────────────────────────────────────────────────────────────── # def _gen_scripts(kids: list) -> str: # blocks = [] # for kid in kids: # chores = get_chores(kid) # ks = kid_slug(kid) # n = len(chores) # card_updates = "" # for chore in chores: # e = eid(kid, chore) # card_updates += f"""\ # if (id({e}).state) {{ # lv_obj_set_style_bg_color(id(card_{e}), lv_color_hex(0xF0FFF4), LV_PART_MAIN); # lv_obj_set_style_border_color(id(card_{e}), lv_color_hex(0x6BCB77), LV_PART_MAIN); # lv_obj_set_style_border_width(id(card_{e}), 3, LV_PART_MAIN); # lv_label_set_text(id(check_{e}), "\\u2705"); # }} else {{ # lv_obj_set_style_bg_color(id(card_{e}), lv_color_hex(0xFFFFFF), LV_PART_MAIN); # lv_obj_set_style_border_color(id(card_{e}), lv_color_hex(0xEEEEEE), LV_PART_MAIN); # lv_obj_set_style_border_width(id(card_{e}), 2, LV_PART_MAIN); # lv_label_set_text(id(check_{e}), ""); # }} # """ # count_done = " + ".join(f"(id({eid(kid,c)}).state ? 1 : 0)" for c in chores) # reset_lines = "\n".join( # f" id({eid(kid,c)}).turn_off();" for c in chores # ) # blocks.append(f"""\ # - id: reset_{ks}_chores # mode: single # then: # - lambda: |- # {reset_lines} # - script.execute: update_{ks}_ui # """) # blocks.append(f"""\ # - id: update_{ks}_ui # mode: single # then: # - lambda: |- # int done = {count_done}; # int total = {n}; # char buf[24]; # // Progress bar + label # lv_bar_set_value(id(progress_bar_{ks}), done, LV_ANIM_ON); # snprintf(buf, sizeof(buf), "%d / {n}", done); # lv_label_set_text(id(progress_label_{ks}), buf); # // All-done message in sidebar # lv_label_set_text(id(all_done_label_{ks}), done == total ? "All done!" : ""); # // Home button: RED outline = incomplete, SOLID GREEN = all done # if (done == total) {{ # lv_obj_set_style_bg_opa(id(home_btn_{ks}), LV_OPA_COVER, LV_PART_MAIN); # lv_obj_set_style_bg_color(id(home_btn_{ks}), lv_color_hex(0x6BCB77), LV_PART_MAIN); # lv_obj_set_style_border_width(id(home_btn_{ks}), 0, LV_PART_MAIN); # lv_label_set_text(id(home_status_{ks}), "\\u2713 all done!"); # lv_obj_set_style_text_color(id(home_status_{ks}), lv_color_hex(0xFFFFFF), LV_PART_MAIN); # }} else {{ # lv_obj_set_style_bg_opa(id(home_btn_{ks}), LV_OPA_COVER, LV_PART_MAIN); # lv_obj_set_style_bg_color(id(home_btn_{ks}), lv_color_hex(0xFF4757), LV_PART_MAIN); # lv_obj_set_style_border_width(id(home_btn_{ks}), 0, LV_PART_MAIN); # snprintf(buf, sizeof(buf), "%d left", total - done); # lv_label_set_text(id(home_status_{ks}), buf); # lv_obj_set_style_text_color(id(home_status_{ks}), lv_color_hex(0xFF4757), LV_PART_MAIN); # }} # // Card colours # {card_updates} # """) # return "\n".join(blocks) # # ───────────────────────────────────────────────────────────────────────────── # # Home Assistant YAML generator # # ───────────────────────────────────────────────────────────────────────────── # def gen_ha(cfg: dict) -> str: # s = cfg["settings"] # kids = cfg["kids"] # sensor_blocks = [] # for kid in kids: # chores = get_chores(kid) # ks = kid_slug(kid) # n = len(chores) # states_list = ", ".join(f"states('{ha_switch(kid,c)}')" for c in chores) # sensor_blocks.append(f"""\ # - name: "{kid['name']} Chores Done Today" # unique_id: {ks}_chores_done_today # icon: mdi:check-circle # state: > # {{% set chores = [{states_list}] %}} # {{{{ chores | select('equalto', 'on') | list | count }}}} # - name: "{kid['name']} All Chores Done" # unique_id: {ks}_all_chores_done # icon: mdi:check-all # state: > # {{{{ states('sensor.{ks}_chores_done_today') | int == {n} }}}} # """) # input_buttons = "".join(f"""\ # {kid_slug(k)}_reset_chores: # name: "Reset {k['name']}'s Chores" # icon: mdi:restart # """ for k in kids) # input_buttons += """\ # reset_all_chores: # name: "Reset All Chores" # icon: mdi:restart-alert # """ # auto_blocks = [] # for kid in kids: # ks = kid_slug(kid) # off_calls = "\n".join( # f" - service: switch.turn_off\n target:\n entity_id: {ha_switch(kid, c)}" # for c in get_chores(kid) # ) # auto_blocks.append(f"""\ # - id: {ks}_reset_from_ha # alias: "Chore Tracker - Reset {kid['name']}'s chores from HA" # trigger: # - platform: state # entity_id: input_button.{ks}_reset_chores # action: # {off_calls} # """) # all_off = "\n".join( # f" - service: switch.turn_off\n target:\n entity_id: {ha_switch(kid, c)}" # for kid in kids for c in get_chores(kid) # ) # auto_blocks.append(f"""\ # - id: reset_all_from_ha # alias: "Chore Tracker - Reset ALL chores from HA" # trigger: # - platform: state # entity_id: input_button.reset_all_chores # action: # {all_off} # """) # for kid in kids: # ks = kid_slug(kid) # n = len(get_chores(kid)) # auto_blocks.append(f"""\ # - id: {ks}_all_done_notify # alias: "Chore Tracker - {kid['name']} all done!" # trigger: # - platform: state # entity_id: sensor.{ks}_all_chores_done # to: "True" # action: # - service: {s['notify_service']} # data: # title: "{kid['name']} finished all chores!" # message: "{kid['name']} completed all {n} chores today!" # """) # reminder_actions = "".join(f"""\ # - if: # condition: template # value_template: "{{{{ states('sensor.{kid_slug(k)}_all_chores_done') != 'True' }}}}" # then: # - service: {s['notify_service']} # data: # title: "{k['name']} has unfinished chores" # message: > # {k['name']} has done # {{{{ states('sensor.{kid_slug(k)}_chores_done_today') }}}}/{len(get_chores(k))} chores today. # """ for k in kids) # auto_blocks.append(f"""\ # - id: chore_reminder_evening # alias: "Chore Tracker - Evening reminder" # trigger: # - platform: time # at: "{s['reminder_time']}:00" # action: # {reminder_actions} # """) # return f"""\ # ################################################################################ # # chore-tracker-ha.yaml (AUTO-GENERATED — edit chores_config.yaml) # # Kids: {", ".join(k["name"] for k in kids)} # # # # BIDIRECTIONAL SYNC: # # Screen to HA: Each switch calls homeassistant.service on toggle # # HA to Screen: ESPHome native API handles this automatically # ################################################################################ # input_button: # {input_buttons} # template: # - sensor: # {"".join(sensor_blocks)} # automation: # {"".join(auto_blocks)} # """ # # ───────────────────────────────────────────────────────────────────────────── # # Lovelace dashboard generator # # ───────────────────────────────────────────────────────────────────────────── # def gen_dashboard(cfg: dict) -> str: # kids = cfg["kids"] # summary_cards = "\n".join(f"""\ # - type: markdown # content: > # ### {kid['avatar']} {kid['name']} # {{{{ # 'All done!' if states('sensor.{kid_slug(kid)}_all_chores_done') == 'True' # else states('sensor.{kid_slug(kid)}_chores_done_today') ~ '/{len(get_chores(kid))} chores done' # }}}} # """ for kid in kids) # chore_cards = [] # for kid in kids: # chores = get_chores(kid) # ks = kid_slug(kid) # chore_rows = "\n".join(f"""\ # - entity: {ha_switch(kid, c)} # name: "{c['icon']} {c['name']}" # """ for c in chores) # chore_cards.append(f"""\ # - type: entities # title: "{kid['avatar']} {kid['name']}'s Chores" # entities: # {chore_rows} - type: divider # - entity: sensor.{ks}_chores_done_today # name: "Chores done today" # - entity: sensor.{ks}_all_chores_done # name: "All Done?" # - type: button # name: "Reset {kid['name']}'s Chores" # tap_action: # action: call-service # service: input_button.press # target: # entity_id: input_button.{ks}_reset_chores # """) # return f"""\ # ################################################################################ # # chore-tracker-dashboard.yaml (AUTO-GENERATED — edit chores_config.yaml) # # Kids: {", ".join(k["name"] for k in kids)} # ################################################################################ # title: "Chore Tracker" # views: # - title: Today # icon: mdi:checkbox-marked-circle # path: chores # cards: # - type: horizontal-stack # cards: # {summary_cards} # - type: button # name: "Reset ALL Chores" # icon: mdi:restart-alert # tap_action: # action: call-service # service: input_button.press # target: # entity_id: input_button.reset_all_chores # {"".join(chore_cards)} # """ # # ───────────────────────────────────────────────────────────────────────────── # # Main # # ───────────────────────────────────────────────────────────────────────────── # def main(): # parser = argparse.ArgumentParser(description="Chore Tracker config generator") # parser.add_argument("--config", default="chores_config.yaml") # parser.add_argument("--out", default=".") # args = parser.parse_args() # config_path = Path(args.config) # if not config_path.exists(): # print(f"❌ Config not found: {config_path}") # sys.exit(1) # out_dir = Path(args.out) # out_dir.mkdir(parents=True, exist_ok=True) # class SecretLoader(yaml.SafeLoader): # pass # SecretLoader.add_constructor( # "!secret", # lambda loader, node: f"!secret {loader.construct_scalar(node)}" # ) # with open(config_path) as f: # cfg = yaml.load(f, Loader=SecretLoader) # kids = cfg.get("kids", []) # if not kids: # print("❌ No kids defined in config!") # sys.exit(1) # errors = [k["name"] for k in kids if "chores" not in k or not k["chores"]] # if errors: # print(f"❌ These kids have no chores defined: {', '.join(errors)}") # print(" Add a 'chores:' list under each kid in chores_config.yaml") # sys.exit(1) # # Warn about missing icons (will use default, not crash) # for k in kids: # for c in k.get("chores", []): # if "icon" not in c: # print(f"⚠️ {k['name']} / '{c['name']}' has no icon — using default") # total_chores = sum(len(get_chores(k)) for k in kids) # print(f"✅ Config loaded: {len(kids)} kid(s), {total_chores} total chores") # for k in kids: # chores = get_chores(k) # print(f" {k['avatar']} {k['name']}: {len(chores)} chores" # f" — {', '.join(c['name'] for c in chores)}") # print() # files = { # "chore-tracker-esphome.yaml": gen_esphome(cfg), # "chore-tracker-ha.yaml": gen_ha(cfg), # "chore-tracker-dashboard.yaml": gen_dashboard(cfg), # } # for name, content in files.items(): # path = out_dir / name # path.write_text(content) # print(f"📄 {path}") # print() # print("✅ Done!") # print(" 1. Flash chore-tracker-esphome.yaml -> ESPHome dashboard") # print(" 2. Merge chore-tracker-ha.yaml -> HA config + restart HA") # print(" 3. Paste chore-tracker-dashboard.yaml -> New HA dashboard") # if __name__ == "__main__": # main()