diff --git a/accessories/neatoVacuumRobot.js b/accessories/neatoVacuumRobot.js index 678c43c..2c3908a 100644 --- a/accessories/neatoVacuumRobot.js +++ b/accessories/neatoVacuumRobot.js @@ -218,75 +218,7 @@ NeatoVacuumRobotAccessory.prototype = { setClean: function (on, callback) { - debug(this.name + ": " + (on ? "Enabled ".brightGreen : "Disabled".red) + " Clean " + (this.boundary ? JSON.stringify(this.boundary) : '')); - this.platform.updateRobot(this.robot._serial, (error, result) => - { - // Start - if (on) - { - // No room given or same room - if (this.boundary == null || this.robot.cleaningBoundaryId === this.boundary.id) - { - // Resume cleaning - if (this.robot.canResume) - { - debug(this.name + ": ## Resume cleaning"); - this.robot.resumeCleaning((error) => - { - callback(error); - }); - } - // Start cleaning - else if (this.robot.canStart) - { - this.clean(callback); - } - // Cannot start - else - { - debug(this.name + ": Cannot start, maybe already cleaning (expected)"); - callback(); - } - } - // Different room given - else - { - // Return to dock - if (this.robot.canPause || this.robot.canResume) - { - debug(this.name + ": ## Returning to dock to start cleaning of new room"); - this.setGoToDock(true, (error, result) => - { - this.nextRoom = this.boundary.id; - callback(); - }); - } - // Start new cleaning of new room - else - { - debug(this.name + ": ## Start cleaning of new room"); - this.clean(callback); - } - } - } - // Stop - else - { - if (this.robot.canPause) - { - debug(this.name + ": ## Pause cleaning"); - this.robot.pauseCleaning((error) => - { - callback(error); - }); - } - else - { - debug(this.name + ": Already paused"); - callback(); - } - } - }); + }, clean: function (callback, spot) diff --git a/index.js b/old_index.js similarity index 99% rename from index.js rename to old_index.js index 92c18f6..e3b1bf5 100644 --- a/index.js +++ b/old_index.js @@ -26,7 +26,7 @@ function NeatoVacuumRobotPlatform(log, config) this.hiddenServices = ('hidden' in config ? config['hidden'] : this.hiddenServices); // Array of real robots and associated robot accessories (incl rooms) - this.robots = []; + this.robotAccessories = []; this.nextRoom = null; if ('refresh' in config && config['refresh'] !== 'auto') @@ -149,7 +149,7 @@ NeatoVacuumRobotPlatform.prototype = { else if (robots.length === 0) { this.log.error("Successful login but no robots associated with your account."); - this.robots = []; + this.robotAccessories = []; callback(); } else diff --git a/src/accessories/neatoVacuumRobot.ts b/src/accessories/neatoVacuumRobot.ts index 79022fe..d279046 100644 --- a/src/accessories/neatoVacuumRobot.ts +++ b/src/accessories/neatoVacuumRobot.ts @@ -1,141 +1,187 @@ -import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; - -import { HomebridgeNeatoPlatform } from '../homebridgeNeatoPlatform'; +import {CharacteristicValue, PlatformAccessory, Service} from 'homebridge'; +import {HomebridgeNeatoPlatform} from '../homebridgeNeatoPlatform'; +const debug = require('debug')('my-app:my-module'); /** * 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. */ -export class NeatoVacuumRobotAccessory { - private service: Service; +export class NeatoVacuumRobotAccessory +{ + private cleanService: Service; + private robot: any; + // private goToDockService: Service; + // private dockStateService: Service; + // private ecoService: Service; + // private noGoLinesService: Service; + // private extraCareService: Service; + // private scheduleService: Service; + // private findMeService: Service; + // private spotCleanService: Service; - /** - * These are just used to create a working example - * You should implement your own code to track the state of your accessory - */ - private exampleStates = { - On: false, - Brightness: 100, - }; + /** + * These are just used to create a working example + * You should implement your own code to track the state of your accessory + */ - constructor( - private readonly platform: HomebridgeNeatoPlatform, - private readonly accessory: PlatformAccessory, - ) { + constructor( + private readonly platform: HomebridgeNeatoPlatform, + private readonly accessory: PlatformAccessory, + private readonly isNew: Boolean) + { + this.robot = accessory.context.robot; + + // set accessory information + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, "Neato Robotics") + .setCharacteristic(this.platform.Characteristic.Model, this.robot.meta.modelName) + .setCharacteristic(this.platform.Characteristic.SerialNumber, this.robot._serial) + .setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.robot.meta.firmware) + .setCharacteristic(this.platform.Characteristic.Name, this.robot.name); - // set accessory information - this.accessory.getService(this.platform.Service.AccessoryInformation)! - .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Default-Manufacturer') - .setCharacteristic(this.platform.Characteristic.Model, 'Default-Model') - .setCharacteristic(this.platform.Characteristic.SerialNumber, 'Default-Serial'); - // get the LightBulb service if it exists, otherwise create a new LightBulb service - // you can create multiple services for each accessory - this.service = this.accessory.getService(this.platform.Service.Lightbulb) || this.accessory.addService(this.platform.Service.Lightbulb); + let cleanServiceName = robot.name + " Clean"; + this.cleanService = this.accessory.getService(cleanServiceName) || this.accessory.addService(this.platform.Service.Switch, cleanServiceName, "CLEAN"); + + this.cleanService.getCharacteristic(this.platform.Characteristic.On) + .onSet(this.setClean.bind(this)) + .onGet(this.getClean.bind(this)); - // 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, accessory.context.device.exampleDisplayName); + // /** + // * 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; + // setInterval(() => { + // // EXAMPLE - inverse the trigger + // motionDetected = !motionDetected; + // + // // push the new value to HomeKit + // motionSensorOneService.updateCharacteristic(this.platform.Characteristic.MotionDetected, motionDetected); + // motionSensorTwoService.updateCharacteristic(this.platform.Characteristic.MotionDetected, !motionDetected); + // + // this.platform.log.debug('Triggering motionSensorOneService:', motionDetected); + // this.platform.log.debug('Triggering motionSensorTwoService:', !motionDetected); + // }, 10000); + } - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/Lightbulb - // register handlers for the On/Off Characteristic - this.service.getCharacteristic(this.platform.Characteristic.On) - .onSet(this.setOn.bind(this)) // SET - bind to the `setOn` method below - .onGet(this.getOn.bind(this)); // GET - bind to the `getOn` method below + async setClean(on: CharacteristicValue) + { + // TODO debug(this.robot.name + ": " + (on ? "Enabled ".brightGreen : "Disabled".red) + " Clean " + (this.boundary ? JSON.stringify(this.boundary) : '')); + this.platform.updateRobot(this.robot._serial, (error, result) => + { + // Start + if (on) + { + // No room given or same room + if (this.boundary == null || this.robot.cleaningBoundaryId === this.boundary.id) + { + // Resume cleaning + if (this.robot.canResume) + { + debug(this.name + ": ## Resume cleaning"); + this.robot.resumeCleaning((error) => + { + callback(error); + }); + } + // Start cleaning + else if (this.robot.canStart) + { + this.clean(callback); + } + // Cannot start + else + { + debug(this.name + ": Cannot start, maybe already cleaning (expected)"); + callback(); + } + } + // Different room given + else + { + // Return to dock + if (this.robot.canPause || this.robot.canResume) + { + debug(this.name + ": ## Returning to dock to start cleaning of new room"); + this.setGoToDock(true, (error, result) => + { + this.nextRoom = this.boundary.id; + callback(); + }); + } + // Start new cleaning of new room + else + { + debug(this.name + ": ## Start cleaning of new room"); + this.clean(callback); + } + } + } + // Stop + else + { + if (this.robot.canPause) + { + debug(this.name + ": ## Pause cleaning"); + this.robot.pauseCleaning((error) => + { + callback(error); + }); + } + else + { + debug(this.name + ": Already paused"); + callback(); + } + } + }); + } + + async getClean(): Promise + { + // implement your own code to check if the device is on + const isOn = this.exampleStates.On; - // register handlers for the Brightness Characteristic - this.service.getCharacteristic(this.platform.Characteristic.Brightness) - .onSet(this.setBrightness.bind(this)); // SET - bind to the 'setBrightness` method below + this.platform.log.debug('Get Characteristic On ->', isOn); - /** - * 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.) - */ + // if you need to return an error to show the device as "Not Responding" in the Home app: + // throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE); - // 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; - setInterval(() => { - // EXAMPLE - inverse the trigger - motionDetected = !motionDetected; - - // push the new value to HomeKit - motionSensorOneService.updateCharacteristic(this.platform.Characteristic.MotionDetected, motionDetected); - motionSensorTwoService.updateCharacteristic(this.platform.Characteristic.MotionDetected, !motionDetected); - - this.platform.log.debug('Triggering motionSensorOneService:', motionDetected); - this.platform.log.debug('Triggering motionSensorTwoService:', !motionDetected); - }, 10000); - } - - /** - * Handle "SET" requests from HomeKit - * These are sent when the user changes the state of an accessory, for example, turning on a Light bulb. - */ - async setOn(value: CharacteristicValue) { - // implement your own code to turn your device on/off - this.exampleStates.On = value as boolean; - - this.platform.log.debug('Set Characteristic On ->', value); - } - - /** - * 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 getOn(): Promise { - // implement your own code to check if the device is on - const isOn = this.exampleStates.On; - - this.platform.log.debug('Get Characteristic On ->', isOn); - - // if you need to return an error to show the device as "Not Responding" in the Home app: - // throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE); - - return isOn; - } - - /** - * Handle "SET" requests from HomeKit - * These are sent when the user changes the state of an accessory, for example, changing the Brightness - */ - async setBrightness(value: CharacteristicValue) { - // implement your own code to set the brightness - this.exampleStates.Brightness = value as number; - - this.platform.log.debug('Set Characteristic Brightness -> ', value); - } + return isOn; + } + + // /** + // * 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 getOn(): Promise + // { + // // implement your own code to check if the device is on + // const isOn = this.exampleStates.On; + // + // this.platform.log.debug('Get Characteristic On ->', isOn); + // + // // if you need to return an error to show the device as "Not Responding" in the Home app: + // // throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE); + // + // return isOn; + // } } diff --git a/src/characteristics/spotHeight.ts b/src/characteristics/spotHeight.ts new file mode 100644 index 0000000..1546aed --- /dev/null +++ b/src/characteristics/spotHeight.ts @@ -0,0 +1,19 @@ +import type { Characteristic, WithUUID } from 'homebridge'; +import { Formats, Perms } from 'homebridge'; + +export default function spotHeight(CustomCharacteristic: typeof Characteristic): WithUUID Characteristic> { + return class SpotHeight extends CustomCharacteristic { + static readonly UUID = 'CA282DB2-62BF-4325-A1BE-F8BB5478781A'; + + constructor() { + super('Spot ↕', SpotHeight.UUID, { + format: Formats.INT, + unit: 'cm', + maxValue: 400, + minValue: 100, + minStep: 50, + perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE] + }); + } + }; +} \ No newline at end of file diff --git a/src/characteristics/spotRepeat.ts b/src/characteristics/spotRepeat.ts new file mode 100644 index 0000000..f4d8f09 --- /dev/null +++ b/src/characteristics/spotRepeat.ts @@ -0,0 +1,15 @@ +import type { Characteristic, WithUUID } from 'homebridge'; +import { Formats, Perms } from 'homebridge'; + +export default function spotRepeat(CustomCharacteristic: typeof Characteristic): WithUUID Characteristic> { + return class SpotRepeat extends CustomCharacteristic { + static readonly UUID = '1E79C603-63B8-4E6A-9CE1-D31D67981831'; + + constructor() { + super('Spot 2x', SpotRepeat.UUID, { + format: Formats.BOOL, + perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE] + }); + } + }; +} \ No newline at end of file diff --git a/src/characteristics/spotWidth.ts b/src/characteristics/spotWidth.ts new file mode 100644 index 0000000..8632445 --- /dev/null +++ b/src/characteristics/spotWidth.ts @@ -0,0 +1,19 @@ +import type { Characteristic, WithUUID } from 'homebridge'; +import { Formats, Perms } from 'homebridge'; + +export default function spotWidth(CustomCharacteristic: typeof Characteristic): WithUUID Characteristic> { + return class SpotWidth extends CustomCharacteristic { + static readonly UUID = 'A7889A9A-2F27-4293-BEF8-3FE805B36F4E'; + + constructor() { + super('Spot ↔', SpotWidth.UUID, { + format: Formats.INT, + unit: 'cm', + maxValue: 400, + minValue: 100, + minStep: 50, + perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE] + }); + } + }; +} \ No newline at end of file diff --git a/src/homebridgeNeatoPlatform.ts b/src/homebridgeNeatoPlatform.ts index 18e595a..5880bc5 100644 --- a/src/homebridgeNeatoPlatform.ts +++ b/src/homebridgeNeatoPlatform.ts @@ -17,7 +17,7 @@ export class HomebridgeNeatoPlatform implements DynamicPlatformPlugin public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; // this is used to track restored cached accessories - public readonly accessories: PlatformAccessory[] = []; + public readonly robotAccessories: PlatformAccessory[] = []; constructor( public readonly log: Logger, @@ -41,7 +41,7 @@ export class HomebridgeNeatoPlatform implements DynamicPlatformPlugin this.log.info('Loading accessory from cache:', accessory.displayName); // add the restored accessory to the accessories cache so we can track if it has already been registered - this.accessories.push(accessory); + this.robotAccessories.push(accessory); } discoverRobots() @@ -50,11 +50,11 @@ export class HomebridgeNeatoPlatform implements DynamicPlatformPlugin let client = new NeatoApi.Client(); // Login - client.authorize(this.email, this.password, false, (error) => { + client.authorize((this.config)['email'], (this.config)['password'], false, (error) => { if (error) { this.log.error("Can't log on to neato cloud. Please check your internet connection and your credentials. Try again later if the neato servers have issues: " + error); - callback(); + return; } else { @@ -63,13 +63,12 @@ export class HomebridgeNeatoPlatform implements DynamicPlatformPlugin if (error) { this.log.error("Successful login but can't connect to your neato robot: " + error); - callback(); + return; } else if (robots.length === 0) { this.log.error("Successful login but no robots associated with your account."); - this.robots = []; - callback(); + return; } else { @@ -82,60 +81,97 @@ export class HomebridgeNeatoPlatform implements DynamicPlatformPlugin if (error) { this.log.error("Error getting robot meta information: " + error + ": " + state); - callback(); + return; } else { - // Get all maps for each robot - robot.getPersistentMaps((error, maps) => { - if (error) - { - this.log.error("Error updating persistent maps: " + error + ": " + maps); - callback(); - } - // Robot has no maps - else if (maps.length === 0) - { - robot.maps = []; - this.robots.push({device: robot, meta: state.meta, availableServices: state.availableServices}); - loadedRobots++; - if (loadedRobots === robots.length) - { - callback(); - } - } - // Robot has maps - else - { - robot.maps = maps; - let loadedMaps = 0; - robot.maps.forEach((map) => { - // Save zones in each map - robot.getMapBoundaries(map.id, (error, result) => { - if (error) - { - this.log.error("Error getting boundaries: " + error + ": " + result) - } - else - { - map.boundaries = result.boundaries; - } - loadedMaps++; + const uuid = this.api.hap.uuid.generate(robot._serial); + const existingAccessory = this.robotAccessories.find(accessory => accessory.UUID === uuid); - // Robot is completely requested if zones for all maps are loaded - if (loadedMaps === robot.maps.length) - { - this.robots.push({device: robot, meta: state.meta, availableServices: state.availableServices}); - loadedRobots++; - if (loadedRobots === robots.length) - { - callback(); - } - } - }) - }); - } - }); + if (existingAccessory) + { + // the accessory already exists + this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); + + // TODO update maps + + // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: + // existingAccessory.context.device = device; + // this.api.updatePlatformAccessories([existingAccessory]); + + // create the accessory handler for the restored accessory + // this is imported from `platformAccessory.ts` + new NeatoVacuumRobotAccessory(this, existingAccessory, false); + + // it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.: + // remove platform accessories when no longer present + // this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]); + // this.log.info('Removing existing accessory from cache:', existingAccessory.displayName); + } + else + { + this.log.info('Adding new accessory: ', robot.name); + const accessory = new this.api.platformAccessory(robot.name, uuid); + + accessory.context.robot = robot; + new NeatoVacuumRobotAccessory(this, accessory, true); + + // link the accessory to your platform + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + // TODO get maps + } + + + // // Get all maps for each robot + // robot.getPersistentMaps((error, maps) => { + // if (error) + // { + // this.log.error("Error updating persistent maps: " + error + ": " + maps); + // callback(); + // } + // // Robot has no maps + // else if (maps.length === 0) + // { + // robot.maps = []; + // this.robotAccessories.push({device: robot, meta: state.meta, availableServices: state.availableServices}); + // loadedRobots++; + // if (loadedRobots === robots.length) + // { + // callback(); + // } + // } + // // Robot has maps + // else + // { + // robot.maps = maps; + // let loadedMaps = 0; + // robot.maps.forEach((map) => { + // // Save zones in each map + // robot.getMapBoundaries(map.id, (error, result) => { + // if (error) + // { + // this.log.error("Error getting boundaries: " + error + ": " + result) + // } + // else + // { + // map.boundaries = result.boundaries; + // } + // loadedMaps++; + // + // // Robot is completely requested if zones for all maps are loaded + // if (loadedMaps === robot.maps.length) + // { + // this.robotAccessories.push({device: robot, meta: state.meta, availableServices: state.availableServices}); + // loadedRobots++; + // if (loadedRobots === robots.length) + // { + // callback(); + // } + // } + // }) + // }); + // } + // }); } }); }); @@ -143,68 +179,5 @@ export class HomebridgeNeatoPlatform implements DynamicPlatformPlugin }); } }); - - const exampleDevices = [ - { - exampleUniqueId: 'ABCD', - exampleDisplayName: 'Bedroom', - }, - { - exampleUniqueId: 'EFGH', - exampleDisplayName: 'Kitchen', - }, - ]; - - // loop over the discovered devices and register each one if it has not already been registered - for (const device of exampleDevices) - { - - // generate a unique id for the accessory this should be generated from - // something globally unique, but constant, for example, the device serial - // number or MAC address - const uuid = this.api.hap.uuid.generate(device.exampleUniqueId); - - // see if an accessory with the same uuid has already been registered and restored from - // the cached devices we stored in the `configureAccessory` method above - const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); - - if (existingAccessory) - { - // the accessory already exists - this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); - - // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: - // existingAccessory.context.device = device; - // this.api.updatePlatformAccessories([existingAccessory]); - - // create the accessory handler for the restored accessory - // this is imported from `platformAccessory.ts` - new NeatoVacuumRobotAccessory(this, existingAccessory); - - // it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.: - // remove platform accessories when no longer present - // this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]); - // this.log.info('Removing existing accessory from cache:', existingAccessory.displayName); - } - else - { - // the accessory does not yet exist, so we need to create it - this.log.info('Adding new accessory:', device.exampleDisplayName); - - // create a new accessory - const accessory = new this.api.platformAccessory(device.exampleDisplayName, uuid); - - // store a copy of the device object in the `accessory.context` - // the `context` property can be used to store any data about the accessory you may need - accessory.context.device = device; - - // create the accessory handler for the newly create accessory - // this is imported from `platformAccessory.ts` - new NeatoVacuumRobotAccessory(this, accessory); - - // link the accessory to your platform - this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); - } - } } }