homebridge-kasa-python
Version:
Plugin that uses Python-Kasa API to communicate with Kasa Devices.
212 lines • 10.4 kB
JavaScript
import axios from 'axios';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { delay, prefixLogger, runCommand } from '../utils.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SUPPORTED_PYTHON_VERSIONS = ['3.11', '3.12', '3.13'];
class PythonChecker {
log;
platform;
advancedPythonLogging;
pythonExecutables;
pluginDirPath;
venvPath;
venvConfigPath;
requirementsPath = path.join(__dirname, '..', '..', 'requirements.txt');
pythonExecutable = '';
venvPipExecutable = '';
venvPythonExecutable = '';
constructor(platform) {
this.platform = platform;
this.log = prefixLogger(this.platform.log, '[Python Check]');
this.advancedPythonLogging = this.platform.config.advancedOptions.advancedPythonLogging ?? false;
this.pythonExecutables = ['python', 'python3', 'python3.11', 'python3.12', 'python3.13'];
this.pluginDirPath = path.join(this.platform.storagePath, 'kasa-python');
this.venvPath = path.join(this.pluginDirPath, '.venv');
this.venvConfigPath = path.join(this.venvPath, 'pyvenv.cfg');
}
async allInOne(isUpgrade) {
this.log.debug('Starting python environment check...');
this.ensurePluginDir();
await this.ensurePythonVersion();
await this.ensureVenvCreated(isUpgrade);
await this.ensureVenvUsesCorrectPythonHome();
await this.ensureVenvPipUpToDate();
await this.ensureVenvRequirementsSatisfied();
this.log.debug('Python environment check completed successfully');
}
ensurePluginDir() {
this.log.debug('Ensuring plugin directory exists:', this.pluginDirPath);
if (!fs.existsSync(this.pluginDirPath)) {
fs.mkdirSync(this.pluginDirPath);
this.log.debug('Plugin directory created:', this.pluginDirPath);
}
else {
this.log.debug('Plugin directory already exists:', this.pluginDirPath);
}
}
async ensurePythonVersion() {
this.log.debug('Ensuring system Python version is supported');
const versions = await this.getSystemPythonVersions();
const versionMap = {
'3.13': 'python3.13',
'3.12': 'python3.12',
'3.11': 'python3.11',
};
versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
let supported = false;
for (const version of versions) {
const majorMinorVersion = version.split('.').slice(0, 2).join('.');
if (SUPPORTED_PYTHON_VERSIONS.includes(majorMinorVersion)) {
supported = true;
if (versionMap[majorMinorVersion]) {
this.pythonExecutable = versionMap[majorMinorVersion];
this.venvPythonExecutable = path.join(this.venvPath, 'bin', versionMap[majorMinorVersion]);
this.venvPipExecutable = path.join(this.venvPath, 'bin', `pip${majorMinorVersion}`);
this.log.debug(`Using Python executable: ${this.pythonExecutable}`);
}
break;
}
}
if (!supported) {
this.log.error(`Python ${versions.join(', ')} is installed. However, only Python ` +
`${SUPPORTED_PYTHON_VERSIONS.join(', ')} is supported.`);
await delay(300000);
}
else {
this.log.debug('System Python version is supported');
}
}
async ensureVenvCreated(isUpgrade) {
this.log.debug('Ensuring virtual environment is created');
if (isUpgrade || !this.isVenvCreated()) {
await this.createVenv();
}
else {
this.log.debug('Virtual environment already exists');
}
}
isVenvCreated() {
const venvExists = fs.existsSync(this.venvPipExecutable) &&
fs.existsSync(this.venvConfigPath) &&
fs.existsSync(this.venvPythonExecutable);
this.log.debug('Virtual environment exists:', venvExists);
return venvExists;
}
async createVenv() {
this.log.debug('Creating virtual environment at path:', this.venvPath);
const [stdout] = await runCommand(this.log, this.pythonExecutable, ['-m', 'venv', this.venvPath, '--clear', '--upgrade-deps'], undefined, this.advancedPythonLogging ? false : true, this.advancedPythonLogging ? false : true);
if (stdout.includes('not created successfully') || !this.isVenvCreated()) {
this.log.error('virtualenv python module is not installed.');
await delay(300000);
}
else {
this.log.debug('Virtual environment created successfully');
}
}
async ensureVenvUsesCorrectPythonHome() {
this.log.debug('Ensuring virtual environment uses correct Python home');
const venvPythonHome = await this.getPythonHome(this.venvPythonExecutable);
const pythonHome = await this.getPythonHome(this.pythonExecutable);
this.log.debug('Virtual environment Python home:', venvPythonHome);
this.log.debug('System Python home:', pythonHome);
if (venvPythonHome !== pythonHome) {
this.log.debug('Python homes do not match, recreating virtual environment');
await this.createVenv();
}
else {
this.log.debug('Python homes match');
}
}
async getPythonHome(executable) {
this.log.debug('Getting Python home for executable:', executable);
const [venvPythonHome] = await runCommand(this.log, executable, [path.join(__dirname, 'pythonHome.py')], undefined, this.advancedPythonLogging ? false : true, this.advancedPythonLogging ? false : true);
return venvPythonHome.trim();
}
async ensureVenvPipUpToDate() {
this.log.debug('Ensuring virtual environment pip is up to date');
const venvPipVersion = await this.getVenvPipVersion();
const mostRecentPipVersion = await this.getMostRecentPipVersion();
if (venvPipVersion !== mostRecentPipVersion) {
await this.updatePip();
}
else {
this.log.debug('Virtual environment pip is up to date');
}
}
async updatePip() {
this.log.debug('Updating pip in virtual environment');
await runCommand(this.log, this.venvPipExecutable, ['install', '--upgrade', 'pip'], undefined, this.advancedPythonLogging ? false : true, this.advancedPythonLogging ? false : true);
this.log.debug('Pip updated successfully');
}
async ensureVenvRequirementsSatisfied() {
if (!await this.areRequirementsSatisfied()) {
await this.installRequirements();
}
else {
this.log.debug('Virtual environment requirements are already satisfied');
}
}
async areRequirementsSatisfied() {
this.log.debug('Checking if virtual environment requirements are satisfied');
const [freezeStdout] = await runCommand(this.log, this.venvPipExecutable, ['freeze'], undefined, this.advancedPythonLogging ? false : true, this.advancedPythonLogging ? false : true);
const freeze = this.stringToObject(freezeStdout);
this.log.debug('Current virtual environment packages:', JSON.stringify(freeze, null, 2));
const requirementsStdout = fs.readFileSync(this.requirementsPath).toString();
const requirements = this.stringToObject(requirementsStdout);
this.log.debug('Required packages:', JSON.stringify(requirements, null, 2));
const requirementsSatisfied = Object.keys(requirements).every(pkg => freeze[pkg] === requirements[pkg]);
this.log.debug('Requirements satisfied:', requirementsSatisfied);
return requirementsSatisfied;
}
stringToObject(value) {
return value.trim().split('\n').reduce((acc, line) => {
const [pkg, version] = line.split('==').map(part => part.trim());
acc[pkg.replaceAll('_', '-').toLowerCase()] = version;
return acc;
}, {});
}
async installRequirements() {
this.log.debug('Installing requirements from:', this.requirementsPath);
await runCommand(this.log, this.venvPipExecutable, ['install', '-r', this.requirementsPath], undefined, this.advancedPythonLogging ? false : true, this.advancedPythonLogging ? false : true);
this.log.debug('Requirements installed successfully');
}
async getSystemPythonVersions() {
this.log.debug('Getting system Python versions');
const suppressErrors = this.pythonExecutables.map(exec => `spawn ${exec.toLowerCase()}`);
const versionPromises = this.pythonExecutables.map(async (pythonExecutable) => {
try {
const [stdout] = await runCommand(this.log, pythonExecutable, ['--version'], undefined, this.advancedPythonLogging ? false : true, this.advancedPythonLogging ? false : true, false, suppressErrors);
return stdout.trim().replace('Python ', '');
}
catch {
return null;
}
});
const versions = await Promise.all(versionPromises);
const validVersions = Array.from(new Set(versions.filter((version) => version !== null)));
this.log.debug('Found Python versions:', validVersions);
return validVersions;
}
async getVenvPipVersion() {
this.log.debug('Getting virtual environment pip version');
const [version] = await runCommand(this.log, this.venvPipExecutable, ['--version'], undefined, this.advancedPythonLogging ? false : true, this.advancedPythonLogging ? false : true);
this.log.debug('Virtual environment pip version:', version.trim().split(' ')[1]);
return version.trim().split(' ')[1];
}
async getMostRecentPipVersion() {
this.log.debug('Fetching most recent pip version from PyPI');
try {
const response = await axios.get('https://pypi.org/pypi/pip/json');
this.log.debug('Most recent pip version fetched:', response.data.info.version);
return response.data.info.version;
}
catch (e) {
this.log.error(`Error fetching most recent pip version: ${e}`);
return 'error';
}
}
}
export default PythonChecker;
//# sourceMappingURL=pythonChecker.js.map