Fermenter editor layout is ready. Load record done. Add and Delete a new fermenter is done.

Wed, 24 Apr 2024 16:46:45 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Wed, 24 Apr 2024 16:46:45 +0200
changeset 703
344470c6bb1c
parent 702
d77891f8915d
child 704
66b7a1135452

Fermenter editor layout is ready. Load record done. Add and Delete a new fermenter is done.

thermferm/mqtt.c file | annotate | diff | comparison | revisions
thermferm/server.c file | annotate | diff | comparison | revisions
www/dbfermenters.php file | annotate | diff | comparison | revisions
www/drop_contacts.php file | annotate | diff | comparison | revisions
www/drop_switches.php file | annotate | diff | comparison | revisions
www/includes/global.inc.php file | annotate | diff | comparison | revisions
www/js/global.js file | annotate | diff | comparison | revisions
www/js/set_devices.js file | annotate | diff | comparison | revisions
www/js/set_fermenters.js file | annotate | diff | comparison | revisions
www/set_devices.php file | annotate | diff | comparison | revisions
www/set_fermenters.php file | annotate | diff | comparison | revisions
--- a/thermferm/mqtt.c	Wed Apr 24 11:04:00 2024 +0200
+++ b/thermferm/mqtt.c	Wed Apr 24 16:46:45 2024 +0200
@@ -936,6 +936,9 @@
         payload = xstrcat(payload, (char *)"\",\"temperature\":");
         sprintf(buf, "%.3f", unit->air_temperature / 1000.0);
         payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"idx\":");
+	sprintf(buf, "%d", unit->air_idx);
+	payload = xstrcat(payload, buf);
 	payload = xstrcat(payload, (char *)"}");
 	pub_domoticz_temp(unit->air_idx, unit->air_temperature);
     } else {
@@ -953,6 +956,9 @@
         payload = xstrcat(payload, (char *)"\",\"temperature\":");
         sprintf(buf, "%.3f", unit->beer_temperature / 1000.0);
         payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"idx\":");
+        sprintf(buf, "%d", unit->beer_idx);
+        payload = xstrcat(payload, buf);
 	payload = xstrcat(payload, (char *)"}");
 	pub_domoticz_temp(unit->beer_idx, unit->beer_temperature);
     } else {
@@ -970,6 +976,9 @@
 	payload = xstrcat(payload, (char *)"\",\"temperature\":");
 	sprintf(buf, "%.3f", unit->chiller_temperature / 1000.0);
 	payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"idx\":");
+        sprintf(buf, "%d", unit->chiller_idx);
+        payload = xstrcat(payload, buf);
 	payload = xstrcat(payload, (char *)"}");
 	pub_domoticz_temp(unit->chiller_idx, unit->chiller_temperature);
     } else {
@@ -988,6 +997,12 @@
 	payload = xstrcat(payload, (char *)",\"usage\":");
 	sprintf(buf, "%d", unit->heater_usage);
 	payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"idx\":");
+        sprintf(buf, "%d", unit->heater_idx);
+        payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"delay\":");
+        sprintf(buf, "%d", unit->heater_delay);
+        payload = xstrcat(payload, buf);
 	payload = xstrcat(payload, (char *)"}");
 	pub_domoticz_output(unit->heater_idx, unit->heater_state);
     } else {
@@ -1006,6 +1021,12 @@
 	payload = xstrcat(payload, (char *)",\"usage\":");
 	sprintf(buf, "%d", unit->cooler_usage);
 	payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"idx\":");
+        sprintf(buf, "%d", unit->cooler_idx);
+        payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"delay\":");
+        sprintf(buf, "%d", unit->cooler_delay);
+        payload = xstrcat(payload, buf);
 	payload = xstrcat(payload, (char *)"}");
 	pub_domoticz_output(unit->cooler_idx, unit->cooler_state);
     } else {
@@ -1024,6 +1045,12 @@
 	payload = xstrcat(payload, (char *)",\"usage\":");
 	sprintf(buf, "%d", unit->fan_usage);
 	payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"idx\":");
+        sprintf(buf, "%d", unit->fan_idx);
+        payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"delay\":");
+        sprintf(buf, "%d", unit->fan_delay);
+        payload = xstrcat(payload, buf);
 	payload = xstrcat(payload, (char *)"}");
 	pub_domoticz_output(unit->fan_idx, unit->fan_state);
     } else {
@@ -1042,6 +1069,12 @@
 	payload = xstrcat(payload, (char *)",\"usage\":");
 	sprintf(buf, "%d", unit->light_usage);
 	payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"idx\":");
+        sprintf(buf, "%d", unit->light_idx);
+        payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"delay\":");
+        sprintf(buf, "%d", unit->light_delay);
+        payload = xstrcat(payload, buf);
 	payload = xstrcat(payload, (char *)"}");
 	pub_domoticz_output(unit->light_idx, unit->light_state);
     } else {
@@ -1057,6 +1090,9 @@
 	payload = xstrcat(payload, (char *)"\",\"state\":");
 	sprintf(buf, "%d", (unit->door_state) ? 0:1);
 	payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"idx\":");
+        sprintf(buf, "%d", unit->door_idx);
+        payload = xstrcat(payload, buf);
 	payload = xstrcat(payload, (char *)"}");
 	pub_domoticz_output(unit->door_idx, unit->door_state);
     } else {
@@ -1072,6 +1108,9 @@
 	payload = xstrcat(payload, (char *)"\",\"state\":");
 	sprintf(buf, "%d", unit->psu_state);
 	payload = xstrcat(payload, buf);
+	payload = xstrcat(payload, (char *)",\"idx\":");
+        sprintf(buf, "%d", unit->psu_idx);
+        payload = xstrcat(payload, buf);
 	payload = xstrcat(payload, (char *)"}");
 	pub_domoticz_output(unit->psu_idx, unit->psu_state);
     } else {
@@ -1095,6 +1134,38 @@
     sprintf(buf, "%d", unit->alarm_flag);
     payload = xstrcat(payload, buf);
 
+    payload = xstrcat(payload, (char *)",\"pidc\":{\"P\":");
+    sprintf(buf, "%.3f", unit->PID_cool->pGain);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"I\":");
+    sprintf(buf, "%.3f", unit->PID_cool->iGain);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"D\":");
+    sprintf(buf, "%.3f", unit->PID_cool->dGain);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"imax\":");
+    sprintf(buf, "%.1f", unit->PID_cool->iMax);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"idle\":");
+    sprintf(buf, "%.2f", unit->PID_cool->idleRange);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)"},\"pidh\":{\"P\":");
+    sprintf(buf, "%.3f", unit->PID_heat->pGain);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"I\":");
+    sprintf(buf, "%.3f", unit->PID_heat->iGain);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"D\":");
+    sprintf(buf, "%.3f", unit->PID_heat->dGain);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"imax\":");
+    sprintf(buf, "%.1f", unit->PID_heat->iMax);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"idle\":");
+    sprintf(buf, "%.2f", unit->PID_heat->idleRange);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)"}");
+
     /*
      * Loaded profile and state
      */
--- a/thermferm/server.c	Wed Apr 24 11:04:00 2024 +0200
+++ b/thermferm/server.c	Wed Apr 24 16:46:45 2024 +0200
@@ -1550,6 +1550,9 @@
 	unit->PID_heat = (pid_var *)malloc(sizeof(pid_var));
 	InitPID(unit->PID_cool, PID_TYPE_COOL);
 	InitPID(unit->PID_heat, PID_TYPE_HEAT);
+	unit->PID_heat->pGain = unit->PID_cool->pGain = 5000.0;
+	unit->PID_heat->iGain = unit->PID_cool->iGain = 25.0;
+	unit->PID_heat->dGain = unit->PID_cool->dGain = 5.0;
 
 	/*
 	 * Block main process
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/dbfermenters.php	Wed Apr 24 16:46:45 2024 +0200
@@ -0,0 +1,163 @@
+<?php
+
+
+function open_socket()
+{
+    $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+
+    if (!($sock === false)) {
+        if (socket_connect($sock, "localhost", 6554)) {
+            socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => 15, 'usec' => 0));
+        } else {
+            socket_close($sock);
+        }
+    }
+    return $sock;
+}
+
+
+/**
+ * @param string $command to send to the server.
+ * @return string with the complete reply from the
+ *         server. This can be a multiline reply.
+ */
+function send_cmd($command)
+{
+    $sock = open_socket();
+    if ($sock == false) {
+        return "";
+    }
+    socket_write($sock, $command . "\r\n", 4096);
+
+    $answer = "";
+    while (1) {
+        $line = socket_read($sock, 4096);
+        if ($line === '')
+            break;
+        $answer .= $line;
+    }
+    socket_close($sock);
+
+    return $answer;
+}
+
+
+function startsWith($haystack, $needle)
+{
+    return !strncmp($haystack, $needle, strlen($needle));
+}
+
+
+$response = array(
+   'error' => false,
+   'msg' => 'Ok',
+);
+
+
+if (isset($_POST['update'])) {
+
+    $cmd  = "UNIT PUT " . $_POST['uuid'] . "\r\n";
+    if (isset($_POST['air_address'])) {
+	$cmd .= "AIR_ADDRESS," . $_POST['air_address'] . "\r\n";
+	$cmd .= "AIR_IDX," . $_POST['air_idx'] . "\r\n";
+    }
+    if (isset($_POST['beer_address'])) {
+	$cmd .= "BEER_ADDRESS," . $_POST['beer_address'] . "\r\n";
+	if (isset($_POST['beer_address2']))
+	    $cmd .= "BEER_ADDRESS2," . $_POST['beer_address2'] . "\r\n";
+        $cmd .= "BEER_IDX," . $_POST['beer_idx'] . "\r\n";
+    }
+    if (isset($_POST['chiller_address'])) {
+	$cmd .= "CHILLER_ADDRESS," . $_POST['chiller_address'] . "\r\n";
+        $cmd .= "CHILLER_IDX," . $_POST['chiller_idx'] . "\r\n";
+    }
+    if (isset($_POST['heater_address'])) {
+	$cmd .= "HEATER_ADDRESS," . $_POST['heater_address'] . "\r\n";
+	$cmd .= "HEATER_STATE," . $_POST['heater_state'] . "\r\n";
+	$cmd .= "HEATER_DELAY," . $_POST['heater_delay'] . "\r\n";
+        $cmd .= "HEATER_IDX," . $_POST['heater_idx'] . "\r\n";
+    }
+    if (isset($_POST['cooler_address'])) {
+        $cmd .= "COOLER_ADDRESS," . $_POST['cooler_address'] . "\r\n";
+        $cmd .= "COOLER_STATE," . $_POST['cooler_state'] . "\r\n";
+        $cmd .= "COOLER_DELAY," . $_POST['cooler_delay'] . "\r\n";
+        $cmd .= "COOLER_IDX," . $_POST['cooler_idx'] . "\r\n";
+    }
+    if (isset($_POST['fan_address'])) {
+        $cmd .= "FAN_ADDRESS," . $_POST['fan_address'] . "\r\n";
+        $cmd .= "FAN_STATE," . $_POST['fan_state'] . "\r\n";
+        $cmd .= "FAN_DELAY," . $_POST['fan_delay'] . "\r\n";
+        $cmd .= "FAN_IDX," . $_POST['fan_idx'] . "\r\n";
+    }
+    if (isset($_POST['light_address'])) {
+        $cmd .= "LIGHT_ADDRESS," . $_POST['light_address'] . "\r\n";
+        $cmd .= "LIGHT_STATE," . $_POST['light_state'] . "\r\n";
+        $cmd .= "LIGHT_DELAY," . $_POST['light_delay'] . "\r\n";
+        $cmd .= "LIGHT_IDX," . $_POST['light_idx'] . "\r\n";
+    }
+    if (isset($_POST['door_address'])) {
+        $cmd .= "DOOR_ADDRESS," . $_POST['door_address'] . "\r\n";
+        $cmd .= "DOOR_IDX," . $_POST['door_idx'] . "\r\n";
+    }
+    if (isset($_POST['psu_address'])) {
+        $cmd .= "PSU_ADDRESS," . $_POST['psu_address'] . "\r\n";
+        $cmd .= "PSU_IDX," . $_POST['psu_idx'] . "\r\n";
+    }
+    $cmd .= "STAGE," . $_POST['stage'] . "\r\n";
+    $cmd .= "MODE," . $_POST['mode'] . "\r\n";
+    $cmd .= "PIDC_IMAX," . $_POST['pidc_imax'] . "\r\n";
+    $cmd .= "PIDC_PGAIN," . $_POST['pidc_pgain'] . "\r\n";
+    $cmd .= "PIDC_IGAIN," . $_POST['pidc_igain'] . "\r\n";
+    $cmd .= "PIDC_DGAIN," . $_POST['pidc_dgain'] . "\r\n";
+    $cmd .= "PIDC_IDLERANGE," . $_POST['pidc_idlerange'] . "\r\n";
+    $cmd .= "PIDH_IMAX," . $_POST['pidh_imax'] . "\r\n";
+    $cmd .= "PIDH_PGAIN," . $_POST['pidh_pgain'] . "\r\n";
+    $cmd .= "PIDH_IGAIN," . $_POST['pidh_igain'] . "\r\n";
+    $cmd .= "PIDH_DGAIN," . $_POST['pidh_dgain'] . "\r\n";
+    $cmd .= "PIDH_IDLERANGE," . $_POST['pidh_idlerange'] . "\r\n";
+    $cmd .= "TEMP_SET_MIN," . $_POST['temp_set_min'] . "\r\n";
+    $cmd .= "TEMP_SET_MAX," . $_POST['temp_set_max'] . "\r\n";
+    $cmd .= ".";
+    $answer = send_cmd($cmd);
+    $arr = explode("\r\n", $answer);
+    if (! startsWith($arr[0], "219")) {
+	$response['error'] = true;
+	$response['msg'] = $arr[0];
+    }
+    exit(json_encode($response));
+
+} else if (isset($_POST['add'])) {
+
+    // Name is not used and is generated.
+    $answer = send_cmd("UNIT ADD " . $_POST['name']);
+    $arr = explode("\r\n", $answer);
+    if (! startsWith($arr[0], "211")) {
+	$response['error'] = true;
+	$response['msg'] = $arr[0];
+    }
+    exit(json_encode($response));
+
+} else if (isset($_POST['del'])) {
+
+    $answer = send_cmd("UNIT DEL " . $_POST['uuid']);
+    $arr = explode("\r\n", $answer);
+    if (! startsWith($arr[0], "211")) {
+        $response['error'] = true;
+        $response['msg'] = $arr[0];
+    }
+    exit(json_encode($response));
+
+} else {
+
+    $answer = send_cmd("UNIT JSON");
+    header("Content-type: application/json");
+
+    $arr = explode("\r\n", $answer);
+    if (startsWith($arr[0], "212")) {
+    	echo $arr[1];
+    } else {
+        echo '{}';
+    }
+}
+
+?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/drop_contacts.php	Wed Apr 24 16:46:45 2024 +0200
@@ -0,0 +1,79 @@
+<?php
+
+function open_socket()
+{
+    $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+
+    if (!($sock === false)) {
+        if (socket_connect($sock, "localhost", 6554)) {
+            socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => 15, 'usec' => 0));
+        } else {
+            socket_close($sock);
+        }
+    }
+    return $sock;
+}
+
+
+/**
+ * @param string $command to send to the server.
+ * @return string with the complete reply from the
+ *         server. This can be a multiline reply.
+ */
+function send_cmd($command)
+{
+    $sock = open_socket();
+    if ($sock == false) {
+        return "";
+    }
+    socket_write($sock, $command . "\r\n", 4096);
+
+    $answer = "";
+    while (1) {
+        $line = socket_read($sock, 4096);
+        if ($line === '')
+            break;
+        $answer .= $line;
+    }
+    socket_close($sock);
+
+    return $answer;
+}
+
+
+function startsWith($haystack, $needle)
+{
+    return !strncmp($haystack, $needle, strlen($needle));
+}
+
+$answer = send_cmd("DEVICES JSON");
+
+$arr = explode("\r\n", $answer);
+if (startsWith($arr[0], "212")) {
+
+    /*
+     * Build reply with only temperature sensors.
+     */
+    $sensors[] = array(
+	'uuid' => '',
+	'name' => 'Not Assigned'
+    );
+
+    $rows = json_decode($arr[1], true);
+
+    foreach($rows as $item) {
+	if ($item['direction'] == "IN_BIN") {
+	    $sensors[] = array(
+		'uuid' => $item['uuid'],
+		'name' => $item['description']." ".$item['comment']
+	    );
+	}
+    }
+    header("Content-type: application/json");
+    exit(json_encode($sensors));
+}
+
+header("Content-type: application/json");
+echo '{}';
+
+?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/drop_switches.php	Wed Apr 24 16:46:45 2024 +0200
@@ -0,0 +1,79 @@
+<?php
+
+function open_socket()
+{
+    $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+
+    if (!($sock === false)) {
+        if (socket_connect($sock, "localhost", 6554)) {
+            socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, array('sec' => 15, 'usec' => 0));
+        } else {
+            socket_close($sock);
+        }
+    }
+    return $sock;
+}
+
+
+/**
+ * @param string $command to send to the server.
+ * @return string with the complete reply from the
+ *         server. This can be a multiline reply.
+ */
+function send_cmd($command)
+{
+    $sock = open_socket();
+    if ($sock == false) {
+        return "";
+    }
+    socket_write($sock, $command . "\r\n", 4096);
+
+    $answer = "";
+    while (1) {
+        $line = socket_read($sock, 4096);
+        if ($line === '')
+            break;
+        $answer .= $line;
+    }
+    socket_close($sock);
+
+    return $answer;
+}
+
+
+function startsWith($haystack, $needle)
+{
+    return !strncmp($haystack, $needle, strlen($needle));
+}
+
+$answer = send_cmd("DEVICES JSON");
+
+$arr = explode("\r\n", $answer);
+if (startsWith($arr[0], "212")) {
+
+    /*
+     * Build reply with only temperature sensors.
+     */
+    $sensors[] = array(
+	'uuid' => '',
+	'name' => 'Not Assigned'
+    );
+
+    $rows = json_decode($arr[1], true);
+
+    foreach($rows as $item) {
+	if ($item['direction'] == "OUT_BIN") {
+	    $sensors[] = array(
+		'uuid' => $item['uuid'],
+		'name' => $item['description']." ".$item['comment']
+	    );
+	}
+    }
+    header("Content-type: application/json");
+    exit(json_encode($sensors));
+}
+
+header("Content-type: application/json");
+echo '{}';
+
+?>
--- a/www/includes/global.inc.php	Wed Apr 24 11:04:00 2024 +0200
+++ b/www/includes/global.inc.php	Wed Apr 24 16:46:45 2024 +0200
@@ -198,9 +198,9 @@
       Press "Cancel" to close without deleting this record.
      </div>
      <div>
-      <div style="float: right; margin-top: 15px; margin-bottom: 10px;">
+      <div style="float: right; margin-top: 45px; margin-bottom: 10px;">
        <input type="button" id="delOk" value="OK" style="margin-right: 15px" />
-       <input type="button" id="delCancel" value="Cancel" style="margin-right: 65px" />
+       <input type="button" id="delCancel" value="Cancel" style="margin-right: 125px" />
       </div>
      </div>
     </div>
--- a/www/js/global.js	Wed Apr 24 11:04:00 2024 +0200
+++ b/www/js/global.js	Wed Apr 24 16:46:45 2024 +0200
@@ -56,14 +56,42 @@
 },
 tempsensorlist = new $.jqx.dataAdapter(tempsensorSource),
 
+// Switches dropdown list
+switchesSource = {
+ datatype: 'json',
+ datafields: [
+  { name: 'uuid', type: 'string' },
+  { name: 'name', type: 'string' }
+ ],
+ url: 'drop_switches.php'
+},
+switcheslist = new $.jqx.dataAdapter(switchesSource),
+
+// Contacts dropdown list
+contactsSource = {
+ datatype: 'json',
+ datafields: [
+  { name: 'uuid', type: 'string' },
+  { name: 'name', type: 'string' }
+ ], 
+ url: 'drop_contacts.php'
+},
+contactslist = new $.jqx.dataAdapter(contactsSource),
+
+
 // options for editors
 
 Show0dec = { inputMode: 'simple', theme: theme, width: 90, height: 23, readOnly: true, decimalDigits: 0 },
 Show1dec = { inputMode: 'simple', theme: theme, width: 90, height: 23, readOnly: true, decimalDigits: 1 },
 Spin0dec = { inputMode: 'simple', theme: theme, width: 110, height: 23, min: 0, decimalDigits: 0, spinButtons: true },
+Spin1dec = { inputMode: 'simple', theme: theme, width: 110, height: 23, min: 0, decimalDigits: 1, spinButtons: true },
+Spin2dec = { inputMode: 'simple', theme: theme, width: 110, height: 23, min: 0, decimalDigits: 2, spinButtons: true },
+Spin3dec = { inputMode: 'simple', theme: theme, width: 110, height: 23, min: 0, decimalDigits: 3, spinButtons: true },
   PosInt = { inputMode: 'simple', theme: theme, width: 110, height: 23, min: 0, decimalDigits: 0, spinButtons: true },
   SubInt = { inputMode: 'simple', theme: theme, width: 110, height: 23, min: 0, max: 63, decimalDigits: 0, spinButtons: true },
- GPIOInt = { inputMode: 'simple', theme: theme, width: 110, height: 23, min: -1, max:31, decimalDigits: 0, spinButtons: true };
+ GPIOInt = { inputMode: 'simple', theme: theme, width: 110, height: 23, min: -1, max:31, decimalDigits: 0, spinButtons: true },
+Perc1dec = { inputMode: 'simple', theme: theme, width: 110, height: 23, min: 0, max: 100, decimalDigits: 1, spinButtons: true },
+   Perc0 = { inputMode: 'simple', theme: theme, width: 110, height: 23, min: 0, max: 100, decimalDigits: 0, spinButtons: true };
 
 
 /* Websocket interface. Place "websocket.onmessage = function(evt) {}" in the user script. */
--- a/www/js/set_devices.js	Wed Apr 24 11:04:00 2024 +0200
+++ b/www/js/set_devices.js	Wed Apr 24 16:46:45 2024 +0200
@@ -24,8 +24,8 @@
 function createDelElements() {
  $('#eventWindow').jqxWindow({
   theme: theme,
-  position: { x: 490, y: 210 },
-  width: 300,
+  position: { x: 430, y: 210 },
+  width: 420,
   height: 175,
   resizable: false,
   isModal: true,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/js/set_fermenters.js	Wed Apr 24 16:46:45 2024 +0200
@@ -0,0 +1,373 @@
+/*****************************************************************************
+ * Copyright (C) 2024
+ *
+ * Michiel Broek <mbroek at mbse dot eu>
+ *
+ * This file is part of mbsePi-apps
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any
+ * later version.
+ *
+ * BrewCloud is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with ThermFerm; see the file COPYING.  If not, write to the Free
+ * Software Foundation, 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ *****************************************************************************/
+
+
+function createDelElements() {
+ $('#eventWindow').jqxWindow({
+  theme: theme,
+  position: { x: 430, y: 210 },
+  width: 420,
+  height: 175,
+  resizable: false,
+  isModal: true,
+  modalOpacity: 0.4,
+  okButton: $('#delOk'),
+  cancelButton: $('#delCancel'),
+  initContent: function() {
+   $('#delOk').jqxButton({ template: 'danger', width: '65px', theme: theme });
+   $('#delCancel').jqxButton({ template: 'success', width: '65px', theme: theme });
+   $('#delCancel').focus();
+  }
+ });
+ $('#eventWindow').jqxWindow('hide');
+}
+
+
+function createAddElements() {
+ $('#addWindow').jqxWindow({
+  theme: theme,
+  position: { x: 400, y: 210 },
+  width: 480,
+  height: 180,
+  resizable: false,
+  isModal: true,
+  modalOpacity: 0.4,
+  okButton: $('#addOk'),
+  cancelButton: $('#addCancel'),
+  initContent: function() {
+   $('#addOk').jqxButton({ template: 'success', width: '65px', theme: theme });
+   $('#addCancel').jqxButton({ template: 'primary', width: '65px', theme: theme });
+   $('#addCancel').focus();
+  }
+ });
+ $('#addWindow').jqxWindow('hide');
+}
+
+
+
+$(document).ready(function() {
+ var dataRecord = {},
+ url = 'dbfermenters.php',
+ source = {
+  datatype: 'json',
+  cache: false,
+  datafields: [
+   { name: 'uuid', type: 'string' },
+   { name: 'alias', type: 'string' },
+   { name: 'product_code', map: 'product>code' },
+   { name: 'product_name', map: 'product>name' },
+   { name: 'air_address', map: 'air>address' },
+   { name: 'air_idx', map: 'air>idx', type: 'int' },
+   { name: 'beer_address', map: 'beer>address' },
+   { name: 'beer_idx', map: 'beer>idx', type: 'int' },
+   { name: 'chiller_address', map: 'chiller>address' },
+   { name: 'chiller_idx', map: 'chiller>idx', type: 'int' },
+   { name: 'light_address', map: 'light>address' },
+   { name: 'light_state', map: 'light>state', type: 'int' },
+   { name: 'light_delay', map: 'light>delay', type: 'int' },
+   { name: 'light_idx', map: 'light>idx', type: 'int' },
+   { name: 'heater_address', map: 'heater>address' },
+   { name: 'heater_state', map: 'heater>state', type: 'int' },
+   { name: 'heater_delay', map: 'heater>delay', type: 'int' },
+   { name: 'heater_idx', map: 'heater>idx', type: 'int' },
+   { name: 'cooler_address', map: 'cooler>address' },
+   { name: 'cooler_state', map: 'cooler>state', type: 'int' },
+   { name: 'cooler_delay', map: 'cooler>delay', type: 'int' },
+   { name: 'cooler_idx', map: 'cooler>idx', type: 'int' },
+   { name: 'fan_address', map: 'fan>address' },
+   { name: 'fan_state', map: 'fan>state', type: 'int' },
+   { name: 'fan_delay', map: 'fan>delay', type: 'int' },
+   { name: 'fan_idx', map: 'fan>idx', type: 'int' },
+   { name: 'door_address', map: 'door>address' },
+   { name: 'door_idx', map: 'door>idx', type: 'int' },
+   { name: 'door_state', map: 'door>state', type: 'int' },
+   { name: 'psu_address', map: 'psu>address' },
+   { name: 'psu_idx', map: 'psu>idx', type: 'int' },
+   { name: 'psu_state', map: 'psu>state', type: 'int' },
+   { name: 'stage' },
+   { name: 'mode' },
+   { name: 'setpoint_low', map: 'setpoint>low', type: 'float' },
+   { name: 'setpoint_high', map: 'setpoint>high', type: 'float' },
+   { name: 'alarm', type: 'int' },
+   { name: 'pidc_p', map: 'pidc>P', type: 'float' },
+   { name: 'pidc_i', map: 'pidc>I', type: 'float' },
+   { name: 'pidc_d', map: 'pidc>D', type: 'float' },
+   { name: 'pidc_imax', map: 'pidc>imax', type: 'float' },
+   { name: 'pidc_idle', map: 'pidc>idle', type: 'float' },
+   { name: 'pidh_p', map: 'pidh>P', type: 'float' },
+   { name: 'pidh_i', map: 'pidh>I', type: 'float' },
+   { name: 'pidh_d', map: 'pidh>D', type: 'float' },
+   { name: 'pidh_imax', map: 'pidh>imax', type: 'float' },
+   { name: 'pidh_idle', map: 'pidh>idle', type: 'float' }
+  ],
+  id: 'uuid',
+  url: url
+ },
+ dataAdapter = new $.jqx.dataAdapter(source),
+ editrow = -1;
+
+ // initialize the input fields.
+ $('#uuid').jqxInput({ theme: theme, width: 360, height: 23 });
+ $('#alias').jqxInput({ theme: theme, width: 120, height: 23 });
+ $('#product_code').jqxInput({ theme: theme, width: 120, height: 23 });
+ $('#product_name').jqxInput({ theme: theme, width: 360, height: 23 });
+ $('#mode').jqxInput({ theme: theme, width: 120, height: 23 });
+ $('#stage').jqxInput({ theme: theme, width: 120, height: 23 });
+ $('#air_address,#beer_address,#beer_address2,#chiller_address').jqxDropDownList({
+  theme: theme,
+  source: tempsensorlist,
+  valueMember: 'uuid',
+  displayMember: 'name',
+  width: 240,
+  dropDownWidth: 480,
+  autoDropDownHeight: true
+ });
+ $('#air_idx,#beer_idx,#chiller_idx,#heater_idx,#cooler_idx,#fan_idx,#light_idx,#door_idx,#psu_idx').jqxNumberInput(Spin0dec);
+ $('#heater_address,#cooler_address,#fan_address,#light_address').jqxDropDownList({
+  theme: theme,
+  source: switcheslist,
+  valueMember: 'uuid',
+  displayMember: 'name',
+  width: 240,
+  dropDownWidth: 480,
+  autoDropDownHeight: true
+ });
+ $('#heater_state,#cooler_state,#fan_state,#light_state').jqxNumberInput(Perc0);
+ $('#heater_delay,#cooler_delay,#fan_delay,#light_delay').jqxNumberInput(Spin0dec);
+ $('#pidh_p,#pidh_i,#pidh_d,#pidc_p,#pidc_i,#pidc_d').jqxNumberInput(Spin3dec);
+ $('#pidh_imax,#pidc_imax').jqxNumberInput(Perc1dec);
+ $('#pidh_idle,#pidc_idle').jqxNumberInput(Spin2dec);
+ $('#door_address,#psu_address').jqxDropDownList({
+  theme: theme,
+  source: contactslist,
+  valueMember: 'uuid',
+  displayMember: 'name',
+  width: 240,
+  dropDownWidth: 480,
+  autoDropDownHeight: true
+ });
+ $('#door_state,#psu_state').jqxNumberInput(Show0dec);
+
+ // initialize jqxGrid
+ $('#jqxgrid').jqxGrid({
+  width: 1280,
+  height: 630,
+  source: dataAdapter,
+  theme: theme,
+  showstatusbar: true,
+  renderstatusbar: function(statusbar) {
+   var rowCount = $("#jqxgrid").jqxGrid('getrows').length;
+   statusbar.append('<div style="float: left; margin: 8px; color: orange !important;">Total items: ' + rowCount + '</div>');
+   var container, addButton, impButton;
+   container = $('<div style="overflow: hidden; position: relative; margin: 5px;"></div>');
+   addButton = $('<div style="float: right; margin-right: 15px;"><img style="position: relative; margin-top: 2px;" ' +
+     'src="images/add.png"/><span style="margin-left: 4px; position: relative; top: -4px;">New</span></div>');
+   container.append(addButton);
+   statusbar.append(container);
+   addButton.jqxButton({ theme: theme, width: 90, height: 17 });
+   // add new row.
+   addButton.click(function(event) {
+    $('#addWindow').jqxWindow('open');
+    $('#addOk').click(function() {
+     var data,
+     data = 'add=true&name=dummy'
+     console.log(data);
+     $.ajax({
+      dataType: 'json',
+      url: url,
+      cache: false,
+      data: data,
+      type: 'POST',
+      success: function(data) {
+       if (data.error) {
+        console.log('add: ' + data.msg);
+        alert('Error: ' + data.msg);
+       } else {
+        console.log('add: success');
+       }
+      },
+      error: function(jqXHR, textStatus, errorThrown) {}
+     });
+     $('#jqxgrid').jqxGrid('updatebounddata');
+    });
+   });
+  },
+  columns: [
+   { text: 'Unit', datafield: 'alias', width: 200 },
+   { text: 'Mode', datafield: 'mode', width: 120 },
+   { text: 'Code', datafield: 'product_code', width: 150 },
+   { text: 'Beer', datafield: 'product_name' },
+   { text: '', datafield: 'Edit', width: 100, align: 'center', columntype: 'button', cellsrenderer: function() {
+    return 'Edit';
+    }, buttonclick: function(row) {
+     // open the popup window when the user clicks a button.
+     editrow = row;
+     $('#popupWindow').jqxWindow({ position: { x: 40, y: 15 } });
+     dataRecord = $('#jqxgrid').jqxGrid('getrowdata', editrow);
+     $('#uuid').val(dataRecord.uuid);
+     $('#alias').val(dataRecord.alias);
+     $('#product_code').val(dataRecord.product_code);
+     $('#product_name').val(dataRecord.product_name);
+     $('#air_address').val(dataRecord.air_address);
+     $('#air_idx').val(dataRecord.air_idx);
+     $('#beer_address').val(dataRecord.beer_address);
+     $('#beer_address2').val(dataRecord.beer_address2);
+     $('#beer_idx').val(dataRecord.beer_idx);
+     $('#chiller_address').val(dataRecord.chiller_address);
+     $('#chiller_idx').val(dataRecord.chiller_idx);
+     $('#heater_address').val(dataRecord.heater_address);
+     $('#heater_idx').val(dataRecord.heater_idx);
+     $('#heater_state').val(dataRecord.heater_state);
+     $('#heater_delay').val(dataRecord.heater_delay);
+     $('#cooler_address').val(dataRecord.cooler_address);
+     $('#cooler_idx').val(dataRecord.cooler_idx);
+     $('#cooler_state').val(dataRecord.cooler_state);
+     $('#cooler_delay').val(dataRecord.cooler_delay);
+     $('#fan_address').val(dataRecord.fan_address);
+     $('#fan_idx').val(dataRecord.fan_idx);
+     $('#fan_state').val(dataRecord.fan_state);
+     $('#fan_delay').val(dataRecord.fan_delay);
+     $('#light_address').val(dataRecord.light_address);
+     $('#light_idx').val(dataRecord.light_idx);
+     $('#light_state').val(dataRecord.light_state);
+     $('#light_delay').val(dataRecord.light_delay);
+     $('#door_address').val(dataRecord.door_address);
+     $('#door_idx').val(dataRecord.door_idx);
+     $('#door_state').val(dataRecord.door_state);
+     $('#psu_address').val(dataRecord.psu_address);
+     $('#psu_idx').val(dataRecord.psu_idx);
+     $('#psu_state').val(dataRecord.psu_state);
+     $('#mode').val(dataRecord.mode);
+     $('#stage').val(dataRecord.stage);
+     $('#pidc_p').val(dataRecord.pidc_p);
+     $('#pidc_i').val(dataRecord.pidc_i);
+     $('#pidc_d').val(dataRecord.pidc_d);
+     $('#pidc_imax').val(dataRecord.pidc_imax);
+     $('#pidc_idle').val(dataRecord.pidc_idle);
+     $('#pidh_p').val(dataRecord.pidh_p);
+     $('#pidh_i').val(dataRecord.pidh_i);
+     $('#pidh_d').val(dataRecord.pidh_d);
+     $('#pidh_imax').val(dataRecord.pidh_imax);
+     $('#pidh_idle').val(dataRecord.pidh_idle);
+
+     // show the popup window.
+     $('#popupWindow').jqxWindow('open');
+    }
+   }
+  ],
+ });
+
+ // initialize the popup window and buttons.
+ $('#popupWindow').jqxWindow({
+  width: 1280,
+  height: 625,
+  resizable: false,
+  theme: theme,
+  isModal: true,
+  autoOpen: false,
+  cancelButton: $('#Cancel'),
+  modalOpacity: 0.40
+ });
+ $('#popupWindow').on('open', function() {
+//  $('#dev_description').jqxInput('selectAll');
+ });
+ $('#Delete').jqxButton({ template: 'danger', width: '90px', theme: theme });
+ $('#Delete').click(function() {
+  // Open a popup to confirm this action.
+  $('#eventWindow').jqxWindow('open');
+  $('#delOk').click(function() {
+   var data,
+   data = 'del=true&uuid=' + $('#uuid').val();
+   $.ajax({
+    dataType: 'json',
+    url: url,
+    cache: false,
+    data: data,
+    type: 'POST',
+    success: function(data) {
+     if (data.error) {
+      console.log('del: ' + data.msg);
+      alert('Error: ' + data.msg);
+     } else {
+      console.log('del: success');
+     }
+    },
+    error: function(jqXHR, textStatus, errorThrown) {}
+   });
+   $('#jqxgrid').jqxGrid('updatebounddata');
+  });
+  $('#popupWindow').jqxWindow('hide');
+ });
+ $('#Cancel').jqxButton({ template: 'primary', width: '90px', theme: theme });
+ $('#Save').jqxButton({ template: 'success', width: '90px', theme: theme });
+ $('#Save').click(function() {
+  var data,
+  row = {
+   uuid: dataRecord.uuid,
+//   type: $('#dev_type').val(),
+//   direction: $('#dev_direction').val(),
+//   value: parseInt($('#dev_value').jqxNumberInput('decimal')),
+//   offset: parseInt($('#dev_offset').jqxNumberInput('decimal')),
+//   present: $('#dev_present').val(),
+//   address: $('#dev_address').val(),
+//   subdevice: parseInt($('#dev_subdevice').jqxNumberInput('decimal')),
+//   gpiopin: parseInt($('#dev_gpiopin').jqxNumberInput('val')),
+//   description: $('#dev_description').val(),
+//   comment: $('#dev_comment').val()
+  };
+  data = 'update=true&' + $.param(row);
+  console.log(data);
+  $.ajax({
+   dataType: 'json',
+   url: url,
+   cache: false,
+   data: data,
+   type: 'POST',
+   success: function(data) {
+    if (data.error) {
+     console.log('update: ' + data.msg);
+     alert('Error: ' + data.msg);
+    } else {
+     console.log('update: success');
+    }
+   },
+   error: function(jqXHR, textStatus, errorThrown) {}
+  });
+  $('#popupWindow').jqxWindow('hide');
+ });
+ createDelElements();
+ createAddElements();
+
+ websocket.onmessage = function(evt) {
+  var msg = evt.data;
+  var obj = JSON.parse(msg);
+
+  if (obj.ping) {
+   websocket.send('{"pong":' + obj.ping + '}');
+  }
+
+  if (obj.type == 'device') {
+   // Use the message to trigger update.
+   $('#jqxgrid').jqxGrid('updatebounddata');
+  }
+ }
+});
--- a/www/set_devices.php	Wed Apr 24 11:04:00 2024 +0200
+++ b/www/set_devices.php	Wed Apr 24 16:46:45 2024 +0200
@@ -68,9 +68,6 @@
        <td style="padding-top: 20px;" align="left"><input id="Cancel" type="button" value="Cancel" /></td>
       </tr>
      </table>
-     <div style="float: right; margin-top: 290px; margin-bottom: 10px;">
-      <input style="margin-right: 595px;" type="button" id="Save"   value="Save" />
-     </div>
     </div>
    </div>
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/set_fermenters.php	Wed Apr 24 16:46:45 2024 +0200
@@ -0,0 +1,173 @@
+<?php
+require_once($_SERVER['DOCUMENT_ROOT'].'/includes/global.inc.php');
+page_header('Fermenter units', 'set_fermenters');
+?>
+
+   <div id="jqxgrid"></div>
+    <div style="margin-top: 30px;">
+    <div id="cellbegineditevent"></div>
+    <div style="margin-top: 10px;" id="cellendeditevent"></div>
+   </div>
+
+   <!-- Popup editor window. -->
+   <div id="popupWindow">
+    <div>Edit device</div>
+    <div style="overflow: hidden;">
+     <table style="width: 100%;">
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Unit:</td>
+       <td align="left" style="vertical-align: top; padding: 3px;"><input id="alias" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Uuid:</td>
+       <td align="left" colspan="3" style="vertical-align: top; padding: 3px;"><input id="uuid" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Mode:</td>
+       <td align="left" style="vertical-align: top; padding: 3px;"><input id="mode" readonly /></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Code:</td>
+       <td align="left" style="vertical-align: top; padding: 3px;"><input id="product_code" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Beer:</td>
+       <td align="left" colspan="3" style="vertical-align: top; padding: 3px;"><input id="product_name" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Stage:</td>
+       <td align="left" style="vertical-align: top; padding: 3px;"><input id="stage" readonly /></td>
+      </tr>
+      <tr><td colspan="8"><hr></td></tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Air sensor:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="air_address"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Index:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="air_idx"></div></td>
+       <td></td><td></td><td></td><td></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Beer sensor:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="beer_address"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Index:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="beer_idx"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Alternate:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="beer_address2"></div></td>
+       <td></td><td></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Chiller sensor:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="chiller_address"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Index:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="chiller_idx"></div></td>
+       <td></td><td></td><td></td><td></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Heater:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="heater_address"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Index:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="heater_idx"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">State:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="heater_state"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Delay:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="heater_delay"></div></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Cooler:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="cooler_address"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Index:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="cooler_idx"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">State:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="cooler_state"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Delay:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="cooler_delay"></div></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Fan:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="fan_address"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Index:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="fan_idx"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">State:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="fan_state"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Delay:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="fan_delay"></div></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Light:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="light_address"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Index:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="light_idx"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">State:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="light_state"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Delay:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="light_delay"></div></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Door:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="door_address"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Index:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="door_idx"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">State:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="door_state"></div></td>
+       <td></td><td></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">PSU:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="psu_address"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Index:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="psu_idx"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">State:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="psu_state"></div></td>
+       <td></td><td></td>
+      </tr>
+      <tr><td colspan="8"><hr></td></tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Heater PID:</td>
+       <td style="padding: 3px;" colspan="3">
+	<div style="float: left; margin-right: 15px" id="pidh_p"></div>
+	<div style="float: left; margin-right: 15px" id="pidh_i"></div>
+        <div style="float: left;" id="pidh_d"></div>
+       </td>
+       <td style="vertical-align: top; float: right; padding: 3px;">I Max:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="pidh_imax"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Idle range:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="pidh_idle"></div></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Cooler PID:</td>
+       <td style="padding: 3px;" colspan="3">
+        <div style="float: left; margin-right: 15px" id="pidc_p"></div>
+        <div style="float: left; margin-right: 15px" id="pidc_i"></div>
+        <div style="float: left;" id="pidc_d"></div>
+       </td>
+       <td style="vertical-align: top; float: right; padding: 3px;">I Max:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="pidc_imax"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Idle range:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="pidc_idle"></div></td>
+      </tr>
+
+      <tr><td colspan="8">&nbsp;</td></tr>
+      <tr>
+       <td style="padding-top: 20px;" align="right"><input type="button" id="Delete" value="Delete" /></td>
+       <td></td><td></td><td></td><td></td><td></td>
+       <td style="padding-top: 20px;" align="right"><input style="margin-right: 5px;" type="button" id="Save" value="Save" /></td>
+       <td style="padding-top: 20px;" align="left"><input id="Cancel" type="button" value="Cancel" /></td>
+      </tr>
+     </table>
+    </div>
+   </div>
+
+   <div id="addWindow">
+    <div>
+     Add new device
+    </div>
+    <div>
+     <div>
+      Press "OK" to create a new fermenter unit record.<br>
+      Press "Cancel" to close without adding a new unit.
+     </div>
+     <div>
+      <div style="float: right; margin-top: 25px; margin-bottom: 10px;">
+       <input type="button" id="addOk" value="OK" style="margin-right: 15px" />
+       <input type="button" id="addCancel" value="Cancel" style="margin-right: 65px" />
+      </div>
+     </div>
+    </div>
+   </div>
+
+<?php
+confirm_delete();
+page_footer();
+?>

mercurial