Version 0.9.19b2. Simulator redesign and it is now possible to run more then one simulator. All simulated devices have address names that include the simulator number. Added the setup screen for the most part. Not compatible with previous versions if a simulator was used, delete all simulators and simulated devices during stop and start.

Tue, 30 Apr 2024 17:26:41 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Tue, 30 Apr 2024 17:26:41 +0200
changeset 714
24749c296a50
parent 713
ea24b4ce02b1
child 715
f5d85af156ab

Version 0.9.19b2. Simulator redesign and it is now possible to run more then one simulator. All simulated devices have address names that include the simulator number. Added the setup screen for the most part. Not compatible with previous versions if a simulator was used, delete all simulators and simulated devices during stop and start.

configure file | annotate | diff | comparison | revisions
configure.ac file | annotate | diff | comparison | revisions
thermferm/Makefile file | annotate | diff | comparison | revisions
thermferm/devices.c file | annotate | diff | comparison | revisions
thermferm/rdconfig.c file | annotate | diff | comparison | revisions
thermferm/server.c file | annotate | diff | comparison | revisions
thermferm/simulator.c file | annotate | diff | comparison | revisions
thermferm/simulator.h file | annotate | diff | comparison | revisions
thermferm/thermferm.c file | annotate | diff | comparison | revisions
thermferm/thermferm.h file | annotate | diff | comparison | revisions
www/dbdevices.php file | annotate | diff | comparison | revisions
www/dbsimulators.php file | annotate | diff | comparison | revisions
www/getdevices.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_simulators.js file | annotate | diff | comparison | revisions
www/set_simulators.php file | annotate | diff | comparison | revisions
--- a/configure	Sun Apr 28 15:50:42 2024 +0200
+++ b/configure	Tue Apr 30 17:26:41 2024 +0200
@@ -2037,7 +2037,7 @@
 
 
 PACKAGE="mbsePi-apps"
-VERSION="0.9.19b1"
+VERSION="0.9.19b2"
 COPYRIGHT="Copyright (C) 2014-2024 Michiel Broek, All Rights Reserved"
 CYEARS="2014-2024"
 
--- a/configure.ac	Sun Apr 28 15:50:42 2024 +0200
+++ b/configure.ac	Tue Apr 30 17:26:41 2024 +0200
@@ -8,7 +8,7 @@
 dnl General settings
 dnl After changeing the version number, run autoconf!
 PACKAGE="mbsePi-apps"
-VERSION="0.9.19b1"
+VERSION="0.9.19b2"
 COPYRIGHT="Copyright (C) 2014-2024 Michiel Broek, All Rights Reserved"
 CYEARS="2014-2024"
 AC_SUBST(PACKAGE)
--- a/thermferm/Makefile	Sun Apr 28 15:50:42 2024 +0200
+++ b/thermferm/Makefile	Tue Apr 30 17:26:41 2024 +0200
@@ -67,8 +67,8 @@
 pid.o: thermferm.h pid.h
 rc-switch.o: thermferm.h xutil.h delay.h rc-switch.h
 rdconfig.o: rdconfig.h thermferm.h pid.h futil.h xutil.h
-server.o: rdconfig.h thermferm.h delay.h devices.h server.h lcd-buffer.h xutil.h mqtt.h
-simulator.o: thermferm.h delay.h simulator.h
+server.o: rdconfig.h thermferm.h delay.h one-wire.h devices.h server.h simulator.h lcd-buffer.h xutil.h mqtt.h
+simulator.o: thermferm.h delay.h xutil.h websocket.h simulator.h
 slcd.o: thermferm.h slcd.h futil.h xutil.h
 thermferm.o: lock.h rdconfig.h server.h thermferm.h devices.h delay.h simulator.h lcd-pcf8574.h lcd-buffer.h slcd.h panel.h one-wire.h futil.h xutil.h pid.h mqtt.h statetbl.h websocket.h
 websocket.o: thermferm.h xutil.h devices.h websocket.h
--- a/thermferm/devices.c	Sun Apr 28 15:50:42 2024 +0200
+++ b/thermferm/devices.c	Tue Apr 30 17:26:41 2024 +0200
@@ -250,6 +250,9 @@
     int			rc, i;
     char		buf[40];
 #endif
+#ifdef USE_SIMULATOR
+    simulator_list      *simulator;
+#endif
 
     if (uuid == NULL)
 	return 0;
@@ -314,22 +317,33 @@
 		}
 
 #ifdef USE_SIMULATOR
-		if ((device->type == DEVTYPE_SIM) && (device->direction == DEVDIR_OUT_BIN) && (device->present == DEVPRESENT_YES)) {
-		    if ((strcmp((char *)"SimCooler", device->address) == 0) || (strcmp((char *)"SimHeater", device->address) == 0) ||
-			(strcmp((char *)"SimFan"   , device->address) == 0) || (strcmp((char *)"SimLight" , device->address) == 0)) {
-			if (value != device->value) {
-			    syslog(LOG_NOTICE, "SIM %s value=%d", device->address, value);
-			    device->value = value;
-			    device->timestamp = time(NULL);
-			    if (strcmp((char *)"SimCooler", device->address) == 0) 
-			    	SIMcooling = value;
-			    if (strcmp((char *)"SimHeater", device->address) == 0)
-			    	SIMheating = value;
-			    if (strcmp((char *)"SimFan", device->address) == 0)
-			    	SIMfan = value;
-			    if (strcmp((char *)"SimLight", device->address) == 0)
-			    	SIMlight = value;
-			    devices_ws();
+		if ((device->type == DEVTYPE_SIM) && (device->direction == DEVDIR_OUT_BIN)) {
+		    for (simulator = Config.simulators; simulator; simulator = simulator->next) {
+			if ((strcmp(device->address, simulator->cooler_address) == 0) ||
+			    (strcmp(device->address, simulator->heater_address) == 0) ||
+			    (strcmp(device->address, simulator->fan_address) == 0) ||
+			    (strcmp(device->address, simulator->light_address) == 0)) {
+			    if (value != device->value) {
+				int rc = DEVPRESENT_UNDEF;
+			    	device->value = value;
+                            	device->timestamp = time(NULL);
+			    	if (strcmp(device->address, simulator->cooler_address) == 0) {
+                            	    simulator->cooler_power = value;
+				    rc = simulator->cooler_present;
+			    	} else if (strcmp(device->address, simulator->heater_address) == 0) {
+				    simulator->heater_power = value;
+				    rc = simulator->heater_present;
+			    	} else if (strcmp(device->address, simulator->fan_address) == 0) {
+				    simulator->fan_power = value;
+				    rc = simulator->fan_present;
+			    	} else if (strcmp(device->address, simulator->light_address) == 0) {
+				    simulator->light_power = value;
+				    rc = simulator->light_present;
+			    	}
+				syslog(LOG_NOTICE, "SIM %s value=%d, present=%s", device->address, value, DEVPRESENT[rc]);
+				device->present = rc;
+				devices_ws();
+			    }
 			}
 		    }
 		}
@@ -462,6 +476,9 @@
     int			pin;
     char		buf[40];
 #endif
+#ifdef USE_SIMULATOR
+    simulator_list	*simulator;
+#endif
 
     /*
      * Scan for 1-wire devices. These are already detected by the
@@ -686,87 +703,110 @@
 #endif
 
 #ifdef USE_SIMULATOR
-    found = 0;
-    for (device = Config.devices; device; device = device->next) {
-	if (device->type == DEVTYPE_SIM) {
-	    found++;
-	}
-    }
+
+    for (simulator = Config.simulators; simulator; simulator = simulator->next) {
+	for (i = 0; i < 10; i++) {
+
+	    found = FALSE;
+            for (device = Config.devices; device; device = device->next) {
+            	if ((i == 0 && strcmp(device->address, simulator->room_tempaddress) == 0) ||
+		    (i == 1 && strcmp(device->address, simulator->air_address) == 0) ||
+		    (i == 2 && strcmp(device->address, simulator->beer_address) == 0) ||
+		    (i == 3 && strcmp(device->address, simulator->heater_address) == 0) ||
+		    (i == 4 && strcmp(device->address, simulator->cooler_address) == 0) ||
+		    (i == 5 && strcmp(device->address, simulator->room_humaddress) == 0) ||
+		    (i == 6 && strcmp(device->address, simulator->chiller_address) == 0) ||
+		    (i == 7 && strcmp(device->address, simulator->fan_address) == 0) ||
+		    (i == 8 && strcmp(device->address, simulator->light_address) == 0) ||
+		    (i == 9 && strcmp(device->address, simulator->beer_address2) == 0)) {
+                    found = TRUE;
+                    break;
+            	}
+            }
+
+	    if (found == FALSE) {
+		ndev = (devices_list *)malloc(sizeof(devices_list));
+		ndev->next = NULL;
+		ndev->uuid = malloc(37);
+                uuid_generate(uu);
+                uuid_unparse(uu, ndev->uuid);
+		ndev->type = DEVTYPE_SIM;
+        	ndev->value = ndev->offset = 0;
+        	ndev->present = DEVPRESENT_NO;
+        	ndev->gpiopin = -1;
+		ndev->subdevice = i;
+        	ndev->comment = xstrcpy((char *)"Auto detected device");
+        	ndev->timestamp = time(NULL);
+        	ndev->inuse = 0;
 
-    /*
-     * Create simulated devices, or upgrade with new devices.
-     */
-    subdevices = 10;
-    for (i = found; i < subdevices; i++) {
-	ndev = (devices_list *)malloc(sizeof(devices_list));
-	ndev->next = NULL;
-	ndev->uuid = malloc(37);
-	uuid_generate(uu);
-	uuid_unparse(uu, ndev->uuid);
-	ndev->type = DEVTYPE_SIM;
-	ndev->value = ndev->offset = 0;
-	ndev->present = DEVPRESENT_YES;
-	ndev->subdevice = i;
-	ndev->gpiopin = -1;
-	ndev->comment = xstrcpy((char *)"Auto detected device");
-	ndev->timestamp = time(NULL);
-	ndev->inuse = 0;
-	switch (i) {
-	    case 0:	ndev->direction = DEVDIR_IN_ANALOG;
-			ndev->address = xstrcpy((char *)"SimRoomTemp");
-			ndev->description = xstrcpy((char *)"Simulated room temperature");
-			break;
-	    case 1:	ndev->direction = DEVDIR_IN_ANALOG;
-			ndev->address = xstrcpy((char *)"SimAirTemp");
-			ndev->description = xstrcpy((char *)"Simulated air temperature");
-			break;
-	    case 2:	ndev->direction = DEVDIR_IN_ANALOG;
-			ndev->address = xstrcpy((char *)"SimBeerTemp");
-			ndev->description = xstrcpy((char *)"Simulated beer temperature");
-			break;
-	    case 3:	ndev->direction = DEVDIR_OUT_BIN;
-			ndev->address = xstrcpy((char *)"SimHeater");
-			ndev->description = xstrcpy((char *)"Simulated heater");
-			break;
-	    case 4:	ndev->direction = DEVDIR_OUT_BIN;
-			ndev->address = xstrcpy((char *)"SimCooler");
-			ndev->description = xstrcpy((char *)"Simulated cooler");
-			break;
-	    case 5:	ndev->direction = DEVDIR_IN_ANALOG;
-			ndev->address = xstrcpy((char *)"SimRoomHum");
-			ndev->description = xstrcpy((char *)"Simulated room humidity");
-			break;
-	    case 6:	ndev->direction = DEVDIR_IN_ANALOG;
-			ndev->address = xstrcpy((char *)"SimChillerTemp");
-			ndev->description = xstrcpy((char *)"Simulated Chiller temperature");
-			break;
-	    case 7:	ndev->direction = DEVDIR_OUT_BIN;
-			ndev->address = xstrcpy((char *)"SimFan");
-			ndev->description = xstrcpy((char *)"Simulated fan");
-			break;
-	    case 8:	ndev->direction = DEVDIR_OUT_BIN;
-			ndev->address = xstrcpy((char *)"SimLight");
-			ndev->description = xstrcpy((char *)"Simulated light");
-			break;
-	    case 9:	ndev->direction = DEVDIR_IN_ANALOG;
-			ndev->address = xstrcpy((char *)"SimBeerTemp2");
-			ndev->description = xstrcpy((char *)"Simulated beer temperature (alt)");
-			break;
-	}
+		switch (i) {
+		    case 0:	ndev->direction = DEVDIR_IN_ANALOG;
+				ndev->address = xstrcpy(simulator->room_tempaddress);
+				ndev->description = xstrcpy((char *)"Simulated room temperature");
+				ndev->present = DEVPRESENT_YES;
+				break;
+		    case 1:	ndev->direction = DEVDIR_IN_ANALOG;
+				ndev->address = xstrcpy(simulator->air_address);
+				ndev->description = xstrcpy((char *)"Simulated air temperature");
+				ndev->present = simulator->air_present;
+				break;
+		    case 2:	ndev->direction = DEVDIR_IN_ANALOG;
+				ndev->address = xstrcpy(simulator->beer_address);
+				ndev->description = xstrcpy((char *)"Simulated beer temperature");
+				ndev->present = simulator->beer_present;
+				break;
+		    case 3:	ndev->direction = DEVDIR_OUT_BIN;
+				ndev->address = xstrcpy(simulator->heater_address);
+				ndev->description = xstrcpy((char *)"Simulated heater");
+				ndev->present = simulator->heater_present;
+				break;
+		    case 4:	ndev->direction = DEVDIR_OUT_BIN;
+				ndev->address = xstrcpy(simulator->cooler_address);
+				ndev->description = xstrcpy((char *)"Simulated cooler");
+				ndev->present = simulator->cooler_present;
+				break;
+		    case 5:	ndev->direction = DEVDIR_IN_ANALOG;
+				ndev->address = xstrcpy(simulator->room_humaddress);
+				ndev->description = xstrcpy((char *)"Simulated room humidity");
+				ndev->present = DEVPRESENT_YES;
+				break;
+		    case 6:	ndev->direction = DEVDIR_IN_ANALOG;
+				ndev->address = xstrcpy(simulator->chiller_address);
+				ndev->description = xstrcpy((char *)"Simulated Chiller temperature");
+				ndev->present = simulator->chiller_present;
+				break;
+		    case 7:	ndev->direction = DEVDIR_OUT_BIN;
+				ndev->address = xstrcpy(simulator->fan_address);
+				ndev->description = xstrcpy((char *)"Simulated fan");
+				ndev->present = simulator->fan_present;
+				break;
+		    case 8:	ndev->direction = DEVDIR_OUT_BIN;
+				ndev->address = xstrcpy(simulator->light_address);
+				ndev->description = xstrcpy((char *)"Simulated light");
+				ndev->present = simulator->light_present;
+				break;
+		    case 9:	ndev->direction = DEVDIR_IN_ANALOG;
+				ndev->address = xstrcpy(simulator->beer_address2);
+				ndev->description = xstrcpy((char *)"Simulated beer temperature (alt)");
+				ndev->present = simulator->beer_present2;
+				break;
+		}
 
-	syslog(LOG_NOTICE, "New Simulator device %s, subdevice %d, %s", ndev->address, ndev->subdevice, ndev->description);
+		syslog(LOG_NOTICE, "New Simulator device %s, subdevice %d", ndev->address, ndev->subdevice);
 
-	if (Config.devices == NULL) {
-	    Config.devices = ndev;
-	} else {
-	    for (device = Config.devices; device; device = device->next) {
-		if (device->next == NULL) {
-		    device->next = ndev;
-		    break;
+		if (Config.devices == NULL) {
+		    Config.devices = ndev;
+		} else {
+		    for (device = Config.devices; device; device = device->next) {
+			if (device->next == NULL) {
+			    device->next = ndev;
+			    break;
+			}
+		    }
 		}
+    		rc++;
 	    }
 	}
-    	rc++;
     }
 #endif
 
--- a/thermferm/rdconfig.c	Sun Apr 28 15:50:42 2024 +0200
+++ b/thermferm/rdconfig.c	Tue Apr 30 17:26:41 2024 +0200
@@ -154,6 +154,26 @@
 	    free(simulator->uuid);
 	if (simulator->name)
 	    free(simulator->name);
+	if (simulator->room_tempaddress)
+	    free(simulator->room_tempaddress);
+	if (simulator->room_humaddress)
+	    free(simulator->room_humaddress);
+	if (simulator->air_address)
+	    free(simulator->air_address);
+	if (simulator->beer_address)
+	    free(simulator->beer_address);
+	if (simulator->beer_address2)
+	    free(simulator->beer_address2);
+	if (simulator->chiller_address)
+	    free(simulator->chiller_address);
+	if (simulator->cooler_address)
+	    free(simulator->cooler_address);
+	if (simulator->heater_address)
+	    free(simulator->heater_address);
+	if (simulator->fan_address)
+	    free(simulator->fan_address);
+	if (simulator->light_address)
+	    free(simulator->light_address);
 	free(simulator);
     }
     Config.simulators = NULL;
@@ -430,22 +450,45 @@
     	    xmlTextWriterStartElement(writer, BAD_CAST "SIMULATOR");
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "UUID", "%s", simulator->uuid);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "NAME", "%s", simulator->name);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "SIMNO", "%d", simulator->simno);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "VOLUME_AIR", "%d", simulator->volume_air);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "VOLUME_BEER", "%d", simulator->volume_beer);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "ROOM_TEMPADDRESS", "%s", simulator->room_tempaddress);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "ROOM_TEMPERATURE", "%.1f", simulator->room_temperature);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "ROOM_HUMADDRESS", "%s", simulator->room_humaddress);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "ROOM_HUMIDITY", "%.1f", simulator->room_humidity);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "AIR_ADDRESS", "%s", simulator->air_address);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "AIR_TEMPERATURE", "%f", simulator->air_temperature);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "AIR_PRESENT", "%s", DEVPRESENT[simulator->air_present]);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "BEER_ADDRESS", "%s", simulator->beer_address);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "BEER_TEMPERATURE", "%f", simulator->beer_temperature);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "BEER_PRESENT", "%s", DEVPRESENT[simulator->beer_present]);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "BEER_ADDRESS2", "%s", simulator->beer_address2);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "BEER_TEMPERATURE2", "%f", simulator->beer_temperature2);
+            xmlTextWriterWriteFormatElement(writer, BAD_CAST "BEER_PRESENT2", "%s", DEVPRESENT[simulator->beer_present2]);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "CHILLER_ADDRESS", "%s", simulator->chiller_address);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "CHILLER_TEMPERATURE", "%f", simulator->chiller_temperature);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "CHILLER_PRESENT", "%s", DEVPRESENT[simulator->chiller_present]);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "COOLER_ADDRESS", "%s", simulator->cooler_address);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "COOLER_TEMP", "%f", simulator->cooler_temp);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "COOLER_TIME", "%d", simulator->cooler_time);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "COOLER_SIZE", "%.3f", simulator->cooler_size);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "COOLER_PRESENT", "%s", DEVPRESENT[simulator->cooler_present]);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "COOLER_POWER", "%d", simulator->cooler_power);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "HEATER_ADDRESS", "%s", simulator->heater_address);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "HEATER_TEMP", "%f", simulator->heater_temp);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "HEATER_TIME", "%d", simulator->heater_time);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "HEATER_SIZE", "%.3f", simulator->heater_size);
-	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "HEATER_STATE", "%d", simulator->heater_state);
-	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "COOLER_STATE", "%d", simulator->cooler_state);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "HEATER_PRESENT", "%s", DEVPRESENT[simulator->heater_present]);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "HEATER_POWER", "%d", simulator->heater_power);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "FAN_ADDRESS", "%s", simulator->fan_address);
+            xmlTextWriterWriteFormatElement(writer, BAD_CAST "FAN_PRESENT", "%s", DEVPRESENT[simulator->fan_present]);
+            xmlTextWriterWriteFormatElement(writer, BAD_CAST "FAN_POWER", "%d", simulator->fan_power);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "LIGHT_ADDRESS", "%s", simulator->light_address);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "LIGHT_PRESENT", "%s", DEVPRESENT[simulator->light_present]);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "LIGHT_POWER", "%d", simulator->light_power);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "FRIGO_ISOLATION", "%.3f", simulator->frigo_isolation);
+	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "TIMESTAMP", "%ld", (long)simulator->timestamp);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "S_YEAST_HEAT", "%f", simulator->s_yeast_heat);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "S_YEAST_STARTED", "%d", (int)simulator->s_yeast_started);
 	    xmlTextWriterWriteFormatElement(writer, BAD_CAST "S_COOL_TEMP", "%f", simulator->s_cool_temp);
@@ -1328,18 +1371,22 @@
 {
     xmlChar             *key;
     simulator_list      *simulator, *tmp;
-    int                 ival;
+    int                 ival, i;
     float		fval;
 
     simulator = (simulator_list *)malloc(sizeof(simulator_list));
     simulator->next = NULL;
     simulator->uuid = simulator->name = NULL;
-    simulator->volume_air = simulator->volume_beer = 0;
+    simulator->room_tempaddress = simulator->room_humaddress = simulator->air_address = simulator->beer_address = simulator->beer_address2 = NULL;
+    simulator->chiller_address = simulator->cooler_address = simulator->heater_address = simulator->fan_address = simulator->light_address = NULL;
+    simulator->simno = simulator->volume_air = simulator->volume_beer = 0;
     simulator->room_temperature = simulator->air_temperature = simulator->beer_temperature = simulator->s_cool_temp = simulator->s_heat_temp = 20.0;
     simulator->chiller_temperature = 1.5;
     simulator->room_humidity = 49.2;
     simulator->cooler_temp = simulator->cooler_size = simulator->heater_temp = simulator->heater_size = simulator->frigo_isolation = 0.0;
-    simulator->cooler_time = simulator->heater_time = simulator->cooler_state = simulator->heater_state = 0;
+    simulator->cooler_time = simulator->heater_time = 0;
+    simulator->air_present = simulator->beer_present = DEVPRESENT_YES;
+    simulator->beer_present2 = simulator->chiller_present = simulator->cooler_present = simulator->heater_present = DEVPRESENT_UNDEF;
     simulator->s_yeast_started = simulator->s_cool_changed = simulator->s_heat_changed = (time_t)0;
     simulator->s_yeast_heat = simulator->s_cool_temp = simulator->s_heat_temp = 0.0;
 
@@ -1351,6 +1398,12 @@
 	if ((!xmlStrcmp(cur->name, (const xmlChar *)"NAME"))) {
 	    simulator->name = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
 	}
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"SIMNO"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            if (sscanf((const char *)key, "%d", &ival) == 1)
+                simulator->simno = ival;
+            xmlFree(key);
+        }
 	if ((!xmlStrcmp(cur->name, (const xmlChar *)"VOLUME_AIR"))) {
 	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
 	    if (sscanf((const char *)key, "%d", &ival) == 1)
@@ -1363,36 +1416,103 @@
 		simulator->volume_beer = ival;
 	    xmlFree(key);
 	}
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"ROOM_TEMPADDRESS"))) {
+            simulator->room_tempaddress = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+        }
 	if ((!xmlStrcmp(cur->name, (const xmlChar *)"ROOM_TEMPERATURE"))) {
 	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
 	    if (sscanf((const char *)key, "%f", &fval) == 1)
 		simulator->room_temperature = fval;
 	    xmlFree(key);
 	}
-	if ((!xmlStrcmp(cur->name, (const xmlChar *)"ROOM_GUMIDITY"))) {
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"ROOM_HUMADDRESS"))) {
+            simulator->room_humaddress = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+        }
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"ROOM_HUMIDITY"))) {
 	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
 	    if (sscanf((const char *)key, "%f", &fval) == 1)
 		simulator->room_humidity= fval;
 	    xmlFree(key);
 	}
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"AIR_ADDRESS"))) {
+            simulator->air_address = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+        }
 	if ((!xmlStrcmp(cur->name, (const xmlChar *)"AIR_TEMPERATURE"))) {
 	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
 	    if (sscanf((const char *)key, "%f", &fval) == 1)
 		simulator->air_temperature = fval;
 	    xmlFree(key);
 	}
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"AIR_PRESENT"))) {
+	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            for (i = 0; i < 4; i++) {
+                if (! xmlStrcmp(key, (const xmlChar *)DEVPRESENT[i])) {
+                    simulator->air_present = i;
+                    break;
+                }
+            }
+            xmlFree(key);
+	}
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"BEER_ADDRESS"))) {
+            simulator->beer_address = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+        }
 	if ((!xmlStrcmp(cur->name, (const xmlChar *)"BEER_TEMPERATURE"))) {
 	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
 	    if (sscanf((const char *)key, "%f", &fval) == 1)
 		simulator->beer_temperature = fval;
 	    xmlFree(key);
 	}
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"BEER_PRESENT"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            for (i = 0; i < 4; i++) {
+                if (! xmlStrcmp(key, (const xmlChar *)DEVPRESENT[i])) {
+                    simulator->beer_present = i;
+                    break;
+                }
+            }
+            xmlFree(key);
+        }
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"BEER_ADDRESS2"))) {
+            simulator->beer_address2 = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+        }
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"BEER_TEMPERATURE2"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            if (sscanf((const char *)key, "%f", &fval) == 1)
+                simulator->beer_temperature2 = fval;
+            xmlFree(key);
+        }
+        if ((!xmlStrcmp(cur->name, (const xmlChar *)"BEER_PRESENT2"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            for (i = 0; i < 4; i++) {
+                if (! xmlStrcmp(key, (const xmlChar *)DEVPRESENT[i])) {
+                    simulator->beer_present2 = i;
+                    break;
+                }
+            }
+            xmlFree(key);
+        }
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"CHILLER_ADDRESS"))) {
+            simulator->chiller_address = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+        }
 	if ((!xmlStrcmp(cur->name, (const xmlChar *)"CHILLER_TEMPERATURE"))) {
 	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
 	    if (sscanf((const char *)key, "%f", &fval) == 1)
 		simulator->chiller_temperature = fval;
 	    xmlFree(key);
 	}
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"CHILLER_PRESENT"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            for (i = 0; i < 4; i++) {
+                if (! xmlStrcmp(key, (const xmlChar *)DEVPRESENT[i])) {
+                    simulator->chiller_present = i;
+                    break;
+                }
+            }
+            xmlFree(key);
+        }
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"COOLER_ADDRESS"))) {
+            simulator->cooler_address = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+        }
 	if ((!xmlStrcmp(cur->name, (const xmlChar *)"COOLER_TEMP"))) {
 	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
 	    if (sscanf((const char *)key, "%f", &fval) == 1)
@@ -1411,6 +1531,25 @@
 		simulator->cooler_size = fval;
 	    xmlFree(key);
 	}
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"COOLER_PRESENT"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            for (i = 0; i < 4; i++) {
+                if (! xmlStrcmp(key, (const xmlChar *)DEVPRESENT[i])) {
+                    simulator->cooler_present = i;
+                    break;
+                }
+            }
+            xmlFree(key);
+        }
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"COOLER_POWER"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            if (sscanf((const char *)key, "%d", &ival) == 1)
+                simulator->cooler_power = ival;
+            xmlFree(key);
+        }
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"HEATER_ADDRESS"))) {
+            simulator->heater_address = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+        }
 	if ((!xmlStrcmp(cur->name, (const xmlChar *)"HEATER_TEMP"))) {
 	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
 	    if (sscanf((const char *)key, "%f", &fval) == 1)
@@ -1429,24 +1568,72 @@
 		simulator->heater_size = fval;
 	    xmlFree(key);
 	}
-	if ((!xmlStrcmp(cur->name, (const xmlChar *)"HEATER_STATE"))) {
-	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
-	    if (sscanf((const char *)key, "%d", &ival) == 1)
-		simulator->heater_state = ival;
-	    xmlFree(key);
-	}
-	if ((!xmlStrcmp(cur->name, (const xmlChar *)"COOLER_STATE"))) {
-	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
-	    if (sscanf((const char *)key, "%d", &ival) == 1)
-		simulator->cooler_state = ival;
-	    xmlFree(key);
-	}
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"HEATER_PRESENT"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            for (i = 0; i < 4; i++) {
+                if (! xmlStrcmp(key, (const xmlChar *)DEVPRESENT[i])) {
+                    simulator->heater_present = i;
+                    break;
+                }
+            }
+            xmlFree(key);
+        }
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"HEATER_POWER"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            if (sscanf((const char *)key, "%d", &ival) == 1)
+                simulator->heater_power = ival;
+            xmlFree(key);
+        }
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"FAN_ADDRESS"))) {
+            simulator->fan_address = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+        }
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"FAN_PRESENT"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            for (i = 0; i < 4; i++) {
+                if (! xmlStrcmp(key, (const xmlChar *)DEVPRESENT[i])) {
+                    simulator->fan_present = i;
+                    break;
+                }
+            }
+            xmlFree(key);
+        }
+        if ((!xmlStrcmp(cur->name, (const xmlChar *)"FAN_POWER"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            if (sscanf((const char *)key, "%d", &ival) == 1)
+                simulator->fan_power = ival;
+            xmlFree(key);
+        }
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"LIGHT_ADDRESS"))) {
+            simulator->light_address = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+        }
+        if ((!xmlStrcmp(cur->name, (const xmlChar *)"LIGHT_PRESENT"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            for (i = 0; i < 4; i++) {
+                if (! xmlStrcmp(key, (const xmlChar *)DEVPRESENT[i])) {
+                    simulator->light_present = i;
+                    break;
+                }
+            }
+            xmlFree(key);
+        }
+        if ((!xmlStrcmp(cur->name, (const xmlChar *)"LIGHT_POWER"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            if (sscanf((const char *)key, "%d", &ival) == 1)
+                simulator->light_power = ival;
+            xmlFree(key);
+        }
 	if ((!xmlStrcmp(cur->name, (const xmlChar *)"FRIGO_ISOLATION"))) {
 	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
 	    if (sscanf((const char *)key, "%f", &fval) == 1)
 		simulator->frigo_isolation = fval;
 	    xmlFree(key);
 	}
+	if ((!xmlStrcmp(cur->name, (const xmlChar *)"TIMESTAMP"))) {
+            key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
+            if (sscanf((const char *)key, "%d", &ival) == 1)
+                simulator->timestamp = ival;
+            xmlFree(key);
+        }
 
 	if ((!xmlStrcmp(cur->name, (const xmlChar *)"S_YEAST_HEAT"))) {
 	    key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1);
--- a/thermferm/server.c	Sun Apr 28 15:50:42 2024 +0200
+++ b/thermferm/server.c	Tue Apr 30 17:26:41 2024 +0200
@@ -26,6 +26,7 @@
 #include "one-wire.h"
 #include "devices.h"
 #include "server.h"
+#include "simulator.h"
 #include "lcd-buffer.h"
 #include "xutil.h"
 #include "mqtt.h"
@@ -1003,7 +1004,7 @@
 {
     char		*opt, *param, *kwd, *val, ibuf[SS_BUFSIZE];
     simulator_list	*simulator, *tmps;
-    int			rc, rlen, ival;
+    int			rc, rlen, ival, i;
     float		fval;
     uuid_t		uu;
 
@@ -1024,6 +1025,7 @@
 	srv_send(s, (char *)"SIMULATOR LIST                List all Simulators");
 	srv_send(s, (char *)"SIMULATOR GET uuid            Get Simulator record by uuid");
 	srv_send(s, (char *)"SIMULATOR PUT uuid            Put Simulator record by uuid");
+	srv_send(s, (char *)"SIMULATOR JSON                Get json records");
 	srv_send(s, (char *)".");
 	return 0;
     }
@@ -1031,23 +1033,57 @@
     if (strcmp(opt, (char *)"LIST") == 0) {
 	srv_send(s, (char *)"212 Simulators list follows:");
 	for (simulator = Config.simulators; simulator; simulator = simulator->next) {
-	    srv_send(s, (char *)"%s,%s", simulator->uuid, simulator->name);
+	    srv_send(s, (char *)"%s,%d,%s", simulator->uuid, simulator->simno, simulator->name);
 	}
 	srv_send(s, (char *)".");
 	return 0;
     }
 
+    if (strcmp(opt, (char *)"JSON") == 0) {
+	char    *payload = NULL, *payloadu = NULL;
+        bool    comma = false;
+
+	if (param == NULL) {
+	    srv_send(s, (char *)"212 Simulators json list follows:");
+            payload = xstrcpy((char *)"[");
+	    for (simulator = Config.simulators; simulator; simulator = simulator->next) {
+                if (comma)
+                    payload = xstrcat(payload, (char *)",");
+                payloadu = simulator_json(simulator);
+                payload = xstrcat(payload, payloadu);
+                comma = true;
+                free(payloadu);
+                payloadu = NULL;
+            }
+            payload = xstrcat(payload, (char *)"]");
+            large_send(s, payload);
+            srv_send(s, (char *)".");
+            free(payload);
+            payload = NULL;
+            return 0;
+        }
+    }
+
     if (param == NULL) {
 	srv_send(s, (char *)"502 Parameter missing");
 	return 0;
     }
 
     if (strcmp(opt, (char *)"ADD") == 0) {
+	int	highno = 0, count = 0;
+	char	abuf[64];
 
 	/*
 	 * For now, only one simulator is allowed.
 	 */
 	if (Config.simulators) {
+	    for (tmps = Config.simulators; tmps; tmps = tmps->next) {
+		if (tmps->simno > highno)
+		    highno = tmps->simno;
+		count++;
+	    }
+	}
+	if (count >= 5) {
 	    srv_send(s, (char *)"441 Maximum simulators reached");
 	    return 0;
 	}
@@ -1058,10 +1094,33 @@
 	uuid_generate(uu);
 	uuid_unparse(uu, simulator->uuid);
 	simulator->name = xstrcpy(param);
+	simulator->simno = highno + 1;
+	sprintf(abuf, "%d-", simulator->simno);
 	simulator->volume_air = 150;
 	simulator->volume_beer = 50;
-	simulator->room_temperature = simulator->air_temperature = simulator->beer_temperature = simulator->s_cool_temp = simulator->s_heat_temp = 20.0;
+	simulator->room_tempaddress = xstrcpy(abuf);
+	simulator->room_tempaddress = xstrcat(simulator->room_tempaddress, (char *)"SimRoomTemp");
+	simulator->room_temperature = simulator->air_temperature = simulator->beer_temperature = simulator->beer_temperature2 = 20.0;
+	simulator->s_cool_temp = simulator->s_heat_temp = 20.0;
+	simulator->room_humaddress = xstrcpy(abuf);
+        simulator->room_humaddress = xstrcat(simulator->room_humaddress, (char *)"SimRoomHum");
 	simulator->room_humidity = 48.6;
+	simulator->air_address = xstrcpy(abuf);
+	simulator->air_address = xstrcat(simulator->air_address, (char *)"SimAirTemp");
+	simulator->beer_address = xstrcpy(abuf);
+        simulator->beer_address = xstrcat(simulator->beer_address, (char *)"SimBeerTemp");
+	simulator->beer_address2 = xstrcpy(abuf);
+        simulator->beer_address2 = xstrcat(simulator->beer_address2, (char *)"SimBeerTemp2");
+	simulator->chiller_address = xstrcpy(abuf);
+        simulator->chiller_address = xstrcat(simulator->chiller_address, (char *)"SimChillerTemp");
+	simulator->heater_address = xstrcpy(abuf);
+        simulator->heater_address = xstrcat(simulator->heater_address, (char *)"SimHeater");
+	simulator->cooler_address = xstrcpy(abuf);
+        simulator->cooler_address = xstrcat(simulator->cooler_address, (char *)"SimCooler");
+	simulator->fan_address = xstrcpy(abuf);
+        simulator->fan_address = xstrcat(simulator->fan_address, (char *)"SimFan");
+	simulator->light_address = xstrcpy(abuf);
+        simulator->light_address = xstrcat(simulator->light_address, (char *)"SimLight");
 	simulator->chiller_temperature = 1.5;	/* Chiller temperature */
 	simulator->cooler_temp =  1.5;	/* Cooling temperature */
 	simulator->cooler_time = 720;	/* About 12 minutes for the cooler plate */
@@ -1069,8 +1128,10 @@
 	simulator->heater_temp = 150.0;	/* Heating temperature */
 	simulator->heater_time = 3;	/* 3 seconds to heat-up	*/
 	simulator->heater_size = 0.01;	/* 0.01 square meter heater plate */
-	simulator->heater_state = simulator->cooler_state = 0;
+	simulator->air_present = simulator->beer_present = DEVPRESENT_YES;
+	simulator->beer_present2 = simulator->chiller_present = simulator->cooler_present = simulator->heater_present = DEVPRESENT_UNDEF;
 	simulator->frigo_isolation = 0.002;
+	simulator->timestamp = time(NULL);
 	simulator->s_yeast_heat = 0.0;
 	simulator->s_yeast_started = simulator->s_cool_changed = simulator->s_heat_changed = (int)0;
 
@@ -1085,12 +1146,14 @@
 	    }
 	}
 
-	syslog(LOG_NOTICE, "Simulator %s added", simulator->uuid);
+	syslog(LOG_NOTICE, "Simulator %s no %d added", simulator->uuid, simulator->simno);
 	srv_send(s, (char *)"211 Simulator %s added", simulator->uuid);
 	return 1;
     }
 
     if (strcmp(opt, (char *)"DEL") == 0) {
+	    // TODO: check devices in use.
+	    // TODO: delete simulated devices.
 	rc = delete_Simulator(param);
 	if (rc) {
 	    syslog(LOG_NOTICE, "Simulator %s deleted", param);
@@ -1107,22 +1170,45 @@
 	    if (strcmp(simulator->uuid, param) == 0) {
 		srv_send(s, (char *)"213 Simulator record follows:");
 		srv_send(s, (char *)"NAME,%s", simulator->name);
+		srv_send(s, (char *)"SIMNO,%d", simulator->simno);
 		srv_send(s, (char *)"VOLUME_AIR,%d", simulator->volume_air);
 		srv_send(s, (char *)"VOLUME_BEER,%d", simulator->volume_beer);
+		srv_send(s, (char *)"ROOM_TEMPADDRESS,%s", simulator->room_tempaddress);
 		srv_send(s, (char *)"ROOM_TEMPERATURE,%.1f", simulator->room_temperature);
+		srv_send(s, (char *)"ROOM_HUMADDRESS,%s", simulator->room_humaddress);
 		srv_send(s, (char *)"ROOM_HUMIDITY,%.1f", simulator->room_humidity);
+		srv_send(s, (char *)"AIR_ADDRESS,%s", simulator->air_address);
 		srv_send(s, (char *)"AIR_TEMPERATURE,%.3f", simulator->air_temperature);
+		srv_send(s, (char *)"AIR_PRESENT,%s", DEVPRESENT[simulator->air_present]);
+		srv_send(s, (char *)"BEER_ADDRESS,%s", simulator->beer_address);
 		srv_send(s, (char *)"BEER_TEMPERATURE,%.3f", simulator->beer_temperature);
+		srv_send(s, (char *)"BEER_PRESENT,%s", DEVPRESENT[simulator->beer_present]);
+		srv_send(s, (char *)"BEER_ADDRESS2,%s", simulator->beer_address2);
+		srv_send(s, (char *)"BEER_TEMPERATURE2,%.3f", simulator->beer_temperature2);
+                srv_send(s, (char *)"BEER_PRESENT2,%s", DEVPRESENT[simulator->beer_present2]);
+		srv_send(s, (char *)"CHILLER_ADDRESS,%s", simulator->chiller_address);
 		srv_send(s, (char *)"CHILLER_TEMPERATURE,%.3f", simulator->chiller_temperature);
+		srv_send(s, (char *)"CHILLER_PRESENT,%s", DEVPRESENT[simulator->chiller_present]);
+		srv_send(s, (char *)"COOLER_ADDRESS,%s", simulator->cooler_address);
 		srv_send(s, (char *)"COOLER_TEMP,%.1f", simulator->cooler_temp);
 		srv_send(s, (char *)"COOLER_TIME,%d", simulator->cooler_time);
 		srv_send(s, (char *)"COOLER_SIZE,%.3f", simulator->cooler_size);
+		srv_send(s, (char *)"COOLER_PRESENT,%s", DEVPRESENT[simulator->cooler_present]);
+		srv_send(s, (char *)"COOLER_POWER,%d", simulator->cooler_power);
+		srv_send(s, (char *)"HEATER_ADDRESS,%s", simulator->heater_address);
 		srv_send(s, (char *)"HEATER_TEMP,%.1f", simulator->heater_temp);
 		srv_send(s, (char *)"HEATER_TIME,%d", simulator->heater_time);
 		srv_send(s, (char *)"HEATER_SIZE,%.3f", simulator->heater_size);
-		srv_send(s, (char *)"HEATER_STATE,%d", simulator->heater_state);
-		srv_send(s, (char *)"COOLER_STATE,%d", simulator->cooler_state);
+		srv_send(s, (char *)"HEATER_PRESENT,%s", DEVPRESENT[simulator->heater_present]);
+		srv_send(s, (char *)"HEATER_POWER,%d", simulator->heater_power);
+		srv_send(s, (char *)"FAN_ADDRESS,%s", simulator->fan_address);
+                srv_send(s, (char *)"FAN_PRESENT,%s", DEVPRESENT[simulator->fan_present]);
+                srv_send(s, (char *)"FAN_POWER,%d", simulator->fan_power);
+		srv_send(s, (char *)"LIGHT_ADDRESS,%s", simulator->light_address);
+		srv_send(s, (char *)"LIGHT_PRESENT,%s", DEVPRESENT[simulator->light_present]);
+		srv_send(s, (char *)"LIGHT_POWER,%d", simulator->light_power);
 		srv_send(s, (char *)"FRIGO_ISOLATION,%.3f", simulator->frigo_isolation);
+		srv_send(s, (char *)"TIMESTAMP,%ld", (long)simulator->timestamp);
 		srv_send(s, (char *)".");
 		return 0;
 	    }
@@ -1142,6 +1228,7 @@
 		    if (strlen(ibuf)) {
 			if (strcmp(ibuf, (char *)".") == 0) {
 			    srv_send(s, (char *)"219 Accepted Simulator record");
+			    simulator->timestamp = time(NULL);
 			    return 1;
 			}
 			kwd = strtok(ibuf, ",\0");
@@ -1191,6 +1278,16 @@
 				    simulator->air_temperature = fval;
 				}
 
+			    } else if (strcmp(kwd, (char *)"AIR_PRESENT") == 0) {
+                                for (i = 0; i < 4; i++) {
+                                    if (strcmp(val, DEVPRESENT[i]) == 0) {
+                                        if (simulator->air_present != i)
+                                            syslog(LOG_NOTICE, "Simulator %s air_present %s to %s", simulator->uuid, DEVPRESENT[simulator->air_present], DEVPRESENT[i]);
+                                        simulator->air_present = i;
+                                        break;
+                                    }
+                                }
+
 			    } else if (strcmp(kwd, (char *)"BEER_TEMPERATURE") == 0) {
 				if (sscanf(val, "%f", &fval) == 1) {
 				    if (simulator->beer_temperature != fval)
@@ -1198,6 +1295,33 @@
 				    simulator->beer_temperature = fval;
 				}
 
+			    } else if (strcmp(kwd, (char *)"BEER_PRESENT") == 0) {
+                                for (i = 0; i < 4; i++) {
+                                    if (strcmp(val, DEVPRESENT[i]) == 0) {
+                                        if (simulator->beer_present != i)
+                                            syslog(LOG_NOTICE, "Simulator %s beer_present %s to %s", simulator->uuid, DEVPRESENT[simulator->beer_present], DEVPRESENT[i]);
+                                        simulator->beer_present = i;
+                                        break;
+                                    }
+                                }
+
+			    } else if (strcmp(kwd, (char *)"BEER_TEMPERATURE2") == 0) {
+                                if (sscanf(val, "%f", &fval) == 1) {
+                                    if (simulator->beer_temperature2 != fval)
+                                        syslog(LOG_NOTICE, "Simulator %s beer temperature2 %.1f to %.1f", simulator->uuid, simulator->beer_temperature2, fval);
+                                    simulator->beer_temperature2 = fval;
+                                }
+
+			    } else if (strcmp(kwd, (char *)"BEER_PRESENT2") == 0) {
+                                for (i = 0; i < 4; i++) {
+                                    if (strcmp(val, DEVPRESENT[i]) == 0) {
+                                        if (simulator->beer_present2 != i)
+                                            syslog(LOG_NOTICE, "Simulator %s beer_present2 %s to %s", simulator->uuid, DEVPRESENT[simulator->beer_present2], DEVPRESENT[i]);
+                                        simulator->beer_present2 = i;
+                                        break;
+                                    }
+                                }
+
 			    } else if (strcmp(kwd, (char *)"CHILLER_TEMPERATURE") == 0) {
 				if (sscanf(val, "%f", &fval) == 1) {
 				    if (simulator->chiller_temperature != fval)
@@ -1205,6 +1329,16 @@
 				    simulator->chiller_temperature = fval;
 				}
 
+			    } else if (strcmp(kwd, (char *)"CHILLER_PRESENT") == 0) {
+                                for (i = 0; i < 4; i++) {
+                                    if (strcmp(val, DEVPRESENT[i]) == 0) {
+                                        if (simulator->chiller_present != i)
+                                            syslog(LOG_NOTICE, "Simulator %s chiller_present %s to %s", simulator->uuid, DEVPRESENT[simulator->chiller_present], DEVPRESENT[i]);
+                                        simulator->chiller_present = i;
+                                        break;
+                                    }
+                                }
+
 			    } else if (strcmp(kwd, (char *)"COOLER_TEMP") == 0) {
 				if (sscanf(val, "%f", &fval) == 1) {
 				    if (simulator->cooler_temp != fval)
@@ -1226,6 +1360,23 @@
 				    simulator->cooler_size = fval;
 				}
 
+			    } else if (strcmp(kwd, (char *)"COOLER_PRESENT") == 0) {
+                                for (i = 0; i < 4; i++) {
+                                    if (strcmp(val, DEVPRESENT[i]) == 0) {
+                                        if (simulator->cooler_present != i)
+                                            syslog(LOG_NOTICE, "Simulator %s cooler_present %s to %s", simulator->uuid, DEVPRESENT[simulator->cooler_present], DEVPRESENT[i]);
+                                        simulator->cooler_present = i;
+                                        break;
+                                    }
+                                }
+
+			    } else if (strcmp(kwd, (char *)"COOLER_POWER") == 0) {
+                                if (sscanf(val, "%d", &ival) == 1) {
+                                    if (simulator->cooler_power != ival)
+                                        syslog(LOG_NOTICE, "Simulator %s cooler power %d to %d", simulator->uuid, simulator->cooler_power, ival);
+                                    simulator->cooler_power = ival;
+                                }
+
 			    } else if (strcmp(kwd, (char *)"HEATER_TEMP") == 0) {
 				if (sscanf(val, "%f", &fval) == 1) {
 				    if (simulator->heater_temp != fval)
@@ -1247,19 +1398,56 @@
 				    simulator->heater_size = fval;
 				}
 
-			    } else if (strcmp(kwd, (char *)"HEATER_STATE") == 0) {
-				if (sscanf(val, "%d", &ival) == 1) {
-				    if (simulator->heater_state != ival)
-					syslog(LOG_NOTICE, "Simulator %s heater state %d to %d", simulator->uuid, simulator->heater_state, ival);
-				    simulator->heater_state = ival;
-				}
+			    } else if (strcmp(kwd, (char *)"HEATER_PRESENT") == 0) {
+                                for (i = 0; i < 4; i++) {
+                                    if (strcmp(val, DEVPRESENT[i]) == 0) {
+                                        if (simulator->heater_present != i)
+                                            syslog(LOG_NOTICE, "Simulator %s heater_present %s to %s", simulator->uuid, DEVPRESENT[simulator->heater_present], DEVPRESENT[i]);
+                                        simulator->heater_present = i;
+                                        break;
+                                    }
+                                }
+
+			    } else if (strcmp(kwd, (char *)"HEATER_POWER") == 0) {
+                                if (sscanf(val, "%d", &ival) == 1) {
+                                    if (simulator->heater_power != ival)
+                                        syslog(LOG_NOTICE, "Simulator %s heater power %d to %d", simulator->uuid, simulator->heater_power, ival);
+                                    simulator->heater_power = ival;
+                                }
 
-			    } else if (strcmp(kwd, (char *)"COOLER_STATE") == 0) {
-				if (sscanf(val, "%d", &ival) == 1) {
-				    if (simulator->cooler_state != ival)
-					syslog(LOG_NOTICE, "Simulator %s cooler state %d to %d", simulator->uuid, simulator->cooler_state, ival);
-				    simulator->cooler_state = ival;
-				}
+			    } else if (strcmp(kwd, (char *)"FAN_PRESENT") == 0) {
+                                for (i = 0; i < 4; i++) {
+                                    if (strcmp(val, DEVPRESENT[i]) == 0) {
+                                        if (simulator->fan_present != i)
+                                            syslog(LOG_NOTICE, "Simulator %s fan_present %s to %s", simulator->uuid, DEVPRESENT[simulator->fan_present], DEVPRESENT[i]);
+                                        simulator->fan_present = i;
+                                        break;
+                                    }
+                                }
+
+			    } else if (strcmp(kwd, (char *)"FAN_POWER") == 0) {
+                                if (sscanf(val, "%d", &ival) == 1) {
+                                    if (simulator->fan_power != ival)
+                                        syslog(LOG_NOTICE, "Simulator %s fan power %d to %d", simulator->uuid, simulator->fan_power, ival);
+                                    simulator->fan_power = ival;
+                                }
+
+			    } else if (strcmp(kwd, (char *)"LIGHT_PRESENT") == 0) {
+                                for (i = 0; i < 4; i++) {
+                                    if (strcmp(val, DEVPRESENT[i]) == 0) {
+                                        if (simulator->light_present != i)
+                                            syslog(LOG_NOTICE, "Simulator %s light_present %s to %s", simulator->uuid, DEVPRESENT[simulator->light_present], DEVPRESENT[i]);
+                                        simulator->light_present = i;
+                                        break;
+                                    }
+                                }
+
+                            } else if (strcmp(kwd, (char *)"LIGHT_POWER") == 0) {
+                                if (sscanf(val, "%d", &ival) == 1) {
+                                    if (simulator->fan_power != ival)
+                                        syslog(LOG_NOTICE, "Simulator %s light power %d to %d", simulator->uuid, simulator->light_power, ival);
+                                    simulator->light_power = ival;
+                                }
 
 			    } else if (strcmp(kwd, (char *)"FRIGO_ISOLATION") == 0) {
 				if (sscanf(val, "%f", &fval) == 1) {
--- a/thermferm/simulator.c	Sun Apr 28 15:50:42 2024 +0200
+++ b/thermferm/simulator.c	Tue Apr 30 17:26:41 2024 +0200
@@ -22,6 +22,8 @@
 
 #include "thermferm.h"
 #include "delay.h"
+#include "xutil.h"
+#include "websocket.h"
 #include "simulator.h"
 
 int			my_simulator_state = 0;
@@ -29,6 +31,7 @@
 #ifdef USE_SIMULATOR
 
 extern sys_config	Config;
+extern const char	DEVPRESENT[4][6];
 
 int			my_simulator_shutdown = 0;
 int			SIMcooling = 0;
@@ -37,6 +40,157 @@
 int			SIMlight = 0;
 
 
+/*
+ * Return json data for one simulator
+ */
+char *simulator_json(simulator_list *simulator)
+{
+    char	*payload, buf[64];
+
+    payload = xstrcpy((char *)"{\"uuid\":\"");
+    payload = xstrcat(payload, simulator->uuid);
+    payload = xstrcat(payload, (char *)"\",\"name\":\"");
+    payload = xstrcat(payload, simulator->name);
+    payload = xstrcat(payload, (char *)"\",\"simno\":");
+    sprintf(buf, "%d", simulator->simno);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"volume_air\":");
+    sprintf(buf, "%d", simulator->volume_air);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"volume_beer\":");
+    sprintf(buf, "%d", simulator->volume_beer);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"room\":{\"tempaddress\":\"");
+    payload = xstrcat(payload, simulator->room_tempaddress);
+    payload = xstrcat(payload, (char *)"\",\"temperature\":");
+    sprintf(buf, "%.1f", simulator->room_temperature);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"humaddress\":\"");
+    payload = xstrcat(payload, simulator->room_humaddress);
+    payload = xstrcat(payload, (char *)"\",\"humidity\":");
+    sprintf(buf, "%.1f", simulator->room_humidity);
+    payload = xstrcat(payload, buf);
+
+    payload = xstrcat(payload, (char *)"},\"air\":{\"address\":\"");
+    payload = xstrcat(payload, simulator->air_address);
+    payload = xstrcat(payload, (char *)"\",\"temperature\":");
+    sprintf(buf, "%.4f", simulator->air_temperature);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"present\":\"");
+    payload = xstrcat(payload, (char *)DEVPRESENT[simulator->air_present]);
+    payload = xstrcat(payload, (char *)"\"},\"beer\":{\"address\":\"");
+
+    payload = xstrcat(payload, simulator->beer_address);
+    payload = xstrcat(payload, (char *)"\",\"temperature\":");
+    sprintf(buf, "%.4f", simulator->beer_temperature);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"present\":\"");
+    payload = xstrcat(payload, (char *)DEVPRESENT[simulator->beer_present]);
+    payload = xstrcat(payload, (char *)"\"},\"beer2\":{\"address\":\"");
+
+    payload = xstrcat(payload, simulator->beer_address2);
+    payload = xstrcat(payload, (char *)"\",\"temperature\":");
+    sprintf(buf, "%.4f", simulator->beer_temperature2);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"present\":\"");
+    payload = xstrcat(payload, (char *)DEVPRESENT[simulator->beer_present2]);
+    payload = xstrcat(payload, (char *)"\"},\"chiller\":{\"address\":\"");
+
+    payload = xstrcat(payload, simulator->chiller_address);
+    payload = xstrcat(payload, (char *)"\",\"temperature\":");
+    sprintf(buf, "%.4f", simulator->chiller_temperature);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"present\":\"");
+    payload = xstrcat(payload, (char *)DEVPRESENT[simulator->chiller_present]);
+    payload = xstrcat(payload, (char *)"\"},\"cooler\":{\"address\":\"");
+
+    payload = xstrcat(payload, simulator->cooler_address);
+    payload = xstrcat(payload, (char *)"\",\"temperature\":");
+    sprintf(buf, "%.4f", simulator->cooler_temp);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"time\":");
+    sprintf(buf, "%d", simulator->cooler_time);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"size\":");
+    sprintf(buf, "%.4f", simulator->cooler_size);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"present\":\"");
+    payload = xstrcat(payload, (char *)DEVPRESENT[simulator->cooler_present]);
+    payload = xstrcat(payload, (char *)"\",\"power\":");
+    sprintf(buf, "%d", simulator->cooler_power);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)"},\"heater\":{\"address\":\"");
+
+    payload = xstrcat(payload, simulator->heater_address);
+    payload = xstrcat(payload, (char *)"\",\"temperature\":");
+    sprintf(buf, "%.4f", simulator->heater_temp);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"time\":");
+    sprintf(buf, "%d", simulator->heater_time);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"size\":");
+    sprintf(buf, "%.4f", simulator->heater_size);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)",\"present\":\"");
+    payload = xstrcat(payload, (char *)DEVPRESENT[simulator->heater_present]);
+    payload = xstrcat(payload, (char *)"\",\"power\":");
+    sprintf(buf, "%d", simulator->heater_power);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)"},\"fan\":{\"address\":\"");
+
+    payload = xstrcat(payload, simulator->fan_address);
+    payload = xstrcat(payload, (char *)"\",\"present\":\"");
+    payload = xstrcat(payload, (char *)DEVPRESENT[simulator->fan_present]);
+    payload = xstrcat(payload, (char *)"\",\"power\":");
+    sprintf(buf, "%d", simulator->fan_power);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)"},\"light\":{\"address\":\"");
+
+    payload = xstrcat(payload, simulator->light_address);
+    payload = xstrcat(payload, (char *)"\",\"present\":\"");
+    payload = xstrcat(payload, (char *)DEVPRESENT[simulator->light_present]);
+    payload = xstrcat(payload, (char *)"\",\"power\":");
+    sprintf(buf, "%d", simulator->light_power);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)"},\"frigo_isolation\":");
+
+    sprintf(buf, "%.4f", simulator->frigo_isolation);
+    payload = xstrcat(payload, buf);
+
+    payload = xstrcat(payload, (char *)",\"timestamp\":");
+    sprintf(buf, "%ld", (long)simulator->timestamp);
+    payload = xstrcat(payload, buf);
+    payload = xstrcat(payload, (char *)"}");
+
+    return payload;
+}
+
+
+
+void simulator_ws(void)
+{
+    bool		comma = false;
+    char		*payload = NULL, *payloadu = NULL;
+    simulator_list	*simulator;
+
+    payload = xstrcpy((char *)"{\"type\":\"simulator\",\"metric\":[");
+    for (simulator = Config.simulators; simulator; simulator = simulator->next) {
+        if (comma)
+            payload = xstrcat(payload, (char *)",");
+        payloadu = simulator_json(simulator);
+        payload = xstrcat(payload, payloadu);
+        comma = true;
+        free(payloadu);
+        payloadu = NULL;
+    }
+    payload = xstrcat(payload, (char *)"]}");
+    ws_broadcast(payload);
+    free(payload);
+    payload = NULL;
+}
+
+
+
 void *my_simulator_loop(void *threadid)
 {
     simulator_list	*simulator;
@@ -59,17 +213,17 @@
 	if (my_simulator_shutdown)
 	    break;
 
-	for (simulator = Config.simulators; simulator; simulator = simulator->next) {
-	    if (my_simulator_shutdown)
-	    	break;
+	now = time(NULL);
+	if (now != last) {
+	    last = now;
+	    /*
+	     * Each second
+	     */
+	    seconds++;
 
-	    now = time(NULL);
-	    if (now != last) {
-		last = now;
-		/*
-		 * Each second
-		 */
-	    	seconds++;
+	    for (simulator = Config.simulators; simulator; simulator = simulator->next) {
+		if (my_simulator_shutdown)
+	    	    break;
 
 		/*
 	    	 * First, calculate temperature difference between the room and the air in the
@@ -79,7 +233,7 @@
 	    	 */
 		sqm_room_air = (cbrtl(simulator->volume_air) * cbrtl(simulator->volume_air) * 6) / 100;	/* square meters all fridge sides */
 		thick_room_air = 0.04;	/* 4 cm walls	*/
-		k_room_air = 0.03;	/* Polystrene	*/
+		k_room_air = 0.03;		/* Polystrene	*/
 		air_heat_transfer=(k_room_air * sqm_room_air * (simulator->room_temperature - simulator->air_temperature)) / thick_room_air;
 		air_change = (air_heat_transfer / (vhc_air * ((simulator->volume_air - simulator->volume_beer) * 1000))) / 60.0;
 		simulator->air_temperature += air_change;
@@ -117,7 +271,7 @@
 		    simulator->s_cool_temp -= (simulator->s_cool_temp - simulator->air_temperature) / 25.0;
 		}
 
-	    	/*
+		/*
 	    	 * Calculate final temperature of the beer and the air.
 	    	 */
 		// Cheap trick, just follow slowly the air temp.
--- a/thermferm/simulator.h	Sun Apr 28 15:50:42 2024 +0200
+++ b/thermferm/simulator.h	Tue Apr 30 17:26:41 2024 +0200
@@ -3,12 +3,10 @@
 
 #ifdef USE_SIMULATOR
 
+char *simulator_json(simulator_list *simulator);
+void simulator_ws(void);
 
-#ifdef HAVE_WIRINGPI_H
-PI_THREAD (my_simulator_loop);
-#else
 void *my_simulator_loop(void *);
-#endif
 
 #endif
 
--- a/thermferm/thermferm.c	Sun Apr 28 15:50:42 2024 +0200
+++ b/thermferm/thermferm.c	Tue Apr 30 17:26:41 2024 +0200
@@ -1412,7 +1412,7 @@
 	    if ((unit->mode == UNITMODE_BEER) && ((unit->air_temperature / 1000.0) > (unit->PID_heat->Input + 8.0))) {
 	    	unit->PID_heat->OutP = 0.0;
 	    }
-	    if (seconds == 60) {
+	    if ((seconds == 60) && (unit->mode > UNITMODE_NONE)) {
 	    	syslog(LOG_NOTICE, "Heat: sp=%.3f Input=%.3f iState=%.3f Err=%.3f Out=%.1f",
 				unit->PID_heat->SetP, unit->PID_heat->Input, unit->PID_heat->iState, unit->PID_heat->Err, unit->PID_heat->OutP);
 	    }
@@ -1438,7 +1438,7 @@
 		    }
 	    	}
 	    }
-	    if (seconds == 60) {
+	    if ((seconds == 60) && (unit->mode > UNITMODE_NONE)) {
 	    	syslog(LOG_NOTICE, "Cool: sp=%.3f Input=%.3f iState=%.3f Err=%.3f Out=%.1f",
 	    	unit->PID_cool->SetP, unit->PID_cool->Input, unit->PID_cool->iState, unit->PID_cool->Err, unit->PID_cool->OutP);
 	    }
--- a/thermferm/thermferm.h	Sun Apr 28 15:50:42 2024 +0200
+++ b/thermferm/thermferm.h	Tue Apr 30 17:26:41 2024 +0200
@@ -330,22 +330,46 @@
     struct _simulator	*next;
     char		*uuid;			/* Simulator uuid		*/
     char		*name;			/* Simulator name		*/
+    int			simno;			/* Simulator number		*/
     int			volume_air;		/* Volume air of the frigo	*/
     int			volume_beer;		/* Volume beer inside frigo	*/
+    char		*room_tempaddress;	/* Address 			*/
     double		room_temperature;	/* Temp outside frigo		*/
+    char		*room_humaddress;	/* Address 			*/
     double		room_humidity;		/* Humidity outside frigo	*/
+    char		*air_address;		/* Simulated air address		*/
     double		air_temperature;	/* Simulated air temperature	*/
+    int			air_present;		/* Simulated air present	*/
+    char		*beer_address;
     double		beer_temperature;	/* Simulated beer temperature	*/
+    int			beer_present;		/* Simulated beer present	*/
+    char		*beer_address2;
+    double		beer_temperature2;	/* Simulated beer temperature2	*/
+    int			beer_present2;		/* Simulated beer present2	*/
+    char		*chiller_address;
     double		chiller_temperature;	/* Simulated chiller temp.	*/
+    int			chiller_present;	/* Simulated chiller present	*/
+    char		*cooler_address;
     double		cooler_temp;		/* Lowest cooler temperature	*/
     int			cooler_time;		/* Time to reach temperature	*/
     float		cooler_size;		/* Size of cooler in square mtr	*/
+    int			cooler_present;		/* Simulated cooler present	*/
+    int			cooler_power;		/* Simulated cooler 0..100%	*/
+    char		*heater_address;
     double		heater_temp;		/* Highest heater temperature	*/
     int			heater_time;		/* Time to reach temperature	*/
     float		heater_size;		/* Size of heater in square mtr	*/
-    int			heater_state;		/* Heater status		*/
-    int			cooler_state;		/* Cooler status		*/
+    int			heater_present;		/* Simulated heater present	*/
+    int			heater_power;		/* Simulated heater 0..100%	*/
+    char		*fan_address;		/* Simulated fan		*/
+    int			fan_present;
+    int			fan_power;
+    char		*light_address;		/* Simulated interior light	*/
+    int			light_present;
+    int			light_power;
     float		frigo_isolation;	/* Frigo isolation value	*/
+    time_t		timestamp;
+
     /*
      * Status values, maintained by the simulator but stored
      * here so they don't get lost over program restarts.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/dbdevices.php	Tue Apr 30 17:26:41 2024 +0200
@@ -0,0 +1,112 @@
+<?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  = "DEVICE PUT " . $_POST['uuid'] . "\r\n";
+    $cmd .= "TYPE," . $_POST['type'] . "\r\n";
+    $cmd .= "DIRECTION," . $_POST['direction'] . "\r\n";
+    $cmd .= "VALUE," . $_POST['value'] . "\r\n";		// Only for outputs
+    $cmd .= "OFFSET," . $_POST['offset'] . "\r\n";		// Only analog
+    $cmd .= "PRESENT," . $_POST['present'] . "\r\n";
+    $cmd .= "ADDRESS," . $_POST['address'] . "\r\n";		// Not for auto detected
+    $cmd .= "SUBDEVICE," . $_POST['subdevice'] . "\r\n";	// Not for auto detected
+    $cmd .= "GPIOPIN," . $_POST['gpiopin'] . "\r\n";
+    $cmd .= "DESCRIPTION," . $_POST['description'] . "\r\n";
+    $cmd .= "COMMENT," . $_POST['comment'] . "\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'])) {
+
+    $answer = send_cmd("DEVICE ADD " . $_POST['type']);
+    $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("DEVICE 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("DEVICE 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/dbsimulators.php	Tue Apr 30 17:26:41 2024 +0200
@@ -0,0 +1,126 @@
+<?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  = "SIMULATOR PUT " . $_POST['uuid'] . "\r\n";
+    $cmd .= "NAME," . $_POST['name'] . "\r\n";
+    $cmd .= "VOLUME_AIR," . $_POST['volume_air'] . "\r\n";
+    $cmd .= "VOLUME_BEER," . $_POST['volume_beer'] . "\r\n";
+    $cmd .= "ROOM_TEMPERATURE," . $_POST['room_temperature'] . "\r\n";
+    $cmd .= "ROOM_HUMIDITY," . $_POST['room_humidity'] . "\r\n";
+    $cmd .= "AIR_PRESENT," . $_POST['air_present'] . "\r\n";
+    $cmd .= "BEER_PRESENT," . $_POST['beer_present'] . "\r\n";
+    $cmd .= "BEER_PRESENT2," . $_POST['beer_present2'] . "\r\n";
+    $cmd .= "CHILLER_PRESENT," . $_POST['chiller_present'] . "\r\n";
+    $cmd .= "COOLER_TEMP," . $_POST['cooler_temp'] . "\r\n";
+    $cmd .= "COOLER_TIME," . $_POST['cooler_time'] . "\r\n";
+    $cmd .= "COOLER_SIZE," . $_POST['cooler_size'] . "\r\n";
+    $cmd .= "COOLER_PRESENT," . $_POST['cooler_present'] . "\r\n";
+    $cmd .= "COOLER_POWER," . $_POST['cooler_power'] . "\r\n";
+    $cmd .= "HEATER_TEMP," . $_POST['heater_temp'] . "\r\n";
+    $cmd .= "HEATER_TIME," . $_POST['heater_time'] . "\r\n";
+    $cmd .= "HEATER_SIZE," . $_POST['heater_size'] . "\r\n";
+    $cmd .= "HEATER_PRESENT," . $_POST['heater_present'] . "\r\n";
+    $cmd .= "HEATER_POWER," . $_POST['heater_power'] . "\r\n";
+    $cmd .= "FAN_PRESENT," . $_POST['fan_present'] . "\r\n";
+    $cmd .= "FAN_POWER," . $_POST['fan_power'] . "\r\n";
+    $cmd .= "LIGHT_PRESENT," . $_POST['light_present'] . "\r\n";
+    $cmd .= "LIGHT_POWER," . $_POST['light_power'] . "\r\n";
+    $cmd .= "FRIGO_ISOLATION," . $_POST['frigo_isolation'] . "\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'])) {
+
+    $answer = send_cmd("SIMULATOR 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("SIMULATOR 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("SIMULATOR JSON");
+    header("Content-type: application/json");
+
+    $arr = explode("\r\n", $answer);
+    if (startsWith($arr[0], "212")) {
+    	echo $arr[1];
+    } else {
+        echo '{}';
+    }
+}
+
+?>
--- a/www/getdevices.php	Sun Apr 28 15:50:42 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,112 +0,0 @@
-<?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  = "DEVICE PUT " . $_POST['uuid'] . "\r\n";
-    $cmd .= "TYPE," . $_POST['type'] . "\r\n";
-    $cmd .= "DIRECTION," . $_POST['direction'] . "\r\n";
-    $cmd .= "VALUE," . $_POST['value'] . "\r\n";		// Only for outputs
-    $cmd .= "OFFSET," . $_POST['offset'] . "\r\n";		// Only analog
-    $cmd .= "PRESENT," . $_POST['present'] . "\r\n";
-    $cmd .= "ADDRESS," . $_POST['address'] . "\r\n";		// Not for auto detected
-    $cmd .= "SUBDEVICE," . $_POST['subdevice'] . "\r\n";	// Not for auto detected
-    $cmd .= "GPIOPIN," . $_POST['gpiopin'] . "\r\n";
-    $cmd .= "DESCRIPTION," . $_POST['description'] . "\r\n";
-    $cmd .= "COMMENT," . $_POST['comment'] . "\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'])) {
-
-    $answer = send_cmd("DEVICE ADD " . $_POST['type']);
-    $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("DEVICE 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("DEVICE JSON");
-    header("Content-type: application/json");
-
-    $arr = explode("\r\n", $answer);
-    if (startsWith($arr[0], "212")) {
-    	echo $arr[1];
-    } else {
-        echo '{}';
-    }
-}
-
-?>
--- a/www/js/global.js	Sun Apr 28 15:50:42 2024 +0200
+++ b/www/js/global.js	Tue Apr 30 17:26:41 2024 +0200
@@ -83,10 +83,14 @@
 
 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 },
+Show2dec = { inputMode: 'simple', theme: theme, width: 90, height: 23, readOnly: true, decimalDigits: 2 },
+Show3dec = { inputMode: 'simple', theme: theme, width: 90, height: 23, readOnly: true, decimalDigits: 3 },
+Show4dec = { inputMode: 'simple', theme: theme, width: 90, height: 23, readOnly: true, decimalDigits: 4 },
 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 },
+Spin4dec = { inputMode: 'simple', theme: theme, width: 110, height: 23, min: 0, decimalDigits: 4, 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 },
--- a/www/js/set_devices.js	Sun Apr 28 15:50:42 2024 +0200
+++ b/www/js/set_devices.js	Tue Apr 30 17:26:41 2024 +0200
@@ -78,7 +78,7 @@
 
 $(document).ready(function() {
  var dataRecord = {},
- url = 'getdevices.php',
+ url = 'dbdevices.php',
  source = {
   datatype: 'json',
   cache: false,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/js/set_simulators.js	Tue Apr 30 17:26:41 2024 +0200
@@ -0,0 +1,358 @@
+/*****************************************************************************
+ * 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 = 'dbsimulators.php',
+ source = {
+  datatype: 'json',
+  cache: false,
+  datafields: [
+   { name: 'uuid', type: 'string' },
+   { name: 'name', type: 'string' },
+   { name: 'simno', type: 'int' },
+   { name: 'volume_air', type: 'int' },
+   { name: 'volume_beer', type: 'int' },
+   { name: 'room_tempaddress', map: 'room>tempaddress' },
+   { name: 'room_temperature', map: 'room>temperature', type: 'float' },
+   { name: 'room_humaddress', map: 'room>humaddress' },
+   { name: 'room_humidity', map: 'room>humidity', type: 'float' },
+   { name: 'air_address', map: 'air>address' },
+   { name: 'air_temperature', map: 'air>temperature', type: 'float' },
+   { name: 'air_present', map: 'air>present' },
+   { name: 'beer_address', map: 'beer>address' },
+   { name: 'beer_temperature', map: 'beer>temperature', type: 'float' },
+   { name: 'beer_present', map: 'beer>present' },
+   { name: 'beer_address2', map: 'beer2>address' },
+   { name: 'beer_temperature2', map: 'beer2>temperature', type: 'float' },
+   { name: 'beer_present2', map: 'beer2>present' },
+   { name: 'chiller_address', map: 'chiller>address' },
+   { name: 'chiller_temperature', map: 'chiller>temperature', type: 'float' },
+   { name: 'chiller_present', map: 'chiller>present' },
+   { name: 'cooler_address', map: 'cooler>address' },
+   { name: 'cooler_temp', map: 'cooler>temperature', type: 'float' },
+   { name: 'cooler_time', map: 'cooler>time', type: 'int' },
+   { name: 'cooler_size', map: 'cooler>size', type: 'float' },
+   { name: 'cooler_present', map: 'cooler>present' },
+   { name: 'cooler_power', map: 'cooler>power', type: 'int' },
+   { name: 'heater_address', map: 'heater>address' },
+   { name: 'heater_temp', map: 'heater>temperature', type: 'float' },
+   { name: 'heater_time', map: 'heater>time', type: 'int' },
+   { name: 'heater_size', map: 'heater>size', type: 'float' },
+   { name: 'heater_present', map: 'heater>present' },
+   { name: 'heater_power', map: 'heater>power', type: 'int' },
+   { name: 'fan_address', map: 'fan>address' },
+   { name: 'fan_present', map: 'fan>present' },
+   { name: 'fan_power', map: 'fan>power', type: 'int' },
+   { name: 'light_address', map: 'light>address' },
+   { name: 'light_present', map: 'light>present' },
+   { name: 'light_power', map: 'light>power', type: 'int' },
+   { name: 'frigo_isolation', type: 'float' },
+   { name: 'timestamp', type: 'int' }
+  ],
+  id: 'uuid',
+  url: url
+ },
+ dataAdapter = new $.jqx.dataAdapter(source),
+ editrow = -1,
+ tzoffset = (new Date()).getTimezoneOffset() * 60000; //offset in milliseconds
+
+ // initialize the input fields.
+ $('#name').jqxInput({ theme: theme, width: 180, height: 23 });
+ $('#simno').jqxNumberInput(Show0dec);
+ $('#uuid').jqxInput({ theme: theme, width: 360, height: 23 });
+ $('#room_temperature,#room_humidity').jqxNumberInput(Perc1dec);
+ $('#volume_air,#volume_beer').jqxNumberInput(PosInt);
+ $('#frigo_isolation').jqxNumberInput(Spin3dec);
+ $('#timestamp').jqxInput({ theme: theme, width: 180, height: 23 });
+
+ $('#air_address,#beer_address,#beer_address2,#chiller_address,#cooler_address,#heater_address,#fan_address,#light_address').jqxInput({ theme: theme, width: 180, height: 23 });
+ $('#air_temperature,#beer_temperature,#beer_temperature2,#chiller_temperature').jqxNumberInput(Show4dec);
+ $('#air_present,#beer_present,#beer_present2,#chiller_present,#cooler_present,#heater_present,#fan_present,#light_present').jqxDropDownList({
+  theme: theme,
+  source: DevicePresentAdapter,
+  valueMember: 'mno',
+  displayMember: 'en',
+  width: 180,
+  height: 23,
+  autoDropDownHeight: true
+ });
+ $('#cooler_power,#heater_power,#fan_power,#light_power').jqxNumberInput(Perc0);
+ $('#cooler_temp,#heater_temp').jqxNumberInput(Spin1dec);
+ $('#cooler_time,#heater_time').jqxNumberInput(PosInt);
+ $('#cooler_size,#heater_size').jqxNumberInput(Spin4dec);
+
+ // 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;
+   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');
+    $('#addName').jqxInput({ theme: theme, width: 180, height: 23 });
+    $('#addOk').click(function() {
+     var data,
+     data = 'add=true&name=' + $('#addName').val();
+     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: 'Name', datafield: 'name' },
+   { text: 'No.', datafield: 'simno', width: 80 },
+   { text: 'Air temp', datafield: 'air_temperature', width: 100 },
+   { text: 'Beer temp', datafield: 'beer_temperature', width: 100 },
+   { text: 'Heater', datafield: 'heater_power', width: 80 },
+   { text: 'Cooler', datafield: 'cooler_power', width: 80 },
+   { text: 'Fan', datafield: 'fan_power', width: 80 },
+   { text: 'Last change', datafield: 'timestamp', width: 190,
+     cellsrenderer: function(index, datafield, value, defaultvalue, column, rowdata) {
+      var date = new Date((value * 1000) - tzoffset).toISOString().slice(0, 19).replace("T", " ");
+      return '<span style="margin: 3px; margin-top: 6px; float: left;">' +  date + '</span>';
+    }
+   },
+   { 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);
+     $('#name').val(dataRecord.name);
+     $('#uuid').val(dataRecord.uuid);
+     $('#simno').val(dataRecord.simno);
+     $('#room_temperature').val(dataRecord.room_temperature);
+     $('#room_humidity').val(dataRecord.room_humidity);
+     $('#volume_air').val(dataRecord.volume_air);
+     $('#volume_beer').val(dataRecord.volume_beer);
+     $('#frigo_isolation').val(dataRecord.frigo_isolation);
+
+     $('#air_address').val(dataRecord.air_address);
+     $('#air_temperature').val(dataRecord.air_temperature);
+     $('#air_present').val(dataRecord.air_present);
+     $('#beer_address').val(dataRecord.beer_address);
+     $('#beer_temperature').val(dataRecord.beer_temperature);
+     $('#beer_present').val(dataRecord.beer_present);
+     $('#beer_address2').val(dataRecord.beer_address2);
+     $('#beer_temperature2').val(dataRecord.beer_temperature2);
+     $('#beer_present2').val(dataRecord.beer_present2);
+     $('#chiller_address').val(dataRecord.chiller_address);
+     $('#chiller_temperature').val(dataRecord.chiller_temperature);
+     $('#chiller_present').val(dataRecord.chiller_present);
+     $('#cooler_address').val(dataRecord.cooler_address);
+     $('#cooler_power').val(dataRecord.cooler_power);
+     $('#cooler_present').val(dataRecord.cooler_present);
+     $('#cooler_temp').val(dataRecord.cooler_temp);
+     $('#cooler_time').val(dataRecord.cooler_time);
+     $('#cooler_size').val(dataRecord.cooler_size);
+     $('#heater_address').val(dataRecord.heater_address);
+     $('#heater_power').val(dataRecord.heater_power);
+     $('#heater_present').val(dataRecord.heater_present);
+     $('#heater_temp').val(dataRecord.heater_temp);
+     $('#heater_time').val(dataRecord.heater_time);
+     $('#heater_size').val(dataRecord.heater_size);
+     $('#fan_address').val(dataRecord.fan_address);
+     $('#fan_power').val(dataRecord.fan_power);
+     $('#fan_present').val(dataRecord.fan_present);
+     $('#light_address').val(dataRecord.light_address);
+     $('#light_power').val(dataRecord.light_power);
+     $('#light_present').val(dataRecord.light_present);
+     var date = new Date((dataRecord.timestamp * 1000) - tzoffset).toISOString().slice(0, 19).replace("T", " ");
+     $('#timestamp').val(date);
+     // 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() {
+  if (editrow >= 0) {
+   // Open a popup to confirm this action.
+   $('#eventWindow').jqxWindow('open');
+   $('#delOk').click(function() {
+    var data,
+    data = 'del=true&uuid=' + $('#dev_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 == 'simulator') {
+   // Use the message to trigger update.
+   $('#jqxgrid').jqxGrid('updatebounddata');
+  }
+ }
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/set_simulators.php	Tue Apr 30 17:26:41 2024 +0200
@@ -0,0 +1,175 @@
+<?php
+require_once($_SERVER['DOCUMENT_ROOT'].'/includes/global.inc.php');
+page_header('Simulator engines', 'set_simulators');
+?>
+
+   <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 simulator</div>
+    <div style="overflow: hidden;">
+     <table style="width: 100%;">
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Name:</td>
+       <td colspan="3" align="left" style="vertical-align: top; padding: 3px;"><input id="name" /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">No:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="simno"></div></td>
+       <td></td><td></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Temperature:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="room_temperature"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Humidity:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="room_humidity"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Volume air:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="volume_air"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Volume beer:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="volume_beer"></div></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Frigo isulation:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="frigo_isolation"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Last update:</td>
+       <td style="vertical-align: top; padding: 3px;"><input id="timestamp" 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>
+      </tr>
+      <tr><td colspan="8"><hr></td></tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Air address:</td>
+       <td style="vertical-align: top; padding: 3px;"><input id="air_address" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Temperature:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="air_temperature"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Present:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="air_present"></div></td>
+       <td></td><td></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Beer address:</td>
+       <td style="vertical-align: top; padding: 3px;"><input id="beer_address" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Temperature:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="beer_temperature"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Present:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="beer_present"></div></td>
+       <td></td><td></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Beer alternate:</td>
+       <td style="vertical-align: top; padding: 3px;"><input id="beer_address2" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Temperature:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="beer_temperature2"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Present:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="beer_present2"></div></td>
+       <td></td><td></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Chiller address:</td>
+       <td style="vertical-align: top; padding: 3px;"><input id="chiller_address" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Temperature:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="chiller_temperature"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Present:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="chiller_present"></div></td>
+       <td></td><td></td>
+      </tr>
+
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Cooler address:</td>
+       <td style="vertical-align: top; padding: 3px;"><input id="cooler_address" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Power:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="cooler_power"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Present:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="cooler_present"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Min temp:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="cooler_temp"></div></td>
+      </tr>
+      <tr>
+       <td></td><td></td>
+       <td></td><td></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Time:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="cooler_time"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Size:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="cooler_size"></div></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Heater address:</td>
+       <td style="vertical-align: top; padding: 3px;"><input id="heater_address" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Power:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="heater_power"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Present:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="heater_present"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Max temp:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="heater_temp"></div></td>
+      </tr>
+      <tr>
+       <td></td><td></td>
+       <td></td><td></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Time:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="heater_time"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Size:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="heater_size"></div></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Fan address:</td>
+       <td style="vertical-align: top; padding: 3px;"><input id="fan_address" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Power:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="fan_power"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Present:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="fan_present"></div></td>
+       <td></td><td></td>
+      </tr>
+      <tr>
+       <td style="vertical-align: top; float: right; padding: 3px;">Light address:</td>
+       <td style="vertical-align: top; padding: 3px;"><input id="light_address" readonly /></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Power:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="light_power"></div></td>
+       <td style="vertical-align: top; float: right; padding: 3px;">Present:</td>
+       <td style="padding: 3px;"><div style="float: left;" id="light_present"></div></td>
+       <td></td><td></td>
+      </tr>
+
+
+
+      <tr><td colspan="8"></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 style="padding-top: 20px;" align="right"><input style="margin-right: 5px;" type="button" id="Save" value="Save" /></td>
+       <td></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 simulator record.<br>
+      Press "Cancel" to close without adding a new simulator.
+     </div>
+     <div style="margin-top: 10px;">
+      <div style="float: right; vertical-align: top; padding: 10px; margin-right: 120px">Name:<input id="addName" /></div>
+     </div>
+     <div>
+      <div style="float: right; margin-top: 10px; margin-bottom: 10px;">
+       <input type="button" id="addOk" value="OK" style="margin-right: 15px" />
+       <input type="button" id="addCancel" value="Cancel" style="margin-right: 160px" />
+      </div>
+     </div>
+    </div>
+   </div>
+
+<?php
+confirm_delete();
+page_footer();
+?>

mercurial