Release 3.1.0

master v3.1.0
sfeakes 2026-04-09 13:37:21 -05:00
parent 57da0a7803
commit 7416dc6ced
22 changed files with 405 additions and 64 deletions

View File

@ -160,8 +160,11 @@ Need to look at sub panel (combined panels)
when serial port is wrong, can't edit config.
-->
## Release 3.0.5 (dev)
## Release 3.1.0
* added device power to watts in MQTT discovery for power monitoring
* Updated timers to be finer grained, (seconds vs minutes). - ie support for dosing pumps.
* Added config options for button (circuit) runtimes to set default runtimes for a device.
* Added support for onetouch macro's to OneTouch protocol.
## Release 3.0.4 (March 2026)
* Fixed AqualinkD not starting when IP not assigned and MQTT enabled.

View File

@ -4,6 +4,14 @@ All notable changes to AqualinkD are documented here. Releases are listed in rev
---
## Release 3.1.0 (April 2026)
* added device power to watts in MQTT discovery for power monitoring
* Updated timers to be finer grained, (seconds vs minutes). - ie support for dosing pumps.
* Added config options for button (circuit) runtimes to set default runtimes for a device.
* Added support for onetouch macro's to OneTouch protocol.
---
## Release 3.0.4 (March 2026)
* Fixed AqualinkD not starting when IP not assigned and MQTT enabled.
* Fixed aqmanager config editor issue when adding pumpType.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1337,6 +1337,8 @@ const char* getActionName(action_type type)
break;
case TIMER:
return "Timer";
case TIMER_SEC:
return "Timer (Seconds)";
break;
case LIGHT_MODE:
return "Light Mode";
@ -1451,7 +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 ( button->runtime_sec > 0) {
LOG(PANL_LOG, LOG_INFO, "'%s' is has runtime %u seconds, setting timer\n", button->name, button->runtime_sec);
start_timer(aqdata, deviceIndex, 0, button->runtime_sec);
}
#ifdef AQ_PDA
if (isPDA_PANEL) {
if (button->special_mask & PROGRAM_LIGHT/* && isPDA_IAQT*/) {
//if ( isPDA_IAQT && isIAQL_ACTIVE) {
@ -1525,9 +1533,12 @@ bool setDeviceState(struct aqualinkdata *aqdata, int deviceIndex, bool isON, req
} else {
aq_programmer(AQ_SET_IAQTOUCH_DEVICE_ON_OFF, button, (isON == false ? OFF : ON), deviceIndex, aqdata);
set_pre_state = false;
}
}
} else if (isONET_ENABLED){
aq_programmer(AQ_SET_ONETOUCH_MACRO, button, (isON == false ? OFF : ON), deviceIndex, aqdata);
set_pre_state = false;
} else {
LOG(PANL_LOG, LOG_ERR, "Can only use Aqualink Touch protocol for Virtual Buttons");
LOG(PANL_LOG, LOG_ERR, "Can only use Aqualink Touch for Virtual Buttons");
}
} else {
// Everything else, simply send the button code.
@ -1831,7 +1842,7 @@ bool panel_device_request(struct aqualinkdata *aqdata, action_type type, int dev
deviceIndex,
value,
getRequestName(source));
} else if (type == ON_OFF || type == TIMER || type == LIGHT_BRIGHTNESS || type == LIGHT_MODE){
} else if (type == ON_OFF || type == TIMER ||type == TIMER_SEC || type == LIGHT_BRIGHTNESS || type == LIGHT_MODE){
LOG(PANL_LOG,LOG_INFO, "Device request type '%s' for deviceindex %d '%s' of value %d from '%s'\n",
getActionName(type),
deviceIndex,
@ -1856,10 +1867,12 @@ bool panel_device_request(struct aqualinkdata *aqdata, action_type type, int dev
}
break;
case TIMER:
//setDeviceState(&aqdata->aqbuttons[deviceIndex], true);
setDeviceState(aqdata, deviceIndex, true, source);
//start_timer(aqdata, &aqdata->aqbuttons[deviceIndex], deviceIndex, value);
start_timer(aqdata, deviceIndex, value);
start_timer(aqdata, deviceIndex, value, 0);
break;
case TIMER_SEC:
setDeviceState(aqdata, deviceIndex, true, source);
start_timer(aqdata, deviceIndex, 0, value);
break;
case LIGHT_BRIGHTNESS:
// Allow value=0 here (unlike LIGHT_MODE) since we could get multiple requests from a slider. (aka HomeKit)

View File

@ -100,6 +100,7 @@ const func_ptr _prog_functions[AQP_RSSADAPTER_MAX] = {
[AQ_SET_IAQTOUCH_PUMP_VS_PROGRAM] = set_aqualink_iaqtouch_pump_vs_program,
[AQ_SET_IAQTOUCH_LIGHTCOLOR_MODE] = set_aqualink_iaqtouch_light_colormode,
[AQ_SET_IAQTOUCH_DEVICE_ON_OFF] = set_aqualink_iaqtouch_device_on_off,
[AQ_SET_ONETOUCH_MACRO] = set_aqualink_onetouch_macro,
//[AQ_SET_IAQTOUCH_ONETOUCH_ON_OFF] = set_aqualink_iaqtouch_onetouch_on_off, // Not finished and not needed
[AQ_PDA_INIT] = set_aqualink_PDA_init,
[AQ_PDA_WAKE_INIT] = set_aqualink_PDA_wakeinit,
@ -734,7 +735,7 @@ void _aq_programmer_(program_type r_type, char *args, aqkey *button, int value,
return; // No need to create this as thread.
break;
default:
// Should check that _prog_functions[type] is valid.
// NSF REALLY Should check that _prog_functions[type] is valid.
if( pthread_create( &programmingthread->thread_id , NULL , _prog_functions[type], (void*)programmingthread) < 0) {
LOG(PROG_LOG, LOG_ERR, "could not create thread\n");
return;

View File

@ -188,7 +188,7 @@ typedef enum pump_type {
PT_UNKNOWN = -1,
EPUMP, // = ePump AC & Jandy ePUMP
VSPUMP, // = Intelliflo VS
VFPUMP // = Intelliflo VF (GPM)
VFPUMP, // = Intelliflo VF (GPM)
} pump_type;

View File

@ -19,6 +19,7 @@ struct timerthread {
int deviceIndex;
struct aqualinkdata *aqdata;
int duration_min;
u_int32_t duration_sec;
struct timespec timeout;
time_t started_at;
struct timerthread *next;
@ -57,7 +58,25 @@ int get_timer_left(aqkey *button)
return 0;
}
void clear_timer(struct aqualinkdata *aqdata, /*aqkey *button,*/ int deviceIndex)
uint32_t get_timer_left_sec(aqkey *button)
{
struct timerthread *t_ptr = find_timerthread(button);
if (t_ptr != NULL) {
time_t now = time(0);
long total_duration_sec = (t_ptr->duration_min * 60) + t_ptr->duration_sec;
long elapsed_sec = (long)difftime(now, t_ptr->started_at);
if (elapsed_sec >= total_duration_sec) {
return 0;
}
return (uint32_t)(total_duration_sec - elapsed_sec);
}
return 0;
}
void clear_timer(struct aqualinkdata *aqdata, int deviceIndex)
{
//struct timerthread *t_ptr = find_timerthread(button);
struct timerthread *t_ptr = find_timerthread(&aqdata->aqbuttons[deviceIndex]);
@ -65,18 +84,21 @@ void clear_timer(struct aqualinkdata *aqdata, /*aqkey *button,*/ int deviceIndex
if (t_ptr != NULL) {
LOG(TIMR_LOG, LOG_INFO, "Clearing timer for '%s'\n",t_ptr->button->name);
t_ptr->duration_min = 0;
t_ptr->duration_sec = 0;
pthread_cond_broadcast(&t_ptr->thread_cond);
}
}
void start_timer(struct aqualinkdata *aqdata, /*aqkey *button,*/ int deviceIndex, int duration)
void start_timer(struct aqualinkdata *aqdata, int deviceIndex, int duration_min, u_int32_t duration_sec)
{
aqkey *button = &aqdata->aqbuttons[deviceIndex];
struct timerthread *t_ptr = find_timerthread(button);
if (t_ptr != NULL) {
LOG(TIMR_LOG, LOG_INFO, "Timer already active for '%s', resetting\n",t_ptr->button->name);
t_ptr->duration_min = duration;
t_ptr->duration_min = duration_min;
t_ptr->duration_sec = duration_sec;
pthread_cond_broadcast(&t_ptr->thread_cond);
return;
}
@ -86,7 +108,8 @@ void start_timer(struct aqualinkdata *aqdata, /*aqkey *button,*/ int deviceIndex
tmthread->button = button;
tmthread->deviceIndex = deviceIndex;
tmthread->thread_id = 0;
tmthread->duration_min = duration;
tmthread->duration_min = duration_min;
tmthread->duration_sec = duration_sec;
tmthread->next = NULL;
tmthread->started_at = time(0); // This will get reset once we actually start. But need it here incase someone calls get_timer_left() before we start
@ -158,8 +181,9 @@ void *timer_worker( void *ptr )
}
}
/*
// Wait the entire duration.
pthread_mutex_lock(&tmthread->thread_mutex);
do {
if (retval != 0) {
LOG(TIMR_LOG, LOG_ERR, "pthread_cond_timedwait failed for '%s', error %d %s\n",tmthread->button->name,retval,strerror(retval));
@ -169,15 +193,72 @@ void *timer_worker( void *ptr )
break;
}
clock_gettime(CLOCK_REALTIME, &tmthread->timeout);
tmthread->timeout.tv_sec += (tmthread->duration_min * 60);
tmthread->timeout.tv_sec += (tmthread->duration_min * 60) + tmthread->duration_sec;
tmthread->started_at = time(0);
LOG(TIMR_LOG, LOG_INFO, "Will turn off '%s' in %d minutes\n",tmthread->button->name, tmthread->duration_min);
LOG(TIMR_LOG, LOG_INFO, "Will turn off '%s' in %d:%d\n",tmthread->button->name, tmthread->duration_min, tmthread->duration_sec);
} while ((retval = pthread_cond_timedwait(&tmthread->thread_cond, &tmthread->thread_mutex, &tmthread->timeout)) != ETIMEDOUT);
pthread_mutex_unlock(&tmthread->thread_mutex);
*/
// Waake every minute (or second) and set the dirty flag.
pthread_mutex_lock(&tmthread->thread_mutex);
// 1. Calculate the absolute end time once
struct timespec end_time;
clock_gettime(CLOCK_REALTIME, &end_time);
end_time.tv_sec += (tmthread->duration_min * 60) + tmthread->duration_sec;
tmthread->started_at = time(0);
LOG(TIMR_LOG, LOG_INFO, "Timer started for '%s': %d:%02d total duration\n", tmthread->button->name, tmthread->duration_min, tmthread->duration_sec);
while (1) {
struct timespec now;
clock_gettime(CLOCK_REALTIME, &now);
// 2. Calculate remaining time
long remaining_sec = end_time.tv_sec - now.tv_sec;
if (remaining_sec <= 0) {
break; // Timer finished
}
SET_DIRTY(tmthread->aqdata->is_dirty);
// 3. Print time left
if (remaining_sec >= 60) {
LOG(TIMR_LOG, LOG_INFO, "Time left for '%s': %ldm %lds\n", tmthread->button->name, remaining_sec / 60, remaining_sec % 60);
} else {
LOG(TIMR_LOG, LOG_INFO, "Time left for '%s': %ld seconds\n", tmthread->button->name, remaining_sec);
}
// Set next wake time
tmthread->timeout = now;
tmthread->timeout.tv_sec += (remaining_sec > 60) ? 60 : 1;
// Wait for timeout or signal
retval = pthread_cond_timedwait(&tmthread->thread_cond, &tmthread->thread_mutex, &tmthread->timeout);
if (retval == 0) {
// We were signaled! Someone changed tmthread->duration_min or duration_sec
if (tmthread->duration_min <= 0 && tmthread->duration_sec <= 0) {
break; // Cancelled
}
LOG(TIMR_LOG, LOG_INFO, "Timer update received for '%s'. Recalculating...\n", tmthread->button->name);
// Update the end_time based on the NEW duration
clock_gettime(CLOCK_REALTIME, &end_time);
end_time.tv_sec += (tmthread->duration_min * 60) + tmthread->duration_sec;
// Also update started_at so get_timer_left_sec() remains accurate
tmthread->started_at = time(0);
}
else if (retval != ETIMEDOUT) {
LOG(TIMR_LOG, LOG_ERR, "pthread_cond_timedwait error: %d\n", retval);
break;
}
}
pthread_mutex_unlock(&tmthread->thread_mutex);
LOG(TIMR_LOG, LOG_NOTICE, "End timer for '%s'\n",tmthread->button->name);
LOG(TIMR_LOG, LOG_NOTICE, "End timer for '%s'\n", tmthread->button->name);
// We need to detect if we ended on time or were killed.
// If killed the device is probable off (or being set to off), so we should probably poll a few times before turning off.
@ -187,7 +268,7 @@ void *timer_worker( void *ptr )
// if duration_min is 0 we were killed, if not we got here on timeout, so turn off device.
if (tmthread->duration_min != 0 && tmthread->button->led->state != OFF) {
if ( (tmthread->duration_min != 0 || tmthread->duration_sec != 0) && tmthread->button->led->state != OFF) {
LOG(TIMR_LOG, LOG_INFO, "Timer waking turning '%s' off\n",tmthread->button->name);
panel_device_request(tmthread->aqdata, ON_OFF, tmthread->deviceIndex, false, NET_TIMER);
} else if (tmthread->button->led->state == OFF) {

View File

@ -4,9 +4,10 @@
#include "aqualink.h"
void start_timer(struct aqualinkdata *aq_data, /*aqkey *button,*/ int deviceIndex, int duration);
void start_timer(struct aqualinkdata *aqdata, int deviceIndex, int duration_min, uint32_t duration_sec);
int get_timer_left(aqkey *button);
void clear_timer(struct aqualinkdata *aq_data, /*aqkey *button,*/ int deviceIndex);
u_int32_t get_timer_left_sec(aqkey *button);
void clear_timer(struct aqualinkdata *aq_data, int deviceIndex);
// Not best place for this, but leave it here so all requests are in net services, this is forward decleration of function in net_services.c
#ifdef AQ_PDA
void create_PDA_on_off_request(aqkey *button, bool isON);

View File

@ -100,18 +100,14 @@ typedef struct aqualinkled
typedef struct aqualinkkey
{
//int number;
//aqledstate ledstate; // In the future there is no need to aqled struct so move code over to this.
aqled *led;
char *label;
char *name;
//#ifdef AQ_PDA
// char *pda_label;
//#endif
unsigned char code;
unsigned char rssd_code;
uint8_t special_mask;
void *special_mask_ptr;
uint32_t runtime_sec;
} aqkey;
@ -176,6 +172,7 @@ typedef enum action_type {
SPA_HTR_INCREMENT, // Setpoint add value
ON_OFF,
TIMER,
TIMER_SEC,
LIGHT_MODE,
LIGHT_BRIGHTNESS,
DATE_TIME

View File

@ -1154,12 +1154,10 @@ if (strlen(cleanwhitespace(value)) <= 0) {
rtn=true;
*/
} else if (strncasecmp(param + 9, "_light", 6) == 0) {
if ( ! populateLightData(aqdata, param + 10, &aqdata->aqbuttons[num], value) )
{
LOG(AQUA_LOG,LOG_ERR, "Config error, %s Ignored!",param,value);
}
rtn=true;
/*
@ -1175,14 +1173,15 @@ if (strlen(cleanwhitespace(value)) <= 0) {
LOG(AQUA_LOG,LOG_ERR, "Config error, Couldn't find light for '%s'\n",value);
}*/
} else if (strncasecmp(param + 9, "_pump", 5) == 0) {
if ( ! populatePumpData(aqdata, param + 10, &aqdata->aqbuttons[num], value) )
{
LOG(AQUA_LOG,LOG_ERR, "Config error, VSP Pumps limited to %d, ignoring : %s",MAX_PUMPS,param);
}
rtn=true;
}
} else if (strncasecmp(param + 9, "_runtime", 8) == 0) {
aqdata->aqbuttons[num].runtime_sec = time_string_to_seconds(value);
rtn=true;
}
//#if defined AQ_IAQTOUCH
} else if (strncasecmp(param, "virtual_button_", 15) == 0) {
@ -1270,12 +1269,18 @@ if (strlen(cleanwhitespace(value)) <= 0) {
vbutton->rssd_code = NUL;
break;
}
} else {
LOG(AQUA_LOG,LOG_ERR, "Config error, could not find vitrual button for `%s`",param);
}
rtn=true;
}
} else if (strncasecmp(param + 17, "_runtime", 8) == 0) {
aqkey *vbutton = getVirtualButton(aqdata, num);
if (vbutton != NULL) {
vbutton->runtime_sec = time_string_to_seconds(value);
rtn=true;
}
}
} else if (strncasecmp(param, "sensor_", 7) == 0) {
int num = strtoul(param + 7, NULL, 10) - 1;
if (num + 1 > MAX_SENSORS || num < 0) {
@ -1644,7 +1649,7 @@ char *errorlevel2text(int level)
#define ERR_VBUTTON_NO_EXTENDEDID (1<<8)
#define ERR_VBUTTON_4_BOOSTON (1<<9)
#define ERR_HEATPUMP_CHILLER (1<<10)
#define ERR_VBUTTON_ONET_NOID (1<<11)
void check_print_config (struct aqualinkdata *aqdata)
@ -1988,6 +1993,14 @@ void check_print_config (struct aqualinkdata *aqdata)
//char ext[] = " VSP ID None | AL ID 0 ";
char ext[120];
ext[0] = '\0';
char runtime[11];
runtime[0] = '\0';
if (aqdata->aqbuttons[i].runtime_sec > 0) {
seconds_to_time_string(aqdata->aqbuttons[i].runtime_sec, runtime, sizeof(runtime));
sprintf(runtime+8, " |");
}
for (j = 0; j < aqdata->num_pumps; j++) {
if (aqdata->pumps[j].button == &aqdata->aqbuttons[i]) {
sprintf(ext, "VSP ID 0x%02hhx | PumpID %-1d | %s",
@ -2008,15 +2021,23 @@ void check_print_config (struct aqualinkdata *aqdata)
sprintf(ext,"%-12s|", ((altlabel_detail *)aqdata->aqbuttons[i].special_mask_ptr)->altlabel);
}
LOG(AQUA_LOG,LOG_NOTICE, "Button %-13s = label %-15.15s | %s\n",
aqdata->aqbuttons[i].name, aqdata->aqbuttons[i].label, ext);
LOG(AQUA_LOG,LOG_NOTICE, "Button %-13s = label %-15.15s | %s%s\n",
aqdata->aqbuttons[i].name, aqdata->aqbuttons[i].label, runtime,ext);
if ( ((aqdata->aqbuttons[i].special_mask & VIRTUAL_BUTTON) == VIRTUAL_BUTTON) &&
((aqdata->aqbuttons[i].special_mask & VS_PUMP ) != VS_PUMP) &&
( ! is_aqualink_touch_id(_aqconfig_.extended_device_id))) {
//(_aqconfig_.extended_device_id < 0x30 || _aqconfig_.extended_device_id > 0x33 ) ){
setMASK(errors, ERR_VBUTTON_NO_EXTENDEDID);
if ( isVBUTTON(aqdata->aqbuttons[i].special_mask) && ( ! is_aqualink_touch_id(_aqconfig_.extended_device_id)))
{
// Onetouch protocol is good for if vbutton is pump or has onetouchID's
if (is_onetouch_id(_aqconfig_.extended_device_id)) {
//removeMASK(errors, ERR_VBUTTON_NO_EXTENDEDID);
if ( !isVS_PUMP(aqdata->aqbuttons[i].special_mask) && (aqdata->aqbuttons[i].rssd_code == NUL)) {
setMASK(errors, ERR_VBUTTON_ONET_NOID);
} //else if (!isVS_PUMP(aqdata->aqbuttons[i].special_mask)) {
//removeMASK(errors, ERR_VBUTTON_NO_EXTENDEDID);
//}
} else {
setMASK(errors, ERR_VBUTTON_NO_EXTENDEDID);
}
//LOG(AQUA_LOG,LOG_WARNING, "Config error, extended_device_id must be one of the folowing (0x30,0x31,0x32,0x33) to use virtual button : '%s'",aqdata->aqbuttons[i].label);
}
@ -2068,6 +2089,9 @@ void check_print_config (struct aqualinkdata *aqdata)
if (isMASK_SET(errors, ERR_HEATPUMP_CHILLER)) {
LOG(AQUA_LOG,LOG_ERR, "Config error, Heat pump / Chiller was not configured correctly\n");
}
if (isMASK_SET(errors, ERR_VBUTTON_ONET_NOID)) {
LOG(AQUA_LOG,LOG_ERR, "Config error, for Virtual Buttons using OneTouch protocol, 'onetouchID' must be set\n");
}
}
@ -2149,6 +2173,7 @@ int save_config_js(const char* inBuf, int inSize, char* outBuf, int outSize, str
aqdata->aqbuttons[i].special_mask_ptr = NULL;
aqdata->aqbuttons[i].code = NUL;
aqdata->aqbuttons[i].rssd_code = NUL;
aqdata->aqbuttons[i].runtime_sec = 0;
}
aqdata->num_pumps = 0;
aqdata->num_lights = 0;
@ -2459,6 +2484,12 @@ bool writeCfg (struct aqualinkdata *aqdata)
if (isVBUTTON_ALTLABEL(aqdata->aqbuttons[i].special_mask)) {
fprintf(fp,"%s_altLabel=%s\n", prefix, ((altlabel_detail *)aqdata->aqbuttons[i].special_mask_ptr)->altlabel);
}
if (aqdata->aqbuttons[i].runtime_sec > 0) {
char tmstr[30];
seconds_to_time_string(aqdata->aqbuttons[i].runtime_sec, tmstr, sizeof(tmstr));
fprintf(fp,"%s_runtime=%s\n", prefix, tmstr);
}
}

View File

@ -323,7 +323,8 @@ char *get_aux_information(aqkey *button, struct aqualinkdata *aqdata, char *buff
//printf("Button %s is Switch\n", button->name);
length += sprintf(buffer+length, ",\"type_ext\": \"switch_timer\", \"timer_active\":\"%s\"", (((button->special_mask & TIMER_ACTIVE) == TIMER_ACTIVE)?JSON_ON:JSON_OFF) );
if ((button->special_mask & TIMER_ACTIVE) == TIMER_ACTIVE) {
length += sprintf(buffer+length,",\"timer_duration\":\"%d\"", get_timer_left(button));
//length += sprintf(buffer+length,",\"timer_duration\":\"%d\"", get_timer_left(button));
length += sprintf(buffer+length,",\"timer_duration\":\"%u\"", get_timer_left_sec(button));
}
return buffer;
}
@ -835,7 +836,7 @@ printf("Pump GPM %d\n",aqdata->pumps[i].watts);
printf("Pump Type %d\n",aqdata->pumps[i].pumpType);
*/
//if (aqdata->pumps[i].pumpType != PT_UNKNOWN && (aqdata->pumps[i].rpm != TEMP_UNKNOWN || aqdata->pumps[i].gpm != TEMP_UNKNOWN || aqdata->pumps[i].watts != TEMP_UNKNOWN)) {
if (aqdata->pumps[i].pumpType != PT_UNKNOWN ) {
if (aqdata->pumps[i].pumpType != PT_UNKNOWN) {
length += sprintf(buffer+length, "\"Pump_%d\":{\"name\":\"%s\",\"id\":\"%s\",\"RPM\":\"%d\",\"GPM\":\"%d\",\"Watts\":\"%d\",\"Pump_Type\":\"%s\",\"Status\":\"%d\"},",
i+1,aqdata->pumps[i].button->label,aqdata->pumps[i].button->name,aqdata->pumps[i].rpm,aqdata->pumps[i].gpm,aqdata->pumps[i].watts,
(aqdata->pumps[i].pumpType==VFPUMP?"vfPump":(aqdata->pumps[i].pumpType==VSPUMP?"vsPump":"ePump")),
@ -861,7 +862,8 @@ printf("Pump Type %d\n",aqdata->pumps[i].pumpType);
for (i=0; i < aqdata->total_buttons; i++)
{
if ((aqdata->aqbuttons[i].special_mask & TIMER_ACTIVE) == TIMER_ACTIVE) {
length += sprintf(buffer+length, "\"%s\": \"%d\",", aqdata->aqbuttons[i].name, get_timer_left(&aqdata->aqbuttons[i]) );
//length += sprintf(buffer+length, "\"%s\": \"%d\",", aqdata->aqbuttons[i].name, get_timer_left(&aqdata->aqbuttons[i]) );
length += sprintf(buffer+length, "\"%s\": \"%u\",", aqdata->aqbuttons[i].name, get_timer_left_sec(&aqdata->aqbuttons[i]) );
}
}
if (buffer[length-1] == ',')
@ -1454,6 +1456,19 @@ int build_aqualink_config_JSON(char* buffer, int size, struct aqualinkdata *aqda
} else
length += result;
if (aqdata->aqbuttons[i].runtime_sec > 0) {
sprintf(buf,"%s_runtime", prefix);
char buf1[10];
buf1[0] = '\0';
seconds_to_time_string(aqdata->aqbuttons[i].runtime_sec, buf1, sizeof(buf1));
stringptr = buf1; // Create a pointer to the buffer
if ((result = json_cfg_element(buffer+length, size-length, buf, &stringptr, CFG_STRING, 0, NULL, 0)) <= 0) {
LOG(NET_LOG,LOG_ERR, "Config json buffer full in, result truncated! size=%d curently used=%d\n",size,length);
return length;
} else
length += result;
}
if (isVS_PUMP(aqdata->aqbuttons[i].special_mask))
{
if (((pump_detail *)aqdata->aqbuttons[i].special_mask_ptr)->pumpIndex > 0) {

View File

@ -1406,11 +1406,11 @@ uriAtype action_URI(request_source from, const char *URI, int uri_length, float
if ( strncasecmp(ri2, "ORP", 3) == 0 ) {
SET_IF_CHANGED(_aqualink_data->orp, round(value), _aqualink_data->is_dirty);
rtn = uActioned;
LOG(NET_LOG,LOG_NOTICE, "%s: request to set ORP to %d\n",actionName[from],_aqualink_data->orp);
LOG(NET_LOG,LOG_INFO, "%s: request to set ORP to %d\n",actionName[from],_aqualink_data->orp);
} else if ( strncasecmp(ri2, "Ph", 2) == 0 ) {
SET_IF_CHANGED(_aqualink_data->ph, value, _aqualink_data->is_dirty);
rtn = uActioned;
LOG(NET_LOG,LOG_NOTICE, "%s: request to set Ph to %.2f\n",actionName[from],_aqualink_data->ph);
LOG(NET_LOG,LOG_INFO, "%s: request to set Ph to %.2f\n",actionName[from],_aqualink_data->ph);
} else {
LOG(NET_LOG,LOG_WARNING,"%s: ignoring, unknown URI %.*s\n",actionName[from],uri_length,URI);
rtn = uBad;
@ -1424,12 +1424,11 @@ uriAtype action_URI(request_source from, const char *URI, int uri_length, float
//bool istimer = false;
action_type atype = ON_OFF;
//int timer=0;
if (strncasecmp(ri2, "timer", 5) == 0) {
//istimer = true;
atype = TIMER;
//timer = value; // Save off timer
//value = 1; // Make sure we turn device on if timer.
} else if ( value > 1 || value < 0) {
if (strncasecmp(ri2, "timer_sec", 9) == 0) {
atype = TIMER_SEC;
} else if (strncasecmp(ri2, "timer", 5) == 0) {
atype = TIMER;
}else if ( value > 1 || value < 0) {
LOG(NET_LOG,LOG_WARNING, "%s: URI %s has invalid value %.2f\n",actionName[from], URI, value);
*rtnmsg = INVALID_VALUE;
rtn = uBad;

View File

@ -43,7 +43,7 @@ static struct ot_macro _macros[3];
bool _panel_version_P2 = false; // Older panels REV 0.1 and 0.2
void set_macro_status();
void set_macro_status(struct aqualinkdata *aqdata);
void pump_update(struct aqualinkdata *aqdata, int updated);
bool log_heater_setpoints(struct aqualinkdata *aqdata);
@ -789,7 +789,7 @@ bool new_menu(struct aqualinkdata *aqdata)
switch (menu_type) {
case OTM_ONETOUCH:
set_macro_status();
set_macro_status(aqdata);
break;
case OTM_EQUIPTMENT_STATUS:
if (initRS == false) {
@ -835,7 +835,8 @@ bool new_menu(struct aqualinkdata *aqdata)
return rtn;
}
void set_macro_status()
void set_macro_status(struct aqualinkdata *aqdata)
{
// OneTouch Menu Line 2 = SPA MODE OFF
// OneTouch Menu Line 5 = CLEAN MODE ON
@ -860,6 +861,18 @@ void set_macro_status()
LOG(ONET_LOG,LOG_DEBUG, "Macro #2 '%s' is %s\n",_macros[1].name,_macros[1].ison?"On":"Off");
LOG(ONET_LOG,LOG_DEBUG, "Macro #3 '%s' is %s\n",_macros[2].name,_macros[2].ison?"On":"Off");
for (int i = aqdata->virtual_button_start; i < aqdata->total_buttons; i++) {
if (aqdata->aqbuttons[i].rssd_code - 15 == 1) {
SET_IF_CHANGED(aqdata->aqbuttons[i].led->state, _macros[0].ison?ON:OFF, aqdata->is_dirty);
LOG(ONET_LOG,LOG_DEBUG, "Setting Macro #1 '%s' to %s for button '%s'\n",_macros[0].name,_macros[0].ison?"On":"Off",aqdata->aqbuttons[i].label);
} else if (aqdata->aqbuttons[i].rssd_code - 15 == 2) {
SET_IF_CHANGED(aqdata->aqbuttons[i].led->state, _macros[1].ison?ON:OFF, aqdata->is_dirty);
LOG(ONET_LOG,LOG_DEBUG, "Setting Macro #1 '%s' to %s for button '%s'\n",_macros[1].name,_macros[1].ison?"On":"Off",aqdata->aqbuttons[i].label);
} else if (aqdata->aqbuttons[i].rssd_code - 15 == 3) {
SET_IF_CHANGED(aqdata->aqbuttons[i].led->state, _macros[2].ison?ON:OFF, aqdata->is_dirty);
LOG(ONET_LOG,LOG_DEBUG, "Setting Macro #1 '%s' to %s for button '%s'\n",_macros[2].name,_macros[2].ison?"On":"Off",aqdata->aqbuttons[i].label);
}
}
}
}

View File

@ -262,6 +262,41 @@ bool goto_onetouch_system_menu(struct aqualinkdata *aqdata)
return true;
}
bool goto_onetouch_macros_menu(struct aqualinkdata *aqdata)
{
for(int i=0; i < 4; i++) {
if (get_onetouch_menu_type() == OTM_ONETOUCH) {
return true;
}
if (get_onetouch_menu_type() == OTM_SYSTEM) {
select_onetouch_menu_item(aqdata, "ONETOUCH ON/OFF");
waitfor_ot_queue2empty();
//LOG(ONET_LOG,LOG_DEBUG, "goto_onetouch_macros_menu queue empty\n");
waitForNextOT_Menu(aqdata);
//LOG(ONET_LOG,LOG_DEBUG, "goto_onetouch_macros_menu next menu\n");
//waitForOT_MessageTypes(aqdata,CMD_PDA_HIGHLIGHT,CMD_PDA_HIGHLIGHT,15);
//LOG(ONET_LOG,LOG_DEBUG, "goto_onetouch_macros_menu CMD_PDA_HIGHLIGHT\n");
//delay(500);
//LOG(ONET_LOG,LOG_DEBUG, "goto_onetouch_macros_menu delay\n");
//waitForOT_MessageTypes(aqdata,CMD_PDA_CLEAR,CMD_PDA_HIGHLIGHTCHARS,15);
//waitForOT_MessageTypes(aqdata,CMD_PDA_HIGHLIGHT,CMD_PDA_HIGHLIGHT,15);
//waitForOT_MessageType(aqdata,CMD_PDA_HIGHLIGHT,CMD_PDA_0x04,10);
} else if (get_onetouch_menu_type() != OTM_ONETOUCH){
send_ot_cmd(KEY_ONET_BACK);
waitfor_ot_queue2empty();
waitForNextOT_Menu(aqdata);
}
}
LOG(ONET_LOG,LOG_ERR, "OneTouch device programmer couldn't get to ONETOUCH menu\n");
return false;
}
bool goto_onetouch_menu(struct aqualinkdata *aqdata, ot_menu_type menu)
{
bool equErr = false;
@ -270,6 +305,10 @@ bool goto_onetouch_menu(struct aqualinkdata *aqdata, ot_menu_type menu)
LOG(ONET_LOG,LOG_DEBUG, "OneTouch device programmer request for menu %d\n",menu);
if (menu == OTM_ONETOUCH){
return goto_onetouch_macros_menu(aqdata);
}
if ( ! goto_onetouch_system_menu(aqdata) ) {
LOG(ONET_LOG,LOG_ERR, "OneTouch device programmer failed to get system menu\n");
return false;
@ -569,9 +608,10 @@ void *set_aqualink_onetouch_freezeprotect( void *ptr )
void *set_aqualink_onetouch_macro( void *ptr )
{
/*
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
//struct aqualinkdata *aqdata = threadCtrl->aqdata;
struct aqualinkdata *aqdata = threadCtrl->aqdata;
//sprintf(msg, "%-5d%-5d",index, (strcmp(value, "on") == 0)?ON:OFF);
// Use above to set
@ -585,13 +625,58 @@ void *set_aqualink_onetouch_macro( void *ptr )
unsigned int device = atoi(&buf[0]);
unsigned int state = atoi(&buf[5]);
#endif
*/
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aqdata = threadCtrl->aqdata;
#ifdef NEW_AQ_PROGRAMMER
struct programmerArgs *pargs = &threadCtrl->pArgs;
aqkey *button = threadCtrl->pArgs.button;
int value = pargs->value;
#else
//int val = atoi((char*)threadCtrl->thread_args);
#endif
if (! isVBUTTON(button->special_mask)){
LOG(ONET_LOG,LOG_ERR, "OneTouch macro programmer only supports VBUTTON macros\n");
return ptr;
}
waitForSingleThreadOrTerminate(threadCtrl, AQ_SET_ONETOUCH_MACRO);
LOG(ONET_LOG,LOG_DEBUG, "OneTouch Marco\n");
LOG(ONET_LOG,LOG_ERR, "OneTouch Macro not implimented (device=%d|state=%d)\n",button->label,state);
//LOG(ONET_LOG,LOG_ERR, "OneTouch Macro not implimented (device=%d|state=%d)\n",button->label,state);
if ( !goto_onetouch_menu(aqdata, OTM_ONETOUCH) ){
LOG(ONET_LOG,LOG_ERR, "OneTouch device programmer failed to get heater temp menu\n");
}
// Check button is not= value. (don't do this before as the menu command above may change the state of the button)
if (button->led->state == value) {
LOG(ONET_LOG,LOG_DEBUG, "OneTouch Macro '%s' already in desired state, no change needed\n", button->label);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
switch(button->rssd_code - 15) {
case 1:
send_ot_cmd(KEY_ONET_SELECT_1);
SET_IF_CHANGED(button->led->state, !button->led->state, aqdata->is_dirty);
break;
case 2:
send_ot_cmd(KEY_ONET_SELECT_2);
SET_IF_CHANGED(button->led->state, !button->led->state, aqdata->is_dirty);
break;
case 3:
send_ot_cmd(KEY_ONET_SELECT_3);
SET_IF_CHANGED(button->led->state, !button->led->state, aqdata->is_dirty) ;
break;
default:
LOG(ONET_LOG,LOG_ERR, "OneTouch Macro programmer only has 3 buttons OneTouchID %d is invalid\n",button->rssd_code - 15);
}
cleanAndTerminateThread(threadCtrl);
// just stop compiler error, ptr is not valid as it's just been freed

View File

@ -968,6 +968,69 @@ char *prittyString(char *str)
return str;
}
/*
#include <stdio.h>
#include <stdint.h>
#include <string.h>
*/
/**
* Converts a time string to total seconds.
* Supported formats: "HH:MM:SS", "MM:SS", or "SS"
*/
uint32_t time_string_to_seconds(const char *time_str) {
if (time_str == NULL) return 0;
int parts[3] = {0, 0, 0};
int count = 0;
// Count the number of colons to determine the format
const char *p = time_str;
int colons = 0;
while (*p) {
if (*p == ':') colons++;
p++;
}
if (colons == 2) {
// Format: HH:MM:SS
count = sscanf(time_str, "%d:%d:%d", &parts[0], &parts[1], &parts[2]);
if (count == 3) {
return (uint32_t)((parts[0] * 3600) + (parts[1] * 60) + parts[2]);
}
} else if (colons == 1) {
// Format: MM:SS
count = sscanf(time_str, "%d:%d", &parts[0], &parts[1]);
if (count == 2) {
return (uint32_t)((parts[0] * 60) + parts[1]);
}
} else {
// Format: SS
count = sscanf(time_str, "%d", &parts[0]);
if (count == 1) {
return (uint32_t)parts[0];
}
}
return 0; // Return 0 if parsing failed
}
/**
* Converts total seconds into a string with format "HH:MM:SS".
* Ensure the output buffer is at least 9 bytes (8 chars + null terminator).
*/
void seconds_to_time_string(uint32_t total_seconds, char *output_buffer, size_t buffer_size) {
if (output_buffer == NULL || buffer_size < 9) {
return;
}
uint32_t h = total_seconds / 3600;
uint32_t m = (total_seconds % 3600) / 60;
uint32_t s = total_seconds % 60;
// %02u ensures two digits with leading zeros
snprintf(output_buffer, buffer_size, "%02u:%02u:%02u", h, m, s);
}
temperatureUOM getTemperatureUOM(const char *uom) {

View File

@ -140,7 +140,8 @@ char *prittyString(char *str);
//void closePacketLog();
float timespec2float(const struct timespec *elapsed);
bool isUomTemperature( const char *uom);
uint32_t time_string_to_seconds(const char *time_str);
void seconds_to_time_string(uint32_t total_seconds, char *output_buffer, size_t buffer_size);
temperatureUOM getTemperatureUOM(const char *uom);

View File

@ -4,5 +4,5 @@
#define AQUALINKD_SHORT_NAME "AqualinkD"
// Use Magor . Minor . Patch
#define AQUALINKD_VERSION "3.0.5 (dev)"
#define AQUALINKD_VERSION "3.1.0"

View File

@ -955,8 +955,8 @@
// }
// key = 1
//}
const buttonTypelist = ["pumpIndex", "pumpID", "pumpType", "pumpName", "pumpMaxSpeed", "pumpMinSpeed", "lightMode"];
const virtButtonTypelist = ["pumpIndex", "pumpID", "pumpType", "pumpName", "pumpMaxSpeed", "pumpMinSpeed", "lightMode", "onetouchID", "altLabel"];
const buttonTypelist = ["pumpIndex", "pumpID", "pumpType", "pumpName", "pumpMaxSpeed", "pumpMinSpeed", "lightMode", "runtime"];
const virtButtonTypelist = ["pumpIndex", "pumpID", "pumpType", "pumpName", "pumpMaxSpeed", "pumpMinSpeed", "lightMode", "runtime", "onetouchID", "altLabel"];
const configtable = document.getElementById("config_table");
const newRow = configtable.insertRow();
var cell1 = newRow.insertCell();
@ -1036,7 +1036,7 @@
_addedBlankVirtualButton = true;
return;
}
const virtButtonTypelist = ["pumpIndex", "pumpID", "pumpType", "pumpName", "pumpMaxSpeed", "pumpMinSpeed", "lightMode", "onetouchID", "altLabel"];
const virtButtonTypelist = ["pumpIndex", "pumpID", "pumpType", "pumpName", "pumpMaxSpeed", "pumpMinSpeed", "lightMode", "runtime", "onetouchID", "altLabel"];
const configtable = document.getElementById("config_table");
const vbname = "virtual_button_" + String(num).padStart(2, '0');
const vblabel = vbname + "_label";
@ -1156,6 +1156,8 @@
} else if(deleted && key.startsWith("light_program_")) {
//console.log(key.slice(0, 9));
delete _config[key];
} else {
//console.log("delete - Unknown delete option " + key);
}
if(rebuild) {
resetConfig(null);
@ -1288,6 +1290,7 @@
break;
case "pumpName":
case "altLabel":
case "runtime":
js.type = "string";
break;
}

View File

@ -2017,6 +2017,31 @@
const minutes = totalMinutes % 60;
return `${padToTwoDigits(hours)}:${padToTwoDigits(minutes)}`;
}
/**
* Formats seconds into a human-readable string.
* Returns HH:MM if duration is 1 minute or more.
* Returns MM:SS if duration is less than 1 minute.
*/
function formatDuration(totalSeconds) {
if (totalSeconds < 0) totalSeconds = 0;
// If we are at 1 minute or more, round to the nearest minute
// (e.g., 1:30 becomes 2:00, 1:29 stays 1:00)
if (totalSeconds >= 60) {
const roundedSeconds = Math.round(totalSeconds / 60) * 60;
const h = Math.floor(roundedSeconds / 3600);
const m = Math.floor((roundedSeconds % 3600) / 60);
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
}
// If under 60 seconds, show actual seconds
const s = Math.floor(totalSeconds % 60);
return `00:${s.toString().padStart(2, '0')}s`;
}
function padToTwoDigits(num) {
return num.toString().padStart(2, "0");
}
@ -3186,7 +3211,9 @@
//console.log("TIMER "+obj.toString());
}
for (var obj in data.timer_durations) {
setTileOnText(obj.toString(),"Timer "+toHoursAndMinutes(data.timer_durations[obj]));
//setTileOnText(obj.toString(),"Timer "+toHoursAndMinutes(data.timer_durations[obj]));
setTileOnText(obj.toString(),"Timer "+formatDuration(data.timer_durations[obj]));
//setTileOnTextLine2(obj.toString(),"Timer "+toHoursAndMinutes(data.timer_durations[obj]));
//console.log("TIMER "+obj.toString()+" duration "+data.timer_durations[obj]);
}