Release 3.0.0

pull/492/head
sfeakes 2025-12-12 16:38:18 -06:00
parent daa6b350be
commit a5db4e0100
24 changed files with 592 additions and 188 deletions

View File

@ -154,7 +154,6 @@ AqualinkD will be moving over to github hosted runners for compiling, currently
<br>
# 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.
* 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.
@ -164,7 +163,7 @@ AqualinkD will be moving over to github hosted runners for compiling, currently
* HTTPS is for two way auth only, ie You create your own cert and load on both AqualinkD server and all client devices.
* Example script to generate HTTPS certificates is in (./extras/generate-certs.sh)
* 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)
* Added option to select version to install, including dev releases.
* MQTT Discovery for all supporting hubs (HomeAssistant Domoticz Hubitat OpenHAB etc)
* Moved Domoticz support over to MQTT autodiscovery.
* Change tile color & label for ph / orp & ppm tiles when values are out of optimal range.
@ -175,12 +174,13 @@ AqualinkD will be moving over to github hosted runners for compiling, currently
* Changed caching of HTTP server. (Better for UI config updates)
* Autoconfigure will now get panel size/type for panels that support PC-Dock interface.
* Autoconfigure will *try* to work for PDA panels.
* PDA Color light fixes.
* PDA Dimmer lights now supported.
* Cleaned up exit & errors when running as daemon and docker.
* Fixed issues with external sensors and homekit.
* Added preliminary support for Jandy Infinite water color lights
* Added preliminary support for Jandy Infinite water color lights.
- Need to finish off :-
* HAT serial optimizations broke some USB serial adapters
* SWG not auto finding
# Updates in 2.6.11 (Sept 14 2025)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -583,7 +583,7 @@ void *set_allbutton_light_programmode( void *ptr )
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aqdata = threadCtrl->aqdata;
const int seconds = 1000;
waitForSingleThreadOrTerminate(threadCtrl, AQ_SET_LIGHTPROGRAM_MODE);
@ -643,12 +643,14 @@ void *set_allbutton_light_programmode( void *ptr )
// Light is on, we know the mode, we can advance to next mode without re-setting program to 1
int cmode = ((clight_detail *)button->special_mask_ptr)->currentValue;
if (cmode > 0) {
int numPrograms = get_num_light_modes(0);
int adv_steps = (val - cmode + numPrograms) % numPrograms;
int numPrograms = get_num_light_modes(0) - 1; // Need to ignore program 0=off, so reduce by 1
//int adv_steps = (val - cmode + numPrograms) % numPrograms;
int adv_steps = ((val - cmode) % numPrograms + numPrograms) % numPrograms;
LOG(ALLB_LOG, LOG_INFO, "Advancing Light program by %d (current=%d, new=%d, total=%d)\n",adv_steps, cmode, val, numPrograms);
if (adv_steps > 0) {
send_cmd(button->code);
waitfor_queue2empty();
delay(pmode * seconds); // 0.3 works, but using 0.4 to be safe
}
final_mode = val;
val = adv_steps;
@ -662,7 +664,7 @@ void *set_allbutton_light_programmode( void *ptr )
//LOG(ALLB_LOG, LOG_INFO, "Not using advance mode (state=%d, usemode=%d)\n",button->led->state,useProgAdvance);
}
const int seconds = 1000;
// Needs to start programming sequence with light on, if off we need to turn on for 15 seconds
// before we can send the next off.
if ( button->led->state != ON && iOn > 0) {

View File

@ -1453,9 +1453,13 @@ bool setDeviceState(struct aqualinkdata *aqdata, int deviceIndex, bool isON, req
LOG(PANL_LOG, LOG_INFO, "received '%s' for '%s', turning '%s'\n", (isON == false ? "OFF" : "ON"), button->name, (isON == false ? "OFF" : "ON"));
#ifdef AQ_PDA
if (isPDA_PANEL) {
if (button->special_mask & PROGRAM_LIGHT && isPDA_IAQT) {
if (button->special_mask & PROGRAM_LIGHT/* && isPDA_IAQT*/) {
//if ( isPDA_IAQT && isIAQL_ACTIVE) {
// AqualinkTouch in PDA mode, we can program light. (if turing off, use standard AQ_PDA_DEVICE_ON_OFF below)
programDeviceLightMode(aqdata, (isON?USE_LAST_VALUE:0), deviceIndex, (source==NET_MQTT?true:false), source);
// programDeviceLightMode(aqdata, (isON?USE_LAST_VALUE:0), deviceIndex, (source==NET_MQTT?true:false), source);
//} else {
aq_programmer(AQ_SET_LIGHTCOLOR_MODE, button, (isON == false ? 0 : 4), true, aqdata);
//}
} else {
// If we are using AqualinkTouch with iAqualink enabled, we can send button on/off much faster using that.
if ( isPDA_IAQT && isIAQL_ACTIVE) {
@ -1571,10 +1575,18 @@ void programDeviceLightBrightness(struct aqualinkdata *aqdata, int value, int de
{
//int extra_value = false;
clight_detail *light = getProgramableLight(aqdata, deviceIndex);
if (value < 0 || value > 100) {
LOG(PANL_LOG,LOG_ERR, "Dimmer value %d is not valid, using %d\n",value,AQ_CLAMP(value,0,100));
value = AQ_CLAMP(value,0,100);
}
if (light->lightType == LC_DIMMER && value !=0 && value != 25&& value != 50 && value != 75 && value != 100) {
// Make sure we have 0/25/50/75 for LC_DIMMER
LOG(PANL_LOG,LOG_DEBUG, "Dimmer value %d is not valid, rounding to nearest 25!\n",value);
value = dimmer_mode_to_percent(dimmer_percent_to_mode_index(value));
}
if (expectMultiple) {
// Queue up a request, this will call us back through with expectMultiple=false
@ -1585,8 +1597,6 @@ void programDeviceLightBrightness(struct aqualinkdata *aqdata, int value, int de
return;
}
clight_detail *light = getProgramableLight(aqdata, deviceIndex);
if (light == NULL || (light->lightType != LC_DIMMER2 && light->lightType != LC_DIMMER)) {
LOG(PANL_LOG,LOG_ERR, "Can not set light brightness on device '%s'\n",aqdata->aqbuttons[deviceIndex].label);
return;
@ -1597,6 +1607,11 @@ void programDeviceLightBrightness(struct aqualinkdata *aqdata, int value, int de
return;
}
if (isPDA_PANEL) {
aq_programmer(AQ_SET_LIGHTCOLOR_MODE, light->button, dimmer_percent_to_mode_index(value), false, aqdata);
return;
}
if (value == 0) {
// We simply need to turn the light off at this point, so use allbutton key as it's the quickest.
// but can't turn off a virtual light.
@ -1692,6 +1707,16 @@ void programDeviceLightMode(struct aqualinkdata *aqdata, int value, int deviceIn
return;
}
// If it's a PDA panel,
if (isPDA_PANEL) {
if (value == USE_LAST_VALUE) {
extra_value = true;
value = 1;
}
aq_programmer(AQ_SET_LIGHTCOLOR_MODE, light->button, value, extra_value, aqdata);
return;
}
// Turn on a light with no mode set.
if (value == USE_LAST_VALUE) {
extra_value = true;
@ -1883,7 +1908,7 @@ void updateLightProgram(struct aqualinkdata *aqdata, int value, clight_detail *l
if (value > 0 && light->lastValue != value) {
light->lastValue = value;
if (_aqconfig_.save_light_programming_value && light->lightType == LC_PROGRAMABLE ) {
LOG(PANL_LOG,LOG_NOTICE, "Writing light programming value to config for %s\n",light->button->label);
LOG(PANL_LOG,LOG_INFO, "Writing light programming value to config for %s\n",light->button->label);
writeCfg(aqdata);
}
}

View File

@ -502,6 +502,7 @@ void _aq_programmer_(program_type r_type, char *args, aqkey *button, int value,
program_type type = r_type;
//DPRINTF("**** aq_programmer() with %d - %s\n",r_type, ptypeName(type));
// RS SerialAdapter is quickest for changing thermostat temps, so use that if enabeled.
// VSP RPM can only be changed with oneTouch or iAquatouch so check / use those
// VSP Program is only available with iAquatouch, so check / use that.

View File

@ -5,6 +5,7 @@
//#define COLOR_LIGHTS_C_
#include "color_lights.h"
#include "rs_msg_utils.h"
/*
@ -185,8 +186,6 @@ void setColorLightsPanelVersion(uint8_t supported)
bool is_valid_light_mode(clight_type type, int index)
{
printf("Checking _color_light_options[%d][%d]\n",type,index);
if (type == LC_DIMMER2) {
if (index >= 0 && index <=100) {
return true;
@ -199,8 +198,6 @@ bool is_valid_light_mode(clight_type type, int index)
return false;
}
printf("result = %s\n", _color_light_options[type][index]);
return true;
}
@ -317,6 +314,23 @@ const char *light_mode_name(clight_type type, int index, emulation_type protocol
return _color_light_options[type][index];
}
int light_mode_index(clight_type type, const char *name)
{
for (int i=0; i < LIGHT_COLOR_OPTIONS; i++) {
if (_color_light_options[type][i] == NULL) {
return AQ_UNKNOWN;
}
if ( rsm_strmatch(_color_light_options[type][i], name) == 0) {
return i;
} else if (rsm_strmatch_ignore(name, _color_light_options[type][i], -1) == 0) { // Remove 1 char from check for USA! to USA.
return i;
}
}
return AQ_UNKNOWN;
}
bool isShowMode(const char *mode)
{

View File

@ -42,6 +42,7 @@ void clear_aqualinkd_light_modes();
bool set_currentlight_value(clight_detail *light, int index);
bool is_valid_light_mode(clight_type type, int index);
const char* lightTypeName(clight_type type);
int light_mode_index(clight_type type, const char *name);
bool set_aqualinkd_light_mode_name(char *name, int index, bool isShow);
const char *get_aqualinkd_light_mode_name(int index, bool *isShow);

View File

@ -1753,7 +1753,10 @@ void check_print_config (struct aqualinkdata *aqdata)
}
if ( ! is_aqualink_touch_id(_aqconfig_.extended_device_id) && ! is_onetouch_id(_aqconfig_.extended_device_id)) {
setMASK(errors, ERR_VSP_EXTENDEDID);
// Ignore this error for PDA panels. PDA needs it to read VSP. (but can;t set VSP)
if (_aqconfig_.device_id != 0x60) {
setMASK(errors, ERR_VSP_EXTENDEDID);
}
//LOG(AQUA_LOG,LOG_WARNING, "Config error, '%s' must be set for VSP's\n", CFG_N_extended_device_id_programming);
}
}
@ -1775,6 +1778,15 @@ void check_print_config (struct aqualinkdata *aqdata)
LOG(AQUA_LOG,LOG_WARNING, "Config error, Light mode %d is only valid for a virtual button\n",LC_JANDYINFINATE);
}
}
if (aqdata->lights[i].lightType == LC_PROGRAMABLE && _aqconfig_.device_id == 0x60) {
LOG(AQUA_LOG,LOG_WARNING, "Config error, Light mode %d is not supported in PDA mode\n",LC_PROGRAMABLE);
}
if (aqdata->lights[i].lightType == LC_DIMMER && _aqconfig_.device_id == 0x60) {
LOG(AQUA_LOG,LOG_WARNING, "Config error, Light mode %d is not supported in PDA mode\n",LC_PROGRAMABLE);
}
if (aqdata->lights[i].lightType == LC_DIMMER2 && _aqconfig_.device_id == 0x60) {
LOG(AQUA_LOG,LOG_WARNING, "Config error, Light mode %d is not supported in PDA mode\n",LC_PROGRAMABLE);
}
}
// Check valid setting

View File

@ -823,10 +823,6 @@ void processPage(struct aqualinkdata *aqdata)
}
}
// if enable_iaqualink this poll count can be increased if we sit on the device status page
// all device status are quicker to update in enable_iaqualink, so leaves just pump/swg info to get.
#define FULL_STATUS_POLL_COUNT 200 // We did have this at 20, but put too much load on panel, (couldn't program light)
#define DEVICE_STATUS_POLL_COUNT 20 // This must be less than FULL_STATUS_POLL_COUNT
//#define REQUEST_DEVICES_POLL_COUNT 30 // if _aqconfig_.enable_iaqualink=true then REQUEST_STATUS_POLL_COUNT will be used.
@ -1083,7 +1079,7 @@ if not programming && poll packet {
LOG(IAQT_LOG,LOG_ERR,"Poll count=%d, too high, looks like page is stuck\n",_pollCnt);
_pollCnt=0;
} else {
LOG(IAQT_LOG,LOG_DEBUG,"Poll count=%d, Curent Page=0x%02hhx\n",_pollCnt, _currentPage);
//LOG(IAQT_LOG,LOG_DEBUG,"Poll count=%d, Curent Page=0x%02hhx\n",_pollCnt, _currentPage);
}
if (_pollCnt++ > FULL_STATUS_POLL_COUNT) {

View File

@ -4,6 +4,17 @@
#define IAQT_MSGLEN 21
// if enable_iaqualink this poll count can be increased if we sit on the device status page
// all device status are quicker to update in enable_iaqualink, so leaves just pump/swg/chiller info to get.
// Below are numbers that are coprime or have few common factors, so less lightly to be executed at the same time.
#define FULL_STATUS_POLL_COUNT 199 // We did have this at 20, but put too much load on panel, (couldn't program light)
#define DEVICE_STATUS_POLL_COUNT 43 // This must be less than FULL_STATUS_POLL_COUNT
#define IAQALINK_STATUS_POLL_COUNT 101
struct iaqt_page_button {
char name[IAQT_MSGLEN];
unsigned char type; // 0x01 box, 0x08 icon wirlpool, 0x0b icon heater, 0x01 icon (main page), 0x07 light

View File

@ -21,6 +21,7 @@
#include "aq_serial.h"
#include "aqualink.h"
#include "iaqualink.h"
#include "iaqtouch.h"
#include "packetLogger.h"
#include "aq_serial.h"
#include "serialadapter.h"
@ -681,7 +682,7 @@ bool process_iaqualink_packet(unsigned char *packet, int length, struct aqualink
if (packet[PKT_CMD] == 0x53)
{
cnt++;
if (cnt == 20) { // 20 is probably too low, should increase. (only RS16 and below)
if (cnt >= IAQALINK_STATUS_POLL_COUNT) { // 20 is probably too low, should increase. (only RS16 and below)
cnt=0;
/*
sendid=sendid==0x18?0x60:0x18;

View File

@ -261,9 +261,9 @@ int LED2int(aqledstate state)
}
}
#define AUX_BUFFER_SIZE 200
#define AUX_BUFFER_SIZE 500
char *get_aux_information(aqkey *button, struct aqualinkdata *aqdata, char *buffer)
char *get_aux_information(aqkey *button, struct aqualinkdata *aqdata, char *buffer, bool homekit)
{
int i;
int length = 0;
@ -293,7 +293,7 @@ char *get_aux_information(aqkey *button, struct aqualinkdata *aqdata, char *buff
//printf("Button %s is ProgramableLight\n", button->name);
for (i=0; i < aqdata->num_lights; i++) {
if (button == aqdata->lights[i].button) {
if (aqdata->lights[i].lightType == LC_DIMMER2) {
if (aqdata->lights[i].lightType == LC_DIMMER2 ) {
length += sprintf(buffer, ",\"type_ext\": \"light_dimmer\", \"Light_Type\":\"%d\", \"Light_Program\":\"%d\", \"Program_Name\":\"%d%%\" ",
aqdata->lights[i].lightType,
aqdata->lights[i].currentValue,
@ -304,7 +304,12 @@ char *get_aux_information(aqkey *button, struct aqualinkdata *aqdata, char *buff
aqdata->lights[i].currentValue,
get_currentlight_mode_name(aqdata->lights[i], ALLBUTTON));
//light_mode_name(aqdata->lights[i].lightType, aqdata->lights[i].currentValue, ALLBUTTON));
length += sprintf(buffer+length, ",\"light_programs\": [");
length += build_color_light_jsonarray(aqdata->lights[i].lightType, &buffer[length], AUX_BUFFER_SIZE-length);
length += sprintf(buffer+length, "]");
}
return buffer;
}
}
@ -380,7 +385,7 @@ int build_device_JSON(struct aqualinkdata *aqdata, char* buffer, int size, bool
// We will add this VButton as a thermostat
continue;
}
get_aux_information(&aqdata->aqbuttons[i], aqdata, aux_info);
get_aux_information(&aqdata->aqbuttons[i], aqdata, aux_info, homekit);
//length += sprintf(buffer+length, "{\"type\": \"switch\", \"type_ext\": \"switch_vsp\", \"id\": \"%s\", \"name\": \"%s\", \"state\": \"%s\", \"status\": \"%s\", \"int_status\": \"%d\" %s},",
length += sprintf(buffer+length, "{\"type\": \"switch\", \"id\": \"%s\", \"name\": \"%s\", \"state\": \"%s\", \"status\": \"%s\", \"int_status\": \"%d\" %s},",
aqdata->aqbuttons[i].name,
@ -659,6 +664,7 @@ int build_aqualink_aqmanager_JSON(struct aqualinkdata *aqdata, char* buffer, int
length += sprintf(buffer+length, ",\"panel_type_full\":\"%s\"",getPanelString());
length += sprintf(buffer+length, ",\"panel_type\":\"%s\"",getShortPanelString());
length += sprintf(buffer+length, ",\"panel_revision\":\"%s %s\"",aqdata->panel_cpu, aqdata->panel_rev );//8157 REV MMM",
length += sprintf(buffer+length, ",\"panel_reported_string\":\"%s\"",aqdata->panel_string);

View File

@ -778,8 +778,6 @@ void rs16led_update(struct aqualinkdata *aqdata, int updated) {
}
}
bool new_menu(struct aqualinkdata *aqdata)
{
static bool initRS = false;
@ -800,8 +798,9 @@ bool new_menu(struct aqualinkdata *aqdata)
}
rtn = log_qeuiptment_status(aqdata);
// Hit select to get to next menu ASAP.
if ( in_ot_programming_mode(aqdata) == false )
ot_queue_cmd(KEY_ONET_SELECT);
if ( in_ot_programming_mode(aqdata) == false ) {
ot_queue_cmd(KEY_ONET_SELECT); // This makes it run through the menu's quicker. But no need to do it.
}
break;
case OTM_SET_TEMP:
rtn = log_heater_setpoints(aqdata);

View File

@ -45,6 +45,7 @@ bool waitForPDAMessageHighlight(struct aqualinkdata *aqdata, int highlighIndex,
bool waitForPDAMessageType(struct aqualinkdata *aqdata, unsigned char mtype, int numMessageReceived);
bool waitForPDAMessageTypes(struct aqualinkdata *aqdata, unsigned char mtype1, unsigned char mtype2, int numMessageReceived);
bool waitForPDAMessageTypesOrMenu(struct aqualinkdata *aqdata, unsigned char mtype1, unsigned char mtype2, int numMessageReceived, char *text, int line);
bool waitForPDAMessages(struct aqualinkdata *aqdata, int numberMessages);
bool goto_pda_menu(struct aqualinkdata *aqdata, pda_menu_type menu);
bool wait_pda_selected_item(struct aqualinkdata *aqdata);
bool waitForPDAnextMenu(struct aqualinkdata *aqdata);
@ -804,6 +805,39 @@ void *set_aqualink_PDA_device_on_off( void *ptr )
}
#endif
bool waitForLightCycleMessage(struct aqualinkdata *aqdata)
{
waitfor_queue2empty();
// Wait for the message to appear.
waitForPDAMessages(aqdata, 15);
// check the message did appear ..... PDA Menu Line 4 = Please
if (rsm_strmatch(pda_m_line(4), "Please") == 0)
{
// Wait for it to disapear
waitForPDAMessageTypes(aqdata, CMD_PDA_HIGHLIGHT, CMD_PDA_HIGHLIGHTCHARS, 60); // Long wait
}
else
{
LOG(PDA_LOG, LOG_WARNING, "PDA light Programming :- Didn't see Cycling message\n");
return false;
}
return true;
}
bool waitForLightOffMessage(struct aqualinkdata *aqdata)
{
if (rsm_strmatch(pda_m_line(3),"Light will turn") == 0) {
waitForPDAnextMenu(aqdata);
} else {
LOG(PDA_LOG,LOG_WARNING, "PDA light Programming :- Didn't see off message\n");
return false;
}
return true;
}
void *set_aqualink_PDA_light_mode( void *ptr )
{
@ -822,8 +856,11 @@ void *set_aqualink_PDA_light_mode( void *ptr )
struct programmerArgs *pargs = &threadCtrl->pArgs;
aqkey *button = threadCtrl->pArgs.button;
//unsigned char code = pargs->button->code;
int val = pargs->value;
int typ = ((clight_detail *)button->special_mask_ptr)->lightType;
int mode = pargs->value;
use_current_mode = pargs->alt_value;
//clight_type typ = ((clight_detail *)button->special_mask_ptr)->lightType;
clight_detail *light = (clight_detail *)button->special_mask_ptr;
clight_type typ = light->lightType;
#else
char *buf = (char*)threadCtrl->thread_args;
int val = atoi(&buf[0]);
@ -845,26 +882,14 @@ void *set_aqualink_PDA_light_mode( void *ptr )
return ptr;
}
clight_type lighttype = ((clight_detail *)button->special_mask_ptr)->lightType;
mode_name = light_mode_name(typ, mode, AQUAPDA);
if (val <= 0) {
use_current_mode = true;
LOG(PDA_LOG, LOG_INFO, "PDA Light Programming #: %d, on button: %s, color light type: %d, using current mode\n", val, button->label, typ);
} else {
if (lighttype == LC_DIMMER2 || LC_DIMMER2 == LC_DIMMER) {
mode_name = light_mode_name(typ, val-1, AQUAPDA);
} else {
mode_name = light_mode_name(typ, val, AQUAPDA);
}
use_current_mode = false;
if (mode_name == NULL) {
LOG(PDA_LOG, LOG_ERR, "PDA Light Programming #: %d, on button: %s, color light type: %d, couldn't find mode name '%s'\n", val, button->label, typ, mode_name);
if (mode_name == NULL) {
LOG(PDA_LOG, LOG_ERR, "PDA Light Programming #: Received %d, on button: %s, color light type: %d, couldn't find mode name '%s'\n", mode, button->label, typ, mode_name);
cleanAndTerminateThread(threadCtrl);
return ptr;
} else {
LOG(PDA_LOG, LOG_INFO, "PDA Light Programming #: %d, on button: %s, color light type: %d, name '%s'\n", val, button->label, typ, mode_name);
}
} else {
LOG(PDA_LOG, LOG_INFO, "PDA Light Programming #: Received %d, on button: %s, color light type: %d, name '%s'\n", mode, button->label, typ, mode_name);
}
if (! goto_pda_menu(aqdata, PM_EQUIPTMENT_CONTROL)) {
@ -876,26 +901,80 @@ void *set_aqualink_PDA_light_mode( void *ptr )
if ( find_pda_menu_item(aqdata, button->label, 0) ) { // Remove 1 char to account for '100%' (4 chars not the usual 3)
LOG(PDA_LOG,LOG_INFO, "PDA Light Programming, found device '%s', changing to '%s'\n",button->label,mode_name);
force_queue_delete(); // NSF This is a bad thing to do. Need to fix this
// get the status as it would have been updated by pda.c seeing the state so we know it's accurate.
// BUT, it could change after next key press
aqledstate current_state = button->led->state;
send_pda_cmd(KEY_PDA_SELECT);
waitForPDAMessageTypes(aqdata,CMD_PDA_HIGHLIGHT,CMD_PDA_HIGHLIGHTCHARS,15);
// if we get `PDA Menu Line 3 = Light will turn ` light is on and we need to press enter again.
// if we get `PDA Menu Line 2 = Dimmer Power ` we need to cycle over 25%/50%/75%/100%
// if we get `Menu Line 0 = Set Color` we can set color.
if (lighttype == LC_DIMMER2 || LC_DIMMER2 == LC_DIMMER) {
waitfor_queue2empty();
waitForPDAMessages(aqdata, 15); // We get a number of different things here depending on light state, so simply wait 15 messages
} else {
if (strncasecmp(pda_m_line(3),"Light will turn", 15) == 0) {
send_pda_cmd(KEY_PDA_SELECT);
waitForPDAMessageTypes(aqdata,CMD_PDA_HIGHLIGHT,CMD_PDA_HIGHLIGHTCHARS,5);
if (typ == LC_DIMMER2 || typ == LC_DIMMER) {
if (mode == 0) {
// We are simply turning it off, and that would have happened above, so do nothing but wait for the light turn off message
//waitForLightOffMessage(aqdata);
} else {
if (current_state == ON && mode > 0) { // Need to use the state BEFORE the last key press
// Button was on, and we are changing mode so turn it on as the previous send_pda_cmd(KEY_PDA_SELECT)
// would have tured it off, so turn it on
send_pda_cmd(KEY_PDA_SELECT);
waitfor_queue2empty();
waitForPDAMessages(aqdata, 5);
}
if (use_current_mode) {
char *current_mode = pda_m_hlight();
send_pda_cmd(KEY_PDA_SELECT);
mode = light_mode_index(typ, current_mode);
LOG(PDA_LOG,LOG_INFO, "PDA light Programming :- Current Mode = %d '%s'\n",mode,current_mode);
// No light cycling message at this point.
} else {
//int current = rsm_atoi(pda_m_line(4));
//while(current != mode_name)
int i = 0;
while(rsm_strmatch(pda_m_line(4), mode_name) != 0) {
send_pda_cmd(KEY_PDA_DOWN);
waitfor_queue2empty();
waitForPDAMessages(aqdata, 2);
if (++i > 6) {
LOG(PDA_LOG,LOG_INFO, "PDA light Programming :- Couldn't find %s\n",mode_name);
}
}
send_pda_cmd(KEY_PDA_SELECT);
waitfor_queue2empty();
waitForPDAMessages(aqdata, 5);
}
}
if (use_current_mode && mode_name != NULL) {
} else {
// Turn off look for "Light will turn" and simply wait.
// Turn on to default, get light color name from menu and press select.
// Turn to mode, loop over mode options.
if (mode == 0) { // off
//waitForPDAMessageTypes(aqdata,CMD_PDA_HIGHLIGHT,CMD_STATUS,5); // Wait for the actual text to show.
if (waitForLightOffMessage(aqdata)) {
light->button->led->state = OFF;
}
} else if (use_current_mode) { // use current
//waitForPDAMessageTypes(aqdata,CMD_PDA_HIGHLIGHT,CMD_PDA_HIGHLIGHTCHARS,15);
char *current_color = pda_m_hlight();
send_pda_cmd(KEY_PDA_SELECT);
// Reset the mode indet
mode = light_mode_index(typ, current_color);
LOG(PDA_LOG,LOG_INFO, "PDA light Programming :- Current Color = %d '%s'\n",mode,current_color);
waitForLightCycleMessage(aqdata);
} else { // set mode.
if (strncasecmp(pda_m_line(3),"Light will turn", 15) == 0) {
// If light is currently on, we will get this message, and need to clear it.
send_pda_cmd(KEY_PDA_SELECT);
waitForPDAMessageTypes(aqdata,CMD_PDA_HIGHLIGHT,CMD_PDA_HIGHLIGHTCHARS,15);
}
if (find_pda_menu_item(aqdata,(char *)mode_name,strlen(mode_name))) {
send_pda_cmd(KEY_PDA_SELECT);
waitForLightCycleMessage(aqdata);
} else {
LOG(PDA_LOG,LOG_ERR, "PDA Light Programming, could find mode '%s' for device '%s'\n",mode_name,button->label);
}
}
}
if (mode > 0) {updateLightProgram(aqdata, mode, light);}
} else {
LOG(PDA_LOG,LOG_ERR, "PDA Light Programming, device '%s' not found\n",button->label);
}
@ -1158,9 +1237,6 @@ bool waitForPDANextMessageType(struct aqualinkdata *aqdata, unsigned char mtype,
}
// Wait for Message, hit return on particular menu.
bool waitForPDAMessageTypesOrMenu(struct aqualinkdata *aqdata, unsigned char mtype1, unsigned char mtype2, int numMessageReceived, char *text, int line)
{
LOG(PDA_LOG,LOG_DEBUG, "waitForPDAMessageTypes 0x%02hhx or 0x%02hhx\n",mtype1,mtype2);
@ -1202,6 +1278,21 @@ bool waitForPDAMessageTypes(struct aqualinkdata *aqdata, unsigned char mtype1, u
return waitForPDAMessageTypesOrMenu(aqdata, mtype1, mtype2, numMessageReceived, NULL, 0);
}
bool waitForPDAMessages(struct aqualinkdata *aqdata, int numberMessages)
{
int received=0;
pthread_mutex_lock(&aqdata->active_thread.thread_mutex);
while( ++received <= numberMessages)
{
LOG(PDA_LOG,LOG_DEBUG, "waitForPDAMessages: %d of %d\n",received,numberMessages);
pthread_cond_wait(&aqdata->active_thread.thread_cond, &aqdata->active_thread.thread_mutex);
}
pthread_mutex_unlock(&aqdata->active_thread.thread_mutex);
return true;
}
/*
Use -1 for cur_val if you want this to find the current value and change it.
Use number for cur_val to increase / decrease from known start point

View File

@ -335,6 +335,8 @@ int rsm_strmatch(const char *haystack, const char *needle)
// Match two strings, used for button labels
// exact character length once white space removed is used for match
// ignore_chars will delete the last X chars from haystack.
// ignore_chars negative will also strip spaces from end before removing char.
// ignore_chars positive will remove chars from end.
// use case insensative for match.
// so 'spa' !- 'spa mode'
int rsm_strmatch_ignore(const char *haystack, const char *needle, int ignore_chars)
@ -349,12 +351,15 @@ int rsm_strmatch_ignore(const char *haystack, const char *needle, int ignore_cha
while(isspace(*sp1)) sp1++;
while(isspace(*sp2)) sp2++;
while(isspace(*ep2) && (ep2 >= sp2)) ep2--;
if (ignore_chars > 0)
if (ignore_chars < 0) {
while(isspace(*ep1) && (ep1 >= sp1)) ep1--;
ep1 = ep1 + ignore_chars;
}else if (ignore_chars > 0)
ep1 = ep1 - ignore_chars;
else
while(isspace(*ep1) && (ep1 >= sp1)) ep1--;
int l1 = ep1 - sp1 +1;
int l2 = ep2 - sp2 +1;

View File

@ -4,5 +4,5 @@
#define AQUALINKD_SHORT_NAME "AqualinkD"
// Use Magor . Minor . Patch
#define AQUALINKD_VERSION "3.0.0 (beta 3)"
#define AQUALINKD_VERSION "3.0.0"

View File

@ -1,32 +1,42 @@
/*
For manual setup, follow the below.
For easy setup, read the online documentation
Below is the basic premise of the file if you need to create something for a different home automation
1) put below in aqualinkd config.json
"external_script": "/HA_tilePlugin.js"
exampleCreateTile()
2) Configure any custom icons in aqualinkd config.json, example below 'light.back_floodlights' is HA ID:-
function exampleCreateTile() {
var tile = {};
tile["id"] = "my.unique.id";
tile["name"] = "Example Switch";
tile["display"] = "true";
tile["type"] = "switch"; // switch or value
tile["state"] = 'on';
tile["status"] = tile["state"]; // status and state are different for AqualinkD, but for purposes of a switch or sensor they are the same.
"light.back_floodlights": {
"display": "true",
"on_icon": "extra/light-on.png",
"off_icon": "extra/light-off-grey.png"
},
// Call AqualinkD function to create the tile and add to display.
createTile(tile);
// Make sure we use our own callback for button press. (only needed for a switch)
var subdiv = document.getElementById(tile["id"]);
subdiv.setAttribute('onclick', "exampleTilePressedCallback('" + tile["id"] + "')");
}
3) Add the HA ID's you want to the setTile function below :-
homeassistantAction("light.back_floodlights");
// use this function to update the state or value of a tile
function exampleUpdateTileStatus() {
// For Switch
setTileOn("my.unique.id", 'on'); // valid values are 'on' and 'off'
// For value
setTileValue("my.unique.id", "0.00");
}
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.
// This will be called when a tile is clicked in the UI.
function exampleTilePressedCallback() {
// Get the state of the tile
var state = (document.getElementById("my.unique.id").getAttribute('status') == 'off') ? 'on' : 'off';
// Action what needs to happen. ie send request to home automation hub.
// Change / re-set the tile in teh display.
setTileOn("my.unique.id", state);
}
*/
// Add your HA API token
@ -38,6 +48,7 @@ var HA_SERVER = 'http://192.168.1.255:8123';
setupTiles();
// Only use asunv to garantee order, also why homeassistantAction() function returns a promise.
function setupTiles() {
// If we have specific user agents setup, make sure one matches.
if (_config?.HA_tilePlugin && _config?.HA_tilePlugin?.userAgents) {
@ -197,6 +208,7 @@ function getURL(service, action) {
}
}
function homeassistantAction(id, action="status") {
var http = new XMLHttpRequest();
if (http) {
@ -229,6 +241,5 @@ function homeassistantAction(id, action="status") {
http.setRequestHeader("Authorization", "Bearer "+HA_TOKEN);
http.send('{"entity_id": "'+id+'"}');
}
}

View File

@ -518,13 +518,12 @@
</style>
<script type='text/javascript'>
'use strict';
//'use strict';
var _panel_size = 6;
var _panel_set = 0;
var _currentVersion = 0;
var _latestVersionAvailable = 0;
//var _latestDevVersionAvailable = 0;
var _rssd_logmask = 0;
const RSSD_MASK_ID = 512; // Must match RSSD_LOG in utils.c
@ -537,6 +536,7 @@
var _config = {};
const VERSION_URL = "https://api.github.com/repos/aqualinkd/AqualinkD/releases"
const WEB_CONFIG_DEFAULT = "https://raw.githubusercontent.com/aqualinkd/AqualinkD/refs/heads/master/web/config.json"
let _aqualinkVersions = {};
const _urlParams = new URLSearchParams(window.location.search);
@ -1570,7 +1570,7 @@
if (_config[obj].value !== undefined) {
if (obj.toString().startsWith("light_program_") ) {
//console.log(obj.toString());
num = parseInt(lastKey.match(/[0-9]+/));
let num = parseInt(lastKey.match(/[0-9]+/));
num++;
//console.log("check for light_program_"+String(num+1).padStart(2, '0'));
//console.log("check for light_program_"+num);
@ -1695,14 +1695,14 @@
}*/
//alert(data.type+" r="+data.panel_revision+", t="+data.panel_type);
if (data['version']) {
document.getElementById("panelversion").innerHTML = data['version'];
document.getElementById("panelversion").textContent = data['version'];
}
else if (data['panel_revision']) {
document.getElementById("panelversion").innerHTML = data['panel_revision'];
document.getElementById("panelversion").textContent = data['panel_revision'];
}
if (data['panel_type']) {
document.getElementById("paneltype").innerHTML = data['panel_type'];
document.getElementById("paneltype").textContent = data['panel_type'];
}
if (data['status']) {
update_status_message(data['status']);
@ -1855,28 +1855,36 @@
async function updateWebConfigFromMaster() {
let masterJSON;
// Get new master
const jsonInput = document.getElementById('webconfig-json-input');
const resultArea = document.getElementById('validation-result');
// Clear previous results
resultArea.className = '';
resultArea.textContent = '';
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' });
const response = await fetch(WEB_CONFIG_DEFAULT, { 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)
const configText = await response.text();
//masterJSON = JSON.parse(configText)
// Populate the textarea
//jsonInput.value = configText;
//jsonInput.placeholder = "Enter or edit your JSON here...";
//webconfig_validateAndDisplay(); // Optional: Validate immediately upon loading
jsonInput.value = configText;
jsonInput.placeholder = "Enter or edit your JSON here...";
webconfig_validateAndDisplay();
} catch (error) {
alert("Error loading master cfg "+error);
}
// Now get local
/* This will merge the 2 files
try{
const jsonInput = document.getElementById('webconfig-json-input');
const jsonString = jsonInput.value;
var webcfgJSON = JSON.parse(jsonString);
@ -1889,7 +1897,7 @@
} catch (error) {
alert("Error "+error);
}
*/
}
function mergeMissingKeysRecursive(target, source) {
@ -2671,10 +2679,10 @@
<table border="0" cellpadding="0px" width="100%">
<tr>
<td align="right" style="width:50%;padding-right: 10px;">
<input id="saveconfig" type="button" onclick="saveconfig(this);" value="Save Config"></input>
<input id="saveconfig" type="button" onclick="saveconfig(this);" value="Save"></input>
</td>
<td align="left" style="width:50%;padding-left: 10px;">
<input id="saveconfig" type="button" onclick="closeconfig(this);" value="Close (without saving)"></input>
<input id="saveconfig" type="button" onclick="closeconfig(this);" value="Discard"></input>
</td>
</tr>
</table>
@ -2708,15 +2716,15 @@
</tr>
<tr>
<td align="right" style="width:33%;padding-right: 10px;">
<input type="button" onclick="webconfig_validateAndDisplay()" value="Validate"></input>
<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>
<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-->
<input type="button" onclick="closeWebConfig()" value="Discard"></input>
<!-- For the future, to reset config to github contents, or to add new keys to json config pulling from github-->
<!-- <input type="button" onclick="updateWebConfigFromMaster()" value="get new"></input> -->
</td>
</tr>
<tr>

View File

@ -273,7 +273,7 @@
"show_vsp_gpm": "true",
"disable_off_icon_background": "true"
},
"EXAMPLE_colors_dark": {
"colors-dark-example": {
"body_background": "#121212",
"body_text": "#FFFFFF",
"options_pane_background": "#1E1E1E",

View File

@ -1,45 +0,0 @@
/*
put below in aqualinkd config.json
"external_script": "/exampleTilePlugin.js"
*/
exampleCreateTile()
function exampleCreateTile() {
var tile = {};
tile["id"] = "my.unique.id";
tile["name"] = "Example Switch";
tile["display"] = "true";
tile["type"] = "switch"; // switch or value
tile["state"] = 'on';
tile["status"] = tile["state"];
createTile(tile);
// Make sure we use out own callback for button press.
var subdiv = document.getElementById(tile["id"]);
subdiv.setAttribute('onclick', "exampleTilePressedCallback('" + tile["id"] + "')");
}
// use this function to update the state or value of a tile
function exampleUpdateTileStatus() {
// For Switch
setTileOn("my.unique.id", 'on');
// For value
setTileValue("my.unique.id", "0.00");
}
// This will be called when a tile is clicked in the UI.
function exampleTilePressedCallback() {
// Get the state of the tile
var state = (document.getElementById(id).getAttribute('status') == 'off') ? 'on' : 'off';
/*
Action what needs to happen. ie send request to home automation hub.
*/
setTileOn(id, state);
}

View File

@ -146,12 +146,29 @@
border-radius: 20px;
justify-content: center;
align-items: center;
width: 650px;
/*width: 650px;*/
min-width: 660px;
max-width: 680px;
padding: 10px;
/*overflow-y: scroll; didn't work*/
/*height: 100%;*/
/*flex-basis: content;*/
/*flex-direction: column;*/
/*
max-height: 300px;
overflow: auto;
border: 1px solid #ae2424;
*/
}
.scheduler_table {
overflow-x: hidden;
overflow-y: auto;
/*max-height: 100px;*/
width: 665px;
}
.simulator_pane {
@ -186,6 +203,7 @@
/*
height: 100%;
*/
}
/*
@ -564,9 +582,31 @@
box-shadow: 0 0.0625em 0 0.0625em rgba(0,0,0,0.075);*/
}
.sc_table,
.sc_table th,
.sc_table td {
/*border: 1px solid #ccc;*/
border: 0px;
padding: 0px;
}
.sc_tablehead {
vertical-align: top;
text-align: center;
vertical-align: top;
text-align: center;
}
.sc_tablerowhidden {
visibility: collapse;
line-height: 0;
}
.sc_tablerowhidden tr {
visibility: hidden;
height: 0;
max-height: 0;
padding: 0;
border: none;
}
.cs_commandcell {
@ -670,6 +710,31 @@
background-repeat: no-repeat;
}
.pane_close_button {
font-size: 10px !important;
font-weight: bold !important;
/*color: var(--body_text);*/
/*background-color: #7f8385 !important;*/
border: none !important;
color: rgb(0, 0, 0) !important;
padding: 2px 2px !important;
text-decoration: none !important;
margin: 2px 2px 2px 2px !important;
min-width: 18px !important;
border-radius: 2px !important;
height: 18px !important;
}
.pane_close_button_contaner {
position: absolute;
top: 0;
right: 0;
padding-top: 5px;
padding-right: 5px;
}
/*
.timedate {
font-size: 10px;
@ -681,6 +746,7 @@
var _config = {};
var _aq_display_tile_size = 'small';
var _lightProgramDropdown = false;
var _pressEvent;
@ -1017,6 +1083,7 @@
// 375x724 = 271500
if ((w * h) > 370000) { // 370000 is kind-a just guess
_aq_display_tile_size = "large";
document.documentElement.style.setProperty('--tile-width', '125px');
document.documentElement.style.setProperty('--tile_icon-height', '45px');
document.documentElement.style.setProperty('--tile_name-height', '42px');
@ -1025,7 +1092,10 @@
document.documentElement.style.setProperty('--tile_status-height', '15px');
document.documentElement.style.setProperty('--tile_grid-gap', '20px');
document.documentElement.style.setProperty('--tile_name-lineheight', '1.4');
} else {
_aq_display_tile_size = "small";
}
if (w > h)
_landscape = true;
else
@ -1317,6 +1387,54 @@
}
// Reduce the number of decimal places if a float is longer than a
// a set number of digits. Leave any int / string alone, and ONLY reduce the decimal part of a float.
function reduct(value) {
const str = String(value);
const parts = str.split('.');
const integerDigits = parts[0].length;
let fractionalDigits = 0;
// If the array has more than one part, a decimal point was found.
if (parts.length > 1) {
fractionalDigits = parts[1].length;
} else {
// No decimal places, return.
return value;
}
// Large tiles vs small tiles.
let total_digits = 3;
if ( _aq_display_tile_size == "large" ) {
total_digits = 5;
}
if (integerDigits + fractionalDigits <= total_digits) {
// Total length is less so no change
return value;
} else if (integerDigits >= total_digits) {
// Simply return the intiger and not decimal places.
return parts[0].toString();
} else {
let numFractionalDigits = total_digits - integerDigits;
return parts[0].toString() + "." + parts[1].substring(0, numFractionalDigits);
}
return value;
}
/*
Set any UOM that's not deg to smaller font
*/
function formatUOM(uom) {
if (uom == "&deg;" || uom == "°") {
return uom;
}
return '<span class="uom_text">'+uom+'</span>';
}
function setTileValue(id, value) {
var ext = '';
@ -1331,13 +1449,17 @@
let uom;
if ((uom = document.getElementById(id).getAttribute('UOM')) != null) {
ext = uom;
//ext = '<span class="uom_text">'+uom+'</span>';
} else if ((type = document.getElementById(id).getAttribute('type')) != null) {
if (type == 'temperature' || type == 'setpoint_thermo' || type == 'setpoint_freeze' || type == 'setpoint_chiller')
if (type == 'temperature' || type == 'setpoint_thermo' || type == 'setpoint_freeze' || type == 'setpoint_chiller') {
ext = '&deg;';
else if (type == 'setpoint_swg')
} else if (type == 'setpoint_swg') {
ext = '%';
else if (type == 'value' && id == 'CHEM/ORP')
ext = '<span class="uom_text">mV</span>';
//ext = '<span class="uom_text">%</span>';
} else if (type == 'value' && id == 'CHEM/ORP') {
//ext = '<span class="uom_text">mV</span>';
ext = "mV";
}
//else if (type == 'value' && id == 'CHEM/pH')
// ext = '<span class="uom_text">pH</span>';
}
@ -1366,8 +1488,12 @@
}
//document.getElementById(id + '_tile_icon_value').textContent = value;
var tile;
if ((tile = document.getElementById(id + '_tile_icon_value')) != null)
tile.innerHTML = value + ext;
if ((tile = document.getElementById(id + '_tile_icon_value')) != null){
// Stupid way but testing fixing false positives from codeQL
//tile.innerHTML = formatUOM(ext);
//tile.textContent = reduct(value) + tile.textContent;
tile.innerHTML = reduct(value) + formatUOM(ext);
}
}
function setTileThresholds(id, value) {
@ -1420,9 +1546,11 @@
try {
var tile = document.getElementById(id);
if (tile.getAttribute('status') == 'on' || tile.getAttribute('status') == 'enabled') {
document.getElementById(id + '_status').innerHTML = text;
//document.getElementById(id + '_status').innerHTML = text;
setElementHTML(id + '_status', text);
} else if (tile.getAttribute('status') == 'rangeoptimal' || tile.getAttribute('status') == 'rangewarning' || tile.getAttribute('status') == 'rangecritical') {
document.getElementById(id + '_status').innerHTML = text;
//document.getElementById(id + '_status').innerHTML = text;
setElementHTML(id + '_status', text);
} else {
//document.getElementById(id + '_status').innerHTML = text;
//console.log("Tile "+id+" status is '"+document.getElementById(id).getAttribute('status')+"' not setting text to '"+text+"'");
@ -1432,7 +1560,8 @@
function setTileOnTextLine2(id, text) {
try {
if (document.getElementById(id).getAttribute('status') == 'on') {
document.getElementById(id + '_status_line2').innerHTML = text;
//document.getElementById(id + '_status_line2').innerHTML = text;
setElementHTML(id + '_status_line2', text);
return true;
} else {
//console.log("Tile "+id+" status is '"+document.getElementById(id).getAttribute('status')+"' not setting text to '"+text+"'");
@ -1625,17 +1754,27 @@
}
type = tile.getAttribute('type');
/*
try {
if (type != "value" && type != "temperature") {
//console.log("Set Text "+id+" "+" - "+type+" "+text);
document.getElementById(id + '_status').innerHTML = text;
// Clear line 2 status if we have it.
document.getElementById(id + '_status_line2').innerHTML = "";
} else if ( status == "off" (type == "value" || type == "temperature") ) {
} else if ( status == "off" && (type == "value" || type == "temperature") ) {
document.getElementById(id + '_status').innerHTML = "";
document.getElementById(id + '_status_line2').innerHTML = "";
}
} catch (e) {}
} catch (e) {console.log(e)}
*/
if (type != "value" && type != "temperature") {
setElementHTML(id + '_status', text);
setElementHTML(id + '_status_line2', "");
} else if ( status == "off" && (type == "value" || type == "temperature") ) {
setElementHTML(id + '_status', "");
setElementHTML(id + '_status_line2', "");
}
if (status != null) {
if (status == 'enabled' || status == 'flash') {
@ -2194,6 +2333,8 @@
} else if (type == 'scheduler') {
// NSF BUILD SCHEDULER DEFAULTS HERE
cs_clearSchedules();
setElementHTML("crontext", "");
setElementHTML("cronhelp", "");
//cs_loadJSON("/api/schedules", cs_schedules,'jsonp');
get_schedules();
} else if (type == 'simulator') {
@ -2372,6 +2513,18 @@
displayMessage(message, timeout);
}
function setElementHTML(element, html) {
const elmnt = document.getElementById(element);
if (elmnt) {
elmnt.innerHTML = html;
//console.log("Call in "+element+ " "+html);
} else {
//console.log("fail in "+element+ " "+html);
}
}
function displayMessage(message, timeoutSeconds) {
// Check to see if we can display message
if (_displayMsgTimeout > 0) {
@ -2397,7 +2550,8 @@
document.getElementById("header").classList.remove("error");
}
*/
document.getElementById("message").innerHTML = message;
//document.getElementById("message").innerHTML = message;
setElementHTML("message", message);
}
/*********************************************************************
*
@ -2408,7 +2562,8 @@
// NSF change this import to dynamic config in future.
//import("./cronstrue.min.js");
var CS_ROW_INDEX = 2;
//var CS_ROW_INDEX = 2;
var CS_ROW_INDEX = 0;
var V_OFF = 0;
var V_ON = 1;
var V_SETPOINT = 3;
@ -2418,16 +2573,32 @@
function showScheduler(caller){
var height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
//console.log("height = "+height+" - "+caller.id);
if (typeof _enable_schedules === "undefined" || _enable_schedules != false)
{
if ( parseInt(getComputedStyle(document.querySelector('.scheduler_pane')).width) > Math.max(document.documentElement.clientWidth, window.innerWidth) ) {
if ( parseInt(getComputedStyle(document.querySelector('.scheduler_pane')).width) > Math.max(document.documentElement.clientWidth, window.innerWidth) ||
height < 300 ) {
//alert("Sorry, Scheduler won't fit on screen\nPlease use a different device");
displayMessage("Scheduler won't fit on screen", 3);
//console.log("Height="+height+", computedwidth="+getComputedStyle(document.querySelector('.scheduler_pane')).width+", clientWidth="+document.documentElement.clientWidth+", innerWidth="+window.innerWidth);
return;
}
import("./cronstrue.min.js");
caller.setAttribute('type', 'scheduler');
showTileOptions(true, caller.id);
//h=400;
document.getElementById("scheduler_options_pane").style.maxHeight = (height-40)+'px';
document.getElementById("scheduler_scrollabletable").style.maxHeight = (height-280)+'px';
const cs_table = document.getElementById('cronschedules');
if (cs_table) {
const tableObserver = new ResizeObserver(cs_synchronizeTableWidths);
tableObserver.observe(cs_table);
}
}
}
@ -2492,6 +2663,7 @@
function cs_crontext(caller){
for (let cell of caller.parentNode.parentNode.cells) {
switch (cell.firstChild.id) {
case "min":
min = cell.firstChild.value;
@ -2525,12 +2697,13 @@
//command=cs_getExtraCommand(device);
extra = " set "+device2english(device, true)+" "+device2english(device, false)+" to "+value
} else {
extra = " turn "+device2english(device, true)+" "+((value ==1) ? 'on' : 'off')
extra = " turn "+device2english(device, true)+" "+((command == 1) ? 'on' : 'off')
}
var cron = min + " " + hour + " " + daym + " " + month + " " + dayw
try {
document.getElementById("crontext").innerHTML = "<span class='cs_crontext'>&nbsp;" + cronstrue.toString(cron) + "<font size='-1'>" + extra + "</font>&nbsp;</span>";
//document.getElementById("crontext").innerHTML = "<span class='cs_crontext'>&nbsp;" + cronstrue.toString(cron) + "<font size='-1'>" + extra + "</font>&nbsp;</span>";
setElementHTML("crontext", "<span class='cs_crontext'>&nbsp;" + cronstrue.toString(cron) + "<font size='-1'>" + extra + "</font>&nbsp;</span>");
} catch (error) {}
var html = ""
@ -2565,17 +2738,19 @@
"</table>";
}
document.getElementById("cronhelp").innerHTML = html;
//document.getElementById("cronhelp").innerHTML = html;
setElementHTML("cronhelp", html);
}
function cs_clearSchedules() {
var table = document.getElementById("cronschedules");
while (table.rows.length > 5) {
while (table.rows.length > 1) {
table.deleteRow(CS_ROW_INDEX+1);
}
}
function cs_addschedule(enabled, min, hour, daym, month, dayw, device, subdev, value) {
function cs_addschedule(enabled, min, hour, daym, month, dayw, device, subdev, value) {
var table = document.getElementById("cronschedules");
//index = table.rows.length-ROW_INDEX;
@ -2621,7 +2796,9 @@
var element = row.querySelector('#command')
var event = new Event('change');
element.dispatchEvent(event);
}
// cs_synchronizeTableWidths();
}
function cs_isSelected(option, val1, val2) {
var rtn = "";
@ -2941,8 +3118,10 @@
var tile = document.getElementById('SWG');
if (tile != null) {
var sp_value = tile.getAttribute('setpoint');
if (sp_value == 101)
document.getElementById('SWG' + '_status').innerHTML = 'Boost '+data.swg_boost_msg;
if (sp_value == 101) {
//document.getElementById('SWG' + '_status').innerHTML = 'Boost '+data.swg_boost_msg;
setElementHTML('SWG' + '_status', 'Boost '+data.swg_boost_msg);
}
}
}
var i = 1;
@ -3118,7 +3297,8 @@
}
socket_di.onclose = function() {
// something went wrong
document.getElementById("message").innerHTML = ' Connection error! '
//document.getElementById("message").innerHTML = ' Connection error! '
setElementHTML("message", ' Connection error! ');
document.getElementById("header").classList.add("error");
// Try to reconnect every 5 seconds.
setTimeout(function() {
@ -3283,6 +3463,68 @@
document.head.appendChild(script);
}
function cs_synchronizeTableWidths() {
//console.log("SET table");
const sourceTable = document.getElementById('cronschedules');
const destinationTable = document.getElementById('cronschedules-main');
if (!sourceTable || !destinationTable) {
console.error("Source or destination table not found.");
return;
}
// Optional: Force tables to obey set widths for consistency
//sourceTable.style.tableLayout = 'fixed';
//destinationTable.style.tableLayout = 'fixed';
const sourceCells = sourceTable.rows[0]?.cells;
const destinationCells = destinationTable.rows[0]?.cells;
if (!sourceCells || !destinationCells) {
console.error("Could not find the first row/cells in one of the tables.");
return;
}
//if (sourceCells.length !== destinationCells.length) {
if (sourceCells.length > destinationCells.length) {
console.error(`Width sync failed: Source has ${sourceCells.length} cells in the first row, Destination has ${destinationCells.length}.`);
return;
}
//let total=0;
// Iterate over the columns and apply the width
for (let i = 0; i < sourceCells.length; i++) {
// Get the computed width of the source cell
//const width = sourceCells[i].offsetWidth + 'px';
//total += sourceCells[i].getBoundingClientRect().width;
const width = sourceCells[i].getBoundingClientRect().width + 'px';
// Apply the exact width to the destination cell's style
// Setting min/max width helps ensure the browser obeys the instruction
destinationCells[i].style.minWidth = width;
destinationCells[i].style.maxWidth = width;
destinationCells[i].style.width = width;
}
/*
if (sourceCells.length < destinationCells.length) {
// Set the remaining cell to take up space.
console.log("total = "+total);
console.log("overall = "+destinationTable.getBoundingClientRect().width );
let left = 680 - total + 'px';
console.log("left = "+left);
destinationCells[sourceCells.length].style.minWidth = left;
destinationCells[sourceCells.length].style.maxWidth = left;
destinationCells[sourceCells.length].style.width = left;
}
*/
}
/*
function updateTimerOptions(source) {
if (source.type == 'range') {
@ -3613,11 +3855,22 @@
</div>
-->
<div id='scheduler_options' class='options hide'>
<div id='scheduler_options_pane' class='scheduler_pane' onclick='event.stopPropagation();'>
<table id="cronschedules" border="0">
<th colspan='10'><span id="scheduler_options_title"></span></th>
<div id='scheduler_options_pane' class='scheduler_pane' onclick='event.stopPropagation();' style="display: flex; position: relative;">
<div class="pane_close_button_contaner">
<input class="pane_close_button options_button" type="button" id="scheduler" onclick="showTileOptions(false, this.id);;" value="x"></input>
</div>
<table id="cronschedules-main" class="sc_table" >
<tr class="sc_tablehead sc_tablerowhidden">
<!-- Below is hidden, but used for computed table column widths -->
<td>Enable</td><td>Minute</td><td>Hour</td><td>Day</td><td>Month</td><td>Day</td><td>Device</td><td>Cmd</td><td>Value</td><td>Del</td><td>&nbsp;&nbsp;&nbsp;</td>
</tr>
<tr>
<td colspan="10" align="center" id="crontext">&nbsp</td>
<th colspan='11'><span id="scheduler_options_title"></span></th>
</tr>
<tr>
<td colspan="11" align="center" id="crontext" style="height: 22px">&nbsp;</td>
</tr>
<tr class="sc_tablehead">
<td>Enable</td>
@ -3630,17 +3883,30 @@
<td>Cmd</td>
<td>Value</td>
<td>Del</td>
<td></td> <!-- scroll bar lands in this space -->
</tr>
<tr>
<td colspan="11">
<div class="scheduler_table" id="scheduler_scrollabletable">
<table id="cronschedules" class="sc_table">
<tr class="sc_tablehead sc_tablerowhidden">
<!-- Below is hidden, but used for computed table column widths -->
<td>Enable</td><td>Minute</td><td>Hour</td><td>Day</td><td>Month</td><td>Day</td><td>Device</td><td>Cmd</td><td>Value</td><td>Del</td>
</tr>
</table>
</div>
</td>
<!-- HTML will be added here -->
</tr>
<tr class="sc_tablehead">
<td colspan="10">
<td colspan="11">
<input type="button" class="options_button" id="Add" value="Add" onclick="cs_addschedule(1,0,0,'*','*','*','','','');">
&nbsp;&nbsp;&nbsp;&nbsp;
<input type="button" class="options_button" id="scheduler_options_close" value="Save" onclick="cs_createJSON();">
</td>
</tr>
<tr>
<td colspan="10" id="cronhelp"></td>
<td colspan="11" id="cronhelp"></td>
</tr>
<table>
</div>