homebridge-philips-tv-ambilight-extended
Version:
Enhanced Homebridge platform plugin for Philips Android TV with proper TV icon, Ambilight control, and extended features
251 lines (225 loc) • 8.29 kB
JavaScript
const axios = require("axios");
const wol = require('wake_on_lan');
const pkg = require("./package.json");
class PhilipsTV {
constructor(config) {
this.config = config;
this.wolURL = config.wol_url;
this.model_year = config.model_year;
this.model_year_nr = parseInt(this.model_year);
// CHOOSING API VERSION BY MODEL/YEAR
switch (this.model_year_nr) {
case 2016:
this.api_version = 6;
break;
case 2014:
this.api_version = 5;
break;
default:
this.api_version = 1;
}
// CONNECTION SETTINGS
this.protocol = (this.api_version > 5) ? 'https' : 'http';
this.portno = (this.api_version > 5) ? '1926' : '1925';
this.apiUrl = `${this.protocol}://${config.ip_address}:${this.portno}/${this.api_version}/`;
// Configure axios with default settings
this.httpClient = axios.create({
timeout: 5000, // Reduced from 10000 to 5000ms for faster response
httpsAgent: this.api_version > 5 ? new (require('https').Agent)({
rejectUnauthorized: false
}) : undefined,
auth: {
username: this.config.username,
password: this.config.password
}
});
}
async api(path, body = null) {
try {
const config = {
method: body ? "POST" : "GET",
url: this.apiUrl + path,
data: body || undefined,
headers: body ? { 'Content-Type': 'application/json' } : undefined
};
const response = await this.httpClient(config);
return response.data || {};
} catch (error) {
if (error.response) {
// Request made but server responded with error status
return {};
}
throw error;
}
}
async getSystemState() {
try {
const res = await this.api("system");
return res
} catch {
return false;
}
}
async getPowerState() {
try {
const res = await this.api("powerstate");
return res.powerstate === "On";
} catch {
return false;
}
}
async setPowerState(value) {
if (value && this.wolURL?.toUpperCase().startsWith("WOL")) {
const mac = this.wolURL.replace(/^WOL[:\/]*/i, "");
return new Promise((resolve) => {
wol.wake(mac, (error) => {
if (error) {
console.warn("[PhilipsTV WARN] WOL error:", error.message);
}
// Wait a bit for TV to wake up before sending power command
setTimeout(async () => {
try {
await this.api("powerstate", { powerstate: "On" });
} catch (e) {
console.warn("[PhilipsTV WARN] Power on after WOL failed:", e.message);
}
resolve();
}, 2000);
});
});
} else {
await this.api("powerstate", { powerstate: value ? "On" : "Standby" });
}
}
async sendRemoteKey(key) {
const map = {
0: "Rewind", 1: "FastForward", 2: "Next", 3: "Previous",
4: "CursorUp", 5: "CursorDown", 6: "CursorLeft", 7: "CursorRight",
8: "Confirm", 9: "Back", 10: "Exit", 11: "PlayPause", 15: "Info"
};
if (map[key]) await this.api("input/key", { key: map[key] });
}
async getVolumeState() {
let result = 50; // default
try {
const vol = await this.api("audio/volume");
if (vol && vol.current !== undefined && vol.max !== undefined && vol.max > 0) {
result = Math.round((vol.current / vol.max) * 100);
} else {
console.warn("[PhilipsTV WARN] Invalid volume data from TV, returning default 50. Data:", vol);
}
} catch (e) {
console.warn("[PhilipsTV WARN] getVolumeState error, returning default 50:", e.message);
}
return result;
}
async setVolumeState(value) {
const vol = await this.api("audio/volume");
vol.current = Math.round(vol.max * (value / 100));
await this.api("audio/volume", vol);
}
async getMuteState() {
let result = false; // default
try {
const vol = await this.api("audio/volume");
if (vol && vol.muted !== undefined) {
result = vol.muted;
} else {
console.warn("[PhilipsTV WARN] Invalid mute data from TV, returning false. Data:", vol);
}
} catch (e) {
console.warn("[PhilipsTV WARN] getMuteState error, returning false:", e.message);
}
return result;
}
async setMuteState(value) {
const vol = await this.api("audio/volume");
vol.muted = value;
await this.api("audio/volume", vol);
}
async setAmbilightMode(mode) {
try {
// First check if ambilight is available
const power = await this.api("ambilight/power");
if (!power || power.power !== "On") {
// Turn on ambilight first
await this.api("ambilight/power", { power: "On" });
}
// Map mode names to API values
const modeMap = {
"Follow Video": "FOLLOW_VIDEO",
"Follow Audio": "FOLLOW_AUDIO",
"Lounge Light": "LOUNGE_LIGHT",
"Manual": "MANUAL"
};
const apiMode = modeMap[mode] || "FOLLOW_VIDEO";
// Set the mode
await this.api("ambilight/mode", {
current: apiMode
});
} catch (error) {
console.warn("[PhilipsTV WARN] setAmbilightMode error:", error.message);
}
}
async launchApp(packageName, className) {
try {
if (typeof packageName === 'object') {
// If packageName is actually an app config object
const intent = {
intent: {
component: {
packageName: packageName.packageName || packageName,
className: packageName.className || className
},
action: "android.intent.action.MAIN"
}
};
await this.api("activities/launch", intent);
} else {
// Simple package name
const intent = {
intent: {
component: {
packageName: packageName,
className: className || packageName + ".MainActivity"
},
action: "android.intent.action.MAIN"
}
};
await this.api("activities/launch", intent);
}
} catch (error) {
console.warn("[PhilipsTV WARN] launchApp error:", error.message);
}
}
async setChannel(channel) {
try {
await this.api("activities/tv", {
channel: {
ccid: channel,
preset: channel.toString(),
name: ""
},
channelList: {
id: "allter",
version: "30"
}
});
} catch (error) {
console.warn("[PhilipsTV WARN] setChannel error:", error.message);
}
}
async getCurrentActivity() {
try {
const res = await this.api("activities/current");
return res;
} catch (error) {
console.warn("[PhilipsTV WARN] getCurrentActivity error:", error.message);
return null;
}
}
getServices() {
return this.services;
}
}
module.exports = PhilipsTV;