Newer
Older
homeassistant-infra / ibsystem / sync_names.py
#!/usr/bin/env python3
"""
Synchronizuje friendly_names i dashboard IBSystem z dokumentacji projektu.
Źródło: /smart-home/proj-ib-lighting-rs485-zarki.yaml
"""
import yaml, re, sys, os, subprocess, shutil, io
from datetime import datetime
import copy

PROJECT_YAML    = "/smart-home/proj-ib-lighting-rs485-zarki.yaml"
CONFIG_YAML     = "/ibsystem/ibsystem2mqtt.yaml"
DASHBOARD_LOCAL = "/ibsystem/ibsystem_dashboard.yaml"
DASHBOARD_HA    = "root@192.168.50.151:/config/dashboards/ibsystem_dashboard.yaml"
HA_SSH_PORT     = 2222
MQTT_HOST       = "192.168.50.151"
MQTT_USER       = "mqtt"
MQTT_PASS       = "mqtt123"

# Zakresy ID sterowników oświetlenia (pomijamy ogrzewanie itp.)
LIGHTING_ID_MIN = 30
LIGHTING_ID_MAX = 99

def log(msg): print(f"[{datetime.now():%H:%M:%S}] {msg}")

def device_id_to_rs_id(device_id):
    m = re.match(r"rs\.(\d+)\.id\.(\d+)", device_id)
    return (m.group(1), m.group(2)) if m else (None, None)

def io_key(rs, dev_id, io_id):
    safe = re.sub(r"[^a-zA-Z0-9]+", "_", io_id).strip("_")
    return f"rs{rs}_id{dev_id}_{safe}"

def entity_id(rs, dev_id, io_id):
    safe = re.sub(r"[^a-zA-Z0-9]+", "_", io_id).strip("_")
    return f"switch.ibsystem_rs{rs}_id{dev_id}_rs{rs}_id{dev_id}_{safe}"

def parse_project_yaml(path):
    with open(path) as f:
        data = yaml.safe_load(f)
    devices = data["project"]["hardware_specification"]["devices"]
    result = []
    for dev in devices:
        device_id = dev.get("device_id", "")
        location  = dev.get("location", "").strip()
        rs, dev_id = device_id_to_rs_id(device_id)
        if not rs:
            continue
        io_list = []
        for io in dev.get("io", []):
            io_id   = io.get("io_id", "")
            desc    = io.get("description", "").strip()
            status  = io.get("status", "ok")
            if io_id.startswith("output.do.") and desc and status != "nc" \
                    and not desc.startswith("Opcjonalne"):
                io_list.append({"io_id": io_id, "description": desc})
        result.append({
            "device_id": device_id,
            "rs": rs,
            "dev_id": dev_id,
            "location": location,
            "io": io_list,
        })
    return result

def build_friendly_names(devices):
    names = {}
    for dev in devices:
        rs, dev_id, location = dev["rs"], dev["dev_id"], dev["location"]
        if location:
            names[f"rs{rs}_id{dev_id}"] = f"{location} - ID{dev_id}"
        for io in dev["io"]:
            names[io_key(rs, dev_id, io["io_id"])] = io["description"]
    return names

def build_dashboard(devices):
    # Tylko sterowniki oświetlenia (ID30+)
    light_devs = [d for d in devices
                  if d["io"] and LIGHTING_ID_MIN <= int(d["dev_id"]) <= LIGHTING_ID_MAX]

    # Lista wszystkich switch entity_id dla WSZYSTKIE ON/OFF
    all_switches = []
    for dev in light_devs:
        rs, dev_id = dev["rs"], dev["dev_id"]
        for io in dev["io"]:
            all_switches.append(entity_id(rs, dev_id, io["io_id"]))

    # Karty per urządzenie
    device_cards = []
    for dev in light_devs:
        rs, dev_id, location = dev["rs"], dev["dev_id"], dev["location"]
        title = f"{location} (ID{dev_id})" if location else f"ID{dev_id}"
        entities = []
        for io in dev["io"]:
            entities.append({
                "entity": entity_id(rs, dev_id, io["io_id"]),
                "name": io["description"],
            })
        device_cards.append({
            "type": "entities",
            "title": title,
            "entities": entities,
        })

    dashboard = {
        "title": "IBSystem Sterowniki",
        "views": [{
            "title": "Wszystkie sterowniki",
            "path": "all",
            "icon": "mdi:lightbulb-group",
            "cards": [
                {
                    "type": "horizontal-stack",
                    "cards": [
                        {
                            "type": "button",
                            "name": "WSZYSTKIE ON",
                            "icon": "mdi:lightbulb-group",
                            "icon_height": "40px",
                            "tap_action": {
                                "action": "call-service",
                                "service": "switch.turn_on",
                                "target": {"entity_id": copy.deepcopy(all_switches)},
                            },
                        },
                        {
                            "type": "button",
                            "name": "WSZYSTKIE OFF",
                            "icon": "mdi:lightbulb-group-off",
                            "icon_height": "40px",
                            "tap_action": {
                                "action": "call-service",
                                "service": "switch.turn_off",
                                "target": {"entity_id": copy.deepcopy(all_switches)},
                            },
                        },
                    ],
                },
            ] + device_cards,
        }],
    }
    return dashboard

def update_friendly_names(config_path, new_names, dry_run=False):
    with open(config_path) as f:
        config = yaml.safe_load(f)
    old_names = config.get("friendly_names") or {}
    changed = {k for k in (set(old_names) | set(new_names)) if old_names.get(k) != new_names.get(k)}
    if not changed:
        log("friendly_names: bez zmian.")
        return False
    log(f"friendly_names: {len(changed)} zmian.")
    if dry_run:
        for k in sorted(changed):
            print(f"  {k}: {old_names.get(k)!r} → {new_names.get(k)!r}")
        return True
    shutil.copy(config_path, config_path + ".bak")
    config["friendly_names"] = new_names
    with open(config_path, "w") as f:
        yaml.dump(config, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
    log(f"  Zapisano {config_path}")
    return True

def update_dashboard(dashboard, local_path, ha_path, dry_run=False):
    # Serializuj do YAML
    buf = io.StringIO()
    yaml.dump(dashboard, buf, allow_unicode=True, default_flow_style=False,
              sort_keys=False, width=200)
    new_content = buf.getvalue()

    # Sprawdź czy się zmieniło
    try:
        with open(local_path) as f:
            old_content = f.read()
        if old_content == new_content:
            log("dashboard: bez zmian.")
            return False
    except FileNotFoundError:
        pass

    log("dashboard: aktualizuję.")
    if dry_run:
        log("  (dry-run, nie zapisuję)")
        return True

    with open(local_path, "w") as f:
        f.write(new_content)

    # Kopiuj na HA przez SSH
    host, remote_path = ha_path.split(":", 1)
    result = subprocess.run(
        ["scp", "-P", str(HA_SSH_PORT), "-o", "StrictHostKeyChecking=no",
         local_path, ha_path],
        capture_output=True, text=True
    )
    if result.returncode != 0:
        log(f"  BŁĄD SCP: {result.stderr.strip()}")
    else:
        log(f"  Skopiowano → {ha_path}")
    return True

def reload_ibsystem():
    log("Czyszczę MQTT discovery cache...")
    os.system(
        f"timeout 5 mosquitto_sub -h {MQTT_HOST} -u {MQTT_USER} -P {MQTT_PASS} "
        f"-t 'homeassistant/#' --retained-only -v 2>/dev/null | "
        f"grep '/config' | grep 'ibsystem' | awk '{{print $1}}' | "
        f"while read t; do mosquitto_pub -h {MQTT_HOST} -u {MQTT_USER} -P {MQTT_PASS} "
        f"-t \"$t\" -n -r; done"
    )
    log("Restartuję ibsystem2mqtt...")
    subprocess.run(["systemctl", "restart", "ibsystem2mqtt"], check=True)

def main():
    dry_run = "--dry-run" in sys.argv
    log(f"Parsing {PROJECT_YAML}...")

    try:
        devices = parse_project_yaml(PROJECT_YAML)
    except Exception as e:
        log(f"BŁĄD parsowania: {e}")
        sys.exit(1)

    log(f"  {len(devices)} urządzeń, "
        f"{sum(len(d['io']) for d in devices)} wyjść DO.")

    names_changed    = update_friendly_names(CONFIG_YAML, build_friendly_names(devices), dry_run)
    dashboard_changed = update_dashboard(build_dashboard(devices), DASHBOARD_LOCAL,
                                         DASHBOARD_HA, dry_run)

    if (names_changed or dashboard_changed) and not dry_run:
        reload_ibsystem()
    else:
        log("Brak zmian wymagających restartu.")

if __name__ == "__main__":
    main()