Skip to content

Commit

Permalink
Added option to use iRobot credentials to get devices info.
Browse files Browse the repository at this point in the history
  • Loading branch information
donavanbecker committed Jan 25, 2025
1 parent 3f8d2f6 commit 47b3e20
Show file tree
Hide file tree
Showing 8 changed files with 472 additions and 91 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [2.1.0](https://github.com/homebridge-plugins/homebridge-roomba/releases/tag/v2.1.0) (2025-01-XX)

### What's Changes
- Added option to use iRobot credentials to get devices info.
- Housekeeping and updated dependencies.

**Full Changelog**: https://github.com/homebridge-plugins/homebridge-roomba/compare/v2.0.0...v2.1.0

## [2.0.0](https://github.com/homebridge-plugins/homebridge-roomba/releases/tag/v2.0.0) (2025-01-25)

### What's Changes
Expand Down
108 changes: 69 additions & 39 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@
"default": "NoIP",
"required": true
},
"email": {
"type": "string",
"title": "iRobot Account Email",
"description": "The email address you use to log into the iRobot Home app.",
"required": true
},
"Password": {
"type": "string",
"title": "iRobot Account Password",
"description": "The password you use to log into the iRobot Home app.",
"required": true
},
"devices": {
"type": "array",
"items": {
Expand Down Expand Up @@ -109,7 +121,6 @@
"type": "number",
"title": "Clean rooms in order",
"default": 1,
"required": true,
"oneOf": [
{
"title": "Yes",
Expand All @@ -131,7 +142,6 @@
"pmap_id": {
"type": "string",
"title": "Pmap Id",
"required": true,
"condition": {
"functionBody": "return (model.devices && model.devices[arrayIndices].cleanBehaviour === 'rooms');"
}
Expand All @@ -144,14 +154,12 @@
"properties": {
"region_id": {
"type": "string",
"title": "Region Id",
"required": true
"title": "Region Id"
},
"type": {
"type": "string",
"title": "Type",
"default": "rid",
"required": true
"default": "rid"
},
"params": {
"type": "object",
Expand Down Expand Up @@ -224,47 +232,69 @@
},
"layout": [
{
"key": "devices",
"notitle": false,
"type": "tabarray",
"title": "{{ value.name || value.ipaddress || value.serialnum || 'New Vacuum' }}",
"type": "fieldset",
"title": "iRobot Account",
"expandable": true,
"expanded": false,
"items": [
"email",
"Password"
]
},
{
"type": "fieldset",
"title": "Roomba Device Settings",
"expandable": true,
"expanded": false,
"orderable": false,
"items": [
"devices[].name",
"devices[].model",
"devices[].serialnum",
"devices[].blid",
"devices[].robotpwd",
"devices[].ipaddress",
"devices[].homeSwitch",
"devices[].dockContactSensor",
"devices[].dockingContactSensor",
"devices[].runningContactSensor",
"devices[].binContactSensor",
"devices[].tankContactSensor",
"devices[].idleWatchInterval",
"devices[].cleanBehaviour",
{
"key": "devices[].mission",
"type": "fieldset",
"title": "Mission Settings",
"type": "help",
"helpvalue": "<em class='primary-text'>With Roomba Device Setting, you can set device specific settings based on <b style='color: var(--secondary-color);'>blid</b>.</em>"
},
{
"key": "devices",
"notitle": false,
"type": "tabarray",
"title": "{{ value.name || value.ipaddress || value.serialnum || 'New Vacuum' }}",
"expandable": true,
"expanded": false,
"orderable": false,
"items": [
"devices[].mission.pmap_id",
"devices[].mission.user_pmapv_id",
"devices[].mission.regions",
"devices[].mission.regions[].region_id",
"devices[].mission.regions[].type",
"devices[].mission.regions[].params.noAutoPasses",
"devices[].mission.regions[].params.twoPass",
"devices[].mission.ordered"
"devices[].name",
"devices[].model",
"devices[].serialnum",
"devices[].blid",
"devices[].robotpwd",
"devices[].ipaddress",
"devices[].homeSwitch",
"devices[].dockContactSensor",
"devices[].dockingContactSensor",
"devices[].runningContactSensor",
"devices[].binContactSensor",
"devices[].tankContactSensor",
"devices[].idleWatchInterval",
"devices[].cleanBehaviour",
{
"key": "devices[].mission",
"type": "fieldset",
"title": "Mission Settings",
"expandable": true,
"expanded": false,
"items": [
"devices[].mission.pmap_id",
"devices[].mission.user_pmapv_id",
"devices[].mission.regions",
"devices[].mission.regions[].region_id",
"devices[].mission.regions[].type",
"devices[].mission.regions[].params.noAutoPasses",
"devices[].mission.regions[].params.twoPass",
"devices[].mission.ordered"
]
},
"devices[].stopBehaviour",
"devices[].idleWatchInterval"
]
},
"devices[].stopBehaviour",
"devices[].idleWatchInterval"
}
]
},
{
Expand Down
14 changes: 12 additions & 2 deletions src/accessory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { RobotMission, RobotState, Roomba } from 'dorita980'
import type { AccessoryPlugin, API, CharacteristicGetCallback, CharacteristicSetCallback, CharacteristicValue, Logging, PlatformAccessory, Service, WithUUID } from 'homebridge'

import type { Robot } from './roomba/getRoombas.js'

import type RoombaPlatform from './platform.js'
import type { DeviceConfig, RoombaPlatformConfig } from './types.js'

Expand Down Expand Up @@ -145,14 +147,22 @@ export default class RoombaAccessory implements AccessoryPlugin {
readonly platform: RoombaPlatform,
accessory: PlatformAccessory,
log: Logging,
device: DeviceConfig,
device: Robot & DeviceConfig,
config: RoombaPlatformConfig,
api: API,
) {
this.api = api
this.debug = !!config.debug

this.log = !this.debug ? log : Object.assign(log, { debug: (message: string, ...parameters: unknown[]) => { log.info(`DEBUG: ${message}`, ...parameters) } })
if (!this.debug) {
this.log = log
} else {
this.log = Object.assign(log, {
debug: (message: string, ...parameters: unknown[]) => {
log.info(`DEBUG: ${message}`, ...parameters)
},
})
}
this.name = device.name
this.model = device.model
this.serialnum = device.serialnum ?? device.ipaddress
Expand Down
92 changes: 43 additions & 49 deletions src/platform.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { API, Characteristic, DynamicPlatformPlugin, Logging, PlatformAccessory, PlatformConfig, Service } from 'homebridge'
import type { API, Characteristic, DynamicPlatformPlugin, Logging, PlatformAccessory, Service } from 'homebridge'

import type { DeviceConfig, RoombaPlatformConfig } from './types.js'

import { readFileSync } from 'node:fs'

import RoombaAccessory from './accessory.js'
import { getRoombas, Robot } from './roomba/getRoombas.js'
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'

export default class RoombaPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service
public readonly Characteristic: typeof Characteristic

private api: API
private log: Logging
private config: RoombaPlatformConfig
Expand All @@ -20,7 +20,6 @@ export default class RoombaPlatform implements DynamicPlatformPlugin {
public constructor(log: Logging, config: RoombaPlatformConfig, api: API) {
this.Service = api.hap.Service
this.Characteristic = api.hap.Characteristic

this.api = api
this.config = config
const debug = !!config.debug
Expand All @@ -38,60 +37,67 @@ export default class RoombaPlatform implements DynamicPlatformPlugin {

public configureAccessory(accessory: PlatformAccessory): void {
this.log(`Configuring accessory: ${accessory.displayName}`)

this.accessories.set(accessory.UUID, accessory)
}

private discoverDevices(): void {
const devices: DeviceConfig[] = this.getDevicesFromConfig()
private async discoveryMethod(): Promise<DeviceConfig[]> {
if (this.config.email && this.config.password) {
const robots: Robot[] = await getRoombas(this.config.email, this.config.password, this.log)
return robots.map(robot => {
const deviceConfig = this.config.devices?.find(device => device.blid === robot.blid) || {}
return {
...robot,
...deviceConfig,
cleanBehaviour: this.config.cleanBehaviour,
stopBehaviour: this.config.stopBehaviour,
idleWatchInterval: this.config.idleWatchInterval
}
})
} else if (this.config.devices) {
return this.config.devices.map(device => ({
...device,
cleanBehaviour: this.config.cleanBehaviour,
stopBehaviour: this.config.stopBehaviour,
idleWatchInterval: this.config.idleWatchInterval
}))
} else {
this.log.error('No configuration provided for devices.')
return []
}
}

private async discoverDevices(): Promise<void> {
const devices: Robot[] = await this.discoveryMethod()
const configuredAccessoryUUIDs = new Set<string>()

for (const device of devices) {
const uuid = this.api.hap.uuid.generate(device.blid)

const existingAccessory = this.accessories.get(uuid)

if (existingAccessory) {
// the accessory already exists
this.log.debug('Restoring existing accessory from cache:', existingAccessory.displayName)

// TODO when should we update the device config

// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. e.g.:
existingAccessory.context.device = device
// this.api.updatePlatformAccessories([existingAccessory]);

// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
new RoombaAccessory(this, existingAccessory, this.log, device, this.config, this.api)

// it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, e.g.:
// 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);
new RoombaAccessory(this, existingAccessory, this.log, {
...device,
cleanBehaviour: this.config.cleanBehaviour,
stopBehaviour: this.config.stopBehaviour,
idleWatchInterval: this.config.idleWatchInterval
}, this.config, this.api)
} else {
// the accessory does not yet exist, so we need to create it
this.log.info('Adding new accessory:', device.name)

// create a new accessory
const accessory = new this.api.platformAccessory(device.name, 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 RoombaAccessory(this, accessory, this.log, device, this.config, this.api)

// link the accessory to your platform
new RoombaAccessory(this, accessory, this.log, {
...device,
cleanBehaviour: this.config.cleanBehaviour,
stopBehaviour: this.config.stopBehaviour,
idleWatchInterval: this.config.idleWatchInterval
}, this.config, this.api)
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
}
configuredAccessoryUUIDs.add(uuid)
}

// you can also deal with accessories from the cache which are no longer present by removing them from Homebridge
// for example, if your plugin logs into a cloud account to retrieve a device list, and a user has previously removed a device
// from this cloud account, then this device will no longer be present in the device list but will still be in the Homebridge cache
const accessoriesToRemove: PlatformAccessory[] = []
for (const [uuid, accessory] of this.accessories) {
if (!configuredAccessoryUUIDs.has(uuid)) {
Expand All @@ -105,18 +111,6 @@ export default class RoombaPlatform implements DynamicPlatformPlugin {
}
}

private getDevicesFromConfig(): DeviceConfig[] {
return this.config.devices || []
}

/**
* Retrieves the version of the plugin from the package.json file.
*
* This method reads the package.json file located in the parent directory,
* parses its content to extract the version, and logs the version using the debug logger.
*
* @returns {string} The version.
*/
private getVersion(): string {
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
this.log.debug(`Plugin Version: ${version}`)
Expand Down
Loading

0 comments on commit 47b3e20

Please sign in to comment.