@homebridge-plugins/homebridge-rainbird
Version:
The Rainbird plugin allows you to access your Rainbird device(s) from HomeKit.
825 lines • 56.6 kB
JavaScript
import { readFileSync } from 'node:fs';
import { argv } from 'node:process';
import { LogLevel, RainBirdService } from 'rainbird';
import { ContactSensor } from './devices/ContactSensor.js';
import { DelayIrrigationSwitch } from './devices/DelayIrrigationSwitch.js';
import { IrrigationSystem } from './devices/IrrigationSystem.js';
import { LeakSensor } from './devices/LeakSensor.js';
import { ProgramSwitch } from './devices/ProgramSwitch.js';
import { StopIrrigationSwitch } from './devices/StopIrrigationSwitch.js';
import { TestZoneSwitch } from './devices/TestZoneSwitch.js';
import { ZoneValve } from './devices/ZoneValve.js';
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
const COMMAND_ID_CONTROLLER_FIRMWARE_VERSION = 0x0B;
const COMMAND_ID_RETRIEVE_SCHEDULE = 0x20;
const COMMAND_ID_WATER_BUDGET = 0x30;
const COMMAND_ID_ZONES_SEASONAL_ADJUST_FACTOR = 0x32;
const COMMAND_ID_TEST_ZONE = 0x3A;
const COMMAND_ID_CONTROLLER_EVENT_TIMESTAMP = 0x4A;
const COMMAND_ID_STACK_RUN_ZONE = 0x4B;
const UNKNOWN_FIRMWARE_VERSION = 'Unknown';
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
export class RainbirdPlatform {
accessories;
handlers = [];
controllerCapabilities = new Map();
api;
log;
hap;
config;
platformConfig;
platformLogging;
platformRefreshRate;
platformPushRate;
platformUpdateRate;
debugMode;
version;
constructor(log, config, api) {
this.accessories = [];
this.api = api;
this.hap = this.api.hap;
this.log = log;
// only load if configured
if (!config) {
return;
}
// Plugin options into our config variables.
this.config = {
platform: 'Rainbird',
name: config.name,
devices: config.devices,
options: config.options,
};
// Plugin Configuration
this.getPlatformLogSettings();
this.getPlatformRateSettings();
this.getPlatformConfigSettings();
this.getVersion();
// Finish initializing the platform
this.debugLog(`Finished initializing platform: ${config.name}`);
// verify the config
(async () => {
try {
this.verifyConfig();
this.debugLog('Config OK');
}
catch (e) {
this.errorLog(`Verify Config, Error Message: ${e.message}, Submit Bugs Here: https://bit.ly/homebridge-rainbird-bug-report`);
this.debugErrorLog(`Verify Config, Error: ${e}`);
}
})();
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
this.api.on('didFinishLaunching', async () => {
this.debugLog('Executed didFinishLaunching callback');
try {
await this.discoverDevices();
}
catch (e) {
this.errorLog(`Failed to Discover Devices, ${JSON.stringify(e.message)}`);
this.debugLog(JSON.stringify(e));
}
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory) {
this.infoLog(`Loading accessory from cache: ${accessory.displayName}`);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
registerHandler(handler) {
this.handlers.push(handler);
return handler;
}
createPlatformAccessory(displayName, uuid) {
const PlatformAccessoryCtor = this.api.platformAccessory;
return new PlatformAccessoryCtor(displayName, uuid);
}
supportsStackRunZone(deviceId) {
return this.controllerCapabilities.get(deviceId)?.supportsStackRunZone ?? false;
}
async detectControllerCapabilities(rainbird) {
const capabilities = {
supportsControllerFirmwareVersion: await rainbird.getCommandSupport(COMMAND_ID_CONTROLLER_FIRMWARE_VERSION),
supportsRetrieveSchedule: await rainbird.getCommandSupport(COMMAND_ID_RETRIEVE_SCHEDULE),
supportsWaterBudget: await rainbird.getCommandSupport(COMMAND_ID_WATER_BUDGET),
supportsZonesSeasonalAdjustFactor: await rainbird.getCommandSupport(COMMAND_ID_ZONES_SEASONAL_ADJUST_FACTOR),
supportsTestZone: await rainbird.getCommandSupport(COMMAND_ID_TEST_ZONE),
supportsControllerEventTimestamp: await rainbird.getCommandSupport(COMMAND_ID_CONTROLLER_EVENT_TIMESTAMP),
supportsStackRunZone: await rainbird.getCommandSupport(COMMAND_ID_STACK_RUN_ZONE),
};
return capabilities;
}
async logControllerEnhancements(rainbird, capabilities) {
this.debugLog(`Controller capabilities: ${JSON.stringify(capabilities)}`);
if (capabilities.supportsWaterBudget) {
for (const program of [0, 1, 2, 3]) {
const waterBudget = await rainbird.getWaterBudget(program);
this.debugLog(`Program ${program} water budget: ${waterBudget}%`);
}
}
if (capabilities.supportsZonesSeasonalAdjustFactor) {
for (const program of [0, 1, 2, 3]) {
const seasonalAdjust = await rainbird.getZonesSeasonalAdjustFactor(program);
if (seasonalAdjust.length > 0) {
this.debugLog(`Program ${program} zones seasonal adjust: ${JSON.stringify(seasonalAdjust)}`);
}
}
}
if (capabilities.supportsControllerEventTimestamp) {
const timestamp = await rainbird.getControllerEventTimestamp(0);
if (timestamp > 0) {
this.debugLog(`Controller event timestamp(0): ${new Date(timestamp * 1000).toISOString()}`);
}
}
}
/**
* Verify the config passed to the plugin is valid
*/
verifyConfig() {
this.initialiseConfig();
if (this.config.devices) {
for (const device of this.config.devices) {
if (!device.ipaddress) {
throw new Error('The devices config section is missing the "IP Address" in the config, and will be skipped.');
}
if (!device.password) {
throw new Error('The devices config section is missing the "Password" in the config, and will be skipped.');
}
}
}
else {
throw new Error('The devices config section is missing from the config. This device will be skipped.');
}
this.config.options = this.config.options || {};
if (!this.config.options.refreshRate) {
// default 300 seconds (5 minutes)
this.config.options.refreshRate = 300;
this.debugLog('Using Default Refresh Rate.');
}
if (!this.config.options.pushRate) {
// default 100 milliseconds
this.config.options.pushRate = 0.1;
this.debugLog('Using Default Push Rate.');
}
}
initialiseConfig() {
for (const device of this.config.devices ?? []) {
device.showRainSensor = device.showRainSensor ?? false;
device.showValveSensor = device.showValveSensor ?? false;
device.showProgramASwitch = device.showProgramASwitch ?? false;
device.showProgramBSwitch = device.showProgramBSwitch ?? false;
device.showProgramCSwitch = device.showProgramCSwitch ?? false;
device.showProgramDSwitch = device.showProgramDSwitch ?? false;
device.showStopIrrigationSwitch = device.showStopIrrigationSwitch ?? false;
device.showZoneValve = device.showZoneValve ?? false;
device.includeZones = device.includeZones ?? '';
device.showDelayIrrigationSwitch = device.showDelayIrrigationSwitch ?? false;
device.irrigationDelay = device.irrigationDelay ?? 1;
device.showTestZoneSwitch = device.showTestZoneSwitch ?? false;
device.syncTime = device.syncTime ?? false;
device.showRequestResponse = device.showRequestResponse ?? false;
device.minValueRemainingDuration = device.minValueRemainingDuration ?? 0;
device.maxValueRemainingDuration = device.maxValueRemainingDuration ?? 3600;
}
}
/**
* This method is used to discover the your location and devices.
*/
async discoverDevices() {
for (const device of this.config.devices) {
try {
const rainbird = new RainBirdService({
address: device.ipaddress,
password: device.password,
refreshRate: this.config.options.refreshRate,
showRequestResponse: device.showRequestResponse,
syncTime: device.syncTime,
});
// Listen for log events
rainbird.on('log', (log) => {
switch (log.level) {
case LogLevel.ERROR:
this.errorLog(`From Rainbird Library: ${log.message}`);
break;
case LogLevel.WARN:
this.warnLog(`From Rainbird Library: ${log.message}`);
break;
case LogLevel.DEBUG:
this.debugLog(`From Rainbird Library: ${log.message}`);
break;
case LogLevel.INFO:
default:
this.infoLog(`From Rainbird Library: ${log.message}`);
}
});
const metaData = await rainbird.init();
this.debugLog(JSON.stringify(metaData));
const capabilities = await this.detectControllerCapabilities(rainbird);
this.controllerCapabilities.set(metaData.serialNumber, capabilities);
await this.logControllerEnhancements(rainbird, capabilities);
// Display device details
this.infoLog(`Model: ${metaData.model}, [Version: ${metaData.version}, Serial Number: ${metaData.serialNumber}, Zones: ${JSON.stringify(metaData.zones)}]`);
const irrigationAccessory = this.createIrrigationSystem(device, rainbird);
this.createLeakSensor(device, rainbird);
for (const zoneId of metaData.zones) {
const configured = (await irrigationAccessory).context.configured[zoneId] ?? this.hap.Characteristic.IsConfigured.CONFIGURED;
if (configured === this.hap.Characteristic.IsConfigured.CONFIGURED) {
this.createZoneValve(device, rainbird, zoneId);
this.createContactSensor(device, rainbird, zoneId);
this.createTestZoneSwitch(device, rainbird, zoneId, capabilities.supportsTestZone);
}
}
for (const programId of ['A', 'B', 'C', 'D']) {
this.createProgramSwitch(device, rainbird, programId);
}
this.createStopIrrigationSwitch(device, rainbird);
this.createDelayIrrigationSwitch(device, rainbird);
// Handle zone enable/disable
rainbird.on('zone_enable', (zoneId, enabled) => {
if (enabled) {
this.createContactSensor(device, rainbird, zoneId);
this.createTestZoneSwitch(device, rainbird, zoneId, capabilities.supportsTestZone);
// this.createZoneValve(device, rainbird, zoneId);
}
else {
this.removeContactSensor(device, rainbird, zoneId);
this.removeTestZoneSwitch(device, rainbird, zoneId);
// this.removeZoneValve(device, rainbird, zoneId);
}
});
}
catch (e) {
this.errorLog(`Failed to connect to RainBird controller at ${device.ipaddress}: ${e.message}`);
// Provide specific troubleshooting guidance based on error type
if (e.message?.includes('ECONNREFUSED')) {
this.errorLog('Connection refused - Troubleshooting steps:');
this.errorLog(' 1. Verify the RainBird controller is powered on and connected to your network');
this.errorLog(' 2. Check that the IP address in your config matches your controller\'s actual IP');
this.errorLog(' 3. Ensure your controller is accessible from this device (try pinging the IP)');
this.errorLog(' 4. Verify the RainBird LNK WiFi module is properly installed and functioning');
this.errorLog(' 5. Check if your router has "Band Steering" enabled and try disabling it');
this.errorLog(' 6. Ensure your WiFi network is not using channel 13 (not supported by some RainBird modules)');
this.errorLog(' 7. Close the RainBird mobile app if it\'s running (can cause connectivity conflicts)');
}
else if (e.message?.includes('ETIMEDOUT') || e.message?.includes('timeout')) {
this.errorLog('Connection timeout - The controller may be slow to respond or overloaded');
this.errorLog(' Try restarting the RainBird controller and check your network connection');
}
else if (e.message?.includes('EHOSTUNREACH')) {
this.errorLog('Host unreachable - Check your network configuration and firewall settings');
}
else {
this.errorLog('Connection error - Please check your controller configuration and network settings');
}
this.errorLog(`Skipping device at ${device.ipaddress} and continuing with other devices...`);
continue;
}
}
}
async createIrrigationSystem(device, rainbird) {
const uuid = this.api.hap.uuid.generate(`${device.ipaddress}-${rainbird.model}-${rainbird.serialNumber}`);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
if (!device.hide_device) {
this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName}`);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
existingAccessory.displayName = device.configDeviceName
? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName)
: await this.validateAndCleanDisplayName(rainbird.model, 'model', rainbird.model);
existingAccessory.context.device = device;
existingAccessory.context.deviceID = rainbird.serialNumber;
existingAccessory.context.model = rainbird.model;
existingAccessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new IrrigationSystem(this, existingAccessory, device, rainbird));
this.debugLog(`Irrigation System uuid: ${device.ipaddress}-${rainbird.model}-${rainbird.serialNumber}, (${existingAccessory.UUID})`);
return existingAccessory;
}
else {
this.unregisterPlatformAccessories(existingAccessory);
}
}
else if (!device.hide_device) {
// the accessory does not yet exist, so we need to create it
this.infoLog(`Adding new accessory: ${rainbird.model}`);
// create a new accessory
const accessory = this.createPlatformAccessory(rainbird.model, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.displayName = device.configDeviceName
? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName)
: await this.validateAndCleanDisplayName(rainbird.model, 'model', rainbird.model);
accessory.context.device = device;
accessory.context.deviceID = rainbird.serialNumber;
accessory.context.model = rainbird.model;
accessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new IrrigationSystem(this, accessory, device, rainbird));
this.debugLog(`Irrigation System uuid: ${device.ipaddress}-${rainbird.model}-${rainbird.serialNumber}, (${accessory.UUID})`);
// link the accessory to your platform
this.externalOrPlatform(device, accessory);
this.accessories.push(accessory);
return accessory;
}
else {
if (this.platformLogging === 'debug') {
this.errorLog(`Unable to Register new device: ${rainbird.model}`);
}
}
}
async createLeakSensor(device, rainbird) {
const model = 'WR2';
const leakSensorModel = `${model} Leak Sensor`;
const leakSensorConfigName = device.configDeviceName ? `${device.configDeviceName} Leak Sensor` : 'Leak Sensor';
const uuid = this.api.hap.uuid.generate(`${device.ipaddress}-${model}-${rainbird.serialNumber}`);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
if (!device.hide_device && device.showRainSensor) {
this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName}`);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
existingAccessory.displayName = leakSensorConfigName
? await this.validateAndCleanDisplayName(leakSensorConfigName, 'configDeviceName Leak Sensor', leakSensorConfigName)
: await this.validateAndCleanDisplayName(leakSensorModel, leakSensorModel, leakSensorModel);
existingAccessory.context.device = device;
existingAccessory.context.deviceID = rainbird.serialNumber;
existingAccessory.context.model = model;
existingAccessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new LeakSensor(this, existingAccessory, device, rainbird));
this.debugLog(`Leak Sensor uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${existingAccessory.UUID})`);
}
else {
this.unregisterPlatformAccessories(existingAccessory);
}
}
else if (!device.hide_device && device.showRainSensor) {
// the accessory does not yet exist, so we need to create it
this.infoLog(`Adding new accessory: ${model}`);
// create a new accessory
const accessory = this.createPlatformAccessory(model, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.displayName = leakSensorConfigName
? await this.validateAndCleanDisplayName(leakSensorConfigName, 'configDeviceName Leak Sensor', leakSensorConfigName)
: await this.validateAndCleanDisplayName(leakSensorModel, leakSensorModel, leakSensorModel);
accessory.context.device = device;
accessory.context.deviceID = rainbird.serialNumber;
accessory.context.model = model;
accessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new LeakSensor(this, accessory, device, rainbird));
this.debugLog(`Leak Sensor uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${accessory.UUID})`);
// link the accessory to your platform
this.externalOrPlatform(device, accessory);
this.accessories.push(accessory);
}
else {
if (this.platformLogging === 'debug' && device.showRainSensor) {
this.errorLog(`Unable to Register new device: ${model}`);
}
}
}
async FirmwareRevision(rainbird, device) {
if (device.firmware) {
return String(device.firmware);
}
const controllerFirmwareVersion = await rainbird.getControllerFirmwareVersion();
if (controllerFirmwareVersion && controllerFirmwareVersion !== UNKNOWN_FIRMWARE_VERSION) {
return controllerFirmwareVersion;
}
return String(rainbird.version ?? this.version);
}
async createZoneValve(device, rainbird, zoneId) {
const model = `${rainbird.model}-valve-${zoneId}`;
const uuid = this.api.hap.uuid.generate(`${device.ipaddress}-${model}-${rainbird.serialNumber}`);
const name = `Zone ${zoneId}`;
const valveConfigName = device.configDeviceName ? `${device.configDeviceName} ${name}` : 'Valve';
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
const irrigationUuid = this.api.hap.uuid.generate(`${device.ipaddress}-${rainbird.model}-${rainbird.serialNumber}`);
const irrigationAccessory = this.accessories.find(accessory => accessory.UUID === irrigationUuid);
const includeZones = device.includeZones.split(',').map(Number);
const registerZoneValve = !device.hide_device
&& device.showZoneValve
&& (includeZones.includes(0) || includeZones.includes(zoneId));
if (existingAccessory) {
// the accessory already exists
if (registerZoneValve) {
this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName}`);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
existingAccessory.displayName = valveConfigName
? await this.validateAndCleanDisplayName(valveConfigName, `configDeviceName ${name}`, `${device.configDeviceName} ${name}`)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
existingAccessory.context.device = device;
existingAccessory.context.deviceID = rainbird.serialNumber;
existingAccessory.context.model = model;
existingAccessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
existingAccessory.context.zoneId = zoneId;
this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new ZoneValve(this, existingAccessory, device, rainbird, irrigationAccessory.context));
this.debugLog(`Zone Valve uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${existingAccessory.UUID})`);
}
else {
this.unregisterPlatformAccessories(existingAccessory);
}
}
else if (registerZoneValve) {
// the accessory does not yet exist, so we need to create it
this.infoLog(`Adding new accessory: ${model}`);
// create a new accessory
const accessory = this.createPlatformAccessory(name, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.displayName = valveConfigName
? await this.validateAndCleanDisplayName(valveConfigName, `configDeviceName ${name}`, `${device.configDeviceName} ${name}`)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
accessory.context.device = device;
accessory.context.deviceID = rainbird.serialNumber;
accessory.context.model = model;
accessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
accessory.context.zoneId = zoneId;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new ZoneValve(this, accessory, device, rainbird, irrigationAccessory.context));
this.debugLog(`Valve Zone uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${accessory.UUID})`);
// link the accessory to your platform
this.externalOrPlatform(device, accessory);
this.accessories.push(accessory);
}
else {
if (this.platformLogging === 'debug' && device.showZoneValve) {
this.errorLog(`Unable to Register new device: ${model}`);
}
}
}
removeZoneValve(device, rainbird, zoneId) {
const model = `${rainbird.model}-valve-${zoneId}`;
const uuid = this.api.hap.uuid.generate(`${device.ipaddress}-${model}-${rainbird.serialNumber}`);
const index = this.accessories.findIndex(accessory => accessory.UUID === uuid);
if (index >= 0) {
this.unregisterPlatformAccessories(this.accessories[index]);
this.accessories.splice(index, 1);
}
}
async createContactSensor(device, rainbird, zoneId) {
const model = `${rainbird.model}-${zoneId}`;
const uuid = this.api.hap.uuid.generate(`${device.ipaddress}-${model}-${rainbird.serialNumber}`);
const name = `Zone ${zoneId}`;
const contactSensorConfigName = device.configDeviceName ? `${device.configDeviceName} ${name}` : 'Contact Sensor';
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
if (!device.hide_device && device.showValveSensor) {
this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName}`);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
existingAccessory.displayName = contactSensorConfigName
? await this.validateAndCleanDisplayName(contactSensorConfigName, `configDeviceName ${name}`, contactSensorConfigName)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
existingAccessory.context.device = device;
existingAccessory.context.deviceID = rainbird.serialNumber;
existingAccessory.context.model = model;
existingAccessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
existingAccessory.context.zoneId = zoneId;
this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new ContactSensor(this, existingAccessory, device, rainbird));
this.debugLog(`Contact Sensor uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${existingAccessory.UUID})`);
}
else {
this.unregisterPlatformAccessories(existingAccessory);
}
}
else if (!device.hide_device && device.showValveSensor) {
// the accessory does not yet exist, so we need to create it
this.infoLog(`Adding new accessory: ${model}`);
// create a new accessory
const accessory = this.createPlatformAccessory(model, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.displayName = contactSensorConfigName
? await this.validateAndCleanDisplayName(contactSensorConfigName, `configDeviceName ${name}`, contactSensorConfigName)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
accessory.context.device = device;
accessory.context.deviceID = rainbird.serialNumber;
accessory.context.model = model;
accessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
accessory.context.zoneId = zoneId;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new ContactSensor(this, accessory, device, rainbird));
this.debugLog(`Contact Sensor uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${accessory.UUID})`);
// link the accessory to your platform
this.externalOrPlatform(device, accessory);
this.accessories.push(accessory);
}
else {
if (this.platformLogging === 'debug' && device.showValveSensor) {
this.errorLog(`Unable to Register new device: ${rainbird.model}-${zoneId}`);
}
}
}
removeContactSensor(device, rainbird, zoneId) {
const model = `${rainbird.model}-${zoneId}`;
const uuid = this.api.hap.uuid.generate(`${device.ipaddress}-${model}-${rainbird.serialNumber}`);
const index = this.accessories.findIndex(accessory => accessory.UUID === uuid);
if (index >= 0) {
this.unregisterPlatformAccessories(this.accessories[index]);
this.accessories.splice(index, 1);
}
}
async createTestZoneSwitch(device, rainbird, zoneId, supportsTestZone) {
const model = `${rainbird.model}-test-${zoneId}`;
const uuid = this.api.hap.uuid.generate(`${device.ipaddress}-${model}-${rainbird.serialNumber}`);
const name = `Zone ${zoneId} Test`;
const testSwitchConfigName = device.configDeviceName ? `${device.configDeviceName} ${name}` : 'Test Zone Switch';
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
if (!device.hide_device && device.showTestZoneSwitch && supportsTestZone) {
this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName}`);
existingAccessory.displayName = testSwitchConfigName
? await this.validateAndCleanDisplayName(testSwitchConfigName, `configDeviceName ${name}`, testSwitchConfigName)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
existingAccessory.context.device = device;
existingAccessory.context.deviceID = rainbird.serialNumber;
existingAccessory.context.model = model;
existingAccessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
existingAccessory.context.zoneId = zoneId;
this.api.updatePlatformAccessories([existingAccessory]);
this.registerHandler(new TestZoneSwitch(this, existingAccessory, device, rainbird));
this.debugLog(`Test Zone Switch uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${existingAccessory.UUID})`);
}
else {
this.unregisterPlatformAccessories(existingAccessory);
}
}
else if (!device.hide_device && device.showTestZoneSwitch && supportsTestZone) {
this.infoLog(`Adding new accessory: ${model}`);
const accessory = this.createPlatformAccessory(model, uuid);
accessory.displayName = testSwitchConfigName
? await this.validateAndCleanDisplayName(testSwitchConfigName, `configDeviceName ${name}`, testSwitchConfigName)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
accessory.context.device = device;
accessory.context.deviceID = rainbird.serialNumber;
accessory.context.model = model;
accessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
accessory.context.zoneId = zoneId;
this.registerHandler(new TestZoneSwitch(this, accessory, device, rainbird));
this.debugLog(`Test Zone Switch uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${accessory.UUID})`);
this.externalOrPlatform(device, accessory);
this.accessories.push(accessory);
}
else {
if (this.platformLogging.includes('debug') && device.showTestZoneSwitch) {
if (!supportsTestZone) {
this.warnLog(`Skipping Test Zone switch for ${rainbird.model}: controller does not support command 0x${COMMAND_ID_TEST_ZONE.toString(16).toUpperCase()}`);
return;
}
this.errorLog(`Unable to Register new device: ${model}`);
}
}
}
removeTestZoneSwitch(device, rainbird, zoneId) {
const model = `${rainbird.model}-test-${zoneId}`;
const uuid = this.api.hap.uuid.generate(`${device.ipaddress}-${model}-${rainbird.serialNumber}`);
const index = this.accessories.findIndex(accessory => accessory.UUID === uuid);
if (index >= 0) {
this.unregisterPlatformAccessories(this.accessories[index]);
this.accessories.splice(index, 1);
}
}
async createProgramSwitch(device, rainbird, programId) {
const model = `${rainbird.model}-pgm-${programId}`;
const uuid = this.api.hap.uuid.generate(`${device.ipaddress}-${model}-${rainbird.serialNumber}`);
const name = `Program ${programId}`;
const programSwitchConfigName = device.configDeviceName ? `${device.configDeviceName} ${name}` : 'Program Switch';
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
const showProgramSwitch = device[`showProgram${programId}Switch`];
if (existingAccessory) {
// the accessory already exists
if (!device.hide_device && showProgramSwitch) {
this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName}`);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
existingAccessory.displayName = programSwitchConfigName
? await this.validateAndCleanDisplayName(programSwitchConfigName, `configDeviceName ${name}`, programSwitchConfigName)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
existingAccessory.context.device = device;
existingAccessory.context.deviceID = rainbird.serialNumber;
existingAccessory.context.model = model;
existingAccessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
existingAccessory.context.programId = programId;
this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new ProgramSwitch(this, existingAccessory, device, rainbird));
this.debugLog(`Program Switch uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${existingAccessory.UUID})`);
}
else {
this.unregisterPlatformAccessories(existingAccessory);
}
}
else if (!device.hide_device && showProgramSwitch) {
// the accessory does not yet exist, so we need to create it
this.infoLog(`Adding new accessory: ${model}`);
// create a new accessory
const accessory = this.createPlatformAccessory(model, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.displayName = programSwitchConfigName
? await this.validateAndCleanDisplayName(programSwitchConfigName, `configDeviceName ${name}`, programSwitchConfigName)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
accessory.context.device = device;
accessory.context.deviceID = rainbird.serialNumber;
accessory.context.model = model;
accessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
accessory.context.programId = programId;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new ProgramSwitch(this, accessory, device, rainbird));
this.debugLog(`Program Switch uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${accessory.UUID})`);
// link the accessory to your platform
this.externalOrPlatform(device, accessory);
this.accessories.push(accessory);
}
else {
if (this.platformLogging.includes('debug') && showProgramSwitch) {
this.errorLog(`Unable to Register new device: ${model}`);
}
}
}
async createStopIrrigationSwitch(device, rainbird) {
const model = `${rainbird.model}-stop`;
const uuid = this.api.hap.uuid.generate(`${device.ipaddress}-${model}-${rainbird.serialNumber}`);
const name = 'Stop Irrigation';
const stopSwitchConfigName = device.configDeviceName ? `${device.configDeviceName} ${name}` : 'Stop Switch';
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
if (!device.hide_device && device.showStopIrrigationSwitch) {
this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName}`);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
existingAccessory.displayName = stopSwitchConfigName
? await this.validateAndCleanDisplayName(stopSwitchConfigName, `configDeviceName ${name}`, stopSwitchConfigName)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
existingAccessory.context.device = device;
existingAccessory.context.deviceID = rainbird.serialNumber;
existingAccessory.context.model = model;
existingAccessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new StopIrrigationSwitch(this, existingAccessory, device, rainbird));
this.debugLog(`Stop Irrigation Switch uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${existingAccessory.UUID})`);
}
else {
this.unregisterPlatformAccessories(existingAccessory);
}
}
else if (!device.hide_device && device.showStopIrrigationSwitch) {
// the accessory does not yet exist, so we need to create it
this.infoLog(`Adding new accessory: ${model}`);
// create a new accessory
const accessory = this.createPlatformAccessory(model, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.displayName = stopSwitchConfigName
? await this.validateAndCleanDisplayName(stopSwitchConfigName, `configDeviceName ${name}`, stopSwitchConfigName)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
accessory.context.device = device;
accessory.context.deviceID = rainbird.serialNumber;
accessory.context.model = model;
accessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new StopIrrigationSwitch(this, accessory, device, rainbird));
this.debugLog(`Stop Irrigation Switch uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${accessory.UUID})`);
// link the accessory to your platform
this.externalOrPlatform(device, accessory);
this.accessories.push(accessory);
}
else {
if (this.platformLogging.includes('debug') && device.showStopIrrigationSwitch) {
this.errorLog(`Unable to Register new device: ${model}`);
}
}
}
async createDelayIrrigationSwitch(device, rainbird) {
const model = `${rainbird.model}-delay`;
const uuid = this.api.hap.uuid.generate(`${device.ipaddress}-${model}-${rainbird.serialNumber}`);
const name = 'Delay Irrigation';
const delaySwitchConfigName = device.configDeviceName ? `${device.configDeviceName} ${name}` : 'Delay Switch';
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid);
if (existingAccessory) {
// the accessory already exists
if (!device.hide_device && device.showDelayIrrigationSwitch) {
this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName}`);
// if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.:
existingAccessory.displayName = delaySwitchConfigName
? await this.validateAndCleanDisplayName(delaySwitchConfigName, `configDeviceName ${name}`, delaySwitchConfigName)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
existingAccessory.context.device = device;
existingAccessory.context.deviceID = rainbird.serialNumber;
existingAccessory.context.model = model;
existingAccessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
this.api.updatePlatformAccessories([existingAccessory]);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new DelayIrrigationSwitch(this, existingAccessory, device, rainbird));
this.debugLog(`Delay Irrigation Switch uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${existingAccessory.UUID})`);
}
else {
this.unregisterPlatformAccessories(existingAccessory);
}
}
else if (!device.hide_device && device.showDelayIrrigationSwitch) {
// the accessory does not yet exist, so we need to create it
this.infoLog(`Adding new accessory: ${model}`);
// create a new accessory
const accessory = this.createPlatformAccessory(model, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
accessory.displayName = delaySwitchConfigName
? await this.validateAndCleanDisplayName(delaySwitchConfigName, `configDeviceName ${name}`, delaySwitchConfigName)
: await this.validateAndCleanDisplayName(name, `${name} name`, name);
accessory.context.device = device;
accessory.context.deviceID = rainbird.serialNumber;
accessory.context.model = model;
accessory.context.FirmwareRevision = await this.FirmwareRevision(rainbird, device);
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
this.registerHandler(new DelayIrrigationSwitch(this, accessory, device, rainbird));
this.debugLog(`Delay Irrigation Switch uuid: ${device.ipaddress}-${model}-${rainbird.serialNumber}, (${accessory.UUID})`);
// link the accessory to your platform
this.externalOrPlatform(device, accessory);
this.accessories.push(accessory);
}
else {
if (this.platformLogging.includes('debug') && device.showDelayIrrigationSwitch) {
this.errorLog(`Unable to Register new device: ${model}`);
}
}
}
async externalOrPlatform(device, accessory) {
if (device.external) {
this.debugWarnLog(`${accessory.displayName} External Accessory Mode`);
this.externalAccessory(accessory);
}
else {
this.debugLog(`${accessory.displayName} External Accessory Mode: ${device.external}`);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
}
async externalAccessory(accessory) {
this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);
}
unregisterPlatformAccessories(existingAccessory) {
// remove platform accessories when no longer present
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
this.warnLog(`Removing existing accessory from cache: ${existingAccessory.displayName}`);
}
async getPlatformLogSettings() {
this.debugMode = argv.includes('-D') ?? argv.includes('--debug');
this.platformLogging = (this.config.options?.logging === 'debug' || this.config.options?.logging === 'standard'
|| this.config.options?.logging === 'none')
? this.config.options.logging
: this.debugMode ? 'debugMode' : 'standard';
const logging = this.config.options?.logging ? 'Platform Config' : this.debugMode ? 'debugMode' : 'Default';
await this.debugLog(`Using ${logging} Logging: ${this.platformLogging}`);
}
async getPlatformRateSettings() {
// RefreshRate
this.platformRefreshRate = this.config.options?.refreshRate ? this.config.options.refreshRate : undefined;
const refreshRate = this.config.options?.refreshRate ? 'Using Platform Config refreshRate' :