Greenhouse Vents - Linear Actuator Control Rules

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 :slight_smile:

  • 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 :slight_smile: )

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

Some thoughts (without diving deep into the code):

  • The motor will most likely need more time to open the Window than to close it, so you will need different timing per direction.
  • I’m using tasmota devices to control “dumb” roller shutter motors which then will provide absolute positioning.
  • There is a “roller shutter position emulation” addon in the openHAB add-on store, maybe this will help to achieve a solution.

Hi Udo - Thanks for the reply.

Agree - I have 2 Meta-Data items hidden away on each of the target position (aka triggering) items:

items[event.itemName].getMetadata("open_time").value);
items[event.itemName].getMetadata("close_time").value);

I’m not really familiar with the Tasmota Devices, but I presume this is what is managing/maintaining the position on behalf of OpenHAB, instead of OpenHAB itself?

I have a fairly basic Modbus relay board from Ali-express connected to the Motors. It provides no additional logic/capability other than turning on/off the relays upon respective command from OpenHAB.


But I have it all wired up nicely already, so pretty keen to avoid any changes to the hardware:

I did look at that a while ago, in the hope that it would solve all my issues, but struggled to connect the dots between what it does, and my situation. Perhaps my understanding of how a rollershutter works is incorrect (and maybe that’s what I need to sort :slight_smile: - I have never used one previously).

But to distil the problem down into it’s most simple description, I need to turn a changed Target position for the Vent into either a corresponding activation of the up or down relay for a calculated duration, in order to reach the new target position.

If I read the description of the add-on, it looks like more of a profile which calculates the position of the roller-shutter, based on the duration of activation? Would be very very happy to find a way this could be applied to my problem…

Cheers - Glen

Let’s say the current position is 29 and it should be 57, then an absolute positioning is rollershutter.sendCommand(57).

The addon knows about the current position and does the calculation 57 - 29 = 28time = close_time * 28 / 100 → send DOWN → wait calculated time → send STOP → update current position to 57.

Position (by openHAB definition) is always percentage of “closedness” (so DOWN for full time will always result in 100 % closed and UP for full time will always result in 0 % closed)

So you’re creating a rollershutter Item like that:

Rollershutter GHWindow_1 "Greenhouse Window 1" {channel="modbus..."[profile="transform:ROLLERSHUTTERPOSITION", uptime=98, downtime=76, precision=5]}

where 98 is time to open the window from 100 to 0, 76 is the time to close the window from 0 to 100 and the controller will take care that the window will always move at least 5 % to ensure better precision.

Notation here is for .items files, but it’s the same with Main UI.

Of course, modbus has to use UP/DOWN/STOP as commands, but there are ways to circumvent other commands, i.e. send ON at channel 1 instead UP, send ON at channel 2 instead DOWN, send OFF on channel 1 or 2 - depending of last ON command - instead STOP, ensure that channel 1 and 2 never be ON at the same time.

Ok - Thanks so much for your help @Udo_Hartmann - The information you provided helped guide me through the setup of that Profile. Even though I had previously looked at the documentation, I just could not see how that would be applied to a bunch of simple modbus relay channels.

What is somewhat depressing is the amount of time I spent on getting the above JS Rule semi-working - I should have asked a lot earlier :slight_smile:

I have only prototyped and run tests against the modbus device, but using simple seems to be working as expected.

There is only one thing I do still need to figure - Turning off both relays (aka STOP) when it reaches either 0% or 100% (It leaves the relay on). Unless there is anything obvious you can think off, I guess I can just run a rule which sends a STOP command to the rollershutter Item at either of those points. Whilst there are limit switches in the linear motors, there is a small chance the opener arm springs or wind could move the motor a fraction from the limit, causing a bit of ‘bounce’, therefore de-energising the relays at either limit could be a useful additional protection.

I may have some questions as I go, but for now, feeling like I am back on track…

Anyway, for anyone else who trips up across this thread in future, in summary:

  • I created the Rollershutter item via the items file as per the post above (Unless it is available in Main UI - I couldn’t find the rollershutter profile values configuration?)
  • I created the following transform script and added it to one of the relay things, which the above rollershutter item was linked to
  • The item is then linked
    • This transform turns on the appropriate relay upon receiving the UP or DOWN command, and turns off the counterpart relay
    • The Coils in this device are at address ‘0’ & 1’ (for reference in the below script)
    • I have also allowed unmodified values to pass through the transformation as I have both a Rollershutter and switch channel/item linked to the Thing:
      • This, I assume is not usual practice, and I presume use of the switch would cause the profile loosing track of the position, however I have a specific use case, where I want to calibrate the Vent, and will use the Switch channel for this
      • A rule will run (say) every night (and perhaps every reboot), which will set the target position to fully closed via the Rollershutter, then once it reaches that position, turn on the CLOSE switch for say a further 5-10 seconds, to ensure that the position is aligned

I have quite a bit more testing to do, but its looking good so far.

// Wrap everything in a function
// variable "input" contains data passed by openHAB
(function(cmd) {
    var out = cmd;      // allow UNDEF to pass through, and potential values from other (non-rollershutter) channel links
    var openRelay = {"UP": 1,  "DOWN": 0, "STOP": 0};
    var closeRelay = {"UP": 0, "DOWN": 1, "STOP": 0};

    var closeRelayState = closeRelay[cmd];
    var openRelayState = openRelay[cmd];

    if(closeRelayState === undefined || openRelayState === undefined) {
        // unknown command, return original value ()
        return out;
    } else {
        return (
            "["
              + "{\"functionCode\": 5, \"address\":" + "0" + ", \"value\": [" + closeRelayState +  "] }"
            +","
              + "{\"functionCode\": 5, \"address\":" + "1" + ", \"value\": [" + openRelayState +  "] }"
            + "]"
        );
    }
})(input)

Thanks again - Cheers, Glen

My guess (I don’t use Modbus myself and it seems to be very not-straight-forward) is, there aren’t “real” rollershutter channels.
In question of an “auto off” function, if you have additional Switch Items already, you can use Item metadata expiration Timer. As text (also possible via Main UI → Item → Meta data)

Switch greenhouseWindow1Open   "open greenhouse windows 1" {channel="modbus:...", expire="100s,command=OFF"}
Switch greenhouseWindow1Close "close greenhouse windows 1" {channel="modbus:...", expire="95s,command=OFF"}

This will send an OFF command after 100 seconds for “UP” channel and the same after 95 seconds for the “DOWN” channel. The command is only sent if the state is different to the command.
Of course this will only work, if there is a real state for these Items (i.e. if triggering an UP or DOWN command via the rollershutter Item, the corresponding Switch Item also changes it’s state).
Maybe this is an option for the Rollershutter Item, too, but I’m not sure if this will work as intended:

Rollershutter GHWindow_1 "Greenhouse Window 1" {channel="modbus..."[profile="transform:ROLLERSHUTTERPOSITION", uptime=98, downtime=76, precision=5], expire="100s,command=STOP"[ignoreStateUpdates="true"]}

The profile is also configurable via Main UI, it’s in the link definition (available from channel configuration)

Understatement of the century - its definitely not straight forward at all!! :slight_smile:

Really struggling to find it - Other profiles do provide dialogue boxes when selected. I’m running OH4.2.1, and I suspect it may be fixed in the next version?:

Set it up via the items file for now anyway.

Actually - I tried both approaches, and they both seem to work within a quick prototype. However, I think I will lean towards using the ‘STOP’ expire with the rollershutter item, as I imagine the rollershutterposition profile will still then be aware of where it was stopped.
I am probably (over)thinking about a very unlikely series of events which could lead to an edge-case where it was not 0 or 100% when the expire triggered on the other switch items, if they were used instead of the Rollershutter item

Thanks again for your help, and I think I can now move on to the temperature control & wind protection rules, now that the vent position management looks like it may be sorted!!

Just reading this thread and thought I’d clarify that the reason the rollershutterposition does not send a stop at the full open/close position. At startup or if the rollershutter can also be controlled outside of openHAB, the profile does not know what position the rollershutter is in, so it is necessary to recalibrate to a known position. It wouldn’t be difficult to modify the transform to still send a STOP command after the full “uptime” or “downtime” has expired. This would still allow the position to be recalibrated and shouldn’t be an issue for existing uses. I don’t have time to do this now, but would welcome anyone else to make the change. Otherwise, I will try to add at sometime in the future.

Regards,
Jeff

2 Likes

Makes complete sense, and also covers my calibration needs nicely too. I have added the expire suggested by @Udo_Hartmann above, which provides a basic (additional) protection for the linear motor that I was looking for

That would be very much appreciated, if you find the time one day in the future - Would be a great addition to the add-on. Have opened a Github enhancement request for your consideration, and including a suggested approach for configuration (to avoid this being a breaking change):

If you saw my above attempt at writing this functionality into a JS Rule, you probably already know you don’t want me making this change to your code :slight_smile:

And thanks for the great work you have already done on this addon. It means I can throw my JS Positioning Rule attempt in the bin, and move on with finishing the greenhouse control.

Cheers - Glen