Better websocket broadcast messages. Added GLOBAL JSON command to the server. Better logic to trigger websocket and mqtt data updates for the fermenter units. Websocket receive added fermenter mode, stage, setpoints, switches. Added more css styles for the fermenter screen. Added the fermenter screen php and javascript.

Mon, 15 Apr 2024 17:04:57 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Mon, 15 Apr 2024 17:04:57 +0200
changeset 678
cc49115e769e
parent 677
c867eb3f7fc1
child 679
ecfcb1104b54

Better websocket broadcast messages. Added GLOBAL JSON command to the server. Better logic to trigger websocket and mqtt data updates for the fermenter units. Websocket receive added fermenter mode, stage, setpoints, switches. Added more css styles for the fermenter screen. Added the fermenter screen php and javascript.

thermferm/mqtt.c file | annotate | diff | comparison | revisions
thermferm/server.c file | annotate | diff | comparison | revisions
thermferm/thermferm.c file | annotate | diff | comparison | revisions
thermferm/websocket.c file | annotate | diff | comparison | revisions
www/css/style.css file | annotate | diff | comparison | revisions
www/favicon.ico file | annotate | diff | comparison | revisions
www/fermenter.php file | annotate | diff | comparison | revisions
www/getfermenter.php file | annotate | diff | comparison | revisions
www/images/computer.png file | annotate | diff | comparison | revisions
www/includes/global.inc.php file | annotate | diff | comparison | revisions
www/js/fermenter.js file | annotate | diff | comparison | revisions
--- a/thermferm/mqtt.c	Sun Apr 14 14:41:38 2024 +0200
+++ b/thermferm/mqtt.c	Mon Apr 15 17:04:57 2024 +0200
@@ -1220,8 +1220,10 @@
     /*
      * Build and send websocket message.
      */
-    payload = xstrcpy((char *)"{\"fermenter\":");
-    payloadu = unit_data(unit, true);
+    payload = xstrcpy((char *)"{\"type\":\"fermenter\",\"unit\":\"");
+    payload = xstrcat(payload, unit->alias);
+    payload = xstrcat(payload, (char *)"\",\"metric\":");
+    payloadu = unit_data(unit, false);
     payload = xstrcat(payload, payloadu);
     payload = xstrcat(payload, (char *)"}");
     ws_broadcast(payload);
@@ -1229,6 +1231,8 @@
     payload = NULL;
     free(payloadu);
     payloadu = NULL;
+
+    unit->mqtt_flag &= ~MQTT_FLAG_DATA;
 }
 
 
@@ -1410,20 +1414,19 @@
     char		*payload = NULL, buf[64];
     struct utsname	ubuf;
 
-    payload = xstrcpy((char *)"{\"thermferm\":{");
-
+    payload = xstrcpy((char *)"{\"type\":\"global\",\"name\":\"");
+    payload = xstrcat(payload, Config.name);
+    payload = xstrcat(payload, (char *)"\",\"node\":\"");
     if (uname(&ubuf) == 0) {
-	payload = xstrcat(payload, (char *)"\"node\":\"");
 	payload = xstrcat(payload, ubuf.nodename);
 	payload = xstrcat(payload, (char *)"\",\"os\":\"");
 	payload = xstrcat(payload, ubuf.sysname);
 	payload = xstrcat(payload, (char *)"\",\"os_version\":\"");
 	payload = xstrcat(payload, ubuf.release);
-	payload = xstrcat(payload, (char *)"\"");
     } else {
-	payload = xstrcat(payload, (char *)"\"node\":\"Unknown\",\"os\":\"Unknown\",\"os_version\":\"Unknown\"");
+	payload = xstrcat(payload, (char *)"Unknown\",\"os\":\"Unknown\",\"os_version\":\"Unknown");
     }
-    payload = xstrcat(payload, (char *)",\"FW\":\"");
+    payload = xstrcat(payload, (char *)"\",\"FW\":\"");
     payload = xstrcat(payload, (char *)VERSION);
     payload = xstrcat(payload, (char *)"\"");
 
@@ -1444,7 +1447,7 @@
         payload = xstrcat(payload, (char *)"}");
     }
 
-    payload = xstrcat(payload, (char *)"}}");
+    payload = xstrcat(payload, (char *)"}");
     ws_broadcast(payload);
     free(payload);
     payload = NULL;
--- a/thermferm/server.c	Sun Apr 14 14:41:38 2024 +0200
+++ b/thermferm/server.c	Mon Apr 15 17:04:57 2024 +0200
@@ -569,6 +569,7 @@
 /*
  * GLOBAL GET
  * GLOBAL PUT
+ * GLOBAL JSON
  */
 int cmd_global(int s, char *buf)
 {
@@ -588,6 +589,7 @@
 	srv_send(s, (char *)"Recognized commands:");
 	srv_send(s, (char *)"GLOBAL GET                    Get global settings");
 	srv_send(s, (char *)"GLOBAL PUT                    Put global settings");
+	srv_send(s, (char *)"GLOBAL JSON                   Get global json settings");
 	srv_send(s, (char *)".");
 	return 0;
     }
@@ -617,6 +619,52 @@
 	return 0;
     }
 
+    if (strcmp(opt, (char *)"JSON") == 0) {
+	char		*payload = NULL, tbuf[64];
+	struct utsname	ubuf;
+
+    	payload = xstrcpy((char *)"{\"type\":\"global\",\"name\":\"");
+	payload = xstrcat(payload, Config.name);
+	payload = xstrcat(payload, (char *)"\",\"node\":\"");
+    	if (uname(&ubuf) == 0) {
+            payload = xstrcat(payload, ubuf.nodename);
+            payload = xstrcat(payload, (char *)"\",\"os\":\"");
+            payload = xstrcat(payload, ubuf.sysname);
+            payload = xstrcat(payload, (char *)"\",\"os_version\":\"");
+            payload = xstrcat(payload, ubuf.release);
+    	} else {
+            payload = xstrcat(payload, (char *)"Unknown\",\"os\":\"Unknown\",\"os_version\":\"Unknown");
+    	}
+    	payload = xstrcat(payload, (char *)"\",\"FW\":\"");
+    	payload = xstrcat(payload, (char *)VERSION);
+    	payload = xstrcat(payload, (char *)"\"");
+
+    	if (Config.temp_address || Config.hum_address) {
+            payload = xstrcat(payload, (char *)",\"THB\":{");
+            if (Config.temp_address) {
+            	payload = xstrcat(payload, (char *)"\"temperature\":");
+            	sprintf(tbuf, "%.1f", Config.temp_value / 1000.0);
+            	payload = xstrcat(payload, tbuf);
+            }
+            if (Config.temp_address && Config.hum_address)
+            	payload = xstrcat(payload, (char *)",");
+            if (Config.hum_address) {
+            	payload = xstrcat(payload, (char *)"\"humidity\":");
+            	sprintf(tbuf, "%.1f", Config.hum_value / 1000.0);
+            	payload = xstrcat(payload, tbuf);
+            }
+            payload = xstrcat(payload, (char *)"}");
+    	}
+
+    	payload = xstrcat(payload, (char *)"}");
+    	srv_send(s, (char *)"213 Global json data follows:");
+	srv_send(s, payload);
+	srv_send(s, (char *)".");
+    	free(payload);
+    	payload = NULL;
+	return 0;
+    }
+
     if (strcmp(opt, (char *)"PUT") == 0) {
 	int mqtt_reconnect = 0;
 	while (1) {
@@ -1535,13 +1583,17 @@
     }
 
     if (strcmp(opt, (char *)"JSON") == 0) {
+	syslog(LOG_NOTICE, "UNIT JSON %s", param);
         for (unit = Config.units; unit; unit = unit->next) {
             if (strcmp(param, unit->uuid) == 0) {
 		char	*payload, *payloadu;
 
 		srv_send(s, (char *)"213 Unit json data follows:");
-		payload = xstrcpy((char *)"{\"fermenter\":");
-		payloadu = unit_data(unit, true);
+
+		payload = xstrcpy((char *)"{\"type\":\"fermenter\",\"unit\":\"");
+		payload = xstrcat(payload, unit->alias);
+		payload = xstrcat(payload, (char *)"\",\"metric\":");
+		payloadu = unit_data(unit, false);
 		payload = xstrcat(payload, payloadu);
 		payload = xstrcat(payload, (char *)"}");
 		srv_send(s, payload);
--- a/thermferm/thermferm.c	Sun Apr 14 14:41:38 2024 +0200
+++ b/thermferm/thermferm.c	Mon Apr 15 17:04:57 2024 +0200
@@ -1029,7 +1029,7 @@
     float		LCDair, LCDbeer, LCDspL, LCDspH;
     unsigned char	LCDstatC, LCDstatH;
 
-    unit->mqtt_flag &= ~MQTT_FLAG_DATA;
+//    unit->mqtt_flag &= ~MQTT_FLAG_DATA;
     unit->alarm_flag = 0;
 
     if (unit->air_address) {
@@ -1623,7 +1623,7 @@
 	    unit->mqtt_flag &= ~MQTT_FLAG_BIRTH;
 	} else {
 	    publishDData(unit);
-	    unit->mqtt_flag &= ~MQTT_FLAG_DATA;
+//	    unit->mqtt_flag &= ~MQTT_FLAG_DATA;
 	}
 	if (unit->mqtt_flag & MQTT_FLAG_DEATH) {
 	    publishDDeath(unit);
--- a/thermferm/websocket.c	Sun Apr 14 14:41:38 2024 +0200
+++ b/thermferm/websocket.c	Mon Apr 15 17:04:57 2024 +0200
@@ -26,12 +26,17 @@
 
 #include "thermferm.h"
 #include "xutil.h"
+#include "devices.h"
 #include "websocket.h"
 #include <libwebsockets.h>
 
 
 extern sys_config       Config;
 extern int		debug;
+extern const char	UNITMODE[5][8];
+extern const char	UNITSTAGE[4][12];
+//extern const char       PROFSTATE[5][6];
+//extern const char       TEMPSTATE[3][8];
 
 int			my_ws_shutdown = 0;
 int			my_ws_state = 0;
@@ -63,6 +68,157 @@
 
 
 
+void fermenter_ws_receive(char *buf)
+{
+    struct json_object	*val, *val2, *jobj;
+    units_list		*unit;
+    bool		changed;
+
+    jobj = json_tokener_parse(buf);
+    json_object_object_get_ex(jobj, "unit", &val);
+
+    for (unit = Config.units ; unit; unit = unit->next) {
+	if (strcmp((char *)json_object_get_string(val), unit->alias) == 0) {
+	    /*
+	     * Setpoints
+	     * {"type":"fermenter","unit":"unit0","setpoint_low":20.3,"setpoint_high":20.7}
+	     */
+	    if ((unit->mode == UNITMODE_FRIDGE) || (unit->mode == UNITMODE_BEER)) {
+		changed = false;
+		if (json_object_object_get_ex(jobj, "setpoint_low", &val)) {
+		    if (unit->PID_heat->SetP != json_object_get_double(val)) {
+			changed = true;
+		    	syslog(LOG_NOTICE, "ws: unit %s setpoint low from %.1f to %.1f", unit->alias, unit->PID_heat->SetP, json_object_get_double(val));
+		    	unit->PID_heat->SetP = json_object_get_double(val);
+		    }
+		}
+		if (json_object_object_get_ex(jobj, "setpoint_high", &val)) {
+		    if (unit->PID_cool->SetP != json_object_get_double(val)) {
+			changed = true;
+		    	syslog(LOG_NOTICE, "ws: unit %s setpoint high from %.1f to %.1f", unit->alias, unit->PID_cool->SetP, json_object_get_double(val));
+		    	unit->PID_cool->SetP = json_object_get_double(val);
+		    }
+		}
+		if (changed) {
+		    if (unit->mode == UNITMODE_FRIDGE) {
+		    	unit->fridge_set_lo = unit->PID_heat->SetP;
+		    	unit->fridge_set_hi = unit->PID_cool->SetP;
+                    } else {
+		    	unit->beer_set_lo = unit->PID_heat->SetP;
+		    	unit->beer_set_hi = unit->PID_cool->SetP;
+                    }
+		    unit->mqtt_flag |= MQTT_FLAG_DATA;
+		    break;
+		}
+	    }
+
+	    /*
+	     * Unit mode
+	     * {"type":"fermenter","unit":"unit0","mode":"NONE"}
+	     */
+	    if (json_object_object_get_ex(jobj, "mode", &val)) {
+		for (int i = 0; i < 5; i++) {
+		    if (strcmp((char *)json_object_get_string(val), UNITMODE[i]) == 0) {
+			if (unit->mode != i) {
+			    unit->mqtt_flag |= MQTT_FLAG_DATA;
+			    /* Initialize log if the unit is turned on */
+			    if ((unit->mode == UNITMODE_OFF) && (i != UNITMODE_OFF)) {
+				unit->mqtt_flag |= MQTT_FLAG_BIRTH;
+			    }
+			    if (i == UNITMODE_PROFILE) {
+				/* Do some checks and refuse profile mode cannot be set */
+				if (unit->profile_uuid == NULL) {
+				    syslog(LOG_NOTICE, "ws: unit %s refuse mode profile, not loaded", unit->alias);
+				    break;
+				}
+			    }
+			    syslog(LOG_NOTICE, "ws: unit %s mode to %s", unit->alias, UNITMODE[i]);
+			    unit->mode = i;
+			    if ((unit->mode != UNITMODE_OFF) && ! unit->event_msg)
+				unit->event_msg = xstrcpy((char *)UNITMODE[i]);
+			    /* Allways turn everything off after a mode change */
+			    unit->PID_cool->OutP = unit->PID_heat->OutP = 0.0;
+			    unit->PID_cool->Mode = unit->PID_heat->Mode = PID_MODE_NONE;
+			    unit->heater_state = unit->cooler_state = unit->fan_state = unit->light_state = unit->light_timer = 0;
+			    unit->heater_wait = unit->cooler_wait = unit->fan_wait = unit->light_wait = 0;
+			    device_out(unit->heater_address, unit->heater_state);
+			    device_out(unit->cooler_address, unit->cooler_state);
+			    device_out(unit->fan_address, unit->fan_state);
+			    device_out(unit->light_address, unit->light_state);
+			    if (unit->mode == UNITMODE_PROFILE) {
+				/*
+				 * Set a sane default until it will be overruled by the
+				 * main processing loop.
+				 */
+				unit->prof_target_lo = unit->profile_inittemp_lo;
+				unit->prof_target_hi = unit->profile_inittemp_hi;;
+				unit->prof_fridge_mode = 0;
+				unit->prof_state = PROFILE_OFF;
+				unit->prof_started = unit->prof_paused = unit->prof_primary_done = 0;
+				unit->prof_peak_abs = unit->prof_peak_rel = 0.0;
+			    }
+			}
+			break;
+		    }
+		}
+	    }
+
+	    /*
+	     * Unit stage
+	     * {"type":"fermenter","unit":"unit0","stage":"SECONDARY"}
+	     */
+	    if (json_object_object_get_ex(jobj, "stage", &val)) {
+		for (int i = 0; i < 4; i++) {
+		    if (strcmp((char *)json_object_get_string(val), UNITSTAGE[i]) == 0) {
+			if (unit->stage != i) {
+			    syslog(LOG_NOTICE, "DCMD change fermenter %s: stage to %s", unit->alias, UNITSTAGE[i]);
+			    unit->mqtt_flag |= MQTT_FLAG_DATA;
+			    unit->stage = i;
+			    if ((unit->mode != UNITMODE_OFF) && ! unit->event_msg)
+				unit->event_msg = xstrcpy((char *)UNITSTAGE[i]);
+			}
+			break;
+		    }
+		}
+	    }
+
+	    /*
+	     * Unit heater and cooler switch
+	     * {"type":"fermenter","unit":"unit0","heater_state":100,"cooler_state":0}
+	     */
+	    if ((json_object_object_get_ex(jobj, "heater_state", &val)) &&
+		(json_object_object_get_ex(jobj, "cooler_state", &val2)) &&
+		(unit->mode == UNITMODE_NONE)) {
+		if (json_object_get_int(val) != unit->heater_state)
+		    unit->heater_state = json_object_get_int(val);
+		if (json_object_get_int(val2) != unit->cooler_state)
+		    unit->cooler_state = json_object_get_int(val2);
+		if (unit->heater_state || unit->cooler_state)
+		    unit->heater_state = unit->cooler_state = 0;	// Safety
+		unit->mqtt_flag |= MQTT_FLAG_DATA;
+		syslog(LOG_NOTICE, "ws: unit %s heater_state to %d, cooler_state to %d", unit->alias, unit->heater_state, unit->cooler_state);
+		break;
+	    }
+
+	    /*
+             * Unit fan switch
+	     * {"type":"fermenter","unit":"unit0","fan_state":0}
+             */
+	    if ((json_object_object_get_ex(jobj, "fan_state", &val)) && (unit->mode == UNITMODE_NONE)) {
+                if (json_object_get_int(val) != unit->fan_state)
+                    unit->fan_state = json_object_get_int(val);
+                unit->mqtt_flag |= MQTT_FLAG_DATA;
+                syslog(LOG_NOTICE, "ws: unit %s fan_state to %d", unit->alias, unit->fan_state);
+                break;
+            }
+
+	    return;
+	}
+    }
+    syslog(LOG_NOTICE, "fermenter_ws_receive(%s)", buf);
+}
+
+
 static int callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
 {
     struct per_session_data__lws_mirror *pss = (struct per_session_data__lws_mirror *)user;
@@ -123,15 +279,15 @@
 		 * {"node":"rpi01","group_id":"fermenters","control":"reboot"}
 		 * {"node":"rpi01","group_id":"fermenters","control":"rebirth"}
 		 */
-//		if (strncmp(buf, (char *)"{\"device\":\"fermenters\",", 23) == 0) {
-//		    fermenter_ws_receive(buf);
+		if (strncmp(buf, (char *)"{\"type\":\"fermenter\",", 20) == 0) {
+		    fermenter_ws_receive(buf);
 //		} else if (strncmp(buf, (char *)"{\"device\":\"co2meters\",", 22) == 0) {
 //		    co2meter_ws_receive(buf);
 //		} else if (strncmp(buf, (char *)"{\"device\":\"ispindels\",", 22) == 0) {
 //		    ispindel_ws_receive(buf);
 //		} else if (strncmp(buf, (char *)"{\"node\":\"", 9) == 0) {
 //		    node_ws_receive(buf);
-//		}
+		}
 
                 break;
 
--- a/www/css/style.css	Sun Apr 14 14:41:38 2024 +0200
+++ b/www/css/style.css	Mon Apr 15 17:04:57 2024 +0200
@@ -29,3 +29,227 @@
 }
 
 
+#fermenter_table {
+    width: 960px;
+    height: 210px;
+    background: #252526;
+    margin: 5px;
+    border: 2px solid;
+    border-color: #59b4d4;
+    border-radius: 5px 5px 5px 5px;
+}
+
+
+/*
+ * +----------------------------------------------------+
+ * |+-------------------++--------++-------------------+|
+ * ||                   ||        ||                   ||
+ * ||                   || chiller||                   ||
+ * ||                   ||        ||                   ||
+ * ||     Air temp      |+--------+|     Beer temp     ||
+ * ||    Temperature    |          |     Pressure      ||
+ * ||                   |          |                   ||
+ * ||                   |          |                   ||
+ * ||                   |          |                   ||
+ * |+-------------------+          +-------------------+|
+ * +----------------------------------------------------+
+ */
+
+#fermenter_thermometers {
+    width: 960px;
+    height: 390px;
+    float: left;
+    background-color: #252526;
+    margin: 5px;
+    margin-top: 3px;
+    border: 2px solid;
+    border-color: #59b4d4;
+    border-radius: 5px 5px 5px 5px;
+}
+
+
+/*
+ * +----------panel_top------------+
+ * |  door light alarm power LEDs  |
+ * |                               |
+ * +-------------------------------+
+ * +---------panel_display---------+
+ * |+-------------++--------------+|
+ * ||  display 1  ||  display 2   ||
+ * |+-------------++--------------+|
+ * +-------------------------------+
+ * +---------panel_control---------+
+ * |+--------++---------++--------+|
+ * ||  led 1 ||  led 2  ||  led 3 ||
+ * |+--------++---------++--------+|
+ * |+--------++---------++--------+|
+ * ||  sw 1  ||   sw 2  ||  sw 3  ||
+ * |+--------++---------++--------+|
+ * +-------------------------------+
+ * +--------panel_buttons----------+
+ * |                               |
+ * +-------------------------------+
+ */
+#fermenter_panel_top {
+  width: 290px;
+  height: 100px;
+  float: right;
+  margin: 5px;
+  background-color: #252526;
+  border: 2px solid;
+  border-color: #59b4d4;
+  border-radius: 5px 5px 5px 5px;
+}
+
+#fermenter_doorled,
+#fermenter_lightled,
+#fermenter_alarmled,
+#fermenter_powerled {
+  width: 50px;
+  height: 30px;
+  float: left;
+  text-align: center;
+  margin-top: 15px;
+  margin-left: 20px;
+}
+
+#fermenter_panel_display {
+  width: 290px;
+  height:  98px;
+  float: right;
+  margin: 5px;
+  margin-top: 3px;
+  background-color: #252526;
+  border: 2px solid;
+  border-color: #59b4d4;
+  border-radius: 5px 5px 5px 5px;
+}
+
+#fermenter_display {
+  width: 145px;
+  height: 98px;
+  float: left;
+  text-align: center;
+}
+
+
+#fermenter_panel_control {
+  width: 290px;
+  height: 150px;
+  float: right;
+  margin: 5px;
+  margin-top: 3px;
+  background-color: #252526;
+  border: 2px solid;
+  border-color: #59b4d4;
+  border-radius: 5px 5px 5px 5px;
+}
+
+#fermenter_led1,
+#fermenter_led2,
+#fermenter_led3 {
+  width: 96px;
+  height: 30px;
+  float: left;
+  text-align: center;
+  margin-top: 13px;
+}
+
+#fermenter_toggle1 {
+  float: left;
+  margin-left: 29px;
+  margin-top: 20px;
+}
+
+#fermenter_toggle2,
+#fermenter_toggle3 {
+  float: left;
+  margin-left: 60px;
+  margin-top: 20px;
+}
+
+#fermenter_panel_buttons {
+  width: 290px;
+  height: 227px;
+  float: right;
+  margin: 5px;
+  margin-top: 3px;
+  background-color: #252526;
+  border: 2px solid;
+  border-color: #59b4d4;
+  border-radius: 5px 5px 5px 5px;
+}
+
+.LEDred_on {
+    margin: 5px auto;
+    width: 18px;
+    height: 18px;
+    background-color: #F40;
+    border-radius: 50%;
+    box-shadow: #000 0 0px 4px 1px, inset #C33 0 -1px 5px, #f44 0 2px 12px;
+}
+
+.LEDred_off {
+    margin: 5px auto;
+    width: 18px;
+    height: 18px;
+    background-color: #820;
+    border-radius: 50%;
+    box-shadow: #400 0 0px 1px 1px;
+}
+
+.LEDyellow_on {
+    margin: 5px auto;
+    width: 18px;
+    height: 18px;
+    background-color: #FF0;
+    border-radius: 50%;
+    box-shadow: #000 0 0px 4px 1px, inset #860 0 -1px 5px, #DD0 0 2px 12px;
+}
+
+.LEDyellow_off {
+    margin: 5px auto;
+    width: 18px;
+    height: 18px;
+    background-color: #A90;
+    border-radius: 50%;
+    box-shadow: #440 0 0px 1px 1px;
+}
+
+.LEDgreen_on {
+    margin: 5px auto;
+    width: 18px;
+    height: 18px;
+    background-color: #5E0;
+    border-radius: 50%;
+    box-shadow: #000 0 0px 4px 1px, inset #270 0 -1px 5px, #5D0 0 2px 12px;
+}
+
+.LEDgreen_off {
+    margin: 5px auto;
+    width: 18px;
+    height: 18px;
+    background-color: #270;
+    border-radius: 50%;
+    box-shadow: #250 0 0px 1px 1px;
+}
+
+.LEDblue_on {
+    margin: 5px auto;
+    width: 18px;
+    height: 18px;
+    background-color: #4AF;
+    border-radius: 50%;
+    box-shadow: #000 0 0px 4px 1px, inset #247 0 -1px 5px, #48F 0 2px 12px;
+}
+
+.LEDblue_off {
+    margin: 5px auto;
+    width: 18px;
+    height: 18px;
+    background-color: #137;
+    border-radius: 50%;
+    box-shadow: #024 0 0px 1px 1px;
+}
+
+
Binary file www/favicon.ico has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/fermenter.php	Mon Apr 15 17:04:57 2024 +0200
@@ -0,0 +1,109 @@
+<?php
+require_once($_SERVER['DOCUMENT_ROOT'].'/includes/global.inc.php');
+page_header('Monitor Fermentation', 'fermenter');
+?>
+
+   <div id="fermenter">
+
+    <div style="float: left; width: 970px;">
+     <div id="fermenter_table">
+      <div id="fermenter_info">
+       <table style='width: 100%; padding: 10px;'>
+	<col width="20%">
+	<col width="40%">
+        <col width="40%">
+	<tr><th colspan=3>Fermenter overview</th></tr>
+        <tr style="height: 25px;">
+         <td>Room TH</td>
+         <td colspan="2"><div id="room_thb"></div></td>
+        </tr>
+	<tr style="height: 25px;">
+         <td>Fermenter unit</td>
+	 <td colspan="2"><div id="info_system"></div></td>
+        </tr>
+	<tr style="height: 25px;">
+         <td>Code and beer</td>
+         <td colspan="2"><div id="info_beer"></div></td>
+        </tr>
+	<tr style="height: 25px;">
+         <td>Working mode</td>
+         <td><div id="info_mode"></div></td>
+         <td><input type="button" id="Profile1" value="Profile 1" /><input style="margin-left: 10px;" type="button" id="Profile2" value="Profile 2" /></td>
+        </tr>
+	<tr style="height: 25px;">
+         <td>Fermentation fase</td>
+         <td colspan="2"><div id="info_stage"></div></td>
+        </tr>
+	<tr style="height: 25px;">
+         <td>Fermentation profile</td>
+         <td><div id="info_profile"></div></td>
+         <td><div id="status_profile"></div></td>
+        </tr>
+       </table>
+      </div>
+     </div>
+     <div id='fermenter_thermometers'>
+      <div id="gaugeContainer_air" style='float: left; margin-top: 10px; margin-left: 10px;'></div>
+      <div id="gaugeContainer_beer" style="float: right; margin-top: 10px; margin-right: 10px;"></div>
+      <div id="gaugeContainer_chiller" style="float: left; margin-top: 15px;"></div>
+     </div>
+    </div> <!-- End left side -->
+
+    <div style="float: right; width: 295px;">
+     <div id="fermenter_panel_top">
+      <div id="fermenter_doorled"></div>
+      <div id="fermenter_lightled"></div>
+      <div id="fermenter_alarmled"></div>
+      <div id="fermenter_powerled"></div>
+     </div> <!-- fermenter_panel_top -->
+
+     <div id="fermenter_panel_display">
+      <div id="fermenter_display" class="f_display">
+       <div id="target_lo" style="margin-left: 40px; margin-top: 15px;"></div>
+       <div style="margin-top: 5px;">&deg;C low</div>
+      </div>
+      <div id="fermenter_display" class="f_display">
+       <div id="target_hi" style="margin-left: 40px; margin-top: 15px;"></div>
+       <div style="margin-top: 5px;">&deg;C high</div>
+      </div>
+     </div> <!-- fermenter_panel_display -->
+
+     <div id="fermenter_panel_control">
+      <div class="f_control_leds">
+       <div id="fermenter_led1"></div>
+       <div id="fermenter_led2"></div>
+       <div id="fermenter_led3"></div>
+      </div>
+      <div class="f_control_switches">
+       <div id="fermenter_toggle1"></div>
+       <div id="fermenter_toggle2"></div>
+       <div id="fermenter_toggle3"></div>
+      </div>
+     </div> <!-- fermenter_panel_control -->
+
+     <div id="fermenter_panel_buttons">
+     </div> <!-- fermenter_panel_buttons -->
+    </div> <!-- End right side -->
+   </div>
+
+   <div id="eventWindow">
+    <div>
+     Confirm abort profile
+    </div>
+    <div>
+     <div>
+      Press "OK" to abort this profile. When started it will start from the beginning.<br>
+      Press "Cancel" to close without aborting the profile.
+     </div>
+     <div>
+      <div style="float: right; margin-top: 40px; margin-bottom: 10px;">
+       <input type="button" id="delOk" value="OK" style="margin-right: 90px" />
+       <input type="button" id="delCancel" value="Cancel" style="margin-right: 80px" />
+      </div>
+     </div>
+    </div>
+   </div>
+
+<?php
+page_footer();
+?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/getfermenter.php	Mon Apr 15 17:04:57 2024 +0200
@@ -0,0 +1,65 @@
+<?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));
+}
+
+
+if (isset($_GET["uuid"]))
+	    $uuid = $_GET["uuid"];
+else
+	    $uuid = "e53858c3-a97e-4b07-85e9-524257909f45";
+
+$answer = send_cmd("UNIT JSON ".$uuid);
+header("Content-type: application/json");
+
+$arr = explode("\r\n", $answer);
+if (startsWith($arr[0], "213")) {
+    echo $arr[1];
+} else {
+    echo '{}';
+}
+
Binary file www/images/computer.png has changed
--- a/www/includes/global.inc.php	Sun Apr 14 14:41:38 2024 +0200
+++ b/www/includes/global.inc.php	Mon Apr 15 17:04:57 2024 +0200
@@ -27,6 +27,11 @@
 //$my_style = 'ui-redmond';
 $my_style = 'ui-mbse';
 
+if (isset($_GET['uuid']))
+        $my_uuid = $_GET['uuid'];
+else
+        $my_uuid = '';
+
 require_once($_SERVER['DOCUMENT_ROOT'].'/version.php');
 
 
@@ -80,6 +85,7 @@
 function page_header($title, $loadjs) {
 	global $my_style;
 	global $my_version;
+	global $my_uuid;
 ?>
 <!DOCTYPE html>
 <html>
@@ -91,7 +97,7 @@
   <link type="text/css" href="jqwidgets/styles/jqx.<?php echo $my_style; ?>.css" rel="stylesheet" />
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <script>
-    var theme = "<?php echo $my_style; ?>";
+    var theme = "<?php echo $my_style; ?>", my_uuid = "<?php echo $my_uuid; ?>";
   </script>
   <script src="js/jquery-1.11.1.js"></script>
   <script src="js/reconnecting-websocket.min.js"></script>
@@ -99,8 +105,25 @@
   <script src="jqwidgets/jqxwindow.js"></script>
   <script src="jqwidgets/jqxmenu.js"></script>
   <script src="jqwidgets/jqxtabs.js"></script>
+  <script src="jqwidgets/jqxdata.js"></script>
+  <script src="jqwidgets/jqxdata.export.js"></script>
   <script src="jqwidgets/jqxloader.js"></script>
   <script src="jqwidgets/jqxbuttons.js"></script>
+  <script src="jqwidgets/jqxscrollbar.js"></script>
+  <script src="jqwidgets/jqxnumberinput.js"></script>
+  <script src="jqwidgets/jqxlistbox.js"></script>
+  <script src="jqwidgets/jqxdropdownlist.js"></script>
+  <script src="jqwidgets/jqxdropdownbutton.js"></script>
+  <script src="jqwidgets/jqxinput.js"></script>
+  <script src="jqwidgets/jqxeditor.js"></script>
+  <script src="jqwidgets/jqxtooltip.js"></script>
+  <script src="jqwidgets/jqxswitchbutton.js"></script>
+  <script src="jqwidgets/jqxradiobutton.js"></script>
+  <script src="jqwidgets/jqxcheckbox.js"></script>
+
+  <script src="jqwidgets/jqxgauge.js"></script>
+
+  <script src="jqwidgets/jqxdraw.js"></script>
 
   <script src="jqwidgets/globalization/globalize.js"></script>
   <script src="js/global.js"></script>
@@ -132,8 +155,8 @@
                 if (strcmp($arr[$i], ".") == 0)
                         break;
 		$parts = explode(",", $arr[$i]);
-		echo '       <li><img style="float: left; margin-right: 5px;" src="images/fridge.png" />';
-		echo '<a href="fermenter.php?uuid='.$parts[0].'">'.$parts[1].'</a></li>'.PHP_EOL;
+		echo "       <li><img style='float: left; margin-right: 5px;' src='images/fridge.png' />";
+		echo "<a href='fermenter.php?uuid=".$parts[0]."'>".$parts[1]."</a></li>".PHP_EOL;
                 $i++;
             }
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/js/fermenter.js	Mon Apr 15 17:04:57 2024 +0200
@@ -0,0 +1,494 @@
+/*****************************************************************************
+ * Copyright (C) 2024
+ *
+ * Michiel Broek <mbroek at mbse dot eu>
+ *
+ * This file is part of mbsePi-apps thermferm
+ *
+ * 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.
+ *
+ * Brewery Management System istributed 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 createAbortElements() {
+ $('#eventWindow').jqxWindow({
+  theme: theme,
+  position: { x: 440, y: 210 },
+  width: 400,
+  height: 200,
+  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');
+}
+
+
+$(document).ready(function() {
+
+ var record = {},
+ global = {},
+ blank = {},
+ ppayload = '',
+ yl = 12, // Normal yeast temp range
+ yh = 24,
+
+ gaugeoptions = {
+  min: 0, max: 45, width: 375, height: 375,
+  ranges: [{ startValue: 0, endValue: yl, style: { fill: '#3399FF', stroke: '#3399FF' }, endWidth: 10, startWidth: 10 },
+           { startValue: yl, endValue: yh, style: { fill: '#00CC33', stroke: '#00CC33' }, endWidth: 10, startWidth: 10 },
+           { startValue: yh, endValue: 45, style: { fill: '#FC6A6A', stroke: '#FC6A6A' }, endWidth: 10, startWidth: 10 }],
+  ticksMinor: { interval: 1, size: '5%' },
+  ticksMajor: { interval: 5, size: '9%' },
+  labels: { interval: 5 },
+  style: { fill: '#eeeeee', stroke: '#666666' },
+  value: 0,
+  colorScheme: 'scheme05'
+ },
+ gaugeSmalloptions = {
+  min: -15, max: 25, width: 190, height: 190,
+  ranges: [{ startValue: -15, endValue: 0, startWidth: 5, endWidth: 5, style: { fill: '#3399FF', stroke: '#3399FF' }},
+           { startValue: 0, endValue: 10, startWidth: 5, endWidth: 5, style: { fill: '#00CC33', stroke: '#00CC33' }},
+           { startValue: 10, endValue: 25, startWidth: 5, endWidth: 5, style: { fill: '#FC6A6A', stroke: '#FC6A6A' }}],
+  ticksMinor: { interval: 1, size: '5%' },
+  ticksMajor: { interval: 5, size: '9%' },
+  labels: { interval: 5 },
+  style: { fill: '#eeeeee', stroke: '#666666' },
+  value: 0,
+  colorScheme: 'scheme05',
+  caption: { value: 'Chiller', position: 'bottom', offset: [0, 10] }
+ },
+ switchoptions = {
+  height: 68,
+  width: 35,
+  onLabel: 'ON',
+  offLabel: 'OFF',
+  theme: theme,
+  thumbSize: '50%',
+  orientation: 'vertical'
+ },
+ targetoptions = { inputMode: 'simple', theme: theme, width: 70, min: 0, max: 45, decimalDigits: 1, spinButtons: true },
+
+ globalSource = {
+  datatype: 'json',
+  cache: false,
+  datafields: [
+   { name: 'type' },
+   { name: 'name' },
+   { name: 'node' },
+   { name: 'os' },
+   { name: 'os_version' },
+   { name: 'FW' },
+   { name: 'room_temp', map: 'THB>temperature', type: 'float' },
+   { name: 'room_hum', map: 'THB>humidity', type: 'float' }
+  ],
+  id: 'name',
+  url: 'getglobal.php'
+ },
+ globalData = new $.jqx.dataAdapter(globalSource, {
+  loadComplete: function(records) {
+   global = globalData.records[0];
+   updateScreen();
+  }
+ }),
+ url = 'getfermenter.php?uuid=' + my_uuid,
+ source = {
+  datatype: 'json',
+  datafields: [
+   { name: 'type' },
+   { name: 'unit' },
+   { name: 'beercode', map: 'metric>product>code' },
+   { name: 'beername', map: 'metric>product>name' },
+   { name: 'yeast_lo', map: 'metric>product>yeast_lo' },
+   { name: 'yeast_hi', map: 'metric>product>yeast_hi' },
+   { name: 'air_state', map: 'metric>air>state' },
+   { name: 'air_temperature', map: 'metric>air>temperature' },
+   { name: 'beer_state', map: 'metric>beer>state' },
+   { name: 'beer_temperature', map: 'metric>beer>temperature' },
+   { name: 'chiller_state', map: 'metric>chiller>state' },
+   { name: 'chiller_temperature', map: 'metric>chiller>temperature' },
+   { name: 'heater_state', map: 'metric>heater>state' },
+   { name: 'heater_usage', map: 'metric>heater>usage' },
+   { name: 'cooler_state', map: 'metric>cooler>state' },
+   { name: 'cooler_usage', map: 'metric>cooler>usage' },
+   { name: 'fan_state', map: 'metric>fan>state' },
+   { name: 'fan_usage', map: 'metric>fan>usage' },
+   { name: 'light_address', map: 'metric>light>address' },
+   { name: 'light_state', map: 'metric>light>state' },
+   { name: 'light_usage', map: 'metric>light>usage' },
+   { name: 'door_address', map: 'metric>door>address' },
+   { name: 'door_state', map: 'metric>door>state' },
+   { name: 'psu_address', map: 'metric>psu>address' },
+   { name: 'psu_state', map: 'metric>psu>state' },
+   { name: 'mode', map: 'metric>mode' },
+   { name: 'alarm', map: 'metric>alarm', type: 'int' },
+   { name: 'setpoint_high', map: 'metric>setpoint>high' },
+   { name: 'setpoint_low', map: 'metric>setpoint>low' },
+   { name: 'profile_uuid', type: 'string' },
+   { name: 'profile_name', type: 'string' },
+   { name: 'profile_state', type: 'string' },
+   { name: 'profile_percent', type: 'int' },
+   { name: 'profile_inittemp_high', type: 'float' },
+   { name: 'profile_inittemp_low', type: 'float' },
+   { name: 'profile_steps', type: 'string' },
+   { name: 'stage', map: 'metric>stage', type: 'string' },
+   { name: 'beeruuid', map: 'metric>product>uuid' }
+  ],
+  id: 'alias',
+  url: url
+ },
+ dataAdapter = new $.jqx.dataAdapter(source, {
+  loadComplete: function(records) {
+   record = dataAdapter.records[0];
+   updateScreen();
+  }
+ });
+
+ function updateScreen() {
+   $('#room_thb').html(global.room_temp + '&deg;C&nbsp;&nbsp;' + global.room_hum + '% humidity');
+   $('#info_system').html(record.unit);
+   $('#info_beer').html(record.beercode + ' - ' + record.beername);
+   $('#info_mode').jqxDropDownList('selectItem', record.mode);
+   $('#info_stage').jqxDropDownList('selectItem', record.stage);
+   if (record.door_address) {
+     if (record.door_state != '0') {
+      $('#fermenter_doorled').html('<div class="LEDyellow_on"></div>Door');
+     } else {
+      $('#fermenter_doorled').html('<div class="LEDyellow_off"></div>Door');
+     }
+   }
+   if (record.light_address) {
+     if (record.light_state != '0') {
+      $('#fermenter_lightled').html('<div class="LEDyellow_on"></div>Light');
+     } else {
+      $('#fermenter_lightled').html('<div class="LEDyellow_off"></div>Light');
+     }
+   }
+   if (record.mode != 'OFF') {
+    $('#fermenter_powerled').html('<div class="LEDblue_on"></div>Power');
+   } else {
+    $('#fermenter_powerled').html('<div class="LEDblue_off"></div>Power');
+   }
+    if (record.alarm != '0') {
+     $('#fermenter_alarmled').html('<div class="LEDred_on"></div>Alarm');
+    } else {
+     $('#fermenter_alarmled').html('<div class="LEDred_off"></div>Alarm');
+    }
+
+    $('#target_lo').val(record.setpoint_low);
+    $('#target_hi').val(record.setpoint_high);
+    if ((record.mode == 'FRIDGE') || (record.mode == 'BEER')) {
+     $('#target_lo').jqxNumberInput({ readOnly: false, Width: 70, spinButtons: true });
+     $('#target_hi').jqxNumberInput({ readOnly: false, Width: 70, spinButtons: true });
+    } else {
+     $('#target_lo').jqxNumberInput({ readOnly: true, Width: 50, spinButtons: false });
+     $('#target_hi').jqxNumberInput({ readOnly: true, Width: 50, spinButtons: false });
+    }
+
+    $('.f_control_leds').show();
+    if (record.heater_state != '0') {
+     $('#fermenter_led1').html('<div class="LEDgreen_on"></div>Heat');
+    } else {
+     $('#fermenter_led1').html('<div class="LEDgreen_off"></div>Heat');
+    }
+    if (record.cooler_state != '0') {
+     $('#fermenter_led2').html('<div class="LEDgreen_on"></div>Cool');
+    } else {
+     $('#fermenter_led2').html('<div class="LEDgreen_off"></div>Cool');
+    }
+    if (record.fan_state != '0') {
+     $('#fermenter_led3').html('<div class="LEDgreen_on"></div>Fan');
+    } else {
+     $('#fermenter_led3').html('<div class="LEDgreen_off"></div>Fan');
+    }
+
+    if (record.mode == 'NONE') {
+     $('.f_control_switches').show();
+    } else {
+     $('.f_control_switches').hide();
+    }
+    if ((record.heater_state != '0') != $('#fermenter_toggle1').jqxSwitchButton('val'))
+     $('#fermenter_toggle1').val(record.heater_state != '0');
+    if ((record.cooler_state != '0') != $('#fermenter_toggle2').jqxSwitchButton('val'))
+     $('#fermenter_toggle2').val(record.cooler_state != '0');
+    if ((record.fan_state != '0') != $('#fermenter_toggle3').jqxSwitchButton('val'))
+     $('#fermenter_toggle3').val(record.fan_state != '0');
+
+    $('#info_profile').html(record.profile_name);
+    if (record.profile_name == '')
+     $('#info_mode').jqxDropDownList('disableItem', 'PROFILE');
+    else
+     $('#info_mode').jqxDropDownList('enableItem', 'PROFILE');
+
+    if (record.mode == 'PROFILE') {
+     if (record.profile_state == 'OFF') {
+      $('#info_mode').jqxDropDownList({ disabled: false });
+      $('#Profile1').jqxButton({ template: 'success', value: 'Starten' });
+      $('#Profile1').show();
+      $('#Profile2').hide();
+      $('#status_profile').html('');
+     } else if (record.profile_state == 'RUN') {
+      $('#info_mode').jqxDropDownList({ disabled: true });
+      $('#Profile1').jqxButton({ template: 'danger', value: 'Afbreken' });
+      $('#Profile2').jqxButton({ template: 'primary', value: 'Pauze' });
+      $('#Profile1').show();
+      $('#Profile2').show();
+      $('#status_profile').html('Profiel actief, ' + record.profile_percent + '% gereed');
+     } else if (record.profile_state == 'PAUSE') {
+      $('#info_mode').jqxDropDownList({ disabled: true });
+      $('#Profile1').jqxButton({ template: 'danger', value: 'Afbreken' });
+      $('#Profile2').jqxButton({ template: 'success', value: 'Doorgaan' });
+      $('#Profile1').show();
+      $('#Profile2').show();
+      $('#status_profile').html('Profiel pauze, ' + record.profile_percent + '% gereed');
+     } else if (record.profile_state == 'DONE') {
+      $('#info_mode').jqxDropDownList({ disabled: true });
+      $('#Profile1').jqxButton({ template: 'primary', value: 'Profiel Ok' });
+      $('#Profile1').show();
+      $('#Profile2').hide();
+      $('#status_profile').html('Profiel is gereed');
+     }
+    } else {
+     $('#info_mode').jqxDropDownList({ disabled: false });
+     $('#Profile1').hide();
+     $('#Profile2').hide();
+     $('#status_profile').html('');
+    }
+
+    var yl = record.yeast_lo;
+    var yh = record.yeast_hi;
+    var range = { ranges: [{ startValue: 0, endValue: yl, style: { fill: '#3399FF', stroke: '#3399FF' }, endWidth: 10, startWidth: 10 },
+                      { startValue: yl, endValue: yh, style: { fill: '#00CC33', stroke: '#00CC33' }, endWidth: 10, startWidth: 10 },
+                      { startValue: yh, endValue: 45, style: { fill: '#FC6A6A', stroke: '#FC6A6A' }, endWidth: 10, startWidth: 10 }]};
+    $('#gaugeContainer_air').jqxGauge(range);
+    $('#gaugeContainer_beer').jqxGauge(range);
+
+    if (record.air_temperature !== undefined) {
+     $('#gaugeContainer_air').jqxGauge({ caption: { value: 'Air: ' + record.air_temperature.toFixed(3) }});
+     $('#gaugeContainer_air').jqxGauge({ value: record.air_temperature });
+    }
+    if (record.air_state == 'OK') {
+     $('#gaugeContainer_air').jqxGauge({ disabled: false });
+    } else {
+     $('#gaugeContainer_air').jqxGauge({ disabled: true });
+    }
+    if (record.beer_temperature !== undefined) {
+     $('#gaugeContainer_beer').jqxGauge({ caption: { value: 'Beer: ' + record.beer_temperature.toFixed(3) }});
+     $('#gaugeContainer_beer').jqxGauge({ value: record.beer_temperature });
+    }
+    if (record.beer_state == 'OK') {
+     $('#gaugeContainer_beer').jqxGauge({ disabled: false });
+    } else {
+     $('#gaugeContainer_beer').jqxGauge({ disabled: true });
+    }
+    if (record.chiller_temperature !== undefined) {
+     $('#gaugeContainer_chiller').jqxGauge({ value: record.chiller_temperature });
+    }
+    if (record.chiller_state == 'OK') {
+     $('#gaugeContainer_chiller').jqxGauge({ disabled: false });
+    } else {
+     $('#gaugeContainer_chiller').jqxGauge({ disabled: true });
+    }
+ }
+
+ $('#gaugeContainer_air').jqxGauge(gaugeoptions);
+ $('#gaugeContainer_air').jqxGauge({ caption: { value: 'Air: 00.000' }});
+ $('#gaugeContainer_beer').jqxGauge(gaugeoptions);
+ $('#gaugeContainer_beer').jqxGauge({ caption: { value: 'Beer: 00.000' }});
+ $('#gaugeContainer_chiller').jqxGauge(gaugeSmalloptions);
+
+ $('#fermenter_toggle1').jqxSwitchButton(switchoptions);
+ $('#fermenter_toggle2').jqxSwitchButton(switchoptions);
+ $('#fermenter_toggle3').jqxSwitchButton(switchoptions);
+
+ srcMode = ['OFF', 'NONE', 'FRIDGE', 'BEER', 'PROFILE'];
+ srcStage = ['PRIMARY', 'SECONDARY', 'TERTIARY', 'CARBONATION'];
+ $('#info_mode').jqxDropDownList({ theme: theme, source: srcMode, width: 100, height: 24, dropDownHeight: 156 });
+ $('#info_stage').jqxDropDownList({ theme: theme, source: srcStage, width: 150, height: 24, dropDownHeight: 125 });
+
+ $('#target_lo').jqxNumberInput(targetoptions);
+ $('#target_hi').jqxNumberInput(targetoptions);
+
+ $('#Profile1').jqxButton({ template: 'info', width: '150px', height: 24, theme: theme });
+ $('#Profile2').jqxButton({ template: 'info', width: '150px', height: 24, theme: theme });
+ $('#Profile1').hide(); // Hide these until they are needed.
+ $('#Profile2').hide();
+
+ // Get the data immediatly and then at regular intervals to refresh.
+ dataAdapter.dataBind();
+ globalData.dataBind();
+
+ $('#info_mode').on('select', function(event) {
+  if (event.args && event.args.item.value != record.mode) {
+   record.mode = event.args.item.value;
+   console.log('set mode ' + record.mode);
+   var msg = '{"type":"fermenter","unit":"' + record.unit + '","mode":"' + record.mode + '"}';
+   websocket.send(msg);
+  }
+ });
+ $('#info_stage').on('select', function(event) {
+  if (event.args && event.args.item.value != record.stage) {
+   record.stage = event.args.item.value;
+   console.log('set stage ' + record.stage);
+   var msg = '{"type":"fermenter","unit":"' + record.unit + '","stage":"' + record.stage + '"}';
+   websocket.send(msg);
+  }
+ });
+
+ $('#target_lo').on('change', function(event) {
+  record.setpoint_low = parseFloat(event.args.value);
+  // Keep the high target above the low.
+  if (record.setpoint_low > record.setpoint_high) {
+   record.setpoint_high = record.setpoint_low;
+   $('#target_hi').val(record.setpoint_high);
+  }
+  console.log('set setpoints ' + record.setpoint_low + ' ' + record.setpoint_high);
+  websocket.send('{"type":"fermenter","unit":"' + record.unit +
+            '","setpoint_low":' + record.setpoint_low + ',"setpoint_high":' + record.setpoint_high + '}');
+ });
+ $('#target_hi').on('change', function(event) {
+  record.setpoint_high = parseFloat(event.args.value);
+  // Keep the low target below the high.
+  if (record.setpoint_high < record.setpoint_low) {
+   record.setpoint_low = record.setpoint_high;
+   $('#target_lo').val(record.setpoint_low);
+  }
+  console.log('set setpoints ' + record.setpoint_low + ' ' + record.setpoint_high);
+  websocket.send('{"type":"fermenter","unit":"' + record.unit +
+            '","setpoint_low":' + record.setpoint_low + ',"setpoint_high":' + record.setpoint_high + '}');
+ });
+
+ $('#fermenter_toggle1').on('checked', function(event) {
+  if (record.mode == 'NONE' && record.heater_state != 0) {
+   console.log('set heater ' + $("#fermenter_toggle1").jqxSwitchButton('val'));
+   websocket.send('{"type":"fermenter","unit":"' + record.unit + '","heater_state":0}');
+  }
+ });
+ $('#fermenter_toggle1').on('unchecked', function(event) {
+  if (record.mode == 'NONE' && record.heater_state == 0) {
+   console.log('set heater ' + $("#fermenter_toggle1").jqxSwitchButton('val'));
+   websocket.send('{"type":"fermenter","unit":"' + record.unit + '","heater_state":100,"cooler_state":0}');
+  }
+ });
+ $('#fermenter_toggle2').on('checked', function(event) {
+  if (record.mode == 'NONE' && record.cooler_state != 0) {
+   console.log('set cooler ' + $("#fermenter_toggle2").jqxSwitchButton('val'));
+   websocket.send('{"type":"fermenter","unit":"' + record.unit + '","cooler_state":0}');
+  }
+ });
+ $('#fermenter_toggle2').on('unchecked', function(event) {
+  if (record.mode == 'NONE' & record.cooler_state == 0) {
+   console.log('set cooler ' + $("#fermenter_toggle2").jqxSwitchButton('val'));
+   websocket.send('{"type":"fermenter","unit":"' + record.unit + '","cooler_state":100,"heater_state":0}');
+  }
+ });
+ $('#fermenter_toggle3').on('checked', function(event) {
+  if (record.mode == 'NONE' && record.fan_state != 0) {
+   websocket.send('{"type":"fermenter","unit":"' + record.unit + '","fan_state":0}');
+  }
+ });
+ $('#fermenter_toggle3').on('unchecked', function(event) {
+  if (record.mode == 'NONE' && record.fan_state == 0) {
+   websocket.send('{"type":"fermenter","unit":"' + record.unit + '","fan_state":100}');
+  }
+ });
+ $('#Profile1').click(function() {
+  if (record.mode == 'PROFILE') {
+   if (record.profile_state == 'OFF') {
+    websocket.send('{"type":"fermenter","unit":"' + record.unit + '","profile":{"command":"start"}}');
+   } else if ((record.profile_state == 'RUN') || (record.profile_state == 'PAUSE')) {
+    // Open a popup to confirm this action.
+    $('#eventWindow').jqxWindow('open');
+    $('#delOk').click(function() {
+     websocket.send('{"type":"fermenter","unit":"' + record.unit + '","profile":{"command":"abort"}}');
+    });
+   } else if (record.profile_state == 'DONE') {
+    websocket.send('{"type":"fermenter","unit":"' + record.unit + '","profile":{"command":"done"}}');
+   }
+  }
+ });
+ $('#Profile2').click(function() {
+  if (record.mode == 'PROFILE') {
+   if ((record.profile_state == 'RUN') || (record.profile_state == 'PAUSE')) {
+    websocket.send('{"type":"fermenter","unit":"' + record.unit + '","profile":{"command":"pause"}}');
+   }
+  }
+ });
+
+ createAbortElements();
+
+ websocket.onmessage = function(evt) {
+  var msg = evt.data;
+  var obj = JSON.parse(msg);
+
+  console.log('ws got ' + msg);
+
+  if (obj.ping == 1) {
+   console.log('ws got ping');
+   websocket.send('{"pong":1}');
+  }
+
+  if (obj.type == 'fermenter' && obj.unit == record.unit) {
+   console.log('ws got this device ' + msg);
+   record.beeruuid = obj.metric.product.uuid;
+   record.beercode = obj.metric.product.code;
+   record.beername = obj.metric.product.name;
+   record.yeast_lo = obj.metric.product.yeast_lo;
+   record.yeast_hi = obj.metric.product.yeast_hi;
+   record.air_state = obj.metric.air.state;
+   record.air_temperature = obj.metric.air.temperature;
+   record.beer_state = obj.metric.beer.state;
+   record.beer_temperature = obj.metric.beer.temperature;
+   record.chiller_state = obj.metric.chiller.state;
+   record.chiller_temperature = obj.metric.chiller.temperature;
+   if (obj.metric.heater.state !== undefined)
+     record.heater_state = obj.metric.heater.state;
+   if (obj.metric.cooler.state !== undefined)
+     record.cooler_state = obj.metric.cooler.state;
+   if (obj.metric.fan.state !== undefined)
+    record.fan_state = obj.metric.fan.state;
+   if (obj.metric.door)
+     record.door_state = obj.metric.door.state;
+   if (obj.metric.light)
+     record.light_state = obj.metric.light.state;
+   if (obj.metric.psu)
+     record.psu_state = obj.metric.psu.state;
+   record.mode = obj.metric.mode;
+   record.stage = obj.metric.stage;
+   record.alarm = obj.metric.alarm;
+   record.setpoint_low = obj.metric.setpoint.low;
+   record.setpoint_high = obj.metric.setpoint.high;
+   if (obj.profile) {
+     record.profile_uuid = obj.profile_uuid;
+     record.profile_name = obj.profile_name;
+     record.profile_state = obj.profile_state;
+     record.profile_percent = obj.profile_percent;
+     record.profile_inittemp_high = obj.profile_inittemp_high;
+     record.profile_inittemp_low = obj.profile_inittemp_low;
+   } else {
+     record.profile_uuid = '';
+     record.profile_name = '';
+     record.profile_state = '';
+     record.profile_percent = 0;
+   }
+   updateScreen();
+  }
+ }
+});

mercurial