#!/usr/bin/env python3 """ BoneIO YAML Configurator Generuje konfigurację ESPHome (switch/light/fan) na podstawie arkusza Excel. Użycie: python generate_yaml.py # tworzy pusty szablon boneio_config.xlsx python generate_yaml.py --template # to samo co wyżej python generate_yaml.py boneio_config.xlsx # generuje YAML z wypełnionego arkusza """ import sys import argparse from pathlib import Path try: import openpyxl from openpyxl.styles import PatternFill, Font, Alignment from openpyxl.worksheet.datavalidation import DataValidation except ImportError: print("Wymagana biblioteka: pip install openpyxl") sys.exit(1) TEMPLATE_FILE = "boneio_config.xlsx" MAX_OUTPUTS = 32 ENTITY_TYPES = ["switch", "light", "fan"] # Kolumny (1-based) COL_NR = 1 # A COL_ID = 2 # B COL_NAME = 3 # C COL_TYPE = 4 # D COL_ROOM = 5 # E COL_ICON = 6 # F COL_INPUT1 = 7 # G COL_INPUT2 = 8 # H HEADERS = [ "Nr", "Output ID", "Nazwa", "Typ", "Pokój", "Ikona (mdi:...)", "Input 1", "Input 2", ] COLOR_HEADER = "1F4E79" COLOR_ODD_ROW = "DCE6F1" COLOR_EVEN_ROW = "FFFFFF" COLOR_EXAMPLE = "F2F2F2" def create_template(filename=TEMPLATE_FILE): wb = openpyxl.Workbook() ws = wb.active ws.title = "Konfiguracja" # --- Nagłówek --- header_fill = PatternFill(start_color=COLOR_HEADER, end_color=COLOR_HEADER, fill_type="solid") header_font = Font(color="FFFFFF", bold=True, size=11) for col, label in enumerate(HEADERS, 1): cell = ws.cell(row=1, column=col, value=label) cell.fill = header_fill cell.font = header_font cell.alignment = Alignment(horizontal="center", vertical="center") ws.row_dimensions[1].height = 25 # --- Szerokości kolumn --- ws.column_dimensions["A"].width = 5 ws.column_dimensions["B"].width = 12 ws.column_dimensions["C"].width = 32 ws.column_dimensions["D"].width = 10 ws.column_dimensions["E"].width = 22 ws.column_dimensions["F"].width = 22 ws.column_dimensions["G"].width = 16 ws.column_dimensions["H"].width = 16 # --- Walidacja listy rozwijalnej dla kolumny Typ --- dv = DataValidation( type="list", formula1='"switch,light,fan"', allow_blank=True, showDropDown=False, showErrorMessage=True, errorTitle="Błędny typ", error="Wybierz: switch, light lub fan", ) dv.sqref = f"D2:D{MAX_OUTPUTS + 1}" ws.add_data_validation(dv) # --- Wiersze wyjść --- for i in range(1, MAX_OUTPUTS + 1): row = i + 1 fill_color = COLOR_ODD_ROW if i % 2 == 1 else COLOR_EVEN_ROW row_fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type="solid") ws.cell(row=row, column=COL_NR, value=i).font = Font(color="888888") ws.cell(row=row, column=COL_ID, value=f"out_{i:02d}").font = Font(color="888888") for col in range(1, len(HEADERS) + 1): cell = ws.cell(row=row, column=col) cell.fill = row_fill cell.alignment = Alignment(vertical="center") # Zamrożenie nagłówka i pierwszych dwóch kolumn ws.freeze_panes = "C2" # --- Arkusz z podpowiedziami --- ws2 = wb.create_sheet("Pomoc") ws2.column_dimensions["A"].width = 28 ws2.column_dimensions["B"].width = 35 def h(row, col, val, bold=False): c = ws2.cell(row=row, column=col, value=val) if bold: c.font = Font(bold=True) return c h(1, 1, "Typ encji", bold=True) h(1, 2, "Opis", bold=True) h(2, 1, "switch"); h(2, 2, "Zwykły przełącznik (np. gniazdko, grzejnik)") h(3, 1, "light"); h(3, 2, "Światło — widoczne jako żarówka w HA") h(4, 1, "fan"); h(4, 2, "Wentylator — widoczny z ikoną wentylatora w HA") h(6, 1, "Input 1 / Input 2", bold=True) h(7, 1, ""); h(7, 2, "ID binarnego sensora z YAML (np. boneIO_input_01)") h(8, 2, "Wciśnięcie przycisku będzie toggleować dany output") h(9, 2, "Możesz podać dwa różne przyciski sterujące tym samym outputem") h(11, 1, "Popularne ikony", bold=True) icons = [ ("mdi:lightbulb", "żarówka"), ("mdi:ceiling-light", "lampa sufitowa"), ("mdi:floor-lamp", "lampa stojąca"), ("mdi:wall-sconce-flat", "kinkiet"), ("mdi:led-strip-variant", "taśma LED"), ("mdi:outdoor-lamp", "lampa ogrodowa"), ("mdi:fan", "wentylator"), ("mdi:toggle-switch", "przełącznik"), ("mdi:power-socket-eu", "gniazdko"), ("mdi:heating-coil", "ogrzewanie"), ("mdi:air-conditioner", "klimatyzacja"), ("mdi:garage", "garaż"), ("mdi:gate", "brama"), ("mdi:television", "TV / AV"), ("mdi:water-pump", "pompa"), ] ex_fill = PatternFill(start_color=COLOR_EXAMPLE, end_color=COLOR_EXAMPLE, fill_type="solid") for idx, (icon, desc) in enumerate(icons, 12): c1 = ws2.cell(row=idx, column=1, value=icon) c2 = ws2.cell(row=idx, column=2, value=desc) c1.fill = ex_fill c2.fill = ex_fill wb.save(filename) print(f"[OK] Szablon zapisany: {filename}") print(f" Wypełnij arkusz 'Konfiguracja', potem uruchom:") print(f" python generate_yaml.py {filename}") # --------------------------------------------------------------------------- def load_entries(ws): entries = [] for i in range(1, MAX_OUTPUTS + 1): row = i + 1 name = ws.cell(row=row, column=COL_NAME).value etype = (ws.cell(row=row, column=COL_TYPE).value or "").lower().strip() if not name or etype not in ENTITY_TYPES: continue entries.append({ "nr": i, "output_id": ws.cell(row=row, column=COL_ID).value or f"out_{i:02d}", "name": str(name).strip(), "type": etype, "room": str(ws.cell(row=row, column=COL_ROOM).value or "").strip(), "icon": str(ws.cell(row=row, column=COL_ICON).value or "").strip(), "input1": str(ws.cell(row=row, column=COL_INPUT1).value or "").strip(), "input2": str(ws.cell(row=row, column=COL_INPUT2).value or "").strip(), }) return entries def entity_id(e): return f"{e['type']}_{e['nr']:02d}" def yaml_block(entries, etype, platform): items = [e for e in entries if e["type"] == etype] if not items: return [] lines = [f"{etype}:"] for e in items: eid = entity_id(e) room_comment = f" # {e['room']}" if e["room"] else "" lines.append(f" - platform: {platform}{room_comment}") lines.append(f' name: "{e["name"]}"') lines.append(f" output: {e['output_id']}") lines.append(f" id: {eid}") if e["icon"]: lines.append(f" icon: {e['icon']}") lines.append("") return lines def generate_yaml(filename): wb = openpyxl.load_workbook(filename) if "Konfiguracja" not in wb.sheetnames: print(f"[ERROR] Arkusz 'Konfiguracja' nie znaleziony w {filename}") sys.exit(1) ws = wb["Konfiguracja"] entries = load_entries(ws) if not entries: print("[WARN] Brak skonfigurowanych wyjść. Wypełnij kolumny Nazwa i Typ.") return # Mapowanie wejść input_map: dict[str, list[tuple[str, str]]] = {} for e in entries: action = f"{e['type']}.toggle" eid = entity_id(e) for inp in [e["input1"], e["input2"]]: if inp: input_map.setdefault(inp, []).append((action, eid)) lines = [ "# ============================================================", f"# Wygenerowano przez BoneIO Configurator", f"# Źródło: {Path(filename).name}", "# ============================================================", "", ] lines += yaml_block(entries, "switch", "output") lines += yaml_block(entries, "light", "binary") lines += yaml_block(entries, "fan", "binary") if input_map: lines += [ "# --- Mapowanie wejść ---", "# Dopisz poniższe on_click do istniejących binary_sensor w swoim YAML", "# (wyszukaj ID sensora i dodaj on_click)", "", ] for inp_id, actions in sorted(input_map.items()): lines.append(f" # binary_sensor id: {inp_id}") lines.append(f" # on_click:") for action, eid in actions: lines.append(f" # - {action}: {eid}") lines.append("") output_text = "\n".join(lines) out_file = Path(filename).with_suffix(".yaml") out_file.write_text(output_text, encoding="utf-8") n_sw = sum(1 for e in entries if e["type"] == "switch") n_li = sum(1 for e in entries if e["type"] == "light") n_fan = sum(1 for e in entries if e["type"] == "fan") print(f"[OK] YAML zapisany: {out_file}") print(f" switch: {n_sw} light: {n_li} fan: {n_fan} (razem: {len(entries)})") if input_map: print(f" mapowania wejść: {len(input_map)}") print() print(output_text) # --------------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser( description="BoneIO YAML Configurator — switch / light / fan" ) parser.add_argument( "file", nargs="?", help="Plik Excel do przetworzenia (pomięcie = tworzy szablon)", ) parser.add_argument( "--template", action="store_true", help="Utwórz pusty szablon Excel", ) args = parser.parse_args() if args.template or not args.file: create_template() else: if not Path(args.file).exists(): print(f"[ERROR] Plik nie istnieje: {args.file}") sys.exit(1) generate_yaml(args.file) if __name__ == "__main__": main()