Updates for V3.0.0 (dev)

pull/492/head
sfeakes 2025-11-17 18:00:48 -06:00
parent abf1e30f6c
commit 67302f6661
103 changed files with 4632 additions and 8275 deletions

View File

@ -139,9 +139,12 @@ NEED TO FIX FOR THIS RELEASE.
# Updates in 3.0.0 (dev)
* WARNING V3.0 has undergone a significant amount code changes and refactoring, there may be issues.
* Serial optimization for AqualinkD HAT.
* upgraded network library ( HTTP(S), MQTT(S), WS )
* Can now edit webconfig in aqmanager, added many UI customization options.
* web/config.js is now web/config.json any custom settings will need to be migrated.
* Added example plugin of how to get HomeAssistant devices to show up in AqualinkD UI.
* upgraded network library ( HTTP(S), MQTT(S), WS )
* Added support for HTTPS and MQTTS
* Optimized updates to MQTT and WebUI (only update when absolutely necessary)
* Optimized updates to MQTT, web sockets & WebUI (only update when absolutely necessary)
* Added options to force upgrades in aqmanager. (add ?upgrade or ?devupgrade to url to enable upgrade button)
* MQTT Discovery for all supporting hubs (HomeAssistant Domoticz Hubitat OpenHAB etc)
* Moved Domoticz support over to MQTT autodiscovery.
@ -159,7 +162,6 @@ NEED TO FIX FOR THIS RELEASE.
* Added preliminary support for Jandy Infinite water color lights
- Need to finish off :-
* HAT serial optimizations broke some USB serial adapters
* WebUI Config in aqmanager.
# Updates in 2.6.11 (Sept 14 2025)

View File

@ -22,6 +22,10 @@ if [ -d "$CONFDIR" ]; then
if [ -f "$CONFDIR/config.js" ]; then
ln -sf "$CONFDIR/config.js" /var/www/aqualinkd/config.js
fi
# If we have a web config, replace the local filesystem with mounted
if [ -f "$CONFDIR/config.json" ]; then
ln -sf "$CONFDIR/config.json" /var/www/aqualinkd/config.json
fi
# If don't have a cron file, create one
if [ ! -f "$CONFDIR/aqualinkd.schedule" ]; then

Binary file not shown.

Binary file not shown.

View File

@ -218,21 +218,25 @@ else
log "Please install cron, if not the AqualinkD Scheduler will not work"
fi
# V3.0.0 uses config.json not config.js
if [ -f "$WEBLocation/config.js" ]; then
log "AqualinkD web config is old, please migrate and settings from $WEBLocation/config.js to $WEBLocation/config.json"
fi
# V2.3.9 & V2.6.0 has kind-a breaking change for config.js, so check existing and rename if needed
# we added Aux_V? to the button list
if [ -f "$WEBLocation/config.js" ]; then
#if [ -f "$WEBLocation/config.js" ]; then
# Test is if has AUX_V1 in file AND "Spa" is in file (Spa_mode changed to Spa)
# Version 2.6.0 added Chiller as well
if ! grep -q '"Aux_V1"' "$WEBLocation/config.js" ||
! grep -q '"Spa"' "$WEBLocation/config.js" ||
! grep -q '"Chiller"' "$WEBLocation/config.js" ||
! grep -q '"Aux_S1"' "$WEBLocation/config.js"; then
dateext=`date +%Y%m%d_%H_%M_%S`
log "AqualinkD web config is old, making copy to $WEBLocation/config.js.$dateext"
log "Please make changes to new version $WEBLocation/config.js"
mv $WEBLocation/config.js $WEBLocation/config.js.$dateext
fi
fi
#if ! grep -q '"Aux_V1"' "$WEBLocation/config.js" ||
# ! grep -q '"Spa"' "$WEBLocation/config.js" ||
# ! grep -q '"Chiller"' "$WEBLocation/config.js" ||
# ! grep -q '"Aux_S1"' "$WEBLocation/config.js"; then
# dateext=`date +%Y%m%d_%H_%M_%S`
# log "AqualinkD web config is old, making copy to $WEBLocation/config.js.$dateext"
# log "Please make changes to new version $WEBLocation/config.js"
# mv $WEBLocation/config.js $WEBLocation/config.js.$dateext
#fi
#fi
@ -268,14 +272,14 @@ if [ ! -d "$WEBLocation" ]; then
mkdir -p $WEBLocation
fi
if [ -f "$WEBLocation/config.js" ]; then
log "AqualinkD web config exists, did not copy new config, you may need to edit existing $WEBLocation/config.js "
if [ -f "$WEBLocation/config.json" ]; then
log "AqualinkD web config exists, did not copy new config, you may need to edit existing $WEBLocation/config.json "
if command -v "rsync" &>/dev/null; then
rsync -avq --exclude='config.js' $BUILD/../web/* $WEBLocation
rsync -avq --exclude='config.json' $BUILD/../web/* $WEBLocation
else
# This isn;t the right way to do it, but seems to work.
shopt -s extglob
`cp -r "$BUILD/../web/"!(*config.js) "$WEBLocation"`
`cp -r "$BUILD/../web/"!(*config.json) "$WEBLocation"`
shopt -u extglob
# Below should work, but doesn't.
#shopt -s extglob

View File

@ -10,7 +10,8 @@
#include "sensors.h"
//#include "aq_panel.h" // Moved to later in file to overcome circular dependancy. (crappy I know)
#define PRINTF(format, ...) printf("%s:%d: " format, __FILE__, __LINE__, ##__VA_ARGS__)
//#define PRINTF(format, ...) printf("%s:%d: " format, __FILE__, __LINE__, ##__VA_ARGS__)
#define PRINTF(format, ...)
#define isMASK_SET(bitmask, mask) ((bitmask & mask) == mask)
#define setMASK(bitmask, mask) (bitmask |= mask)

View File

@ -948,7 +948,7 @@ void mqtt_broadcast_aqualinkstate(struct mg_connection *nc)
}
typedef enum {uActioned, uBad, uDevices, uStatus, uHomebridge, uDynamicconf, uDebugStatus, uDebugDownload, uSimulator, uSchedules, uSetSchedules, uAQmanager, uLogDownload, uNotAvailable, uConfig, uSaveConfig, uConfigDownload} uriAtype;
typedef enum {uActioned, uBad, uDevices, uStatus, uHomebridge, uDynamicconf, uDebugStatus, uDebugDownload, uSimulator, uSchedules, uSetSchedules, uAQmanager, uLogDownload, uNotAvailable, uConfig, uSaveConfig, uConfigDownload, uSaveWebConfig} uriAtype;
//typedef enum {NET_MQTT=0, NET_API, NET_WS, DZ_MQTT} netRequest;
const char actionName[][5] = {"MQTT", "API", "WS", "DZ"};
@ -1045,6 +1045,8 @@ uriAtype action_URI(request_source from, const char *URI, int uri_length, float
return uConfigDownload;
} else if (strncmp(ri1, "config/set", 10) == 0) {
return uSaveConfig;
} else if (strncmp(ri1, "webconfig/set", 13) == 0) {
return uSaveWebConfig;
} else if (strncmp(ri1, "config", 6) == 0) {
return uConfig;
} else if (strncmp(ri1, "simulator", 9) == 0 && from == NET_WS) { // Only valid from websocket.
@ -1652,7 +1654,7 @@ void action_web_request(struct mg_connection *nc, struct mg_http_message *http_m
{
char message[JSON_BUFFER_SIZE];
DEBUG_TIMER_START(&tid2);
build_webconfig_js(_aqualink_data, message, JSON_BUFFER_SIZE);
build_dynamic_webconfig_js(_aqualink_data, message, JSON_BUFFER_SIZE);
DEBUG_TIMER_STOP(tid2, NET_LOG, "action_web_request() build_webconfig_js took");
mg_http_reply(nc, 200, CONTENT_JS, message);
}
@ -1855,6 +1857,14 @@ void action_websocket_request(struct mg_connection *nc, struct mg_ws_message *wm
DEBUG_TIMER_STOP(tid, NET_LOG, "action_websocket_request() save_config_js took");
ws_send(nc, message);
}
case uSaveWebConfig:
{
DEBUG_TIMER_START(&tid);
char message[JSON_BUFFER_SIZE];
save_web_config_json((char *)wm->data.buf, wm->data.len, message, JSON_BUFFER_SIZE, _aqualink_data);
DEBUG_TIMER_STOP(tid, NET_LOG, "action_websocket_request() save_web_config_json took");
ws_send(nc, message);
}
break;
case uBad:
default:

View File

@ -17,13 +17,19 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "config.h"
#include "color_lights.h"
#include "utils.h"
#include "aq_systemutils.h"
int build_webconfig_js(struct aqualinkdata *aqdata, char* buffer, int size)
#define WEBCONFIGFILE "/config.json"
void fprintf_json(FILE *fp,const char *json_string);
// This should be called dynamic web config, not webconfig to avoid confusion.
int build_dynamic_webconfig_js(struct aqualinkdata *aqdata, char* buffer, int size)
{
memset(&buffer[0], 0, size);
int length = 0;
@ -33,4 +39,126 @@ int build_webconfig_js(struct aqualinkdata *aqdata, char* buffer, int size)
length += sprintf(buffer+length, "var _enable_schedules = %s;\n",(_aqconfig_.enable_scheduler)?"true":"false");
return length;
}
}
char* find_nth_char(const char* str, int character, int n) {
char* p = (char*)str;
int count = 0;
while (count < n && (p = strchr(p, character)) != NULL) {
count++;
if (count == n) {
return p;
}
p++; // Move pointer past the found character for the next search
}
return NULL; // Return NULL if the Nth instance is not found
}
int save_web_config_json(const char* inBuf, int inSize, char* outBuf, int outSize, struct aqualinkdata *aqdata)
{
FILE *fp;
char configfile[256];
bool ro_root;
bool created_file;
char *contents;
snprintf(configfile, 256, "%s%s", _aqconfig_.web_directory,WEBCONFIGFILE);
fp = aq_open_file(configfile, &ro_root, &created_file);
if (fp == NULL) {
LOG(AQUA_LOG,LOG_ERR, "Open config file failed '%s'\n", configfile);
//remount_root_ro(true);
//fprintf(stdout, "Open file failed 'sprinkler.cron'\n");
return false;
}
char* secondColonPos = find_nth_char(inBuf, ':', 2);
// Check if the pointer is not NULL, and calculate the index inside the printf
if (secondColonPos) {
contents = malloc(sizeof(char) * inSize);
int pos = (secondColonPos - inBuf) + 1;
// Need to strip off {"uri":"webconfig/set","values": from beginning and the trailing }
snprintf(contents, inSize - pos, "%s", inBuf+pos);
// Below 2 will print string to file, but on one line
// use fprintf_json to make look nicer
//fprintf(fp, "%s", contents);
//fprintf(fp,"\n");
fprintf_json(fp,contents);
free(contents);
} else {
// Error bad string of something.
LOG(AQUA_LOG,LOG_ERR, "Bad web config '%s'\n", inBuf);
}
//fclose(fp);
aq_close_file(fp, ro_root);
return sprintf(outBuf, "{\"message\":\"Saved Web Config\"}");
}
void fprint_indentation(FILE *fp, int indent_level) {
for (int i = 0; i < indent_level; i++) {
fprintf(fp," "); // Use 2 spaces for indentation
}
}
void fprintf_json(FILE *fp, const char *json_string) {
int indent_level = 0;
int in_string = 0; // Flag to track if inside a string literal
for (int i = 0; i < strlen(json_string); i++) {
char c = json_string[i];
if (in_string) {
fprintf(fp,"%c", c);
if (c == '"' && json_string[i - 1] != '\\') { // End of string, handle escaped quotes
in_string = 0;
}
} else {
switch (c) {
case '{':
case '[':
fprintf(fp,"%c\n", c);
indent_level++;
fprint_indentation(fp,indent_level);
break;
case '}':
case ']':
fprintf(fp,"\n");
indent_level--;
fprint_indentation(fp,indent_level);
fprintf(fp,"%c", c);
break;
case ',':
fprintf(fp,"%c\n", c);
fprint_indentation(fp,indent_level);
break;
case ':':
fprintf(fp,"%c ", c);
break;
case '"':
fprintf(fp,"%c", c);
in_string = 1;
break;
default:
fprintf(fp,"%c", c);
break;
}
}
}
fprintf(fp,"\n");
}

View File

@ -3,7 +3,8 @@
int build_webconfig_js(struct aqualinkdata *aqdata, char* buffer, int size);
int build_dynamic_webconfig_js(struct aqualinkdata *aqdata, char* buffer, int size);
int save_web_config_json(const char* inBuf, int inSize, char* outBuf, int outSize, struct aqualinkdata *aqdata);
#endif

234
web/HA_tilePlugin.js Normal file
View File

@ -0,0 +1,234 @@
/*
For manual setup, follow the below.
For easy setup, read the online documentation
1) put below in aqualinkd config.json
"external_script": "/HA_tilePlugin.js"
2) Configure any custom icons in aqualinkd config.json, example below 'light.back_floodlights' is HA ID:-
"light.back_floodlights": {
"display": "true",
"on_icon": "extra/light-on.png",
"off_icon": "extra/light-off-grey.png"
},
3) Add the HA ID's you want to the setTile function below :-
homeassistantAction("light.back_floodlights");
4) IN HOME ASSISTANT you will need to allow Cross-Origin Resource Sharing
edit configuration.yaml, and add below (change aqualinkd to the machine name running aqualinkd)
http:
cors_allowed_origins
- http://aqualinkd
5) Add your HA API token & server below.
*/
// Add your HA API token
var HA_TOKEN = '<YOUR LONG HA TOKEN HERE>';
// Change to your HA server IP
var HA_SERVER = 'http://192.168.1.255:8123';
setupTiles();
function setupTiles() {
// If we have specific user agents setup, make sure one matches.
if (_config?.HA_tilePlugin && _config?.HA_tilePlugin?.userAgents) {
var found=false;
for (const agent of _config.HA_tilePlugin.userAgents) {
if (navigator.userAgent.search(agent) != -1) {
found = true;
}
}
// User agent doesn't match the list, return and tdo nothing.
if (!found) {
return;
}
}
// If aqualinkd has not added tiles, wait.
if ( document.getElementById("Filter_Pump") === null) {
setTimeout(setupTiles, 100);
return;
}
if (!_config?.HA_tilePlugin) {
// If you are not using config.json to add them, Add your HA ID's below. replace the below examples.
/* ###########################################################
#
# Add manual entries here
#
########################################################### */
//homeassistantAction("light.back_floodlights");
} else {
HA_TOKEN = _config.HA_tilePlugin.HA_token;
HA_SERVER = _config.HA_tilePlugin.HA_server;
for (const id of _config.HA_tilePlugin.HA_entity_ids) {
homeassistantAction(id);
}
}
}
/*. Some helper / formating functions */
function getStringAfterCharacter(mainString, character) {
const index = mainString.indexOf(character);
if (index === -1) {
// Character not found, return the original string or an empty string as desired
return mainString;
}
return mainString.slice(index + 1);
}
function getStringBeforeCharacter(mainString, character) {
const index = mainString.indexOf(character);
if (index === -1) {
// Character not found, return the original string or an empty string as desired
return mainString;
}
return mainString.slice(0, index);
}
function formatTwoDecimalsOrInteger(num) {
const roundedNum = Math.round(num * 100) / 100;
let result = String(roundedNum);
// Check if the string ends with '.00' and remove it if present
if (result.endsWith('.00')) {
result = result.slice(0, -3); // Remove the last 3 characters (".00")
}
return result;
}
function HA_switchTileState(id) {
state = (document.getElementById(id).getAttribute('status') == 'off') ? 'on' : 'off';
homeassistantAction(id, state);
setTileOn(id, state);
}
function HA_updateDevice(data) {
var tile
var name
if (!data || !data.state) {
console.log("Error, missing JSON values ID="+data?.entity_id+", Name="+data?.attributes?.friendly_name+", State="+data?.state);
return;
} else if (!data.attributes.friendly_name) {
name = getStringAfterCharacter(data.entity_id, ".");
} else {
name = data.attributes.friendly_name;
}
const service = getStringBeforeCharacter(data.entity_id, ".");
// If the tile doesn't exist, create it.
if ((tile = document.getElementById(data.entity_id)) == null) {
var tile = {};
tile["id"] = data.entity_id;
tile["name"] = name;
tile["display"] = "true";
if ( service == "sensor" ) {
tile["type"] = "value";
tile["value"] = data.state;
} else {
tile["type"] = "switch"; // switch or value
}
if ( service == "cover") {
tile["state"] = ((data.state == 'closed') ? 'off' : 'on')
} else {
tile["state"] = ((data.state == 'off') ? 'off' : 'on');
}
tile["status"] = tile["state"];
createTile(tile);
// Make sure we use out own callback for button press.
subdiv = document.getElementById(data.entity_id);
subdiv.setAttribute('onclick', "HA_switchTileState('" + data.entity_id + "')");
tile = document.getElementById(data.entity_id);
}
switch (service) {
case "sensor":
setTileValue(data.entity_id, formatTwoDecimalsOrInteger(data.state));
break;
case "cover":
setTileOn(data.entity_id, ((data.state == 'closed') ? 'off' : 'on'), null);
break;
case "light":
case "switch":
case "input_boolean":
default:
setTileOn(data.entity_id, ((data.state == 'off') ? 'off' : 'on'), null);
break;
}
}
function getURL(service, action) {
switch (service) {
case "sensor":
return "";
break;
case "cover":
return '/api/services/'+service+'/'+(action=="on"?"open":"close")+"_cover";
break;
case "light":
case "switch":
case "input_boolean":
default:
return '/api/services/'+service+'/turn_'+action;
break;
}
}
function homeassistantAction(id, action="status") {
var http = new XMLHttpRequest();
if (http) {
http.onreadystatechange = function() {
if (http.readyState === 4) {
if (http.status == 200) {
var data = JSON.parse(http.responseText);
// Sending action on/off returns blank from HA, so only bother acting on status messages
if (action == "status") {
HA_updateDevice(data);
}
} else {
console.error("Http error "+http.status);
}
}
}
};
if (action == "status") {
http.open('GET', HA_SERVER+'/api/states/' + id, true);
http.setRequestHeader("Content-Type", "application/json");
http.setRequestHeader("Authorization", "Bearer "+HA_TOKEN);
_poller = setTimeout(function() { homeassistantAction(id, action); }, 5000);
http.send(null);
} else {
const service = getStringBeforeCharacter(id, ".");
// Below should be api/services/cover/open_cover for cover.
http.open('POST', HA_SERVER+getURL(service,action), true);
http.setRequestHeader("Content-Type", "application/json");
http.setRequestHeader("Authorization", "Bearer "+HA_TOKEN);
http.send('{"entity_id": "'+id+'"}');
}
}

View File

@ -337,6 +337,36 @@
.config_options_added {
background-color: white;
}
.web_config_options {
top: 10px;
position: fixed;
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
/*height: 100vw;*/
}
.web_config_options_pane {
background-color: var(--options_pane_background);
border: 2px solid var(--options_pane_bordercolor);
border-radius: 20px;
justify-content: center;
align-items: center;
width: 700px;
/*max-height: 870px;*/
max-height: var(--max-config-height);
overflow-x: hidden;
overflow-y: auto;
}
.web_config_textarea {
width:90%;
/*height:700px;*/
height:var(--web-config-textarea-height);
box-sizing: border-box;
resize: vertical;
}
.hide {
display: none;
filter: alpha(opacity=0);
@ -358,6 +388,35 @@
height: 18px !important;
}
.valid_json {
background-color: var(--options_pane_background);
}
.invalid_json {
background-color: rgb(255, 100, 0);
}
#alertOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
z-index: 999;
}
#alertCustomDialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border: 1px solid #ccc;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
/*
.helptxt {
@ -383,6 +442,8 @@
_addedBlankSensor = false;
_addedBlankVirtualButton = false;
const _webConfigFile = "/config.json";
//var _cfgRestartAlertShown = false;
let _AlertsShown = {};
@ -1546,6 +1607,182 @@
*/
}
async function showWebConfig() {
var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
//console.log("Height="+h);
document.documentElement.style.setProperty('--max-config-height', (h-50)+'px');
document.documentElement.style.setProperty('--web-config-textarea-height', (h-160)+'px');
document.getElementById("web_config_options").classList.remove("hide");
document.getElementById("web_config_options_pane").classList.remove("hide");
const jsonInput = document.getElementById('webconfig-json-input');
try {
// Fetch the file from the specified path (make sure config.json exists)
const response = await fetch(_webConfigFile, { cache: 'no-store' });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Get the response as plain text
const configText = await response.text();
// Populate the textarea
jsonInput.value = configText;
jsonInput.placeholder = "Enter or edit your JSON here...";
webconfig_validateAndDisplay(); // Optional: Validate immediately upon loading
} catch (error) {
console.error('There was a problem fetching the config file:', error);
jsonInput.value = ''; // Clear placeholder
jsonInput.placeholder = "Could not load "+_webConfigFile+". Enter JSON manually.";
const resultArea = document.getElementById('validation-result');
resultArea.classList.add('invalid');
resultArea.textContent = 'Error: Could not load '+_webConfigFile+' from server.';
}
//populateconfigtable(data);
}
function closeWebConfig() {
clearconfigtable();
document.getElementById("web_config_options").classList.add("hide");
document.getElementById("web_config_options_pane").classList.add("hide");
}
function saveWebConfig() {
if (!webconfig_validateAndDisplay()) {
alert("bad web config, please validate");
//timedAlert("bad web config, please validate", 5);
return;
}
const jsonInput = document.getElementById('webconfig-json-input');
const jsonString = jsonInput.value;
const jsonObject = JSON.parse(jsonString);
var json_ordered = {
uri: "webconfig/set",
values: jsonObject
};
send_command(json_ordered);
closeWebConfig();
}
// Function to validate and display the JSON
function webconfig_validateAndDisplay() {
const jsonInput = document.getElementById('webconfig-json-input');
const resultArea = document.getElementById('validation-result');
const jsonString = jsonInput.value;
// Clear previous results
resultArea.className = '';
resultArea.textContent = '';
try {
// Attempt to parse the string into a JavaScript object
const jsonObject = JSON.parse(jsonString);
// If parsing succeeds, format the JSON nicely (pretty print)
const prettyJsonString = JSON.stringify(jsonObject, null, 2);
// Update the text area with the pretty-printed JSON
jsonInput.value = prettyJsonString;
// Display success message
resultArea.classList.add('valid_json');
resultArea.textContent = 'JSON is VALID.';
return true;
} catch (error) {
// If parsing fails, catch the error
// Display error message
resultArea.classList.add('invalid_json');
// The error object provides useful information about where the syntax error is
resultArea.textContent = 'JSON is INVALID: ' + error.message;
}
return false;
}
async function updateWebConfigFromMaster() {
let masterJSON;
// Get new master
try {
// Fetch the file from the specified path (make sure config.json exists)
// BELOW file should be from github. File with any new config keys in it
const response = await fetch("/config.json.tmp", { cache: 'no-store' });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Get the response as plain text
const text = await response.text();
masterJSON = JSON.parse(text)
// Populate the textarea
//jsonInput.value = configText;
//jsonInput.placeholder = "Enter or edit your JSON here...";
//webconfig_validateAndDisplay(); // Optional: Validate immediately upon loading
} catch (error) {
alert("Error loading master cfg "+error);
}
// Now get local
try{
const jsonInput = document.getElementById('webconfig-json-input');
const jsonString = jsonInput.value;
var webcfgJSON = JSON.parse(jsonString);
webcfgJSON = mergeMissingKeysRecursive(webcfgJSON, masterJSON);
jsonInput.value = JSON.stringify(webcfgJSON);
alert(jsonInput.value);
webconfig_validateAndDisplay();
} catch (error) {
alert("Error "+error);
}
}
function mergeMissingKeysRecursive(target, source) {
// Ensure we are dealing with actual objects at this level
if (typeof target !== 'object' || target === null || typeof source !== 'object' || source === null) {
return target;
}
// Iterate over all keys in the source object
for (const key in source) {
if (Object.hasOwn(source, key)) {
const sourceValue = source[key];
const targetValue = target[key];
// If the source value is a nested object and the target value is also
// a nested object (and not an array or null), recurse.
if (
typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue) &&
typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue)
) {
// Recursively merge the nested objects
target[key] = mergeMissingKeysRecursive(targetValue, sourceValue);
} else if (targetValue === undefined) {
// Otherwise, if the key is missing entirely in the target, add it
target[key] = sourceValue;
// console.log("Added missing key:", key);
}
// If the key already exists and is not an object pair ready for recursion, it is ignored (target takes precedence).
}
}
return target;
}
function update_status_message(message, error = false) {
try {
if (error || message.substring(0, 5).toLowerCase() == "error")
@ -2008,6 +2245,21 @@
xmlhttp.setRequestHeader("Accept","application/vnd.github.raw");
xmlhttp.send();
}
/*
function timedAlert(message, timeout) {
document.getElementById('alertDialogMessage').innerText = message;
document.getElementById('alertOverlay').style.display = 'block';
document.getElementById('alertCustomDialog').style.display = 'block';
// Set a timeout to automatically close the dialog
setTimeout(closeAlert, timeout * 1000);
}
function closeAlert() {
document.getElementById('alertOverlay').style.display = 'none';
document.getElementById('alertCustomDialog').style.display = 'none';
}
*/
</script>
<body onload="init();init_collapsible();">
@ -2128,6 +2380,10 @@
<input id="editconfig" type="button" onclick="editconfig(this);"
value="edit config"></input>
</td>
<td align="center">
<input id="webconfig" type="button" onclick="showWebConfig(this);"
value="web config"></input>
</td>
<td align="center">
<input id="downloadconfig" type="button" onclick="requestFileDownload(this);""
value="download config"></input>
@ -2179,9 +2435,51 @@
</td>
</tr></table>
</div>
</div>
</div>
<div id="web_config_options" class="web_config_options hide" style="display: flex;">
<div id="web_config_options_pane" class="web_config_options_pane hide">>
<!--<div class="inner">END</div>-->
<table id="config_table" border="0" cellpadding="1px" width="100%" style="border-collapse: collapse;">
<tbody><tr class="options_title">
<th colspan="3" style="padding: 10px;"><span>AqualinkD Web Configuration</span></th>
</tr>
<tr>
<td align="center" colspan="3">
<textarea class="web_config_textarea" id="webconfig-json-input" placeholder="Loading configuration..."></textarea>
</td>
</tr>
<tr>
<td align="right" style="width:33%;padding-right: 10px;">
<input type="button" onclick="webconfig_validateAndDisplay()" value="Validate"></input>
</td>
<td <td align="center" style="width:33%;">
<input type="button" onclick="saveWebConfig()" value="Save"></input>
</td>
<td align="left" style="width:33%;padding-left: 10px;">
<input type="button" onclick="closeWebConfig()" value="Close without Saving"></input>
<!--<input onclick="updateWebConfigFromMaster()" value="get new"></input>-->
<!-- For the future, to add new keys to json config pulling from github-->
</td>
</tr>
<tr>
<td colspan="3">
<div id="validation-result" style="padding-left: 10px;padding-right: 10px;"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!--<iframe src='about:blank' id="logdownload"></iframe>-->
</div>
<!--
<div id="alertOverlay" style="display: none;"></div>
<div id="alertCustomDialog" style="display: none;">
<p id="alertDialogMessage"></p>
<button onclick="closeAlert()">Close</button>
</div>
-->
</body>
</html>

View File

@ -1,180 +0,0 @@
// Display order of devices. Tiles will be displayed in the order below,
// any devices you don't want to see you can comment the ID. (with // e.g. `//"Solar_Heater",` )
// If the device isn't listed below is will NOT be shown.
// For a complete list returned from your particular aqualinkd instance
// use the below URL and look at the ID value for each device.
// http://aqualink.ip.address/api/devices
var config_js=true;
var devices = [
"Filter_Pump",
"Spa",
"Aux_1",
"Aux_2",
"Aux_3",
"Aux_4",
"Aux_5",
"Aux_6",
"Aux_7",
"Aux_B1",
"Aux_B2",
"Aux_B3",
"Aux_B4",
"Aux_B5",
"Aux_B6",
"Aux_B7",
"Aux_B8",
"Pool_Heater",
"Spa_Heater",
"SWG",
//"SWG/Percent",
"SWG/PPM",
//"SWG/Boost",
"Temperature/Air",
"Temperature/Pool",
"Temperature/Spa",
"Pool_Water",
"Spa_Water",
"Freeze_Protect",
"CHEM/pH",
"CHEM/ORP",
"Solar_Heater",
"Extra_Aux",
"Chiller",
"Aux_V1",
"Aux_V2",
"Aux_V3",
"Aux_V4",
"Aux_V5",
"Aux_V6",
"Aux_V7",
"Aux_V8",
"Aux_V9",
"Aux_V10",
"Aux_V11",
"Aux_V12",
"Aux_V13",
"Aux_V14",
"Aux_V15",
"Aux_S1",
"Aux_S2",
"Aux_S3",
"Aux_S4",
"Aux_S5",
"Aux_S6",
"Aux_S7",
"Aux_S8",
"Aux_S9",
"Aux_S10",
];
// all SWG return a status number, some have different meanings. Change the text below to suit, NOT THE NUMBER.
var swgStatus = {
0: "On",
1: "No flow",
2: "Low salt",
4: "High salt",
8: "Clean cell",
9: "Turning off",
16: "High current",
32: "Low volts",
64: "Low temp",
128: "Check PCB",
253: "General Fault",
254: "Unknown",
255: "Off"
}
/*
* BELOW IS NOT RELIVENT FOR simple.html or simple inteface
*
*/
// Background image, delete or leave blank for solid color
//var background_url = "http://192.168.144.224/snap.jpeg";
var background_url='hk/background.jpg';
//var background_url='';
// Reload background image every X seconds.(useful if camera snapshot)
// 0 means only load once when page loads.
//var background_reload = 10;
//var background_reload = 0;
// By default all Variable Speed Pumps will show RPM or GPM depending on how they are controlled.
// this will show RPM for all pumps (ie Jandy VF pumps)
//var show_vsp_gpm=false;
// Keeps sensor tiles in on state showing last reading. (default true= turn off tiles when device is off. ie SWG=off turn off PPM tile)
//var turn_off_sensortiles = false;
// This will turn on/off the Spa Heater when you turn on/off Spa Mode.
//var link_spa_and_spa_heater = true;
/* Example of setting custom range and appropriate messages
var tile_thresholds = {
"SWG/PPM": {
outofrange: {min: 2600, max: 3500, mintext:"Add Salt"},
attention: {min: 2700, max: 3400, mintext:"Add Salt"}
},
"CHEM/pH": {
outofrange: {min: 7, max: 8},
attention: {min: 7.2, max: 7.8, mintext:"Low", maxtext:"High"}
},
"CHEM/ORP": {
outofrange: {min: 560, max: 900},
attention: {min: 650, max: 850, mintext:"Low", maxtext:"High"}
},
// Example of how to set color to Aux_S1 (if Aux_S1 was a CPU temperature)
//"Aux_S1": {
// outofrange: {min: 0, max: 170, maxtext:"ALERT High"},
// attention: {min: 0, max: 140, maxtext:"High"}
//},
}
*/
// Change the min max for heater slider
var heater_slider_min = 36;
var heater_slider_max = 104;
// Change the slider for timers
var timer_slider_min = 0;
//var timer_slider_max = 360;
//var timer_slider_step = 10;
var timer_slider_max = 120;
var timer_slider_step = 1;
// Colors
var body_background = "#EBEBEA";
var body_text = "#000000";
var options_pane_background = "#F5F5F5";
var options_pane_bordercolor = "#7C7C7C";
var options_slider_highlight = "#2196F3";
var options_slider_lowlight = "#D3D3D3";
var head_background = "#2B6A8F";
var head_text = "#FFFFFF)";
var error_background = "#8F2B2B";
var tile_background = "#DCDCDC";
var tile_text = "#6E6E6E";
var tile_on_background = "#FFFFFF";
var tile_on_text = "#000000";
var tile_status_text = "#575757";
// Change the default color for vales in and out of range.
//var value_tile_normal_color = "#4ec400ff";
//var value_tile_attention_color = "#ffbf00ff";
//var value_tile_outofrange_color = "#ff0000";
// Dark colors
// var body_background = "#000000";
// var tile_background = "#646464";
// var tile_text = "#B9B9B9";
// var tile_status_text = "#B2B2B2";
// var head_background = "#000D53";
// REMOVE THIS.
//document.writeln("<script type='text/javascript' src='extra/extra.js'></script>");

302
web/config.json Normal file
View File

@ -0,0 +1,302 @@
{
"background_image": {
"url": "-hk/background.jpg",
"url_reload": 0
},
"colors": {
"body_background": "#EBEBEB",
"body_text": "#000000",
"options_pane_background": "#F5F5F5",
"options_pane_bordercolor": "#7C7C7C",
"options_slider_highlight": "#2196F3",
"options_slider_lowlight": "#D3D3D3",
"head_background": "#2B6A8F",
"head_text": "#FFFFFF",
"error_background": "#8F2B2B",
"tile_background": "#DCDCDC",
"tile_text": "#6E6E6E",
"tile_on_background": "#FFFFFF",
"tile_on_text": "#000000",
"tile_status_text": "#575757",
"value_tile_normal_color": "#049FF8",
"value_tile_normal_color_": "#4ec400ff",
"value_tile_attention_color": "#FF8000",
"value_tile_outofrange_color": "#FF6400",
"options_radio_highlight": "#2196F3",
"options_radio_lowlight": "#D3D3D3",
"tile_icon_background_color_heat": "rgb(255, 123, 0)",
"tile_icon_background_color_cool": "rgb(4, 159, 248)",
"tile_icon_background_color_enabled": "rgb(78, 196, 0)",
"tile_icon_background_color_disabled": "rgb(110, 110, 110)"
},
"devices": {
"Filter_Pump": {
"display": "true"
},
"Spa": {
"display": "true"
},
"Aux_1": {
"display": "true"
},
"Aux_2": {
"display": "true"
},
"Aux_3": {
"display": "true"
},
"Aux_4": {
"display": "true"
},
"Aux_5": {
"display": "true"
},
"Aux_6": {
"display": "true"
},
"Aux_7": {
"display": "true"
},
"Aux_B1": {
"display": "true"
},
"Aux_B2": {
"display": "true"
},
"Aux_B3": {
"display": "true"
},
"Aux_B4": {
"display": "true"
},
"Aux_B5": {
"display": "true"
},
"Aux_B6": {
"display": "true"
},
"Aux_B7": {
"display": "true"
},
"Aux_B8": {
"display": "true"
},
"Pool_Heater": {
"display": "true"
},
"Spa_Heater": {
"display": "true"
},
"SWG": {
"display": "true"
},
"SWG/PPM": {
"display": "true"
},
"SWG/Percent": {
"display": "false"
},
"SWG/Boost": {
"display": "false"
},
"Temperature/Air": {
"display": "true"
},
"Temperature/Pool": {
"display": "true"
},
"Temperature/Spa": {
"display": "true"
},
"Pool_Water": {
"display": "true"
},
"Spa_Water": {
"display": "true"
},
"Freeze_Protect": {
"display": "true"
},
"CHEM/pH": {
"display": "true"
},
"CHEM/ORP": {
"display": "true"
},
"Solar_Heater": {
"display": "true"
},
"Extra_Aux": {
"display": "true"
},
"Chiller": {
"display": "true"
},
"Aux_V1": {
"display": "true"
},
"Aux_V2": {
"display": "true"
},
"Aux_V3": {
"display": "true"
},
"Aux_V4": {
"display": "true"
},
"Aux_V5": {
"display": "true"
},
"Aux_V6": {
"display": "true"
},
"Aux_V7": {
"display": "true"
},
"Aux_V8": {
"display": "true"
},
"Aux_V9": {
"display": "true"
},
"Aux_V10": {
"display": "true"
},
"Aux_V11": {
"display": "true"
},
"Aux_V12": {
"display": "true"
},
"Aux_V13": {
"display": "true"
},
"Aux_V14": {
"display": "true"
},
"Aux_V15": {
"display": "true"
},
"Sensor/Aux_S1": {
"display": "true"
},
"Sensor/Aux_S2": {
"display": "true"
},
"Sensor/Aux_S3": {
"display": "true"
},
"Sensor/Aux_S4": {
"display": "true"
},
"Sensor/Aux_S5": {
"display": "true"
},
"Sensor/Aux_S6": {
"display": "true"
},
"Sensor/Aux_S7": {
"display": "true"
},
"Sensor/Aux_S8": {
"display": "true"
},
"Sensor/Aux_S9": {
"display": "true"
},
"Sensor/Aux_S10": {
"display": "true"
}
},
"slider_range": {
"heater_slider_min": 36,
"heater_slider_max": 104,
"timer_slider_min": 0,
"timer_slider_max": 120,
"timer_slider_step": 1
},
"tile_thresholds": {
"SWG/PPM": {
"outofrange": {
"min": 2600,
"max": 3500,
"mintext": "Add Salt"
},
"attention": {
"min": 2700,
"max": 3400,
"mintext": "Add Salt"
}
},
"CHEM/pH": {
"outofrange": {
"min": 7,
"max": 8
},
"attention": {
"min": 7.2,
"max": 7.8,
"mintext": "Low",
"maxtext": "High"
}
},
"CHEM/ORP": {
"outofrange": {
"min": 560,
"max": 900
},
"attention": {
"min": 650,
"max": 850,
"mintext": "Low",
"maxtext": "High"
}
}
},
"swg_status": {
"0": "On",
"1": "No flow",
"2": "Low salt",
"4": "High salt",
"8": "Clean cell",
"9": "Turning off",
"16": "High current",
"32": "Low volts",
"64": "Low temp",
"128": "Check PCB",
"253": "General Fault",
"254": "Unknown",
"255": "Off"
},
"tile_settings": {
"turn_off_sensortiles": "true",
"show_vsp_gpm": "true",
"disable_off_icon_background": "true"
},
"EXAMPLE_colors_dark": {
"body_background": "#121212",
"body_text": "#FFFFFF",
"options_pane_background": "#1E1E1E",
"options_pane_bordercolor": "#555555",
"options_slider_highlight": "#64B5F6",
"options_slider_lowlight": "#555555",
"head_background": "#004d7a",
"head_text": "#FFFFFF",
"error_background": "#b00020",
"tile_background": "#333333",
"tile_text": "#AAAAAA",
"tile_on_background": "#BBBBBB",
"tile_on_text": "#000000",
"tile_status_text": "#888888",
"value_tile_normal_color": "#049FF8",
"value_tile_normal_color_": "#69F0AE",
"value_tile_attention_color": "#FFB74D",
"value_tile_outofrange_color": "#FF8A65",
"options_radio_highlight": "#64B5F6",
"options_radio_lowlight": "#555555",
"tile_icon_background_color_heat": "rgb(255, 123, 0)",
"tile_icon_background_color_cool": "rgb(4, 159, 248)",
"tile_icon_background_color_enabled": "rgb(78, 196, 0)",
"tile_icon_background_color_disabled": "rgb(110, 110, 110)"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
aqmanager.html

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

View File

@ -1 +0,0 @@
./switch-off.png

View File

@ -1 +0,0 @@
./switch-on.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Some files were not shown because too many files have changed in this diff Show More