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).

391 lines 21.3 kB
"use strict"; // src/pioneer-avr/inputs.ts 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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InputManagementMixin = InputManagementMixin; const fs_1 = __importDefault(require("fs")); // For file system operations const path_1 = __importDefault(require("path")); // For handling file paths const exitHandler_1 = require("../exitHandler"); let HAPStorage; try { HAPStorage = require('hap-nodejs').HAPStorage; } catch (error) { HAPStorage = {}; } /** * This mixin adds input management methods to a base class, including input setup, handling, * and monitoring functionality. * @param Base - The base class to extend with input management methods. * @returns A new class that extends the base class with added input handling capabilities. */ function InputManagementMixin(Base) { return class extends Base { constructor(...args) { var _a, _b, _c, _d, _e; super(...args); this.prefsDir = ''; this.inputCacheFile = ''; this.inputBeingAdded = false; this.inputBeingAddedWaitCount = 0; this.inputMissing = []; this.inputs = []; this.enabledServices = []; this.inputToType = {}; this.pioneerAvrClassCallbackCalled = false; this.initCount = 0; const storagePath = typeof HAPStorage.storagePath === 'function' ? HAPStorage.storagePath() // Homebridge v2+ : (_c = (_b = (_a = this.platform) === null || _a === void 0 ? void 0 : _a.api) === null || _b === void 0 ? void 0 : _b.user) === null || _c === void 0 ? void 0 : _c.storagePath(); // Homebridge v1 // Set `prefsDir` and `inputCacheFile` this.prefsDir = ((_e = (_d = this.platform) === null || _d === void 0 ? void 0 : _d.config) === null || _e === void 0 ? void 0 : _e.prefsDir) || storagePath + '/pioneerAvr/'; this.inputCacheFile = path_1.default.join(this.prefsDir, `inputCache_${this.device.host}.json`); this.inputs = []; // Ensure the preferences directory exists if (!fs_1.default.existsSync(this.prefsDir)) { fs_1.default.mkdirSync(this.prefsDir, { recursive: true }); } // Map boolean -> HomeKit visibility state this.booleanToVisibilityState = (visible) => visible ? 0 : 1; // Map HomeKit visibility state -> boolean this.visibilityStateToBoolean = (state) => state === 0; let disconnectTime = null; this.telnetAvr.addOnDisconnectCallback(() => __awaiter(this, void 0, void 0, function* () { disconnectTime = Date.now(); })); // Add a callback to manage inputs when the Telnet connection is established this.telnetAvr.addOnConnectCallback(() => __awaiter(this, void 0, void 0, function* () { if (disconnectTime === null || ((Date.now() - disconnectTime) > (30 * 60 * 1000))) { // refresh inputs setTimeout(() => { this.loadInputs(() => __awaiter(this, void 0, void 0, function* () { // Callback from pioneerAvr class if (!this.pioneerAvrClassCallbackCalled) { this.pioneerAvrClassCallbackCalled = true; yield new Promise((resolve) => setTimeout(resolve, 50)); setTimeout(() => { var _a; try { const runThis = (_a = this.pioneerAvrClassCallback) === null || _a === void 0 ? void 0 : _a.bind(this); if (runThis) { runThis(); } } catch (e) { this.log.debug('pioneerAvrClassCallback() inputs.ts Error', e); } this.__updateInput(() => { }); }, 1500); } else { this.__updateInput(() => { }); } })); }, disconnectTime === null ? 0 : (35 * 1000)); } })); (0, exitHandler_1.addExitHandler)(() => { if (this.saveInputs) { this.saveInputs(); } if (this.saveInputsTimeout) { clearTimeout(this.saveInputsTimeout); } }, this); } /** * Loads all available inputs, either from cache or by refreshing. * @param callback - Optional callback function to run after loading inputs. */ loadInputs(callback) { return __awaiter(this, void 0, void 0, function* () { let refresh = false; if (this.isReady) { if (this.inputs.length > 0) { refresh = true; } else { refresh = false; } } const savedVisibility = {}; // Check if cached inputs are valid if (fs_1.default.existsSync(this.inputCacheFile)) { try { const cache = JSON.parse(fs_1.default.readFileSync(this.inputCacheFile, 'utf-8')); let cacheTimestamp = null; const now = new Date(); if (cache.timestamp) { cacheTimestamp = new Date(cache.timestamp); } if ('inputs' in cache && Array.isArray(cache.inputs) && cache.inputs.length > 0) { for (const [index, input] of cache.inputs.entries()) { if (input.visible !== undefined) { savedVisibility[input.id] = cache.inputs[index].visible === true; } } } // Use cached inputs if they are less than 30 minutes old if (!refresh && cacheTimestamp !== null && cache.inputs.length > 0 && now.getTime() - cacheTimestamp.getTime() <= 30 * 60 * 1000) { this.inputs = cache.inputs.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10)); this.log.info('Load inputs from cache.', this.device.host); setTimeout(() => __awaiter(this, void 0, void 0, function* () { while (!this.accessory.tvService || !this.accessory.enabledServices.includes(this.accessory.tvService)) { yield new Promise((resolve) => setTimeout(resolve, 50)); } // Sort inputs by `id`, assign original index, reverse order, and iterate const sortedInputs = [...this.inputs] .sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10)) // Sort inputs numerically by `id` .map((input, index) => (Object.assign(Object.assign({}, input), { originalIndex: index }))) // Assign original index .sort((a, b) => a.name.localeCompare(b.name)); for (const [_sortedIndex, input] of sortedInputs.entries()) { // Map input ID to its type this.inputToType[input.id] = input.type; // Add the input source service for the current input // The originalIndex reflects the position in the sorted order (before reversing) yield this.addInputSourceService(null, input.originalIndex); // this.log.debug('addInputSourceService called', input.name, input.originalIndex); // Add a small delay between adding inputs to prevent potential issues yield new Promise((resolve) => setTimeout(resolve, 100)); } }), 100); this.isReady = true; yield this.platform.updateConfigSchema(this.platform.devicesFound, this.device.host, this.inputs); this.accessory.handleInputSwitches(); if (callback) { callback(); } return; } } catch (err) { this.log.debug('Failed to load cached inputs:', err); } } // Refresh inputs if no valid cache is available if (!refresh) { this.log.info('Getting inputs...', this.device.host); this.inputs = []; } else { this.log.info('Refreshing inputs...', this.device.host); } for (let i = 1; i <= 60; i++) { const key = i.toString().padStart(2, '0'); // This line maps input IDs (keyed by `i`) to their corresponding `Characteristic.InputSourceType` values. // The mapping was intentionally compressed to save space, even though it sacrifices readability. // Previously, the mapping was more explicit, resembling a structured list, such as: // inputToTypeList = [['25', 3], ['04', 0], ...]. // However, since the `Characteristic.InputSourceType.*` values are not visible in the Home app, // this simplified and compact version was deemed sufficient. // // Explanation of the compact logic: // - Input IDs [2, 18, 38] are mapped to type `2` (e.g., Tuner or NETRADIO). // - Input IDs [19, 20, 21, ..., 15] are mapped to type `3` (e.g., HDMI). // - Specific individual mappings: // - ID `10` -> type `4` (Composite Video) // - ID `14` -> type `6` (Component Video) // - ID `17` -> type `9` (USB/iPod) // - ID `26` -> type `10` (Application) // - ID `46` -> type `8` (Airplay) // - Any input ID not listed defaults to type `0` (Other). // // While this compact approach minimizes code size, it retains functionality and ensures that each // input ID is correctly assigned a `Characteristic.InputSourceType` value. this.inputToType[key] = [2, 18, 38].includes(i) ? 2 : [ 19, 20, 21, 22, 23, 24, 25, 26, 31, 5, 6, 15, ].includes(i) ? 3 : i == 10 ? 4 : i == 14 ? 6 : i == 17 ? 9 : i == 26 ? 10 : i == 46 ? 8 : 0; if (typeof this.inputBeingAdded === 'string' && this.inputBeingAddedWaitCount++ < 90) { yield new Promise((resolve) => setTimeout(resolve, 10)); this.inputBeingAddedWaitCount = 0; while (typeof this.inputBeingAdded === 'string' && this.inputBeingAddedWaitCount++ < 90) { yield new Promise((resolve) => setTimeout(resolve, 50)); } } this.inputBeingAdded = String(key); let index = -1; for (const i in this.inputMissing) { if (this.inputMissing[i].includes(key)) { index = parseInt(i, 10); break; } } if (index === -1) { this.inputMissing.push([key]); } yield this.telnetAvr.sendMessage(`?RGB${key}`, `RGB${key}`, () => { }); yield new Promise((resolve) => setTimeout(resolve, 55)); } if (this.inputMissing.length === 0 && Object.keys(this.inputToType).length > 0) { this.inputs = this.inputs.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10)); try { for (const [index, input] of this.inputs.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10)).entries()) { // Only set type if it hasn't been already defined if (this.inputs[index].type === undefined) { this.inputs[index].type = this.inputToType[input.id] || 0; } // Only set visibility if it hasn't been already defined if (this.inputs[index].visible === undefined) { this.inputs[index].visible = input.id in savedVisibility ? savedVisibility[input.id] : true; // Default to visible } } if (!refresh) { setTimeout(() => __awaiter(this, void 0, void 0, function* () { while (!this.accessory.tvService || !this.accessory.enabledServices.includes(this.accessory.tvService)) { yield new Promise((resolve) => setTimeout(resolve, 50)); } // Sort inputs by `id`, assign original index, reverse order, and iterate const sortedInputs = [...this.inputs] .sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10)) // Sort inputs numerically by `id` .map((input, index) => (Object.assign(Object.assign({}, input), { originalIndex: index }))) // Assign original index .sort((a, b) => a.name.localeCompare(b.name)); for (const [_sortedIndex, input] of sortedInputs.entries()) { // Map input ID to its type // this.inputToType[input.id] = input.type; // Add the input source service for the current input // The originalIndex reflects the position in the sorted order (before reversing) yield this.addInputSourceService(null, input.originalIndex); // this.log.debug('addInputSourceService called', input.name, input.originalIndex); // Add a small delay between adding inputs to prevent potential issues yield new Promise((resolve) => setTimeout(resolve, 100)); } }), 100); } // Save inputs to the cache file if (this.saveInputsTimeout) { clearTimeout(this.saveInputsTimeout); } this.saveInputs(); } catch (error) { // Catch and log any unexpected errors during processing this.log.debug('Error processing input visibility file:', error); } this.isReady = true; yield this.platform.updateConfigSchema(this.platform.devicesFound, this.device.host, this.inputs); if (!refresh) { this.accessory.handleInputSwitches(); } } if (callback) { callback(); } }); } /** * Saves the current inputs to the cache file. */ saveInputs() { try { if (this.inputs.length > 0) { // Write inputs and a timestamp to the cache file fs_1.default.writeFileSync(this.inputCacheFile, JSON.stringify({ timestamp: new Date().toISOString(), // Add a timestamp for tracking inputs: this.inputs, // Save the current inputs })); this.log.debug('Inputs saved to cache file [' + this.inputCacheFile + '].'); // Log success message } } catch (error) { // Log any errors encountered during the file write operation this.log.error('Failed to save inputs to cache file:', error); } } /** * Retrieves the current input status. * @param callback - The callback function to handle the input status result. */ inputStatus(callback) { if (!this.telnetAvr || !this.telnetAvr.connectionReady || !this.state.on || this.state.input === null) { callback(null, this.state.input || 0); return; } // this.log.debug('inputStatus updated %s', this.state.input); try { callback(null, this.state.input); } catch (e) { this.log.debug('inputStatus callback error', e); } } /** * Sets a specific input on the AVR. * @param id - The identifier of the input to set. */ setInput(id) { this.lastUserInteraction = Date.now(); const inputIndex = this.inputs.findIndex((findInput) => findInput.id === id); if (this.state.input === inputIndex) { return; } this.telnetAvr.sendMessage(`${id}FN`); } /** * Renames an existing input on the AVR. * @param id - The identifier of the input to rename. * @param newName - The new name to assign to the input. */ renameInput(id, newName) { return __awaiter(this, void 0, void 0, function* () { if (!this.telnetAvr || !this.telnetAvr.connectionReady || !this.state.on) { return; } const shrinkName = newName .replace(/[^\p{L}\p{N} /:._-]/gu, '') .substring(0, 14); this.telnetAvr.sendMessage(`${shrinkName}1RGB${id}`); }); } /** * Placeholder for adding an input source service; should be overridden externally. */ addInputSourceService(_error, _key) { // Placeholder method, should be externally overwritten } /** * Placeholder for setting the power state of the AVR. * Intended to be overridden externally. */ functionSetActiveIdentifier(_set) { // Placeholder logic for setting power state, should be overridden externally } /** * Updates the current input on the AVR. * @param callback - The callback function to execute after updating. */ __updateInput(callback) { return __awaiter(this, void 0, void 0, function* () { this.telnetAvr.sendMessage('?F', 'FN', callback); }); } }; } //# sourceMappingURL=inputs.js.map