homebridge-rachio-irrigation
Version:
Rachio Irrigation System platform plugin for [Homebridge](https://github.com/nfarina/homebridge).
298 lines • 17.3 kB
JavaScript
import pkg from 'homebridge-rachio-irrigation/package.json' with { type: 'json' };
import RachioAPI from '../rachioapi.js';
import listen from '../listener.js';
import poll from '../polling.js';
export default class valve {
platform;
log;
config;
rachioapi;
listener;
polling;
Service;
Characteristic;
valves;
constructor(platform, log = platform.log, config = platform.config, rachioapi = new RachioAPI(platform), listener = new listen(platform), polling = new poll(platform)) {
this.platform = platform;
this.log = log;
this.config = config;
this.rachioapi = rachioapi;
this.listener = listener;
this.polling = polling;
this.Service = platform.Service;
this.Characteristic = platform.Characteristic;
this.valves = [];
}
createValveAccessory(base, property, valve, platformAccessory) {
if (!platformAccessory) {
// Create new Valve System Service
this.log.debug(`Create valve accessory ${valve.id} ${property.property.address.locality} ${valve.name}`);
platformAccessory = new this.platform.api.platformAccessory(`${property.property.address.locality} ${valve.name}`, valve.id);
//const valveService: Service = platformAccessory.addService(this.Service.Valve, valve.id.replace(/-/g, ''), valve.id);
const valveService = platformAccessory.addService(this.Service.Valve, valve.name, valve.id);
valveService.addCharacteristic(this.Characteristic.SerialNumber); //Use Serial Number to store the zone id
valveService.addCharacteristic(this.Characteristic.Model);
valveService.addCharacteristic(this.Characteristic.ConfiguredName);
valveService.getCharacteristic(this.Characteristic.SetDuration).setProps({
minValue: 0,
maxValue: 64800,
});
valveService.getCharacteristic(this.Characteristic.RemainingDuration).setProps({
minValue: 0,
maxValue: 64800,
});
valveService.addCharacteristic(this.Characteristic.ProgramMode);
}
else {
// Update Valve System Service
this.log.debug(`Update valve accessory ${valve.id} ${valve.name}`);
}
// Check if the valve is connected
const valveService = platformAccessory.getService(this.Service.Valve);
if (valve.state.reportedState.connected == true) {
valveService.setCharacteristic(this.Characteristic.StatusFault, this.Characteristic.StatusFault.NO_FAULT);
}
else {
this.log.warn(`${valve.name} disconnected at ${valve.state.reportedState.lastSeen}! This will show as non-responding in Homekit until the connection is restored.`);
valveService.setCharacteristic(this.Characteristic.StatusFault, this.Characteristic.StatusFault.GENERAL_FAULT);
}
// Create AccessoryInformation Service
platformAccessory.getService(this.Service.AccessoryInformation)
.setCharacteristic(this.Characteristic.Name, `${property.property.address.locality} ${valve.name}`)
.setCharacteristic(this.Characteristic.Manufacturer, 'Rachio')
.setCharacteristic(this.Characteristic.SerialNumber, base.serialNumber)
.setCharacteristic(this.Characteristic.Model, 'SHVK001')
.setCharacteristic(this.Characteristic.Identify, true)
.setCharacteristic(this.Characteristic.FirmwareRevision, valve.state.reportedState.firmwareVersion)
.setCharacteristic(this.Characteristic.HardwareRevision, base.macAddress)
.setCharacteristic(this.Characteristic.SoftwareRevision, pkg.version)
.setCharacteristic(this.Characteristic.ProductData, 'Valve');
// Create Valve Service
this.updateValveService(base, valve, valveService);
this.configureValveService(valve, valveService);
return platformAccessory;
}
configureValveService(valve, valveService) {
const zone_Name = valveService.getCharacteristic(this.Characteristic.Name).value;
const zone_Runtime = Number(valveService.getCharacteristic(this.Characteristic.SetDuration).value) / 60;
this.log.info(`Configured Hose Timer ${zone_Name} with ${zone_Runtime} min runtime`);
this.platform.valveServices.push(valveService);
valveService.getCharacteristic(this.Characteristic.Active)
.onGet(this.getValveValue.bind(this, valveService, 'ValveActive'))
.onSet(this.setValveValue.bind(this, valve, valveService));
valveService.getCharacteristic(this.Characteristic.InUse)
.onGet(this.getValveValue.bind(this, valveService, 'ValveInUse'))
.onSet(this.setValveValue.bind(this, valve, valveService));
valveService.getCharacteristic(this.Characteristic.SetDuration)
.onGet(this.getValveValue.bind(this, valveService, 'ValveSetDuration'))
.onSet(this.setValveSetDuration.bind(this, valve, valveService));
valveService.getCharacteristic(this.Characteristic.RemainingDuration)
.onGet(this.getValveValue.bind(this, valveService, 'ValveRemainingDuration'));
}
updateValveService(base, valve, valveService) {
const pollValves = this.config.pollValves ? this.config.pollValves : false;
if (pollValves) {
this.log.warn('Polling for Hose Timers is enabled, try using webhooks');
}
let defaultRuntime = this.platform.defaultRuntime;
valve.enabled = true; // need rachio valve version of enabled
this.log.debug(`valve state ${valve}`);
try {
switch (this.platform.runtimeSource) {
case 0:
defaultRuntime = this.platform.defaultRuntime;
break;
case 1:
if (valve.state.reportedState.defaultRuntimeSeconds > 0) {
defaultRuntime = valve.state.desiredState.defaultRuntimeSeconds;
}
break;
case 2:
//if (valve.flow_data.cycle_run_time_sec > 0) { //can't find json value
defaultRuntime = valve.state.desiredState.defaultRuntimeSeconds;
//}
break;
default:
defaultRuntime = this.platform.defaultRuntime;
break;
}
}
catch (err) {
this.log.debug('Error setting runtime, using default runtime');
}
this.log.debug(`Created valve service for ${valve.name} with ${defaultRuntime} sec runtime (${Math.round(defaultRuntime / 60)} min)`);
valveService
.setCharacteristic(this.Characteristic.ValveType, this.platform.valveType)
.setCharacteristic(this.Characteristic.ServiceLabelIndex, valve.zone)
.setCharacteristic(this.Characteristic.StatusFault, !valve.state.reportedState.connected)
.setCharacteristic(this.Characteristic.SerialNumber, valve.id)
.setCharacteristic(this.Characteristic.Name, valve.name)
.setCharacteristic(this.Characteristic.ConfiguredName, valve.name)
.setCharacteristic(this.Characteristic.Model, 'SHV101');
if (valve.state.reportedState.lastWateringAction) {
const start = valve.state.reportedState.lastWateringAction.start;
const duration = valve.state.reportedState.lastWateringAction.durationSeconds;
const endTime = new Date(start).getTime() + duration * 1000;
const remaining = Math.max(Math.round((endTime - Date.now()) / 1000), 0);
this.platform.endTime[Number(valveService.getCharacteristic(this.Characteristic.SerialNumber).value)] = endTime;
valveService
.setCharacteristic(this.Characteristic.Active, this.Characteristic.Active.ACTIVE)
.setCharacteristic(this.Characteristic.InUse, this.Characteristic.InUse.IN_USE)
.setCharacteristic(this.Characteristic.SetDuration, duration)
.setCharacteristic(this.Characteristic.RemainingDuration, remaining);
}
else {
const start = Date.now();
const duration = Math.ceil(defaultRuntime / 60) * 60;
const endTime = new Date(start).getTime() + duration * 1000;
const remaining = Math.max(Math.round((endTime - Date.now()) / 1000), 0);
this.platform.endTime[Number(valveService.getCharacteristic(this.Characteristic.SerialNumber).value)] = endTime;
valveService
.setCharacteristic(this.Characteristic.Active, this.Characteristic.Active.INACTIVE)
.setCharacteristic(this.Characteristic.InUse, this.Characteristic.InUse.NOT_IN_USE)
.setCharacteristic(this.Characteristic.SetDuration, duration)
.setCharacteristic(this.Characteristic.RemainingDuration, remaining);
}
if (valve.enabled) {
valveService.setCharacteristic(this.Characteristic.IsConfigured, this.Characteristic.IsConfigured.CONFIGURED);
}
else {
valveService.setCharacteristic(this.Characteristic.IsConfigured, this.Characteristic.IsConfigured.NOT_CONFIGURED);
}
return valveService;
}
getValveValue(valveService, characteristicName) {
//this.log.debug(`value ${valveService.getCharacteristic(this.Characteristic.Name).value} ${characteristicName}`)
if (valveService.getCharacteristic(this.Characteristic.StatusFault).value == this.Characteristic.StatusFault.GENERAL_FAULT) {
throw new this.platform.HapStatusError(-70402 /* this.platform.HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
}
let currentValue = null;
const pollValves = this.config.pollValves ? this.config.pollValves : false;
switch (characteristicName) {
case 'ValveActive':
if (pollValves) {
this.polling.startLiveUpdate(valveService);
}
currentValue = valveService.getCharacteristic(this.Characteristic.Active).value;
break;
case 'ValveInUse':
currentValue = valveService.getCharacteristic(this.Characteristic.InUse).value;
break;
case 'ValveSetDuration':
currentValue = valveService.getCharacteristic(this.Characteristic.SetDuration).value;
break;
case 'ValveRemainingDuration': {
// Calc remain duration
const index = this.platform.valveServices.findIndex(valve => valve.subtype === valveService.subtype);
const timeEnding = Date.parse(this.platform.endTime[index]);
const timeNow = Date.now();
let timeRemaining = Math.max(Math.round((timeEnding - timeNow) / 1000), 0);
if (isNaN(timeRemaining)) {
timeRemaining = 0;
}
//valveService.getCharacteristic(this.Characteristic.RemainingDuration).updateValue(timeRemaining)
currentValue = timeRemaining;
break;
}
default:
this.log.debug('Unknown Valve Characteristic Name called', characteristicName);
break;
}
return currentValue;
}
async setValveValue(valve, valveService, value) {
this.log.debug(`${valveService.getCharacteristic(this.Characteristic.Name).value} - Set Active state to ${value}`);
if (value == valveService.getCharacteristic(this.Characteristic.Active).value) {
//IOS 17 bug fix for duplicate calls
this.log.debug(`supressed duplicate call from IOS for ${valveService.getCharacteristic(this.Characteristic.Name).value}, current value ${value}, new value ${valveService.getCharacteristic(this.Characteristic.Active).value}`);
return;
}
// Set homekit state and prepare message for rachio API
const runTime = Number(valveService.getCharacteristic(this.Characteristic.SetDuration).value);
let response;
switch (value) {
case this.Characteristic.Active.ACTIVE:
// Turn on/idle the valve
this.log.info(`Starting ${valveService.getCharacteristic(this.Characteristic.Name).value} valve for ${runTime / 60} mins`);
response = await this.rachioapi.startWatering(this.platform.token, valve.id, runTime);
if (response?.status == 200) {
//json start stuff
const myJsonStart = {
eventId: '53936ce8-299a-3a02-9f8a-754a55291333',
eventType: 'VALVE_RUN_START_EVENT',
externalId: this.platform.webhook_key_local,
payload: {
durationSeconds: valveService.getCharacteristic(this.Characteristic.SetDuration).value,
flowDetected: false,
runType: 'QUICK_RUN',
startTime: new Date().toISOString().split('.')[0] + 'Z',
},
resourceId: valve.id,
resourceType: 'VALVE',
timestamp: new Date().toISOString().split('.')[0] + 'Z',
};
const myJsonStop = {
eventId: '6defa6ff-a169-3477-aa57-3c028455387d',
eventType: 'VALVE_RUN_END_EVENT',
externalId: this.platform.webhook_key_local,
payload: {
durationSeconds: Math.round(Number(valveService.getCharacteristic(this.Characteristic.SetDuration).value)),
endReason: 'COMPLETED',
flowDetected: false,
runType: 'QUICK_RUN',
startTime: new Date().toISOString().split('.')[0] + 'Z',
},
resourceId: valve.id,
resourceType: 'VALVE',
timestamp: new Date().toISOString().split('.')[0] + 'Z',
};
this.log.debug(`Simulating websocket event for ${myJsonStart.resourceId}`);
this.listener.localMessage(null, valveService, myJsonStart);
this.platform.localWebhook = setTimeout(() => {
this.log.debug(`Simulating websocket event for ${myJsonStop.resourceId}`);
this.platform.endTime[Number(valveService.getCharacteristic(this.Characteristic.SerialNumber).value)] = new Date(Date.now()).toISOString();
this.listener.localMessage(null, valveService, myJsonStop);
}, runTime * 1000);
}
break;
case this.Characteristic.Active.INACTIVE:
// Turn off/stopping the valve
this.log.info(`Stopping Zone ${valveService.getCharacteristic(this.Characteristic.Name).value}`);
response = await this.rachioapi.stopWatering(this.platform.token, valve.id);
if (response?.status == 200) {
//json stop stuff
const myJsonStop = {
eventId: '6defa6ff-a169-3477-aa57-3c028455387d',
eventType: 'VALVE_RUN_END_EVENT',
externalId: this.platform.webhook_key_local,
payload: {
durationSeconds: Math.round(Number(valveService.getCharacteristic(this.Characteristic.SetDuration).value) - (Date.parse(this.platform.endTime[Number(valveService.subtype)]) - Date.now()) / 1000),
endReason: 'COMPLETED',
flowDetected: false,
runType: 'QUICK_RUN',
startTime: new Date().toISOString().split('.')[0] + 'Z',
},
resourceId: valve.id,
resourceType: 'VALVE',
timestamp: new Date().toISOString().split('.')[0] + 'Z',
};
this.log.debug(`Simulating websocket event for ${myJsonStop.resourceId}`);
this.listener.localMessage(null, valveService, myJsonStop);
clearTimeout(this.platform.localWebhook);
}
else {
this.log.info('Failed to stop valve');
}
break;
}
return;
}
setValveSetDuration(valve, valveService, value) {
// Set default duration from Homekit value
valveService.getCharacteristic(this.Characteristic.SetDuration).updateValue(value);
this.log.debug(`Set ${valve.name} duration for ${Number(value) / 60} mins`);
this.log.info(`Set ${valveService.getCharacteristic(this.Characteristic.Name).value} duration for ${Number(value) / 60} mins`);
return;
}
}
//# sourceMappingURL=valve.js.map