UNPKG

homebridge-irobot

Version:

A homebridge plugin for controlling iRobot devices

501 lines (497 loc) 27.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.iRobotPlatformAccessory = void 0; const events_1 = __importDefault(require("events")); const eventEmitter = new events_1.default.EventEmitter(); const dorita980_1 = __importDefault(require("dorita980")); /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ class iRobotPlatformAccessory { constructor(platform, accessory, device) { this.platform = platform; this.accessory = accessory; this.device = device; this.shutdown = false; this.binConfig = this.device.multiRoom && this.platform.config.ignoreMultiRoomBin ? [] : this.platform.config.bin.split(':'); this.active = false; this.lastStatus = { cycle: '', phase: '' }; this.lastCommandStatus = { pmap_id: null }; this.state = 0; this.binfull = 0; this.batteryStatus = { 'low': false, 'percent': 50, 'charging': true }; this.stuckStatus = false; this.roomByRoom = false; this.platform.api.on('shutdown', () => { this.platform.log.info('Disconnecting From Roomba:', device.name); this.shutdown = true; if (this.accessory.context.connected) { this.roomba.end(); } }); this.configureRoomba(); // set accessory information this.accessory.getService(this.platform.Service.AccessoryInformation) .setCharacteristic(this.platform.Characteristic.Manufacturer, 'iRobot') .setCharacteristic(this.platform.Characteristic.Model, this.device.model || this.device.info.sku || 'N/A') .setCharacteristic(this.platform.Characteristic.SerialNumber, this.device.info.serialNum || this.accessory.UUID || 'N/A') .setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.device.info.sw || this.device.info.ver || 'N/A') .getCharacteristic(this.platform.Characteristic.Identify).on('set', this.identify.bind(this)); this.service = this.accessory.getService(this.device.name) || this.accessory.addService(this.platform.Service.Fanv2, this.device.name, 'Main-Service'); this.service.setPrimaryService(true); if (this.device.multiRoom && this.accessory.context.maps !== undefined) { this.updateRooms(); } if (this.binConfig.includes('filter')) { this.binFilter = this.accessory.getService(this.device.name + '\'s Bin Filter') || this.accessory.addService(this.platform.Service.FilterMaintenance, this.device.name + '\'s Bin Filter', 'Filter-Bin'); } if (this.binConfig.includes('contact')) { this.binContact = this.accessory.getService(this.device.name + '\'s Bin Contact Sensor') || this.accessory.addService(this.platform.Service.ContactSensor, this.device.name + '\'s Bin Contact Sensor', 'Contact-Bin'); } if (this.binConfig.includes('motion')) { this.binMotion = this.accessory.getService(this.device.name + '\'s Bin Motion Sensor') || this.accessory.addService(this.platform.Service.MotionSensor, this.device.name + '\'s Bin Motion Sensor', 'Motion-Bin'); } this.battery = this.accessory.getService(this.device.name + '\'s Battery') || this.accessory.addService(this.platform.Service.Battery, this.device.name + '\'s Battery', 'Battery-Service'); if (!this.platform.config.hideStuckSensor) { this.stuck = this.accessory.getService(this.device.name + ' Stuck') || this.accessory.addService(this.platform.Service.MotionSensor, this.device.name + ' Stuck', 'Stuck-MotionSensor'); } // set the service name, this is what is displayed as the default name on the Home app // in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method. /*this.service.setCharacteristic(this.platform.Characteristic.Name, this.device.name); if(this.binConfig.includes('filter')){ this.binFilter.setCharacteristic(this.platform.Characteristic.Name, this.device.name + '\'s Bin'); } if(this.binConfig.includes('contact')){ this.binContact.setCharacteristic(this.platform.Characteristic.Name, this.device.name + '\'s Bin'); } if(this.binConfig.includes('motion')){ this.binMotion.setCharacteristic(this.platform.Characteristic.Name, this.device.name + '\'s Bin'); } this.battery.setCharacteristic(this.platform.Characteristic.Name, this.device.name + '\'s Battery'); this.stuck.setCharacteristic(this.platform.Characteristic.Name, this.device.name + ' Stuck'); */ // each service must implement at-minimum the "required characteristics" for the given service type // see https://developers.homebridge.io/#/service/Lightbulb this.service.getCharacteristic(this.platform.Characteristic.Active) .onSet(this.set.bind(this)) // SET - bind to the `setOn` method below .onGet(this.get.bind(this)); // GET - bind to the `getOn` method below this.service.getCharacteristic(this.platform.Characteristic.CurrentFanState) .onGet(this.getState.bind(this)); // GET - bind to the if (this.device.multiRoom) { this.service.getCharacteristic(this.platform.Characteristic.TargetFanState) .onGet(this.getMode.bind(this)) // GET .onSet(this.setMode.bind(this)); } if (this.binConfig.includes('filter')) { this.binFilter.getCharacteristic(this.platform.Characteristic.FilterChangeIndication) .onGet(this.getBinfull.bind(this)); } if (this.binConfig.includes('contact')) { this.binContact.getCharacteristic(this.platform.Characteristic.ContactSensorState) .onGet(this.getBinfull.bind(this)); } if (this.binConfig.includes('motion')) { this.binMotion.getCharacteristic(this.platform.Characteristic.MotionDetected) .onGet(this.getBinfullBoolean.bind(this)); } this.battery.getCharacteristic(this.platform.Characteristic.StatusLowBattery) .onGet(this.getBatteryStatus.bind(this)); this.battery.getCharacteristic(this.platform.Characteristic.BatteryLevel) .onGet(this.getBatteryLevel.bind(this)); this.battery.getCharacteristic(this.platform.Characteristic.ChargingState) .onGet(this.getChargeState.bind(this)); if (!this.platform.config.hideStuckSensor) { this.stuck.getCharacteristic(this.platform.Characteristic.MotionDetected) .onGet(this.getStuck.bind(this)); } /*this.battery.getCharacteristic(this.platform.Characteristic.StatusLowBattery) .onSet(this.get) */ /** * Creating multiple services of the same type. * * To avoid "Cannot add a Service with the same UUID another Service without also defining a unique 'subtype' property." error, * when creating multiple services of the same type, you need to use the following syntax to specify a name and subtype id: * this.accessory.getService('NAME') || this.accessory.addService(this.platform.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE_ID'); * * The USER_DEFINED_SUBTYPE must be unique to the platform accessory (if you platform exposes multiple accessories, each accessory * can use the same sub type id.) */ // Example: add two "motion sensor" services to the accessory /* const motionSensorOneService = this.accessory.getService('Motion Sensor One Name') || this.accessory.addService(this.platform.Service.MotionSensor, 'Motion Sensor One Name', 'YourUniqueIdentifier-1'); const motionSensorTwoService = this.accessory.getService('Motion Sensor Two Name') || this.accessory.addService(this.platform.Service.MotionSensor, 'Motion Sensor Two Name', 'YourUniqueIdentifier-2'); /* /** * Updating characteristics values asynchronously. * * Example showing how to update the state of a Characteristic asynchronously instead * of using the `on('get')` handlers. * Here we change update the motion sensor trigger states on and off every 10 seconds * the `updateCharacteristic` method. * */ //let motionDetected = false; } async configureRoomba() { var _a; this.accessory.context.connected = false; if ((_a = this.device.info.sku) === null || _a === void 0 ? void 0 : _a.startsWith('j')) { process.env.ROBOT_CIPHERS = 'TLS_AES_256_GCM_SHA384'; } this.roomba = new dorita980_1.default.Local(this.device.blid, this.device.password, this.device.ip, this.device.info.ver !== undefined ? parseInt(this.device.info.ver) : 2); this.roomba.on('connect', () => { this.accessory.context.connected = true; this.platform.log.info('Succefully connected to roomba', this.device.name); }).on('offline', () => { this.accessory.context.connected = false; this.platform.log.warn('Roomba', this.device.name, ' went offline, disconnecting...'); this.roomba.end(); //throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE); }).on('close', () => { this.accessory.context.connected = false; this.roomba.removeAllListeners(); if (this.shutdown) { this.platform.log.info('Roomba', this.device.name, 'connection closed'); } else { this.platform.log.warn('Roomba', this.device.name, ' connection closed, reconnecting in 5 seconds'); setTimeout(() => { this.platform.log.warn('Attempting To Reconnect To Roomba', this.device.name); this.configureRoomba(); }, 5000); //throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE); } }).on('state', this.updateRoombaState.bind(this)); } updateRoombaState(data) { if (data.cleanMissionStatus.cycle !== this.lastStatus.cycle || data.cleanMissionStatus.phase !== this.lastStatus.phase) { eventEmitter.emit('state'); this.platform.log.debug(this.device.name + '\'s mission update:', '\n cleeanMissionStatus:', JSON.stringify(data.cleanMissionStatus, null, 2), '\n batPct:', data.batPct, '\n bin:', JSON.stringify(data.bin), '\n lastCommand:', JSON.stringify(data.lastCommand)); if (data.cleanMissionStatus.phase === 'stuck' && this.lastStatus.phase !== 'stuck') { this.platform.log.warn('Roomba', this.device.name, 'Is Stuck!'); } else if (this.lastStatus.phase === 'stuck' && data.cleanMissionStatus.phase !== 'stuck') { this.platform.log.info('Roomba', this.device.name, 'Says "Thank You For Freeing Me"'); } } this.lastStatus = data.cleanMissionStatus; if ((this.device.multiRoom && (data.lastCommand.pmap_id !== null && data.lastCommand.pmap_id !== undefined)) && data.lastCommand.pmap_id !== this.lastCommandStatus.pmap_id) { this.updateMap(data.lastCommand); } this.lastCommandStatus = data.lastCommand; /*------------------------------------------------------------------------------------------------------------------------------------*/ this.active = this.getHomekitActive(data.cleanMissionStatus); this.state = this.active ? 2 : this.getEveInactive(data.cleanMissionStatus) ? 0 : 1; this.binfull = data.bin.full ? 1 : 0; this.stuckStatus = data.cleanMissionStatus.phase === 'stuck'; this.batteryStatus.charging = data.cleanMissionStatus.phase === 'charge'; this.batteryStatus.low = this.batteryStatus.charging && data.batPct < (this.platform.config.lowBattery || 20); this.batteryStatus.percent = data.batPct; /*------------------------------------------------------------------------------------------------------------------------------------*/ this.service.updateCharacteristic(this.platform.Characteristic.Active, this.active ? 1 : 0); this.service.updateCharacteristic(this.platform.Characteristic.CurrentFanState, this.state); if (this.binConfig.includes('filter')) { this.binFilter.updateCharacteristic(this.platform.Characteristic.FilterChangeIndication, this.binfull); } if (this.binConfig.includes('contact')) { this.binContact.updateCharacteristic(this.platform.Characteristic.ContactSensorState, this.binfull); } if (this.binConfig.includes('motion')) { this.binMotion.updateCharacteristic(this.platform.Characteristic.MotionDetected, this.binfull === 1); } if (this.platform.config.hideStuckSensor) { this.stuck.updateCharacteristic(this.platform.Characteristic.MotionDetected, this.stuckStatus); } this.battery.updateCharacteristic(this.platform.Characteristic.BatteryLevel, this.batteryStatus.percent); this.battery.updateCharacteristic(this.platform.Characteristic.StatusLowBattery, this.batteryStatus.low); this.battery.updateCharacteristic(this.platform.Characteristic.ChargingState, this.batteryStatus.charging); } updateMap(lastCommand) { if (this.accessory.context.maps !== undefined) { let index = -1; for (const map of this.accessory.context.maps) { if (map.pmap_id === lastCommand.pmap_id) { index = this.accessory.context.maps.indexOf(map); } } if (index !== -1) { //const oldRegions = this.accessory.context.maps[index].regions; for (const region of lastCommand.regions) { let exists = false; for (const region_ of this.accessory.context.maps[index].regions) { if (region_.region_id === region.region_id) { exists = true; } } if (!exists) { this.platform.log.info('Adding new region for roomba:', this.device.name, '\n', region); this.accessory.context.maps[index].regions.push(region); } } //if (oldRegions !== this.accessory.context.maps[index].regions) { this.platform.log.debug(this.device.name + '\'s map update:', '\n map:', JSON.stringify(this.accessory.context.maps)); this.platform.log.debug('Updating Homekit Rooms for Roomba:', this.device.name); this.updateRooms(); //} } else { this.platform.log.info('Creating new map for roomba:', this.device.name); this.accessory.context.maps.push({ 'pmap_id': lastCommand.pmap_id, 'regions': lastCommand.regions, 'user_pmapv_id': lastCommand.user_pmapv_id, }); this.platform.log.debug(this.device.name + '\'s map update:', '\n map:', JSON.stringify(this.accessory.context.maps)); this.platform.log.debug('Updating Homekit Rooms for Roomba:', this.device.name); this.updateRooms(); } } else { this.platform.log.info('Creating new map for roomba:', this.device.name); this.accessory.context.maps = [{ 'pmap_id': lastCommand.pmap_id, 'regions': lastCommand.regions, 'user_pmapv_id': lastCommand.user_pmapv_id, }]; this.platform.log.debug(this.device.name + '\'s map update:', '\n map:', JSON.stringify(this.accessory.context.maps)); this.platform.log.debug('Updating Homekit Rooms for Roomba:', this.device.name); this.updateRooms(); } } updateRooms() { this.accessory.context.activeRooms = []; //if (this.accessory.context.maps !== undefined) { for (const map of this.accessory.context.maps) { const index = this.accessory.context.maps.indexOf(map); for (const region of map.regions) { ((this.accessory.getService('Map ' + index + ' Room ' + region.region_id) || this.accessory.addService(this.platform.Service.Switch, 'Map ' + index + ' Room ' + region.region_id, index + ':' + region.region_id)) .getCharacteristic(this.platform.Characteristic.On)) .removeAllListeners() .onSet((activate) => { if (activate) { this.accessory.context.activeMap = index; if (!this.accessory.context.activeRooms.includes(region.region_id)) { this.accessory.context.activeRooms.push(region.region_id); } } else { this.accessory.context.activeRooms.splice(this.accessory.context.activeRooms.indexOf(region.region_id)); } this.platform.log.info(activate ? 'enabling' : 'disabling', 'room ' + region.region_id + ' of map ' + index + ' on roomba ' + this.device.name); }) .onGet(() => { return this.accessory.context.activeMap === index ? this.accessory.context.activeRooms.includes(region.region_id) : false; }); } } } //} getHomekitActive(cleanMissionStatus) { const configStatus = this.platform.config.status.split(':'); switch (configStatus[0]) { case true: return true; case false: return false; case 'inverted': return cleanMissionStatus[configStatus[1]] !== configStatus[2]; default: return cleanMissionStatus[configStatus[0]] === configStatus[1]; } } getEveInactive(cleanMissionStatus) { const configStatus = this.platform.config.eveStatus.split(':'); switch (configStatus[0]) { case true: return true; case false: return false; case 'inverted': return cleanMissionStatus[configStatus[1]] !== configStatus[2]; default: return cleanMissionStatus[configStatus[0]] === configStatus[1]; } } /** * Handle the "GET" requests from HomeKit * These are sent when HomeKit wants to know the current state of the accessory, for example, checking if a Light bulb is on. * * GET requests should return as fast as possbile. A long delay here will result in * HomeKit being unresponsive and a bad user experience in general. * * If your device takes time to respond you should update the status of your device * asynchronously instead using the `updateCharacteristic` method instead. * @example * this.service.updateCharacteristic(this.platform.Characteristic.On, true) */ async get() { if (this.accessory.context.connected) { this.platform.log.debug('Updating', this.device.name, 'To', this.active ? 'On' : 'Off'); return this.active ? 1 : 0; } else { throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */); } } async getState() { if (this.accessory.context.connected) { this.platform.log.debug('Updating', this.device.name, 'Mode To', this.state === 0 ? 'Off' : this.state === 1 ? 'Idle' : 'On'); return this.state; } else { throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */); } } async getBinfull() { if (this.accessory.context.connected) { this.platform.log.debug('Updating', this.device.name, 'Binfull To', this.binfull === 0 ? 'OK' : 'FULL'); return this.binfull; } else { throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */); } } async getBinfullBoolean() { if (this.accessory.context.connected) { this.platform.log.debug('Updating', this.device.name, 'Binfull To', this.binfull === 0 ? 'OK' : 'FULL'); return this.binfull === 1; } else { throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */); } } async getBatteryLevel() { if (this.accessory.context.connected) { this.platform.log.debug('Updating', this.device.name, 'Battery Level To', this.batteryStatus.percent); return this.batteryStatus.percent; } else { throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */); } } async getBatteryStatus() { if (this.accessory.context.connected) { this.platform.log.debug('Updating', this.device.name, 'Battery Status To', this.batteryStatus.low ? 'Low' : 'Normal'); return this.batteryStatus.low ? 1 : 0; } else { throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */); } } async getChargeState() { if (this.accessory.context.connected) { this.platform.log.debug('Updating', this.device.name, 'Charge Status To', this.batteryStatus.charging ? 'Charging' : 'Not Charging'); return this.batteryStatus.charging ? 1 : 0; } else { throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */); } } async getMode() { if (this.accessory.context.connected) { this.platform.log.debug('Updating', this.device.name, 'Mode To', this.roomByRoom ? 'Room-By-Room' : 'Everywhere'); return this.roomByRoom ? 0 : 1; } else { throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */); } } async getStuck() { if (this.accessory.context.connected) { this.platform.log.debug('Updating', this.device.name, 'Stuck To', this.stuckStatus); return this.stuckStatus; } else { throw new this.platform.api.hap.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */); } } async identify() { if (this.accessory.context.connected) { await this.roomba.find(); this.platform.log.info('Identifying', this.device.name, '(Note: Some Models Won\'t Beep If Docked'); } } /** * Handle "SET" requests from HomeKit * These are sent when the user changes the state of an accessory, for example, changing the Brightness */ async set(value) { if (this.accessory.context.connected) { const configOffAction = this.platform.config.offAction.split(':'); let args; try { if (value === 1) { //give scenes a chance to run setTimeout(async () => { if (this.roomByRoom) { await this.roomba.stop(); if (this.accessory.context.activeRooms !== undefined) { args = { 'ordered': 1, 'pmap_id': this.accessory.context.maps[this.accessory.context.activeMap].pmap_id, 'user_pmapv_id': this.accessory.context.maps[this.accessory.context.activeMap].user_pmapv_id, 'regions': [{}], }; args.regions.splice(0); for (const room of this.accessory.context.activeRooms) { for (const region of this.accessory.context.maps[this.accessory.context.activeMap].regions) { if (region.region_id === room) { args.regions.push(region); } } } this.platform.log.debug('Clean Room Args:\n', JSON.stringify(args)); this.roomba.cleanRoom(args); } } else { await this.roomba.clean(); await this.roomba.resume(); } }, this.device.multiRoom ? 1000 : 0); } else { await this.roomba[configOffAction[0]](); setTimeout(async () => { eventEmitter.emit('state'); }, 5000); eventEmitter.on('state', async () => { if (configOffAction[1] !== 'none') { await this.roomba[configOffAction[1]](); } eventEmitter.removeAllListeners(); }); } this.platform.log.info('Set', this.device.name, 'To', value === 0 ? configOffAction[0] + (configOffAction[1] !== 'none' ? ' and ' + configOffAction[1] : '') : 'Clean', this.roomByRoom ? 'With args:' + JSON.stringify(args) : ''); } catch (err) { this.platform.log.warn('Error Seting', this.device.name, 'To', value === 0 ? configOffAction[0] + (configOffAction[1] !== 'none' ? ' and ' + configOffAction[1] : '') : 'Clean', this.roomByRoom ? 'With args:' + JSON.stringify(args) : ''); this.platform.log.error(err); } } } async setMode(value) { if (this.accessory.context.connected) { this.platform.log.info('Set', this.device.name, 'To', value === 0 ? 'Room-By-Room' : 'Everywhere'); this.roomByRoom = value === 0; } } } exports.iRobotPlatformAccessory = iRobotPlatformAccessory; //# sourceMappingURL=platformAccessory.js.map