Files
BoneIO-ESP/configurator/generate_yaml.py
2026-04-11 02:20:59 +02:00

306 lines
9.9 KiB
Python

#!/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()