I know this is probably already an XY problem, but would be super-stoked if someone could identify an alternate (and much easier) approach to solving this problem than the route I am currently taking. Or if not, perhaps some tips on the behemoth code I am trying to write to solve this issue.
Background
-
I have a greenhouse with 4 vents currently actuated by ‘Wax’ Self Openers (just temp based - no electrics)
-
I want to replace the 2 northern vents with Electrically controlled vents, so they can be Opened and closed by OpenHAB, based on both Temperature AND Humidity
-
I will also use wind-speed/direction to close the vents when we have gale force winds coming through, which tend to be hot northerly winds
-
I am leaving the 2 Southern Vents as the ‘dumb’ wax openers, as any gale southerly winds tend to be cold, so they would not open anyway. And they provide redundancy, so I don’t get some kind of Java Exception Error killing all my wife’s plants in there from overheating
-
I have 2 greenhouse vent actuators which I have removed the wax openers, and modified to include 12vDC linear actuator motors to open and close these
-
The Actuators are controlled by 2 separate Modbus SPDT relays with both NC connected to Ground , one for open, one for close.
-
Just to alleviate any concerns:
- These actuators have limit switches inbuilt, so trying to drive them past their limit will do no harm
- Any accidental energizing of both relays will also do no harm (shorts etc), as there will be no potential voltage across the motor (Both would then be connected to +ve)
So far, so good - Link a couple of switch items to the Modbus channels, and I can manually raise and lower the vent via OpenHab, and they seem to maintain the Vent position nicely.
The Problem
I want to be able to control these via an Item, where I can specify a Opening Percentage in increments of (say) 10% from 0-100. This in turn will drive the Linear motor to the new target position from its current position by turning on either the open or close relay for the appropriate amount of time.
Ultimately this item will be updated by another rule which sets the target based on Greenhouse temp/humidity, and windspeed & direction (This other rule is not the focus of this post/question…yet !!)
The ideal solution
I kept on looking at roller-shutters, and scripts/controls/add-ons and the like, to see if there was an ‘out-of-the-box’ solution I could leverage to control the relay opening and maintain a ‘record’ of the Vents position. Yes I know given the linear drive/timing arrangement, it wont be perfectly accurate, but nor does it need to be for this application. Occasionally (Say every night), I could drive it closed for a couple of extra seconds to re-calibrate (See note about limit switches above - I wont cook the linear motors by doing this).
The (perhaps) not so ideal solution
I’m really hoping we don’t need to go here, but I have written/refactored this code dozens of times, and it kind of works. It is a prototype, so not all functionally complete, and needs some ‘productionising’
Some notes:
- I take the opening and closing time parameters from Item MetaData
- I also take the Actual Position from an Item (So it can be persisted across restarts), BUT eventually found I had to read it into cache, and use that for the actual operations, as trying to read the actual position from an Item milliseconds after it was updated, caused various issues for back-to-back operations
- At the conclusion of each movement operation, it’s supposed to check if the actual position matches the target position, and if it does, turn off the appropriate relay. If it doesn’t, it should leave the relay/motor going, scheduled for a new time to turn it off.
- I will probably add more logic for the Calibration (e.g. calibrating the vents by driving ‘past zero’) on startup and nightly
Where the code seems to not work so well/predictably, is immediate back-to-back target position changes while it is already actuating, or actually needs to change the direction, as the target position has gone in the other direction (Yeah, for the future rules I create to set the target position, I will include some kind of hysteresis, so the vents aren’t flapping up and down all day )
var {UnDefType} = require('@runtime');
var {TimerMgr} = require('openhab_rules_tools');
var timers = cache.shared.get('position_control', () => TimerMgr());
if(event.itemName != undefined)
//Stops script from running if triggered via OpenHAB console - Event would be undefined, and fail
{
console.debug("Position control rule triggered by",items[event.itemName].name);
var nextpos = cache.shared.get(items[event.itemName].name+'_nextpos',0);
var operation = cache.shared.get(items[event.itemName].name+'_operation',null);
var actpos = cache.shared.get(items[event.itemName].name+'_actual',null);
console.debug(items[event.itemName].name+" : Init: NextPos was: "+parseInt(cache.shared.get(items[event.itemName].name+'_nextpos')));
console.debug(items[event.itemName].name+" : Init: Operation was: "+cache.shared.get(items[event.itemName].name+'_operation'));
console.debug(items[event.itemName].name+" : Init: ActPos (cache) was: "+cache.shared.get(items[event.itemName].name+'_actual'));
now = time.ZonedDateTime.now();
//Display opening & closing & calibration time parameters (in seconds) from Item Metadata for Debug
console.debug(items[event.itemName].name+" : NextOp : Open Time = "+items[event.itemName].getMetadata("open_time").value);
console.debug(items[event.itemName].name+" : NextOp : Close Time = "+items[event.itemName].getMetadata("close_time").value);
console.debug(items[event.itemName].name+" : NextOp : Calibrate Time = "+items[event.itemName].getMetadata("calibrate_time").value);
function calibrate(itemname)
{
console.debug(itemname+" : Calibrate: Calibration Starting - Closing for "+items[itemname].getMetadata("calibrate_time").value+" Seconds");
items[itemname+"_openRelay"].sendCommand("OFF");
items[itemname+"_closeRelay"].sendCommand("ON");
cache.shared.put(itemname+'_operation',"CALIBRATE");
cache.shared.put(itemname+'_nextpos',0);
console.debug(itemname+" : Calibrate: NextPos was: "+parseInt(cache.shared.get(itemname+'_nextpos')));
schedtime = time.ZonedDateTime.now().plusSeconds(parseInt(items[itemname].getMetadata("calibrate_time").value));
console.debug(itemname+" : Calibrate: Schedtime: "+schedtime);
timers.check(itemname+"_control",schedtime.toString(),moveexpire(itemname),true,null,itemname+"_control");
}
//Function to run at expiry of timer to either turn off Relay's or initial a further operation if Target and Actual are not equal
function moveexpire(itemname) {
return function () {
console.debug(itemname+" : MoveExpire: NextPos was: "+parseInt(cache.shared.get(itemname+'_nextpos')));
console.debug(itemname+" : MoveExpire: Current Operation is "+cache.shared.get(itemname+'_operation'));
console.debug(itemname+" : MoveExpire: (Pre)Actual was: "+items[itemname+"_actual"].numericState);
console.debug(itemname+" : MoveExpire: Target is: "+items[itemname].numericState);
if(parseInt(cache.shared.get(itemname+'_nextpos')) == items[itemname].numericState)
{
items[itemname+"_actual"].sendCommand(parseInt(cache.shared.get(itemname+'_nextpos')).toString());
items[itemname+"_openRelay"].sendCommand("OFF");
items[itemname+"_closeRelay"].sendCommand("OFF");
cache.shared.put(itemname+'_operation',"IDLE");
console.debug(itemname+" : MoveExpire: Actual Position: "+parseInt(cache.shared.get(itemname+'_nextpos'))+" = Target Postion: "+items[itemname].numericState+" - Turning off Both Relays");
}
else
{
switch(cache.shared.get(itemname+'_operation'))
{
case "OPENING":
{
if(parseInt(cache.shared.get(itemname+'_nextpos')) >= items[itemname].numericState)
{
items[itemname+"_actual"].sendCommand(parseInt(cache.shared.get(itemname+'_nextpos')).toString());
console.debug(itemname+" : MoveExpire: OPENING complete. Actual Position: "+parseInt(cache.shared.get(itemname+'_nextpos'))+" >= Target Postion: "+items[itemname].numericState+" - Turn off OPEN Relay & Set State to IDLE and initiate NextOp");
items[itemname+"_openRelay"].sendCommand("OFF");
cache.shared.put(itemname+'_operation',"IDLE");
nextop(itemname);
}
else
{
items[itemname+"_actual"].sendCommand(parseInt(cache.shared.get(itemname+'_nextpos')).toString());
console.debug(itemname+" : MoveExpire: OPENING complete. Actual Position: "+parseInt(cache.shared.get(itemname+'_nextpos'))+" < Target Postion: "+items[itemname].numericState+" - Leave OPEN Relay ON & Set State to IDLE and initiate NextOp ");
cache.shared.put(itemname+'_operation',"IDLE");
nextop(itemname);
}
break;
}
case "CLOSING":
{
if(parseInt(cache.shared.get(itemname+'_nextpos')) <= items[itemname].numericState)
{
console.debug(itemname+" : MoveExpire: CLOSING complete. Actual Position "+parseInt(cache.shared.get(itemname+'_nextpos'))+" < Target Postion: "+items[itemname].numericState+" - Turn off CLOSE Relay & Set State to IDLE and initiate NextOp");
items[itemname+"_closeRelay"].sendCommand("OFF");
cache.shared.put(itemname+'_operation',"IDLE");
nextop(itemname);
}
else
{
console.debug(itemname+" : MoveExpire: CLOSING complete. Actual Position: "+parseInt(cache.shared.get(itemname+'_nextpos'))+" > Target Postion: "+items[itemname].numericState+" - Leave CLOSE Relay ON & Change State to IDLE and initiate NextOp");
cache.shared.put(itemname+'_operation',"IDLE");
nextop(itemname);
}
break;
}
case "CALIBRATE":
{
console.debug(itemname+" : MoveExpire: Calibration operation complete. Actual Position: "+parseInt(cache.shared.get(itemname+'_nextpos'))+" != Target Postion of "+items[itemname].numericState+" - Turn off both Relays & Set State to IDLE and initiate nextop");
items[itemname+"_openRelay"].sendCommand("OFF");
items[itemname+"_closeRelay"].sendCommand("OFF");
cache.shared.put(itemname+'_operation',"IDLE");
nextop(itemname);
break;
}
default:
{
console.error(itemname+" MoveExpire: Unknown Operation State: "+cache.shared.get(itemname+'_operation'));
break;
}
}
}
};
}
function nextop(itemname)
{
//Read actual position from associated item containing actual position
if(items[itemname+"_actual"].rawState instanceof UnDefType)
{
console.debug(items[itemname+"_actual"].name+" : NextOp: Position is NULL - Requires Calibration. Also check that this item has correct persistence settings (everychange and restoreonstartup )");
calibrate(itemname);
}
else
{
cache.shared.put(items[itemname].name+'_actual',items[itemname+"_actual"].numericState);
console.debug(itemname+" : NextOp: Actual Position (item) is "+items[itemname+"_actual"].numericState);
console.debug(itemname+" : NextOp: Current Operation is "+cache.shared.get(itemname+'_operation'));
//Read current operation from cache
operation = cache.shared.get(itemname+'_operation');
switch(operation)
{
case null:
{
console.debug(itemname+" : NextOp: Operation is NULL - Reading State from Relays");
//If the cache is NULL, figure out if an operation is currently in progress by reading the Opening & Closing Relay states
//If both relays are OFF then set operation to IDLE as OpenHAB was probably restarted, or rule was saved (thereby clearing the cache value) - No calibration should be required
if((items[itemname+"_openRelay"].state==="OFF") && (items[itemname+"_closeRelay"].state==="OFF"))
{
console.debug(itemname+" : NextOp: Operation Cache State initialising from Relay State (Both OFF): Currently IDLE");
cache.shared.put(itemname+'_operation',"IDLE");
}
else
{
//This section handles where an Operation was in progress, firstly by turning off both relays, and then triggering a calibration
items[itemname+"_openRelay"].sendCommand("OFF");
items[itemname+"_closeRelay"].sendCommand("OFF");
calibrate(itemname);
console.debug(itemname+" : NextOp: Operation Cache State initialising from Relay State Operation was in progress: Triggering Calibration");
}
break;
}
case "IDLE":
{
if(items[itemname+"_actual"].numericState == items[itemname].numericState)
{
console.debug(items[itemname].name+" : NextOp: Actual Position of "+items[itemname+"_actual"].numericState+" is equal to Target Postion of "+items[itemname].numericState+" - No Operation Initiated");
cache.shared.put(items[itemname].name+'_nextpos',items[itemname+"_actual"].state);
items[itemname+"_openRelay"].sendCommand("OFF"); //required for certain scenarios where called by MoveExpire
items[itemname+"_closeRelay"].sendCommand("OFF"); //required for certain scenarios where called by MoveExpire
}
else
if(items[itemname+"_actual"].numericState > items[itemname].numericState)
{
//Routine to handle vent closing
runtime = (parseInt(items[itemname].getMetadata("close_time").value) * ((items[itemname+"_actual"].numericState - items[itemname].numericState)/100));
schedtime= time.ZonedDateTime.now().plusSeconds(runtime);
console.debug(items[itemname].name+" : NextOp: Actual Position of "+items[itemname+"_actual"].numericState+" is greater than Target Postion of "+items[itemname].numericState+" - Closing Operation Initiated (or extended) for "+runtime+" Seconds");
cache.shared.put(itemname+'_operation',"CLOSING");
items[itemname+"_openRelay"].sendCommand("OFF");
items[itemname+"_closeRelay"].sendCommand("ON");
cache.shared.put(items[itemname].name+'_nextpos',items[itemname].state);
console.debug(itemname+" : NextOp: Now: "+time.ZonedDateTime.now());
console.debug(itemname+" : NextOp: Schedtime: "+schedtime);
timers.check(itemname+"_control",schedtime.toString(),moveexpire(itemname),true,null,itemname+"_control");
}
else
{
if(items[itemname+"_actual"].numericState < items[itemname].numericState)
{
//Routine to handle vent opening
runtime = (parseInt(items[itemname].getMetadata("open_time").value) * ((items[itemname].numericState - items[itemname+"_actual"].numericState)/100));
schedtime = time.ZonedDateTime.now().plusSeconds(runtime);
console.debug(items[itemname].name+" : NextOp: Actual Position: "+items[itemname+"_actual"].numericState+" < Target Postion: "+items[itemname].numericState+" - Opening Operation Initiated (or extended) for "+runtime+" Seconds");
cache.shared.put(itemname+'_operation',"OPENING");
items[itemname+"_closeRelay"].sendCommand("OFF");
items[itemname+"_openRelay"].sendCommand("ON");
cache.shared.put(items[itemname].name+'_nextpos',items[itemname].numericState);
console.debug(itemname+" : NextOp: Now: "+time.ZonedDateTime.now());
console.debug(itemname+" : NextOp: Schedtime: "+schedtime);
timers.check(itemname+"_control",schedtime.toString(),moveexpire(itemname),true,null,itemname+"_control");
}
}
break;
}
case "OPENING":
{
console.debug(items[itemname].name+" : NextOp: Opening Actuation currently in Progress to nextpos: "+cache.shared.get(itemname+'_nextpos'));
break;
}
case "CLOSING":
{
console.debug(items[itemname].name+" : NextOp: Closing Actuation currently in Progress to nextpos: "+cache.shared.get(itemname+'_nextpos'));
break;
}
case "CALIBRATE":
{
console.debug(items[itemname].name+" : NextOp: Currently calibrating - nextop will be rerun on expire");
break;
}
default:
{
console.error(items[itemname].name+" : NextOp: Unknown Operation State: "+cache.shared.get(itemname+'_operation'));
break;
}
}
}
}
nextop(items[event.itemName].name);
}
And just for completeness, the debug output for some basic operations
2024-08-30 16:07:24.231 [DEBUG] [on.script.ui.greenhouse_vent_control] - Position control rule triggered by greenhouse_vent_east
2024-08-30 16:07:24.231 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : Init: NextPos was: 30
2024-08-30 16:07:24.231 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : Init: Operation was: IDLE
2024-08-30 16:07:24.232 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : Init: ActPos (cache) was: 40
2024-08-30 16:07:24.232 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp : Open Time = 10
2024-08-30 16:07:24.232 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp : Close Time = 10
2024-08-30 16:07:24.233 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp : Calibrate Time = 15
2024-08-30 16:07:24.233 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Actual Position (item) is 30
2024-08-30 16:07:24.233 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Current Operation is IDLE
2024-08-30 16:07:24.234 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Actual Position: 30 < Target Postion: 40 - Opening Operation Initiated (or extended) for 1 Seconds
2024-08-30 16:07:24.234 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Now: 2024-08-30T16:07:24.233+12:00[SYSTEM]
2024-08-30 16:07:24.235 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Schedtime: 2024-08-30T16:07:25.233+12:00[SYSTEM]
2024-08-30 16:07:25.233 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : MoveExpire: NextPos was: 40
2024-08-30 16:07:25.234 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : MoveExpire: Current Operation is OPENING
2024-08-30 16:07:25.234 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : MoveExpire: (Pre)Actual was: 30
2024-08-30 16:07:25.234 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : MoveExpire: Target is: 40
2024-08-30 16:07:25.235 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : MoveExpire: Actual Position: 40 = Target Postion: 40 - Turning off Both Relays
2024-08-30 16:07:43.971 [DEBUG] [on.script.ui.greenhouse_vent_control] - Position control rule triggered by greenhouse_vent_east
2024-08-30 16:07:43.972 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : Init: NextPos was: 40
2024-08-30 16:07:43.972 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : Init: Operation was: IDLE
2024-08-30 16:07:43.972 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : Init: ActPos (cache) was: 30
2024-08-30 16:07:43.973 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp : Open Time = 10
2024-08-30 16:07:43.973 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp : Close Time = 10
2024-08-30 16:07:43.973 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp : Calibrate Time = 15
2024-08-30 16:07:43.973 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Actual Position (item) is 40
2024-08-30 16:07:43.973 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Current Operation is IDLE
2024-08-30 16:07:43.974 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Actual Position: 40 < Target Postion: 60 - Opening Operation Initiated (or extended) for 2 Seconds
2024-08-30 16:07:43.974 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Now: 2024-08-30T16:07:43.974+12:00[SYSTEM]
2024-08-30 16:07:43.975 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Schedtime: 2024-08-30T16:07:45.973+12:00[SYSTEM]
2024-08-30 16:07:45.138 [DEBUG] [on.script.ui.greenhouse_vent_control] - Position control rule triggered by greenhouse_vent_east
2024-08-30 16:07:45.138 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : Init: NextPos was: 60
2024-08-30 16:07:45.139 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : Init: Operation was: OPENING
2024-08-30 16:07:45.139 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : Init: ActPos (cache) was: 40
2024-08-30 16:07:45.139 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp : Open Time = 10
2024-08-30 16:07:45.139 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp : Close Time = 10
2024-08-30 16:07:45.140 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp : Calibrate Time = 15
2024-08-30 16:07:45.140 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Actual Position (item) is 40
2024-08-30 16:07:45.140 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Current Operation is OPENING
2024-08-30 16:07:45.140 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Opening Actuation currently in Progress to nextpos: 60
2024-08-30 16:07:45.973 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : MoveExpire: NextPos was: 60
2024-08-30 16:07:45.974 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : MoveExpire: Current Operation is OPENING
2024-08-30 16:07:45.974 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : MoveExpire: (Pre)Actual was: 40
2024-08-30 16:07:45.974 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : MoveExpire: Target is: 70
2024-08-30 16:07:45.975 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : MoveExpire: OPENING complete. Actual Position: 60 < Target Postion: 70 - Leave OPEN Relay ON & Set State to IDLE and initiate NextOp
2024-08-30 16:07:45.975 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Actual Position (item) is 60
2024-08-30 16:07:45.976 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Current Operation is IDLE
2024-08-30 16:07:45.976 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Actual Position: 60 < Target Postion: 70 - Opening Operation Initiated (or extended) for 1 Seconds
2024-08-30 16:07:45.977 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Now: 2024-08-30T16:07:45.976+12:00[SYSTEM]
2024-08-30 16:07:45.977 [DEBUG] [on.script.ui.greenhouse_vent_control] - greenhouse_vent_east : NextOp: Schedtime: 2024-08-30T16:07:46.975+12:00[SYSTEM]
I have honestly looked at this rule for far too long now, refactored this many times, and just hope there is an easier route to take, than the one I have so far. But if not, any tips/tricks would also be welcome for further refactoring of the above… Cheers