feat: mqtt audio volume control #82
This commit is contained in:
+142
-27
@@ -28,6 +28,9 @@ global.HARDWARE = global.HARDWARE || {
|
|||||||
keyboard: {
|
keyboard: {
|
||||||
visible: null,
|
visible: null,
|
||||||
},
|
},
|
||||||
|
audio: {
|
||||||
|
device: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +53,7 @@ const init = async () => {
|
|||||||
HARDWARE.display.status.command = getDisplayStatusCommand();
|
HARDWARE.display.status.command = getDisplayStatusCommand();
|
||||||
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
||||||
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
|
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
|
||||||
|
HARDWARE.audio.device = getAudioDevice();
|
||||||
HARDWARE.support = checkSupport();
|
HARDWARE.support = checkSupport();
|
||||||
HARDWARE.initialized = true;
|
HARDWARE.initialized = true;
|
||||||
|
|
||||||
@@ -91,13 +95,31 @@ const init = async () => {
|
|||||||
`Display Brightness [${HARDWARE.display.brightness.path || unsupported}]:`,
|
`Display Brightness [${HARDWARE.display.brightness.path || unsupported}]:`,
|
||||||
`${getDisplayBrightness() || unsupported}`,
|
`${getDisplayBrightness() || unsupported}`,
|
||||||
);
|
);
|
||||||
|
console.log(
|
||||||
|
`Audio Volume [${HARDWARE.support.audioVolume ? HARDWARE.audio.device : unsupported}]:`,
|
||||||
|
`${getAudioVolume() || unsupported}`,
|
||||||
|
);
|
||||||
console.log(
|
console.log(
|
||||||
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "squeekboard" : unsupported}]:`,
|
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "squeekboard" : unsupported}]:`,
|
||||||
`${getKeyboardVisibility() || unsupported}`,
|
`${getKeyboardVisibility() || unsupported}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check for keyboard visibility
|
|
||||||
process.stdout.write("\n");
|
process.stdout.write("\n");
|
||||||
|
|
||||||
|
// Monitor audio volume
|
||||||
|
if (HARDWARE.support.audioVolume) {
|
||||||
|
commandMonitor("pactl", ["subscribe"], (reply, error) => {
|
||||||
|
if (!reply || error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reply.includes("'change' on sink")) {
|
||||||
|
console.log("Update Audio Volume:", getAudioVolume());
|
||||||
|
EVENTS.emit("updateVolume");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor keyboard visibility
|
||||||
|
if (HARDWARE.support.keyboardVisibility) {
|
||||||
setKeyboardVisibility("OFF", (reply, error) => {
|
setKeyboardVisibility("OFF", (reply, error) => {
|
||||||
if (!reply || error) {
|
if (!reply || error) {
|
||||||
return;
|
return;
|
||||||
@@ -107,9 +129,11 @@ const init = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
HARDWARE.keyboard.visibility = property.Visible === "true";
|
HARDWARE.keyboard.visibility = property.Visible === "true";
|
||||||
|
console.log("Update Keyboard Visibility:", getKeyboardVisibility());
|
||||||
EVENTS.emit("updateKeyboard");
|
EVENTS.emit("updateKeyboard");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check for display changes
|
// Check for display changes
|
||||||
setDisplayStatus("ON", () => {
|
setDisplayStatus("ON", () => {
|
||||||
@@ -209,6 +233,7 @@ const checkSupport = () => {
|
|||||||
const battery = HARDWARE.battery.level;
|
const battery = HARDWARE.battery.level;
|
||||||
const status = HARDWARE.display.status;
|
const status = HARDWARE.display.status;
|
||||||
const brightness = HARDWARE.display.brightness;
|
const brightness = HARDWARE.display.brightness;
|
||||||
|
const audio = HARDWARE.audio.device;
|
||||||
const keyboard = processRuns("squeekboard");
|
const keyboard = processRuns("squeekboard");
|
||||||
const service = serviceRuns(APP.name);
|
const service = serviceRuns(APP.name);
|
||||||
const sudo = sudoRights();
|
const sudo = sudoRights();
|
||||||
@@ -218,8 +243,9 @@ const checkSupport = () => {
|
|||||||
displayStatus: !!status.path && !!status.command,
|
displayStatus: !!status.path && !!status.command,
|
||||||
displayBrightness: sudo && !!brightness.path && !!brightness.max,
|
displayBrightness: sudo && !!brightness.path && !!brightness.max,
|
||||||
keyboardVisibility: keyboard,
|
keyboardVisibility: keyboard,
|
||||||
|
audioVolume: !!audio,
|
||||||
sudoRights: sudo,
|
sudoRights: sudo,
|
||||||
appUpdate: sudo && service && deb,
|
appUpdate: service && sudo && deb,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -483,7 +509,7 @@ const getDisplayStatus = () => {
|
|||||||
case "xset":
|
case "xset":
|
||||||
const xset = execSyncCommand("xset", ["-q"]);
|
const xset = execSyncCommand("xset", ["-q"]);
|
||||||
if (xset !== null) {
|
if (xset !== null) {
|
||||||
const output = /Monitor is On/.test(xset);
|
const output = xset.includes("Monitor is On");
|
||||||
return output ? "ON" : "OFF";
|
return output ? "ON" : "OFF";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -599,6 +625,65 @@ const setDisplayBrightness = (brightness, callback = null) => {
|
|||||||
proc.stdin.end();
|
proc.stdin.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the default audio device using `pactl`.
|
||||||
|
*
|
||||||
|
* @returns {string|null} The default audio device or null if an error occurs.
|
||||||
|
*/
|
||||||
|
const getAudioDevice = () => {
|
||||||
|
if (!commandExists("pactl")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const output = execSyncCommand("pactl", ["get-default-sink"]);
|
||||||
|
if (!output) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return !output.includes("auto_null") ? output.trim() : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the default audio device volume using `pactl`.
|
||||||
|
*
|
||||||
|
* @returns {number|null} The default audio device volume as a percentage or null if an error occurs.
|
||||||
|
*/
|
||||||
|
const getAudioVolume = () => {
|
||||||
|
if (!HARDWARE.support.audioVolume) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const mute = execSyncCommand("pactl", ["get-sink-mute", "@DEFAULT_SINK@"]);
|
||||||
|
const volume = execSyncCommand("pactl", ["get-sink-volume", "@DEFAULT_SINK@"]);
|
||||||
|
if (!mute || !volume) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = volume.match(/\/(\s*(\d+)%)/);
|
||||||
|
if (match) {
|
||||||
|
return Math.max(0, Math.min(100, mute.includes("yes") ? 0 : parseInt(match[2])));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default audio device volume using `pactl`.
|
||||||
|
*
|
||||||
|
* This function takes a volume value between 0 to 100 percent and sends it to the device.
|
||||||
|
*
|
||||||
|
* @param {number} volume - The desired volume level (0-100).
|
||||||
|
* @param {Function} callback - A callback function that receives the output or error.
|
||||||
|
*/
|
||||||
|
const setAudioVolume = (volume, callback = null) => {
|
||||||
|
if (!HARDWARE.support.audioVolume) {
|
||||||
|
if (typeof callback === "function") callback(null, "Not supported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof volume !== "number" || volume < 0 || volume > 100) {
|
||||||
|
console.error("Volume must be a number between 0 and 100");
|
||||||
|
if (typeof callback === "function") callback(null, "Invalid volume");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
execAsyncCommand("pactl", ["set-sink-mute", "@DEFAULT_SINK@", volume === 0 ? "1" : "0"]);
|
||||||
|
execAsyncCommand("pactl", ["set-sink-volume", "@DEFAULT_SINK@", `${volume}%`], callback);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the keyboard visibility using global properties.
|
* Gets the keyboard visibility using global properties.
|
||||||
*
|
*
|
||||||
@@ -689,8 +774,8 @@ const rebootSystem = (callback = null) => {
|
|||||||
*/
|
*/
|
||||||
const sudoRights = () => {
|
const sudoRights = () => {
|
||||||
try {
|
try {
|
||||||
const output = cpr.execSync(`sudo -n true`, { encoding: "utf8" });
|
cpr.execSync(`sudo -n true`, { encoding: "utf8", stdio: "ignore" });
|
||||||
return output !== null && !output.includes("password is required");
|
return true;
|
||||||
} catch {}
|
} catch {}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -703,7 +788,8 @@ const sudoRights = () => {
|
|||||||
*/
|
*/
|
||||||
const serviceRuns = (name) => {
|
const serviceRuns = (name) => {
|
||||||
try {
|
try {
|
||||||
return !!cpr.execSync(`systemctl --user is-active ${name}`, { encoding: "utf8" });
|
cpr.execSync(`systemctl --user is-active ${name}`, { encoding: "utf8", stdio: "ignore" });
|
||||||
|
return true;
|
||||||
} catch {}
|
} catch {}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -716,7 +802,8 @@ const serviceRuns = (name) => {
|
|||||||
*/
|
*/
|
||||||
const processRuns = (name) => {
|
const processRuns = (name) => {
|
||||||
try {
|
try {
|
||||||
return !!cpr.execSync(`pidof ${name}`, { encoding: "utf8" });
|
cpr.execSync(`pidof ${name}`, { encoding: "utf8", stdio: "ignore" });
|
||||||
|
return true;
|
||||||
} catch {}
|
} catch {}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -729,7 +816,8 @@ const processRuns = (name) => {
|
|||||||
*/
|
*/
|
||||||
const commandExists = (name) => {
|
const commandExists = (name) => {
|
||||||
try {
|
try {
|
||||||
return !!cpr.execSync(`which ${name}`, { encoding: "utf8" });
|
cpr.execSync(`which ${name}`, { encoding: "utf8", stdio: "ignore" });
|
||||||
|
return true;
|
||||||
} catch {}
|
} catch {}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -741,7 +829,7 @@ const commandExists = (name) => {
|
|||||||
* @param {Array<string>} args - The arguments for the command.
|
* @param {Array<string>} args - The arguments for the command.
|
||||||
* @returns {string|null} The output of the command or null if an error occurs.
|
* @returns {string|null} The output of the command or null if an error occurs.
|
||||||
*/
|
*/
|
||||||
const execSyncCommand = (cmd, args = []) => {
|
const execSyncCommand = (cmd, args) => {
|
||||||
try {
|
try {
|
||||||
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
const output = cpr.execSync([cmd, ...args].join(" "), { encoding: "utf8" });
|
||||||
return output.trim().replace(/\0/g, "");
|
return output.trim().replace(/\0/g, "");
|
||||||
@@ -759,7 +847,7 @@ const execSyncCommand = (cmd, args = []) => {
|
|||||||
* @param {Function} callback - A callback function that receives the output or error.
|
* @param {Function} callback - A callback function that receives the output or error.
|
||||||
* @returns {Object} The spawned process object.
|
* @returns {Object} The spawned process object.
|
||||||
*/
|
*/
|
||||||
const execAsyncCommand = (cmd, args = [], callback = null) => {
|
const execAsyncCommand = (cmd, args, callback = null) => {
|
||||||
let errorOutput = "";
|
let errorOutput = "";
|
||||||
let successOutput = "";
|
let successOutput = "";
|
||||||
let proc = cpr.spawn(cmd, args);
|
let proc = cpr.spawn(cmd, args);
|
||||||
@@ -794,7 +882,7 @@ const execAsyncCommand = (cmd, args = [], callback = null) => {
|
|||||||
* @param {Function} callback - A callback function that receives the progress or error.
|
* @param {Function} callback - A callback function that receives the progress or error.
|
||||||
* @returns {Object} The spawned process object.
|
* @returns {Object} The spawned process object.
|
||||||
*/
|
*/
|
||||||
const execScriptCommand = (cmd, args = [], callback = null) => {
|
const execScriptCommand = (cmd, args, callback = null) => {
|
||||||
let progress = 1;
|
let progress = 1;
|
||||||
let proc = cpr.spawn(cmd, args);
|
let proc = cpr.spawn(cmd, args);
|
||||||
if (typeof callback === "function") callback(progress, null);
|
if (typeof callback === "function") callback(progress, null);
|
||||||
@@ -836,25 +924,28 @@ const execScriptCommand = (cmd, args = [], callback = null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a D-Bus method call synchronously using `dbus-send`.
|
* Monitors a command asynchronously.
|
||||||
*
|
*
|
||||||
* @param {string} path - The D-Bus object path.
|
* @param {string} cmd - The command to monitor.
|
||||||
* @param {string} method - The D-Bus method name.
|
* @param {Array<string>} args - The arguments for the command.
|
||||||
* @param {Array<string>} values - The argument values for the D-Bus method.
|
|
||||||
* @param {Function} callback - A callback function that receives the output or error.
|
* @param {Function} callback - A callback function that receives the output or error.
|
||||||
|
* @returns {Object} The spawned process object.
|
||||||
*/
|
*/
|
||||||
const dbusCall = (path, method, values, callback = null) => {
|
const commandMonitor = (cmd, args, callback) => {
|
||||||
const cmd = "dbus-send";
|
const proc = cpr.spawn(cmd, args);
|
||||||
const iface = path.slice(1).replace(/\//g, ".");
|
proc.stdout.on("data", (data) => {
|
||||||
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
|
if (data) {
|
||||||
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
|
const output = data.toString().trim().replace(/\0/g, "");
|
||||||
try {
|
if (typeof callback === "function") callback(output, null);
|
||||||
const output = cpr.execSync([cmd, ...args].join(" ").trim(), { encoding: "utf8" });
|
|
||||||
if (typeof callback === "function") callback(output.trim().replace(/\0/g, ""), null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Call D-Bus:", error.message);
|
|
||||||
if (typeof callback === "function") callback(null, error.message);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
proc.stderr.on("data", (data) => {
|
||||||
|
if (data) {
|
||||||
|
const output = data.toString().trim().replace(/\0/g, "");
|
||||||
|
if (typeof callback === "function") callback(null, output);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return proc;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -899,6 +990,28 @@ const dbusMonitor = (path, callback) => {
|
|||||||
return proc;
|
return proc;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a D-Bus method call synchronously using `dbus-send`.
|
||||||
|
*
|
||||||
|
* @param {string} path - The D-Bus object path.
|
||||||
|
* @param {string} method - The D-Bus method name.
|
||||||
|
* @param {Array<string>} values - The argument values for the D-Bus method.
|
||||||
|
* @param {Function} callback - A callback function that receives the output or error.
|
||||||
|
*/
|
||||||
|
const dbusCall = (path, method, values, callback = null) => {
|
||||||
|
const cmd = "dbus-send";
|
||||||
|
const iface = path.slice(1).replace(/\//g, ".");
|
||||||
|
const dest = `${iface} ${path} ${iface}.${method} ${values.join(" ")}`;
|
||||||
|
const args = ["--print-reply", "--type=method_call", `--dest=${dest}`];
|
||||||
|
try {
|
||||||
|
const output = cpr.execSync([cmd, ...args].join(" ").trim(), { encoding: "utf8" });
|
||||||
|
if (typeof callback === "function") callback(output.trim().replace(/\0/g, ""), null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Call D-Bus:", error.message);
|
||||||
|
if (typeof callback === "function") callback(null, error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function for asynchronous interval calls.
|
* Helper function for asynchronous interval calls.
|
||||||
*
|
*
|
||||||
@@ -938,6 +1051,8 @@ module.exports = {
|
|||||||
setDisplayStatus,
|
setDisplayStatus,
|
||||||
getDisplayBrightness,
|
getDisplayBrightness,
|
||||||
setDisplayBrightness,
|
setDisplayBrightness,
|
||||||
|
getAudioVolume,
|
||||||
|
setAudioVolume,
|
||||||
getKeyboardVisibility,
|
getKeyboardVisibility,
|
||||||
setKeyboardVisibility,
|
setKeyboardVisibility,
|
||||||
checkPackageUpgrades,
|
checkPackageUpgrades,
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const init = async () => {
|
|||||||
initRefresh();
|
initRefresh();
|
||||||
initKiosk();
|
initKiosk();
|
||||||
initDisplay();
|
initDisplay();
|
||||||
|
initVolume();
|
||||||
initKeyboard();
|
initKeyboard();
|
||||||
initPageNumber();
|
initPageNumber();
|
||||||
initPageZoom();
|
initPageZoom();
|
||||||
@@ -97,6 +98,7 @@ const init = async () => {
|
|||||||
|
|
||||||
// Update sensor states from events
|
// Update sensor states from events
|
||||||
EVENTS.on("updateDisplay", updateDisplay);
|
EVENTS.on("updateDisplay", updateDisplay);
|
||||||
|
EVENTS.on("updateVolume", updateVolume);
|
||||||
EVENTS.on("updateKeyboard", updateKeyboard);
|
EVENTS.on("updateKeyboard", updateKeyboard);
|
||||||
|
|
||||||
// Update time sensors periodically (30s)
|
// Update time sensors periodically (30s)
|
||||||
@@ -462,6 +464,48 @@ const updateDisplay = async () => {
|
|||||||
publishState("display/brightness", brightness);
|
publishState("display/brightness", brightness);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the audio volume and handles the execute logic.
|
||||||
|
*/
|
||||||
|
const initVolume = () => {
|
||||||
|
const root = `${INTEGRATION.root}/volume`;
|
||||||
|
const config = {
|
||||||
|
name: "Volume",
|
||||||
|
unique_id: `${INTEGRATION.node}_volume`,
|
||||||
|
command_topic: `${root}/set`,
|
||||||
|
state_topic: `${root}/state`,
|
||||||
|
value_template: "{{ value | int }}",
|
||||||
|
mode: "slider",
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
unit_of_measurement: "%",
|
||||||
|
icon: "mdi:volume-high",
|
||||||
|
device: INTEGRATION.device,
|
||||||
|
};
|
||||||
|
if (!HARDWARE.support.audioVolume) {
|
||||||
|
removeConfig("number", config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publishConfig("number", config)
|
||||||
|
.on("message", (topic, message) => {
|
||||||
|
if (topic === config.command_topic) {
|
||||||
|
const volume = parseInt(message, 10);
|
||||||
|
console.log("Set Audio Volume:", volume);
|
||||||
|
hardware.setAudioVolume(volume);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.subscribe(config.command_topic);
|
||||||
|
updateVolume();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the audio volume via the mqtt connection.
|
||||||
|
*/
|
||||||
|
const updateVolume = async () => {
|
||||||
|
const volume = hardware.getAudioVolume();
|
||||||
|
publishState("volume", volume);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the keyboard visibility and handles the execute logic.
|
* Initializes the keyboard visibility and handles the execute logic.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user