.. |Bullet| image:: S:/uniplot-obj/buttons/Bullet.png :align: top .. highlightlang:: us .. index:: IoT applied to AirZone - cURL, REST API and SQL .. _IoTAirZone: IoT applied to AirZone ====================== **Internet Of Thing** (IoT) is the capacity of the electronic that surrounds us to interact and make our life easier. The UniPlot use case presented below will give an extensive example of how to use UniPlot's feature in this framework while navigating through. |Bullet| :ref:`cURL requests` |Bullet| REST API |Bullet| :ref:`SQL DataBase` |Bullet| :ref:`Services in UniPlot` .. note:: The whole code and template for the use case is available in the sample directory :file:`UniPlot\\Samples\\IoT` * AirZone1.ic: the code * AirZone.ipz: the template 1. Why Controlling an A/C system with UniPlot --------------------------------------------- In this example there is a flat where the AirZone system is installed to allow the user to control how the Air Conditionning will heat-up or cool each individual room. 1.1 The system ^^^^^^^^^^^^^^ A representation of this system is shown below. Each output of the conducts are equipped with an electrical motor controlled grid that is managed by the AirZone system based on the analysis of each room temperature. The main machine blows air at a given temperature, and each grid will open or close itself to regulate the temperature of the rooms. Indeed each room is equipped with a thermostat where a specific target temperature can be chosen for the room. .. image:: S:/uniplot-obj/images/airzone-housesystem.* This system is meant to be working on its own but it does not provide any history of the system's state over the time. .. _uniplot_and_airzone: 1.2 UniPlot and AirZone ^^^^^^^^^^^^^^^^^^^^^^^ AirZone provides the user with the possibility to interact with the system thanks to a RESTful API. UniPlot can then interact with this API to: |Bullet| Get data from it (temperature, set point, ON/OFF...) using the **POST** method: POST http://XXX.XXX.XXX.XX:3000/api/v1/hvac With the following body: .. code-block:: none { "systemID": n (system number), "zoneID": m (zone number) } |Bullet| Send temperature set-points via an home-made user interface using the **PUT** method: PUT http://XXX.XXX.XXX.XX:3000/api/v1/hvac With the following body: .. code-block:: none { "systemID": n (system number), "zoneID": m (zone number), "parameter" (parameter to modify, e.g "setpoint"): f (value) } As we can get data from the API, we will store it in an SQL database to be able to plot all the data over a period of time and do statistics and system analysis. 2. Discuss with the API: ``AirZone_curl_POST()`` ------------------------------------------------ 2.1 The cURL POST method ^^^^^^^^^^^^^^^^^^^^^^^^ First things first, we need to contact the API to get some information. UniPlot comes with the :ref:`cURL library ` that will help us create our request. :: // Initialize the request curl = curl_easy_init(); o = [. cnt = 0, data = [.]]; // Preparing options oOptions = [. URL = "http://" + IPCASA + ":3000/api/v1/hvac", COPYPOSTFIELDS = "{\"systemID\":0,\"zoneID\":" + ltostr(key) + "}", HTTPHEADER = ["charset: utf-8"; "Accept: application/json"; "Content-Type: application/json"], WRITEFUNCTION = "_DownloadCBData", CURLOPT_WRITEDATA = o]; // Setting options curl_easy_setopt(curl, oOptions); // Send the request r = curl_easy_perform(curl); As one can see above, creating a request is done in 3 steps. |Bullet| **Initialize the request** :: curl = curl_easy_init(); |Bullet| **Preparing the options** :: oOptions = [. URL = "http://" + IPCASA + ":3000/api/v1/hvac", COPYPOSTFIELDS = "{\"systemID\":0,\"zoneID\":" + ltostr(key) + "}", HTTPHEADER = ["charset: utf-8"; "Accept: application/json"; "Content-Type: application/json"], WRITEFUNCTION = "_DownloadCBData", CURLOPT_WRITEDATA = o]; This is the trickiest part where all the important information goes. Each line is a flag that corresponds to a specific action in our request. Some of them will always be present and some will depend on what you need to achieve. All the flags are explained here: https://curl.se/libcurl/c/curl_easy_setopt.html * ``URL``: the URL where the request is sent. It will always be present * ``COPYPOSTFIELDS``: This is the body needed in the request which holds the information of the system we want to know the status of. * ``HTTPHEADER``: It gives a description of what we send (here JSON encoded in utf-8). * ``WRITEFUNCTION``: It is a call back function used to store the answer received from the API. * ``CURLOPT_WRITEDATA``: It tells the previous callback function what to use to store the data ( here the object o). |Bullet| **Linking the options to the curl request**:: curl_easy_setopt(curl, oOptions); |Bullet| **Executing the request**:: r = curl_easy_perform(curl); 2.2 Receiving the answer ^^^^^^^^^^^^^^^^^^^^^^^^ The following code will check whether an answer was received or not and then transform the information to use it in UniPlot :: if (r != 0) { return curl_easy_strerror(r); // Return a possible error message } else { bool = curl_easy_cleanup(curl); // Needed to close the curl s = ""; if (o.cnt == 0) { return s; } for (i in 1:o.cnt) { // Unpack the data stored by the callback function s = s + mem_unpack(o.data[i]); } s_txt = utf8_decode(s); print(s_txt); svDataName = AirZonesvDataName(); // Vector of human readable room names. systemID[0].zoneID[key] = [.]; // We store the information in a UniScript object for (ssName in svDataName) { s_txt_pos = strfind(s_txt, "\"" + ssName + "\""); if (s_txt_pos != 0) { s_txt_cut = strextract(s_txt, s_txt_pos + strlen("\"" + ssName + "\"") + 1); val = strtok(s_txt_cut, ",")[1]; systemID[0].zoneID[key][ssName] = strtrim(strtok(s_txt_cut, ",")[1]); } } } The output of this function gives the following object to be used in UniPlot :: [. = [. zoneID = [. [1] = [. air_demand = "0", coldStage = "1", coldStages = "1", errors = "[]", floor_demand = "0", heatStage = "1", heatStages = "1", humidity = "65", maxTemp = "27", minTemp = "15", mode = "3", name = "\"Salon\"", on = "0", roomTemp = "21.600000381469727", setpoint = "22.5", systemID = "1", units = "0", zoneID = "1", ], [2] = [. air_demand = "0", coldStage = "1", coldStages = "1", errors = "[]", floor_demand = "0", heatStage = "1", heatStages = "1", humidity = "74", maxTemp = "27", minTemp = "15", mode = "3", name = "\"Hab.Matrimo\"", on = "0", roomTemp = "21", setpoint = "18", systemID = "1", units = "0", zoneID = "2", ], 3. Store the data: ``AirZone_DB()`` ----------------------------------- Now that we can contact the API and get the information of our system, let's store it in an sql database to decide later on what to do with it. |Bullet| We start by creating the database :: db = sqlite3_open(ssSrcDir + "/AirZone.db"); |Bullet| We then create the tables needed if they do not exist :: for (zone in zones) { sqlite3_execute(db, "create table if not exists " + zone ... + "(" + svValName[1] + " varchar(100), " ... + svValName[2] + " varchar(100), " ... + svValName[3] + " varchar(100), " ... + svValName[4] + " varchar(100), " ... + svValName[5] + " varchar(100));"); } b = sqlite3_execute(db, "create table if not exists TimeStamp(" ... + "TimeStamp varchar(100), " ... + "Year varchar(100), " ... + "Month varchar(100), "... + "Day varchar(100), ... + "YearMonthDay varchar(100));"); } In this example we create 1 table per room and each table will hold 5 information: * TimeStamp * Room Temperature * Room Temperature Setpoint * Room Regulation state (ON/OFF) * Room Air Demande (blowing or not) Next to these tables we create another one to store date information under different form: * TimeStamp * Year * Month * Day * YearMonthDay |Bullet| Then we call the function created earlier to get a set of fresh data and also inquire the actual time stamp :: Temperatures = AirZone_curl_POST(); t = DT_GetCurrentTime(); |Bullet| Next step is to store the information in the tables :: for (zone in 1:len(zones)) { obj = Temperatures[0].zoneID[zone]; sqlite3_execute(db, "insert into " + zones[zone] + " values("+ sprintf("%.4f", t) + "," ... + obj.roomTemp + "," ... + obj.setpoint + "," ... + obj.on + "," ... + obj.air_demand+ ");"); } |Bullet| And for the time stamps :: ssYear = DT_Format(t, "20%y"); ssMonth = DT_Format(t, "%m"); ssDay = DT_Format(t, "%d"); ssYearMonthDay = "'" + DT_Format(t, "20%y.%m.%d") + "'"; request = "insert into TimeStamp values("+ sprintf("%.4f", t) + "," + ssYear + "," + ssMonth + "," + ssDay + "," + ssYearMonthDay + ");"; sqlite3_execute(db, request); |Bullet| Finally we close properly the database :: db = sqlite3_close(db); 3. Let UniPlot work: ``AirZone_DB_service_ON()`` ------------------------------------------------ We have created a structure able to call a REST API using curl library, fetch some information on our system and store it in a database. We now want to gather data for a given period to be able to analyze how our system works. To do so UniPlot can create a kind of service called **Timer**. The function ``AirZone_DB_service_ON()`` is in charge of creating this timer for us :: def AirZone_DB_service_ON() { _g().AirZone = [.]; _g().AirZone["Timer"] = AppNewTimer("AirZone_DB", AirZone_dtUpdate * 1000); log_warning("uniscript", "AirZone", "AirZone Service ON (Refresh rate: " + ltostr(AirZone_dtUpdate) + "s)"); } We use the global object ``_g()`` to store a dedicated object ``AirZone`` to which we assign the identifier of the timer :: _g().AirZone["Timer"] = AppNewTimer("AirZone_DB", AirZone_dtUpdate * 1000); When this function is executed, the function AirZoneDB will be called every ``AirZone_dtUpdate`` seconds. To Stop it we also created the following function which kills the timer by getting its identifier thank to the object created earlier :: def AirZone_DB_service_OFF() { AppKillTimer(_g().AirZone["Timer"]); log_warning("uniscript", "AirZone", "AirZone Service OFF"); } 4. Time to use the data ----------------------- Now that we have setup our database and that it stores at a given frequency the state of the AC controller and the flat temperature, we want to make good use of this data and plot it. In the following sections we will see a possible way to plot the data into an existing template via a small interface and how to use UniPlot features to live update the diagrams. 4.1 Plot it: ``AirZone_FetchAndPlot(bWatch, DateMin, DateMax)`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This function will accept 3 parameters: * ``bWatch``: Use for continuous plotting. We will come back to it later on. * ``DateMin, DateMax``: It is the range of data we want to plot. The data will be plotted in the following template. .. image:: S:/uniplot-obj/images/airzone-template.* |Bullet| To start we must open a template prepared to receive the data :: // Open the template if (bWatch == 0) { ssName = strcat(SplitPath(source(stack(0))), ""); ssSrcDir = sum(SplitPath(ssName)[1, 2]); hPage = auto_LoadTemplate(ssSrcDir+"\\AirZone.ipz"); _g().Airzone = [. "ssSrcDir" = ssSrcDir, ... "hPage" = hPage]; print(_g().Airzone); } else { ssSrcDir = _g().Airzone["ssSrcDir"]; hPage = _g().Airzone["hPage"]; } |Bullet| Then we gather the dataset handles to be updated and store it into an object to make the interaction easier. Each dataset has a name in the template. So we store the dataset handle in a key that has the dataset name. :: // Prepare data replacement //hvPage = DocGetAllPages(hDoc, FALSE, FALSE); hvData = PageGetAllDatasets(hPage); o_Assignment = [.] for (i in 1:len(hvData)) { o_Assignment[ObjGetName(hvData[i])] = hvData[i]; } |Bullet| Time to interrogate the database to fetch the data. **Request creation** We create the correct requests for each zone using SQL syntax :: "SELECT TimeStamp.TimeStamp, RoomTemp, SetPoint, AcOn, AirDemande "... // We select the columns to extract + "FROM TimeStamp "... // In the main TimeStamp table + "INNER JOIN " + zone + " " ... // Making an inner join with the table corresponding to the zone we analyze + "ON TimeStamp.TimeStamp = " + zone + ".TimeStamp " ... // using TimeStamp as index + " WHERE TimeStamp.YearMonthDay >= " + DateMin + " AND TimeStamp.YearMonthDay <= " + DateMax ... // only within the requested time frame + ";"; The code looks like this :: ssName = strcat(SplitPath(source( stack(0))), ""); db = sqlite3_open(ssSrcDir + "/AirZone.db"); o = [.]; for (zone in zones) { if (bWatch == 0) { request = "SELECT TimeStamp.TimeStamp, RoomTemp, SetPoint, AcOn, AirDemande "... + "FROM TimeStamp "... + "INNER JOIN " + zone + " " ... + "ON TimeStamp.TimeStamp = " + zone + ".TimeStamp " ... + " WHERE TimeStamp.YearMonthDay >= " + DateMin + " AND TimeStamp.YearMonthDay <= " + DateMax ... + ";"; } else { request = "SELECT TimeStamp.TimeStamp, RoomTemp, SetPoint, AcOn, AirDemande "... + "FROM TimeStamp "... + "INNER JOIN " + zone + " " ... + "ON TimeStamp.TimeStamp = " + zone + ".TimeStamp " ... + "WHERE TimeStamp.TimeStamp=(SELECT max(TimeStamp.TimeStamp) FROM TimeStamp);"; } } **Request execution** We ask UniPlot to execute the request and store the output in ``smTimeStamps`` :: smTimeStamps = sqlite3_execute(db, request); **Replacing the data** That data has to be assigned to its dataset. As shown below, each dataset has been given a name which is the concatenation of the zone name (office, bedroom etc...) and the variable (Temperature, Set-point, ...). .. image:: S:/uniplot-obj/images/airzone-template-datasetlist.* The code will create an object whose keys are the name of the dataset and the values the handles :: for (idx in 2:len(svValName)) { hData = o_Assignment[zone + "_" + svValName[idx]]; rvX = strtod(smTimeStamps[;1]); rvY = strtod(smTimeStamps[;idx]); if (bWatch == 1) { rmXY = XYGetData(hData); rvX = [rmXY[;1];rvX]; rvY = [rmXY[;2];rvY]; } bool = XYSetData(hData, rvX, rvY); } **Closing the database** :: bool = sqlite3_close(db); **Updating the template** :: PageReplot(hPage); log_info("uniscript", "AirZone", "AirZone Plot update"); After all these efforts we should get the following output. .. image:: S:/uniplot-obj/images/airzone-template_filled.* 4.2 Let's make it user friendly: ``AirZone_Interface()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``AirZone_FetchAndPlot(bWatch, DateMin, DateMax)`` function mentionned in the previous chapter would benefit being called by an interface where the user can select start and end dates to be plotted. This is what the ``AirZone_Interface()`` function is about. .. image:: S:/uniplot-obj/images/airzone-interface_main_date.* The interface creation is built using the following philosophy. As one can see, it displays the months where data is available. To do so we need to interrogate the database get this information. The we format it properly and create the dialog template. Then the dialog is launched, the user select the period he wants to analyze. With this information, the ``AirZone_FetchAndPlot`` function is called. |Bullet| Let's retrieve the database and store its identifier in **db** :: // Database // Opening and creation if needed ssName = strcat(SplitPath(source( stack(0))), ""); ssSrcDir = sum(SplitPath(ssName)[1, 2]); db_file = ssSrcDir + "AirZone.db"; db = sqlite3_open(db_file); |Bullet| We then execute an SQL call onto ``db`` to retrieve all the time stamp information and close the database. :: smTimeStamps = sqlite3_execute(db, "select * from TimeStamp;"); bool = sqlite3_close(db); |Bullet| A bit of formating of the ``smTimeStamps`` string matrix lead us to have a usable :ref:`ListBoxText` usable in the init string of the dialog box. :: svYearMonth = ST_set(DT_Format(strtod(smTimeStamps[;1]), "20%y.%m")); svYearMonth_List = CreateListBoxText(svYearMonth, 2); svYearMonthDay = ST_set(DT_Format(strtod(smTimeStamps[;1]), "%x")); svYearMonthDay_List = CreateListBoxText(svYearMonthDay, svYearMonthDay[1]); |Bullet| To create our dialog box we will now prepare the init string and the template. If you have a doubt on how it works you can read the :ref:`DialogBox documentation`. :: // Initialization string svInitString = ["Select date: ", svYearMonth_List, "0"]; //svYearMonthDay_List, "0"] // Creation of template svTemplate = ["Select the range of time to plot.", ... "|G10 |", ... "|M10 |", ... strempty(1, 10), ... "|BActivate live plotting|"]; |Bullet| The dialog box ready, we launch it using the ``DialogBox`` function and store the output in ``DateMin`` and ``DateMax``. Also notice the ``bContinuousPlot`` variable, I will mention it later on. :: // Launch DialogBox svOutput = DialogBox(svTemplate, svInitString, "Airzone Interface"); if (svOutput[1] == "DLG_CANCEL") { return 0; } = GetListBoxText(svOutput[2]); DateMin = "'" + svList[1] + ".01'"; DateMax = "'" + svList[len(svList)] + ".31'"; bContinousPlot = strtol(svOutput[3]); |Bullet| Time to finally call our FetchAndPlot function. And the template will be populated with the desired data. :: AirZone_FetchAndPlot(0, DateMin, DateMax); .. image:: S:/uniplot-obj/images/airzone-template_filled.* 4.3 Continuous Plotting: ``FileWatchAdd()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ We now have two sources of data to analyze. |Bullet| Direct reading from the API |Bullet| Our SQL database with our recordings To do so we created a simple interface which aim is to show the user the data available and to plot it as seen in the previous chapter. In this earlier part I mentionned the ``bContinuousPlot`` which the user can set to 1 by clicking on the related checkbox. This check box will enable an automatic update of the template when new data is available. .. image:: S:/uniplot-obj/images/airzone-interface_activate_live_plotting.* If set to 1, in the ``AirZone_Interface()`` the ``FileWatchAdd`` function is called along with the callback function ``AirZone_FetchAndPlot_WATCH()``. :: // Setting up continous plotting if (bContinousPlot) { bool = FileWatchAdd(db_file, "AirZone_FetchAndPlot_WATCH", 1); } The aim here is to tell UniPlot to check whenever the database is modified (so a new set of data has been stored) and to call the function ``AirZone_FetchAndPlot_WATCH``. Which in its turn will call ``AirZone_FetchAndPlot`` whith the boolean ``bWatch`` set at 1. :: def AirZone_FetchAndPlot_WATCH(rsFile, bWatch) { // This function has been created to support the FileWatch which has only 1 argument AirZone_FetchAndPlot(bWatch); } We saw earlier that the ``bWatch`` variable changes the code behavior. As here we want to activate a continuous plotting, this variable has 2 effects : * It retrieves the template actually in use which is stored in ``_g().Airzone`` to start updating it instead of creating a new one. :: ssSrcDir = _g().Airzone["ssSrcDir"]; hPage = _g().Airzone["hPage"]; * If modifies the SQL request to retrieve all the data available up to the actual time. :: request = "SELECT TimeStamp.TimeStamp, RoomTemp, SetPoint, AcOn, AirDemande "... + "FROM TimeStamp "... + "INNER JOIN " + zone + " " ... + "ON TimeStamp.TimeStamp = " + zone + ".TimeStamp " ... + "WHERE TimeStamp.TimeStamp=(SELECT max(TimeStamp.TimeStamp) FROM TimeStamp);" The rest of the function is used as previously to plot the updated graph. 5. Control the A/C AirZone system with UniPlot ---------------------------------------------- I mentionned in the :ref:`UniPlot and AirZone` chapter that the API offers two methods. * *POST* to read info from the system * *PUT* to send control request to the system. Thanks to this I will now empower my template with controls (buttons) that can switch ON and OFF my A/C and even set the desired temperature! .. image:: S:/uniplot-obj/images/airzone-template_control_main.* To do so, each button of my template will be assigned a callback function and specific parameters depending on the need. 5.1 Adding the callback to our button ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To add a callback function to a textbox, one has to right-click on it and select Properties in the context menu. Then go to the Properties tab. In this tab: * Check *Display as a button* * Check ``OnClick``-Callback-Name * Add a callback function name as follows ``FunctioName?Param1,Param2,Param3`` .. image:: S:/uniplot-obj/images/airzone-button-callback.* .. code-block:: none AirZone_curl_PUT?1,setpoint,1 The string placed in the callback function name will be parsed in the function afterward. 5.2 The callback function ``AirZone_curl_PUT()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. function:: bool = AirZone_curl_PUT(_zoneID, _action, _val) .. us.return **Return Value** *bool* is TRUE (1) if no error happened during the process. The function receives 3 arguments .. us.params **Parameters** .. uparam:: _zoneID *_zoneID* is the zone we want to act on. .. uparam:: _action *_action* is the action that will be executed. .. uparam:: _val *_val* is the value is for the action. For example the temperature setpoint. The parameters can come from the UniScript call where they are explicitely given, or from the Callback function name as explained earlier. |Bullet| Parse the arguments hTB is the handle of the textbox and with this parameter and the function ``ObjectGetCallbackFunctionName`` we get the name of the callback function that we parse using the ``strtok`` function. We then store only the parameters as a vector in ``sv``. :: hTB = _g()._handle(); ssFuncName = ObjectGetCallbackFunctionName(hTB, "ObjectClickCallback"); sv = strtok(ssFuncName, "?"); // sv = FunctioName?Param1,Param2,Param3 // sv[1] --> function names // sv[2] --> coma separated arguments if (len(sv) == 2) { sv = strtok(sv[2], ", "); } else { log_warning("uniscript", "Check parameter input for AirZone request -> function?zoneID,action,val"); return 0; } |Bullet| Store the arguments If the function is called without arguments we use what was coming from the ``sv`` variable, otherwise we use the arguments. :: if (nargsin() == 0) { // Sub Call if (len(sv) == 3) { zoneID = sv[1]; action = sv[2]; val = sv[3]; } else { log_warning("uniscript", "Check parameter input for AirZone request -> ?zoneID,action,val"); return 0; } } else if (nargsin() == 3) { zoneID = _zoneID; action = _action; val = _val; } |Bullet| Setpoint preparation If the user clicked the *+* or *-* action from our control, we need to know the actual temperature setpoint to add or remove a degree from it and later on send it to the A/C system. We also use the occasion to switch on the zone sending ``AirZone_curl_PUT(zoneID, "on", "1")``. :: // Request preparation if action is temperature setpoint if (action == "setpoint") { setpoint = strtod(AirZone_curl_POST(strtol(zoneID), "setpoint")); if (val == "1") val = setpoint + 1 else val = setpoint - 1; val = ltostr(val); AirZone_curl_PUT(zoneID, "on", "1") } |Bullet| Sending the request The following code will send the PUT request to the system with the correct zone, action and value to the system. :: // Curl request creation curl = curl_easy_init(); data = "{\"systemID\":0,\"zoneID\":4}" o = [. cnt = 0, data = [.]]; // Preparing options oOptions = [. URL = "http://"+IPCASA+":3000/api/v1/hvac", CUSTOMREQUEST = "PUT", COPYPOSTFIELDS = "{\"systemID\":0,\"zoneID\":"+zoneID+",\"" + action + "\":" + val + "}", HTTPHEADER = ["charset: utf-8";"Accept: application/json";"Content-Type: application/json"]]; // Setting options curl_easy_setopt(curl, oOptions); r = curl_easy_perform(curl); //curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, NULL) // To avoid further requests to be set as PUT if (r != 0) { return curl_easy_strerror(r); } else { bool = curl_easy_cleanup(curl); // Setting up format __FF_FieldUpdate(0, 0, 0, 0, FFU_ONDATACHANGE); b=PageReplot(GetParent(GetParent(hTB))); return 1; } 5.3 Airzone field function ^^^^^^^^^^^^^^^^^^^^^^^^^^ To close the loop, if you open the template AirZone.ipz and check one of the text field to display the temperature and its setpoint that I use a field function that uses everything we created up to know to display a numeric information on the state of our system. .. image:: S:/uniplot-obj/images/airzone-_FF_airzone.* The field function uses ``AirZone_curl_POST`` as defined previously and will be called everytime the page is refreshed. :: def __ff_airzone(hDoc, hPage, hLayer, hText, nAction, svParameter) { // Argument parsing if (len(svParameter) == 2) { zoneID = strtol(svParameter[1]); action = svParameter[2]; } else { log_warning("uniscript", "Check parameter input for AirZone request -> ?zoneID,action"); return "??"; } info = AirZone_curl_POST(zoneID, action); return info; } Conclusion ---------- This use case covered a wide range of UniPlot functionality so that you can apply them to your own projects. We saw that UniPlot can be your new interface to your IoT at home, but also to monitor and control installations at your work place and it will of course give you all the tools you need to get insight on the data created. :sub:`id-1870160`