mirror of https://github.com/sfeakes/AqualinkD.git
Release 3.0.0
parent
daa6b350be
commit
a5db4e0100
|
|
@ -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.
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
@ -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+'"}');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
330
web/index.html
330
web/index.html
|
|
@ -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 == "°" || 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 = '°';
|
||||
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'> " + cronstrue.toString(cron) + "<font size='-1'>" + extra + "</font> </span>";
|
||||
//document.getElementById("crontext").innerHTML = "<span class='cs_crontext'> " + cronstrue.toString(cron) + "<font size='-1'>" + extra + "</font> </span>";
|
||||
setElementHTML("crontext", "<span class='cs_crontext'> " + cronstrue.toString(cron) + "<font size='-1'>" + extra + "</font> </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> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="10" align="center" id="crontext"> </td>
|
||||
<th colspan='11'><span id="scheduler_options_title"></span></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="11" align="center" id="crontext" style="height: 22px"> </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,'*','*','*','','','');">
|
||||
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue