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
JavaScript
"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