Skip to content

Commit

Permalink
up
Browse files Browse the repository at this point in the history
  • Loading branch information
clement-berard committed Oct 29, 2024
1 parent cc6c648 commit 807ee6e
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 88 deletions.
9 changes: 3 additions & 6 deletions docs/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,12 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; // Disable TLS certificate verific

const pjs = new PhilTVPairing({ tvIp: '192.168.0.22', apiPort: 1926 });

// you must to call init() before startPairing(), to check if the TV is reachable and compatible
await pjs.init();

// `startPairing` returns a function to prompt for the pin, can be useful
const { promptForPin } = await pjs.startPairing();
const pin = await promptForPin();

// `completePairing` returns the configuration object, or an error
const { config, error } = await pjs.completePairing(pin);
const [error, config] = await pjs.completePairing(pin);

if (!error) {
console.log('Pairing successful:', config);
Expand All @@ -54,7 +51,7 @@ Result example of `config`:
"password": "5bewertrewref6968be556667552a49da5bf5fce3b379127cf74af2a3951026c2b",
"apiUrl": "https://192.168.0.22:1926",
"apiVersion": 6,
"fullApiUrl": "https://10.0.0.19:1926/6"
"fullApiUrl": "https://192.168.0.22:1926/6"
}
```
You can store the `user` and `password` in a secure location and use them to interact with your TV.
Expand All @@ -74,7 +71,7 @@ You must have the `user` and `password` from the pairing process to use the Join

```typescript
const apiClient = new PhilTVApi({
apiUrl: 'https://10.0.0.19:1926/6',
apiUrl: 'https://192.168.0.22:1926/6',
password: '5bewertrewref6968be556667552a49da5bf5fce3b379127cf74af2a3951026c2b',
user: 'd1443b9fdeecd187277as5464564565e6315',
});
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"build": "pnpm tsup --dts --clean --minify --format esm src/index.ts"
},
"dependencies": {
"consola": "3.2.3",
"digest-fetch": "3.1.1",
"ky": "1.7.2",
"node-fetch": "3.3.2",
Expand All @@ -29,9 +30,9 @@
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/node": "22.7.8",
"@types/node": "22.8.2",
"tsup": "8.3.5",
"tsx": "4.19.1",
"tsx": "4.19.2",
"typedoc": "^0.26.10",
"typedoc-plugin-markdown": "^4.2.9",
"typedoc-vitepress-theme": "1.0.2",
Expand Down
49 changes: 26 additions & 23 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/http-clients/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function createKyJointSpaceClient() {
export function createKyDigestClient(baseURL: string, username: string, password: string) {
const digestClient = new DigestClient(username, password);

return createKyJointSpaceClient().extend({
return ky.create({
throwHttpErrors: true,
prefixUrl: baseURL,
hooks: {
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { PhilTVPairing } from './lib/pairing';
export { PhilTVApi } from './lib/tv-api';
export { PhilTVPairing } from './lib/PhilTVPairing';
export { PhilTVApi } from './lib/PhilTVApi';
export type * from './types';
File renamed without changes.
79 changes: 44 additions & 35 deletions src/lib/pairing.ts → src/lib/PhilTVPairing.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { randomBytes } from 'node:crypto';
import { consola } from 'consola';
import ky, { type KyInstance } from 'ky';
import { tryit } from 'radash';
import { JS_SECRET_KEY } from '../constants';
import { createKyDigestClient, createKyJointSpaceClient } from '../http-clients';
import type { CompletePairingResponse, HttpClients, PhilTVPairingParams } from '../types';
import type { HttpClients, PhilTVPairingParams } from '../types';
import { createSignature, getDeviceObject, promptText } from '../utils';

export async function getInformationSystem(httpClient: KyInstance) {
Expand All @@ -26,7 +28,7 @@ export async function getInformationSystem(httpClient: KyInstance) {
export class PhilTVPairing {
private tvBase: PhilTVPairingParams;
private deviceId!: string;
private apiUrls: { unsecure: string; secure: string };
private apiUrls: { secure: string };
private deviceInformation!: ReturnType<typeof getDeviceObject>;
private httpClients!: HttpClients;
private startPairingResponse!: { authKey: any; authTimestamp: any; timeout: any };
Expand All @@ -37,46 +39,58 @@ export class PhilTVPairing {
this.tvBase = initParams;
this.apiUrls = {
secure: `https://${this.tvBase.tvIp}:${this.tvBase.apiPort}`,
unsecure: `http://${this.tvBase.tvIp}:${this.tvBase.apiPort}`,
};
this.deviceId = randomBytes(16).toString('hex');
this.deviceInformation = getDeviceObject(this.deviceId);
}

async init() {
const { apiVersion, isReady } = await getInformationSystem(
private async init() {
const [errGetInfoSystem, dataInfoSystem] = await tryit(getInformationSystem)(
ky.create({
prefixUrl: this.apiUrls.secure,
}),
);

if (!isReady) {
throw new Error('TV is not ready for pairing');
if (errGetInfoSystem) {
consola.error(`
${errGetInfoSystem.message}\n
${errGetInfoSystem.cause}\n
Check if the TV is on and the IP is correct.
Only Philips TVs with API version 6 or higher are supported.
Only secure transport and digest auth pairing are supported (https).\n
Bye.
`);
process.exit(1);
}

this.deviceId = randomBytes(16).toString('hex');
this.deviceInformation = getDeviceObject(this.deviceId);
const { apiVersion, isReady } = dataInfoSystem;

this.apiVersion = apiVersion;
this.httpClients = {
secure: createKyJointSpaceClient().extend({
prefixUrl: `${this.apiUrls.secure}/${apiVersion}`,
}),
unsecure: createKyJointSpaceClient().extend({
prefixUrl: `${this.apiUrls.unsecure}/${apiVersion}`,
}),
digest: undefined,
};
}

async startPairing() {
await this.init();
const res = await this.httpClients?.secure
.post('pair/request', {
json: { device: this.deviceInformation, scope: ['read', 'write', 'control'] },
})
.json();

const { timestamp: authTimestamp, auth_key: authKey, timeout, error_id } = res as any;
const { timestamp: authTimestamp, auth_key: authKey, timeout, error_id, error_text } = res as any;

if (error_id !== 'SUCCESS') {
throw new Error('Failed to start pairing');
consola.error(`
Failed to start pairing.\n
${error_text}\n
Bye.
`);
process.exit(1);
}

this.startPairingResponse = {
Expand All @@ -96,14 +110,19 @@ export class PhilTVPairing {
this.credentials.password,
);

const promptForPin = async () => await promptText('Enter pin code?');
const promptForPin = async () =>
await consola.prompt('Enter pin code from TV:', {
type: 'text',
});

return {
promptForPin,
};
}

async completePairing(pin: string): Promise<CompletePairingResponse> {
async completePairing(pin: string) {
consola.start('Completing pairing...');

if (this.startPairingResponse) {
const decodedSecretKey = Buffer.from(JS_SECRET_KEY, 'base64');
const authTimestamp = this.startPairingResponse?.authTimestamp + pin;
Expand All @@ -119,22 +138,20 @@ export class PhilTVPairing {
},
};

const res = await this.httpClients.digest?.post('pair/grant', {
const [errorGrant, dataGrant] = await tryit(this.httpClients.digest?.post as KyInstance['post'])('pair/grant', {
json: authData,
timeout: false,
retry: 0,
throwHttpErrors: false,
});

const response = (await res?.json()) as any;
if (errorGrant) {
return [errorGrant, undefined] as const;
}

const response = (await dataGrant?.json()) as any;

if (response?.error_id !== 'SUCCESS') {
return {
config: undefined,
error: {
message: `Failed to complete pairing: ${response?.error_text}`,
},
};
return [new Error(`Failed to complete pairing: ${response?.error_text}`), undefined] as const;
}

const config = {
Expand All @@ -145,17 +162,9 @@ export class PhilTVPairing {
fullApiUrl: `${this.apiUrls.secure}/${this.apiVersion}`,
};

return {
config,
error: undefined,
};
return [undefined, config] as const;
}

return {
config: undefined,
error: {
message: 'Failed to complete pairing',
},
};
return [new Error('Failed to complete pairing'), undefined] as const;
}
}
Loading

0 comments on commit 807ee6e

Please sign in to comment.