306 lines
9.9 KiB
Python
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()
|