UNPKG

homebridge-pioneer-avr-2025

Version:

A Pioneer AVR plugin for homebridge. This plugin is designed for Pioneer models that use Telnet Commands (e.g., VSX-922). It may not be compatible with newer models (e.g., VSX-LX304).

989 lines 56.9 kB
"use strict";
// src/pioneer-avr-platform.ts
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PioneerAvrPlatform = void 0;
let HAPStorage;
try {
    HAPStorage = require('hap-nodejs').HAPStorage;
}
catch (error) {
    HAPStorage = {};
}
const discovery_1 = require("./discovery");
const pioneer_avr_accessory_js_1 = __importDefault(require("./pioneer-avr-accessory.js"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const package_json_1 = __importDefault(require("../package.json")); // Import package.json
const exitHandler_1 = require("./exitHandler");
/**
 * PioneerAvrPlatform
 * This class serves as the main entry point for the plugin, where user configuration is parsed,
 * accessories are registered, and discovered devices are managed.
 */
class PioneerAvrPlatform {
    constructor(log, config, api) {
        var _a;
        this.log = log;
        this.config = config;
        this.api = api;
        this.homebridgeConfigPath = '';
        // Used to track restored cached accessories
        this.accessories = [];
        // Configuration constants as instance variables
        this.TELNET_PORTS = [23, 24, 8102];
        this.TARGET_NAME = 'VSX';
        this.MAX_ATTEMPTS = 1000;
        this.RETRY_DELAY = 10000; // 10 seconds in milliseconds
        /**
         * Cache for storing discovered receivers and their inputs.
         * The structure maps each host to its corresponding input data.
         */
        this.cachedReceivers = new Map();
        this.devicesFound = [];
        this.service = this.api.hap.Service;
        this.characteristic = this.api.hap.Characteristic;
        let platformName = package_json_1.default.platformName || 'pioneerAvr2025';
        platformName = platformName.replace(/[^a-zA-Z0-9 ']/g, '');
        platformName = platformName
            .replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, '')
            .trim();
        let pluginName = package_json_1.default.name || 'homebridge-pioneer-avr-2025';
        pluginName = pluginName.replace(/[^a-zA-Z0-9 \-']/g, '');
        pluginName = pluginName
            .replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, '')
            .trim();
        this.platformName = platformName || 'pioneerAvr2025';
        this.pluginName = pluginName || 'homebridge-pioneer-avr-2025';
        const storagePath = typeof HAPStorage.storagePath === 'function'
            ? HAPStorage.storagePath() // Homebridge v2+
            : this.api.user.storagePath(); // Homebridge v1
        this.prefsDir =
            this.config.prefsDir ||
                storagePath + '/pioneerAvr/';
        this.homebridgeConfigPath = path.join(storagePath, 'config.json');
        const possiblePaths = [
            path.join(storagePath, 'config.json'),
            path.join(this.prefsDir, 'config.json'),
            path.resolve(__dirname, '../config.json')
        ];
        for (const configPath of possiblePaths) {
            if (fs.existsSync(configPath)) {
                this.homebridgeConfigPath = configPath;
                break;
            }
        }
        this.log.debug('Platform started:', this.platformName, this.pluginName);
        if (((_a = config.device) === null || _a === void 0 ? void 0 : _a.port) &&
            !this.TELNET_PORTS.includes(parseInt(config.device.port, 10))) {
            this.TELNET_PORTS.unshift(parseInt(config.device.port, 10));
        }
        if (config.port &&
            !this.TELNET_PORTS.includes(parseInt(config.port, 10))) {
            this.TELNET_PORTS.unshift(parseInt(config.port, 10));
        }
        if (config.devices &&
            Array.isArray(config.devices) &&
            config.devices.length > 0) {
            for (const device of config.devices) {
                if (device.port &&
                    !this.TELNET_PORTS.includes(parseInt(device.port, 10))) {
                    this.TELNET_PORTS.unshift(parseInt(device.port, 10));
                }
            }
        }
        // Register for the 'didFinishLaunching' event to start device discovery after Homebridge startup
        this.api.on('didFinishLaunching', () => {
            this.discoverDevices();
        });
    }
    /**
     * Cleans up old or invalid cached accessories from Homebridge.
     * Filters cached accessories to remove those no longer managed by the current plugin,
     * identified by matching their plugin name against expected identifiers (e.g., 'homebridge-pioneer-avr', 'vsx').
     * Ensures the cache remains accurate and prevents stale accessories from being loaded.
     */
    cleanCachedAccessories() {
        const storagePath = typeof HAPStorage.storagePath === 'function'
            ? HAPStorage.storagePath() // Homebridge v2+
            : this.api.user.storagePath(); // Homebridge v1
        const cachedAccessoriesPath = path.join(storagePath, 'accessories/cachedAccessories');
        // Check if the cachedAccessories file exists
        if (fs.existsSync(cachedAccessoriesPath)) {
            this.log.debug('Found cached accessories file, starting cleanup.');
            // Read and parse the cached accessories JSON file
            const cachedAccessories = JSON.parse(fs.readFileSync(cachedAccessoriesPath, 'utf-8'));
            // Filter accessories to retain only those not related to homebridge-pioneer-avr
            const filteredAccessories = cachedAccessories.filter((accessory) => {
                var _a;
                // Convert to lowercase and check for 'homebridge-pioneer-avr' or 'vsx'
                const pluginName = ((_a = accessory.plugin) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || '';
                return !(pluginName.includes('homebridge-pioneer-avr') || pluginName.includes('vsx') || pluginName.includes('pioneer'));
            });
            // Check if any accessories were removed
            if (filteredAccessories.length !== cachedAccessories.length) {
                // Write the filtered accessories back to the cachedAccessories file
                fs.writeFileSync(cachedAccessoriesPath, JSON.stringify(filteredAccessories, null, 4), 'utf-8');
                this.log.info(`Removed ${cachedAccessories.length - filteredAccessories.length} cached accessories related to homebridge-pioneer-avr.`);
            }
            else {
                this.log.debug('No cached accessories related to homebridge-pioneer-avr found to remove.');
            }
        }
        else {
            this.log.debug('Cached accessories file not found, skipping cleanup.');
        }
    }
    // /**
    //  * Retrieves the cached receiver data for a specific host.
    //  * @param host The host to retrieve inputs for.
    //  * @returns The cached inputs, or undefined if the host is not found.
    //  */
    // public getCachedInputsForHost(host: string): { id: string; name: string }[] | undefined {
    //     return this.cachedReceivers.get(host)?.inputs;
    // }
    /**
     * Invoked when Homebridge restores cached accessories from disk at startup.
     * Sets up event handlers for each cached accessory.
     */
    configureAccessory(accessory) {
        this.log.info('Loading accessory from cache:', accessory.displayName);
        this.accessories.push(accessory);
    }
    /**
     * Initiates the device discovery process or uses a manually configured device.
     * Attempts up to MAX_ATTEMPTS times if no devices are found, with a delay of RETRY_DELAY between attempts.
     */
    discoverDevices() {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
            // Check if the device is manually configured, bypassing discovery
            if (((_a = this.config) === null || _a === void 0 ? void 0 : _a.devices) &&
                Array.isArray((_b = this.config) === null || _b === void 0 ? void 0 : _b.devices) &&
                this.config.devices.length > 0) {
                for (const device of this.config.devices) {
                    if (device &&
                        (device.host || device.ip) &&
                        String(device.host || device.ip).length > 0) {
                        const addDevice = {
                            name: device.name ||
                                String(device.host || device.ip)
                                    .replace(/\.local$/, '')
                                    .replace(/[^a-zA-Z0-9 ]/g, ''),
                            origName: device.name ||
                                String(device.host || device.ip)
                                    .replace(/\.local$/, '')
                                    .replace(/[^a-zA-Z0-9 ]/g, ''),
                            host: device.host || device.ip,
                            port: device.port || 23,
                            source: 'pluginConfig',
                            fqdn: device.host || device.ip,
                        };
                        if (device.listeningMode) {
                            addDevice.listeningMode = device.listeningMode;
                        }
                        if (device.listeningModeFallback) {
                            addDevice.listeningModeFallback = device.listeningModeFallback;
                        }
                        if (device.listeningModeOther) {
                            addDevice.listeningModeOther = device.listeningModeOther;
                        }
                        if (device.minVolume) {
                            addDevice.minVolume = device.minVolume;
                        }
                        if (device.maxVolume) {
                            addDevice.maxVolume = device.maxVolume;
                        }
                        if (device.inputSwitches) {
                            addDevice.inputSwitches = device.inputSwitches;
                        }
                        this.devicesFound.push(addDevice);
                    }
                }
            }
            else if (((_c = this.config) === null || _c === void 0 ? void 0 : _c.device) &&
                (this.config.device.host || this.config.device.ip) &&
                String(this.config.device.host || this.config.device.ip).length >
                    0 &&
                this.config.device.port) {
                const addDevice = {
                    name: this.config.device.name ||
                        String(this.config.device.host || this.config.device.ip)
                            .replace(/\.local$/, '')
                            .replace(/[^a-zA-Z0-9 ]/g, ''),
                    origName: this.config.device.name ||
                        String(this.config.device.host || this.config.device.ip)
                            .replace(/\.local$/, '')
                            .replace(/[^a-zA-Z0-9 ]/g, ''),
                    host: this.config.device.host || this.config.device.ip,
                    port: this.config.device.port || 23,
                    source: 'pluginConfig',
                    fqdn: this.config.device.host || this.config.device.ip,
                };
                if (this.config.device.listeningMode) {
                    addDevice.listeningMode = this.config.device.listeningMode;
                }
                if (this.config.device.listeningModeFallback) {
                    addDevice.listeningModeFallback = this.config.device.listeningModeFallback;
                }
                if (this.config.device.listeningModeOther) {
                    addDevice.listeningModeOther = this.config.device.listeningModeOther;
                }
                if (this.config.device.minVolume) {
                    addDevice.minVolume = this.config.device.minVolume;
                }
                if (this.config.device.maxVolume) {
                    addDevice.maxVolume = this.config.device.maxVolume;
                }
                if (this.config.device.inputSwitches) {
                    addDevice.inputSwitches = this.config.device.inputSwitches;
                }
                this.devicesFound.push(addDevice);
            }
            else if (this.config &&
                (this.config.host || this.config.ip) &&
                String(this.config.host || this.config.ip).length > 0) {
                let name = String(this.config.host || this.config.ip);
                const ip = name.match(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/);
                if (ip && this.config.name) {
                    name = this.config.name;
                }
                const addDevice = {
                    name: name.replace(/\.local$/, '')
                        .replace(/[^a-zA-Z0-9 ]/g, ''),
                    origName: name,
                    host: this.config.host || this.config.ip,
                    port: this.config.port || 23,
                    source: 'pluginConfig',
                    fqdn: this.config.host || this.config.ip,
                };
                this.devicesFound.push(addDevice);
            }
            if (this.devicesFound.length > 0) {
                this.log.debug('Using manually configured device:', this.devicesFound);
            }
            else {
                let attempts = 0;
                // Retry discovery up to MAX_ATTEMPTS times if no devices are found
                while (attempts < this.MAX_ATTEMPTS && this.devicesFound.length === 0) {
                    attempts++;
                    const maxDevices = 5;
                    const discoveredDevices = yield (0, discovery_1.findDevices)(this.TARGET_NAME, this.TELNET_PORTS, this.log, maxDevices);
                    // If devices are found, add them to this.devicesFound and exit loop
                    if (discoveredDevices.length > 0) {
                        for (const dDevice of discoveredDevices) {
                            this.devicesFound.push({
                                name: dDevice.name,
                                origName: dDevice.origName,
                                host: dDevice.host,
                                port: dDevice.port,
                                source: dDevice.source,
                                fqdn: dDevice.fqdn,
                                minVolume: ((_e = (_d = this.config.discoveredDevices) === null || _d === void 0 ? void 0 : _d.find((device) => device.host.toLowerCase() === dDevice.host.toLowerCase())) === null || _e === void 0 ? void 0 : _e.minVolume) || undefined,
                                maxVolume: ((_g = (_f = this.config.discoveredDevices) === null || _f === void 0 ? void 0 : _f.find((device) => device.host.toLowerCase() === dDevice.host.toLowerCase())) === null || _g === void 0 ? void 0 : _g.maxVolume) || undefined,
                                inputSwitches: ((_j = (_h = this.config.discoveredDevices) === null || _h === void 0 ? void 0 : _h.find((device) => device.host.toLowerCase() === dDevice.host.toLowerCase())) === null || _j === void 0 ? void 0 : _j.inputSwitches) || [],
                                listeningMode: ((_l = (_k = this.config.discoveredDevices) === null || _k === void 0 ? void 0 : _k.find((device) => device.host.toLowerCase() === dDevice.host.toLowerCase())) === null || _l === void 0 ? void 0 : _l.listeningMode) || undefined,
                                listeningModeFallback: ((_o = (_m = this.config.discoveredDevices) === null || _m === void 0 ? void 0 : _m.find((device) => device.host.toLowerCase() === dDevice.host.toLowerCase())) === null || _o === void 0 ? void 0 : _o.listeningModeFallback) || undefined,
                                listeningModeOther: ((_q = (_p = this.config.discoveredDevices) === null || _p === void 0 ? void 0 : _p.find((device) => device.host.toLowerCase() === dDevice.host.toLowerCase())) === null || _q === void 0 ? void 0 : _q.listeningModeOther) || undefined,
                            });
                        }
                        this.log.debug('Discovered devices:', this.devicesFound);
                        break;
                    }
                    // Log warning and wait before next attempt if no devices were found
                    this.log.warn(`Attempt ${attempts} of ${this.MAX_ATTEMPTS}: No devices found. Retrying in ${this.RETRY_DELAY / 1000} seconds...`);
                    if (attempts < this.MAX_ATTEMPTS) {
                        yield new Promise((resolve) => setTimeout(resolve, this.RETRY_DELAY));
                    }
                }
                // Log error if all attempts failed and no devices were found
                if (this.devicesFound.length === 0) {
                    this.log.error('No devices found after maximum retry attempts. Please configure manually.');
                    (0, exitHandler_1.addExitHandler)(() => {
                        this.cleanCachedAccessories();
                    }, this);
                    return;
                }
            }
            // Process each device found or manually configured
            // await this.loopDevices(this.devicesFound);
            this.loopDevices(this.devicesFound);
            this.updateConfigSchema(this.devicesFound);
        });
    }
    /**
     * Updates the config.schema.json file with the current device settings and inputs.
     */
    updateConfigSchema(foundDevicesin, host, inputs) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
        // Cache inputs if both host and inputs are provided
        if (host && inputs) {
            this.cachedReceivers.set(host, { inputs });
        }
        // this.log.debug('updateConfigSchema() called', host, inputs);
        if (foundDevicesin.length === 0) {
            return;
        }
        const foundDevices = JSON.parse(JSON.stringify(foundDevicesin));
        try {
            const schemaPath = path.resolve(__dirname, '../config.schema.json');
            if (!fs.existsSync(schemaPath)) {
                this.log.error(`Config schema file not found at path: ${schemaPath}`);
                return;
            }
            const rawSchema = fs.readFileSync(schemaPath, 'utf8');
            let schema;
            try {
                schema = JSON.parse(rawSchema);
            }
            catch (error) {
                this.log.debug('Failed to parse config.schema.json:', error);
                return;
            }
            if (!((_a = schema.schema) === null || _a === void 0 ? void 0 : _a.properties)) {
                this.log.debug('Schema properties are missing in config.schema.json.');
                return;
            }
            if (!schema.schema.properties.devices) {
                schema.schema.properties.devices = {
                    type: 'array', // array
                    title: 'Devices',
                    description: 'Add multiple Pioneer AVR devices to your configuration.',
                    items: {
                        type: 'object',
                        title: 'Device Configuration',
                        properties: {},
                    },
                };
            }
            schema.schema.properties.devices.default = []; // Initialize as an empty array
            schema.schema.properties.devices.minItems = 0;
            const firstDevice = foundDevices[0];
            schema.schema.properties.devices.items.properties.port = {
                type: 'integer',
                title: 'Device Port',
                description: `Enter the port number for the device connection (e.g., 23 or 8102). To open the port, visit: http://${firstDevice.host || 'vsx-922.local'}/1000/port_number.asp`,
                placeholder: firstDevice.port || '23',
            };
            if (firstDevice.source !== 'bonjour' && firstDevice.port) {
                schema.schema.properties.devices.items.properties.port.default =
                    firstDevice.port;
            }
            schema.schema.properties.devices.items.properties.name = {
                type: 'string',
                title: 'Device Name',
                description: 'Enter the name of the device visible in HomeKit.',
                placeholder: String(firstDevice.name || 'VSX922').replace(/[^a-zA-Z0-9]/g, ''),
            };
            if (firstDevice.source !== 'bonjour' && firstDevice.name) {
                schema.schema.properties.devices.items.properties.name.default =
                    firstDevice.name.replace(/(^[^a-zA-Z0-9]+)|([^a-zA-Z0-9]+$)|([^a-zA-Z0-9 '])/g, '');
            }
            schema.schema.properties.devices.items.properties.host = {
                type: 'string',
                title: 'Device IP Address',
                description: 'Enter the IP address or the DNS name of the device (e.g., VSX-922.local).',
                placeholder: firstDevice.host || '192.168.1.99',
            };
            if (firstDevice.source !== 'bonjour' && firstDevice.host) {
                schema.schema.properties.devices.items.properties.host.default =
                    firstDevice.host;
            }
            schema.schema.properties.devices.items.properties.minVolume = {
                type: 'integer',
                title: 'Minimum Volume',
                description: 'Set the minimum volume level (0-100). Overrides global setting.',
                minimum: 0,
                maximum: 100,
                placeholder: this.config.minVolume || 30,
            };
            if (firstDevice.source !== 'bonjour' && firstDevice.minVolume) {
                schema.schema.properties.devices.items.properties.minVolume.default =
                    parseInt(firstDevice.minVolume, 10);
            }
            schema.schema.properties.devices.items.properties.maxVolume = {
                type: 'integer',
                title: 'Maximum Volume Setting (Lightbulb)',
                description: 'Set the maximum volume level (0-100). Overrides global setting.',
                minimum: 0,
                maximum: 100,
                placeholder: this.config.maxVolume || 65,
            };
            if (firstDevice.source !== 'bonjour' && firstDevice.maxVolume) {
                schema.schema.properties.devices.items.properties.maxVolume.default =
                    parseInt(firstDevice.maxVolume, 10);
            }
            let bonjourCounter = 0;
            for (const foundDevice of foundDevices) {
                if (foundDevice.source === 'bonjour') {
                    bonjourCounter++;
                }
            }
            const allInputs = [...this.cachedReceivers.entries()].flatMap(([host, device]) => (device.inputs || []).map((input) => ({
                id: input.id,
                name: input.name,
                host,
            })));
            const groupedById = allInputs.reduce((acc, input) => {
                if (!acc[input.id]) {
                    acc[input.id] = [];
                }
                acc[input.id].push(input);
                return acc;
            }, {});
            const uniqueInputs = Object.values(groupedById).flatMap((inputs) => {
                if (inputs.length === 1) {
                    const input = inputs[0];
                    return [{ id: input.id, name: `${input.name} (${input.id})` }];
                }
                else {
                    return inputs.map((input) => ({
                        id: input.id,
                        name: `${input.name} (${input.id}) ${input.host}`,
                    }));
                }
            }).sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
            const enums = uniqueInputs.map((input) => input.id);
            const enumNames = uniqueInputs.map((input) => input.name);
            let newProperties = {};
            if (enums.length > 0) {
                // Add the 'toggleOffIfActive' field directly under schema.schema.properties
                // schema.schema.properties.toggleOffIfActive = {
                //     title: "Toggle Off If Already Active (For Exposed Input Switches)",
                //     type: "boolean",
                //     default: this.config.toggleOffIfActive ?? true,
                //     description: "If enabled, pressing an input switch that is already active will turn off the receiver. This allows a single button to toggle the receiver on and off, facilitating one-button control in HomeKit. If disabled, the receiver will remain on and simply reselect the current input."
                // };
                // Define the 'toggleOffIfActive' field
                const toggleOffIfActive = {
                    title: "Toggle Off If Already Active (For Exposed Input Switches)",
                    type: "boolean",
                    default: (_b = this.config.toggleOffIfActive) !== null && _b !== void 0 ? _b : true,
                    description: "If enabled, pressing an input switch that is already active will turn off the receiver. This allows a single button to toggle the receiver on and off, facilitating one-button control in HomeKit. If disabled, the receiver will remain on and simply reselect the current input."
                };
                // Remove 'toggleOffIfActive' if it already exists to avoid duplication
                if (schema.schema.properties.toggleOffIfActive) {
                    delete schema.schema.properties.toggleOffIfActive;
                }
                // Reconstruct properties to insert 'toggleOffIfActive' before 'devices'
                newProperties = {};
                for (const key in schema.schema.properties) {
                    if (key === 'devices') {
                        newProperties['toggleOffIfActive'] = toggleOffIfActive;
                    }
                    newProperties[key] = schema.schema.properties[key];
                }
                schema.schema.properties = newProperties;
            }
            else {
                // remove the 'toggleOffIfActive' field if no enums are present
                if (schema.schema.properties.toggleOffIfActive) {
                    delete schema.schema.properties.toggleOffIfActive;
                }
            }
            // Define the 'toggleListeningMode' field
            const toggleListeningMode = {
                title: "Toggle Listening Mode",
                type: "boolean",
                default: (_c = this.config.toggleListeningMode) !== null && _c !== void 0 ? _c : true,
                description: "If enabled, the HomeKit receiver will display a switch that allows you to toggle between two predefined listening modes. If disabled, the switch will not be available in HomeKit."
            };
            // Remove 'toggleListeningMode' if it already exists to avoid duplication
            if (schema.schema.properties.toggleListeningMode) {
                delete schema.schema.properties.toggleListeningMode;
            }
            // Reconstruct properties to insert 'toggleListeningMode' before 'devices'
            newProperties = {};
            for (const key in schema.schema.properties) {
                if (key === 'devices') {
                    newProperties['toggleListeningMode'] = toggleListeningMode;
                }
                newProperties[key] = schema.schema.properties[key];
            }
            schema.schema.properties = newProperties;
            // Define the 'toggleListeningModeLink' field
            const toggleListeningModeLink = {
                title: "Listening Mode Switch Display",
                type: "boolean",
                default: (_d = this.config.toggleListeningModeLink) !== null && _d !== void 0 ? _d : true,
                description: "If enabled, the HomeKit receiver will display the listening mode toggle directly within its view when devices are bundled (default behavior). If disabled, the toggle will appear as a separate switch, independent of the bundling setting."
            };
            // Remove 'toggleListeningModeLink' if it already exists to avoid duplication
            if (schema.schema.properties.toggleListeningModeLink) {
                delete schema.schema.properties.toggleListeningModeLink;
            }
            // Reconstruct properties to insert 'toggleListeningModeLink' before 'devices'
            newProperties = {};
            for (const key in schema.schema.properties) {
                newProperties[key] = schema.schema.properties[key];
                if (key === 'toggleListeningMode') {
                    newProperties['toggleListeningModeLink'] = toggleListeningModeLink;
                }
            }
            schema.schema.properties = newProperties;
            if (bonjourCounter !== foundDevices.length) {
                schema.schema.properties.devices.items.properties.inputSwitches = {
                    type: 'array',
                    title: 'Input Switches to Expose',
                    description: 'Select up to 5 inputs to expose as switches in HomeKit.',
                    items: {
                        type: 'string',
                    },
                    uniqueItems: true,
                    maxItems: 5,
                    minItems: 0,
                    default: [],
                };
                if (enums.length > 0) {
                    // Set the enum and enumNames for inputSwitches
                    schema.schema.properties.devices.items.properties.inputSwitches.items.enum = enums;
                    schema.schema.properties.devices.items.properties.inputSwitches.items.enumNames = enumNames;
                }
                else {
                    // Remove the inputSwitches field if no enums are present
                    if (schema.schema.properties.devices.items.properties.inputSwitches) {
                        delete schema.schema.properties.devices.items.properties.inputSwitches;
                    }
                }
            }
            else if (schema.schema.properties.devices.items.properties.inputSwitches) {
                delete schema.schema.properties.devices.items.properties.inputSwitches;
            }
            if (bonjourCounter > 0) {
                schema.schema.properties.discoveredDevices = {
                    type: 'array',
                    title: 'Discovered Devices',
                    items: {
                        type: 'object',
                        properties: {
                            host: {
                                type: 'string',
                                title: 'Host (IP or DNS Name). Do not edit this value.',
                                readOnly: true,
                            },
                            maxVolume: {
                                type: 'integer',
                                title: 'Maximum Volume Setting (Lightbulb)',
                                description: 'Set the maximum volume level (0-100). Overrides global setting.',
                                minimum: 0,
                                maximum: 100,
                            },
                            minVolume: {
                                type: 'integer',
                                title: 'Minimum Volume',
                                description: 'Set the minimum volume level (0-100). Overrides global setting.',
                                minimum: 0,
                                maximum: 100,
                                placeholder: this.config.minVolume || 30,
                            },
                            listeningMode: {
                                title: 'Primary Listening Mode',
                                type: 'string',
                                placeholder: this.config.listeningMode || '0013',
                                description: 'The default listening mode when the switch in HomeKit is active. Default 0013 PRO LOGIC2 MOVIE',
                            },
                            listeningModeOther: {
                                title: 'Alternative Listening Mode',
                                type: 'string',
                                placeholder: this.config.listeningModeFallback || '0112',
                                description: 'The alternative listening mode that is toggled via HomeKit switch or the iOS Remote app. Default 0112 EXTENDED STEREO',
                            },
                            listeningModeFallback: {
                                title: 'Fallback Listening Mode',
                                type: 'string',
                                placeholder: this.config.listeningModeOther || '0101',
                                description: 'A backup listening mode used when the Primary Listening Mode is unavailable (e.g., due to input signal restrictions). This mode should be **different** from the Primary Listening Mode and should be chosen based on what is likely to be supported. Default 0101 ACTION\nAvailable modes can be found in the list at the bottom of this page.',
                            },
                            inputSwitches: {
                                type: 'array',
                                title: 'Input Switches to Expose',
                                description: 'Select up to 5 inputs to expose as switches in HomeKit.',
                                items: {
                                    type: 'string',
                                },
                                uniqueItems: true,
                                maxItems: 5,
                                default: [],
                            },
                        },
                        required: ['host'],
                    },
                    default: [],
                };
                if (enums.length > 0) {
                    schema.schema.properties.discoveredDevices.items.properties.inputSwitches.items.enum =
                        enums;
                    schema.schema.properties.discoveredDevices.items.properties.inputSwitches.items.enumNames =
                        enumNames;
                }
                else if ((_e = schema.schema.properties.discoveredDevices) === null || _e === void 0 ? void 0 : _e.items.properties.inputSwitches) {
                    delete schema.schema.properties.discoveredDevices.items.properties.inputSwitches;
                }
            }
            else if ((_f = schema.schema.properties.discoveredDevices) === null || _f === void 0 ? void 0 : _f.items.properties.inputSwitches) {
                delete schema.schema.properties.discoveredDevices.items.properties.inputSwitches;
            }
            for (const foundDevice of foundDevices) {
                const cachedInputs = ((_g = this.cachedReceivers.get(foundDevice.host)) === null || _g === void 0 ? void 0 : _g.inputs) || [];
                const deviceInputs = inputs && host && foundDevice.host && host.toLowerCase() === foundDevice.host.toLowerCase() ? inputs : cachedInputs;
                let existingConfigInputSwitches = ((_j = (_h = this.config.discoveredDevices) === null || _h === void 0 ? void 0 : _h.find((device) => device.host.toLowerCase() === foundDevice.host.toLowerCase())) === null || _j === void 0 ? void 0 : _j.inputSwitches) || []; //inputSwitches.slice(0, 3);
                // Validate inputSwitches only if deviceInputs is not empty
                if (deviceInputs.length > 0) {
                    const validInputIds = deviceInputs.map((input) => input.id);
                    // Filter out invalid switches
                    existingConfigInputSwitches = existingConfigInputSwitches.filter((switchId) => validInputIds.includes(switchId));
                }
                const addDevice = {
                    name: foundDevice.name,
                    host: foundDevice.host,
                    port: foundDevice.port,
                    listeningMode: foundDevice.listeningMode || '0013',
                    listeningModeOther: foundDevice.listeningModeOther || '0012',
                    listeningModeFallback: foundDevice.listeningModeFallback || '0101',
                    maxVolume: foundDevice.maxVolume
                        ? parseInt(foundDevice.maxVolume, 10)
                        : undefined,
                    minVolume: foundDevice.minVolume
                        ? parseInt(foundDevice.minVolume, 10)
                        : undefined,
                    inputSwitches: existingConfigInputSwitches,
                };
                foundDevice.inputSwitches = existingConfigInputSwitches;
                if (foundDevice.source === 'bonjour') {
                    schema.schema.properties.discoveredDevices.default.push(addDevice);
                }
                else {
                    schema.schema.properties.devices.default.push(addDevice);
                }
            }
            if (bonjourCounter === 0) {
                if (schema.schema.properties.discoveredDevices) {
                    delete schema.schema.properties.discoveredDevices;
                }
            }
            else {
                const schemaDiscoveredDevices = schema.schema.properties.discoveredDevices;
                schema.schema.properties = Object.assign({ discoveredDevices: schemaDiscoveredDevices }, Object.fromEntries(Object.entries(schema.schema.properties).filter(([key]) => key !== 'discoveredDevices')));
            }
            if (bonjourCounter > 0) {
                // If the property already exists, remove it first
                if (schema.schema.properties.maxReconnectAttemptsBeforeDiscover) {
                    delete schema.schema.properties.maxReconnectAttemptsBeforeDiscover;
                }
                // Create a new properties object to ensure ordering
                const newProperties = {};
                for (const key in schema.schema.properties) {
                    newProperties[key] = schema.schema.properties[key];
                    // After 'maxReconnectAttempts', insert our new property
                    if (key === "maxReconnectAttempts") {
                        newProperties["maxReconnectAttemptsBeforeDiscover"] = {
                            "title": "Maximum Reconnect Attempts Before Discovery",
                            "type": "integer",
                            "default": 10,
                            "minimum": 10,
                            "maximum": 100,
                            "description": "Set the maximum number of reconnect attempts before triggering a device rediscovery process."
                        };
                    }
                }
                // Assign the new properties object back to schema.schema.properties
                schema.schema.properties = newProperties;
            }
            else if (schema.schema.properties.maxReconnectAttemptsBeforeDiscover) {
                delete schema.schema.properties.maxReconnectAttemptsBeforeDiscover;
            }
            const dynamicHost = firstDevice.host || 'vsx-922.local';
            const dynamicHeaderLink = `To open a telnet port on the receiver or set Network Standby, click here: [http://${dynamicHost}/1000/port_number.asp](http://${dynamicHost}/1000/port_number.asp).`;
            if (!schema.headerDisplay) {
                schema.headerDisplay = `# Configuration\n\n${dynamicHeaderLink}`;
            }
            else {
                const regex = /To open a telnet port on the receiver or set Network Standby, click here: \[http:\/\/.*?\/1000\/port_number\.asp\]\(http:\/\/.*?\/1000\/port_number\.asp\)\./;
                schema.headerDisplay = schema.headerDisplay
                    .replace(regex, '')
                    .trim();
                schema.headerDisplay += `\n\n${dynamicHeaderLink}`;
            }
            fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 4), 'utf8');
            this.log.debug('Updated config.schema.json successfully.', schemaPath);
            if (schema.schema.properties.discoveredDevices && schema.schema.properties.discoveredDevices.default.length > 0) {
                try {
                    // Read the existing config.json
                    const rawConfig = fs.existsSync(this.homebridgeConfigPath) ? fs.readFileSync(this.homebridgeConfigPath, 'utf8') : '{}';
                    const config = JSON.parse(rawConfig);
                    if (Array.isArray(config.discoveredDevices)) {
                        delete config.discoveredDevices;
                    }
                    // get config for 'pioneerAvr2025' platform
                    let pioneerPlatform = (_k = config.platforms) === null || _k === void 0 ? void 0 : _k.find((platform) => platform.name === this.platformName);
                    // Ensure the discoveredDevices array exists in the config.json
                    if (!Array.isArray(pioneerPlatform.discoveredDevices)) {
                        pioneerPlatform.discoveredDevices = [];
                    }
                    // Update discoveredDevices in config.json with the default values from schema
                    const discoveredDevicesFromSchema = schema.schema.properties.discoveredDevices.default;
                    const updatedDiscoveredDevices = discoveredDevicesFromSchema.map((device) => ({
                        host: device.host, // Include only host
                    }));
                    // Avoid duplicates by checking for existing devices in config.json
                    let writeConfig = false;
                    updatedDiscoveredDevices.forEach((newDevice) => {
                        if (!pioneerPlatform.discoveredDevices.some((device) => device.host.toLowerCase() === newDevice.host.toLowerCase())) {
                            pioneerPlatform.discoveredDevices.push(newDevice);
                            writeConfig = true;
                        }
                    });
                    // Write back the updated config.json
                    if (writeConfig && Object.keys(config).length > 1) {
                        function sortConfig(config) {
                            const result = {};
                            const isPrimitive = (val) => val === null || ["string", "number", "boolean"].includes(typeof val);
                            const reservedKeys = ["accessories", "platforms", "bridge", "disabledPlugins"];
                            const primitiveKeys = [];
                            // Utility: Sort object keys alphabetically with optional prioritized key first
                            const sortObjectKeys = (obj, prioritizeKey) => {
                                const sorted = {};
                                const keys = Object.keys(obj).sort();
                                if (prioritizeKey && keys.includes(prioritizeKey)) {
                                    sorted[prioritizeKey] = obj[prioritizeKey];
                                }
                                for (const key of keys) {
                                    if (key === prioritizeKey)
                                        continue;
                                    const value = obj[key];
                                    if (Array.isArray(value)) {
                                        sorted[key] = value.map((item) => {
                                            if (item && typeof item === 'object' && !Array.isArray(item)) {
                                                return sortObjectKeys(item);
                                            }
                                            return item;
                                        });
                                    }
                                    else if (value && typeof value === 'object') {
                                        sorted[key] = sortObjectKeys(value);
                                    }
                                    else {
                                        sorted[key] = value;
                                    }
                                }
                                return sorted;
                            };
                            // 1. accessories
                            if (Array.isArray(config.accessories)) {
                                result.accessories = [...config.accessories].sort((a, b) => (a.name || "").localeCompare(b.name || "")).map((_a) => {
                                    var { accessory, name } = _a, rest = __rest(_a, ["accessory", "name"]);
                                    const sortedRest = sortObjectKeys(rest);
                                    return Object.assign({ accessory, name }, sortedRest);
                                });
                            }
                            // 2. platforms
                            if (Array.isArray(config.platforms)) {
                                result.platforms = [...config.platforms].sort((a, b) => {
                                    const platformA = a.platform || "";
                                    const platformB = b.platform || "";
                                    if (platformA === "pioneerAvr2025")
                                        return -1;
                                    if (platformB === "pioneerAvr2025")
                                        return 1;
                                    if (platformA === "config")
                                        return 1;
                                    if (platformB === "config")
                                        return -1;
                                    return platformA.localeCompare(platformB);
                                }).map((platformObj) => {
                                    const { platform, name, _bridge } = platformObj, rest = __rest(platformObj, ["platform", "name", "_bridge"]);
                                    const minMaxPairs = {};
                                    const otherKeys = [];
                                    const keys = Object.keys(rest);
                                    keys.forEach((key) => {
                                        if (key.startsWith("min")) {
                                            const maxKey = key.replace(/^min/, "max");
                                            if (keys.includes(maxKey)) {
                                                minMaxPairs[key] = maxKey;
                                            }
                                            else {
                                                otherKeys.push(key);
                                            }
                                        }
                                        else if (!Object.values(minMaxPairs).includes(key)) {
                                            otherKeys.push(key);
                                        }
                                    });
                                    const orderedKeys = [];
                                    for (const minKey of Object.keys(minMaxPairs).sort()) {
                                        orderedKeys.push(minKey, minMaxPairs[minMaxPairs[minKey]]);
                                    }
                                    otherKeys.sort();
                                    const sortedObj = { platform, name };
                                    for (const key of [...orderedKeys, ...otherKeys]) {
                                        let value = rest[key];
                                        // Special case: discoveredDevices
                                        if (key === "discoveredDevices" && Array.isArray(value)) {
                                            value = value
                                                .sort((a, b) => (a.host || "").localeCompare(b.host || ""))
                                                .map((device) => sortObjectKeys(device, "host"));
                                        }
                                        else if (Array.isArray(value)) {
                                            value = value.map((item) => typeof item === "object" && item !== null && !Array.isArray(item)
                                                ? sortObjectKeys(item)
                                                : item);
                                        }
                                        else if (typeof value === "object" && value !== null) {
                                            value = sortObjectKeys(value);
                                        }
                                        sortedObj[key] = value;
                                    }
                                    if (_bridge) {
                                        sortedObj._bridge = sortObjectKeys(_bridge);
                                    }
                                    return sortedObj;
                                });
                            }
                            // 3. Other config keys (non-reserved)
                            for (const key of Object.keys(config)) {
                                if (reservedKeys.includes(key))
                                    continue;
                                const value = config[key];
                                if (isPrimitive(value)) {
                                    primitiveKeys.push(key);
                                }
                                else {
                                    result[key] = value;
                                }
                            }
                            // 4. bridge
                            if (config.bridge) {
                                result.bridge = config.bridge;
                            }
                            // 5. disabledPlugins
                            if (config.disabledPlugins) {
                                result.disabledPlugins = config.disabledPlugins;
                            }
                            // 6. primitives (like insecure: true)
                            for (const key of primitiveKeys.sort()) {
                                result[key] = config[key];
                            }
                            return result;
                        }
                        fs.writeFileSync(this.homebridgeConfigPath, JSON.stringify(sortConfig(config), null, 4), 'utf8');
                        this.log.debug('Successfully updated discoveredDevices in config.json.', this.homebridgeConfigPath);
                    }
                }
                catch (error) {
                    this.log.error('Failed to update config.json with discoveredDevices:', error);
                }
            }
        }
        catch (error) {
            this.log.error('Failed to update config.schema.json: ', error);
        }
    }
    /**
     * Processes each discovered or manually configured device asynchronously.
     * Registers or restores devices in Homebridge as necessary.
     *
     * @param foundDevices - List of devices found during discovery or configured manually
     */
    loopDevices(foundDevices) {
        return __awaiter(this, void 0, void 0, function* () {
            for (const foundDevice of foundDevices) {
                // this.log.debug('Processing device:', foundDevice);
                try {
                    // Generate a unique name to avoid duplicate accessory names in Homebridge
                    let uniqueName = foundDevice.name.replace(/(^[^a-zA-Z0-9]+)|([^a-zA-Z0-9]+$)|([^a-zA-Z0-9 '])/g, '');
                    let counter = 1;
                    if (foundDevices.length > 1) {
                        // Check if another accessory with the same name already exists
                        while (foundDevices.some((fd) => fd.name === uniqueName)) {
                            uniqueName = `${foundDevice.name}_${counter}`;
                            counter++;
                        }
                    }
                    // Log the renaming if necessary
                    if (uniqueName !== foundDevice.name.replace(/(^[^a-zA-Z0-9]+)|([^a-zA-Z0-9]+$)|([^a-zA-Z0-9 '])/g, '')) {
                        this.log.warn(`Device with name '${foundDevice.name.replace(/(^[^a-zA-Z0-9]+)|([^a-zA-Z0-9]+$)|([^a-zA-Z0-9 '])/g, '')}' already exists. Renaming to '${uniqueName}'.`);
                    }
                    // Generate a unique identifier (UUID) for the accessory based on device information
                    const uuid = this.api.hap.uuid.generate(String(uniqueName) + String(foundDevice.host));
                    // Check if an accessory with this UUID is already registered in Homebridge
                    const existingAccessory = this.accessories.find((accessory) => accessory.UUID === uuid);
                    if (existingAccessory) {
                        // Restore the existing accessory from cache
                        this.log.debug('Restoring existing accessory from cache:', existingAccessory.displayName);
                        new pioneer_avr_accessory_js_1.default(foundDevice, this, existingAccessory);
                    }
                    else {
                        this.log.debug('Adding new accessory:', uniqueName, foundDevice);
                        // Initialize a new accessory instance with the unique name and set device context
                        const accessory = new this.api.platformAccessory(uniqueName, uuid);
                        accessory.context.device = Object.assign(Object.assign({}, foundDevice), { name: uniqueName });
                        // Initialize the accessory and wait for it to be ready
                        new pioneer_avr_accessory_js_1.default(foundDevice, this, accessory);
                        // Register the accessory with Homebridge once it is fully initialized
                        this.api.registerPlatformAccessories(this.pluginName, this.platformName, [accessory]);
                    }
                }
                catch (e) {
                    this.log.debug('Error processing device in loopDevices:', e);
                }
            }
        });
    }
}
exports.PioneerAvrPlatform = PioneerAvrPlatform;
//# sourceMappingURL=pioneer-avr-platform.js.map