UNPKG

homebridge-rachio-irrigation

Version:

Rachio Irrigation System platform plugin for [Homebridge](https://github.com/nfarina/homebridge).

862 lines (832 loc) 39.5 kB
/* Known issues Time remaining for homebridge accessory, homekit and Rachio run a little out of sync. Zone Cyclying message may be out of sequence */ 'use strict' 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