diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ac35ef..7adaa03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- New `getPowerState` function on `LGTV` allowing testing if the TV is on, off, + or in an unknown state. + ## 4.1.1 - 2023-12-31 ### Fixed diff --git a/packages/lgtv-ip-control/README.md b/packages/lgtv-ip-control/README.md index 83ef7b6..30de8ac 100644 --- a/packages/lgtv-ip-control/README.md +++ b/packages/lgtv-ip-control/README.md @@ -156,6 +156,19 @@ Gets the mute state. const muteState = await lgtv.getMuteState(); ``` +### `.getPowerState(): Promise` + +Gets the current TV power state. + +Because the TV might be offline, you should call this function before calling +`.connect()`, otherwise you can get a `TimeoutError`. + +```js +const powerState = await lgtv.getPowerState(); +``` + +See [`PowerStates`](#PowerStates) for available states. + ### `.powerOff(): Promise` Powers the TV off. @@ -319,6 +332,14 @@ See [`ScreenMuteModes`](#ScreenMuteModes) for available modes. | volumeUp | Volume Up | | yellowButton | Yellow Button | +### PowerStates + +| Key | State | +| ------- | -------------------------------------------- | +| on | The TV is on and responding to connections | +| off | The TV is off or powering off | +| unknown | The state of the TV is unknown, possibly off | + ### ScreenMuteModes | Key | Effect | diff --git a/packages/lgtv-ip-control/src/classes/LGTV.ts b/packages/lgtv-ip-control/src/classes/LGTV.ts index 1d32e94..f140922 100644 --- a/packages/lgtv-ip-control/src/classes/LGTV.ts +++ b/packages/lgtv-ip-control/src/classes/LGTV.ts @@ -7,10 +7,11 @@ import { Inputs, Keys, PictureModes, + PowerStates, ScreenMuteModes, } from '../constants/TV.js'; import { LGEncoder, LGEncryption } from './LGEncryption.js'; -import { TinySocket } from './TinySocket.js'; +import { TimeoutError, TinySocket } from './TinySocket.js'; export class ResponseParseError extends Error {} @@ -101,6 +102,30 @@ export class LGTV { return false; } + async getPowerState(): Promise { + const testPowerState = async () => { + const currentApp = await this.getCurrentApp(); + return currentApp === null ? PowerStates.off : PowerStates.on; + }; + + if (this.connected) { + return testPowerState(); + } + + try { + await this.connect(); + return await testPowerState(); + } catch (error) { + if (error instanceof TimeoutError) { + return PowerStates.unknown; + } else { + throw error; + } + } finally { + this.disconnect(); + } + } + async powerOff(): Promise { throwIfNotOK(await this.sendCommand(`POWER off`)); } diff --git a/packages/lgtv-ip-control/src/constants/TV.ts b/packages/lgtv-ip-control/src/constants/TV.ts index 858ac45..90d730c 100644 --- a/packages/lgtv-ip-control/src/constants/TV.ts +++ b/packages/lgtv-ip-control/src/constants/TV.ts @@ -96,6 +96,12 @@ export enum PictureModes { vivid = 'vivid', } +export enum PowerStates { + on = 'on', + off = 'off', + unknown = 'unknown', +} + export enum ScreenMuteModes { screenMuteOn = 'screenmuteon', videoMuteOn = 'videomuteon', diff --git a/packages/lgtv-ip-control/test/LGTV.test.ts b/packages/lgtv-ip-control/test/LGTV.test.ts index 2a3b6ba..d5cc825 100644 --- a/packages/lgtv-ip-control/test/LGTV.test.ts +++ b/packages/lgtv-ip-control/test/LGTV.test.ts @@ -10,9 +10,11 @@ import { Inputs, Keys, PictureModes, + PowerStates, ScreenMuteModes, } from '../src/constants/TV.js'; +const NULL_IP = '0.0.0.0'; const CRYPT_KEY = 'M9N0AZ62'; const MAC = 'DA:0A:0F:E1:60:CB'; @@ -77,7 +79,11 @@ describe.each([ mockServerSocket = socket; }).listen(); const port = (mockServer.address()).port; - testSettings = { ...DefaultSettings, networkPort: port }; + testSettings = { + ...DefaultSettings, + networkPort: port, + networkTimeout: 50, + }; testTV = new LGTV(address, MAC, crypt ? CRYPT_KEY : null, testSettings); }); @@ -179,6 +185,33 @@ describe.each([ } }); + it.each([{ powerState: PowerStates.on }, { powerState: PowerStates.off }])( + 'gets the TV power state when connected: $powerState', + async ({ powerState }) => { + const mocking = mockResponse( + 'CURRENT_APP', + powerState === PowerStates.on ? 'APP:ANYTHING' : '', + ); + await testTV.connect(); + const actual = testTV.getPowerState(); + await expect(mocking).resolves.not.toThrow(); + await expect(actual).resolves.toBe(powerState); + }, + ); + + it('gets "unknown" TV power state when disconnected', async () => { + const offlineTV = new LGTV( + NULL_IP, + MAC, + crypt ? CRYPT_KEY : null, + testSettings, + ); + + await expect(offlineTV.getPowerState()).resolves.toBe( + PowerStates.unknown, + ); + }); + it.each([ { response: 'OK', error: false }, { response: 'FOO', error: true },