wiegand_2 działa
This commit is contained in:
1298
boneio-32x10_switches_v0_7_standalone.yaml
Normal file
1298
boneio-32x10_switches_v0_7_standalone.yaml
Normal file
File diff suppressed because it is too large
Load Diff
305
configurator/generate_yaml.py
Normal file
305
configurator/generate_yaml.py
Normal file
@@ -0,0 +1,305 @@
|
||||
#!/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()
|
||||
1
configurator/requirements.txt
Normal file
1
configurator/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
openpyxl>=3.1.0
|
||||
@@ -126,9 +126,9 @@ display:
|
||||
it.printf(4, 25, id(size_10), TextAlign::BASELINE_LEFT, "Version:");
|
||||
it.printf(124, 25, id(size_10), TextAlign::BASELINE_RIGHT, "%s", ESPHOME_VERSION);
|
||||
it.printf(4, 37, id(size_10), TextAlign::BASELINE_LEFT, "Free heap:");
|
||||
it.printf(124, 37, id(size_10), TextAlign::BASELINE_RIGHT, "%u B", ESP.getFreeHeap());
|
||||
it.printf(124, 37, id(size_10), TextAlign::BASELINE_RIGHT, "%u B", esp_get_free_heap_size());
|
||||
it.printf(4, 49, id(size_10), TextAlign::BASELINE_LEFT, "CPU freq:");
|
||||
it.printf(124, 49, id(size_10), TextAlign::BASELINE_RIGHT, "%u MHz", ESP.getCpuFreqMHz());
|
||||
it.printf(124, 49, id(size_10), TextAlign::BASELINE_RIGHT, "%u MHz", esp_clk_cpu_freq() / 1000000);
|
||||
- id: last_page
|
||||
lambda: |-
|
||||
it.rectangle(0, 0, 126, 15);
|
||||
|
||||
Reference in New Issue
Block a user