13. Smart Greenhouse - a real-world IoT application at scale

Background

In order to model a real-world IoT application or a solution it is imperative to distribute load. In Greenhouse example we have a total of 270+ Sensors and Actuators.

It is impractical to run entire sensors and actuators on one single node (either OnPrem or Cloud)

TQLEngine Deployment Matrix

TQLEngine

Lane#

Zone#Grid#DNS NameLanes CoveredZones CoveredGrids CoveredSimulated

TQLEngine-1

11-51-45ghsim.atomiton.com0.5545Yes

TQLEngine-2

16-1046-90ghsim2.atomiton.com0.5545Yes

TQLEngine-3

21-51-45ghsim3.atomiton.com0.5545Yes
TQLEngine-426-1046-90ghsim4.atomiton.com0.5545Yes
TQLEngine-531-51-45ghsim5.atomiton.com0.5545Yes
TQLEngine-636-101-44ghsim6.atomiton.com0.44.944Yes
TQLEngine-7359010.0.1.520.10.11No
TQLEngine-C1-31-51-270greenhouse.atomiton.com3 (Receive only)30 (Receive only)270 (Receive only)Cloud

Complete Greenhouse Model

Complete Greenhouse Model can be found here

Key Modeling Takeaways

Pipeline Macros

Pipeline Macros
<Macro Name="ExecuteQuery">
  <Argument>
    <QueryString>
      <Query/>
    </QueryString>
  </Argument>
  <Result>
    <OnRequest Target="[:RuntimeParams.FacetIDName:]" Disable="CMD_SERVER">
      <Process Return="CMD_NOP">
        <Message>
          <Value>
            [:$Macro.Argument.QueryString:]
          </Value>
        </Message>
      </Process>
    </OnRequest>
  </Result>
</Macro>
 
<Macro Name="ScheduleJob">
  <Argument>
    <ScheduleInterval>
    </ScheduleInterval>
    <StartTime>
      0
    </StartTime>
    <EndTime>
    </EndTime>
    <ActionCode>
    </ActionCode>
    <Name>
    </Name>
  </Argument>
  <Result>
    <Log Message="Inside ScheduleJob macro"/>
    <DoRequest target="[:RuntimeParams.SequenceFacetIDName:]">
      <Process>
        <Message>
          <Value>
            <Execute eid="[:$Macro.Argument.Name:]" schedule="[[:$Macro.Argument.StartTime:]..[:$Macro.Argument.EndTime:]/[:$Macro.Argument.ScheduleInterval:]]">
              <Action>
                <ExecuteQuery>
                  <QueryString>
                    [:$Macro.Argument.ActionCode:]
                  </QueryString>
                </ExecuteQuery>
              </Action>
            </Execute>
          </Value>
        </Message>
      </Process>
    </DoRequest>
  </Result>
</Macro>
<Macro Name="SubscribeToTQL">
	<Argument>
		<TopicName>TQL.*</TopicName>
		<TopicId>GenericTopicID</TopicId>
		<ActionName />
	</Argument>
	<Result>
		<DoRequest target="[:RuntimeParams.TopicFacetIDName:]">
			<Process>
				<Message type="xml">
					<Value>
						<Subscribe sid="[:$Macro.Argument.TopicId:]" topic="[:$Macro.Argument.TopicName:]">
							<Action>[:$Macro.Argument.ActionName:]</Action>
						</Subscribe>
					</Value>
				</Message>
			</Process>
		</DoRequest>
	</Result>
</Macro>

Start / Stop Simulation

Start and Stop Simulation
<Macro Name="StartSimulation">
  <Argument>
    <GreenhouseID/>
  </Argument>
  <Result>
    <ExecuteQuery>
      <QueryString>
        <Query>
          <Find format="Version">
            <Greenhouse>
              <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
            </Greenhouse>
          </Find>
          <SetResponseData key="Message.Value.Find.Result.Greenhouse.Running.value">
            <Value>True</Value>
          </SetResponseData>
          <Save>
            <from>Result</from>
            <Include>$Response.Message.Value.Find</Include>
          </Save>
        </Query>
      </QueryString>
    </ExecuteQuery>

    <ExecuteQuery>
      <QueryString>
        <Query>
          <Find format="Version">
            <ExtTempSensor>
              <sensorId ne=""/>
            </ExtTempSensor>
          </Find>
          <SetResponseData key="Message.Value.Find.Result.ExtTempSensor.simulated.value">
            <Value>True</Value>
          </SetResponseData>
          <Save>
            <from>Result</from>
            <Include>$Response.Message.Value.Find</Include>
          </Save>
        </Query>
      </QueryString>
    </ExecuteQuery>
    <ExecuteQuery>
      <QueryString>
        <Query>
          <Find format="Version">
            <GHTempSensor>
              <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
            </GHTempSensor>
          </Find>
          <SetResponseData key="Message.Value.Find.Result.GHTempSensor.simulated.value">
            <Value>True</Value>
          </SetResponseData>
          <Save>
            <from>Result</from>
            <Include>$Response.Message.Value.Find</Include>
          </Save>
        </Query>
      </QueryString>
    </ExecuteQuery>
    <ExecuteQuery>
      <QueryString>
        <Query>
          <Find format="Version">
            <ExtHumiditySensor>
              <sensorId ne=""/>
            </ExtHumiditySensor>
          </Find>
          <SetResponseData key="Message.Value.Find.Result.ExtHumiditySensor.simulated.value">
            <Value>True</Value>
          </SetResponseData>
          <Save>
            <from>Result</from>
            <Include>$Response.Message.Value.Find</Include>
          </Save>
        </Query>
      </QueryString>
    </ExecuteQuery>
    <ExecuteQuery>
      <QueryString>
        <Query>
          <Find format="Version">
            <GHHumiditySensor>
              <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
            </GHHumiditySensor>
          </Find>
          <SetResponseData key="Message.Value.Find.Result.GHHumiditySensor.simulated.value">
            <Value>True</Value>
          </SetResponseData>
          <Save>
            <from>Result</from>
            <Include>$Response.Message.Value.Find</Include>
          </Save>
        </Query>
      </QueryString>
    </ExecuteQuery>
    <ExecuteQuery>
      <QueryString>
        <Query>
          <Find format="Version">
            <ExtLightSensor>
              <sensorId ne=""/>
            </ExtLightSensor>
          </Find>
          <SetResponseData key="Message.Value.Find.Result.ExtLightSensor.simulated.value">
            <Value>True</Value>
          </SetResponseData>
          <Save>
            <from>Result</from>
            <Include>$Response.Message.Value.Find</Include>
          </Save>
        </Query>
      </QueryString>
    </ExecuteQuery>
    <SetSunnyDaysFlag>
        <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
    </SetSunnyDaysFlag>

    <ScheduleJob>
      <Name>
        SetSunnyDaysFlag
      </Name>
      <ScheduleInterval>
        1Day
      </ScheduleInterval>
      <ActionCode>
        <SetSunnyDaysFlag>
            <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
        </SetSunnyDaysFlag>
      </ActionCode>
    </ScheduleJob>

    <SetVentAndFansOnOffState>
      <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
    </SetVentAndFansOnOffState>
    <ScheduleJob>
      <Name>
        SetVentAndFansOnOffState
      </Name>
      <ScheduleInterval>
        5min
      </ScheduleInterval>
      <ActionCode>
        <SetVentAndFansOnOffState>
          <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
        </SetVentAndFansOnOffState>
      </ActionCode>
    </ScheduleJob>
    <UpdateGridAmbientLight>
      <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
    </UpdateGridAmbientLight>
    <ScheduleJob>
      <Name>
        UpdateGridAmbientLightValue
      </Name>
      <ScheduleInterval>
        5min
      </ScheduleInterval>
      <ActionCode>
        <UpdateGridAmbientLight>
          <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
        </UpdateGridAmbientLight>
      </ActionCode>
    </ScheduleJob>
    <UpdateZoneAvgLight>
      <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
    </UpdateZoneAvgLight>
    <ScheduleJob>
      <Name>
        UpdateZoneAvgLightValue
      </Name>
      <ScheduleInterval>
        5min
      </ScheduleInterval>
      <ActionCode>
        <UpdateZoneAvgLight>
          <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
        </UpdateZoneAvgLight>
      </ActionCode>
    </ScheduleJob>
    <UpdateGridSoilMoisture>
      <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
    </UpdateGridSoilMoisture>
    <ScheduleJob>
      <Name>
        UpdateGridSoilMoistureValue
      </Name>
      <ScheduleInterval>
        5min
      </ScheduleInterval>
      <ActionCode>
        <UpdateGridSoilMoisture>
          <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
        </UpdateGridSoilMoisture>
      </ActionCode>
    </ScheduleJob>

    <UpdateZoneAvgSoilMoisture>
      <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
    </UpdateZoneAvgSoilMoisture>
    <ScheduleJob>
      <Name>
        UpdateZoneAvgSoilMoistureValue
      </Name>
      <ScheduleInterval>
        5min
      </ScheduleInterval>
      <ActionCode>
        <UpdateZoneAvgSoilMoisture>
          <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
        </UpdateZoneAvgSoilMoisture>
      </ActionCode>
    </ScheduleJob>
    <CalculateZonesVPD>
      <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
    </CalculateZonesVPD>
    <ScheduleJob>
      <Name>
        CalculateZonesVPDValue
      </Name>
      <ScheduleInterval>
        5min
      </ScheduleInterval>
      <ActionCode>
        <CalculateZonesVPD>
          <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
        </CalculateZonesVPD>
      </ActionCode>
    </ScheduleJob>
  </Result>
</Macro>

Calculate VPD

Calculate VPD
<Macro Name="CalculateZonesVPD">
  <Argument>
    <GreenhouseID></GreenhouseID>
  </Argument>
  <Result>
  <GetLiveZone>
    <GreenhouseID>[:$Macro.Argument.GreenhouseID:]</GreenhouseID>
  </GetLiveZone>
    <if condition="$Response.Message/Value/Zone/count(ZoneID) gt 0">
      <then>
        <ExecuteQuery>
          <QueryString>
            <Query>
              <Find>
                <Zone>
                  <ZoneID>[:$Response.Message.Value.Zone.ZoneID:]</ZoneID>
                </Zone>
              </Find>
            </Query>
          </QueryString>
        </ExecuteQuery>
        <if condition="$Response.Message.Value/Find/Status eq 'Success'">
          <then>
            <for each="findRes" in="Find.Result" using="$ProcessData">
              <SetLocalData key="ZoneID" value="[:$ProcessData.findRes.Zone.ZoneID:]"/>
              <SetLocalData key="ZoneTemp" value="[:$ProcessData.findRes.Zone.ZoneTemperature:]"/>
              <SetLocalData key="ZoneHumidity" value="[:$ProcessData.findRes.Zone.ZoneHumidity:]"/>
              <JavaScript>
              var temperature = [:$LocalData.ZoneTemp:];
              var humidity = [:$LocalData.ZoneHumidity:];
              if(!isNaN(temperature) &amp;&amp; !isNaN(humidity)){
                   var A= -1.044*Math.pow(10,4);
                   var B= -1.129*10;
                   var C= -2.702*Math.pow(10,-2);
                   var D= 1.289*Math.pow(10,-5);
                   var E= -2.489*Math.pow(10,-9);
                   var F= 6.456;
                   var Vsat = Math.exp((A/temperature) + B + (C*temperature) + (D*Math.pow(temperature, 2)) + (E*Math.pow(temperature,3)) + (F*Math.log(temperature))) * 6894.7;
                   var Vair = Vsat * (humidity/100);
                   var Vpd = Vsat - Vair;
                  sffContext.execute("SetContextData","key","VPD","value",Vpd);
              }else{
                  sffContext.execute("SetContextData","key","VPD","value",0);
             }
  
               <!--  Saturation Vapor Pressure -->
                <!-- var es = 0.6108 * Math.exp(17.27 * [:$LocalData.ZoneTemp:] / ([:$LocalData.ZoneTemp:] + 237.3)); -->
                <!-- Actual Vapor Pressure -->
                <!-- var ea = ([:$LocalData.ZoneHumidity:] / 100) * es ; -->
                <!-- Vapor Pressure Deficit -->
                <!-- var vpd = ea - es; -->
                <!-- sffContext.execute("SetContextData","key","VPD","value",vpd); -->
              </JavaScript>
              <Log message="VPD for Zones =============== [:$ContextData.VPD:]"/>
              <ExecuteQuery>
                <QueryString>
                  <Query>
                    <Find format="Version">
                      <Zone>
                        <ZoneID eq='[:$LocalData.ZoneID:]'/>
                      </Zone>
                    </Find>
                    <SetResponseData>
                      <Key>Message.Value.Find.Result.Zone.VPD.Value</Key>
                      <Value>[:$ContextData.VPD:]</Value>
                    </SetResponseData>
                    <Save>
                      <from>Result</from>
                      <Include>$Response.Message.Value.Find</Include>
                    </Save>
                  </Query>
                </QueryString>
              </ExecuteQuery>
            </for>
          </then>
        </if>
    </then>
    </if>
  </Result>
</Macro>

Queries Used by User Interface

Query NameQuery 
 
Pipeline Macros
<Query>
  <Find>
    <Greenhouse>
      <GreenhouseID ne=""/>
    </Greenhouse>
  </Find>
</Query>
 
  • Greenhouse
  • All Lanes
  • Each Lane search for first zone to get crop type
  • Do Sum of all distinct crop types
 
Pipeline Macros
<Query>
  <GetIrrigationMotorList>
    <GreenhouseID>{0}</GreenhouseID>
  </GetIrrigationMotorList>
</Query>
 
Pipeline Macros
<Query>
  <GetHeaterList>
    <GreenhouseID>{0}</GreenhouseID>
  </GetHeaterList>
</Query>
 
Pipeline Macros
Query>
  <Find>
    <Greenhouse>
      <GreenhouseID ne=""/>
    </Greenhouse>
  </Find>
</Query>
 
  • Greenhouse
  • All Lanes
  • All Zones in each Lane
 
  • Part of Zone
 

Zone details <Find><Heater><ZoneID>{0}</ZoneID></Heater></Find>

 
Pipeline Macros
<Query><Find><Grid><ZoneID eq="{0}" /></Grid></Find></Query>
 
Pipeline Macros
<Query>
  <Find>
    <LightModel>
      <GridID>{0}</GridID>
    </LightModel>
  </Find>
</Query>
 
Pipeline Macros
<Query>
  <Find format="All">
    <IrrigationMotor>
      <ZoneID>{0}</ZoneID>
    </IrrigationMotor>
  </Find>
</Query>
 
Pipeline Macros
<Query>
<UpdateIrrigationMotorState>
    <ZoneID>{0}</ZoneID>
    <OnOffState>{1}</OnOffState>
 </UpdateIrrigationMotorState>
</Query>
 
Pipeline Macros
<Query>
  <UpdateHeaterState>
    <ZoneID>{0}</ZoneID>
    <OnOffState>{1}</OnOffState>
    <HeatingLevel>{2}</HeatingLevel>
  </UpdateHeaterState>
</Query>
 
Pipeline Macros
<Query>
  <Find>
    <TempSensor>
      <ZoneID eq="{0}"/>
    </TempSensor>
    <HumiditySensor>
      <ZoneID eq="{0}"/>
    </HumiditySensor>
    <Camera>
      <ZoneID eq="{0}"/>
    </Camera>
    <IrrigationMotor>
      <ZoneID eq="{0}"/>
    </IrrigationMotor>
    <Heater>
      <ZoneID eq="{0}"/>
    </Heater>
  </Find>
</Query>
 
Pipeline Macros
<Query>
  <Find>
    <SoilSensorModel>
      <GridID eq="{0}"/>
    </SoilSensorModel>
    <LightModel>
      <GridID eq="{0}"/>
    </LightModel>
    <AmbientLightSensorModel>
      <GridID eq="{0}"/>
   </AmbientLightSensorModel>
  </Find>
</Query>
 
Pipeline Macros
<Query>
  <Find format="All">
    <GHTempSensorCurrentTemp>
      <GreenhouseID>{0}</GreenhouseID>
   </GHTempSensorCurrentTemp>
  </Find>
</Query>
 
Pipeline Macros
<Query>
  <Find format="All">   <GHHumiditySensorCurrentHumidity>
      <GreenhouseID>{0}</GreenhouseID>    </GHHumiditySensorCurrentHumidity>
  </Find>
</Query>
 
Pipeline Macros
<Query>
  <GetHeaterList>
    <GreenhousID>{0}</GreenhouseID>
  </GetHeaterList>
</Query>
<Query>
  <GetIrrigationMotorList>
    <GreenhouseID>{0}</GreenhouseID>
  </GetIrrigationMotorList>
</Query>
 
  • Greenhouse
  • All Lanes
  • All Zones in each Lane