UNPKG

homebridge-zidoo-androidtv

Version:
2,521 lines 125 kB
"use strict";
const PLATFORM_NAME = 'zidooPlugin';
const PLUGIN_NAME = 'homebridge-zidoo-androidtv';
const WOL = require('wol');
const request = require('http');
const udp = require('dgram');
const STATELESS_SWITCH_CONFIGS = [
    { configKey: 'cursorUpB', propertyName: 'cursorUp', serviceName: 'Cursor Up', uniqueId: 'CataNicoGaTa-31', commandName: 'CURSOR UP' },
    { configKey: 'cursorDownB', propertyName: 'cursorDown', serviceName: 'Cursor Down', uniqueId: 'CataNicoGaTa-32', commandName: 'CURSOR DOWN' },
    { configKey: 'cursorLeftB', propertyName: 'cursorLeft', serviceName: 'Cursor Left', uniqueId: 'CataNicoGaTa-33', commandName: 'CURSOR LEFT' },
    { configKey: 'cursorRightB', propertyName: 'cursorRight', serviceName: 'Cursor Right', uniqueId: 'CataNicoGaTa-34', commandName: 'CURSOR RIGHT' },
    { configKey: 'cursorEnterB', propertyName: 'cursorEnter', serviceName: 'Cursor Enter', uniqueId: 'CataNicoGaTa-35', commandName: 'CURSOR ENTER' },
    { configKey: 'menuB', propertyName: 'menu', serviceName: 'Menu', uniqueId: 'CataNicoGaTa-36', commandName: 'MENU' },
    { configKey: 'backButtonB', propertyName: 'backButton', serviceName: 'Back', uniqueId: 'CataNicoGaTa-37', commandName: 'BACK' },
    { configKey: 'homeMenuB', propertyName: 'homeMenu', serviceName: 'Home Menu', uniqueId: 'CataNicoGaTa-43', commandName: 'HOME MENU' },
    { configKey: 'infoB', propertyName: 'infoButton', serviceName: 'Info', uniqueId: 'CataNicoGaTa-44', commandName: 'INFO' },
    { configKey: 'pageUpB', propertyName: 'pageUp', serviceName: 'Page Up', uniqueId: 'CataNicoGaTa-50', commandName: 'PAGE UP' },
    { configKey: 'pageDownB', propertyName: 'pageDown', serviceName: 'Page Down', uniqueId: 'CataNicoGaTa-51', commandName: 'PAGE DOWN' },
    { configKey: 'popUpMenuB', propertyName: 'popUpMenu', serviceName: 'Pop-Up Menu', uniqueId: 'CataNicoGaTa-52', commandName: 'POP-UP MENU' },
    { configKey: 'mediaButtons', propertyName: 'previous', serviceName: 'Previous', uniqueId: 'CataNicoGaTa-38', commandName: 'PREVIOUS' },
    { configKey: 'mediaButtons', propertyName: 'next', serviceName: 'Next', uniqueId: 'CataNicoGaTa-39', commandName: 'NEXT' },
    { configKey: 'mediaButtons', propertyName: 'rewindButton', serviceName: 'Rewind', uniqueId: 'CataNicoGaTa-46', commandName: 'REWIND' },
    { configKey: 'mediaButtons', propertyName: 'forwardButton', serviceName: 'Forward', uniqueId: 'CataNicoGaTa-80', commandName: 'FORWARD' },
    { configKey: 'redB', propertyName: 'red', serviceName: 'Red', uniqueId: 'CataNicoGaTa-53', commandName: 'RED' },
    { configKey: 'greenB', propertyName: 'green', serviceName: 'Green', uniqueId: 'CataNicoGaTa-54', commandName: 'GREEN' },
    { configKey: 'blueB', propertyName: 'blue', serviceName: 'Blue', uniqueId: 'CataNicoGaTa-55', commandName: 'BLUE' },
    { configKey: 'yellowB', propertyName: 'yellow', serviceName: 'Yellow', uniqueId: 'CataNicoGaTa-56', commandName: 'YELLOW' },
    { configKey: 'audioB', propertyName: 'audio', serviceName: 'Audio', uniqueId: 'CataNicoGaTa-57', commandName: 'AUDIO' },
    { configKey: 'subtitleB', propertyName: 'subtitle', serviceName: 'Subtitle', uniqueId: 'CataNicoGaTa-58', commandName: 'SUBTITLE' },
    { configKey: 'repeatB', propertyName: 'repeat', serviceName: 'Repeat', uniqueId: 'CataNicoGaTa-63', commandName: 'REPEAT' },
    { configKey: 'pipB', propertyName: 'pip', serviceName: 'PIP', uniqueId: 'CataNicoGaTa-64', commandName: 'PIP' },
    { configKey: 'resolutionB', propertyName: 'resolution', serviceName: 'Resolution', uniqueId: 'CataNicoGaTa-65', commandName: 'RESOLUTION' },
    { configKey: 'recordB', propertyName: 'record', serviceName: 'Record', uniqueId: 'CataNicoGaTa-9002', commandName: 'RECORD' },
    { configKey: 'movieB', propertyName: 'movie', serviceName: 'Movie', uniqueId: 'CataNicoGaTa-9003', commandName: 'MOVIE' },
    { configKey: 'musicB', propertyName: 'music', serviceName: 'Music', uniqueId: 'CataNicoGaTa-9004', commandName: 'MUSIC' },
    { configKey: 'photoB', propertyName: 'photo', serviceName: 'Photo', uniqueId: 'CataNicoGaTa-9005', commandName: 'PHOTO' },
    { configKey: 'fileB', propertyName: 'file', serviceName: 'File', uniqueId: 'CataNicoGaTa-9006', commandName: 'FILE' },
    { configKey: 'lightB', propertyName: 'light', serviceName: 'Light', uniqueId: 'CataNicoGaTa-9007', commandName: 'LIGHT' },
    { configKey: 'screenshotB', propertyName: 'screenshot', serviceName: 'Screenshot', uniqueId: 'CataNicoGaTa-9008', commandName: 'SCREENSHOT' },
    { configKey: 'appSwitchB', propertyName: 'appSwitch', serviceName: 'App Switch', uniqueId: 'CataNicoGaTa-9009', commandName: 'APPSWITCH' },
    { configKey: 'rebootB', propertyName: 'rebootB', serviceName: 'Reboot', uniqueId: 'CataNicoGaTa-X09', commandName: 'REBOOT' },
];
const COMMAND_NAME_MATCHES = [
    { token: 'Poweroff', label: 'Power Off' },
    { token: 'Standby', label: 'Standby' },
    { token: 'Reboot', label: 'Reboot' },
    { token: 'VolumeUp', label: 'Volume Up' },
    { token: 'VolumeDown', label: 'Volume Down' },
    { token: 'PowerOn', label: 'Power On' },
    { token: 'Up', label: 'Cursor Up' },
    { token: 'Down', label: 'Cursor Down' },
    { token: 'Left', label: 'Cursor Left' },
    { token: 'Right', label: 'Cursor Right' },
    { token: 'Ok', label: 'Enter' },
    { token: 'Menu', label: 'Menu' },
    { token: 'Back', label: 'Back' },
    { token: 'MediaPlay.Pause', label: 'Play/Pause' },
    { token: 'MediaPlay', label: 'Play' },
    { token: 'MediaPause', label: 'Pause' },
    { token: 'MediaStop', label: 'Stop' },
    { token: 'MediaPrev', label: 'Previous Chapter' },
    { token: 'MediaNext', label: 'Next Chapter' },
    { token: 'Home', label: 'Home Menu' },
    { token: 'Info', label: 'Information' },
    { token: 'MediaBackward', label: 'Rewind' },
    { token: 'MediaForward', label: 'Forward' },
    { token: 'PageUP', label: 'Page Up' },
    { token: 'PageDown', label: 'Page Down' },
    { token: 'PopMenu', label: 'Pop-Up Menu' },
    { token: 'UserDefine_A', label: 'Red' },
    { token: 'UserDefine_B', label: 'Green' },
    { token: 'UserDefine_D', label: 'Blue' },
    { token: 'UserDefine_C', label: 'Yellow' },
    { token: 'Audio', label: 'Audio' },
    { token: 'Subtitle', label: 'Subtitle' },
    { token: 'Repeat', label: 'Repeat' },
    { token: 'Pip', label: 'PIP' },
    { token: 'Resolution', label: 'Resolution' },
    { token: 'Mute', label: 'Mute' },
    { token: 'Record', label: 'Record' },
    { token: 'movie', label: 'Movie' },
    { token: 'music', label: 'Music' },
    { token: 'photo', label: 'Photo' },
    { token: 'file', label: 'File' },
    { token: 'light', label: 'Light' },
    { token: 'Screenshot', label: 'Screenshot' },
    { token: 'APP.Switch', label: 'App Switch' },
    { token: 'getModel', label: 'Get Model Information' },
    { token: 'getState', label: 'Music Information' },
    { token: 'getPlayStatus', label: 'Movie Information' },
    { token: 'changeVolume', label: 'Volume Status' },
    { token: 'seek', label: 'Searching' },
];
const BUTTON_KEY_SUFFIXES = {
    'POWER ON': 'PowerOn',
    'REBOOT': 'PowerOn.Reboot',
    'CURSOR UP': 'Up',
    'CURSOR DOWN': 'Down',
    'CURSOR LEFT': 'Left',
    'CURSOR RIGHT': 'Right',
    'CURSOR ENTER': 'Ok',
    'MENU': 'Menu',
    'BACK': 'Back',
    'PLAY': 'MediaPlay',
    'PLAY/PAUSE': 'MediaPlay.Pause',
    'PAUSE': 'MediaPause',
    'STOP': 'MediaStop',
    'PREVIOUS': 'MediaPrev',
    'NEXT': 'MediaNext',
    'HOME MENU': 'Home',
    'INFO': 'Info',
    'REWIND': 'MediaBackward',
    'FORWARD': 'MediaForward',
    'PAGE UP': 'PageUP',
    'PAGE DOWN': 'PageDown',
    'POP-UP MENU': 'PopMenu',
    'RED': 'UserDefine_A',
    'GREEN': 'UserDefine_B',
    'BLUE': 'UserDefine_D',
    'YELLOW': 'UserDefine_C',
    'AUDIO': 'Audio',
    'SUBTITLE': 'Subtitle',
    'REPEAT': 'Repeat',
    'PIP': 'Pip',
    'RESOLUTION': 'Resolution',
    'VOLUME UP': 'VolumeUp',
    'VOLUME DOWN': 'VolumeDown',
    'MUTE': 'Mute',
    'RECORD': 'Record',
    'MOVIE': 'movie',
    'MUSIC': 'music',
    'PHOTO': 'photo',
    'FILE': 'file',
    'LIGHT': 'light',
    'SCREENSHOT': 'Screenshot',
    'APPSWITCH': 'APP.Switch',
};
module.exports = (api) => {
    api.registerPlatform(PLATFORM_NAME, zidooPlatform);
};
//// Platform/////////////////////////////////////////////////////////////////////////////////////////////////
class zidooPlatform {
    // Stores plugin configuration and delays accessory creation until Homebridge is fully launched.
    constructor(log, config, api) {
        if (!config) {
            return;
        }
        this.log = log;
        this.config = config;
        this.api = api;
        if (this.api) {
            this.api.on('didFinishLaunching', this.initDevices.bind(this));
        }
        /*
             this.Service = this.api.hap.Service;
                this.Characteristic = this.api.hap.Characteristic;
                this.config.name = this.config.name || 'Zidoo AndroidTV';
                this.config.newPlatformUUID = this.config.newPlatformUUID || false;
                // this is used to track restored cached accessories
                this.accessories = [];
                this.log.debug('Finished initializing platform:', this.config.name);
                this.api.on('didFinishLaunching', () => {
                    log.debug('didFinishLaunching callback');
                    this.iniDevice();
                });
        */

    }
    // Restores cached accessories so Homebridge can reuse persisted metadata on restart.
    configureAccessory(accessory) {
        this.log.info('Loading accessory from cache:', accessory.displayName);
        this.accessories.push(accessory);
    }
    // Builds and publishes one external accessory per configured Zidoo device.
    initDevices() {

        this.log.info('Init - initializing devices');

        // read from config.devices
        if (this.config.devices && Array.isArray(this.config.devices)) {
            //this.log.info(this.config.devices.length);
            let deviceNumber = this.config.devices.length;
            let i = 0;
            for (let device of this.config.devices) {
                if (device) {
                    this.config = device
                    this.config.numberOfDevices = deviceNumber;
                    this.Service = this.api.hap.Service;
                    this.Characteristic = this.api.hap.Characteristic;
                    this.config.name = this.config.name || 'Zidoo AndroidTV';
                    this.config.newPlatformUUID = this.config.newPlatformUUID || false;
                    // this is used to track restored cached accessories
                    this.accessories = [];
                    /////
                    if (i === 0) {
                        if (this.config.newPlatformUUID === false) {
                            this.zidooDevice =
                            {
                                zidooUniqueId: 'nicocate',
                                zidooDisplayName: `${this.config.name}`
                            };
                        }
                        else {
                            this.zidooDevice =
                            {
                                zidooUniqueId: `${this.config.name}catanico`,
                                zidooDisplayName: `${this.config.name}`
                            };
                        }
                    }
                    else {
                        if (this.config.newPlatformUUID === false) {
                            this.zidooDevice =
                            {
                                zidooUniqueId: `${i}nicocata`,
                                zidooDisplayName: `${this.config.name}`
                            };
                        }
                        else {
                            this.zidooDevice =
                            {
                                zidooUniqueId: `${i}${this.config.name}catanico`,
                                zidooDisplayName: `${this.config.name}`
                            };
                        }
                    }
                    i += 1;
                    /////////

                    this.log.debug('Generationg a new UUID');
                    const uuid = this.api.hap.uuid.generate(this.zidooDevice.zidooUniqueId);
                    this.log.debug('Adding new accessory:', this.zidooDevice.zidooDisplayName);
                    const accessory = new this.api.platformAccessory(this.zidooDevice.zidooDisplayName, uuid);
                    accessory.category = 35;
                    accessory.context.device = this.zidooDevice;
                    new zidooAccessory(this, accessory);
                    this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);
                }
            }
        } else if (this.config) {
            this.log.info('The devices property is not of type array. Cannot initialize. Type: %s', typeof this.config.devices);
        }
        if (!this.config) {
            this.log.info('-------------------------------------------');
            this.log.info('Init - No device configuration found');
            this.log.info('Missing devices in your platform config');
            this.log.info('-------------------------------------------');
        }

        /*
        
                if (this.config.newPlatformUUID === false) {
                    this.zidooDevice =
                    {
                        zidooUniqueId: 'nicocate',
                        zidooDisplayName: `${this.config.name}`
                    };
                }
                if (this.config.newPlatformUUID === true) {
                    this.zidooDevice =
                    {
                        zidooUniqueId: `${this.config.name}catanico`,
                        zidooDisplayName: `${this.config.name}`
                    };
                    this.log.debug('Generationg a new UUID');
                }
                const uuid = this.api.hap.uuid.generate(this.zidooDevice.zidooUniqueId);
                this.log.debug('Adding new accessory:', this.zidooDevice.zidooDisplayName);
                const accessory = new this.api.platformAccessory(this.zidooDevice.zidooDisplayName, uuid);
                accessory.category = this.api.hap.Accessory.Categories.TV_SET_TOP_BOX;
                accessory.context.device = this.zidooDevice;
                new zidooAccessory(this, accessory);
                this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);
        
        */


    }
    // Removes a published accessory from Homebridge.
    removeAccessory(accessory) {
        this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
    }
}
class zidooAccessory {
    // Initializes accessory state, creates HomeKit services, and starts polling/UDP discovery workflows.
    constructor(platform, accessory) {

        this.platform = platform;
        this.accessory = accessory;
        this.config = platform.config;
        this.ZIDOO_IP = this.config.ip;
        this.ZIDOO_PORT = 9529;
        this.ZIDOO_UDP_PORT = 18239;
        this.statelessTimeOut = 1000;
        this.turnOffCommand = false;
        this.moviePlaying = false;
        this.musicPlaying = false;
        //////Initial Switch and sensors state///////////////////////////////////////////////////////////////////////////////////////////////
        this.powerState = false;
        this.playBackState = [false, false, false];
        this.inputState = [false, false, false, false, false, false];
        this.powerStateTV = 0;
        this.currentVolume = 0;
        this.currentMuteState = false;
        this.currentVolumeSwitch = false;
        this.maxVolume = 15;
        this.volumeBeforeMute = 100;
        this.isMute = false;
        this.isMuteAvailable = false;
        this.inputID = 1;
        this.mediaState = 4;
        this.videoState = false;
        this.audioState = false;
        this.inputName = 'Media Title';
        this.mediaDuration = 'Runtime';
        this.subtitleInfo = 'Additional Information';
        this.mediaVideoFormat = 'Video Information';
        this.mediaAudioFormat = 'Audio Information';
        this.language = 'Audio Language';
        this.IPReceived = false;
        this.macReceived = false;
        this.showState = false;
        this.httpNotResponding = 0;
        /////MovieConstants
        this.currentMovieProgress = 0;
        this.currentMovieProgressState = false;
        this.movieElapsed = 0;
        this.movieDuration = 0;
        ////Connection parameters
        this.reconnectionTry = 2;
        this.standbyMode = 0;
        this.deviceAvailableForPolling = false;
        this.availabilityProbeInFlight = false;
        this.nextAvailabilityProbeAt = 0;
        this.waitingForDeviceAvailability = false;
        this.availabilityProbeIntervalMs = 5000;
        this.managedTimeouts = new Set();
        this.managedIntervals = new Set();
        this.pollInFlight = false;
        // Reuse sockets across requests to reduce TCP handshake overhead.
        this.httpAgent = new request.Agent({ keepAlive: true, maxSockets: 8 });
        //Device Information//////////////////////////////////////////////////////////////////////////////////////
        this.config.name = platform.config.name || 'Zidoo Z9X';
        this.config.manufacturer = platform.config.manufacturer || 'Zidoo';
        this.config.pollingInterval = platform.config.pollingInterval || 1000;
        this.config.modelName = platform.config.modelName || 'Z9X';
        this.config.serialN = platform.config.serialN || 'B210U71647033894';
        this.config.mac = platform.config.mac || 'CA:TA:NI:CO:GA:TA';
        this.config.autoIPMac = platform.config.autoIPMac || false;
        this.config.standby = platform.config.standby || false;
        this.config.mediaButtons = platform.config.mediaButtons || false;
        this.config.cursorUpB = platform.config.cursorUpB || false;
        this.config.cursorDownB = platform.config.cursorDownB || false;
        this.config.cursorLeftB = platform.config.cursorLeftB || false;
        this.config.cursorRightB = platform.config.cursorRightB || false;
        this.config.cursorEnterB = platform.config.cursorEnterB || false;
        this.config.menuB = platform.config.menuB || false;
        this.config.backButtonB = platform.config.backButtonB || false;
        this.config.homeMenuB = platform.config.homeMenuB || false;
        this.config.infoB = platform.config.infoB || false;
        this.config.pageUpB = platform.config.pageUpB || false;
        this.config.pageDownB = platform.config.pageDownB || false;
        this.config.popUpMenuB = platform.config.popUpMenuB || false;
        this.config.redB = platform.config.redB || false;
        this.config.yellowB = platform.config.yellowB || false;
        this.config.blueB = platform.config.blueB || false;
        this.config.audioB = platform.config.audioB || false;
        this.config.greenB = platform.config.greenB || false;
        this.config.subtitleB = platform.config.subtitleB || false;
        this.config.repeatB = platform.config.repeatB || false;
        this.config.pipB = platform.config.pipB || false;
        this.config.resolutionB = platform.config.resolutionB || false;
        this.config.movieControl = platform.config.movieControl || false;
        this.config.powerB = platform.config.powerB || false;
        this.config.muteB = platform.config.muteB || false;
        this.config.recordB = platform.config.recordB || false;
        this.config.movieB = platform.config.movieB || false;
        this.config.musicB = platform.config.musicB || false;
        this.config.photoB = platform.config.photoB || false;
        this.config.fileB = platform.config.fileB || false;
        this.config.lightB = platform.config.lightB || false;
        this.config.screenshotB = platform.config.screenshotB || false;
        this.config.appSwitchB = platform.config.appSwitchB || false;
        this.config.rebootB = platform.config.rebootB || false;
        this.config.changeDimmersToFan = platform.config.changeDimmersToFan || false;
        this.config.remainMovieTimer = platform.config.remainMovieTimer || false;
        this.config.volume = platform.config.volume || false;
        this.config.infoToMenu = platform.config.infoToMenu || false;
        this.config.removePlayback = platform.config.removePlayback || false;
        this.config.logDirectory = '/var/lib/homebridge/DeviceStateLogs/';
        this.registerShutdownHandler();
        //this.logInfo(platform.config);
        if (this.config.autoIPMac === false && this.config.numberOfDevices === 1) {
            //this.logInfo('set to false');
            this.config.autoIPMac = true;
        }
        else {
            // this.logInfo('set to true');
            this.config.autoIPMac = false;
        }
        ////Checking if the necessary information was given by the user////////////////////////////////////////////////////
        try {
            if (!this.config.ip && this.config.autoIPMac === false) {
                throw new Error(`Zidoo IP address is required for ${this.config.name}`);
            }
        } catch (error) {
            this.logInfo(error);
            this.logInfo('Failed to create platform device, missing mandatory information!');
            this.logInfo('Please check your device config!');
            return;
        }

        this.setupAccessoryInformationService();
        this.setupTelevisionServices();
        this.setupSpeakerService();
        this.setupOptionalServices(this.accessory);
        this.cleanupDisabledServices();
        this.startConnectivityServices();
        this.startSynchronizationLoops();
    }
    // Applies fallback metadata formatting for firmware responses that omit `video.output`.
    applyBasicVideoMetadata(rawData, videoPayload) {
        const videoHeight = this.toFiniteNumber(videoPayload.height, 0);
        const videoWidth = this.toFiniteNumber(videoPayload.width, 0);
        const framesPerSecond = Math.round(this.toFiniteNumber(videoPayload.fps, 0) * 1000) / 1000;
        const movieAspectRatio = videoWidth + 'x' + videoHeight;
        let movieInfo = '';
        if (videoHeight >= 2160) {
            movieInfo = '4K Video (' + movieAspectRatio + ')';
        }
        else if (videoHeight > 1080) {
            movieInfo = 'UHD Video (' + movieAspectRatio + ')';
        }
        else if (videoHeight === 1080) {
            movieInfo = 'Full HD Video (' + movieAspectRatio + ')';
        }
        else if (videoHeight === 720) {
            movieInfo = 'HD Video (' + movieAspectRatio + ')';
        }
        else {
            movieInfo = 'SD Video (' + movieAspectRatio + ')';
        }
        this.newVideoFormat(this.nameLengthCheck(movieInfo + ' at ' + framesPerSecond + ' Hz'));
        const audioInformation = rawData.audio && typeof rawData.audio.information === 'string' ? rawData.audio.information : 'Unknown';
        const videoAudioInfo = audioInformation.split(" ");
        this.logDebug((videoAudioInfo[1] || 'Unknown') + ' ' + '(' + (videoAudioInfo[0] || 'Unknown') + ')');
        this.newLanguage(this.nameLengthCheck('Audio: ' + (videoAudioInfo[1] || 'Unknown') + ' ' + '(' + (videoAudioInfo[0] || 'Unknown') + ')'));
        let audioText = '';
        for (let i = 2; i < videoAudioInfo.length; i++) {
            audioText += videoAudioInfo[i] + ' ';
        }
        this.newAudioFormat(this.nameLengthCheck('Sound: ' + audioText));
        this.newSubtitle('Additional Information');
        const subtitleInformation = rawData.subtitle && typeof rawData.subtitle.information === 'string' ? rawData.subtitle.information : 'Off';
        const subtitlesOn = subtitleInformation.split(' ');
        this.newSubtitle(this.nameLengthCheck('Subtitles: ' + (subtitlesOn[0] || 'Off')));
    }
    // Applies rich video/audio/subtitle metadata when output detail fields are available in the payload.
    applyDetailedVideoMetadata(rawData, videoPayload) {
        const videoOutput = videoPayload.output.split(' ');
        this.logDebug(videoOutput);
        this.logDebug(videoOutput[videoOutput.length - 1] || '');
        const currentBitrate = typeof videoPayload.bitrate === 'undefined' ? '' : `${videoPayload.bitrate}`;
        const videoHeight = this.toFiniteNumber(videoPayload.height, 0);
        const movieAspectRatio = (videoOutput[0] || '').toLowerCase();
        const movieFileSize = typeof videoPayload.filesize === 'undefined' ? '' : `${videoPayload.filesize}`;
        const framesPerSecond = Math.round(this.toFiniteNumber(videoPayload.fps, 0) * 1000) / 1000;
        let movieInfo = '';
        if (videoHeight >= 2160) {
            movieInfo = '4K Video (' + movieAspectRatio + ')';
        }
        else if (videoHeight > 1080) {
            movieInfo = 'UHD Video (' + movieAspectRatio + ')';
        }
        else if (videoHeight === 1080) {
            movieInfo = 'Full HD Video (' + movieAspectRatio + ')';
        }
        else if (videoHeight === 720) {
            movieInfo = 'HD Video (' + movieAspectRatio + ')';
        }
        else {
            movieInfo = 'SD Video (' + movieAspectRatio + ')';
        }
        let hdrFormat = videoPayload.output;
        if (hdrFormat.includes('DV')) {
            hdrFormat = 'Dolby Vision';
        }
        else if (hdrFormat.includes("HLG")) {
            hdrFormat = 'HLG';
        }
        else if (hdrFormat.includes('+') || hdrFormat.includes('PLUS')) {
            hdrFormat = 'HDR10+';
        }
        else if (!hdrFormat.includes('HDR')) {
            hdrFormat = 'SDR';
        }
        else {
            hdrFormat = 'HDR';
        }
        if (currentBitrate !== '' && currentBitrate !== ' ') {
            this.newVideoFormat(this.nameLengthCheck(hdrFormat + ' ' + movieInfo + ' at ' + framesPerSecond + ' Hz and ' + currentBitrate));
        }
        else {
            this.newVideoFormat(this.nameLengthCheck(hdrFormat + ' ' + movieInfo + ' at ' + framesPerSecond + ' Hz'));
        }
        const audioInformation = rawData.audio && typeof rawData.audio.information === 'string' ? rawData.audio.information : 'Unknown';
        const languageOutput = audioInformation.split(' ');
        const firstLanguageToken = languageOutput[0] || 'Unknown';
        const lastLanguageToken = languageOutput[languageOutput.length - 1] || '';
        this.logDebug(firstLanguageToken + ' ' + lastLanguageToken);
        this.newLanguage(this.nameLengthCheck('Audio: ' + firstLanguageToken + ' ' + lastLanguageToken));
        if (audioInformation.includes(" channel ")) {
            const videoAudioInfo = audioInformation.split(" channel ");
            const videoAudioInfo0 = videoAudioInfo[0].split(' ');
            let audioText = '';
            for (let i = 1; i < videoAudioInfo0.length; i++) {
                audioText += videoAudioInfo0[i] + ' ';
            }
            this.newAudioFormat(this.nameLengthCheck('Sound: ' + audioText + (videoAudioInfo[1] || '')));
        }
        else {
            this.newAudioFormat(this.nameLengthCheck('Sound: ' + audioInformation));
        }
        let subtitlesOn = rawData.subtitle && typeof rawData.subtitle.information === 'string' ? rawData.subtitle.information : 'Off';
        if (subtitlesOn === 'Off') {
            if (movieFileSize !== ' ' && movieFileSize !== '') {
                this.newSubtitle(this.nameLengthCheck('Subtitles: Off' + ' File Size: ' + movieFileSize));
            }
            else {
                this.newSubtitle(this.nameLengthCheck('Subtitles: Off'));
            }
            return;
        }
        const subtitleParts = subtitlesOn.split(' ');
        let subtitleFirstPart = subtitleParts[0] || '';
        if (subtitleFirstPart.includes('(')) {
            subtitleFirstPart = subtitleFirstPart.split('(')[0];
        }
        const subtitleLastPart = subtitleParts[subtitleParts.length - 1] || '';
        if (movieFileSize !== ' ' && movieFileSize !== '') {
            this.newSubtitle(this.nameLengthCheck('Subtitles: ' + subtitleFirstPart + ' ' + subtitleLastPart + ', File Size: ' + movieFileSize));
        }
        else {
            this.newSubtitle(this.nameLengthCheck('Subtitles: ' + subtitleFirstPart + ' ' + subtitleLastPart));
        }
    }
    // Applies volume updates from a status payload and tolerates known firmware field-name variations.
    applyVolumePayload(rawData) {
        const maxVolume = this.toFiniteNumber(rawData.maxVolume, 0);
        const currentVolumeRaw = typeof rawData.currentVolume !== 'undefined' ? rawData.currentVolume : rawData.currenttVolume;
        const currentVolume = this.toFiniteNumber(currentVolumeRaw, NaN);
        if (maxVolume > 0 && Number.isFinite(currentVolume)) {
            this.maxVolume = maxVolume;
            const normalizedVolume = Math.round((currentVolume / maxVolume) * 100);
            this.newVolumeStatus(normalizedVolume);
            return;
        }
        this.logDebug('Skipping volume update because payload is missing valid volume values.');
    }
    // Builds a Lightbulb/Fanv2 range control with shared get/set behavior for state and percentage value.
    buildDualModeRangeService({
        serviceName,
        lightPropertyName,
        fanPropertyName,
        lightUniqueId,
        fanUniqueId,
        stateGetter,
        onStateSet,
        valueGetter,
        onValueSet,
        setNameCharacteristic = false,
    }) {
        const usesFanMode = this.config.changeDimmersToFan === true;
        const propertyName = usesFanMode ? fanPropertyName : lightPropertyName;
        const serviceType = usesFanMode ? this.platform.Service.Fanv2 : this.platform.Service.Lightbulb;
        const uniqueId = usesFanMode ? fanUniqueId : lightUniqueId;
        const stateCharacteristic = usesFanMode ? this.platform.Characteristic.Active : this.platform.Characteristic.On;
        const valueCharacteristic = usesFanMode ? this.platform.Characteristic.RotationSpeed : this.platform.Characteristic.Brightness;
        const service = this.accessory.getService(serviceName)
            || this.accessory.addService(serviceType, serviceName, uniqueId);
        if (setNameCharacteristic === true) {
            service.setCharacteristic(this.platform.Characteristic.Name, serviceName);
        }
        service.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
        service.setCharacteristic(this.platform.Characteristic.ConfiguredName, serviceName);
        service.getCharacteristic(stateCharacteristic)
            .on('get', (callback) => {
                const currentState = stateGetter();
                if (usesFanMode) {
                    callback(null, currentState === true ? 1 : 0);
                    return;
                }
                callback(null, currentState);
            })
            .on('set', (newValue, callback) => {
                onStateSet(newValue, usesFanMode);
                callback(null);
            });
        service.getCharacteristic(valueCharacteristic)
            .on('get', (callback) => {
                callback(null, valueGetter());
            })
            .on('set', (newValue, callback) => {
                onValueSet(newValue, usesFanMode);
                callback(null);
            });
        this[propertyName] = service;
        return service;
    }
    // Declares all TV input sources in one place and builds/links each service from a definition object.
    buildInputSources() {
        const currentShown = this.platform.Characteristic.CurrentVisibilityState.SHOWN;
        const currentHidden = this.platform.Characteristic.CurrentVisibilityState.HIDDEN;
        const targetShown = this.platform.Characteristic.TargetVisibilityState.SHOWN;
        const targetHidden = this.platform.Characteristic.TargetVisibilityState.HIDDEN;
        const showMediaDetails = this.showState ? currentShown : currentHidden;
        const showMediaDetailsTarget = this.showState ? targetShown : targetHidden;
        const inputSourceDefinitions = [
            {
                propertyName: 'videoAudioTitle',
                serviceName: 'Media Title',
                uniqueId: 'CataNicoGaTa-1003',
                identifier: 1,
                configuredName: () => this.inputName,
                inputSourceType: this.platform.Characteristic.InputSourceType.APPLICATION,
                currentVisibility: currentShown,
                onGet: () => this.inputName,
                logOnGet: true,
            },
            {
                propertyName: 'runtime',
                serviceName: 'Runtime',
                uniqueId: 'CataNicoGaTa-1004',
                identifier: 2,
                configuredName: () => this.mediaDuration,
                inputSourceType: this.platform.Characteristic.InputSourceType.HDMI,
                currentVisibility: currentShown,
                onGet: () => this.mediaDuration,
                logOnGet: true,
            },
            {
                propertyName: 'videoAudioElapseTime',
                serviceName: 'Additional Information',
                existingServiceName: 'Elapsed Time',
                uniqueId: 'CataNicoGaTa-1005',
                identifier: 6,
                configuredName: () => this.subtitleInfo,
                inputSourceType: this.platform.Characteristic.InputSourceType.HDMI,
                targetVisibility: targetHidden,
                currentVisibility: currentHidden,
                onGet: () => this.subtitleInfo,
                logOnGet: true,
            },
            {
                propertyName: 'videoFormat',
                serviceName: 'Video Information',
                existingServiceName: 'Video Format',
                uniqueId: 'CataNicoGaTa-4005',
                identifier: 3,
                configuredName: () => this.mediaVideoFormat,
                inputSourceType: this.platform.Characteristic.InputSourceType.HDMI,
                targetVisibility: showMediaDetailsTarget,
                currentVisibility: showMediaDetails,
                onGet: () => this.mediaVideoFormat,
                logOnGet: true,
            },
            {
                propertyName: 'audioFormat',
                serviceName: 'Audio Information',
                existingServiceName: 'Audio Format',
                uniqueId: 'CataNicoGaTa-4006',
                identifier: 4,
                configuredName: () => this.mediaAudioFormat,
                inputSourceType: this.platform.Characteristic.InputSourceType.HDMI,
                targetVisibility: showMediaDetailsTarget,
                currentVisibility: showMediaDetails,
                onGet: () => this.mediaAudioFormat,
                logOnGet: true,
            },
            {
                propertyName: 'audioLanguage',
                serviceName: 'Audio Language',
                uniqueId: 'CataNicoGaTa-4007',
                identifier: 5,
                configuredName: () => this.language,
                inputSourceType: this.platform.Characteristic.InputSourceType.HDMI,
                targetVisibility: showMediaDetailsTarget,
                currentVisibility: showMediaDetails,
                onGet: () => this.language,
                logOnGet: true,
            },
        ];
        for (const definition of inputSourceDefinitions) {
            this.getOrCreateInputSource(definition);
        }
    }
    // Builds the optional media-progress control as a dimmer or fan based on configuration.
    buildMovieProgressService() {
        this.buildDualModeRangeService({
            serviceName: 'Media Progress',
            lightPropertyName: 'movieControlL',
            fanPropertyName: 'movieControlF',
            lightUniqueId: 'CataNicoGaTa-301',
            fanUniqueId: 'CataNicoGaTa-301F',
            setNameCharacteristic: true,
            stateGetter: () => this.currentMovieProgressState,
            onStateSet: (newValue) => {
                this.logInfo('Movie progress state set to: ' + newValue);
            },
            valueGetter: () => this.currentMovieProgress,
            onValueSet: (newValue) => {
                this.setMovieSeekPositionFromPercent(newValue);
                this.logInfo('Movie progress set to: ' + newValue + '%');
            },
        });
    }
    // Builds all optional stateless switches using one shared definition table.
    buildStatelessSwitches() {
        for (const definition of this.getStatelessSwitchConfigs()) {
            if (this.config[definition.configKey] === true) {
                this.createStatelessSwitch(definition);
            }
        }
    }
    // Builds the optional Zidoo volume dimmer/fan control using one dual-mode service helper.
    buildVolumeControlService() {
        this.buildDualModeRangeService({
            serviceName: 'Zidoo Volume',
            lightPropertyName: 'volumeDimmer',
            fanPropertyName: 'volumeFan',
            lightUniqueId: 'CataNicoGaT-98Z',
            fanUniqueId: 'CataNicoGaT-98FZ',
            stateGetter: () => this.currentVolumeSwitch,
            onStateSet: (newValue, usesFanMode) => {
                this.logDebug('Volume Value set to: Mute/Unmute');
                const isActive = usesFanMode ? newValue === 1 : newValue === true;
                if (!isActive) {
                    this.volumeBeforeMute = this.currentVolume;
                    if (this.currentVolume !== 0) {
                        this.volumeChange(0);
                    }
                    return;
                }
                if (this.volumeBeforeMute === 0) {
                    this.volumeChange(100);
                    return;
                }
                this.volumeChange(this.volumeBeforeMute);
            },
            valueGetter: () => this.currentVolume,
            onValueSet: (newValue) => {
                this.volumeChange(newValue);
                this.logInfo('Volume Value set to: ' + newValue + '%');
            },
        });
    }
    // Removes optional services that are disabled in config so HomeKit only shows active features.
    cleanupDisabledServices() {
        if (this.config.remainMovieTimer === false) {
            this.safeRemoveService(this.movieTimer);
        }
        if (this.config.removePlayback === true) {
            this.safeRemoveService(this.play);
            this.safeRemoveService(this.stop);
            this.safeRemoveService(this.pause);
        }
        if (this.config.powerB === false) {
            this.safeRemoveService(this.service);
        }
        if (this.config.movieControl === false) {
            this.safeRemoveService(this.movieControlL);
            this.safeRemoveService(this.movieControlF);
        }
        this.cleanupDisabledStatelessSwitches();
        if (this.config.muteB === false) {
            this.safeRemoveService(this.mute);
        }
        if (this.config.changeDimmersToFan === false) {
            this.safeRemoveService(this.volumeFan);
            this.safeRemoveService(this.movieControlF);
        }
        if (this.config.changeDimmersToFan === true) {
            this.safeRemoveService(this.movieControlL);
            this.safeRemoveService(this.volumeDimmer);
        }
        if (this.config.volume === false) {
            this.safeRemoveService(this.volumeDimmer);
            this.safeRemoveService(this.volumeFan);
        }
    }
    // Removes disabled stateless switches so HomeKit only exposes features enabled in config.
    cleanupDisabledStatelessSwitches() {
        for (const definition of this.getStatelessSwitchConfigs()) {
            if (this.config[definition.configKey] !== true) {
                const service = this[definition.propertyName] || this.accessory.getService(definition.serviceName);
                this.safeRemoveService(service);
            }
        }
    }
    // Clears all tracked interval/timeout handles to avoid orphan timers.
    clearManagedTimers() {
        for (const timeoutHandle of this.managedTimeouts) {
            clearTimeout(timeoutHandle);
        }
        this.managedTimeouts.clear();
        for (const intervalHandle of this.managedIntervals) {
            clearInterval(intervalHandle);
        }
        this.managedIntervals.clear();
    }
    /////////////////Command Log
    // Keeps polling commands in debug logs while elevating user-triggered commands to info logs.
    commandLog(commandPress) {
        if (commandPress.includes('getModel') || commandPress.includes('getState') || commandPress.includes('getPlayStatus') || commandPress.includes('volumeStatus')) {
            this.logDebug(`Sending: ${this.commandName(commandPress)} Command`);
        }
        else {
            this.logInfo(`Sending: ${this.commandName(commandPress)} Command`);
        }
    }
    //////////Sending Command Name Decoder///////////
    // Converts raw Zidoo command tokens into readable labels for logs and history.
    commandName(keyS) {
        this.logDebug(keyS);
        for (const mapping of COMMAND_NAME_MATCHES) {
            if (keyS.includes(mapping.token)) {
                return mapping.label;
            }
        }
        return keyS;
    }
    // Creates a new HomeKit InputSource service and applies all static characteristics from its definition.
    createInputSourceService({
        serviceName,
        uniqueId,
        identifier,
        configuredName,
        inputSourceType,
        targetVisibility,
        currentVisibility,
    }) {
        const configuredNameValue = typeof configuredName === 'function' ? configuredName() : configuredName;
        const service = this.accessory.addService(this.platform.Service.InputSource, serviceName, uniqueId)
            .setCharacteristic(this.platform.Characteristic.Identifier, identifier)
            .setCharacteristic(this.platform.Characteristic.ConfiguredName, configuredNameValue)
            .setCharacteristic(this.platform.Characteristic.IsConfigured, this.platform.Characteristic.IsConfigured.CONFIGURED)
            .setCharacteristic(this.platform.Characteristic.InputSourceType, inputSourceType);
        if (typeof targetVisibility !== 'undefined') {
            service.setCharacteristic(this.platform.Characteristic.TargetVisibilityState, targetVisibility);
        }
        if (typeof currentVisibility !== 'undefined') {
            service.setCharacteristic(this.platform.Characteristic.CurrentVisibilityState, currentVisibility);
        }
        return service;
    }
    // Creates a persistent stateful switch where get/set behavior is provided by bound handlers.
    createStatefulSwitch({ propertyName, serviceName, uniqueId, onGet, onSet }) {
        const service = this.accessory.getService(serviceName)
            || this.accessory.addService(this.platform.Service.Switch, serviceName, uniqueId);
        service.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
        service.setCharacteristic(this.platform.Characteristic.ConfiguredName, serviceName);
        service.getCharacteristic(this.platform.Characteristic.On)
            .on('get', onGet)
            .on('set', onSet);
        this[propertyName] = service;
        return service;
    }
    // Creates a stateless switch that sends one remote command and auto-resets back to off.
    createStatelessSwitch({ propertyName, serviceName, uniqueId, commandName }) {
        const service = this.accessory.getService(serviceName)
            || this.accessory.addService(this.platform.Service.Switch, serviceName, uniqueId);
        service.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
        service.setCharacteristic(this.platform.Characteristic.ConfiguredName, serviceName);
        service.getCharacteristic(this.platform.Characteristic.On)
            .on('get', (callback) => {
                this.logDebug(`${serviceName} GET On`);
                callback(null, false);
            })
            .on('set', (value, callback) => {
                this.logDebug(`${serviceName} SET On: ${value}`);
                if (value === true) {
                    this.sending([this.pressedButton(commandName)]);
                }
                this.scheduleTimeout(() => {
                    service.updateCharacteristic(this.platform.Characteristic.On, false);
                }, this.statelessTimeOut);
                callback(null);
            });
        this[propertyName] = service;
        return service;
    }
    // Resolves after the specified delay using managed timeout tracking for clean shutdown support.
    delayByMs(delayMs) {
        return new Promise((resolve) => {
            this.scheduleTimeout(() => resolve(), delayMs);
        });
    }
    ////////////////Zidoo discovery
    // Opens the multicast discovery socket and captures Zidoo IP/MAC metadata from UDP announcements.
    discoveryUPD() {
        this.discovery = udp.createSocket({ type: 'udp4', reuseAddr: true });
        this.discovery.on('error', (error) => {
            this.logDebug(error);
            this.discovery.close();
        });
        this.discovery.on('listening', () => {
            var address = this.discovery.address();
            this.logDebug('UDP Client listening on ' + address.address + ":" + address.port);
            this.discovery.setBroadcast(true)
            this.discovery.setMulticastTTL(128);
            this.discovery.addMembership('239.39.3.9');
            this.helloMessage();
        });

        this.discovery.on('message', (message, remote) => {
            this.logDebug('Message received From: ' + remote.address + ':' + remote.port);
            // Parse UDP payload defensively because announcements are not guaranteed to include all keys.
            const newMessageRaw = message.toString('utf8');
            const messageBodyStart = newMessageRaw.indexOf('\n');
            const newMessage = messageBodyStart >= 0 ? newMessageRaw.substring(messageBodyStart + 1) : newMessageRaw;
            const properties = newMessage.split('\r\n');
            const zidooInfo = {};
            for (const property of properties) {
                if (!property) {
                    continue;
                }
                const separatorIndex = property.indexOf(':');
                if (separatorIndex <= 0) {
                    continue;
                }
                const propertyKey = property.substring(0, separatorIndex).trim();
                const propertyValue = property.substring(separatorIndex + 1).trim();
                zidooInfo[propertyKey] = propertyValue;
            }
            if (typeof zidooInfo.host === 'string' && zidooInfo.host.length > 0 && !zidooInfo.host.includes('239.39.3.9')) {
                this.logDebug(zidooInfo);
                this.ZIDOO_IP = zidooInfo.host.replace(/\s+/g, '');
                this.IPReceived = true;
            }
        });
        this.discovery.bind(this.ZIDOO_UDP_PORT);

    }
    // Converts any value into a safe single-line string for logs.
    formatLogValue(message) {
        if (typeof message === 'string') {
            return message;
        }
        if (message instanceof Error) {
            return message.stack || message.message;
        }
        try {
            return JSON.stringify(message);
        }
        catch (error) {
            return String(message);
        }
    }
    // Returns the cached on/off state for HomeKit power reads.
    getOn(callback) {
        let isOn = this.powerState;
        this.logDebug('Get Power ->', isOn);
        callback(null, isOn);
    }
    // Reuses an existing input service when available, otherwise creates it and wires dynamic ConfiguredName reads.
    getOrCreateInputSource(definition) {
        const existingService = this.accessory.getService(definition.existingServiceName || definition.serviceName)
            || this.accessory.getService(definition.serviceName);
        const service = existingService || this.createInputSourceService(definition);
        if (typeof definition.onGet === 'function') {
            service.getCharacteristic(this.platform.Characteristic.ConfiguredName)
                .on('get', (callback) => {
                    const currentValue = definition.onGet();
                    if (definition.logOnGet === true) {
                        this.logDebug('Getting' + currentValue);
                    }
                    callback(null, currentValue);
                });
        }
        this.tvService.addLinkedService(service);
        this[definition.propertyName] = service;
        return service;
    }
    // Returns the declarative list of optional stateless remote-control switches.
    getStatelessSwitchConfigs() {
        return STATELESS_SWITCH_CONFIGS;
    }
    // Handles music-state payloads and updates current playback status plus music metadata.
    handleMusicStatusPayload(rawData) {
        this.logDebug(this.moviePlaying);
        if (this.turnOffCommand !== true && this.standbyMode === 0) {
            this.newPowerState(true);
        }
        const volumeData = rawData.volumeData && typeof rawData.volumeData === 'object' ? rawData.volumeData : null;
        if (volumeData) {
            const maxVolume = this.toFiniteNumber(volumeData.maxVolume, 0);
            const currentVolumeRaw = typeof volumeData.currenttVolume !== 'undefined' ? volumeData.currenttVolume : volumeData.currentVolume;
            const currentVolume = this.toFiniteNumber(currentVolumeRaw, NaN);
            if (maxVolume > 0 && Number.isFinite(currentVolume)) {
                this.maxVolume = maxVolume;
                const normalizedVolume = Math.round((currentVolume / maxVolume) * 100);
                this.newVolumeStatus(normalizedVolume);
            }
            if (typeof volumeData.isMute !== 'undefined') {
                this.isMute = volumeData.isMute;
                this.isMuteAvailable = true;
            }
        }
        const state = this.toFiniteNumber(rawData.state, 0);
        if (state === 3) {
            this.newPlayBackState([true, false, false]);
            this.showState = true;
            this.musicPlaying = true;
        }
        else if (state === 4) {
            this.newPlayBackState([false, true, false]);
            this.showState = true;
            this.musicPlaying = true;
        }
        else if (state === 0 && this.moviePlaying === false) {
            this.musicPlaying = false;
            this.newPlayBackState([false, false, false]);
            this.mediaDetailsReset();
        }
        else if (state === 0) {
            this.musicPlaying = false;
        }
        if (state !== 0) {
            if (this.turnOffCommand !== true && this.standbyMode === 0) {
                this.newPowerState(true);
            }
            this.logDebug("Music details");
            this.logDebug(rawData);
            const durationMs = this.toFiniteNumber(rawData.duration, 0);
            const positionMs = this.toFiniteNumber(rawData.position, 0);
            this.movieDuration = Math.floor(durationMs / 1000);
            this.movieElapsed = positionMs / 1000;
            this.newMovieTime(positionMs / 1000);
            const playingMusic = rawData.playingMusic && typeof rawData.playingMusic === 'object' ? rawData.playingMusic : {};
            this.newSubtitle('Audio Channels: ' + (playingMusic.channels || 'Unknown') + ', ' + 'Sample Rate: ' + (playingMusic.sampleRate || 'Unknown'));
            this.newVideoFormat('Format: ' + (playingMusic.extension || 'Unknown'));
            this.newAudioFormat('Bitrate: ' + (playingMusic.bitrate || 'Unknown'));
            this.newLanguage('File Size: ' + (playingMusic.fileSize || 'Unknown'));
            const songTitle = (playingMusic.title || 'Unknown') + " - " + (playingMusic.artist || 'Unknown');
            this.newInputName(this.nameLengthCheck(songTitle));
            this.newInputDuration(this.normalizeRuntimeFromMs(durationMs));
        }
    }
    // Handles movie playback status payloads and updates playback/media metadata defensively.
    handleVideoStatusPayload(rawData) {
        if (this.turnOffCommand !== true && this.standbyMode === 0) {
            this.newPowerState(true);
        }
        const videoPayload = rawData.video && typeof rawData.video === 'object' ? rawData.video : null;
        if (!videoPayload) {
            this.logDebug('Missing video payload in getPlayStatus response.');
            return;
        }
        this.logDebug(videoPayload.status);
        this.logDebug(this.musicPlaying);
        const videoStatus = this.toFiniteNumber(videoPayload.status, -1);
        if (videoStatus === 0 && this.musicPlaying === false) {
            this.newPlayBackState([false, true, false]);
            this.moviePlaying = true;
            this.showState = true;
            this.standbyMode = 0;
        }
        if (videoStatus === 1) {
            this.newPlayBackState([true, false, false]);
            this.moviePlaying = true;
            this.showState = true;
            this.standbyMode = 0;
        }
        const videoPath = typeof videoPayload.path === 'string' ? videoPayload.path : '';
        if (videoPath === '') {
            return;
        }
        this.logDebug(rawData);
        let movie3D = '';
        // Guard optional 3D payload fields to avoid runtime crashes on older firmware responses.
        if (rawData['3D'] && rawData['3D'].information === '3D') {
            movie3D = ' (3D)';
        }
        const videoTitle = typeof videoPayload.title === 'string' ? videoPayload.title : 'Media Title';
        this.updateVideoTitle(videoTitle, videoPath, movie3D);
        const videoDurationMs = this.toFiniteNumber(videoPayload.duration, 0);
        this.movieDuration = Math.floor(videoDurationMs / 1000);
        this.newInputDuration(this.normalizeRuntimeFromMs(videoDurationMs));
        const currentPositionMs = this.toFiniteNumber(videoPayload.currentPosition, 0);
        this.movieElapsed = currentPositionMs / 1000;
        this.newMovieTime(currentPositionMs / 1000);
        if (typeof videoPayload.output !== 'undefined') {
            this.applyDetailedVideoMetadata(rawData, videoPayload);
            return;
        }
        this.applyBasicVideoMetadata(rawData, videoPayload);
    }
    //////Zidoo Hello message
    // Broadcasts a JOIN packet that prompts Zidoo devices to announce themselves.
    helloMessage() {
        this.hello = udp.createSocket({ type: 'udp4', reuseAddr: true });
        const message = Buffer.from('JOIN\r\nuuid: CataNico\r\ntype: 2\r\nhost: 239.39.3.9\r\nport: 18239\r\n\r\n');
        this.hello.send(message, 18239, '239.39.3.9', (err) => {
            if (err) {
                this.logDebug('Error sending UDP message: ' + err);
                this.scheduleTimeout(() => {
                    this.helloMessage();
                }, 2000);
            }
            // this.logInfo('UDP message sent to ' + HOST + ':' + PORT);
            this.hello.close();
        });
    }
    /////////////////HTTP Event decoder
    // Decodes Zidoo API responses and routes updates into focused handlers for each response type.
    httpEventDecoder(rawData, key) {
        this.logDebug(`${key} Sent by HTTP`);
        this.logDebug(rawData);
        if (!rawData || typeof rawData !== 'object') {
            this.logDebug('Ignoring empty or invalid HTTP payload.');
            return;
        }
        if (rawData.status === 806 || rawData.status === 405) {
            this.moviePlaying = false;
            return;
        }
        if (rawData.status !== 200) {
            return;
        }
        this.updateMacFromHttpPayload(rawData);
        if (this.standbyMode === 1 && key.includes('Menu')) {
            this.standbyMode = 0;
        }
        this.logCommandResponse(key);
        this.routeHttpEvent(rawData, key);
    }
    // Detects transport failures that typically mean the device is unreachable/offline.
    isOfflineTransportError(error) {
        if (!error) {
            return false;
        }
        const errorCode = typeof error.code === 'string' ? error.code.toUpperCase() : '';
        if (errorCode === 'ECONNREFUSED'
            || errorCode === 'ECONNRESET'
            || errorCode === 'EHOSTUNREACH'
            || errorCode === 'EHOSTDOWN'
            || errorCode === 'ENETUNREACH'
            || errorCode === 'ETIMEDOUT'
            || errorCode === 'EPIPE') {
            return true;
        }
        const errorMessage = typeof error.message === 'string' ? error.message.toLowerCase() : '';
        return errorMessage.includes('socket hang up')
            || errorMessage.includes('timed out')
            || errorMessage.includes('connect econnrefused')
            || errorMessage.includes('connect ehostdown')
            || errorMessage.includes('host is down')
            || errorMessage.includes('host unreachable')
            || errorMessage.includes('network is unreachable');
    }
    // Writes command response logs at debug/info level based on whether the command is a periodic poll.
    logCommandResponse(key) {
        if (key.includes('getModel') || key.includes('getState') || key.includes('getPlayStatus') || key.includes('volumeStatus')) {
            this.logDebug(`Response: ${this.commandName(key)} Command Executed`);
            return;
        }
        this.logInfo(`Response: ${this.commandName(key)} Command Executed`);
    }
    // Logs a debug message with device-name prefix.
    logDebug(...messages) {
        const prefix = this.config.name ? `[${this.config.name}] ` : '';
        this.platform.log.debug(prefix + messages.map((message) => this.formatLogValue(message)).join(' '));
    }
    // Logs an error with robust value normalization.
    logError(...messages) {
        const prefix = this.config.name ? `[${this.config.name}] ` : '';
        this.platform.log.error(prefix + messages.map((message) => this.formatLogValue(message)).join(' '));
    }
    /////logs

    // Logs an informational message with device-name prefix.
    logInfo(...messages) {
        const prefix = this.config.name ? `[${this.config.name}] ` : '';
        this.platform.log(prefix + messages.map((message) => this.formatLogValue(message)).join(' '));
    }
    // Clears playback-specific metadata and preserves last-played title when available.
    mediaDetailsReset() {
        this.logDebug("Reset details");
        this.showState = false;
        this.movieDuration = 0;
        this.movieElapsed = 0;
        this.newMovieTime(0);
        if (this.videoAudioTitle.getCharacteristic(this.platform.Characteristic.ConfiguredName).value !== 'Media Title' && !this.videoAudioTitle.getCharacteristic(this.platform.Characteristic.ConfiguredName).value.includes('Last Played: ') && this.videoAudioTitle.getCharacteristic(this.platform.Characteristic.ConfiguredName).value !== 'Standby') {
            this.newInputName(this.nameLengthCheck('Last Played: ' + this.videoAudioTitle.getCharacteristic(this.platform.Characteristic.ConfiguredName).value));
        }
        //this.newAudioFormat('Audio Information');
        //this.newInputDuration('Runtime');
        //this.newSubtitle('Additional Information');
        //this.newVideoFormat('Video Information');
        //this.newLanguage('Audio Language');

    }

    ////Name Check
    // Trims long metadata strings to stay under HomeKit characteristic-length limits.
    nameLengthCheck(newName) {
        if (newName.length >= 64) {
            newName = newName.slice(0, 60) + '...';
        }
        return newName;
    }
    // Updates audio-format metadata and visibility.
    newAudioFormat(audioType) {
        if (typeof audioType !== 'undefined') {
            this.logDebug(audioType);
            this.logDebug('New audio information: ' + audioType);
            this.logDebug(audioType);
            if (this.mediaAudioFormat !== audioType) {
                this.mediaAudioFormat = audioType;
                this.logDebug(audioType);
                this.audioFormat.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.mediaAudioFormat);
                this.audioFormat.updateCharacteristic(this.platform.Characteristic.TargetVisibilityState, this.showState ? this.platform.Characteristic.TargetVisibilityState.SHOWN : this.platform.Characteristic.TargetVisibilityState.HIDDEN);
                this.audioFormat.updateCharacteristic(this.platform.Characteristic.CurrentVisibilityState, this.showState ? this.platform.Characteristic.CurrentVisibilityState.SHOWN : this.platform.Characteristic.CurrentVisibilityState.HIDDEN);
            }
        }
    }
    // Maps raw audio codec strings from device telemetry into user-friendly labels.
    newAudioStatus(audio) {
        this.logDebug(audio);
        let audioFormat = '';
        if (audio.includes('Digital Plus')) {
            audioFormat = 'Dolby Digital Plus';
        }
        else if (audio.includes('Dolby Digital')) {
            audioFormat = 'Dolby Digital';
        }
        else if (audio.includes('TrueHD')) {
            audioFormat = 'Dolby TrueHD-Atmos';
        }
        else if (audio.includes('DTS-HD High')) {
            audioFormat = 'DTS-HD High Resolution';
        }
        else if (audio.includes('DTS-HD Master') || audio.includes('DTS-HD MA')) {
            audioFormat = 'DTS-HD MA - DTS X';
        }
        else if (audio.includes('DTS-HD')) {
            audioFormat = 'DTS-HD';
        }
        else if (audio.includes('DTS')) {
            audioFormat = 'DTS';
        }
        else if (audio.includes('LPCM')) {
            audioFormat = 'LPCM';
        }
        else if (audio.includes('MPEG')) {
            audioFormat = 'MPEG Audio';
        }
        else if (audio.includes('CD Audio')) {
            audioFormat = 'CD Audio';
        }
        else {
            audioFormat = 'Unknown';
        }
        this.newAudioFormat(audioFormat);
    }
    // Formats and updates runtime text (Runtime: HH:MM:SS + unit suffix).
    newInputDuration(newDuration) {
        if (typeof newDuration !== 'undefined') {
            this.logDebug('New input duration: ' + newDuration);
            if (!newDuration.includes('Runtime')) {
                let hourMintue = ''
                if (this.movieDuration > 3600) {
                    hourMintue = 'Hours';
                }
                else if (this.movieDuration === 3600) {
                    hourMintue = 'Hour';
                }
                else {
                    hourMintue = 'Minutes';
                }
                this.mediaDuration = 'Runtime: ' + newDuration + ' ' + hourMintue;
            }
            else {
                this.mediaDuration = newDuration;
            }
            if (this.runtime.getCharacteristic(this.platform.Characteristic.ConfiguredName).value !== this.mediaDuration) {
                this.runtime.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.mediaDuration)
            }
        }
    }
    // Updates the current media title input source.
    newInputName(newName) {
        if (typeof newName !== 'undefined') {
            this.logDebug('New input name: ' + newName);
            if (this.inputName !== newName) {
                //this.logInfo(newName);
                this.inputName = newName;
                this.logDebug(this.inputName);
                this.videoAudioTitle.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.inputName);
                //this.videoAudioTitle.getCharacteristic(this.platform.Characteristic.ConfiguredName).updateValue(this.inputName);
            }
        }
    }
    // Converts internal input-state flags to ActiveIdentifier and updates the TV service.
    newInputState(newInput) {
        this.inputState = newInput;
        if (this.inputState[0] === true) {
            this.inputID = 1;
        }
        else if (this.inputState[1] === true) {
            this.inputID = 2;
        }
        else if (this.inputState[2] === true) {
            this.inputID = 3;
        }
        else if (this.inputState[3] === true) {
            this.inputID = 4;
        }
        else if (this.inputState[4] === true) {
            this.inputID = 5;
        }
        else if (this.inputState[5] === true) {
            this.inputID = 6;
        }
        else if (this.inputState[0] === false && this.inputState[1] === false && this.inputState[2] === false
            && this.inputState[3] === false && this.inputState[4] === false && this.inputState[5] === false) {
            this.inputID = this.tvService.getCharacteristic(this.platform.Characteristic.ActiveIdentifier).value;
        }
        else {
        }
        this.tvService.updateCharacteristic(this.platform.Characteristic.ActiveIdentifier, this.inputID);
        // this.tvService.getCharacteristic(this.platform.Characteristic.ActiveIdentifier).updateValue(this.inputID);
    }
    // Updates language metadata and visibility.
    newLanguage(lang) {
        if (typeof lang !== 'undefined') {
            this.logDebug('New audio language: ' + lang);
            if (this.language !== lang) {
                this.language = lang;
                this.audioLanguage.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.language);
                this.audioLanguage.updateCharacteristic(this.platform.Characteristic.TargetVisibilityState, this.showState ? this.platform.Characteristic.TargetVisibilityState.SHOWN : this.platform.Characteristic.TargetVisibilityState.HIDDEN)
                this.audioLanguage.updateCharacteristic(this.platform.Characteristic.CurrentVisibilityState, this.showState ? this.platform.Characteristic.CurrentVisibilityState.SHOWN : this.platform.Characteristic.CurrentVisibilityState.HIDDEN);
            }
        }
    }
    // Computes playback progress and updates optional media-progress controls and timer services.
    newMovieTime(newMovieTime) {
        if (newMovieTime === 0) {
            this.currentMovieProgressState = false;
            this.currentMovieProgress = 0;
        }
        if (newMovieTime !== 0) {
            this.currentMovieProgressState = true;
        }
        if (this.movieDuration !== 0) {
            this.currentMovieProgress = Math.round(newMovieTime * 100 / (this.movieDuration));
        }
        if (this.currentMovieProgressState === true && this.currentMovieProgress === 0) {
            this.currentMovieProgress = 1;
        }
        if (this.currentMovieProgress > 100) { this.currentMovieProgress = 100 }
        if (this.config.movieControl === true) {
            if (this.config.changeDimmersToFan === false) {
                if (this.movieControlL.getCharacteristic(this.platform.Characteristic.Brightness).value !== this.currentMovieProgress) {
                    this.movieControlL.updateCharacteristic(this.platform.Characteristic.Brightness, this.currentMovieProgress);
                    //this.movieControlL.getCharacteristic(this.platform.Characteristic.Brightness).updateValue(this.currentMovieProgress);
                    this.movieControlL.updateCharacteristic(this.platform.Characteristic.On, this.currentMovieProgressState);
                    //this.movieControlL.getCharacteristic(this.platform.Characteristic.On).updateValue(this.currentMovieProgressState);
                }
            }
            else {
                if (this.movieControlF.getCharacteristic(this.platform.Characteristic.RotationSpeed).value !== this.currentMovieProgress) {
                    this.movieControlF.updateCharacteristic(this.platform.Characteristic.RotationSpeed, this.currentMovieProgress);
                    // this.movieControlF.getCharacteristic(this.platform.Characteristic.RotationSpeed).updateValue(this.currentMovieProgress);
                    this.movieControlF.updateCharacteristic(this.platform.Characteristic.Active, this.currentMovieProgressState === true ? 1 : 0);
                    //this.movieControlF.getCharacteristic(this.platform.Characteristic.Active).updateValue(this.currentMovieProgressState === true ? 1 : 0);
                }
            }
        }
        if (this.config.remainMovieTimer) {
            // Keep timer Active/InUse aligned with a stable numeric target (0/1).
            const targetMovieTimerActiveState = this.currentMovieProgressState ? 1 : 0;
            if (this.movieTimer.getCharacteristic(this.platform.Characteristic.Active).value !== targetMovieTimerActiveState) {
                this.movieTimer.updateCharacteristic(this.platform.Characteristic.Active, targetMovieTimerActiveState);
                this.movieTimer.updateCharacteristic(this.platform.Characteristic.InUse, targetMovieTimerActiveState);
            }
            if ((this.movieDuration - this.movieElapsed) !== this.movieTimer.getCharacteristic(this.platform.Characteristic.RemainingDuration).value) {
                this.movieTimer.updateCharacteristic(this.platform.Characteristic.RemainingDuration, (this.movieDuration - this.movieElapsed));
            }
            if (this.movieDuration !== this.movieTimer.getCharacteristic(this.platform.Characteristic.SetDuration).value) {
                this.movieTimer.updateCharacteristic(this.platform.Characteristic.SetDuration, this.movieDuration);
            }
        }
    }
    // Maps play/pause/stop booleans into HomeKit media state and synchronizes related services.
    newPlayBackState(newPlay) {
        this.playBackState = newPlay;
        // Compare array values explicitly; array identity checks never match equivalent literals.
        const isPlaybackFullyStopped = this.playBackState[0] === false
            && this.playBackState[1] === false
            && this.playBackState[2] === false;
        if (this.turnOffCommand === false || isPlaybackFullyStopped) {
            if (this.playBackState[0] === true) {
                this.mediaState = 0;
            }
            if (this.playBackState[1] === true) {
                this.mediaState = 1;
            }
            if (this.playBackState[2] === true) {
                this.mediaState = 2;
            }
            if (this.playBackState[0] === false && this.playBackState[1] === false && this.playBackState[2] === false) {
                this.mediaState = 4;
            }

            if (this.tvService.getCharacteristic(this.platform.Characteristic.Active).value !== this.powerStateTV) {
                this.tvService.updateCharacteristic(this.platform.Characteristic.Active, this.powerStateTV);
            }
            if (!this.config.removePlayback) {
                if (this.play.getCharacteristic(this.platform.Characteristic.On).value !== this.playBackState[0]) {
                    this.play.updateCharacteristic(this.platform.Characteristic.On, this.playBackState[0]);
                    //this.play.getCharacteristic(this.platform.Characteristic.On).updateValue(this.playBackState[0]);
                    //this.tvService.getCharacteristic(this.platform.Characteristic.CurrentMediaState).updateValue(this.mediaState);
                }
                if (this.pause.getCharacteristic(this.platform.Characteristic.On).value !== this.playBackState[1]) {
                    this.pause.updateCharacteristic(this.platform.Characteristic.On, this.playBackState[1]);
                    //this.pause.getCharacteristic(this.platform.Characteristic.On).updateValue(this.playBackState[1]);
                    //this.tvService.getCharacteristic(this.platform.Characteristic.CurrentMediaState).updateValue(this.mediaState);

                }
                if (this.stop.getCharacteristic(this.platform.Characteristic.On).value !== this.playBackState[2]) {
                    this.stop.updateCharacteristic(this.platform.Characteristic.On, this.playBackState[2]);
                    //this.stop.getCharacteristic(this.platform.Characteristic.On).updateValue(this.playBackState[2]);
                    //this.tvService.getCharacteristic(this.platform.Characteristic.CurrentMediaState).updateValue(this.mediaState);
                }
            }
            if (this.tvService.getCharacteristic(this.platform.Characteristic.CurrentMediaState).value !== this.mediaState) {
                this.tvService.updateCharacteristic(this.platform.Characteristic.CurrentMediaState, this.mediaState);
                // this.tvService.getCharacteristic(this.platform.Characteristic.CurrentMediaState).updateValue(this.mediaState);

            }

        }
    }
    // Updates power state in TV/optional switch services and handles Standby/Media Title label transitions.
    newPowerState(newValue) {
        if (newValue === true) {
            this.powerStateTV = 1;
            if (this.videoAudioTitle.getCharacteristic(this.platform.Characteristic.ConfiguredName).value === 'Media Title') {
                this.newInputName('Standby');
            }
        }
        else {
            this.powerStateTV = 0;
            if (this.videoAudioTitle.getCharacteristic(this.platform.Characteristic.ConfiguredName).value === 'Standby') {
                this.newInputName('Media Title');
            }
        }
        if (this.powerState !== newValue) {
            this.powerState = newValue;
            this.tvService.updateCharacteristic(this.platform.Characteristic.Active, this.powerStateTV);
            //this.tvService.getCharacteristic(this.platform.Characteristic.Active).updateValue(this.powerStateTV);
            if (this.config.powerB === true) {
                this.service.updateCharacteristic(this.platform.Characteristic.On, this.powerState);
                //this.service.getCharacteristic(this.platform.Characteristic.On).updateValue(this.powerState);
            }
        }
    }
    // Updates subtitle/additional-info metadata and visibility.
    newSubtitle(newSub) {
        this.logDebug(newSub);
        if (this.subtitleInfo !== newSub) {
            this.subtitleInfo = newSub;
            this.videoAudioElapseTime.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.subtitleInfo);
            this.videoAudioElapseTime.updateCharacteristic(this.platform.Characteristic.TargetVisibilityState, this.showState ? this.platform.Characteristic.TargetVisibilityState.SHOWN : this.platform.Characteristic.TargetVisibilityState.HIDDEN);
            this.videoAudioElapseTime.updateCharacteristic(this.platform.Characteristic.CurrentVisibilityState, this.showState ? this.platform.Characteristic.CurrentVisibilityState.SHOWN : this.platform.Characteristic.CurrentVisibilityState.HIDDEN);
        }
    }
    // Updates video-format metadata and keeps its visibility synced to playback detail state.
    newVideoFormat(videoFormat) {
        if (typeof videoFormat !== 'undefined') {
            this.logDebug('New input progress: ' + videoFormat);
            this.logDebug(videoFormat);
            if (this.mediaVideoFormat !== videoFormat) {
                this.logDebug(videoFormat);
                this.mediaVideoFormat = videoFormat;
                this.videoFormat.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.mediaVideoFormat);
                this.videoFormat.updateCharacteristic(this.platform.Characteristic.TargetVisibilityState, this.showState ? this.platform.Characteristic.TargetVisibilityState.SHOWN : this.platform.Characteristic.TargetVisibilityState.HIDDEN);
                this.videoFormat.updateCharacteristic(this.platform.Characteristic.CurrentVisibilityState, this.showState ? this.platform.Characteristic.CurrentVisibilityState.SHOWN : this.platform.Characteristic.CurrentVisibilityState.HIDDEN);
            }
        }
    }
    //////////Current Status//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Applies normalized volume updates across speaker and optional dimmer/fan control services.
    newVolumeStatus(newVolumeNum) {
        if (this.turnOffCommand !== true || newVolumeNum === 0) {
            if (this.currentVolume !== newVolumeNum) {
                this.currentVolume = newVolumeNum;
                if (newVolumeNum === 0) {
                    this.currentMuteState = true;
                    this.currentVolumeSwitch = false;

                }
                if (newVolumeNum !== 0) {
                    this.currentMuteState = false;
                    this.currentVolumeSwitch = true;
                }
                if (this.isMuteAvailable === true) {
                    this.currentMuteState = this.isMute;
                }
                this.speakerService.updateCharacteristic(this.platform.Characteristic.Volume, this.currentVolume);
                this.speakerService.updateCharacteristic(this.platform.Characteristic.Mute, this.currentMuteState);
                // this.speakerService.getCharacteristic(this.platform.Characteristic.Volume).updateValue(this.currentVolume);
                // this.speakerService.getCharacteristic(this.platform.Characteristic.Mute).updateValue(this.currentMuteState)
                if (this.config.volume === true) {
                    if (this.config.changeDimmersToFan === false) {
                        this.volumeDimmer.updateCharacteristic(this.platform.Characteristic.Brightness, this.currentVolume);
                        //this.volumeDimmer.getCharacteristic(this.platform.Characteristic.Brightness).updateValue(this.currentVolume);
                        this.volumeDimmer.updateCharacteristic(this.platform.Characteristic.On, this.currentVolumeSwitch);
                        //this.volumeDimmer.getCharacteristic(this.platform.Characteristic.On).updateValue(this.currentVolumeSwitch);
                    }
                    else {
                        this.volumeFan.updateCharacteristic(this.platform.Characteristic.RotationSpeed, this.currentVolume);
                        // this.volumeFan.getCharacteristic(this.platform.Characteristic.RotationSpeed).updateValue(this.currentVolume);
                        this.volumeFan.updateCharacteristic(this.platform.Characteristic.Active, this.currentVolumeSwitch === true ? 1 : 0);
                        // this.volumeFan.getCharacteristic(this.platform.Characteristic.Active).updateValue(this.currentVolumeSwitch === true ? 1 : 0);
                    }
                }
            }
        }
    }
    // Formats milliseconds to HH:MM:SS-style runtime text while preserving the previous leading-hour behavior.
    normalizeRuntimeFromMs(durationMs) {
        const runtimeSeconds = Math.max(0, Math.floor(this.toFiniteNumber(durationMs, 0) / 1000));
        let runtimeNumber = this.secondsToTime(runtimeSeconds);
        if (runtimeNumber.startsWith('0')) {
            runtimeNumber = runtimeNumber.substring(1);
        }
        return runtimeNumber;
    }
    /////////////////////////////////////////////////////////////////////////////////////////////////////////////Pause
    // Reports whether playback is currently in the paused state.
    pauseSwitchStateGet(callback) {
        this.logDebug('Pause State');
        let currentValue = this.playBackState[1];
        callback(null, currentValue);
    }
    // Sends a PAUSE key when the Pause switch is toggled on.
    pauseSwitchStateSet(value, callback) {
        this.logDebug('Pause set to', value);
        if (value === true) {
            this.sending([this.pressedButton('PAUSE')]);
        }
        callback(null);
    }
    ///////////////////////////////////////////////////////////////////////////////////////////////////Play
    // Reports whether playback is currently in the playing state.
    playSwitchStateGet(callback) {
        this.logDebug('Play State');
        let currentValue = this.playBackState[0];
        callback(null, currentValue);
    }
    // Sends a PLAY key when the Play switch is toggled on.
    playSwitchStateSet(value, callback) {
        this.logDebug('Play set to:', value);
        if (value === true) {
            this.sending([this.pressedButton('PLAY')]);
        }
        callback(null);
    }
    /////Zidoo controls/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // Maps a logical button name to the exact Zidoo remote-control API key endpoint.
    pressedButton(name) {
        const baseKey = "http://" + this.ZIDOO_IP + ":" + this.ZIDOO_PORT + "/ZidooControlCenter/RemoteControl/sendkey?key=Key.";
        if (name === 'POWER OFF') {
            if (this.config.standby === false) {
                return baseKey + 'PowerOn.Poweroff';
            }
            return baseKey + 'PowerOn.Standby';
        }
        const commandSuffix = BUTTON_KEY_SUFFIXES[name];
        if (typeof commandSuffix === 'undefined') {
            this.logError(`Unknown button command: ${name}`);
            return null;
        }
        return baseKey + commandSuffix;
    }
    // Probes `getModel` and only allows full polling when the player is reachable.
    async probeDeviceAvailability() {
        if (this.config.autoIPMac === true && this.IPReceived !== true) {
            return false;
        }
        const now = Date.now();
        if (this.availabilityProbeInFlight === true) {
            return false;
        }
        if (this.deviceAvailableForPolling === false && now < this.nextAvailabilityProbeAt) {
            return false;
        }
        const probeUrl = this.query('GET MODEL INFO');
        if (!probeUrl) {
            return false;
        }
        this.availabilityProbeInFlight = true;
        try {
            const isAvailable = await this.sending(probeUrl);
            this.deviceAvailableForPolling = isAvailable === true;
            if (this.deviceAvailableForPolling === true) {
                if (this.waitingForDeviceAvailability === true) {
                    this.logDebug('Device is reachable again; resuming status polling.');
                }
                this.waitingForDeviceAvailability = false;
                return true;
            }
            this.nextAvailabilityProbeAt = Date.now() + this.availabilityProbeIntervalMs;
            if (this.waitingForDeviceAvailability === false) {
                this.logDebug('Device is unavailable; waiting before polling all status endpoints.');
                this.waitingForDeviceAvailability = true;
            }
            return false;
        }
        finally {
            this.availabilityProbeInFlight = false;
        }
    }
    ///Query////////////////////////////////////////////////////////////////////////////////////////////////////
    // Builds HTTP polling endpoints for model, music, playback, and volume status.
    query(qName) {
        let key = "http://" + this.ZIDOO_IP + ":" + this.ZIDOO_PORT + "/";
        switch (qName) {
            //POWER ButtonGroup
            case 'GET MODEL INFO':
                key += 'ZidooControlCenter/getModel';
                break;
            case 'MUSIC STATUS':
                key += 'ZidooMusicControl/v2/getState';
                break;
            case 'PLAYBACK STATUS':
                key += 'ZidooVideoPlay/getPlayStatus';
                break;
            case 'VOLUME STATUS':
                key += 'ZidooControlCenter/RemoteControl/changeVolume?type=3';
                break;
            default:
                this.logError(`Unknown query name: ${qName}`);
                return null;
        }
        return key;
    }
    // Probes availability first, then queues periodic status polling while skipping overlap.
    async queuePollingRequests() {
        if (this.pollInFlight === true) {
            this.logDebug('Skipping polling cycle because the previous cycle is still in flight.');
            return;
        }
        this.pollInFlight = true;
        try {
            const isDeviceAvailable = await this.probeDeviceAvailability();
            if (!isDeviceAvailable) {
                return;
            }
            const requests = [
                { delayMs: 0, queryName: 'MUSIC STATUS' },
                { delayMs: 250, queryName: 'PLAYBACK STATUS' },
                { delayMs: 500, queryName: 'VOLUME STATUS' },
            ];
            const tasks = requests.map(async (requestDefinition) => {
                if (requestDefinition.delayMs > 0) {
                    await this.delayByMs(requestDefinition.delayMs);
                }
                const requestUrl = this.query(requestDefinition.queryName);
                if (!requestUrl) {
                    return;
                }
                await this.sending(requestUrl);
            });
            await Promise.allSettled(tasks);
        }
        catch (error) {
            this.logError('Polling cycle failed: ' + error.message);
        }
        finally {
            this.pollInFlight = false;
        }
    }
    // Registers a shutdown hook once so timers/sockets are cleaned up when Homebridge exits.
    registerShutdownHandler() {
        if (!this.platform || !this.platform.api || typeof this.platform.api.on !== 'function') {
            return;
        }
        this.platform.api.on('shutdown', () => {
            this.clearManagedTimers();
            if (this.httpAgent && typeof this.httpAgent.destroy === 'function') {
                this.httpAgent.destroy();
            }
            if (this.discovery) {
                try {
                    this.discovery.close();
                }
                catch (error) {
                    this.logDebug('Error closing discovery socket: ' + error.message);
                }
            }
            if (this.hello) {
                try {
                    this.hello.close();
                }
                catch (error) {
                    this.logDebug('Error closing hello socket: ' + error.message);
                }
            }
        });
    }
    // Routes a parsed HTTP response to the corresponding command-specific handler.
    routeHttpEvent(rawData, key) {
        if (key.includes('PowerOn.Poweroff')) {
            this.turnOffAll();
            return;
        }
        if (key.includes('PowerOn.Standby')) {
            this.turnOffAll();
            this.standbyMode = 1;
            return;
        }
        if (key.includes('volumeStatus')) {
            this.applyVolumePayload(rawData);
            return;
        }
        if (key.includes('MediaPlay')) {
            if (!key.includes('MediaPlay.Pause')) {
                this.newPlayBackState([true, false, false]);
            }
            return;
        }
        if (key.includes('MediaStop')) {
            this.newPlayBackState([false, false, false]);
            this.mediaDetailsReset();
            return;
        }
        if (key.includes('MediaPause')) {
            this.newPlayBackState([false, true, false]);
            return;
        }
        if (key.includes('getModel')) {
            if (this.turnOffCommand !== true && this.standbyMode === 0) {
                this.newPowerState(true);
            }
            return;
        }
        if (key.includes('getPlayStatus')) {
            this.handleVideoStatusPayload(rawData);
            return;
        }
        if (key.includes('getState')) {
            this.handleMusicStatusPayload(rawData);
        }
    }
    // Removes a HomeKit service only when it exists, avoiding startup/shutdown crashes on undefined references.
    safeRemoveService(service) {
        if (typeof service === 'undefined' || service === null) {
            return;
        }
        try {
            this.accessory.removeService(service);
        }
        catch (error) {
            this.logDebug('Error removing service: ' + error.message);
        }
    }
    // Normalizes a user-facing device name into a safe file-name segment for log files.
    sanitizeFileName(name) {
        return `${name || 'zidoo'}`.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_');
    }
    // Schedules an interval and tracks it so it can be cleared during plugin shutdown.
    scheduleInterval(handler, intervalMs) {
        const intervalHandle = setInterval(handler, intervalMs);
        this.managedIntervals.add(intervalHandle);
        return intervalHandle;
    }
    // Schedules a timeout and tracks it so it can be cleared during plugin shutdown.
    scheduleTimeout(handler, delayMs) {
        const timeoutHandle = setTimeout(() => {
            this.managedTimeouts.delete(timeoutHandle);
            handler();
        }, delayMs);
        this.managedTimeouts.add(timeoutHandle);
        return timeoutHandle;
    }
    /////////Data Management/////////////////////////////////////////////////////////////
    // Converts seconds to HH:MM:SS.
    secondsToTime(seconds) {
        let date = new Date(0);
        date.setSeconds(parseInt(seconds)); // specify value for SECONDS here
        let timeString = date.toISOString().substr(11, 8);
        return timeString
    }

    ///////Send HTTP command///////////////////////////

    // Sends one Zidoo HTTP command, parses JSON responses, and suppresses expected offline transport noise for availability/power-off flows.
    sending(url) {
        return new Promise((resolve) => {
            if (this.config.autoIPMac !== false && this.IPReceived !== true) {
                this.logDebug('IP address is not set yet, turn on the device');
                resolve(false);
                return;
            }
            const regexExp = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/;
            if (!regexExp.test(this.ZIDOO_IP)) {
                this.logDebug('IP address is not valid');
                resolve(false);
                return;
            }
            const requestUrl = Array.isArray(url) ? url[0] : url;
            if (typeof requestUrl !== 'string' || requestUrl.length === 0) {
                this.logError('Command URL is invalid or empty.');
                resolve(false);
                return;
            }
            let key;
            if (requestUrl.includes('getState')) {
                key = 'getState';
            }
            else if (requestUrl.includes('getPlayStatus')) {
                key = 'getPlayStatus';
            }
            else if (requestUrl.includes('getModel')) {
                key = 'getModel';
            }
            else if (requestUrl.includes('seek')) {
                key = 'seek';
            }
            else if (requestUrl.includes('RemoteControl/changeVolume?type=')) {
                key = 'volumeStatus';
            }
            else {
                const keyParts = requestUrl.split('=');
                key = keyParts[keyParts.length - 1] || 'UNKNOWN';
            }
            this.logDebug(requestUrl);
            this.logDebug(key);
            this.commandLog(key);
            // Count only real HTTP attempts so offline recovery logic is not triggered by invalid input.
            this.httpNotResponding += 1;
            const suppressExpectedOfflineTransportErrors = key === 'getModel'
                || key === 'Key.PowerOn.Poweroff'
                || key === 'Key.PowerOn.Standby';
            let completed = false;
            let requestTimedOut = false;
            const finish = (success) => {
                if (completed) {
                    return;
                }
                completed = true;
                resolve(success);
            };
            const httpRequest = request.get(requestUrl, { agent: this.httpAgent }, (res) => {
                if (typeof res.statusCode === 'number' && res.statusCode >= 400) {
                    this.logError(`HTTP ${res.statusCode} while sending ${key}`);
                }
                res.setEncoding('utf8');
                let rawData = '';
                res.on('data', (chunk) => { rawData += chunk; });
                res.on('end', () => {
                    try {
                        const parsedData = JSON.parse(rawData);
                        this.httpNotResponding = 0;
                        this.deviceAvailableForPolling = true;
                        this.waitingForDeviceAvailability = false;
                        this.httpEventDecoder(parsedData, key);
                        finish(true);
                    } catch (error) {
                        this.logError(`Failed to parse response for ${key}: ${error.message}`);
                        finish(false);
                    }
                });
            });
            httpRequest.setTimeout(5000, () => {
                requestTimedOut = true;
                const timeoutError = new Error('Request timeout');
                timeoutError.code = 'ETIMEDOUT';
                this.deviceAvailableForPolling = false;
                this.nextAvailabilityProbeAt = Date.now() + this.availabilityProbeIntervalMs;
                if (!(suppressExpectedOfflineTransportErrors && this.isOfflineTransportError(timeoutError))) {
                    this.logError(`HTTP timeout while sending ${key}`);
                }
                httpRequest.destroy(timeoutError);
                finish(false);
            });
            httpRequest.on('error', (error) => {
                if (requestTimedOut === true && error && error.message === 'Request timeout') {
                    finish(false);
                    return;
                }
                const isOfflineFailure = this.isOfflineTransportError(error);
                if (isOfflineFailure) {
                    this.deviceAvailableForPolling = false;
                    this.nextAvailabilityProbeAt = Date.now() + this.availabilityProbeIntervalMs;
                }
                if (!(suppressExpectedOfflineTransportErrors && isOfflineFailure)) {
                    this.logError(`HTTP request failed for ${key}: ${error.message}`);
                }
                finish(false);
            });
        });
    }
    // Converts slider percentage to absolute seek time and sends the appropriate music/video seek endpoint.
    setMovieSeekPositionFromPercent(newValue) {
        let newSendValue = Math.round(newValue * this.movieDuration * 10);
        const totalMovieTime = this.movieDuration * 1000;
        if (newSendValue > totalMovieTime) {
            newSendValue = totalMovieTime;
        }
        if (this.musicPlaying === true) {
            this.sending(["http://" + this.ZIDOO_IP + ":" + this.ZIDOO_PORT + "/ZidooMusicControl/v2/seekTo?time=" + newSendValue]);
        }
        else {
            this.sending(["http://" + this.ZIDOO_IP + ":" + this.ZIDOO_PORT + "/ZidooVideoPlay/seekTo?positon=" + newSendValue]);
        }
        this.newMovieTime(newSendValue / 1000);
    }
    // Handles the optional Power switch service and maps it to wake/shutdown behavior.
    setOn(value, callback) {
        let zidooState = value;
        if (zidooState === true) {
            this.newPowerState(true);
            this.turnOnCommand = true;
            this.turnOffCommand = false;
            this.WakeupOnLAN();
            //this.sending([this.pressedButton('POWER ON')]);
            /*
                this.sending([this.query('GET MODEL INFO')]);
                this.sending([this.query('PLAYBACK STATUS')]);
                this.sending([this.query('MUSIC STATUS')]);
                this.sending([this.pressedButton('POWER ON')]);
                */
        }
        else {
            if (this.playBackState[0] === true || this.playBackState[1] === true) {
                this.sending([this.pressedButton('STOP')]);
                this.newPowerState(false);
                this.turnOffCommand = true;
                this.turnOnCommand = false;
                this.turnOffAll();
                this.scheduleTimeout(() => {
                    this.sending([this.pressedButton('POWER OFF')]);
                }, 100);
            }
            else {
                this.newPowerState(false);
                this.turnOffCommand = true;
                this.turnOnCommand = false;
                this.turnOffAll();
                this.sending([this.pressedButton('POWER OFF')]);
            }
        }
        this.logDebug('Set Power to ->', value);
        callback(null);
    }
    // Configures HomeKit Accessory Information service values.
    setupAccessoryInformationService() {
        this.accessory.getService(this.platform.Service.AccessoryInformation)
            .setCharacteristic(this.platform.Characteristic.Manufacturer, this.config.manufacturer)
            .setCharacteristic(this.platform.Characteristic.Model, this.config.modelName)
            .setCharacteristic(this.platform.Characteristic.SerialNumber, this.config.serialN)
            .setCharacteristic(this.platform.Characteristic.FirmwareRevision, '4.0.0');
    }
    // Creates optional switches and controls as a dedicated setup phase.
    setupOptionalServices(accessory) {
        if (this.config.volume === true) {
            this.buildVolumeControlService();
        }
        if (this.config.movieControl === true) {
            this.buildMovieProgressService();
        }
        if (this.config.powerB === true) {
            // Resolve the power switch by explicit name/subtype to avoid binding to unrelated Switch services.
            const powerSwitchServiceName = `${accessory.context.device.zidooDisplayName} Power Switch`;
            this.service = this.accessory.getService(powerSwitchServiceName)
                || this.accessory.addService(this.platform.Service.Switch, powerSwitchServiceName, 'CataNicoGaTa-PWR');
            this.service.setCharacteristic(this.platform.Characteristic.Name, powerSwitchServiceName);
            this.service.updateCharacteristic(this.platform.Characteristic.Name, powerSwitchServiceName);
            this.service.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
            this.service.setCharacteristic(this.platform.Characteristic.ConfiguredName, powerSwitchServiceName);
            this.service.getCharacteristic(this.platform.Characteristic.On)
                .on('set', this.setOn.bind(this))
                .on('get', this.getOn.bind(this));
        }
        if (!this.config.removePlayback) {
            const playbackSwitchDefinitions = [
                {
                    propertyName: 'play',
                    serviceName: 'Play',
                    uniqueId: 'CataNicoGaTa-10',
                    onGet: this.playSwitchStateGet.bind(this),
                    onSet: this.playSwitchStateSet.bind(this),
                },
                {
                    propertyName: 'pause',
                    serviceName: 'Pause',
                    uniqueId: 'CataNicoGaTa-11',
                    onGet: this.pauseSwitchStateGet.bind(this),
                    onSet: this.pauseSwitchStateSet.bind(this),
                },
                {
                    propertyName: 'stop',
                    serviceName: 'Stop',
                    uniqueId: 'CataNicoGaTa-12',
                    onGet: this.stopSwitchStateGet.bind(this),
                    onSet: this.stopSwitchStateSet.bind(this),
                },
            ];
            for (const definition of playbackSwitchDefinitions) {
                this.createStatefulSwitch(definition);
            }
        }
        this.buildStatelessSwitches();
        if (this.config.muteB === true) {
            this.mute = this.accessory.getService('Mute') ||
                this.accessory.addService(this.platform.Service.Switch, 'Mute', 'CataNicoGaTa-9001');
            this.mute.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
            this.mute.setCharacteristic(this.platform.Characteristic.ConfiguredName, 'Mute');
            this.mute.getCharacteristic(this.platform.Characteristic.On)
                .on('get', (callback) => {
                    this.logDebug('Mute GET On');
                    let currentValue = false;
                    if (this.isMuteAvailable === true) {
                        currentValue = this.isMute;
                    }
                    callback(null, currentValue);
                })
                .on('set', (value, callback) => {
                    this.logDebug('Mute SET On:', value);
                    if (value === true) {
                        this.volumeBeforeMute = this.currentVolume;
                        this.sending([this.pressedButton('MUTE')]);
                    }
                    if (this.isMuteAvailable === false) {
                        this.scheduleTimeout(() => {
                            this.mute.updateCharacteristic(this.platform.Characteristic.On, false);
                        }, this.statelessTimeOut);
                    }
                    else if (value === false) {
                        this.volumeChange(this.volumeBeforeMute);
                    }
                    callback(null);
                });
        }
        if (this.config.remainMovieTimer) {
            // Resolve movie timer by explicit name/subtype to avoid accidental binding to unrelated Valve services.
            this.movieTimer = this.accessory.getService('Zidoo Movie Timer')
                || this.accessory.addService(this.platform.Service.Valve, 'Zidoo Movie Timer', 'Movie Timer');
            this.movieTimer.setCharacteristic(this.platform.Characteristic.Name, 'Zidoo Timer');
            this.movieTimer.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
            this.movieTimer.setCharacteristic(this.platform.Characteristic.ConfiguredName, 'Zidoo Timer');
            this.movieTimer.setCharacteristic(this.platform.Characteristic.ValveType, this.platform.Characteristic.ValveType.IRRIGATION);
            this.movieTimer.getCharacteristic(this.platform.Characteristic.Active)
                .on('get', (callback) => {
                    const currentValue = this.currentMovieProgressState ? 1 : 0;
                    callback(null, currentValue);
                })
                .on('set', (value, callback) => {
                    callback(null);
                });
            this.movieTimer.setCharacteristic(this.platform.Characteristic.InUse, this.platform.Characteristic.InUse.NOT_IN_USE);
            this.movieTimer.getCharacteristic(this.platform.Characteristic.RemainingDuration)
                .on('get', (callback) => {
                    const currentValue = (this.movieDuration - this.movieElapsed);
                    callback(null, currentValue);
                })
                .setProps({
                    maxValue: 86400 / 4,
                });
            this.movieTimer.getCharacteristic(this.platform.Characteristic.SetDuration)
                .on('get', (callback) => {
                    const currentValue = this.movieDuration;
                    callback(null, currentValue);
                })
                .setProps({
                    maxValue: 86400 / 4,
                });
        }
    }
    // Configures TelevisionSpeaker service and links it to the TV service.
    setupSpeakerService() {
        this.speakerService = this.accessory.getService('Zidoo Volume Control')
            || this.accessory.addService(this.platform.Service.TelevisionSpeaker, 'Zidoo Volume Control', 'CataNicoGaTa-20');
        this.speakerService
            .setCharacteristic(this.platform.Characteristic.Active, this.platform.Characteristic.Active.ACTIVE)
            .setCharacteristic(this.platform.Characteristic.VolumeControlType, this.platform.Characteristic.VolumeControlType.ABSOLUTE);
        this.speakerService.getCharacteristic(this.platform.Characteristic.VolumeSelector)
            .on('set', (newValue, callback) => {
                if (newValue === 1) {
                    this.sending([this.pressedButton('VOLUME DOWN')]);
                }
                if (newValue === 0) {
                    this.sending([this.pressedButton('VOLUME UP')]);
                }
                callback(null);
            });
        this.speakerService.getCharacteristic(this.platform.Characteristic.Mute)
            .on('get', (callback) => {
                let currentValue = this.currentMuteState;
                if (this.isMuteAvailable === true) {
                    currentValue = this.isMute;
                    this.currentMuteState = this.isMute;
                }
                callback(null, currentValue);
            })
            .on('set', (newValue, callback) => {
                this.logDebug('Volume Value set to: Mute/Unmute');
                if (newValue === true) {
                    this.volumeBeforeMute = this.currentVolume;
                    this.volumeChange(0);
                }
                else {
                    this.volumeChange(this.volumeBeforeMute);
                }
                callback(null);
            });
        this.speakerService.addCharacteristic(this.platform.Characteristic.Volume)
            .on('get', (callback) => {
                callback(null, this.currentVolume);
            })
            .on('set', (newValue, callback) => {
                this.volumeChange(newValue);
                this.logDebug('Volume Value set to: ' + newValue);
                callback(null);
            });
        this.tvService.addLinkedService(this.speakerService);
    }
    // Configures the main Television service and media-state characteristics.
    setupTelevisionServices() {
        this.tvService = this.accessory.getService(this.config.name) ||
            this.accessory.addService(this.platform.Service.Television, this.config.name, 'CataNicoGaTa-7');
        this.tvService.setCharacteristic(this.platform.Characteristic.ConfiguredName, this.config.name);
        this.tvService.setCharacteristic(this.platform.Characteristic.SleepDiscoveryMode, this.platform.Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE);
        this.tvService.getCharacteristic(this.platform.Characteristic.Active)
            .on('get', (callback) => {
                callback(null, this.powerStateTV);
            })
            .on('set', (newValue, callback) => {
                this.logDebug('Set Zidoo Active to: ' + newValue);
                if (newValue === 1) {
                    this.newPowerState(true);
                    this.turnOnCommand = true;
                    this.turnOffCommand = false;
                    this.WakeupOnLAN();
                }
                else if (this.playBackState[0] === true || this.playBackState[1] === true) {
                    this.sending([this.pressedButton('STOP')]);
                    this.newPowerState(false);
                    this.turnOffCommand = true;
                    this.turnOnCommand = false;
                    this.turnOffAll();
                    this.scheduleTimeout(() => {
                        this.sending([this.pressedButton('POWER OFF')]);
                    }, 100);
                }
                else {
                    this.newPowerState(false);
                    this.turnOffCommand = true;
                    this.turnOnCommand = false;
                    this.turnOffAll();
                    this.sending([this.pressedButton('POWER OFF')]);
                }
                callback(null);
            });
        this.tvService.getCharacteristic(this.platform.Characteristic.ClosedCaptions)
            .on('get', (callback) => {
                this.logDebug('Subtitle GET On');
                callback(null, 0);
            })
            .on('set', (value, callback) => {
                this.logDebug('Subtitle SET On:', value);
                if (value === 1) {
                    this.sending([this.pressedButton('SUBTITLE')]);
                }
                this.tvService.updateCharacteristic(this.platform.Characteristic.ClosedCaptions, 0);
                callback(null);
            });
        this.tvService.getCharacteristic(this.platform.Characteristic.Brightness)
            .on('get', (callback) => {
                callback(null, this.currentVolume);
            })
            .on('set', (newValue, callback) => {
                this.volumeChange(newValue);
                this.logDebug('Volume Value set to: ' + newValue);
                callback(null);
            });
        this.tvService.getCharacteristic(this.platform.Characteristic.PictureMode)
            .on('set', (newValue, callback) => {
                if (newValue === 1) {
                    this.sending([this.pressedButton('VOLUME DOWN')]);
                }
                if (newValue === 0) {
                    this.sending([this.pressedButton('VOLUME UP')]);
                }
                callback(null);
            });
        this.tvService.getCharacteristic(this.platform.Characteristic.RemoteKey)
            .on('set', (newValue, callback) => {
                switch (newValue) {
                    case this.platform.Characteristic.RemoteKey.REWIND: {
                        this.logDebug('set Remote Key Pressed: REWIND');
                        this.sending([this.pressedButton('REWIND')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.FAST_FORWARD: {
                        this.logDebug('set Remote Key Pressed: FAST_FORWARD');
                        this.sending([this.pressedButton('FORWARD')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.NEXT_TRACK: {
                        this.logDebug('set Remote Key Pressed: NEXT_TRACK');
                        this.sending([this.pressedButton('NEXT')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.PREVIOUS_TRACK: {
                        this.logDebug('set Remote Key Pressed: PREVIOUS_TRACK');
                        this.sending([this.pressedButton('PREVIOUS')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.ARROW_UP: {
                        this.logDebug('set Remote Key Pressed: ARROW_UP');
                        this.sending([this.pressedButton('CURSOR UP')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.ARROW_DOWN: {
                        this.logDebug('set Remote Key Pressed: ARROW_DOWN');
                        this.sending([this.pressedButton('CURSOR DOWN')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.ARROW_LEFT: {
                        this.logDebug('set Remote Key Pressed: ARROW_LEFT');
                        this.sending([this.pressedButton('CURSOR LEFT')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.ARROW_RIGHT: {
                        this.logDebug('set Remote Key Pressed: ARROW_RIGHT');
                        this.sending([this.pressedButton('CURSOR RIGHT')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.SELECT: {
                        this.logDebug('set Remote Key Pressed: SELECT');
                        this.sending([this.pressedButton('CURSOR ENTER')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.BACK: {
                        this.logDebug('set Remote Key Pressed: BACK');
                        this.sending([this.pressedButton('BACK')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.EXIT: {
                        this.logDebug('set Remote Key Pressed: EXIT');
                        this.sending([this.pressedButton('HOME MENU')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.PLAY_PAUSE: {
                        this.logDebug('set Remote Key Pressed: PLAY_PAUSE');
                        this.sending([this.pressedButton('PLAY/PAUSE')]);
                        break;
                    }
                    case this.platform.Characteristic.RemoteKey.INFORMATION: {
                        if (this.config.infoToMenu) {
                            this.logDebug('set Remote Key Pressed: MENU');
                            this.sending([this.pressedButton('MENU')]);
                        }
                        else {
                            this.logDebug('set Remote Key Pressed: INFORMATION');
                            this.sending([this.pressedButton('INFO')]);
                        }
                        break;
                    }
                }
                callback(null);
            });
        this.tvService.setCharacteristic(this.platform.Characteristic.ActiveIdentifier, this.inputID);
        this.tvService.getCharacteristic(this.platform.Characteristic.ActiveIdentifier)
            .on('set', (inputIdentifier, callback) => {
                this.logDebug('Active Identifier set to:', inputIdentifier);
                this.inputID = inputIdentifier;
                callback();
            })
            .on('get', (callback) => {
                this.logDebug('Active Identifier set to:', this.inputID);
                callback(null, this.inputID);
            });
        this.tvService.getCharacteristic(this.platform.Characteristic.PowerModeSelection)
            .on('set', (newValue, callback) => {
                this.logDebug('Requested Zidoo Settings ' + newValue);
                this.sending([this.pressedButton('MENU')]);
                callback();
            });
        this.buildInputSources();
        this.tvService.getCharacteristic(this.platform.Characteristic.CurrentMediaState)
            .on('get', (callback) => {
                this.logDebug('Current Playback State', this.mediaState);
                callback(null, this.mediaState);
            });
        this.tvService.getCharacteristic(this.platform.Characteristic.TargetMediaState)
            .on('get', (callback) => {
                let currentValue = this.mediaState;
                if (this.mediaState === 4) {
                    currentValue = 2;
                }
                this.logDebug('Current Playback State', currentValue);
                callback(null, currentValue);
            })
            .on('set', (value, callback) => {
                if (value === 0) {
                    this.sending([this.pressedButton('PLAY')]);
                }
                else if (value === 1) {
                    this.sending([this.pressedButton('PAUSE')]);
                }
                else if (value === 2) {
                    this.sending([this.pressedButton('STOP')]);
                }
                this.logDebug('Playback State set to:', value);
                callback(null);
            });
    }
    // Starts UDP discovery workflows when auto IP/MAC mode is enabled.
    startConnectivityServices() {
        if (this.config.autoIPMac === true) {
            this.discoveryUPD();
        }
    }
    // Starts periodic polling/history intervals as a dedicated startup phase.
    startSynchronizationLoops() {
        this.scheduleInterval(() => {
            if (this.turnOffCommand === false && this.turnOnCommand === false) {
                this.queuePollingRequests();
                if (this.httpNotResponding >= this.reconnectionTry && this.httpNotResponding <= this.reconnectionTry + 4) {
                    this.turnOffAll();
                    this.standbyMode = 0;
                    if (this.config.autoIPMac === true) {
                        this.helloMessage();
                    }
                }
                this.syncPolledCharacteristics();
            }
            else {
                this.scheduleTimeout(() => {
                    this.turnOffCommand = false;
                    this.turnOnCommand = false;
                }, 6000);
            }
        }, this.config.pollingInterval);
        this.scheduleInterval(() => {
            if (this.tvService.getCharacteristic(this.platform.Characteristic.Active).value === 1 && this.playBackState[0] === true) {
            }
        }, 120000);
    }
    /////////////////////////////////////////////////////////////////////////////////////stop
    // Reports whether playback is currently in the stopped state.
    stopSwitchStateGet(callback) {
        this.logDebug('Stop State');
        let currentValue = this.playBackState[2];
        callback(null, currentValue);
    }
    // Sends a STOP key when the Stop switch is toggled on.
    stopSwitchStateSet(value, callback) {
        this.logDebug('Stop set to:', value);
        if (value === true) {
            this.sending([this.pressedButton('STOP')]);
        }
        callback(null);
    }
    // Removes common media-file extensions from display names while keeping all other names unchanged.
    stripKnownMediaExtension(name) {
        if (typeof name !== 'string') {
            return '';
        }
        return name.replace(/\.(iso|mkv|mp4|mp3)$/i, '');
    }
    // Synchronizes HomeKit characteristic cache values from the latest internal state snapshot.
    syncPolledCharacteristics() {
        if (this.tvService.getCharacteristic(this.platform.Characteristic.Active).value !== this.powerStateTV) {
            this.tvService.updateCharacteristic(this.platform.Characteristic.Active, this.powerStateTV);
        }
        if (!this.config.removePlayback) {
            if (this.play.getCharacteristic(this.platform.Characteristic.On).value !== this.playBackState[0]) {
                this.play.updateCharacteristic(this.platform.Characteristic.On, this.playBackState[0]);
            }
            if (this.pause.getCharacteristic(this.platform.Characteristic.On).value !== this.playBackState[1]) {
                this.pause.updateCharacteristic(this.platform.Characteristic.On, this.playBackState[1]);
            }
            if (this.stop.getCharacteristic(this.platform.Characteristic.On).value !== this.playBackState[2]) {
                this.stop.updateCharacteristic(this.platform.Characteristic.On, this.playBackState[2]);
            }
        }
        if (this.videoAudioTitle.getCharacteristic(this.platform.Characteristic.ConfiguredName).value !== this.inputName) {
            this.logDebug('Updating Title');
            this.videoAudioTitle.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.inputName);
        }
        if (this.runtime.getCharacteristic(this.platform.Characteristic.ConfiguredName).value !== this.mediaDuration) {
            this.logDebug('Updating Runtime');
            this.runtime.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.mediaDuration);
        }
        if (this.videoFormat.getCharacteristic(this.platform.Characteristic.ConfiguredName).value !== this.mediaVideoFormat) {
            this.logDebug('Updating Video Information');
            this.videoFormat.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.mediaVideoFormat);
        }
        if (this.audioFormat.getCharacteristic(this.platform.Characteristic.ConfiguredName).value !== this.mediaAudioFormat) {
            this.logDebug('Updating Audio Information');
            this.audioFormat.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.mediaAudioFormat);
        }
        if (this.audioLanguage.getCharacteristic(this.platform.Characteristic.ConfiguredName).value !== this.language) {
            this.logDebug('Updating Language');
            this.audioLanguage.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.language);
        }
        if (this.videoAudioElapseTime.getCharacteristic(this.platform.Characteristic.ConfiguredName).value !== this.subtitleInfo) {
            this.logDebug('Updating Additional Information');
            this.videoAudioElapseTime.updateCharacteristic(this.platform.Characteristic.ConfiguredName, this.subtitleInfo);
        }
    }
    // Returns a timestamp string used in history logs.
    time() {
        let time = new Date();
        return time.toLocaleDateString() + ' ' + time.toLocaleString('en-US', {
            hour12: false,
            hour: 'numeric',
            minute: 'numeric',
            second: 'numeric',
            timeZoneName: 'short',
        });
    }
    // Converts potentially string-based values to finite numbers with a fallback default.
    toFiniteNumber(value, defaultValue = 0) {
        const parsedValue = Number(value);
        return Number.isFinite(parsedValue) ? parsedValue : defaultValue;
    }
    // Resets all runtime state to a fully off/idle baseline.
    turnOffAll() {
        this.newPowerState(false);
        this.newPlayBackState([false, false, false]);
        this.newInputState([false, false, false, false, false, false]);
        this.mediaDetailsReset();
        this.moviePlaying = false;
        this.musicPlaying = false;
        this.newVolumeStatus(0);
        this.isMute = false;

    }
    ////Update instructions
    // Applies an input-state update while writing a decoded command response log.
    updateInputStatus(newInput, response) {
        this.logDebug(`Response: ${this.commandName(response)}`);
        this.newInputState(newInput);
    }
    // Refreshes MAC cache from successful HTTP payloads when the device reports a MAC value.
    updateMacFromHttpPayload(rawData) {
        if (typeof rawData.net_mac !== 'undefined' && this.config.mac !== rawData.net_mac) {
            this.config.mac = rawData.net_mac;
        }
        this.macReceived = true;
    }
    // Derives a user-facing media title from the video payload and updates the active input label.
    updateVideoTitle(videoTitle, videoPath, movie3D) {
        let nameInput = this.stripKnownMediaExtension(videoTitle);
        if (videoTitle === 'AVCHD') {
            const pathParts = typeof videoPath === 'string' ? videoPath.split('/') : [];
            if (pathParts[pathParts.length - 1] === 'AVCHD' && typeof pathParts[pathParts.length - 2] === 'string') {
                nameInput = this.stripKnownMediaExtension(pathParts[pathParts.length - 2]);
            }
        }
        this.newInputName(this.nameLengthCheck(`${nameInput}${movie3D}`));
    }
    /////

    ///////Handlers////////////////////////////////////////////////////////////////////////////////////////
    // Converts HomeKit percentage volume to Zidoo's native range and sends the HTTP volume command.
    volumeChange(newVolume) {
        let newVolumeValue = Math.round(Math.min(Math.max((this.maxVolume * newVolume) / 100, 0), this.maxVolume));
        this.sending(["http://" + this.ZIDOO_IP + ":" + this.ZIDOO_PORT + "/ZidooControlCenter/RemoteControl/changeVolume?type=2&value=" + newVolumeValue]);

    }


    ///////////////Wake up/////
    // Powers the player using WOL, or falls back to MENU when the device is in standby mode.
    WakeupOnLAN() {
        if (this.standbyMode === 1) {
            this.sending([this.pressedButton('MENU')]);
        }
        else {
            if (this.config.mac !== 'CA:TA:NI:CO:GA:TA') {
                this.macReceived = true;
            }
            if (this.config.autoIPMac === false || this.macReceived === true) {
                WOL.wake(this.config.mac, (err) => {
                    if (err) {
                        this.logInfo(`Could not wake up ${this.config.name}: ${err}`);
                        this.newPowerState(false);
                    }
                    this.logInfo(`${this.config.name} woke up!`);
                    this.newPowerState(true);
                    this.turnOffCommand = true;
                    this.scheduleTimeout(() => {
                        if (this.powerState === false) {
                            this.logInfo(`Startup time of 60 seconds expired, reverting state to offline`);
                        }
                    }, 60000);
                });
            }
            else {
                this.logInfo('IP and MAC addresses are not set yet, turn on the device')
            }
        }
    }
}
// Export internals for unit testing without affecting Homebridge runtime behavior.
module.exports.zidooAccessory = zidooAccessory;
module.exports.zidooPlatform = zidooPlatform;
module.exports._private = {
    BUTTON_KEY_SUFFIXES,
    COMMAND_NAME_MATCHES,
};