UNPKG

philtv-js

Version:

A simple library to pair and interact with Philips TV

1 lines 9.64 kB
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./constants.cjs");let t=require("urllib"),n=require("zod"),r=require("radash"),i=require("node:crypto");async function a(e,t,n,r={}){try{let i=`${t}/${e}`,a=await n.request(i,{...r});if(!(a.status>=200&&a.status<300))return{success:!1,error:Error(`Request failed with status ${a.status} (${a.url})`)};let o=a.data.toString();try{return{success:!0,data:JSON.parse(o)}}catch{return{success:!0,data:o}}}catch(e){return{success:!1,error:e instanceof Error?e:Error(String(e))}}}function o(e){let n=new t.HttpClient({connect:{rejectUnauthorized:e?.options?.rejectUnauthorized??!1,timeout:1e3},defaultArgs:{digestAuth:`${e.user}:${e.password}`,contentType:`json`,headers:{}}});return{client:n,request:(t,r={})=>a(t,e.baseUrl,n,r)}}function s(){return new t.HttpClient({connect:{rejectUnauthorized:!1,timeout:1e3},defaultArgs:{contentType:`json`}})}const c=n.z.object({apiUrl:n.z.url(),user:n.z.string().min(1),password:n.z.string().min(1)}),l=n.z.enum(e.inputKeys),u=n.z.enum(e.ambilightFollowVideoModeEnum),d=n.z.enum(e.ambilightFollowAudioModeEnum),f=n.z.literal(e.ambilightBrightnessChoices),p=n.z.enum(e.ambilightModes);function m(e){async function t(){let t=await e.getBrightnessValue(),n=Math.min(10,t+1);await e.setBrightness(n)}async function n(){let t=await e.getBrightnessValue(),n=Math.max(0,t-1);await e.setBrightness(n)}async function r(){let t=await e.getBrightnessInformation();return Number(t.data?.value??0)}async function i(e){let t=await e.getMenuStructureItem(`ambilight_brightness`);return(await e.getCurrentSetting(t.node_id))[0]?.value}async function a(e,t){let n=f.parse(t),r=await e.getMenuStructureItem(`ambilight_brightness`);return e.setMenuItemSetting(r,Number(n))}return{increase:t,decrease:n,getBrightnessValue:r,getBrightnessInformation:i,setBrightness:a}}async function h(e){let[t,n,r,i,a]=await Promise.all([e.getConfiguration(),e.getBrightnessInformation(),e.getMode(),e.getCachedState(),e.getAmbiHue()]);return{configuration:t??null,mode:r??null,brightness:n??null,currentBrightness:Number(n.data?.value??0),cached:i??null,ambiHue:a??null}}function g(e){async function t(){let t=await e.digestClient.request(`ambilight/mode`);if(!t.success)throw t.error;return t.data}async function n(t){let n=p.parse(t);await e.digestClient.request(`ambilight/mode`,{method:`POST`,data:{current:n}})}return{getMode:t,setMode:n}}var _=class{digestClient;menuApi;constructor(e,t){this.digestClient=e,this.menuApi=t}async setBrightness(e){return m(this).setBrightness(this.menuApi,e)}async getBrightnessInformation(){return m(this).getBrightnessInformation(this.menuApi)}async getBrightnessValue(){return m(this).getBrightnessValue()}async increaseBrightness(){await m(this).increase()}async decreaseBrightness(){await m(this).decrease()}async getMode(){return g(this).getMode()}async setMode(e){await g(this).setMode(e)}async getFullInformation(){return h(this)}async setCurrentConfiguration(e){await this.digestClient.request(`ambilight/currentconfiguration`,{method:`POST`,data:{styleName:e.styleName,isExpert:!1,menuSetting:e.menuSetting}})}async setFollowVideoMode(e){let t=u.parse(e);await this.setCurrentConfiguration({styleName:`FOLLOW_VIDEO`,isExpert:!1,menuSetting:t})}async setFollowAudioMode(e){let t=d.parse(e);await this.setCurrentConfiguration({styleName:`FOLLOW_AUDIO`,isExpert:!1,menuSetting:t})}async turnOff(){await this.setCurrentConfiguration({styleName:`OFF`,isExpert:!1})}async setStaticColor(e,t){let n=e.replace(`#`,``),r=Number.parseInt(n.substring(0,2),16),i=Number.parseInt(n.substring(2,4),16),a=Number.parseInt(n.substring(4,6),16);await this.setMode(`manual`),t?.brightness&&await this.setBrightness(t.brightness),await this.setCachedColor({r,g:i,b:a})}async setCachedColor(e){let t=await this.digestClient.request(`ambilight/cached`,{method:`POST`,data:e});if(!t.success)throw t.error}async getCachedState(){let e=await this.digestClient.request(`ambilight/cached`);if(!e.success)throw e.error;return e.data}async getAmbiHue(){let e=await this.digestClient.request(`HueLamp/power`);if(!e.success)throw e.error;return e.data}async getConfiguration(){let e=await this.digestClient.request(`ambilight/currentconfiguration`);if(!e.success)throw e.error;return e.data}},v=class{digestClient;constructor(e){this.digestClient=e}async sendKey(e){let t=l.parse(e),n=await this.digestClient.request(`input/key`,{method:`POST`,data:{key:t}});if(!n.success)throw n.error}};function y(e){let t=[];function n(e){let{node_id:i,type:a,string_id:o,context:s,data:c}=e;if(t.push({node_id:i,type:a,string_id:o,context:s,data:(0,r.omit)(c,[`nodes`])}),c?.nodes)for(let e of c.nodes)n(e)}return n(e),t}function b(e,t){switch(e){case`SLIDER_NODE`:return{value:Number(t)};case`TOGGLE_NODE`:return{boolean_value:t?`true`:`false`};case`LIST_NODE`:return typeof t==`string`?{string_id:t}:{enum_id:Number(t)};default:return t}}var x=class{digestClient;constructor(e){this.digestClient=e}async getMenuStructure(e={flat:!1}){let t=await this.digestClient.request(`menuitems/settings/structure`);if(!t.success)throw t.error;return e.flat?y(t.data.node):t.data}async getMenuStructureItem(e,t){let n=(await this.getMenuStructure({flat:!0})).find(n=>e?n.context===e:n.node_id===t);if(!n)throw Error(`Menu Item not found`);return n}async setMenuItemSetting(e,t){if(!e)throw Error(`Item not found`);let n={values:[{value:{Nodeid:e.node_id,Controllable:`true`,Available:`true`,data:b(e.type,t)}}]},r=await this.digestClient.request(`menuitems/settings/update`,{method:`POST`,data:n});if(!r.success)throw r.error}async getCurrentSetting(e){let t=await this.digestClient.request(`menuitems/settings/current`,{method:`POST`,data:{nodes:[{nodeid:e}]}});if(!t.success)throw Error(`getCurrentSetting`);return t.data.values}},S=class{digestClient;constructor(e){this.digestClient=e}async getSystem(){let e=await this.digestClient.request(`system`);if(!e.success)throw e.error;return{...e.data,fullApiVersion:`${e.data.api_version.Major}.${e.data.api_version.Minor}.${e.data.api_version.Patch}`}}async getCurrentActivity(){let e=await this.digestClient.request(`activities/current`);if(!e.success)throw e.error;return e.data}async getApplications(){let e=await this.digestClient.request(`applications`);if(!e.success)throw e.error;return e.data}},C=class{digestClient;menu;system;input;ambilight;constructor(e){let{user:t,apiUrl:n,password:r}=c.parse(e);this.digestClient=o({user:t,password:r,baseUrl:n}),this.menu=new x(this.digestClient),this.system=new S(this.digestClient),this.input=new v(this.digestClient),this.ambilight=new _(this.digestClient,this.menu)}};function w(e){return{app_id:`gapp.id`,id:e,device_name:`MyName`,device_os:`Android`,app_name:`PhilTV-js APP`,type:`native`}}function T(e,t){let n=(0,i.createHmac)(`sha1`,e);return n.write(t),n.end(),n.read(`binary`).toString(`base64`)}async function E(e){let t=s();try{let{data:n}=await t.request(`${e}/system`,{dataType:`json`,timeout:2e3,signal:AbortSignal.timeout(2e3)}),r=n,i=r.api_version.Major,a=r.featuring.systemfeatures,o=a.secured_transport===`true`,s=a.pairing_type===`digest_auth_pairing`;return{success:!0,data:{apiVersion:i,systemFeatures:a,isSecureTransport:o,isGoodPairingType:s,isReady:o&&s}}}catch(e){return{success:!1,error:e instanceof Error?e:Error(String(e))}}}var D=class{tvBase;deviceId;apiUrls;deviceInformation;httpClients;startPairingResponse;credentials;apiVersion;pairingRequestUrl;constructor(e){this.tvBase=e,this.apiUrls={secure:`https://${this.tvBase.tvIp}:${this.tvBase.apiPort||1926}`},this.deviceId=(0,i.randomBytes)(16).toString(`hex`),this.deviceInformation=w(this.deviceId)}async init(){let e=await E(this.apiUrls.secure);return e.success?e?.data?.isReady?(this.apiVersion=e.data.apiVersion,this.pairingRequestUrl=`${this.apiUrls.secure}/${this.apiVersion}/pair/request`,{success:!0,data:e.data}):{success:!1,error:Error(`TV is not ready`)}:{success:!1,error:e.error}}async startPairing(){try{let{data:e}=await s().request(this.pairingRequestUrl,{method:`POST`,data:{device:this.deviceInformation,scope:[`read`,`write`,`control`]},dataType:`json`}),t=e,n=t.error_id,r=t.error_text;if(n!==`SUCCESS`)return{success:!1,error:Error(`Failed to start pairing: ${r}`)};let i=t.timestamp,a=t.auth_key,c=t.timeout;return this.startPairingResponse={authTimestamp:i,authKey:a,timeout:c},this.credentials={user:this.deviceId,password:a},this.httpClients={digest:o({user:this.credentials.user,password:this.credentials.password,baseUrl:`${this.apiUrls.secure}/${this.apiVersion}`})},{success:!0,data:this.startPairingResponse}}catch(e){return{success:!1,error:e instanceof Error?e:Error(String(e))}}}async completePairing(t){if(!t)return{success:!1,error:Error(`Failed to complete pairing`)};if(this.startPairingResponse){let n=T(Buffer.from(e.JS_SECRET_KEY,`base64`),this.startPairingResponse.authTimestamp.toString()+t),r={device:this.deviceInformation,auth:{auth_AppId:`1`,pin:t,auth_timestamp:this.startPairingResponse.authTimestamp,auth_signature:n}};if(!this.httpClients.digest)return{success:!1,error:Error(`No digest client`)};let i=await this.httpClients.digest.request(`pair/grant`,{method:`POST`,data:r,retry:0});if(!i.success)return i;let a=i.data;return a?.error_id===`SUCCESS`?{success:!0,data:{user:this.credentials.user,password:this.credentials.password,apiUrl:this.apiUrls.secure,apiVersion:this.apiVersion,fullApiUrl:`${this.apiUrls.secure}/${this.apiVersion}`}}:{success:!1,error:Error(`Failed to complete pairing: ${a?.error_text}`)}}return{success:!1,error:Error(`Failed to complete pairing`)}}};exports.PhilTVApi=C,exports.PhilTVPairing=D;