Zigbee devices auf einen anderen Splitter/Koordinator umziehen

Hier ein PHP Skript, welches das tut, was im Titel steht. Im „DRY-RUN“ Modus wird einfach eine gut formatierte Tabelle aller Zigbee-Devices erstellt.

<?php
declare(strict_types=1);

/**
 * Zigbee2MQTT: change MQTTBaseTopic for device instances + show table
 *
 * Modes:
 *   - DRY_RUN = true    : simulate for ALL Zigbee2MQTT Device instances (no changes)
 *   - DRY_RUN = <int>   : wet-run for ONLY that instance id
 *   - DRY_RUN = false   : wet-run for ALL instances
 *
 * After run: prints table
 *   ID | Friendly_Name | IEEE | base_topic | instance_topic
 */

const DEVICE_MODULE_FILTER = 'Zigbee2MQTT Device';
const NEW_BASE_TOPIC       = 'zigbee2mqtt2065';

// DRY_RUN can be: true / false / int
$DRY_RUN = true;  // <-- set to true, false, or e.g. 53693

// Table formatting
const TABLE_WIDTH_FRIENDLY = 42;
const TABLE_WIDTH_TOPIC    = 44;

// ---------------- helpers ----------------
function moduleName(int $iid): string {
    try {
        $inst = IPS_GetInstance($iid);
        $mid  = $inst['ModuleInfo']['ModuleID'] ?? '';
        if ($mid === '') return '';
        $m = IPS_GetModule($mid);
        return (string)($m['ModuleName'] ?? '');
    } catch (Throwable $e) {
        return '';
    }
}

function safeConfig(int $iid): array {
    $cfg = IPS_GetConfiguration($iid); // JSON string
    $arr = json_decode($cfg, true);
    return is_array($arr) ? $arr : [];
}

function extractIEEE(string $friendly, array $cfg): string {
    if (!empty($cfg['IEEE']) && is_string($cfg['IEEE'])) return strtolower(trim($cfg['IEEE']));
    if (preg_match('/0x[0-9a-fA-F]{16}/', $friendly, $m)) return strtolower($m[0]);
    return '';
}

function getMqttBaseTopic(int $iid): string {
    $cfg = safeConfig($iid);
    return (isset($cfg['MQTTBaseTopic']) && is_string($cfg['MQTTBaseTopic'])) ? trim($cfg['MQTTBaseTopic']) : '';
}

function getMqttTopic(int $iid): string {
    $cfg = safeConfig($iid);
    // Your module uses MQTTTopic (no space)
    if (isset($cfg['MQTTTopic']) && is_string($cfg['MQTTTopic']) && trim($cfg['MQTTTopic']) !== '') {
        return trim($cfg['MQTTTopic']);
    }
    // fallback: instance name
    return IPS_GetName($iid);
}

function setMqttBaseTopic(int $iid, string $newBaseTopic): void {
    // Property name is confirmed: MQTTBaseTopic
    IPS_SetProperty($iid, 'MQTTBaseTopic', $newBaseTopic);
    IPS_ApplyChanges($iid);
}

function pad(string $s, int $w): string {
    if (mb_strlen($s) > $w) return mb_substr($s, 0, $w - 1) . '…';
    return str_pad($s, $w);
}

function printTable(array $deviceIds): void {
    $rows = [];
    foreach ($deviceIds as $iid) {
        $friendly = IPS_GetName($iid);
        $cfg      = safeConfig($iid);

        $ieee  = extractIEEE($friendly, $cfg);
        $base  = getMqttBaseTopic($iid);
        $topic = getMqttTopic($iid);

        $instanceTopic = '';
        if ($base !== '' && $topic !== '') {
            $instanceTopic = rtrim($base, '/') . '/' . ltrim($topic, '/');
        }

        $rows[] = [
            'ID'             => (string)$iid,
            'Friendly_Name'  => $friendly,
            'IEEE'           => $ieee,
            'base_topic'     => $base,
            'instance_topic' => $instanceTopic,
        ];
    }

    usort($rows, fn($a, $b) => strcasecmp($a['Friendly_Name'], $b['Friendly_Name']) ?: ((int)$a['ID'] <=> (int)$b['ID']));

    $widths = [
        'ID'             => 7,
        'Friendly_Name'  => TABLE_WIDTH_FRIENDLY,
        'IEEE'           => 20,
        'base_topic'     => 18,
        'instance_topic' => TABLE_WIDTH_TOPIC,
    ];

    $sep = '+';
    foreach ($widths as $w) $sep .= str_repeat('-', $w + 2) . '+';

    $header = '|';
    foreach ($widths as $k => $w) $header .= ' ' . pad($k, $w) . ' |';

    echo "\nZigbee2MQTT Device Enumeration (table)\n";
    echo "=====================================\n";
    echo "Found " . count($rows) . " devices\n\n";
    echo $sep . "\n";
    echo $header . "\n";
    echo $sep . "\n";

    foreach ($rows as $r) {
        echo '| '
            . pad($r['ID'],             $widths['ID'])             . ' | '
            . pad($r['Friendly_Name'],  $widths['Friendly_Name'])  . ' | '
            . pad($r['IEEE'],           $widths['IEEE'])           . ' | '
            . pad($r['base_topic'],     $widths['base_topic'])     . ' | '
            . pad($r['instance_topic'], $widths['instance_topic']) . " |\n";
    }

    echo $sep . "\n";
}

// ---------------- main ----------------
$deviceIds = [];
foreach (IPS_GetInstanceList() as $iid) {
    if (stripos(moduleName($iid), DEVICE_MODULE_FILTER) === false) continue;
    $deviceIds[] = $iid;
}
sort($deviceIds);

echo "Zigbee2MQTT MQTTBaseTopic changer\n";
echo "================================\n";
echo "Devices found: " . count($deviceIds) . "\n";
echo "New MQTTBaseTopic: " . NEW_BASE_TOPIC . "\n";

$targets = [];
$apply   = false;

if ($DRY_RUN === true) {
    $targets = $deviceIds;  // simulate all
    $apply   = false;
    echo "Mode: DRY-RUN (simulate all)\n\n";
} elseif ($DRY_RUN === false) {
    $targets = $deviceIds;  // apply all
    $apply   = true;
    echo "Mode: WET-RUN (apply all)\n\n";
} elseif (is_int($DRY_RUN)) {
    $targets = [$DRY_RUN];  // apply one
    $apply   = true;
    echo "Mode: WET-RUN (apply single instance {$DRY_RUN})\n\n";
} else {
    echo "ERROR: DRY_RUN must be bool or int.\n";
    return;
}

$changed = 0;
$skipped = 0;
$failed  = 0;

foreach ($targets as $iid) {
    if (!IPS_ObjectExists($iid)) {
        echo "FAIL {$iid}: instance does not exist\n";
        $failed++;
        continue;
    }
    if (stripos(moduleName($iid), DEVICE_MODULE_FILTER) === false) {
        echo "SKIP {$iid} (" . IPS_GetName($iid) . "): not a Zigbee2MQTT Device\n";
        $skipped++;
        continue;
    }

    $cur = getMqttBaseTopic($iid);
    if ($cur === NEW_BASE_TOPIC) {
        echo "OK   {$iid} (" . IPS_GetName($iid) . "): already " . NEW_BASE_TOPIC . "\n";
        continue;
    }

    if ($apply) {
        try {
            setMqttBaseTopic($iid, NEW_BASE_TOPIC);
            $after = getMqttBaseTopic($iid);
            echo "CHG  {$iid} (" . IPS_GetName($iid) . "): MQTTBaseTopic {$cur} -> {$after}\n";
            $changed++;
        } catch (Throwable $e) {
            echo "FAIL {$iid} (" . IPS_GetName($iid) . "): " . $e->getMessage() . "\n";
            $failed++;
        }
    } else {
        echo "DRY  {$iid} (" . IPS_GetName($iid) . "): WOULD set MQTTBaseTopic {$cur} -> " . NEW_BASE_TOPIC . "\n";
    }
}

echo "\nSummary:\n";
echo "  Changed: {$changed}\n";
echo "  Skipped: {$skipped}\n";
echo "  Failed : {$failed}\n";

// Always show full table after run
printTable($deviceIds);

echo "Done.\n";