diff --git a/.gitignore b/.gitignore index 7abe7b4..3626bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ ehthumbs.db Thumbs.db Icon +.vscode node_modules dist \ No newline at end of file diff --git a/config.schema.json b/config.schema.json index 60d69f4..ac72a9b 100644 --- a/config.schema.json +++ b/config.schema.json @@ -82,7 +82,7 @@ "wait_time": {"type": "integer"}, "api_key": {"type": "string"}, "device_id": {"type": "string"}, - + "use_smartthings_power": {"type": "boolean"}, "name": { "type": "string", "required": true @@ -240,8 +240,8 @@ "default": "Bedroom TV" }, { - "type": "help", - "helpvalue": "

Make sure that your device have a static IP address. You can set it from your router admin interface!

" + "type": "help", + "helpvalue": "

Make sure that your device has a static IP address. You can set it from your router admin interface!

" }, { "type": "flex", @@ -264,8 +264,8 @@ "title": "SmartThings API", "items": [ { - "type": "help", - "helpvalue": "

SmartThings API is optional. You will need to set these parameters only if you want to use inputs! Please follow this tutorial.

" + "type": "help", + "helpvalue": "

SmartThings API is optional but highly recommended for more reliable power state (see \"Use SmartThings for Power State\") detection and input management. If provided, it will be used for enhanced functionality such as fetching the power state more effectively and managing inputs. Please follow this tutorial to set it up.

" }, { "key": "devices[].api_key", @@ -274,7 +274,14 @@ { "key": "devices[].device_id", "title": "Device ID" - } + }, + { + "key": "devices[].use_smartthings_power", + "type": "boolean", + "default": false, + "title": "Use SmartThings for Power State", + "description": "Enable to use the SmartThings API to check the power state of the TV. This could provide more accurate power readings. Ensure you have provided a valid API Key and Device ID." + } ] }, { @@ -285,7 +292,7 @@ "items": [ { "type": "help", - "helpvalue": "

By default no inputs are set. Use this section to set your own inputs. You can find more informations on our documentation.

" + "helpvalue": "

By default, no inputs are set. Use this section to configure your own inputs. For more information, please refer to our documentation.

" }, { "type": "array", @@ -355,7 +362,7 @@ "items": [ { "type": "help", - "helpvalue": "

This section give you the option to create custom switches (separated accessories) that make specific actions. You can find more informations on our documentation.

" + "helpvalue": "

This section allows you to create custom switches (separate accessories) that perform specific actions. For more information, please visit our documentation.

" }, { "type": "array", @@ -477,7 +484,7 @@ "wss": "Default", "frame": "Frame" }, - "description": "The plugin detects the type of TV automaticaly. Leave this option set to None unless you are having problems and the plugin don't detect your TV type correctly." + "description": "The plugin detects the type of TV automatically. Leave this option set to None unless you are having problems and the plugin don't detect your TV type correctly." }, { "key": "devices[].uuid", @@ -489,14 +496,14 @@ "key": "devices[].delay", "title": "Command Delay Interval", "placeholder": "400", - "description": "This is the delay between each command when you send multiple commands. By lowering the value you risk the commands not being executed. Value is in miliseconds." + "description": "This is the delay between each command when you send multiple commands. By lowering the value you risk the commands not being executed. Value is in milliseconds." }, { "key": "devices[].options", "title": "Options", "titleMap": { - "Switch.DeviceName.Disable": "Disable prepending device name on custom switches", - "Frame.RealPowerMode": "Display and control Real Power with Main Acccessory (for Frame TVs)", + "Switch.DeviceName.Disable": "Disable prepending the device name on custom switches", + "Frame.RealPowerMode": "Display and control Real Power with Main Accessory (for Frame TVs)", "Frame.ArtSwitch.Disable": "Disable Art Switch (for Frame TVs)", "Frame.PowerSwitch.Disable": "Disable Power Switch (for Frame TVs)" } @@ -511,7 +518,7 @@ "items": [ { "type": "help", - "helpvalue": "

You don't have to edit this section unless you want to change the default commands for Remote Control buttons. You can find more informations on our documentation.

", + "helpvalue": "

You do not need to edit this section unless you want to change the default commands for Remote Control buttons. For more information, please refer to our documentation.

", "flex": "1 1 100%" }, { diff --git a/lib/device.js b/lib/device.js index 946444a..ab693ba 100644 --- a/lib/device.js +++ b/lib/device.js @@ -23,7 +23,8 @@ module.exports = class Device extends EventEmitter { wol : {}, options : [], api_key : null, - device_id : null + device_id : null, + use_smartthings_power : false }, Platform.config, config diff --git a/lib/methods/base.js b/lib/methods/base.js index a44722a..5b6ed69 100644 --- a/lib/methods/base.js +++ b/lib/methods/base.js @@ -19,21 +19,32 @@ module.exports = class Base { this.cache = device.cache; } + /** + * Cleans up resources or state. + */ destroy() { this.destroyed = true; } + /** + * Simulates a pairing delay. + * @return {Promise} + */ pair() { return utils.delay(500); } /** - * Get state of TV with cache - * @return {Cache} + * Retrieves the TV's power state by first attempting a network ping. + * If the ping succeeds, it validates the power state via HTTP. + * If the ping fails, it assumes the TV is off, unless previously recorded as on. + * @return {Promise} Resolves with true if on, false if off. */ getState() { return this.cache.get('state', () => this.getStatePing().then(status => { + this.device.log.debug(`Ping status: ${status}`); if (status && this.device.storage.powerstate !== false) { + this.device.log.debug('Ping succeeded, fetching power state via HTTP...'); return this.cache.get('state-http', () => this.getStateHttp(status), 2500); } else { this.cache.forget('state-http'); @@ -44,10 +55,11 @@ module.exports = class Base { } /** - * Get state of TV by sending a Ping - * @return {Promise} + * Checks TV availability via network ping. + * @return {Promise} */ getStatePing() { + this.device.log.debug(`Pinging ${this.ip} with timeout ${this.timeout}ms.`); return isPortReachable(8001, { host: this.ip, timeout: this.timeout @@ -55,13 +67,15 @@ module.exports = class Base { } /** - * Get state of TV from PowerState response - * @param {boolean} fallback - * @return {Promise} + * Fetches the TV's power state via its local API. + * @param {boolean} fallback - Fallback value if HTTP request fails. + * @return {Promise} */ getStateHttp(fallback = false) { + this.device.log.debug(`Fetching power state from ${this.ip} local TV API.`); return this.getInfo().then(data => { if (data.device && data.device.PowerState) { + this.device.log.debug(`Power state: ${data.device.PowerState}`); return data.device.PowerState == 'on'; } @@ -71,8 +85,8 @@ module.exports = class Base { } /** - * Turn the TV On - * @return {Promise} + * Powers on the TV. Uses Wake-on-LAN if necessary. + * @return {Promise} */ async setStateOn() { // If TV is in Sleep mode just send command @@ -90,16 +104,16 @@ module.exports = class Base { } /** - * Turn the TV Off - * @return {Promise} + * Powers off the TV. + * @return {Promise} */ setStateOff() { return this.click('KEY_POWER'); } /** - * Get TV informations - * @return {Promise} + * Retrieves device information from the TV's API. + * @return {Promise} */ getInfo() { return fetch(`http://${this.ip}:8001/api/v2/`, { @@ -110,9 +124,9 @@ module.exports = class Base { } /** - * Get Application Informations - * @param {Number} appId - * @return {Promise} + * Retrieves information about a specific application. + * @param {number} appId - The application ID. + * @return {Promise} */ getApplication(appId) { return fetch(`http://${this.ip}:8001/api/v2/applications/${appId}`, { @@ -122,9 +136,9 @@ module.exports = class Base { } /** - * Launch Application - * @param {Number} appId - * @return {Promise} + * Launches an application on the TV. + * @param {number} appId - The application ID to launch. + * @return {Promise} */ startApplication(appId) { return fetch(`http://${this.ip}:8001/api/v2/applications/${appId}`, { @@ -135,8 +149,8 @@ module.exports = class Base { } /** - * Encode TV name to base64 - * @return {string} + * Encodes the TV name to base64. + * @return {string} The encoded TV name. */ _encodeName() { return new Buffer.from(this.name).toString('base64'); diff --git a/lib/remote.js b/lib/remote.js index da0681c..a0faba5 100644 --- a/lib/remote.js +++ b/lib/remote.js @@ -63,7 +63,7 @@ module.exports = class Remote { let method = MethodWSS; - switch(this.device.config.method) { + switch (this.device.config.method) { case 'ws': method = MethodWS; break; @@ -119,6 +119,16 @@ module.exports = class Remote { if (this.turningOff !== null) { return false; } if (this.standbyMode !== null) { return false; } + // If SmartThings API is available, use it to get the power state reliably, otherwise fall back to default API method + if (this.smartthings.available && this.device.config.use_smartthings_power) { + return this.cache.get('smartthings-power-state', () => this.smartthings.getPowerState(), 2500) + .then(powerState => { + if (powerState !== null) { return powerState; } + this.device.log.debug("SmartThings API call failed, falling back to default API method."); + return this.api.getState(); + }); + } + return await this.api.getState(); } @@ -339,7 +349,7 @@ module.exports = class Remote { if (split[1]) { if (/^.*\*[0-9]*[.]?[0-9]+s$/.test(cmd)) { - return {key: split[0], time: parseFloat(split[1])}; + return { key: split[0], time: parseFloat(split[1]) }; } return Array(parseInt(split[1])).fill(split[0]); diff --git a/lib/smartthings.js b/lib/smartthings.js index d145832..059f845 100644 --- a/lib/smartthings.js +++ b/lib/smartthings.js @@ -22,28 +22,37 @@ module.exports = class SmartThings { this.api_device = this.api_devices + this.device_id; this.api_status = this.api_device + '/states'; this.api_command = this.api_device + '/commands'; - this.api_headers = {'Authorization': 'Bearer ' + this.api_key}; + this.api_switch = this.api_device + '/components/main/capabilities/switch/status'; + this.api_headers = { 'Authorization': 'Bearer ' + this.api_key }; // Emit init event setTimeout(() => this.device.emit('smartthings.init')); } + /** + * Retrieves the status of the device. + * @returns {Promise} A promise that resolves to an object containing the device status. + */ getStatus() { return this.device.cache.get(`st-status`, () => this.refresh() .then(() => this._send(this.api_status)) .catch(() => ({}) - ), 2500).then(response => ({ - pictureMode: response.main.pictureMode, - tvChannel: response.main.tvChannel, - tvChannelName: response.main.tvChannelName, - inputSource: response.main.inputSource - })); + ), 2500).then(response => ({ + pictureMode: response.main.pictureMode, + tvChannel: response.main.tvChannel, + tvChannelName: response.main.tvChannelName, + inputSource: response.main.inputSource + })); } getPictureMode() { return this.getStatus().then(response => response.pictureMode); } + /** + * Retrieves the current input source of the TV. + * @returns {Promise} A promise that resolves with the input source of the TV. + */ getInputSource() { return this.getStatus().then(response => { // Some TVs report inputSource as dtv, transform it to digitalTv @@ -61,26 +70,83 @@ module.exports = class SmartThings { }); } + /** + * Fetches the current powers state of the device (switch capability) from the SmartThings API. + * Returns a boolean, representing the device's power state, if successful, or null if an error occurs. + * @returns {Promise} + */ + getPowerState() { + return this._send(this.api_switch) + .then(response => { + // Check the response for the switch state + if (response && response.switch && typeof response.switch.value === 'string') { + this.device.log.debug(`Switch state fetched: ${response.switch.value}`); + // Return true if 'on', otherwise false + return response.switch.value === 'on'; + } + // If response structure is not as expected, log and return null + throw new SmartThingsResponse('Unexpected response structure when fetching switch (power) state'); + }) + .catch(error => { + // Log any errors and return null + this.device.log.debug(`Error fetching switch state: ${error}`); + return new Promise(null); + }); + } + + /** + * Refreshes the main component's capability. + * @returns {Promise} A promise that resolves when the refresh command is sent. + */ refresh() { - return this.sendCommands({component: 'main', capability: 'refresh', command: 'refresh'}); + return this.sendCommands({ component: 'main', capability: 'refresh', command: 'refresh' }); } + /** + * Sets the input source for the device. + * + * @param {string} value - The input source value to set. + * @returns {Promise} A promise that resolves when the input source is set successfully. + */ setInputSource(value) { return this.sendCommands({component: 'main', capability: 'mediaInputSource', command: 'setInputSource', arguments: [value]}); } + /** + * Sets the picture mode of the device. + * + * @param {string} value - The value representing the picture mode. + * @returns {Promise} A promise that resolves when the command is sent successfully. + */ setPictureMode(value) { - return this.sendCommands({component: 'main', capability: 'custom.picturemode', command: 'setPictureMode', arguments: [value]}); + return this.sendCommands({ component: 'main', capability: 'custom.picturemode', command: 'setPictureMode', arguments: [value] }); } + /** + * Sets the TV channel. + * + * @param {string} value - The channel value to set. + * @returns {Promise} A promise that resolves when the command is sent. + */ setTvChannel(value) { - return this.sendCommands({component: 'main', capability: 'tvChannel', command: 'setTvChannel', arguments: [value + '']}); + return this.sendCommands({ component: 'main', capability: 'tvChannel', command: 'setTvChannel', arguments: [value + ''] }); } + /** + * Sets the volume of the audio. + * + * @param {number} value - The volume value to set. + * @returns {Promise} A promise that resolves when the volume is set. + */ setVolume(value) { - return this.sendCommands({component: 'main', capability: 'audioVolume', command: 'setVolume', arguments: [parseInt(value)]}); + return this.sendCommands({ component: 'main', capability: 'audioVolume', command: 'setVolume', arguments: [parseInt(value)] }); } + /** + * Sends commands to the SmartThings API. + * @param {Array|Object} commands - The commands to send. Can be an array of commands or a single command object. + * @returns {Promise} A promise that resolves with the response from the API. + */ sendCommands(commands) { if (!Array.isArray(commands)) { commands = [commands]; @@ -91,6 +157,13 @@ module.exports = class SmartThings { }, 'post'); } + /** + * Sends a request to the specified endpoint with the provided data using the specified HTTP method. + * @param {string} endpoint - The URL endpoint to send the request to. + * @param {object} data - The data to send in the request body (optional). + * @param {string} method - The HTTP method to use for the request (optional, defaults to 'get'). + * @returns {Promise} - A promise that resolves with the response data or rejects with an error. + */ _send(endpoint, data, method) { this._validate(); @@ -103,28 +176,32 @@ module.exports = class SmartThings { body: JSON.stringify(data), headers: this.api_headers }) - .then(response => { - const contentType = response.headers.get('content-type'); + .then(response => { + const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.includes('application/json')) { - throw new SmartThingsResponse(`${response.status} - ${response.statusText}`); - } + if (!contentType || !contentType.includes('application/json')) { + throw new SmartThingsResponse(`${response.status} - ${response.statusText}`); + } - return response.json(); - }) - .then(response => { - this.device.log.debug(JSON.stringify(response)); + return response.json(); + }) + .then(response => { + this.device.log.debug(JSON.stringify(response)); - if (response.error) { - reject(response.error); - } + if (response.error) { + reject(response.error); + } - resolve(response); - }) - .catch(error => reject(error)); + resolve(response); + }) + .catch(error => reject(error)); }); } + /** + * Validates the API key and device ID are set. + * @throws {SmartThingsNotSet} If the API key or device ID is not set. + */ _validate() { if (this.api_key && this.device_id) return; diff --git a/package.json b/package.json index 4a863f9..81edcb2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "homebridge-samsung-tizen", "displayName": "Homebridge Samsung Tizen", "version": "5.2.7", - "description": "Homebridge plugin for Samsung TV's with Tizen OS", + "description": "Homebridge plugin for Samsung TVs with Tizen OS", "main": "index.js", "directories": { "lib": "lib"