UNPKG

homebridge-kasa-python

Version:

Plugin that uses Python-Kasa API to communicate with Kasa Devices.

212 lines 10.4 kB
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