homebridge-kasa-python
Version:
Plugin that uses Python-Kasa API to communicate with Kasa Devices.
574 lines • 30.1 kB
JavaScript
import axios from 'axios';
import net from 'node:net';
import path from 'node:path';
import { EventEmitter } from 'node:events';
import { promises as fs } from 'node:fs';
import { fileURLToPath } from 'node:url';
import create from './devices/create.js';
import DeviceManager from './devices/deviceManager.js';
import PythonChecker from './python/pythonChecker.js';
import { parseConfig } from './config.js';
import { TaskQueue } from './taskQueue.js';
import { deferAndCombine, runCommand } from './utils.js';
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
import { isObjectLike, lookup, lookupCharacteristicNameByUUID, prefixLogger } from './utils.js';
let packageConfig;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function loadPackageConfig(logger) {
const packageConfigPath = path.join(__dirname, '..', 'package.json');
const log = prefixLogger(logger, '[Package Config]');
log.debug('Loading package configuration from:', packageConfigPath);
try {
const packageConfigData = await fs.readFile(packageConfigPath, 'utf8');
packageConfig = JSON.parse(packageConfigData);
}
catch (error) {
log.error(`Error reading package.json: ${error}`);
throw error;
}
}
function satisfiesVersion(currentVersion, requiredVersion) {
const versions = requiredVersion.split('||').map(v => v.trim());
return versions.some(version => {
const [requiredMajor, requiredMinor, requiredPatch] = version.replace('^', '').split('.').map(Number);
const [currentMajor, currentMinor, currentPatch] = currentVersion.replace('v', '').split('.').map(Number);
if (currentMajor > requiredMajor) {
return true;
}
if (currentMajor < requiredMajor) {
return false;
}
if (currentMinor > requiredMinor) {
return true;
}
if (currentMinor < requiredMinor) {
return false;
}
return currentPatch >= requiredPatch;
});
}
async function checkForUpgrade(storagePath, logger) {
const versionDir = path.join(storagePath, 'kasa-python');
const versionFilePath = path.join(versionDir, 'kasa-python-version.json');
let storedVersion = '';
logger.debug('Checking for upgrade at path:', versionFilePath);
try {
await fs.access(versionFilePath);
const versionData = await fs.readFile(versionFilePath, 'utf8');
storedVersion = JSON.parse(versionData).version;
logger.debug('Stored version:', storedVersion);
}
catch (error) {
if (error.code === 'ENOENT') {
logger.info('Version file does not exist, treating as new install or version change.');
}
else {
logger.error('Error reading version file:', error);
}
}
if (storedVersion !== packageConfig.version) {
try {
logger.debug('Updating version file to new version:', packageConfig.version);
await fs.mkdir(versionDir, { recursive: true });
await fs.writeFile(versionFilePath, JSON.stringify({ version: packageConfig.version }), 'utf8');
logger.info(`Version file updated to version ${packageConfig.version}`);
}
catch (error) {
logger.error('Error writing version file:', error);
}
return true;
}
logger.debug('No upgrade needed, version is up to date.');
return false;
}
async function waitForServer(url, log, timeout = 30000, interval = 1000) {
const startTime = Date.now();
log.debug(`Waiting for server at ${url} with timeout ${timeout}ms and interval ${interval}ms`);
while (Date.now() - startTime < timeout) {
try {
const response = await axios.get(url);
if (response.status === 200) {
log.debug('Server responded successfully');
return;
}
}
catch {
log.debug('Server not responding yet, retrying...');
}
await new Promise(resolve => setTimeout(resolve, interval));
}
log.error(`Server did not respond within ${timeout / 1000} seconds`);
throw new Error(`Server did not respond within ${timeout / 1000} seconds`);
}
async function getAvailablePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(0, () => {
const port = server.address().port;
server.close(() => resolve(port));
});
server.on('error', reject);
});
}
export default class KasaPythonPlatform {
log;
api;
Characteristic;
configuredAccessories = new Map();
offlineAccessories = new Map();
Service;
storagePath;
venvPythonExecutable;
config;
deviceManager;
isShuttingDown = false;
periodicDeviceDiscovering = false;
periodicDeviceDiscoveryEmitter;
port = 0;
taskQueue;
homekitDevicesById = new Map();
hideHomeKitMatter = true;
isUpgrade = false;
kasaProcess = null;
platformInitialization;
constructor(log, config, api) {
this.log = log;
this.api = api;
this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
this.storagePath = this.api.user.storagePath();
this.venvPythonExecutable = path.join(this.storagePath, 'kasa-python', '.venv', 'bin', 'python3');
this.config = parseConfig(config);
this.periodicDeviceDiscoveryEmitter = new EventEmitter();
this.taskQueue = new TaskQueue(this.log);
this.platformInitialization = this.initializePlatform().catch((error) => {
this.log.error('Platform initialization failed:', error);
});
this.api.on('didFinishLaunching', async () => {
this.log.debug('KasaPython Platform finished launching');
await this.platformInitialization;
await this.didFinishLaunching();
if (this.offlineAccessories.size > 0) {
this.log.debug('Unregistering offline accessories');
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, Array.from(this.offlineAccessories.values()));
this.offlineAccessories.clear();
}
});
this.api.on('shutdown', async () => {
this.log.debug('KasaPython shutting down');
if (!this.isShuttingDown) {
this.isShuttingDown = true;
}
this.log.debug('Waiting for tasks to complete');
await this.taskQueue.waitForEmptyQueue();
this.stopKasaApi();
});
}
createHomeKitDevice(kasaDevice) {
this.log.debug('Creating HomeKit device for:', kasaDevice.sys_info);
return create(this, kasaDevice);
}
async initializePlatform() {
try {
await loadPackageConfig(this.log);
this.logInitializationDetails();
await this.verifyEnvironment();
this.isUpgrade = await checkForUpgrade(this.storagePath, this.log);
if (this.isUpgrade) {
this.log.info('Plugin version changed, virtual python environment will be recreated.');
}
}
catch (error) {
this.log.error('Error during platform initialization:', error);
}
}
logInitializationDetails() {
this.log.info(`${packageConfig.name} v${packageConfig.version}, node ${process.version}, ` +
`homebridge v${this.api.serverVersion}, api v${this.api.version} Initializing...`);
}
async verifyEnvironment() {
this.log.debug('Verifying environment');
try {
this.log.debug('Checking Node.js version');
if (!satisfiesVersion(process.version, packageConfig.engines.node)) {
this.log.error(`Error: not using minimum node version ${packageConfig.engines.node}`);
}
else {
this.log.debug(`Node.js version ${process.version} satisfies the requirement ${packageConfig.engines.node}`);
}
this.log.debug('Checking Homebridge version');
if (this.api.versionGreaterOrEqual && !this.api.versionGreaterOrEqual('1.8.4')) {
throw new Error(`homebridge-kasa-python requires homebridge >= 1.8.4. Currently running: ${this.api.serverVersion}`);
}
else {
this.log.debug(`Homebridge version ${this.api.serverVersion} satisfies the requirement >= 1.8.4`);
}
}
catch (error) {
this.log.error('Error verifying environment:', error);
throw error;
}
}
async didFinishLaunching() {
this.log.debug('Finished launching');
try {
this.log.debug('Checking Python environment');
await this.checkPython(this.isUpgrade);
this.log.debug('Getting available port');
this.port = await getAvailablePort();
this.log.debug(`Port assigned: ${this.port}`);
this.log.debug('Initializing DeviceManager');
this.deviceManager = new DeviceManager(this);
this.log.debug('DeviceManager initialized');
await this.startKasaApi();
await waitForServer(`http://127.0.0.1:${this.port}/health`, this.log);
await this.discoverDevices();
this.log.debug('Device discovery completed');
this.log.debug('Setting up periodic device discovery');
setInterval(async () => {
await this.periodicDeviceDiscovery();
}, this.config.discoveryOptions.discoveryPollingInterval);
this.log.debug('Periodic device discovery setup completed');
}
catch (error) {
this.log.error('An error occurred during startup:', error);
}
}
async periodicDeviceDiscovery() {
this.log.debug('Starting periodic device discovery');
if (this.periodicDeviceDiscovering) {
this.log.debug('Periodic device discovery already in progress');
return;
}
this.periodicDeviceDiscovering = true;
const task = async () => {
try {
const discoveredDevices = await this.deviceManager?.discoverDevices() || {};
const now = new Date();
const offlineInterval = this.config.discoveryOptions.offlineInterval;
this.log.info(`Discovered ${Object.keys(discoveredDevices).length} devices`);
this.configuredAccessories.forEach((platformAccessory, uuid) => {
const deviceId = platformAccessory.context.deviceId;
if (deviceId) {
const device = this.findDiscoveredDevice(discoveredDevices, platformAccessory);
if (device) {
this.updateAccessoryDeviceStatus(platformAccessory, device, now);
this.updateOrCreateHomeKitDevice(deviceId, device);
}
else {
this.updateAccessoryStatus(platformAccessory);
this.handleOfflineAccessory(platformAccessory, uuid, now, offlineInterval);
}
}
});
Object.values(discoveredDevices).forEach(device => {
device.last_seen = now;
device.offline = false;
const deviceId = device.sys_info.device_id;
const isConfigured = Array.from(this.configuredAccessories.values()).some(platformAccessory => platformAccessory.context.deviceId === deviceId);
if (!isConfigured) {
this.log.debug(`New device [${deviceId}] found, adding to HomeKit`);
let listenerCount = this.periodicDeviceDiscoveryEmitter.listenerCount('periodicDeviceDiscoveryComplete');
this.log.debug('periodicDeviceDiscoveryEmitter periodicDeviceDiscoveryComplete listener count:', listenerCount);
this.log.debug('periodicDeviceDiscoveryEmitter max listener count:', this.periodicDeviceDiscoveryEmitter.getMaxListeners());
this.foundDevice(device);
this.periodicDeviceDiscoveryEmitter.setMaxListeners(this.periodicDeviceDiscoveryEmitter.getMaxListeners() + 1);
listenerCount = this.periodicDeviceDiscoveryEmitter.listenerCount('periodicDeviceDiscoveryComplete');
this.log.debug('periodicDeviceDiscoveryEmitter periodicDeviceDiscoveryComplete listener count:', listenerCount);
this.log.debug('periodicDeviceDiscoveryEmitter max listener count:', this.periodicDeviceDiscoveryEmitter.getMaxListeners());
}
});
}
catch (error) {
this.log.error('Error during periodic device discovery:', error);
}
finally {
this.periodicDeviceDiscovering = false;
this.periodicDeviceDiscoveryEmitter.emit('periodicDeviceDiscoveryComplete');
this.log.debug('Finished periodic device discovery');
}
};
const deferAndCombinedTask = deferAndCombine(task, this.config.advancedOptions.waitTimeUpdate);
this.taskQueue.addTask(deferAndCombinedTask);
await deferAndCombinedTask();
}
findDiscoveredDevice(discoveredDevices, platformAccessory) {
this.log.debug(`Finding discovered device with Platform Accessory ${platformAccessory.displayName}`);
try {
const device = Object.values(discoveredDevices).find(device => device.sys_info.device_id === platformAccessory.context.deviceId);
if (device) {
this.log.debug(`Discovered device ${device.sys_info.alias}`);
}
else {
this.log.debug(`No discovered device found with Platform Accessory ${platformAccessory.displayName}`);
}
return device;
}
catch (error) {
this.log.error(`Error finding discovered device with Platform Accessory ${platformAccessory.displayName}: ${error}`);
return undefined;
}
}
updateAccessoryDeviceStatus(platformAccessory, device, now) {
this.log.debug(`Updating Platform Accessory and HomeKit device statuses for ${platformAccessory.displayName}`);
try {
this.log.debug(`Setting HomeKit device ${device.sys_info.alias} last seen time to now and marking as online`);
device.last_seen = now;
device.offline = false;
this.log.debug(`Setting Platform Accessory ${platformAccessory.displayName} last seen time to now and marking as online`);
platformAccessory.context.lastSeen = now;
platformAccessory.context.offline = false;
this.log.debug(`Updating Platform Accessory ${platformAccessory.displayName}`);
this.api.updatePlatformAccessories([platformAccessory]);
this.log.debug(`Platform Accessory and HomeKit device statuses for ${platformAccessory.displayName} updated successfully`);
}
catch (error) {
this.log.error(`Error updating Platform Accessory and HomeKit device statuses for ${platformAccessory.displayName}: ${error}`);
}
}
updateOrCreateHomeKitDevice(deviceId, device) {
this.log.debug(`Updating or creating HomeKit device ${device.sys_info.alias}`);
try {
if (this.homekitDevicesById.has(deviceId)) {
this.log.debug(`HomeKit device ${device.sys_info.alias} already exists.`);
const existingDevice = this.homekitDevicesById.get(deviceId);
if (existingDevice) {
if (!existingDevice.isUpdating) {
if (existingDevice.kasaDevice.offline === true && device.offline === false) {
this.log.debug(`HomeKit device ${device.sys_info.alias} was offline and is now online. ` +
'Updating device and starting polling.');
existingDevice.kasaDevice = device;
existingDevice.updateAfterPeriodicDiscovery();
existingDevice.startPolling();
}
else {
this.log.debug(`Updating existing HomeKit device ${device.sys_info.alias}`);
existingDevice.kasaDevice = device;
existingDevice.updateAfterPeriodicDiscovery();
}
}
else {
this.log.debug(`HomeKit device ${device.sys_info.alias} is currently updating. Skipping update.`);
}
}
else {
this.log.error(`Failed to retrieve existing HomeKit device ${device.sys_info.alias} from homekitDevicesById.`);
}
}
else {
this.log.debug(`HomeKit device ${device.sys_info.alias} does not exist.`);
this.foundDevice(device);
}
}
catch (error) {
this.log.error(`Error updating or creating HomeKit device ${device.sys_info.alias}: ${error}`);
}
}
updateAccessoryStatus(platformAccessory) {
try {
this.log.debug(`Setting Platform Accessory ${platformAccessory.displayName} offline status to true`);
platformAccessory.context.offline = true;
this.api.updatePlatformAccessories([platformAccessory]);
this.log.debug(`Platform Accessory ${platformAccessory.displayName} status updated successfully`);
}
catch (error) {
this.log.error(`Error updating Platform Accessory ${platformAccessory.displayName} status: ${error}`);
}
}
handleOfflineAccessory(platformAccessory, uuid, now, offlineInterval) {
this.log.debug(`Handling offline Platform Accessory ${platformAccessory.displayName}`);
try {
const homekitDevice = this.homekitDevicesById.get(platformAccessory.context.deviceId);
if (homekitDevice) {
const timeSinceLastSeen = now.getTime() - new Date(homekitDevice.kasaDevice.last_seen).getTime();
this.log.debug(`Time since last seen for Platform Accessory ${platformAccessory.displayName}: ${timeSinceLastSeen}ms, ` +
`offline interval: ${offlineInterval}ms`);
if (timeSinceLastSeen < offlineInterval) {
this.log.debug(`Platform Accessory ${platformAccessory.displayName} is offline and within offline interval.`);
homekitDevice.kasaDevice.offline = true;
}
else if (timeSinceLastSeen > offlineInterval) {
this.log.info(`Platform Accessory ${platformAccessory.displayName} is offline and outside the offline interval, removing.`);
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [platformAccessory]);
this.configuredAccessories.delete(uuid);
this.log.debug(`Platform Accessory [${platformAccessory.displayName}] removed successfully.`);
}
}
else if (platformAccessory.context.offline === true) {
const timeSinceLastSeen = now.getTime() - new Date(platformAccessory.context.lastSeen).getTime();
this.log.debug(`Time since last seen for Platform Accessory ${platformAccessory.displayName}: ${timeSinceLastSeen}ms, ` +
`offline interval: ${offlineInterval}ms`);
if (timeSinceLastSeen < offlineInterval) {
this.log.debug(`Platform Accessory [${platformAccessory.displayName}] is offline and within offline interval.`);
this.updateAccessoryStatus(platformAccessory);
}
else if (timeSinceLastSeen > offlineInterval) {
this.log.info(`Platform Accessory ${platformAccessory.displayName} is offline and outside the offline interval, removing.`);
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [platformAccessory]);
this.configuredAccessories.delete(uuid);
this.log.debug(`Platform Accessory [${platformAccessory.displayName}] removed successfully.`);
}
}
}
catch (error) {
this.log.error(`Error handling offline Platform Accessory ${platformAccessory.displayName}: ${error}`);
}
}
async checkPython(isUpgrade) {
try {
this.log.debug(`Running PythonChecker with isUpgrade: ${isUpgrade}`);
await new PythonChecker(this).allInOne(isUpgrade);
}
catch (error) {
this.log.error('Error checking python environment:', error);
throw error;
}
}
async startKasaApi() {
const scriptPath = path.join(__dirname, 'python', 'startKasaApi.py');
this.hideHomeKitMatter = this.config.homekitOptions.hideHomeKitMatter;
this.log.debug('Starting Kasa API with script:', scriptPath);
try {
const [, , , process] = await runCommand(this.log, this.venvPythonExecutable, [scriptPath, this.port.toString(), this.hideHomeKitMatter.toString()], undefined, this.config.advancedOptions.advancedPythonLogging ? false : true, this.config.advancedOptions.advancedPythonLogging ? false : true, true);
this.kasaProcess = process;
this.log.debug('Kasa API process started successfully');
}
catch (error) {
this.log.error(`Error starting kasaApi.py process: ${error instanceof Error ? error.message : 'Unknown error'}`);
throw error;
}
}
async discoverDevices() {
try {
const discoveredDevices = await this.deviceManager?.discoverDevices() || {};
const deviceCount = Object.keys(discoveredDevices).length;
this.log.debug(`Number of devices discovered: ${deviceCount}`);
if (deviceCount > 0) {
Object.values(discoveredDevices).forEach(device => {
this.log.debug(`Processing discovered device: ${device.sys_info.device_id}`);
let listenerCount = this.periodicDeviceDiscoveryEmitter.listenerCount('periodicDeviceDiscoveryComplete');
this.log.debug('periodicDeviceDiscoveryEmitter periodicDeviceDiscoveryComplete listener count:', listenerCount);
this.log.debug('periodicDeviceDiscoveryEmitter max listener count:', this.periodicDeviceDiscoveryEmitter.getMaxListeners());
this.foundDevice(device);
this.periodicDeviceDiscoveryEmitter.setMaxListeners(this.periodicDeviceDiscoveryEmitter.getMaxListeners() + 1);
listenerCount = this.periodicDeviceDiscoveryEmitter.listenerCount('periodicDeviceDiscoveryComplete');
this.log.debug('periodicDeviceDiscoveryEmitter periodicDeviceDiscoveryComplete listener count:', listenerCount);
this.log.debug('periodicDeviceDiscoveryEmitter max listener count:', this.periodicDeviceDiscoveryEmitter.getMaxListeners());
});
}
else {
this.log.debug('No devices discovered');
}
}
catch (error) {
this.log.error('Error discovering devices:', error);
}
}
stopKasaApi() {
this.log.debug('Stopping Kasa API');
if (this.kasaProcess) {
this.log.debug('Kasa API process found, attempting to kill the process');
this.kasaProcess.kill();
this.kasaProcess = null;
this.log.debug('Kasa API process successfully killed');
}
else {
this.log.debug('No Kasa API process found to stop');
}
}
lsc(serviceOrCharacteristic, characteristic) {
const serviceName = serviceOrCharacteristic instanceof this.api.hap.Service
? this.getServiceName(serviceOrCharacteristic)
: undefined;
const characteristicName = characteristic instanceof this.api.hap.Characteristic
? this.getCharacteristicName(characteristic)
: serviceOrCharacteristic instanceof this.api.hap.Characteristic || 'UUID' in serviceOrCharacteristic
? this.getCharacteristicName(serviceOrCharacteristic)
: undefined;
const result = `[${serviceName ? serviceName : ''}` +
`${serviceName && characteristicName ? '.' : ''}` +
`${characteristicName ? characteristicName : ''}]`;
return result;
}
getServiceName(service) {
const serviceName = lookup(this.api.hap.Service, (thisKeyValue, value) => isObjectLike(thisKeyValue) && 'UUID' in thisKeyValue && thisKeyValue.UUID === value, service.UUID);
return serviceName;
}
getCharacteristicName(characteristic) {
const name = characteristic.name;
const displayName = characteristic.displayName;
const lookupName = lookupCharacteristicNameByUUID(this.api.hap.Characteristic, characteristic.UUID);
return name ?? displayName ?? lookupName;
}
registerPlatformAccessory(platformAccessory) {
this.log.debug('Registering platform platformAccessory:', platformAccessory.displayName);
if (!this.configuredAccessories.has(platformAccessory.UUID)) {
this.log.debug(`Platform Accessory ${platformAccessory.displayName} is not in configuredAccessories, adding it.`);
this.configuredAccessories.set(platformAccessory.UUID, platformAccessory);
}
else {
this.log.debug(`Platform Accessory ${platformAccessory.displayName} is already in configuredAccessories.`);
}
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [platformAccessory]);
this.log.debug(`Platform Accessory ${platformAccessory.displayName} registered with Homebridge.`);
}
configureAccessory(platformAccessory) {
this.log.debug(`Configuring Platform Accessory: [${platformAccessory.displayName}] UUID: ${platformAccessory.UUID}`);
if (!platformAccessory.context.lastSeen && !platformAccessory.context.offline) {
this.log.debug(`Setting initial lastSeen and offline status for Platform Accessory: [${platformAccessory.displayName}]`);
platformAccessory.context.lastSeen = new Date();
platformAccessory.context.offline = false;
}
if (platformAccessory.context.lastSeen) {
const now = new Date();
const timeSinceLastSeen = now.getTime() - new Date(platformAccessory.context.lastSeen).getTime();
const offlineInterval = this.config.discoveryOptions.offlineInterval;
this.log.debug(`Platform Accessory [${platformAccessory.displayName}] last seen ${timeSinceLastSeen}ms ago, ` +
`offline interval is ${offlineInterval}ms, offline status: ${platformAccessory.context.offline}`);
if (timeSinceLastSeen > offlineInterval && platformAccessory.context.offline === true) {
this.log.info(`Platform Accessory [${platformAccessory.displayName}] is offline and outside the offline interval, ` +
'moving to offlineAccessories');
this.configuredAccessories.delete(platformAccessory.UUID);
this.offlineAccessories.set(platformAccessory.UUID, platformAccessory);
return;
}
else if (timeSinceLastSeen < offlineInterval && platformAccessory.context.offline === true) {
this.log.debug(`Platform Accessory [${platformAccessory.displayName}] is offline and within offline interval.`);
}
else if (platformAccessory.context.offline === false) {
this.log.debug(`Platform Accessory [${platformAccessory.displayName}] is online, updating lastSeen time.`);
platformAccessory.context.lastSeen = now;
this.api.updatePlatformAccessories([platformAccessory]);
}
}
if (!this.configuredAccessories.has(platformAccessory.UUID)) {
this.log.debug(`Platform Accessory [${platformAccessory.displayName}] with UUID ` +
`[${platformAccessory.UUID}] is not in configuredAccessories, adding it.`);
this.configuredAccessories.set(platformAccessory.UUID, platformAccessory);
}
else {
this.log.debug(`Platform Accessory [${platformAccessory.displayName}] with UUID [${platformAccessory.UUID}] is already in configuredAccessories.`);
}
}
foundDevice(device) {
const { sys_info: { alias: deviceAlias, device_id: deviceId, device_type: deviceType, host: deviceHost } } = device;
if (!deviceId) {
this.log.error('Missing deviceId:', deviceHost);
return;
}
if (this.homekitDevicesById.has(deviceId)) {
this.log.info(`HomeKit device already added: [${deviceAlias}] ${deviceType} [${deviceId}]`);
return;
}
this.log.info(`Adding HomeKit device: [${deviceAlias}] ${deviceType} [${deviceId}] at host [${deviceHost}]`);
const homekitDevice = this.createHomeKitDevice(device);
if (homekitDevice) {
this.homekitDevicesById.set(deviceId, homekitDevice);
this.log.debug(`HomeKit device [${deviceAlias}] ${deviceType} [${deviceId}] successfully added`);
}
else {
this.log.error(`Failed to add HomeKit device for: [${deviceAlias}] ${deviceType} [${deviceId}]`);
}
}
}
//# sourceMappingURL=platform.js.map