homebridge-rachio-irrigation
Version: 
Rachio Irrigation System platform plugin for [Homebridge](https://github.com/nfarina/homebridge).
862 lines (832 loc) • 39.5 kB
JavaScript
/*
Known issues
Time remaining for homebridge accessory, homekit and Rachio run a little out of sync.
Zone Cyclying message may be out of sequence
*/
let axios = require('axios')
let RachioAPI = require('./rachioapi')
let RachioUpdate = require('./rachioupdate')
let listener = require('./listener')
let irrigation = require('./devices/irrigation')
let switches = require('./devices/switches')
let valve = require('./devices/valve')
let battery = require('./devices/battery')
let bridge = require('./devices/bridge')
let deviceState
class RachioPlatform {
	constructor(log, config, api) {
		this.rachioapi = new RachioAPI(this, log)
		this.rachio = new RachioUpdate(this, log, config)
		this.listener = new listener(this, log, config)
		this.irrigation = new irrigation(this, log, config)
		this.switches = new switches(this, log)
		this.valve = new valve(this, log, config)
		this.battery = new battery(this, log)
		this.bridge = new bridge(this, log)
		this.log = log
		this.api = api
		this.config = config
		this.token = config.api_key
		this.retryWait = config.retryWait ? config.retryWait : 60 //sec
		this.retryMax = config.retryMax ? config.retryMax : 3 //attempts
		this.retryAttempt = 0
		this.auto_correct_IP = config.auto_correct_IP ? config.auto_correct_IP : false
		this.external_IP_address = config.external_IP_address
		this.external_webhook_port = config.external_webhook_port
		this.internal_IP_address = config.internal_IP_address
		this.internal_webhook_port = config.internal_webhook_port
		this.relay_address = config.relay_address
		this.webhook_key = 'homebridge-' + config.name
		this.webhook_key_local = 'local-webhook'
		this.localWebhook
		this.endTime = []
		this.delete_webhooks = config.delete_webhooks
		this.useBasicAuth = config.use_basic_auth
		this.user = config.user
		this.password = config.password
		this.useIrrigationDisplay = config.use_irrigation_display
		this.defaultRuntime = config.default_runtime * 60
		this.runtimeSource = config.runtime_source
		this.showStandby = config.show_standby
		this.showRunAll = config.show_runall
		this.showSchedules = config.show_schedules
		this.locationAddress = config.location_address
		this.accessories = []
		this.zoneList = []
		this.foundLocations
		this.useHttps = config.https ? config.https : false
		this.key = config.key
		this.cert = config.cert
		this.showAPIMessages = config.showAPIMessages ? config.showAPIMessages : false
		this.showWebhookMessages = config.showWebhookMessages ? config.showWebhookMessages : false
		this.showBridge = config.showBridge ? config.showBridge : false
		this.showControllers = config.showControllers ? config.showControllers : false
		this.showValves = config.showValves ? config.showValves : false
		this.valveType = config.valveType ? config.valveType : 0
		if (this.useBasicAuth && (!this.user || !this.password)) {
			this.log.warn(`HTTP Basic Athentication cannot be used for webhooks without a valid user and password.`)
		}
		if (!this.token) {
			this.log.error(`API KEY is required in order to communicate with the Rachio API, please see https://rachio.readme.io/docs/authentication for instructions.`)
		} else {
			this.log(`Starting Rachio Platform with homebridge API ${api.version}`)
		}
		//**
		//** Platforms should wait until the "didFinishLaunching" event has fired before registering any new accessories.
		//**
		if (this.api) {
			this.api.on(
				'didFinishLaunching',
				async function () {
					//Removed any unwanted devices
					await this.checkDisplay()
					if (this.showControllers || this.showValves) {
						//Get info to configure webhooks
						await this.getWebhookInfo()
						//Configure listerner for webhook messages
						await this.listener.configureListener()
					}
					if (this.showControllers) {
						//Get controllers
						this.log.info('Setting up Controller devices')
						let x = await this.getRachioDevices().catch(err => {
							this.log.error ('Failure launching plugin, controller')
						})
						setTimeout(() => {
							if (x) {
								this.log.success('Rachio Platform finished loading Smart Sprinkler Controller')
							} else {
								this.log.warn('No Smart Sprinkler Controllers found')
							}
						}, 1000)
					}
					if (this.showValves) {
						//Get valves
						this.log.info('Setting up Wifi hub devices')
						let x = await this.getRachioValves().catch(err => {
							this.log.error ('Failure launching plugin, hose timers')
						})
						setTimeout(() => {
							if (x) {
								this.log.success('Rachio Platform finished loading Smart Hose Timers')
							} else {
								this.log.warn('No Smart Hose Timers found')
							}
						}, 1000)
					}
				}.bind(this)
			)
		}
	}
	identify() {
		this.log('Identify the sprinkler!')
	}
	checkDisplay() {
		let accessories = Object.entries(this.accessories) //build array from accessories object
		accessories.forEach(accessory => {
			accessory = accessory[1]
			if (!this.showValves && accessory.getService(Service.AccessoryInformation).getCharacteristic(Characteristic.Model).value.includes('SHV')) {
				this.log.info('Removing Smart Hose Timer %s', accessory.displayName)
				this.log.debug('Removing Smart Hose Timer %s', accessory.UUID)
				this.api.unregisterPlatformAccessories(PluginName, PlatformName, [accessory])
				delete this.accessories[accessory.uuid]
			}
			if (!this.showBridge && accessory.getService(Service.AccessoryInformation).getCharacteristic(Characteristic.Model).value.includes('HUB')) {
				this.log.info('Removing Smart Hose Bridge %s', accessory.displayName)
				this.log.debug('Removing Smart Hose Bridge %s', accessory.UUID)
				this.api.unregisterPlatformAccessories(PluginName, PlatformName, [accessory])
				delete this.accessories[accessory.uuid]
			}
			if (!this.showControllers && accessory.getService(Service.AccessoryInformation).getCharacteristic(Characteristic.Model).value.includes('GENERATION')) {
				this.log.info('Removing Smart Sprinker Controller %s', accessory.displayName)
				this.log.debug('Removing Smart Sprinker Controller %s', accessory.UUID)
				this.api.unregisterPlatformAccessories(PluginName, PlatformName, [accessory])
				delete this.accessories[accessory.uuid]
			}
		})
		if (!this.showValves && !this.showControllers && !this.showBridge) {
			this.log.warn('Plugin is not configured to show any devices!')
		}
	}
	async getWebhookInfo() {
		let ipv4
		let ipv6
		let fqdn
		let ipv4format = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
		let ipv6format =
			/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/
		let fqdnformat = /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/
		if (this.relay_address) {
			this.useBasicAuth = false
			this.external_webhook_address = this.relay_address
			this.external_webhook_addressv2 = this.relay_address
		}
		//check external IP address
		if (this.external_IP_address) {
			ipv4 = this.checkIPaddress(this.external_IP_address, ipv4format)
			ipv6 = this.checkIPaddress(this.external_IP_address, ipv6format)
			fqdn = this.checkIPaddress(this.external_IP_address, fqdnformat)
		} else {
			this.log.warn(`No external IP or domain name configured, will not configure webhooks. Reference Readme for instructions.`)
		}
		if (this.relay_address) {
			this.external_IP_address = this.relay_address
		} else {
			if (ipv4) {
				axios({
					method: 'get',
					//url: 'https://api4.ipify.org?format=json',
					url: 'https://api.ipify.org?format=json',
					responseType: 'json'
				})
					.then(response => {
						let realExternalIP = response.data.ip
						if (ipv4 && this.external_IP_address && realExternalIP != this.external_IP_address) {
							this.log.warn(`Configured external IPv4 address of ${this.external_IP_address} does not match this server's detected external IP of ${realExternalIP} please check webhook config settings.`)
							if (this.auto_correct_IP) {
								this.log.warn(`The external IPv4 of this server's detected IP address of ${realExternalIP} will be used based on config, please update webhook config settings.`)
								this.external_IP_address = realExternalIP
							}
						}
						this.log.debug(`using IPv4 webhook external address ${this.external_IP_address}`)
					})
					.catch(err => {
						this.log.error('Failed to get current external IP', err.cause)
					})
				this.setWebhookURL()
			} else if (ipv6) {
				axios({
					method: 'get',
					//url: 'https://api6.ipify.org?format=json',
					url: 'https://api64.ipify.org?format=json',
					responseType: 'json'
				})
					.then(response => {
						let realExternalIP = response.data.ip
						if (ipv6 && this.external_IP_address && realExternalIP != this.external_IP_address) {
							this.log.warn(`Configured external IPv6 address of ${this.external_IP_address} does not match this server's detected external IP of ${realExternalIP} please check webhook config settings.`)
							if (this.auto_correct_IP) {
								this.log.warn(`The external IPv6 of this server's detected IP address of ${realExternalIP} will be used based on config, please update webhook config settings.`)
								this.external_IP_address = realExternalIP
							}
						}
						this.log.debug(`using IPv6 webhook external address ${this.external_IP_address}`)
					})
					.catch(err => {
						this.log.error('Failed to get current external IP', err.cause)
					})
				this.external_IP_address = '[' + this.external_IP_address + ']'
				this.setWebhookURL()
			} else if (fqdn) {
				this.log.debug(`using FQDN for webhook external destination ${this.external_IP_address}`)
				this.setWebhookURL()
			} else {
				this.log.warn(`Cannot validate webhook destination address, will not set Webhooks. Please check webhook config settings for proper format and does not include any prefx like http://`)
			}
		}
	}
	checkIPaddress(inputText, ipformat) {
		try {
			if (inputText.match(ipformat)) {
				return true
			} else {
				return false
			}
		} catch (err) {
			log.warn(`Error validating IP address ${err}`)
		}
	}
	setWebhookURL() {
		let destination = this.useHttps ? 'https://' : 'http://'
		let port = this.external_webhook_port ? ':' + this.external_webhook_port : ''
		if (this.useBasicAuth && this.user && this.password) {
			this.external_webhook_address = destination + this.user + ':' + this.password + '@' + this.external_IP_address + port
			this.external_webhook_addressv2 = destination + this.external_IP_address + port
		} else {
			this.external_webhook_address = destination + this.external_IP_address + port
			this.external_webhook_addressv2 = destination + this.external_IP_address + port
		}
		if (!this.external_webhook_address) {
			this.log.warn(`Cannot validate webhook destination address, will not set Webhooks. Please check webhook config settings for proper format and does not include any prefx like http://`)
		}
	}
	async getRachioDevices() {
		let completed = true
		try {
			// getting account info
			this.log.debug('Fetching build info for Smart Sprinkler Controllers...')
			this.log.info('Getting Person info...')
			let personId = await this.rachioapi.getPersonInfo(this.token).catch(err => {
				this.log.error('Failed to get info for build', err)
				throw err
			})
			this.log('Found Person ID %s', personId.id)
			this.log.info('Getting Person ID info...')
			let personInfo = await this.rachioapi.getPersonId(this.token, personId.id).catch(err => {
				this.log.error('Failed to get person info for build', err)
				throw err
			})
			this.log.info('Found Account for username %s', personInfo.username)
			this.log.info('Getting Location info...')
			if (personInfo.devices.length > 0) {
				personInfo.devices
					.forEach(async newDevice => {
						try{
							let device = await this.rachioapi.getDevice(this.token, newDevice.id).catch(err => {
								this.log.error('Failed to get location property', err)
								throw err
							})
							let location = await this.rachioapi.getPropertyEntity(this.token, 'location_id',device.device.locationId).catch(err => {
								this.log.error('Failed to get location property', err)
								throw err
							})
							this.log.info('Found Location: id = %s address = %s locality = %s', location.property.address.id, location.property.address.lineOne, location.property.address.locality)
							if (!this.locationAddress || location.property.address.lineOne == this.locationAddress) {
								this.log.info('Adding controller %s found at the configured location: %s', newDevice.name, location.property.address.lineOne)
							} else {
								this.log.info('Skipping controller %s at %s, not found at the configured location: %s', newDevice.name, location.property.address.lineOne, this.locationAddress)
								return
							}
							//adding devices that met filter criteria
							this.log.info('Found Controller %s status %s', newDevice.name, newDevice.status)
							let uuid = newDevice.id
							this.log.info('Getting device state info...')
							deviceState = await this.rachioapi.getDeviceState(this.token, newDevice.id).catch(err => {
								this.log.error('Failed to get device state', err)
								//throw new Error ('Test')
								throw err
							})
							if (!deviceState) {
								return
							}
							this.log('Retrieved device state %s for %s with a %s state, running', deviceState.state.state, newDevice.name, deviceState.state.desiredState, deviceState.state.firmwareVersion)
							if (this.external_webhook_address) {
								this.rachioapi.configureWebhooks(this.token, this.external_webhook_address, this.delete_webhooks, newDevice.id, newDevice.name, this.webhook_key, 'irrigation_controller_id')
								this.rachioapi.configureWebhooksv2(this.token, this.external_webhook_addressv2, this.delete_webhooks, newDevice.id, newDevice.name, this.webhook_key, 'irrigation_controller_id')
							}
							// Create and configure Irrigation Service
							this.log.debug('Creating and configuring new device')
							let irrigationAccessory = this.irrigation.createIrrigationAccessory(newDevice, deviceState, this.accessories[uuid])
							this.irrigation.configureIrrigationService(newDevice, irrigationAccessory.getService(Service.IrrigationSystem))
							// Register platform accessory
							if (!this.accessories[uuid]) {
								this.log.debug('Registering platform accessory')
								this.log.info('Adding new accessory %s', irrigationAccessory.displayName)
								this.accessories[uuid] = irrigationAccessory
								this.api.registerPlatformAccessories(PluginName, PlatformName, [irrigationAccessory])
							}
							// Create and configure Values services and link to Irrigation Service
							newDevice.zones = newDevice.zones.sort(function (a, b) {
								return a.zoneNumber - b.zoneNumber
							})
							newDevice.zones.forEach(zone => {
								if (!this.useIrrigationDisplay && !zone.enabled) {
									this.log.info('Skipping disabled zone %s', zone.name)
								} else {
									this.log.debug('adding zone %s', zone.name)
									this.zoneList.push({
										deviceId: newDevice.id,
										zone: zone.zoneNumber,
										zoneId: zone.id
									})
									let valveService = irrigationAccessory.getServiceById(Service.Valve, zone.id)
									if (valveService) {
										this.irrigation.updateValveService(newDevice, zone, valveService)
										this.irrigation.configureValveService(newDevice, valveService)
										this.api.updatePlatformAccessories([irrigationAccessory])
									} else {
										//add new
										valveService = this.irrigation.createValveService(zone)
										this.irrigation.updateValveService(newDevice, valveService)
										this.irrigation.configureValveService(newDevice, valveService)
										irrigationAccessory.addService(valveService)
										this.api.updatePlatformAccessories([irrigationAccessory])
										if (this.useIrrigationDisplay) {
											this.log.debug('Using irrigation system')
											irrigationAccessory.getService(Service.IrrigationSystem).addLinkedService(valveService)
											this.api.updatePlatformAccessories([irrigationAccessory])
										} else {
											this.log.debug('Using separate tiles')
										}
									}
								}
							})
							if (this.showSchedules) {
								newDevice.scheduleRules.forEach(schedule => {
									this.log.debug('adding schedules %s', schedule.name)
									let switchService = irrigationAccessory.getServiceById(Service.Switch, schedule.id)
									if (switchService) {
										//update
										switchService.setCharacteristic(Characteristic.On, false).setCharacteristic(Characteristic.Name, schedule.name).setCharacteristic(Characteristic.StatusFault, Characteristic.StatusFault.NO_FAULT)
										this.switches.configureSwitchService(newDevice, switchService)
										this.api.updatePlatformAccessories([irrigationAccessory])
									} else {
										//add new
										switchService = this.switches.createScheduleSwitchService(schedule)
										this.switches.configureSwitchService(newDevice, switchService)
										irrigationAccessory.addService(switchService)
										this.api.updatePlatformAccessories([irrigationAccessory])
									}
									irrigationAccessory.getService(Service.IrrigationSystem).addLinkedService(switchService)
								})
								newDevice.flexScheduleRules.forEach(schedule => {
									this.log.debug('adding flex schedules %s', schedule.name)
									let switchService = irrigationAccessory.getServiceById(Service.Switch, schedule.id)
									if (switchService) {
										//update
										switchService.setCharacteristic(Characteristic.On, false).setCharacteristic(Characteristic.Name, schedule.name).setCharacteristic(Characteristic.StatusFault, Characteristic.StatusFault.NO_FAULT)
										this.switches.configureSwitchService(newDevice, switchService)
										this.api.updatePlatformAccessories([irrigationAccessory])
									} else {
										//add new
										switchService = this.switches.createScheduleSwitchService(schedule)
										this.switches.configureSwitchService(newDevice, switchService)
										irrigationAccessory.addService(switchService)
										this.api.updatePlatformAccessories([irrigationAccessory])
									}
									irrigationAccessory.getService(Service.IrrigationSystem).addLinkedService(switchService)
								})
							} else {
								//remove
								newDevice.scheduleRules.forEach(schedule => {
									this.log.debug('removed schedule switch')
									let switchService = irrigationAccessory.getServiceById(Service.Switch, schedule.id)
									if (switchService) {
										irrigationAccessory.removeService(switchService)
										this.api.updatePlatformAccessories([irrigationAccessory])
									}
								})
								newDevice.flexScheduleRules.forEach(schedule => {
									this.log.debug('removed flex schedule switch')
									let switchService = irrigationAccessory.getServiceById(Service.Switch, schedule.id)
									if (switchService) {
										irrigationAccessory.removeService(switchService)
										this.api.updatePlatformAccessories([irrigationAccessory])
									}
								})
							}
							if (this.showStandby) {
								this.log.debug('adding new standby switch')
								let switchType = 'Standby'
								let switchName = newDevice.name + ' ' + switchType
								let uuid = UUIDGen.generate(switchName)
								let switchService = irrigationAccessory.getServiceById(Service.Switch, uuid)
								if (switchService) {
									//update
									switchService.setCharacteristic(Characteristic.On, false).setCharacteristic(Characteristic.Name, switchName).setCharacteristic(Characteristic.StatusFault, Characteristic.StatusFault.NO_FAULT)
									this.switches.configureSwitchService(newDevice, switchService)
									this.api.updatePlatformAccessories([irrigationAccessory])
								} else {
									//add new
									switchService = this.switches.createSwitchService(switchName, uuid)
									this.switches.configureSwitchService(newDevice, switchService)
									irrigationAccessory.addService(switchService)
									this.api.updatePlatformAccessories([irrigationAccessory])
								}
								irrigationAccessory.getService(Service.IrrigationSystem).addLinkedService(switchService)
								this.api.updatePlatformAccessories([irrigationAccessory])
							} else {
								//remove
								this.log.debug('removed standby switch')
								let switchType = 'Standby'
								let switchName = newDevice.name + ' ' + switchType
								let uuid = UUIDGen.generate(switchName)
								let switchService = irrigationAccessory.getServiceById(Service.Switch, uuid)
								if (switchService) {
									irrigationAccessory.removeService(switchService)
									this.api.updatePlatformAccessories([irrigationAccessory])
								}
							}
							if (this.showRunAll) {
								this.log.debug('adding new run all switch')
								let switchType = 'Quick Run All'
								let switchName = newDevice.name + ' ' + switchType
								let uuid = UUIDGen.generate(switchName)
								let switchService = irrigationAccessory.getServiceById(Service.Switch, uuid)
								if (switchService) {
									//update
									switchService.setCharacteristic(Characteristic.On, false).setCharacteristic(Characteristic.Name, switchName).setCharacteristic(Characteristic.StatusFault, Characteristic.StatusFault.NO_FAULT)
									this.switches.configureSwitchService(newDevice, switchService)
									this.api.updatePlatformAccessories([irrigationAccessory])
								} else {
									//add new
									switchService = this.switches.createSwitchService(switchName, uuid)
									this.switches.configureSwitchService(newDevice, switchService)
									irrigationAccessory.addService(switchService)
									this.api.updatePlatformAccessories([irrigationAccessory])
								}
								irrigationAccessory.getService(Service.IrrigationSystem).addLinkedService(switchService)
								this.api.updatePlatformAccessories([irrigationAccessory])
							} else {
								//remove
								let switchType = 'Quick Run All'
								this.log.debug('removed Quick Run All')
								let uuid = UUIDGen.generate(newDevice.name + ' ' + switchType)
								let switchService = irrigationAccessory.getServiceById(Service.Switch, uuid)
								if (switchService) {
									irrigationAccessory.removeService(switchService)
									this.api.updatePlatformAccessories([irrigationAccessory])
								}
							}
							//find any running zone and set its state
							let schedule = await this.rachioapi.currentSchedule(this.token, newDevice.id).catch(err => {
								this.log.error('Failed to get current schedule', err)
								throw err
							})
							this.log.debug('Check current schedule')
							//match state to Rachio state
							this.setOnlineStatus(newDevice)
							this.setDeviceStatus(newDevice)
							this.setValveStatus(schedule.data)
							//remove [UTC] for valid date regex= /\[...]/
							this.log.info(
								'API rate limiting; call limit of %s remaining out of %s until reset at %s',
								schedule.headers['x-ratelimit-remaining'],
								schedule.headers['x-ratelimit-limit'],
								new Date(schedule.headers['x-ratelimit-reset'].replace(/\[...]/, '')).toString()
							)
						} catch (err) {
							this.log.warn(err)
							completed = false
						}
					})
				return completed ///not working
			} else {
				return false
			}
		} catch (err) {
			if (this.retryAttempt < this.retryMax) {
				this.retryAttempt++
				this.log.warn(err)
				this.log.error('Failed to get devices. Retry attempt %s of %s in %s seconds...', this.retryAttempt, this.retryMax, this.retryWait)
				setTimeout(async () => {
					this.getRachioDevices()
				}, this.retryWait * 1000)
			} else {
				this.log.error('Failed to get devices...\n%s', err)
			}
		}
	}
	async getRachioValves() {
		try {
			// getting account info
			this.log.debug('Fetching build info for Smart Hose Timers...')
			this.log.info('Getting Person info...')
			let personId = await this.rachioapi.getPersonInfo(this.token).catch(err => {
				this.log.error('Failed to get info for build', err)
				throw err
			})
			this.log('Found Person ID %s', personId.id)
			this.log.info('Getting Person ID info...')
			let personInfo = await this.rachioapi.getPersonId(this.token, personId.id).catch(err => {
				this.log.error('Failed to get person info for build', err)
				throw err
			})
			this.log.info('Found Account for username %s', personInfo.username)
			this.log.info('Getting Location info...')
			let list = await this.rachioapi.listBaseStations(this.token, personId.id).catch(err => {
				this.log.error('Failed to get base station list', err)
				throw err
			})
			if (list.baseStations.length > 0) {
				list.baseStations
					.forEach(async baseStation => {
							let location = await this.rachioapi.getPropertyEntity(this.token, 'base_station_id', baseStation.id).catch(err => {
							this.log.error('Failed to get base station property', err)
							throw err
						})
						if (!this.locationAddress || location.property.address.lineOne == this.locationAddress) {
							this.log.info('Found WiFi Hub %s at the configured location: %s', baseStation.serialNumber, location.property.address.lineOne)
						} else {
							this.log.info('Skipping WiFi Hub %s st %s, not found at the configured location: %s', baseStation.serialNumber, location.property.address.lineOne, this.locationAddress)
							return
						}
						//pulling bridge location
						location = await this.rachioapi.getPropertyEntity(this.token, 'base_station_id', baseStation.id).catch(err => {
							this.log.error('Failed to get base station property', err)
							throw err
						})
						let uuid = baseStation.id
						if (baseStation.reportedState.firmwareUpgradeAvailable) {
							this.log.warn('Hub firmware upgrade available')
						}
						if (this.showBridge) {
							this.log.debug('Adding Hub Device')
							this.log.debug('Found WiFi Hub %s', location.property.address.locality)
							// Create and configure Bridge Service
							this.log.debug('Creating and configuring new Wifi Hub')
							let bridgeAccessory = this.bridge.createBridgeAccessory(baseStation, location, this.accessories[uuid])
							let bridgeService = bridgeAccessory.getService(Service.WiFiTransport)
							// set current device status
							if (!bridgeService) {
								bridgeService = this.bridge.createBridgeService(baseStation, location)
								bridgeAccessory.addService(bridgeService)
							}
							this.bridge.configureBridgeService(bridgeService)
							bridgeService.getCharacteristic(Characteristic.StatusFault).updateValue(baseStation.reportedState.connected)
							this.log.info('Adding WiFi Hub')
							if (!this.accessories[uuid]) {
								this.log.debug('Registering platform accessory')
								this.accessories[uuid] = bridgeAccessory
								this.api.registerPlatformAccessories(PluginName, PlatformName, [bridgeAccessory])
							}
						} else {
							this.log.info('Skipping WiFi Hub %s based on config for', this.locationAddress)
						}
						let valveList = await this.rachioapi.listValves(this.token, baseStation.id).catch(err => {
							this.log.error('Failed to get valve list', err)
							throw err
						})
						if (valveList.valves.length > 0) {
							valveList.valves.forEach(async (valve, index) => {
								//this.log.debug(JSON.stringify(valve, null, 2))//temp
								let uuid = valve.id
								valve.zone = index + 1
								this.log.debug('Creating and configuring new valve')
								if (this.accessories[uuid]) {
									// Check if accessory changed
									if (this.accessories[uuid].getService(Service.AccessoryInformation).getCharacteristic(Characteristic.ProductData).value != 'Valve') {
										this.log.warn('Changing from Irrigation to Valve, check room assignments in Homekit')
										this.api.unregisterPlatformAccessories(PluginName, PlatformName, [this.accessories[uuid]])
										delete this.accessories[uuid]
									}
								}
								//adding devices that met filter criteria
								this.log.info('Found Smart Hose Timer %s connected: %s', valve.name, valve.state.reportedState.connected)
								if (valve.state.reportedState.firmwareUpgradeAvailable) {
									this.log.warn('Valve %s firmware upgrade available', valve.name)
								}
								if (valve.state.reportedState.firmwareUpgradeInProgress) {
									this.log.warn('Valve %s firmware upgrade in progress %s', valve.name, valve.state.reportedState.firmwareVersion)
								}
								if (this.external_webhook_address) {
									this.rachioapi.configureWebhooksv2(this.token, this.external_webhook_addressv2, this.delete_webhooks, valve.id, valve.name, this.webhook_key, 'valve_id')
								}
								// Create and configure Irrigation Service
								this.log.debug('Creating and configuring new valve')
								let valveAccessory = this.valve.createValveAccessory(baseStation, location.property, valve, this.accessories[uuid])
								let valveService = valveAccessory.getService(Service.Valve)
								this.valve.updateValveService(baseStation, valve, valveService)
								this.valve.configureValveService(valve, valveAccessory.getService(Service.Valve))
								// Register platform accessory
								if (!this.accessories[uuid]) {
									this.log.debug('Registering platform accessory')
									this.log.info('Adding new accessory %s', valveAccessory.displayName)
									this.accessories[uuid] = valveAccessory
									this.api.registerPlatformAccessories(PluginName, PlatformName, [valveAccessory])
								}
								// Create and configure Battery Service
								if (valve.state.reportedState.batteryStatus != null) {
									this.log.info('Adding Battery status for %s', valve.name)
									let batteryStatus = valveAccessory.getService(Service.Battery)
									//batteryStatus.getCharacteristic(Characteristic.SerialNumber).updateValue(valve.id) // should be temp
									if (batteryStatus) {
										//update
										this.battery.configureBatteryService(batteryStatus)
										switch (valve.state.reportedState.batteryStatus) {
											case 'GOOD':
												batteryStatus.getCharacteristic(Characteristic.StatusLowBattery).updateValue(Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL)
												break
											case 'LOW':
												batteryStatus.getCharacteristic(Characteristic.StatusLowBattery).updateValue(Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW)
												break
											case 'REPLACE':
												batteryStatus.setCharacteristic(Characteristic.StatusLowBattery, Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW)
												break
										}
									} else {
										//add new
										batteryStatus = this.battery.createBatteryService(valve, uuid)
										this.battery.configureBatteryService(batteryStatus)
										valveAccessory.addService(batteryStatus)
										this.api.updatePlatformAccessories([valveAccessory])
									}
									batteryStatus = valveAccessory.getService(Service.Battery)
									valveAccessory.getService(Service.Valve).addLinkedService(batteryStatus)
								} else {
									//remove
									this.log.debug('%s has no battery found, skipping add battery service', valve.name)
									let batteryStatus = valveAccessory.getService(Service.Battery)
									if (batteryStatus) {
										valveAccessory.removeService(batteryStatus)
										this.api.updatePlatformAccessories([valveAccessory])
									}
								}
								//find any running zone and set its state
								let programs = await this.rachioapi.listPrograms(this.token, valve.id).catch(err => {
									this.log.error('Failed to get current programs', err)
									throw err
								})
								this.log.debug('Check current programs')
								//this.setValveStatus(programs.data) //future
								//remove [UTC] for valid date regex= /\[...]/
								this.log.info(
									'API rate limiting; call limit of %s remaining out of %s until reset at %s',
									programs.headers['x-ratelimit-remaining'],
									programs.headers['x-ratelimit-limit'],
									new Date(programs.headers['x-ratelimit-reset'].replace(/\[...]/, '')).toString()
								)
							})
						}
					})
				return true
			} else {
				return false
			}
		} catch (err) {
			if (this.retryAttempt < this.retryMax) {
				this.retryAttempt++
				this.log.error('Failed to get valves. Retry attempt %s of %s in %s seconds...', this.retryAttempt, this.retryMax, this.retryWait)
				setTimeout(async () => {
					this.getRachioValves()
				}, this.retryWait * 1000)
			} else {
				this.log.error('Failed to get devices...\n%s', err)
			}
		}
	}
	//**
	//** REQUIRED - Homebridge will call the 'configureAccessory' method once for every cached accessory restored
	//**
	configureAccessory(accessory) {
		// Add cached devices to the accessories array
		this.log.info('Found cached accessory, configuring %s', accessory.displayName)
		this.accessories[accessory.UUID] = accessory
	}
	setOnlineStatus(newDevice) {
		//set current device status
		//create a fake webhook response
		if (newDevice.status) {
			let myJson
			switch (newDevice.status) {
				case 'ONLINE':
					myJson = {
						externalId: this.webhook_key_local,
						type: 'DEVICE_STATUS',
						deviceId: newDevice.id,
						subType: 'ONLINE',
						timestamp: new Date().toISOString()
					}
					break
				case 'OFFLINE':
					myJson = {
						externalId: this.webhook_key_local,
						type: 'DEVICE_STATUS',
						deviceId: newDevice.id,
						subType: 'OFFLINE',
						timestamp: new Date().toISOString()
					}
					break
			}
			this.log.debug('Found %s device', newDevice.status.toLowerCase())
			if (this.showAPIMessages) {
				this.log.debug(myJson)
			}
			let irrigationAccessory = this.accessories[myJson.deviceId]
			let irrigationSystemService = irrigationAccessory.getService(Service.IrrigationSystem)
			let service = irrigationAccessory.getServiceById(Service.IrrigationSystem)
			this.log.debug('Updating device status')
			this.listener.eventMsg(irrigationSystemService, service, myJson)
		}
	}
	setDeviceStatus(newDevice) {
		//set current device state
		//create a fake webhook response
		if (deviceState.state.health == 'GOOD') {
			let myJson
			switch (deviceState.state.desiredState) {
				case 'DESIRED_ACTIVE':
					myJson = {
						summary: 'Scheduled waterings will now run on controller.',
						externalId: this.webhook_key_local,
						eventType: 'DEVICE_MANUAL_STANDBY_ON_EVENT',
						type: 'DEVICE_STATUS',
						title: 'Standby Mode Off',
						deviceId: newDevice.id,
						deviceName: newDevice.name,
						subType: 'SLEEP_MODE_OFF'
					}
					break
				case 'DESIRED_STANDBY':
					myJson = {
						summary: 'No scheduled waterings will run on controller.',
						externalId: this.webhook_key_local,
						eventType: 'DEVICE_MANUAL_STANDBY_ON_EVENT',
						type: 'DEVICE_STATUS',
						title: 'Standby Mode ON',
						deviceId: newDevice.id,
						deviceName: newDevice.name,
						subType: 'SLEEP_MODE_ON'
					}
					break
			}
			this.log.debug('Found healthy device')
			if (this.showAPIMessages) {
				this.log.debug(myJson)
			}
			let irrigationAccessory = this.accessories[myJson.deviceId]
			let irrigationSystemService = irrigationAccessory.getService(Service.IrrigationSystem)
			this.log.debug('Updating standby switch state')
			this.listener.eventMsg(irrigationSystemService, irrigationSystemService, myJson)
		}
	}
	setValveStatus(response) {
		if (response.status == 'PROCESSING') {
			//create a fake webhook response
			this.log.debug('Found zone-%s running', response.zoneNumber)
			let myJson = {
				type: 'ZONE_STATUS',
				title: 'Zone Started',
				deviceId: response.deviceId,
				duration: response.zoneDuration,
				zoneNumber: response.zoneNumber,
				zoneId: response.zoneId,
				zoneRunState: 'STARTED',
				durationInMinutes: Math.round(response.zoneDuration / 60),
				externalId: this.webhook_key_local,
				eventType: 'DEVICE_ZONE_RUN_STARTED_EVENT',
				subType: 'ZONE_STARTED',
				startTime: response.zoneStartDate,
				endTime: new Date(response.zoneStartDate + response.zoneDuration * 1000).toISOString(),
				category: 'DEVICE',
				resourceType: 'DEVICE'
			}
			if (this.showAPIMessages) {
				this.log.debug(myJson)
			}
			let irrigationAccessory = this.accessories[myJson.deviceId]
			let irrigationSystemService = irrigationAccessory.getService(Service.IrrigationSystem)
			let service = irrigationAccessory.getServiceById(Service.Valve, myJson.zoneId)
			this.log.debug('Zone running match found for zone-%s on start will update services', myJson.zoneNumber)
			this.listener.eventMsg(irrigationSystemService, service, myJson)
		}
		if (response.status == 'PROCESSING' && this.showSchedules && response.scheduleId != undefined) {
			this.log.debug('Found schedule %s running', response.scheduleId)
			let myJson = {
				type: 'SCHEDULE_STATUS',
				title: 'Schedule Manually Started',
				deviceId: response.deviceId,
				deviceName: response.name,
				duration: response.zoneDuration / 60,
				scheduleName: 'Quick Run',
				scheduleId: response.scheduleId,
				externalId: this.webhook_key_local,
				eventType: 'SCHEDULE_STARTED_EVENT',
				subType: 'SCHEDULE_STARTED',
				endTime: new Date(response.zoneStartDate + response.zoneDuration * 1000).toISOString(),
				category: 'SCHEDULE',
				resourceType: 'DEVICE'
			}
			if (this.showAPIMessages) {
				this.log.debug(myJson)
			}
			let irrigationAccessory = this.accessories[myJson.deviceId]
			let irrigationSystemService = irrigationAccessory.getService(Service.IrrigationSystem)
			let service = irrigationAccessory.getServiceById(Service.Switch, myJson.scheduleId)
			this.log.debug('Schedule running match found for schedule %s on start will update services', myJson.scheduleName)
			this.listener.eventMsg(irrigationSystemService, service, myJson)
		}
	}
}
module.exports = RachioPlatform