UNPKG

homebridge-rachio-irrigation

Version:

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

813 lines (812 loc) 65.4 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'; import axios from 'axios'; import RachioAPI from './rachioapi.js'; import RachioUpdate from './rachioupdate.js'; import listen from './listener.js'; import irrigation from './devices/irrigation.js'; import switches from './devices/switches.js'; import valve from './devices/valve.js'; import skipSwitch from './devices/skipSwitch.js'; import battery from './devices/battery.js'; import bridge from './devices/bridge.js'; let deviceState; export default class RachioPlatform { log; config; api; Service; Characteristic; HAPStatus; HapStatusError; accessories = []; valveServices = []; constructor(log, config, api) { this.log = log; this.config = config; this.api = api; this.Service = api.hap.Service; this.Characteristic = api.hap.Characteristic; this.HapStatusError = api.hap.HapStatusError; this.platform = this; this.genUUID = api.hap.uuid.generate; this.log.debug(`Finished initializing platform: ${config.name}`); this.rachioapi = new RachioAPI(this); this.rachio = new RachioUpdate(this); this.listener = new listen(this); this.switches = new switches(this); this.irrigation = new irrigation(this); this.valve = new valve(this); this.skipSwitch = new skipSwitch(this); this.battery = new battery(this); this.bridge = new bridge(this); 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 = null; 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.valveServices = []; this.zoneList = []; this.valveList = []; this.foundLocations = null; 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.showSkip = config.showPrograms ? config.showPrograms : false; this.showControllers = config.showControllers ? config.showControllers : false; this.showValves = config.showValves ? config.showValves : false; this.valveType = config.valveType ? config.valveType : 0; if (this.showSkip && !this.showBridge) { this.showBridge = true; this.log.warn('Expose WiFi Bridge must be set to true in plugin in config when exposing programs'); } 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.showValves && !this.showControllers && !this.showBridge) { this.log.warn('Plugin is not configured to show any devices!'); } 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.info(`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 () => { let x; let webhook; if (this.showControllers || this.showValves) { //Get info to configure webhooks await this.getWebhookInfo(); //Configure listerner for webhook messages await this.listener.configureListener(); } //Get controllers x = await this.getRachioDevices().catch((err) => { this.log.error('Failure setting up Controller'); this.log.debug(err); }); if (this.showControllers) { this.log.info('Setting up Controller devices'); setTimeout(async () => { try { if (x) { this.log.success('Rachio Platform finished loading Smart Sprinkler Controller'); webhook = await this.rachioapi.listControllerWebhooks(this.token, this.zoneList[0].deviceId); } else { this.log.warn('No Smart Sprinkler Controllers found'); } } catch (err) { this.log.debug(err); this.log.error(err.message); } }, 1000); } //Get valves x = await this.getRachioValves().catch((err) => { this.log.error('Failure setting up hose timers'); this.log.debug(err); }); if (this.showValves) { this.log.info('Setting up Smart Hose Timers'); setTimeout(async () => { try { if (x) { this.log.success('Rachio Platform finished loading Smart Hose Timers'); webhook = await this.rachioapi.listValveWebhooks(this.token, this.valveList[0].valveId); } else { this.log.warn('No Smart Hose Timers found'); } } catch (err) { this.log.debug(err); this.log.error(err.message); } }, 1000); } //Get bridge x = await this.getRachioBridges().catch((err) => { this.log.error('Failure setting up WiFi Hub'); this.log.debug(err); }); if (this.showBridge) { this.log.info('Setting up Wifi hub'); setTimeout(() => { if (x) { this.log.success('Rachio Platform finished loading WiFi Hub'); } else { this.log.warn('No Wifi Hub found'); } }, 1000); } setTimeout(() => { if (webhook) { const remaining = webhook.headers.get('x-ratelimit-remaining'); const limit = webhook.headers.get('x-ratelimit-limit'); const reset = new Date(webhook.headers.get('x-ratelimit-reset').replace(/\[...]/, '')).toString(); //remove [UTC] for valid date regex= /\[...]/ if (remaining / limit < 0.50) { this.log.warn(`API rate limiting; call limit of ${remaining} remaining out of ${limit} until reset at ${reset}`); } else { this.log.info(`API rate limiting; call limit of ${remaining} remaining out of ${limit} until reset at ${reset}`); } webhook.data.webhooks.forEach((webhook) => { if (webhook.externalId == this.webhook_key) { this.log.info(`To test Webhook setup: Navigate to ${webhook.url}/test to ensure port forwarding is configured correctly.\n` + ' Note: For local config this will not work from this server, you cannot be connected to the same router doing the fowarding. The best way to test this is from a cell phone, with WiFi off.'); } }); } ; }, 1000); }); } } //** //** 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 ${accessory.displayName}`); this.accessories.push(accessory); } identify() { this.log.info('Identify the sprinkler!'); } async getWebhookInfo() { let ipv4; let ipv6; let fqdn; const 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]?)$/; // eslint-disable-next-line max-len const 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]))/; const fqdnformat = /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)/; if (this.relay_address) { if (this.useBasicAuth && this.user && this.password) { const destination = (this.relay_address.split('//')); this.external_webhook_address = `${destination[0]}//${this.user}:${this.password}@${destination[1]}`; } else { 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 if (!this.relay_address) { 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 => { const 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 => { const 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) { this.log.warn(`Error validating IP address ${err}`); } } setWebhookURL() { const destination = this.useHttps ? 'https://' : 'http://'; const 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() { try { // getting account info this.log.debug('Fetching build info for Smart Sprinkler Controllers...'); this.log.debug('Getting Person info...'); const personId = await this.rachioapi.getPersonInfo(this.token).catch((err) => { this.log.error(`Failed to get info for build ${err}`); throw err; }); this.log.info(`Found Person ID ${personId.id}`); this.log.debug('Getting Person ID info...'); const 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 ${personInfo.username}`); if (personInfo.devices.length > 0) { personInfo.devices.forEach(async (newDevice) => { try { this.log.debug('Getting Device info...'); const device = await this.rachioapi.getDevice(this.token, newDevice.id).catch((err) => { this.log.error(`Failed to get device info ${err}`); throw err; }); this.log.debug('Getting Location info...'); const propertyList = await this.rachioapi.getListProperties(this.token, personInfo.id).catch((err) => { this.log.error(`Failed to get property list ${err}`); throw err; }); propertyList.property.forEach((property) => { this.log.info(`Found Location: ${property.name} id ${property.address.id}, at address ${property.address.lineOne}, in locality ${property.address.locality}`); property.entities.forEach((entity) => { this.log.info(`Found entity: ${entity.type}`); if (this.showControllers) { if (!this.locationAddress || (property.address.lineOne == this.locationAddress && entity.id == device.device.locationId)) { this.log.info(`Adding controller ${newDevice.name} found at the configured location: ${property.address.lineOne}`); } else { this.log.info(`Skipping controller ${newDevice.name} at ${property.address.lineOne}, not found at the configured location: ${this.locationAddress}`); return false; } } }); }); const index = this.accessories.findIndex(accessory => accessory.UUID === newDevice.id); // check if still required if (!this.showControllers) { if (index >= 0) { const irrigationAccessory = this.accessories[index]; this.log.info(`Removing Smart Sprinker Controller ${irrigationAccessory.displayName}`); this.log.debug(`Removing Smart Sprinker Controller ${irrigationAccessory.UUID}`); this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [irrigationAccessory]); this.accessories.splice(index, 1); } return false; } //adding devices that met filter criteria this.log.info(`Found ${newDevice.status.toLowerCase()} Controller ${newDevice.name}`); this.log.debug('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 err; }); if (!deviceState) { return; } this.log.info(`Retrieved device state ${deviceState.state.state.toLowerCase()} for ${newDevice.name} with a ${deviceState.state.desiredState.toLowerCase()} state, running firmware ${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'); //v1 still used for device status 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 this.log.debug(`Found Controller ${newDevice.name}`); this.log.debug('Creating and configuring new device'); const irrigationAccessory = this.irrigation.createIrrigationAccessory(newDevice, deviceState, this.accessories[index]); // Register platform accessory if (!this.accessories[index]) { this.log.debug('Registering platform accessory'); this.log.info(`Adding new accessory ${irrigationAccessory.displayName}`); this.accessories.push(irrigationAccessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [irrigationAccessory]); } else { this.log.debug('Accessory exists, refreshing'); } // Create and configure Value service and link to Irrigation newDevice.zones = newDevice.zones.sort((a, b) => { return a.zoneNumber - b.zoneNumber; }); newDevice.zones.forEach(zone => { if (!this.useIrrigationDisplay && !zone.enabled) { this.log.info(`Skipping disabled zone ${zone.name}`); } else { this.log.debug(`adding zone ${zone.name}`); this.zoneList.push({ deviceId: newDevice.id, zone: zone.zoneNumber, zoneId: zone.id, }); const valveService = irrigationAccessory.getServiceById(this.Service.Valve, zone.id); if (!valveService) { //add new const valveService = this.irrigation.createValveService(newDevice, zone); irrigationAccessory.addService(valveService); this.api.updatePlatformAccessories([irrigationAccessory]); if (this.useIrrigationDisplay) { this.log.debug('using irrigation system'); irrigationAccessory.getService(this.Service.IrrigationSystem).addLinkedService(valveService); this.api.updatePlatformAccessories([irrigationAccessory]); } else { this.log.debug('using separate tiles'); } } else { this.irrigation.updateValveService(newDevice, zone, valveService); this.irrigation.configureValveService(newDevice, valveService); this.api.updatePlatformAccessories([irrigationAccessory]); } } }); if (this.showSchedules) { newDevice.scheduleRules.forEach((schedule) => { this.log.debug(`adding schedules ${schedule.name}`); let switchService = irrigationAccessory.getServiceById(this.Service.Switch, schedule.id); if (switchService) { //update switchService.setCharacteristic(this.Characteristic.On, false).setCharacteristic(this.Characteristic.Name, schedule.name).setCharacteristic(this.Characteristic.StatusFault, this.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(this.Service.IrrigationSystem).addLinkedService(switchService); }); newDevice.flexScheduleRules.forEach((schedule) => { this.log.debug(`adding flex schedules ${schedule.name}`); let switchService = irrigationAccessory.getServiceById(this.Service.Switch, schedule.id); if (switchService) { //update switchService.setCharacteristic(this.Characteristic.On, false).setCharacteristic(this.Characteristic.Name, schedule.name).setCharacteristic(this.Characteristic.StatusFault, this.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(this.Service.IrrigationSystem).addLinkedService(switchService); }); } else { //remove newDevice.scheduleRules.forEach((schedule) => { this.log.debug('removed schedule switch'); const switchService = irrigationAccessory.getServiceById(this.Service.Switch, schedule.id); if (switchService) { irrigationAccessory.removeService(switchService); this.api.updatePlatformAccessories([irrigationAccessory]); } }); newDevice.flexScheduleRules.forEach((schedule) => { this.log.debug('removed flex schedule switch'); const switchService = irrigationAccessory.getServiceById(this.Service.Switch, schedule.id); if (switchService) { irrigationAccessory.removeService(switchService); this.api.updatePlatformAccessories([irrigationAccessory]); } }); } if (this.showStandby) { this.log.debug('adding new standby switch'); const switchType = 'Standby'; const switchName = `${newDevice.name} ${switchType}`; const uuid = this.genUUID(switchName); let switchService = irrigationAccessory.getServiceById(this.Service.Switch, uuid); if (switchService) { //update switchService.setCharacteristic(this.Characteristic.On, false).setCharacteristic(this.Characteristic.Name, switchName).setCharacteristic(this.Characteristic.StatusFault, this.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(this.Service.IrrigationSystem).addLinkedService(switchService); this.api.updatePlatformAccessories([irrigationAccessory]); } else { //remove this.log.debug('removed standby switch'); const switchType = 'Standby'; const switchName = `${newDevice.name} ${switchType}`; const uuid = this.genUUID(switchName); const switchService = irrigationAccessory.getServiceById(this.Service.Switch, uuid); if (switchService) { irrigationAccessory.removeService(switchService); this.api.updatePlatformAccessories([irrigationAccessory]); } } if (this.showRunAll) { this.log.debug('adding new run all switch'); const switchType = 'Quick Run All'; const switchName = `${newDevice.name} ${switchType}`; const uuid = this.genUUID(switchName); let switchService = irrigationAccessory.getServiceById(this.Service.Switch, uuid); if (switchService) { //update switchService.setCharacteristic(this.Characteristic.On, false).setCharacteristic(this.Characteristic.Name, switchName).setCharacteristic(this.Characteristic.StatusFault, this.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(this.Service.IrrigationSystem).addLinkedService(switchService); this.api.updatePlatformAccessories([irrigationAccessory]); } else { //remove const switchType = 'Quick Run All'; this.log.debug('removed Quick Run All'); const uuid = this.genUUID(`${newDevice.name} ${switchType}`); const switchService = irrigationAccessory.getServiceById(this.Service.Switch, uuid); if (switchService) { irrigationAccessory.removeService(switchService); this.api.updatePlatformAccessories([irrigationAccessory]); } } //find any running zone and set its state this.log.debug('Getting Schedule info...'); const 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.setValveStatus(schedule.data); if (this.showStandby) { this.setDeviceStatus(newDevice); } } catch (err) { this.log.warn(`Error ${err}`); } }); return true; } else { return false; } } catch (err) { if (this.retryAttempt < this.retryMax) { this.retryAttempt++; this.log.warn(`Error ${err}`); this.log.error(`Failed to get devices. Retry attempt ${this.retryAttempt} of ${this.retryMax} in ${this.retryWait} seconds`); setTimeout(async () => { this.getRachioDevices(); }, this.retryWait * 1000); } else { this.log.error(`Failed to get devices\n${err}`); } } } async getRachioValves() { try { // getting account info this.log.debug('Fetching build info for Smart Hose Timers...'); this.log.debug('Getting Person info...'); const personId = await this.rachioapi.getPersonInfo(this.token).catch((err) => { this.log.error(`Failed to get info for build ${err}`); throw err; }); this.log.info(`Found Person ID ${personId.id}`); this.log.debug('Getting Person ID info...'); const 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 ${personInfo.username}`); this.log.debug('Getting Base Station info...'); const 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) => { this.log.debug('Getting Property info...'); const property = 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.showValves) { if (!this.locationAddress || property.property.address.lineOne == this.locationAddress) { this.log.info(`Found Base Station: ${property.property.name} id ${property.property.id}, at address ${property.property.address.lineOne}, in locality ${property.property.address.locality}`); } else { this.log.info(`Skipping WiFi Hub ${baseStation.serialNumber} for Smart Hose Timers at ${property.property.address.lineOne}, not found at the configured location: ${this.locationAddress}`); return false; } } this.log.debug('Getting Valve list info...'); const 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, zoneNumber) => { try { valve.zone = zoneNumber + 1; this.valveList.push({ valveId: valve.id, name: valve.name, zone: valve.zone, }); const index = this.accessories.findIndex(accessory => accessory.UUID === valve.id); // check if still required if (!this.showValves) { if (index >= 0) { const valveAccessory = this.accessories[index]; this.log.info(`Removing Smart Hose Timer ${valveAccessory.displayName}`); this.log.debug(`Removing Smart Hose Timer ${valveAccessory.UUID}`); this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [valveAccessory]); this.accessories.splice(index, 1); } return false; } // Check if accessory changed if (index >= 0) { if (this.accessories[index].getService(this.Service.AccessoryInformation).getCharacteristic(this.Characteristic.ProductData).value != 'Valve') { this.log.warn('Changing from Irrigation to Valve, check room assignments in Homekit'); this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [this.accessories[index]]); this.accessories.splice(index, 1); } } //adding devices that met filter criteria this.log.info(`Found Smart Hose Timer ${valve.name} connected: ${valve.state.reportedState.connected}`); if (valve.state.reportedState.firmwareUpgradeAvailable) { this.log.warn(`Valve ${valve.name} firmware upgrade available`); } if (valve.state.reportedState.firmwareUpgradeInProgress) { this.log.warn(`Valve ${valve.name} firmware upgrade in progress ${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 Valve this.log.debug('Creating and configuring new valve'); const valveAccessory = this.valve.createValveAccessory(baseStation, property, valve, this.accessories[index]); // Register platform accessory if (!this.accessories[index]) { this.log.debug('Registering platform accessory'); this.log.info(`Adding new accessory ${valveAccessory.displayName}`); this.accessories.push(valveAccessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [valveAccessory]); } else { this.log.debug('Accessory exists, refreshing'); } // Create and configure Battery if (valve.state.reportedState.batteryStatus != null) { this.log.info(`Adding Battery status for ${valve.name}`); let batteryStatus = valveAccessory.getService(this.Service.Battery); //batteryStatus.getCharacteristic(this.Characteristic.SerialNumber).updateValue(valve.id) // should be temp if (batteryStatus) { //update this.battery.configureBatteryService(batteryStatus); switch (valve.state.reportedState.batteryStatus) { case 'GOOD': batteryStatus .setCharacteristic(this.Characteristic.StatusLowBattery, this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL) .setCharacteristic(this.Characteristic.ChargingState, this.Characteristic.ChargingState.NOT_CHARGEABLE) .setCharacteristic(this.Characteristic.BatteryLevel, 100); break; case 'LOW': batteryStatus .setCharacteristic(this.Characteristic.StatusLowBattery, this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW) .setCharacteristic(this.Characteristic.ChargingState, this.Characteristic.ChargingState.NOT_CHARGEABLE) .setCharacteristic(this.Characteristic.BatteryLevel, 40); break; case 'REPLACE': batteryStatus .setCharacteristic(this.Characteristic.StatusLowBattery, this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW) .setCharacteristic(this.Characteristic.ChargingState, this.Characteristic.ChargingState.NOT_CHARGEABLE) .setCharacteristic(this.Characteristic.BatteryLevel, 10); this.log.warn(`Replace batteries for ${valve.name} soon`); break; } } else { //add new batteryStatus = this.battery.createBatteryService(valve, valve.id); this.battery.configureBatteryService(batteryStatus); valveAccessory.addService(batteryStatus); this.api.updatePlatformAccessories([valveAccessory]); } batteryStatus = valveAccessory.getService(this.Service.Battery); valveAccessory.getService(this.Service.Valve).addLinkedService(batteryStatus); } else { //remove this.log.debug(`${valve.name} has no battery found, skipping add battery service`); const batteryStatus = valveAccessory.getService(this.Service.Battery); if (batteryStatus) { valveAccessory.removeService(batteryStatus); this.api.updatePlatformAccessories([valveAccessory]); } } //find any running zone and set its state /* future effort this.log.debug('Finding any running zones...'); const programs = await this.rachioapi.listPrograms(this.token, valve.id).catch((err: unknown) => { this.log.error('Failed to get current programs', err); throw err; }); this.log.debug('Check current programs'); //this.setValveStatus(programs.data) */ } catch (err) { this.log.warn(`Error ${err}`); } }); } else { this.log.warn(`No Smart Hose Timers found on hub ${baseStation.name}`); return false; } }); return true; } else { return false; } } catch (err) { if (this.retryAttempt < this.retryMax) { this.retryAttempt++; this.log.error(`Failed to get valves. Retry attempt ${this.retryAttempt} of ${this.retryMax} in ${this.retryWait} seconds`); setTimeout(async () => { this.getRachioValves(); }, this.retryWait * 1000); } else { this.log.error(`Failed to get devices\n${err}`); } } } async getRachioBridges() { try { // getting account info this.log.debug('Fetching build info for WiFi Hub...'); this.log.debug('Getting Person info...'); const personId = await this.rachioapi.getPersonInfo(this.token).catch((err) => { this.log.error(`Failed to get info for build ${err}`); throw err; }); this.log.info(`Found Person ID ${personId.id}`); this.log.debug('Getting Person ID info...'); const 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 ${personInfo.username}`); this.log.debug('Getting Base Station info...'); const list = await this.rachioapi.listBaseStations(this.token, personId.id).catch((err) => { this.log.error(`Failed to get base station list ${err}`); throw err; });