first commit

pull/46/head
shaun feakes 2017-12-30 14:12:01 -06:00
commit c326f039ba
63 changed files with 32875 additions and 0 deletions

25
LICENSE.md Normal file
View File

@ -0,0 +1,25 @@
# License types
## Non Commercial Project
All non commercial projects can be run using our open source code under GPLv2 licensing. As long as your project remains in this category there is no charge.
## Prototyping
If you are working on a prototype that may lead to a commercial product, you can start off using the Open Source Code under GPLv2 for free. However, once the project is commercialised, you need to change to a Commercial license.
## Commercial Application
Once your end device or application is commercial you need to either open the source code of your end product completely to continue using aqualinkd free of charge under GPLv2 license or alternatively purchase a Commercial license.
If you are unsure which type of license is right for you, contact us to discuss.
## aqualinkd Commercial License
The right to develop and modify the software to embed in your own products
Ability to redistribute your products with aqualinkd included
Option to access Software Maintenance & Support for updates, upgrades and technical support
Our license model is simple and includes full access to source code and accompanying documentation. If your needs require a custom license, wed be happy to work on a solution with you.
## Non Compliance and License Incompatibilities
Integrating aqualinkd software in a commercial product without fully releasing your source code or purchasing a commercial license can have legal ramifications. You agree to the terms under GPLv2 as you integrate our source code.
Incompatibilities, through taking different components of different sources and ignoring any license restrictions, can result in the software pieces you choose not to be permitted to be used in a single product.

73
Makefile Executable file
View File

@ -0,0 +1,73 @@
# define the C compiler to use
CC = gcc
LIBS := -lpthread -lm
#LIBS := -lpthread -lwebsockets
# debug of not
#$DBG = -g
$DBG =
# define any compile-time flags
#CFLAGS = -Wall -g -lpthread -lwiringPi -lm -I.
#CFLAGS = -Wall -g $(LIBS) -I/usr/local/include/ -L/usr/local/lib/
#CFLAGS = -Wall -g $(LIBS) -std=gnu11 -I/nas/data/Development/Raspberry/aqualink/libwebsockets-2.0-stable/lib -L/nas/data/Development/Raspberry/aqualink/libwebsockets-2.0-stable/lib
#CFLAGS = -Wall -g $(LIBS)
#CFLAGS = -Wall -g $(LIBS) -std=gnu11
CFLAGS = -Wall $(DBG) $(LIBS) -D MG_DISABLE_MD5 -D MG_DISABLE_HTTP_DIGEST_AUTH -D MG_DISABLE_MD5 -D MG_DISABLE_JSON_RPC
#CFLAGS = -Wall $(DBG) $(LIBS) -D MG_DISABLE_MQTT -D MG_DISABLE_MD5 -D MG_DISABLE_HTTP_DIGEST_AUTH -D MG_DISABLE_MD5 -D MG_DISABLE_JSON_RPC
# Add inputs and outputs from these tool invocations to the build variables
# define the C source files
SRCS = aqualinkd.c utils.c config.c aq_serial.c init_buttons.c aq_programmer.c net_services.c json_messages.c mongoose.c
OBJS = $(SRCS:.c=.o)
# define the executable file
MAIN = ./release/aqualinkd
all: $(MAIN)
@echo: $(MAIN) have been compiled
$(MAIN): $(OBJS)
$(CC) $(CFLAGS) $(INCLUDES) -o $(MAIN) $(OBJS) $(LFLAGS) $(LIBS)
# this is a suffix replacement rule for building .o's from .c's
# it uses automatic variables $<: the name of the prerequisite of
# the rule(a .c file) and $@: the name of the target of the rule (a .o file)
# (see the gnu make manual section about automatic variables)
.c.o:
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
clean:
$(RM) *.o *~ $(MAIN) $(MAIN_U)
depend: $(SRCS)
makedepend $(INCLUDES) $^
install: $(MAIN)
./release/install.sh
# All Target
#all: aqualinkd
#
# Tool invocations
#aqualinkd: $(OBJS) $(USER_OBJS)
# @echo 'Building target: $@'
# @echo 'Invoking: GCC C Linker'
# gcc -L/home/perry/workspace/libwebsockets/Debug -pg -o"aqualinkd" $(OBJS) $(USER_OBJS) $(LIBS)
# @echo 'Finished building target: $@'
# @echo ' '
#
# Other Targets
#clean:
# -$(RM) $(OBJS)$(C_DEPS)$(EXECUTABLES) aqualinkd
# -@echo ' '
#
#.PHONY: all clean dependents
#.SECONDARY:
#
#-include ../makefile.targets

139
README.md Normal file
View File

@ -0,0 +1,139 @@
# Aqualinkd
linux daemon to control Aqualink RS pool controllers. Provides web UI, MTQQ client for Domoticz, HTTP API endpoints for Samsung, Alexa, Google, etc home hubs.
VERY MUCH A BETA RELEASE.
# TL;DR Install
* Get a linux computer (like a Raspberry PI) linked up to the RS485 interface of your pool controller.
* Get a copy of the repo using git.
* run make
* run sudo make install
* edit `/etc/aqualinkd.conf` to your setup
* Test every works :-
* `sudo aqualinkd -d -c /etc/aqualinkd.conf`
* point a web browser to the IP of the box running aqualinkd
* If all is good enable as service
* sudo `systemctl start aqualinkd`
### Note:-
The install script is designed for systemd / systemctl to run as a service or daemon. If you are using init / init-d then don't run the install script, install manuallt and the init-d script to use is in the xtras directory.
Manual install for init-d systems
* copy ./release/aqualinkd to /usr/local/bin
* copy ./release/aqualinkd.conf to /etc
* copy ./release/aqualinkd.service.avahi to /etc/avahi/services/aqualinkd.service
* copy ./extras/aqualinkd.init.d to /etc/init-d/aqualink
* copy recuesivley ./web/* to /var/www/aqualinkd/
* sudo update-rc.d aqualinkd defaults
## TODO
* Only WEB interface (WS) & AQ_MQTT can change freeze & heater temprature set-points. Need to add support for standard HTTP. (DOMOTICZ_MQTT don't be supported until Domoticz create a better virtual thermostat)
* Web interface has a lot of fixed layout items that as specific to my implimentation. The HTML & CSS need a complete overhall and re-though to support different configurations.
* There is code to control different light modes/shows, but it's not finished and no documentation will be provided until it is finished. It will not work unless you have this exact setup Haywood ColorLogic/Aqualink RS8.
## Hardware
You will need a [USB2RS485](https://www.amazon.com/OctagonStar-Converter-Adapter-Interface-FT232RL/dp/B01LCFRR3E/) adapter connected to your pool equiptmeent RS buss interface. (If you have an inside controller mounted on your wall, this is usually best place, if not the outside control panel is the next best place). Then a computer running linux connected to that USB2RS485 adapter. Code is designed & developed for raspberry pi zero w, so any computer with that as a minimum should work.
## Configuratio with home automation hubs
## Domoticz
Enable MQTT in Domoticz, and install a MQTT broker. (If you don;t want to do this, then follow generic hub setup)
Create a virtual device for each piece of pool equiptment you have, eg Filter Pump, Spa Mode, Pool Light, Cleaner then place the Domoticz IDX for each device in the aqualinkd.conf file under the appropiate button.
## MQTT
Aqualinkd supports generic MQTT implimentations, as well as specific Domoticz one described above.
To enable, simply configure the main topic in `aqualinkd.conf`.
```mqtt_aq_topic = aqualinkd```
Then status messages will be posted to the sub topics listed below, with appropiate information. Each button uses the standard Aqualink RS controller name, so Aux_1 might be your poool light.
```
Tempratures will be a simply number posted to the topic.
aqualinkd/Temperature/Air
aqualinkd/Temperature/Pool
aqualinkd/Temperature/Spa
aqualinkd/Pool_Heater/setpoint
aqualinkd/Spa_Heater/setpoint
Buttons will be a 1 or 0 for state. 1=on 0=off
aqualinkd/Filter_Pump
aqualinkd/Spa_Mode
aqualinkd/Aux_1
........./Aux_.
aqualinkd/Aux_7
aqualinkd/Pool_Heater
aqualinkd/Spa_Heater
```
To turn something on, or set information, simply add `set` to the end of the above topics, and post 1 or 0 in the message for a button, or a number for a setpoint. Topics Aqualinkd will act on.
```
aqualinkd/Filter_Pump/set
aqualinkd/Spa_Mode/set
aqualinkd/Aux_1/set
aqualinkd/Aux_7/set
aqualinkd/Pool_Heater/set
aqualinkd/Spa_Heater/set
aqualinkd/Pool_Heater/setpoint
aqualinkd/Spa_Heater/setpoint
```
Example that would turn on the filter pump
```
aqualinkd/Filter_Pump/set 1
```
## All other hubs excluding Apple HomeKit (Amazon,Samsung,Google etc)
Create a device for each piece of pool equiptment you have, eg Filter Pump, Spa Mode, Pool Light, Cleaner. Then add the following URL to program the switching of each device
```
http://aqualinkd.ip.address:port?command=Filter_Pump&value=on
```
each button ie command=xxxxx uses the default Aqualink name so valid options are
* Filter_Pump
* Spa_Mode
* Aux_1
* Aux_2
* Aux_3
* Aux_4
* Aux_5
* Aux_6
* Aux_7
* Pool_Heater
* Spa_Heater
* Solar_Heater
To get status than can be passed by any smart hub use the below url, a JSON string with the state of each device and current temprature will be returned.
```
http://aqualinkd.ip.address:port?command=status
```
## Apple HomeKit
For the moment, native Homekit support has been removed, it will be added back in the future under a different implimentation.
Recomended option for HomeKit support is to make use of the MQTT interface and use [HomeKit2MQTT](https://www.npmjs.com/package/homekit2mqtt) to bridge between Aqualinkd and you Apple (phone/tablet/tv & hub).
* Install a MQTT broker (mosquitto) is recomended, this can usually be installed with apt-get
* Install HomeKitMQTT. (see webpage for install)
* Then copy the homekit2mqtt configuration file found in the extras directory `homekit2mqtt.json`
You can of course use a myriad of other HomeKit bridges with the URL endpoints listed in the `All other hubs section`, or MQTT topics listed in the `MQTT` section. The majority of them (including HomeBridge the most popular) use Node and HAP-Node.JS, neither of which I am a fan of for the RaspberryPI. But HomeKit2MQTT seemed to have the least overhead of them all. So that's why the recomendation.
## MeteoHub
If you want to log/track water temps within MeteoHub, simple configure it to poll the below URL and water & air tempratures will be sent in a format native to MeteoHub.
```
http://aqualinkd.ip.address:port?command=mhstatus
```
t0 will be air temp and t1 water temp.
To use. copy the `meteohub-aq-plugin.sh` script from the extras directory to your meteohub box, edit the script and use your IP address in the line that makes the URL call, below.
```
wget -O /dev/stdout 'http://your.ip.address.here/?command=mhstatus' 2>/dev/null
```
In meteohub create a new weatherstation plug-in, plug-in path is the path to the above sctipt, and his save. 2 new sensors should now show up as thermo in the sensor page.
# License
## Non Commercial Project
All non commercial projects can be run using our open source code under GPLv2 licensing. As long as your project remains in this category there is no charge.
See License.md for more details.

20
aq_mqtt.h Normal file
View File

@ -0,0 +1,20 @@
#ifndef AQ_MQTT_H_
#define AQ_MQTT_H_
#define AIR_TEMP_TOPIC "Temperature/Air"
#define POOL_TEMP_TOPIC "Temperature/Pool"
#define SPA_TEMP_TOPIC "Temperature/Spa"
#define FREEZE_PROTECT "Freeze_Protect"
// These need to be moved, but at present only aq_mqtt uses them, so there are here.
// Also need to get the C values from aqualink manual and add those just incase
// someone has the controller set to C.
#define HEATER_MAX 104
#define MEATER_MIN 36
#define FREEZE_PT_MAX 42
#define FREEZE_PT_MIN 36
#endif // AQ_MQTT_H_

862
aq_programmer.c Normal file
View File

@ -0,0 +1,862 @@
/*
* Copyright (c) 2017 Shaun Feakes - All rights reserved
*
* You may use redistribute and/or modify this code under the terms of
* the GNU General Public License version 2 as published by the
* Free Software Foundation. For the terms of this license,
* see <http://www.gnu.org/licenses/>.
*
* You are free to use this software under the terms of the GNU General
* Public License, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* https://github.com/sfeakes/aqualinkd
*/
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include "aqualink.h"
#include "utils.h"
#include "aq_programmer.h"
bool select_sub_menu_item(struct aqualinkdata *aq_data, char* item_string);
bool select_menu_item(struct aqualinkdata *aq_data, char* item_string);
void send_cmd(unsigned char cmd, struct aqualinkdata *aq_data);
void cancel_menu(struct aqualinkdata *aq_data);
void *set_aqualink_pool_heater_temps( void *ptr );
void *set_aqualink_spa_heater_temps( void *ptr );
void *set_aqualink_freeze_heater_temps( void *ptr );
void *set_aqualink_time( void *ptr );
void *get_aqualink_pool_spa_heater_temps( void *ptr );
void *get_aqualink_programs( void *ptr );
void *get_freeze_protect_temp( void *ptr );
void *get_aqualink_diag_model( void *ptr );
void *threadded_send_cmd( void *ptr );
void *set_aqualink_light_colormode( void *ptr );
bool waitForButtonState(struct aqualinkdata *aq_data, aqkey* button, aqledstate state, int numMessageReceived);
bool waitForMessage(struct aqualinkdata *aq_data, char* message, int numMessageReceived);
bool waitForEitherMessage(struct aqualinkdata *aq_data, char* message1, char* message2, int numMessageReceived);
void kick_aq_program_thread(struct aqualinkdata *aq_data)
{
if (aq_data->active_thread.thread_id != 0) {
logMessage(LOG_DEBUG, "Kicking thread\n");
pthread_cond_broadcast(&aq_data->active_thread.thread_cond);
}
}
void aq_programmer(program_type type, char *args, struct aqualinkdata *aq_data)
{
struct programmingThreadCtrl *programmingthread = malloc(sizeof(struct programmingThreadCtrl));
programmingthread->aq_data = aq_data;
//programmingthread->thread_args = args;
if (args != NULL)
strncpy(programmingthread->thread_args, args, sizeof(programmingthread->thread_args)-1);
switch(type) {
case AQ_SEND_CMD:
if(aq_data->active_thread.thread_id == 0) { // No need to thread a plane send if no active threads
send_cmd( (unsigned char)*args, aq_data);
} else if( pthread_create( &programmingthread->thread_id , NULL , threadded_send_cmd, (void*)programmingthread) < 0) {
logMessage (LOG_ERR, "could not create thread\n");
return;
}
break;
case AQ_GET_POOL_SPA_HEATER_TEMPS:
if( pthread_create( &programmingthread->thread_id , NULL , get_aqualink_pool_spa_heater_temps, (void*)programmingthread) < 0) {
logMessage (LOG_ERR, "could not create thread\n");
return;
}
break;
case AQ_GET_FREEZE_PROTECT_TEMP:
if( pthread_create( &programmingthread->thread_id , NULL , get_freeze_protect_temp, (void*)programmingthread) < 0) {
logMessage (LOG_ERR, "could not create thread\n");
return;
}
break;
case AQ_SET_TIME:
if( pthread_create( &programmingthread->thread_id , NULL , set_aqualink_time, (void*)programmingthread) < 0) {
logMessage (LOG_ERR, "could not create thread\n");
return;
}
break;
case AQ_SET_POOL_HEATER_TEMP:
if( pthread_create( &programmingthread->thread_id , NULL , set_aqualink_pool_heater_temps, (void*)programmingthread) < 0) {
logMessage (LOG_ERR, "could not create thread\n");
return;
}
break;
case AQ_SET_SPA_HEATER_TEMP:
if( pthread_create( &programmingthread->thread_id , NULL , set_aqualink_spa_heater_temps, (void*)programmingthread) < 0) {
logMessage (LOG_ERR, "could not create thread\n");
return;
}
break;
case AQ_SET_FRZ_PROTECTION_TEMP:
if( pthread_create( &programmingthread->thread_id , NULL , set_aqualink_freeze_heater_temps, (void*)programmingthread) < 0) {
logMessage (LOG_ERR, "could not create thread\n");
return;
}
break;
case AQ_GET_DIAGNOSTICS_MODEL:
if( pthread_create( &programmingthread->thread_id , NULL , get_aqualink_diag_model, (void*)programmingthread) < 0) {
logMessage (LOG_ERR, "could not create thread\n");
return;
}
break;
case AQ_GET_PROGRAMS:
if( pthread_create( &programmingthread->thread_id , NULL , get_aqualink_programs, (void*)programmingthread) < 0) {
logMessage (LOG_ERR, "could not create thread\n");
return;
}
break;
case AQ_SET_COLORMODE:
if( pthread_create( &programmingthread->thread_id , NULL , set_aqualink_light_colormode, (void*)programmingthread) < 0) {
logMessage (LOG_ERR, "could not create thread\n");
return;
}
break;
default:
logMessage (LOG_ERR, "Don't understand thread type\n");
break;
}
pthread_detach(programmingthread->thread_id);
}
void waitForSingleThreadOrTerminate(struct programmingThreadCtrl *threadCtrl, program_type type)
{
//static int tries = 120;
int tries = 120;
static int waitTime = 1;
int i=0;
while ( (threadCtrl->aq_data->active_thread.thread_id != 0) && ( i++ <= tries) ) {
logMessage (LOG_DEBUG, "Thread %d sleeping, waiting for thread %d to finish\n", threadCtrl->thread_id, threadCtrl->aq_data->active_thread.thread_id);
sleep(waitTime);
}
if (i >= tries) {
logMessage (LOG_ERR, "Thread %d timeout waiting, ending\n",threadCtrl->thread_id);
free(threadCtrl);
pthread_exit(0);
}
threadCtrl->aq_data->active_thread.thread_id = &threadCtrl->thread_id;
threadCtrl->aq_data->active_thread.ptype = type;
logMessage (LOG_DEBUG, "Thread %d is active\n", threadCtrl->aq_data->active_thread.thread_id);
}
void cleanAndTerminateThread(struct programmingThreadCtrl *threadCtrl)
{
logMessage(LOG_DEBUG, "Thread %d finished\n",threadCtrl->thread_id);
// Quick delay to allow for last message to be sent.
delay(500);
threadCtrl->aq_data->active_thread.thread_id = 0;
threadCtrl->aq_data->active_thread.ptype = AQP_NULL;
threadCtrl->thread_id = 0;
free(threadCtrl);
pthread_exit(0);
}
bool setAqualinkNumericField(struct aqualinkdata *aq_data, char *value_label, int value)
{
logMessage(LOG_DEBUG,"Setting menu item '%s' to %d\n",value_label, value);
char leading[10]; // description of the field (POOL, SPA, FRZ)
int current_val; // integer value of the current set point
char trailing[10]; // the degrees and scale
char searchBuf[20];
sprintf(searchBuf, "^%s", value_label);
do
{
if (waitForMessage(aq_data, searchBuf, 3) != true) {
logMessage(LOG_WARNING, "Could not set %s temp, current temp not found\n",value_label);
cancel_menu(aq_data);
return false;
}
//logMessage(LOG_DEBUG,"WAITING for kick value=%d\n",current_val);
sscanf(aq_data->last_message, "%s %d%s", leading, &current_val, trailing);
// logMessage(LOG_DEBUG, "%s set to %d, looking for %d\n",value_label,current_val,value);
if(value > current_val) {
// Increment the field.
sprintf(searchBuf, "%s %d", value_label, current_val+1);
send_cmd(KEY_RIGHT, aq_data);
}
else if(value < current_val) {
// Decrement the field.
sprintf(searchBuf, "%s %d", value_label, current_val-1);
send_cmd(KEY_LEFT, aq_data);
}
else {
// Just send ENTER. We are at the right value.
sprintf(searchBuf, "%s %d", value_label, current_val);
send_cmd(KEY_ENTER, aq_data);
}
} while(value != current_val);
return true;
}
void *threadded_send_cmd( void *ptr )
{
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aq_data = threadCtrl->aq_data;
waitForSingleThreadOrTerminate(threadCtrl, AQ_SEND_CMD);
send_cmd( (unsigned char)*threadCtrl->thread_args, aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
void *set_aqualink_light_colormode( void *ptr )
{
int i;
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aq_data = threadCtrl->aq_data;
waitForSingleThreadOrTerminate(threadCtrl, AQ_SET_COLORMODE);
char *buf = (char*)threadCtrl->thread_args;
int val = atoi(&buf[0]);
int btn = atoi(&buf[5]);
float pmode = atof(&buf[10]);
if (btn < 0 || btn >= TOTAL_BUTTONS ) {
logMessage(LOG_ERR, "Can't program light mode on button %d\n", btn);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
aqkey *button = &aq_data->aqbuttons[btn];
unsigned char code = button->code;
logMessage(LOG_NOTICE, "Pool Light Programming #: %d, on button: %s, with mode: %f\n", val, button->label, pmode);
// Simply turn the light off if value is 0
if (val <= 0) {
if ( button->led->state == ON ) {
send_cmd(code, aq_data);
}
cleanAndTerminateThread(threadCtrl);
return ptr;
}
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 ) {
logMessage(LOG_INFO, "Pool Light Initial state off, turning on for 15 seconds\n");
send_cmd(code, aq_data);
delay(15 * seconds);
}
logMessage(LOG_INFO, "Pool Light turn off for 12 seconds\n");
// Now need to turn off for between 11 and 14 seconds to reset light.
send_cmd(code, aq_data);
delay(12 * seconds);
// Now light is reset, pulse the appropiate number of times to advance program.
logMessage(LOG_INFO, "Pool Light button pulsing on/off %d times\n", val);
// Program light in safe mode (slowley), or quick mode
if (pmode > 0) {
for (i = 1; i < (val * 2); i++) {
logMessage(LOG_INFO, "Pool Light button press number %d - %s of %d\n", i, i % 2 == 0 ? "Off" : "On", val);
send_cmd(code, aq_data);
delay(pmode * seconds); // 0.3 works, but using 0.4 to be safe
}
} else {
for (i = 1; i < val; i++) {
logMessage(LOG_INFO, "Pool Light button press number %d - %s of %d\n", i, "ON", val);
send_cmd(code, aq_data);
waitForButtonState(aq_data, button, ON, 2);
logMessage(LOG_INFO, "Pool Light button press number %d - %s of %d\n", i, "OFF", val);
send_cmd(code, aq_data);
waitForButtonState(aq_data, button, OFF, 2);
}
logMessage(LOG_INFO, "Pool Light button press number %d - %s of %d\n", i, "ON", val);
send_cmd(code, aq_data);
}
//waitForButtonState(aq_data, &aq_data->aqbuttons[btn], ON, 2);
cleanAndTerminateThread(threadCtrl);
// just stop compiler error, ptr is not valid as it's just been freed
return ptr;
}
void *set_aqualink_pool_heater_temps( void *ptr )
{
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aq_data = threadCtrl->aq_data;
waitForSingleThreadOrTerminate(threadCtrl, AQ_SET_POOL_HEATER_TEMP);
int val = atoi((char*)threadCtrl->thread_args);
logMessage(LOG_DEBUG, "Setting pool heater setpoint to %d\n", val);
//setAqualinkTemp(aq_data, "SET TEMP", "SET POOL TEMP", NULL, "POOL", val);
if ( select_menu_item(aq_data, "SET TEMP") != true ) {
logMessage(LOG_WARNING, "Could not select SET TEMP menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
if (select_sub_menu_item(aq_data, "SET POOL TEMP") != true) {
logMessage(LOG_WARNING, "Could not select SET POOL TEMP menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
setAqualinkNumericField(aq_data, "POOL", val);
// usually miss this message, not sure why, but wait anyway to make sure programming has ended
waitForMessage(threadCtrl->aq_data, "POOL TEMP IS SET TO", 1);
cleanAndTerminateThread(threadCtrl);
// just stop compiler error, ptr is not valid as it's just been freed
return ptr;
}
void *set_aqualink_spa_heater_temps( void *ptr )
{
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aq_data = threadCtrl->aq_data;
waitForSingleThreadOrTerminate(threadCtrl, AQ_SET_SPA_HEATER_TEMP);
int val = atoi((char*)threadCtrl->thread_args);
logMessage(LOG_DEBUG, "Setting spa heater setpoint to %d\n", val);
//setAqualinkTemp(aq_data, "SET TEMP", "SET SPA TEMP", NULL, "SPA", val);
if ( select_menu_item(aq_data, "SET TEMP") != true ) {
logMessage(LOG_WARNING, "Could not select SET TEMP menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
if (select_sub_menu_item(aq_data, "SET SPA TEMP") != true) {
logMessage(LOG_WARNING, "Could not select SET SPA TEMP menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
setAqualinkNumericField(aq_data, "SPA", val);
// usually miss this message, not sure why, but wait anyway to make sure programming has ended
waitForMessage(threadCtrl->aq_data, "SPA TEMP IS SET TO", 1);
cleanAndTerminateThread(threadCtrl);
// just stop compiler error, ptr is not valid as it's just been freed
return ptr;
}
void *set_aqualink_freeze_heater_temps( void *ptr )
{
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aq_data = threadCtrl->aq_data;
waitForSingleThreadOrTerminate(threadCtrl, AQ_SET_FRZ_PROTECTION_TEMP);
int val = atoi((char*)threadCtrl->thread_args);
logMessage(LOG_DEBUG, "Setting sfreeze protection to %d\n", val);
//setAqualinkTemp(aq_data, "SYSTEM SETUP", "FRZ PROTECT", "TEMP SETTING", "FRZ", val);
if ( select_menu_item(aq_data, "SYSTEM SETUP") != true ) {
logMessage(LOG_WARNING, "Could not select SYSTEM SETUP menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
if (select_sub_menu_item(aq_data, "FRZ PROTECT") != true) {
logMessage(LOG_WARNING, "Could not select FRZ PROTECT menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
if (select_sub_menu_item(aq_data, "TEMP SETTING") != true) {
logMessage(LOG_WARNING, "Could not select TEMP SETTING menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
setAqualinkNumericField(aq_data, "FRZ", val);
waitForMessage(threadCtrl->aq_data, "FREEZE PROTECTION IS SET TO", 3);
cleanAndTerminateThread(threadCtrl);
// just stop compiler error, ptr is not valid as it's just been freed
return ptr;
}
void *set_aqualink_time( void *ptr )
{
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aq_data = threadCtrl->aq_data;
waitForSingleThreadOrTerminate(threadCtrl, AQ_SET_TIME);
logMessage(LOG_NOTICE, "Setting time on aqualink\n");
time_t now = time(0); // get time now
struct tm *result = localtime(&now);
char hour[11];
if (result->tm_hour == 0)
sprintf(hour, "HOUR 12 AM");
else if (result->tm_hour < 11)
sprintf(hour, "HOUR %d AM", result->tm_hour);
else if (result->tm_hour == 12)
sprintf(hour, "HOUR 12 PM");
else // Must be 13 or more
sprintf(hour, "HOUR %d PM", result->tm_hour - 12);
logMessage(LOG_DEBUG, "Setting time to %d/%d/%d %d:%d\n", result->tm_mon + 1, result->tm_mday, result->tm_year + 1900, result->tm_hour + 1, result->tm_min);
if ( select_menu_item(aq_data, "SET TIME") != true ) {
logMessage(LOG_WARNING, "Could not select SET TIME menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
setAqualinkNumericField(aq_data, "YEAR", result->tm_year + 1900);
setAqualinkNumericField(aq_data, "MONTH", result->tm_mon + 1);
setAqualinkNumericField(aq_data, "DAY", result->tm_mday);
//setAqualinkNumericFieldExtra(aq_data, "HOUR", 11, "PM");
select_sub_menu_item(aq_data, hour);
setAqualinkNumericField(aq_data, "MINUTE", result->tm_min);
send_cmd(KEY_ENTER, aq_data);
cleanAndTerminateThread(threadCtrl);
// just stop compiler error, ptr is not valid as it's just been freed
return ptr;
}
void *get_aqualink_diag_model( void *ptr )
{
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aq_data = threadCtrl->aq_data;
waitForSingleThreadOrTerminate(threadCtrl, AQ_GET_DIAGNOSTICS_MODEL);
if ( select_menu_item(aq_data, "SYSTEM SETUP") != true ) {
logMessage(LOG_WARNING, "Could not select HELP menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
if (select_sub_menu_item(aq_data, "DIAGNOSTICS") != true) {
logMessage(LOG_WARNING, "Could not select DIAGNOSTICS menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
waitForMessage(aq_data, NULL, 8); // Receive 8 messages
//8157 REV MMM | BATTERY OK | Cal: -27 0 6 | CONTROL PANEL #1 | CONTROL PANEL #3 | WATER SENSOR OK | AIR SENSOR OK | SOLAR SENSOR OPENED
cleanAndTerminateThread(threadCtrl);
// just stop compiler error, ptr is not valid as it's just been freed
return ptr;
}
void *get_aqualink_pool_spa_heater_temps( void *ptr )
{
//logMessage(LOG_DEBUG, "Could not select TEMP SET menu\n");
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aq_data = threadCtrl->aq_data;
waitForSingleThreadOrTerminate(threadCtrl, AQ_GET_POOL_SPA_HEATER_TEMPS);
logMessage(LOG_NOTICE, "Getting pool & spa heat setpoints from aqualink\n");
if ( select_menu_item(aq_data, "REVIEW") != true ) {
logMessage(LOG_WARNING, "Could not select REVIEW menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
if (select_sub_menu_item(aq_data, "TEMP SET") != true) {
logMessage(LOG_WARNING, "Could not select TEMP SET menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
// Should receive 'POOL TEMP IS SET TO xx' then 'SPA TEMP IS SET TO xx' then 'MAINTAIN TEMP IS (OFF|ON)'
// wait for the last message
waitForMessage(threadCtrl->aq_data, "MAINTAIN TEMP IS", 5);
//cancel_menu(threadCtrl->aq_data);
cleanAndTerminateThread(threadCtrl);
// just stop compiler error, ptr is not valid as it's just been freed
return ptr;
}
void *get_freeze_protect_temp( void *ptr )
{
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aq_data = threadCtrl->aq_data;
waitForSingleThreadOrTerminate(threadCtrl, AQ_GET_FREEZE_PROTECT_TEMP);
logMessage(LOG_NOTICE, "Getting freeze protection setpoints\n");
if ( select_menu_item(aq_data, "REVIEW") != true ) {
logMessage(LOG_WARNING, "Could not select REVIEW menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
if (select_sub_menu_item(aq_data, "FRZ PROTECT") != true) {
logMessage(LOG_WARNING, "Could not select FRZ PROTECT menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
waitForMessage(aq_data, "FREEZE PROTECTION IS SET TO", 3);
//cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
// just stop compiler error, ptr is not valid as it's just been freed
return ptr;
}
bool get_aqualink_program_for_button(struct aqualinkdata *aq_data, unsigned char cmd)
{
int rtnStringsWait = 7;
if (! waitForMessage(aq_data, "SELECT DEVICE TO REVIEW or PRESS ENTER TO END", rtnStringsWait))
return false;
logMessage(LOG_DEBUG, "Send key '%d'\n",cmd);
send_cmd(cmd, aq_data);
return waitForEitherMessage(aq_data, "NOT SET", "TURNS ON", rtnStringsWait);
}
void *get_aqualink_programs( void *ptr )
{
struct programmingThreadCtrl *threadCtrl;
threadCtrl = (struct programmingThreadCtrl *) ptr;
struct aqualinkdata *aq_data = threadCtrl->aq_data;
char keys[10] = {KEY_PUMP, KEY_SPA, KEY_AUX1, KEY_AUX2, KEY_AUX3, KEY_AUX4, KEY_AUX5}; // KEY_AUX6 & KEY_AUX7 kill programming mode
waitForSingleThreadOrTerminate(threadCtrl, AQ_GET_PROGRAMS);
if ( select_menu_item(aq_data, "REVIEW") != true ) {
//logMessage(LOG_WARNING, "Could not select REVIEW menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
if (select_sub_menu_item(aq_data, "PROGRAMS") != true) {
//logMessage(LOG_WARNING, "Could not select PROGRAMS menu\n");
cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
int i;
for (i=0; i < strlen(keys); i++) {
//logMessage(LOG_DEBUG, "**** START program for key in loop %d\n",i);
if (! get_aqualink_program_for_button(aq_data, keys[i])) {
//logMessage(LOG_DEBUG, "**** Didn't find program for key in loop %d\n",i);
//cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
return ptr;
}
//logMessage(LOG_DEBUG, "**** Found program for key in loop %d\n",i);
}
if (waitForMessage(aq_data, "SELECT DEVICE TO REVIEW or PRESS ENTER TO END", 6)) {
//logMessage(LOG_DEBUG, "Send key ENTER\n");
send_cmd(KEY_ENTER, aq_data);
} else {
//logMessage(LOG_DEBUG, "Send CANCEL\n");
cancel_menu(aq_data);
}
//cancel_menu(aq_data);
cleanAndTerminateThread(threadCtrl);
// just stop compiler error, ptr is not valid as it's just been freed
return ptr;
}
void send_cmd(unsigned char cmd, struct aqualinkdata *aq_data)
{
int i=0;
// If there is an unsent command, wait.
while ( (aq_data->aq_command != NUL) && ( i++ < 10) ) {
//sleep(1); // NSF Change to smaller time.
delay(500);
}
aq_data->aq_command = cmd;
//delay(200);
logMessage(LOG_DEBUG, "Sent '0x%02hhx' to controller\n", aq_data->aq_command);
}
void cancel_menu(struct aqualinkdata *aq_data)
{
send_cmd(KEY_CANCEL, aq_data);
}
/*
* added functionality, if start of string is ^ use that as must start with in comparison
*/
bool waitForEitherMessage(struct aqualinkdata *aq_data, char* message1, char* message2, int numMessageReceived)
{
//logMessage(LOG_DEBUG, "waitForMessage %s %d %d\n",message,numMessageReceived,cmd);
int i=0;
pthread_mutex_init(&aq_data->active_thread.thread_mutex, NULL);
pthread_mutex_lock(&aq_data->active_thread.thread_mutex);
char* msgS1;
char* msgS2;
char* ptr;
if (message1 != NULL) {
if (message1[0] == '^')
msgS1 = &message1[1];
else
msgS1 = message1;
}
if (message2 != NULL) {
if (message2[0] == '^')
msgS2 = &message2[1];
else
msgS2 = message2;
}
while( ++i <= numMessageReceived)
{
logMessage(LOG_DEBUG, "Programming mode: loop %d of %d looking for '%s' OR '%s' received message1 '%s'\n",i,numMessageReceived,message1,message2,aq_data->last_message);
if (message1 != NULL) {
ptr = strstr(aq_data->last_message, msgS1);
if (ptr != NULL) { // match
if (msgS1 == message1) // match & don't care if first char
break;
else if (ptr == aq_data->last_message) // match & do care if first char
break;
}
}
if (message2 != NULL) {
ptr = strstr(aq_data->last_message, msgS2);
if (ptr != NULL) { // match
if (msgS2 == message2) // match & don't care if first char
break;
else if (ptr == aq_data->last_message) // match & do care if first char
break;
}
}
//logMessage(LOG_DEBUG, "Programming mode: looking for '%s' received message1 '%s'\n",message1,aq_data->last_message);
pthread_cond_init(&aq_data->active_thread.thread_cond, NULL);
pthread_cond_wait(&aq_data->active_thread.thread_cond, &aq_data->active_thread.thread_mutex);
//logMessage(LOG_DEBUG, "Programming mode: loop %d of %d looking for '%s' received message1 '%s'\n",i,numMessageReceived,message1,aq_data->last_message);
}
pthread_mutex_unlock(&aq_data->active_thread.thread_mutex);
if (message1 != NULL && message2 != NULL && ptr == NULL) {
//logmessage1(LOG_ERR, "Could not select MENU of Aqualink control panel\n");
logMessage(LOG_DEBUG, "Programming mode: did not find '%s'\n",message1);
return false;
}
logMessage(LOG_DEBUG, "Programming mode: found message1 '%s' or '%s' in '%s'\n",message1,message2,aq_data->last_message);
return true;
}
bool waitForMessage(struct aqualinkdata *aq_data, char* message, int numMessageReceived)
{
//logMessage(LOG_DEBUG, "waitForMessage %s %d %d\n",message,numMessageReceived,cmd);
int i=0;
pthread_mutex_init(&aq_data->active_thread.thread_mutex, NULL);
pthread_mutex_lock(&aq_data->active_thread.thread_mutex);
char* msgS;
char* ptr;
if (message != NULL) {
if (message[0] == '^')
msgS = &message[1];
else
msgS = message;
}
while( ++i <= numMessageReceived)
{
logMessage(LOG_DEBUG, "Programming mode: loop %d of %d looking for '%s' received message '%s'\n",i,numMessageReceived,message,aq_data->last_message);
if (message != NULL) {
ptr = strstr(aq_data->last_message, msgS);
if (ptr != NULL) { // match
if (msgS == message) // match & don't care if first char
break;
else if (ptr == aq_data->last_message) // match & do care if first char
break;
}
}
//logMessage(LOG_DEBUG, "Programming mode: looking for '%s' received message '%s'\n",message,aq_data->last_message);
pthread_cond_init(&aq_data->active_thread.thread_cond, NULL);
pthread_cond_wait(&aq_data->active_thread.thread_cond, &aq_data->active_thread.thread_mutex);
//logMessage(LOG_DEBUG, "Programming mode: loop %d of %d looking for '%s' received message '%s'\n",i,numMessageReceived,message,aq_data->last_message);
}
pthread_mutex_unlock(&aq_data->active_thread.thread_mutex);
if (message != NULL && ptr == NULL) {
//logMessage(LOG_ERR, "Could not select MENU of Aqualink control panel\n");
logMessage(LOG_DEBUG, "Programming mode: did not find '%s'\n",message);
return false;
}
logMessage(LOG_DEBUG, "Programming mode: found message '%s' in '%s'\n",message,aq_data->last_message);
return true;
}
bool select_menu_item(struct aqualinkdata *aq_data, char* item_string)
{
char* expectedMsg = "PRESS ENTER* TO SELECT";
int wait_messages = 6;
//int i=0;
// Select the MENU and wait to get the RS8 respond.
send_cmd(KEY_MENU, aq_data);
if (waitForMessage(aq_data, expectedMsg, wait_messages) == false)
return false;
send_cmd(KEY_ENTER, aq_data);
waitForMessage(aq_data, NULL, 1);
// Blindly wait for next message
//sendCmdWaitForReturn(aq_data, KEY_ENTER);
// Can't determin the first response
//delay(500);
return select_sub_menu_item(aq_data, item_string);
}
//bool select_sub_menu_item(char* item_string, struct aqualinkdata *aq_data)
bool select_sub_menu_item(struct aqualinkdata *aq_data, char* item_string)
{
int wait_messages = 25;
int i=0;
while( (strstr(aq_data->last_message, item_string) == NULL) && ( i++ < wait_messages) )
{
send_cmd(KEY_RIGHT, aq_data);
waitForMessage(aq_data, NULL, 1);
logMessage(LOG_DEBUG, "Find item in Menu: loop %d of %d looking for '%s' received message '%s'\n",i,wait_messages,item_string,aq_data->last_message);
}
if (strstr(aq_data->last_message, item_string) == NULL) {
//logMessage(LOG_ERR, "Could not select SUB_MENU of Aqualink control panel\n");
return false;
}
logMessage(LOG_DEBUG, "Programming mode: found menu item '%s'\n", item_string);
// Enter the mode specified by the argument.
send_cmd(KEY_ENTER, aq_data);
waitForMessage(aq_data, NULL, 1);
//sendCmdWaitForReturn(aq_data, KEY_ENTER);
return true;
}
// NSF Need to test this, then use it for the color change mode.
bool waitForButtonState(struct aqualinkdata *aq_data, aqkey* button, aqledstate state, int numMessageReceived)
{
//logMessage(LOG_DEBUG, "waitForMessage %s %d %d\n",message,numMessageReceived,cmd);
int i=0;
pthread_mutex_init(&aq_data->active_thread.thread_mutex, NULL);
pthread_mutex_lock(&aq_data->active_thread.thread_mutex);
while( ++i <= numMessageReceived)
{
logMessage(LOG_DEBUG, "Programming mode: loop %d of %d looking for state change to '%d' for '%s' \n",i,numMessageReceived,button->led->state,button->name);
if (button->led->state == state) {
break;
}
//logMessage(LOG_DEBUG, "Programming mode: looking for '%s' received message '%s'\n",message,aq_data->last_message);
pthread_cond_init(&aq_data->active_thread.thread_cond, NULL);
pthread_cond_wait(&aq_data->active_thread.thread_cond, &aq_data->active_thread.thread_mutex);
//logMessage(LOG_DEBUG, "Programming mode: loop %d of %d looking for '%s' received message '%s'\n",i,numMessageReceived,message,aq_data->last_message);
}
pthread_mutex_unlock(&aq_data->active_thread.thread_mutex);
if (numMessageReceived >= i) {
//logMessage(LOG_ERR, "Could not select MENU of Aqualink control panel\n");
logMessage(LOG_DEBUG, "Programming mode: did not find state '%d' for '%s'\n",button->led->state,button->name);
return false;
}
logMessage(LOG_DEBUG, "Programming mode: found state '%d' for '%s'\n",button->led->state,button->name);
return true;
}

36
aq_programmer.h Normal file
View File

@ -0,0 +1,36 @@
#ifndef AQ_PROGRAMMER_H_
#define AQ_PROGRAMMER_H_
typedef enum {
AQP_NULL = -1,
AQ_GET_POOL_SPA_HEATER_TEMPS,
AQ_GET_FREEZE_PROTECT_TEMP,
AQ_SET_TIME,
AQ_SET_POOL_HEATER_TEMP,
AQ_SET_SPA_HEATER_TEMP,
AQ_SET_FRZ_PROTECTION_TEMP,
AQ_GET_DIAGNOSTICS_MODEL,
AQ_SEND_CMD,
AQ_GET_PROGRAMS,
AQ_SET_COLORMODE,
} program_type;
struct programmingThreadCtrl {
pthread_t thread_id;
//void *thread_args;
char thread_args[20];
struct aqualinkdata *aq_data;
};
//void aq_programmer(program_type type, void *args, struct aqualinkdata *aq_data);
void aq_programmer(program_type type, char *args, struct aqualinkdata *aq_data);
void kick_aq_program_thread(struct aqualinkdata *aq_data);
//void send_cmd(unsigned char cmd, struct aqualinkdata *aq_data);
//void cancel_menu(struct aqualinkdata *aq_data);
//void *set_aqualink_time( void *ptr );
//void *get_aqualink_pool_spa_heater_temps( void *ptr );
#endif

BIN
aq_programmer.o Normal file

Binary file not shown.

306
aq_serial.c Normal file
View File

@ -0,0 +1,306 @@
/*
* Copyright (c) 2017 Shaun Feakes - All rights reserved
*
* You may use redistribute and/or modify this code under the terms of
* the GNU General Public License version 2 as published by the
* Free Software Foundation. For the terms of this license,
* see <http://www.gnu.org/licenses/>.
*
* You are free to use this software under the terms of the GNU General
* Public License, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* https://github.com/sfeakes/aqualinkd
*/
#include <stdio.h>
#include <stdarg.h>
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include "aq_serial.h"
#include "utils.h"
//#define BLOCKING_MODE
static struct termios _oldtio;
void log_packet(unsigned char* packet, int length)
{
int i;
char temp_string[64];
char message_buffer[MAXLEN];
sprintf(temp_string, "%02x ", packet[0]);
strcpy(message_buffer, temp_string);
for (i = 1; i < length; i++) {
sprintf(temp_string, "%02x ", packet[i]);
strcat(message_buffer, temp_string);
}
strcat(message_buffer, "\n");
logMessage(LOG_DEBUG, message_buffer);
}
/*
Open and Initialize the serial communications port to the Aqualink RS8 device.
Arg is tty or port designation string
returns the file descriptor
*/
int init_serial_port(char* tty)
{
long BAUD = B9600;
long DATABITS = CS8;
long STOPBITS = 0;
long PARITYON = 0;
long PARITY = 0;
struct termios newtio; //place for old and new port settings for serial port
//int fd = open(tty, O_RDWR | O_NOCTTY | O_NONBLOCK);
int fd = open(tty, O_RDWR | O_NOCTTY | O_NONBLOCK | O_NDELAY);
if (fd < 0) {
logMessage(LOG_ERR, "Unable to open port: %s\n", tty);
return -1;
}
#ifdef BLOCKING_MODE
fcntl(fd, F_SETFL, 0);
newtio.c_cc[VMIN]= 1;
newtio.c_cc[VTIME]= 0;
#else
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK | O_NDELAY);
newtio.c_cc[VMIN]= 0;
newtio.c_cc[VTIME]= 1;
#endif
tcgetattr(fd, &_oldtio); // save current port settings
// set new port settings for canonical input processing
newtio.c_cflag = BAUD | DATABITS | STOPBITS | PARITYON | PARITY | CLOCAL | CREAD;
newtio.c_iflag = IGNPAR;
newtio.c_lflag = 0; // ICANON;
newtio.c_oflag = 0;
tcflush(fd, TCIFLUSH);
tcsetattr(fd, TCSANOW, &newtio);
return fd;
}
/* close tty port */
void close_serial_port(int fd)
{
tcsetattr(fd, TCSANOW, &_oldtio);
close(fd);
}
// Generate and return checksum of packet.
int generate_checksum(unsigned char* packet, int length)
{
int i, sum, n;
n = length - 3;
sum = 0;
for (i = 0; i < n; i++)
sum += (int) packet[i];
return(sum & 0x0ff);
}
// Send an ack packet to the Aqualink RS8 master device.
// fd: the file descriptor of the serial port connected to the device
// command: the command byte to send to the master device, NUL if no command
//
// NUL = '\x00'
// DLE = '\x10'
// STX = '\x02'
// ETX = '\x03'
//
// masterAddr = '\x00' # address of Aqualink controller
//
//msg = DLE+STX+dest+cmd+args
//msg = msg+self.checksum(msg)+DLE+ETX
// DLE+STX+DEST+CMD+ARGS+CHECKSUM+DLE+ETX
void print_hex(char *pk, int length)
{
int i=0;
for (i=0;i<length;i++)
{
printf("0x%02hhx|",pk[i]);
}
printf("\n");
}
void test_cmd()
{
const int length = 11;
unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, CMD_ACK, NUL, NUL, 0x13, DLE, ETX, NUL };
//send_cmd(fd, CMD_ACK, command);
print_hex((char *)ackPacket, length);
ackPacket[7] = generate_checksum(ackPacket, length-1);
print_hex((char *)ackPacket, length);
ackPacket[6] = 0x02;
ackPacket[7] = generate_checksum(ackPacket, length-1);
print_hex((char *)ackPacket, length);
}
void send_ack(int fd, unsigned char command)
{
const int length = 11;
unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, CMD_ACK, NUL, NUL, 0x13, DLE, ETX, NUL };
//unsigned char ackPacket[] = { NUL, DLE, STX, DEV_MASTER, NUL, NUL, NUL, 0x13, DLE, ETX, NUL };
// Update the packet and checksum if command argument is not NUL.
if(command != NUL) {
ackPacket[6] = command;
ackPacket[7] = generate_checksum(ackPacket, length-1);
// NULL out the command byte if it is the same. Difference implies that
// a new command has come in, and is awaiting processing.
/*
if(aqualink_cmd == command) {
aqualink_cmd = NUL;
}
*/
log_packet(ackPacket, length);
// In debug mode, log the packet to the private log file.
//log_packet(ackPacket, length);
}
// Send the packet to the master device.
//write(fd, ackPacket, length);
#ifdef BLOCKING_MODE
write(fd, ackPacket, length);
#else
int nwrite, i;
for (i=0; i<length; i += nwrite) {
nwrite = write(fd, ackPacket + i, length - i);
if (nwrite < 0)
logMessage(LOG_ERR, "write to serial port failed\n");
}
//tcdrain(fd);
#endif
}
// Reads the bytes of the next incoming packet, and
// returns when a good packet is available in packet
// fd: the file descriptor to read the bytes from
// packet: the unsigned char buffer to store the bytes in
// returns the length of the packet
int get_packet(int fd, unsigned char* packet)
{
unsigned char byte;
int bytesRead;
int index = 0;
int endOfPacket = FALSE;
int packetStarted = FALSE;
int foundDLE = FALSE;
bool started = FALSE;
while (!endOfPacket) {
//printf("Read loop %d\n",++i);
bytesRead = read(fd, &byte, 1);
if (bytesRead < 0 && errno == EAGAIN && started == FALSE) {
// We just have nothing to read
return 0;
} else if (bytesRead < 0 && errno == EAGAIN) {
// If we are in the middle of reading a packet, keep going
delay(10);
} else if (bytesRead == 1) {
started = TRUE;
//if (bytesRead == 1) {
if (byte == DLE) {
// Found a DLE byte. Set the flag, and record the byte.
foundDLE = TRUE;
packet[index] = byte;
}
else if (byte == STX && foundDLE == TRUE) {
// Found the DLE STX byte sequence. Start of packet detected.
// Reset the DLE flag, and record the byte.
foundDLE = FALSE;
packetStarted = TRUE;
packet[index] = byte;
}
else if (byte == NUL && foundDLE == TRUE) {
// Found the DLE NUL byte sequence. Detected a delimited data byte.
// Reset the DLE flag, and decrement the packet index to offset the
// index increment at the end of the loop. The delimiter, [NUL], byte
// is not recorded.
foundDLE = FALSE;
//trimmed = true;
index--;
}
else if (byte == ETX && foundDLE == TRUE) {
// Found the DLE ETX byte sequence. End of packet detected.
// Reset the DLE flag, set the end of packet flag, and record
// the byte.
foundDLE = FALSE;
packetStarted = FALSE;
endOfPacket = TRUE;
packet[index] = byte;
}
else if (packetStarted == TRUE) {
// Found a data byte. Reset the DLE flag just in case it is set
// to prevent anomalous detections, and record the byte.
foundDLE = FALSE;
packet[index] = byte;
}
else {
// Found an extraneous byte. Probably a NUL between packets.
// Ignore it, and decrement the packet index to offset the
// index increment at the end of the loop.
index--;
}
// Finished processing the byte. Increment the packet index for the
// next byte.
index++;
// Break out of the loop if we exceed maximum packet
// length.
if (index >= AQ_MAXPKTLEN) {
break;
}
}
else if(bytesRead < 0) {
// Got a read error. Wait one millisecond for the next byte to
// arrive.
logMessage(LOG_WARNING, "Read error: %d - %s\n", errno, strerror(errno));
if(errno == 9) {
// Bad file descriptor. Port has been disconnected for some reason.
// Return a -1.
return -1;
}
delay(100);
}
}
// Return the packet length.
return index;
}

131
aq_serial.h Normal file
View File

@ -0,0 +1,131 @@
#ifndef AQ_SERIAL_H_
#define AQ_SERIAL_H_
#include <termios.h>
// packet offsets
#define PKT_DEST 2
#define PKT_CMD 3
#define PKT_DATA 4
#define PKT_STATUS_BYTES 5
#define DEV_MASTER 0
// PACKET DEFINES
#define NUL 0x00
#define DLE 0x10
#define STX 0x02
#define ETX 0x03
#define AQ_MINPKTLEN 5
#define AQ_MAXPKTLEN 64
#define AQ_PSTLEN 5
#define AQ_MSGLEN 16
#define AQ_MSGLONGLEN 128
#define AQ_TADLEN 13
/* COMMANDS */
#define CMD_PROBE 0x00
#define CMD_ACK 0x01
#define CMD_STATUS 0x02
#define CMD_MSG 0x03
#define CMD_MSG_LONG 0x04
/* KEY/BUTTON CODES */
#define KEY_PUMP 0x02
#define KEY_SPA 0x01
#define KEY_AUX1 0x05
#define KEY_AUX2 0x0a
#define KEY_AUX3 0x0f
#define KEY_AUX4 0x06
#define KEY_AUX5 0x0b
#define KEY_AUX6 0x10
#define KEY_AUX7 0x15
#define KEY_POOL_HTR 0x12
#define KEY_SPA_HTR 0x17
#define KEY_SOLAR_HTR 0x1c
#define KEY_MENU 0x09
#define KEY_CANCEL 0x0e
#define KEY_LEFT 0x13
#define KEY_RIGHT 0x18
#define KEY_HOLD 0x19
#define KEY_OVERRIDE 0x1e
#define KEY_ENTER 0x1d
#define BTN_PUMP "Filter_Pump"
#define BTN_SPA "Spa_Mode"
#define BTN_AUX1 "Aux_1"
#define BTN_AUX2 "Aux_2"
#define BTN_AUX3 "Aux_3"
#define BTN_AUX4 "Aux_4"
#define BTN_AUX5 "Aux_5"
#define BTN_AUX6 "Aux_6"
#define BTN_AUX7 "Aux_7"
#define BTN_POOL_HTR "Pool_Heater"
#define BTN_SPA_HTR "Spa_Heater"
#define BTN_SOLAR_HTR "Solar_Heater"
#define BUTTON_LABEL_LENGTH 20
#define TOTAL_LEDS 20
// Index starting at 1
#define POOL_HTR_LED_INDEX 15
#define SPA_HTR_LED_INDEX 17
#define SOLAR_HTR_LED_INDEX 19
#define LNG_MSG_SERVICE_ACTIVE "SERVICE MODE IS ACTIVE"
#define LNG_MSG_POOL_TEMP_SET "POOL TEMP IS SET TO"
#define LNG_MSG_SPA_TEMP_SET "SPA TEMP IS SET TO"
#define LNG_MSG_FREEZE_PROTECTION_SET "FREEZE PROTECTION IS SET TO"
#define LNG_MSG_CLEANER_DELAY "CLEANER WILL TURN ON AFTER SAFETY DELAY"
#define LNG_MSG_BATTERY_LOW "BATTERY LOW"
#define MSG_AIR_TEMP "AIR TEMP"
#define MSG_POOL_TEMP "POOL TEMP"
#define MSG_SPA_TEMP "SPA TEMP"
#define MSG_AIR_TEMP_LEN 8
#define MSG_POOL_TEMP_LEN 9
#define MSG_SPA_TEMP_LEN 8
typedef enum {
ON,
OFF,
FLASH,
ENABLE,
LED_S_UNKNOWN
} aqledstate;
typedef struct aqualinkled
{
//int number;
aqledstate state;
} aqled;
// Battery Status Identifiers
enum {
OK = 0,
LOW
};
int init_serial_port(char* tty);
void close_serial_port(int file_descriptor);
int generate_checksum(unsigned char* packet, int length);
void send_ack(int file_descriptor, unsigned char command);
//void send_cmd(int file_descriptor, unsigned char cmd, unsigned char args);
int get_packet(int file_descriptor, unsigned char* packet);
//void close_serial_port(int file_descriptor, struct termios* oldtio);
//void process_status(void const * const ptr);
void process_status(unsigned char* ptr);
#endif // AQ_SERIAL_H_

BIN
aq_serial.o Normal file

Binary file not shown.

89
aqualink.h Normal file
View File

@ -0,0 +1,89 @@
#ifndef AQUALINK_H_
#define AQUALINK_H_
#include <pthread.h>
#include "aq_serial.h"
#include "aq_programmer.h"
#define TIME_CHECK_INTERVAL 3600
//#define TIME_CHECK_INTERVAL 600
#define ACCEPTABLE_TIME_DIFF 120
#define MAX_ZERO_READ_BEFORE_RECONNECT 500
#define TOTAL_BUTTONS 12
#define TEMP_UNKNOWN -999
#define DATE_STRING_LEN 30
enum {
FAHRENHEIT,
CELSIUS,
UNKNOWN
};
typedef struct aqualinkkey
{
//int number;
//aqledstate *state;
aqled *led;
char *label;
char *name;
unsigned char code;
int dz_idx;
} aqkey;
//typedef struct ProgramThread ProgramThread; // Definition is later
struct programmingthread {
pthread_t *thread_id;
pthread_mutex_t thread_mutex;
pthread_cond_t thread_cond;
program_type ptype;
//void *thread_args;
};
typedef enum action_type {
NO_ACTION = -1,
POOL_HTR_SETOINT,
SPA_HTR_SETOINT,
FREEZE_SETPOINT,
} action_type;
struct action {
action_type type;
time_t requested;
int value;
//char value[10];
};
struct aqualinkdata
{
//char crap[AQ_MSGLEN];
char version[AQ_MSGLEN];
char date[AQ_MSGLEN];
char time[AQ_MSGLEN];
//char datestr[DATE_STRING_LEN];
//char message[AQ_MSGLONGLEN];
char *last_message; // Be careful using this, can get core dumps.
unsigned char raw_status[AQ_PSTLEN];
aqled aqualinkleds[TOTAL_LEDS];
aqkey aqbuttons[TOTAL_BUTTONS];
int air_temp;
int pool_temp;
int spa_temp;
int temp_units;
int battery;
//int freeze_protection;
int frz_protect_set_point;
int pool_htr_set_point;
int spa_htr_set_point;
unsigned char aq_command;
struct programmingthread active_thread;
struct action unactioned;
};
#endif

532
aqualinkd.c Normal file
View File

@ -0,0 +1,532 @@
/*
* Copyright (c) 2017 Shaun Feakes - All rights reserved
*
* You may use redistribute and/or modify this code under the terms of
* the GNU General Public License version 2 as published by the
* Free Software Foundation. For the terms of this license,
* see <http://www.gnu.org/licenses/>.
*
* You are free to use this software under the terms of the GNU General
* Public License, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* https://github.com/sfeakes/aqualinkd
*/
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <libgen.h>
#include <termios.h>
#include <signal.h>
#define _GNU_SOURCE 1
#define __USE_XOPEN 1
#include <time.h> // Need GNU_SOURCE & XOPEN defined for strptime
#include "mongoose.h"
#include "aqualink.h"
#include "utils.h"
#include "config.h"
#include "aq_serial.h"
#include "init_buttons.h"
#include "aq_programmer.h"
#include "net_services.h"
#include "version.h"
#define DEFAULT_CONFIG_FILE "./aqualinkd.conf"
static volatile bool _keepRunning = true;
static struct aqconfig _config_parameters;
static struct aqualinkdata _aqualink_data;
void main_loop();
void intHandler(int dummy) {
_keepRunning = false;
logMessage(LOG_NOTICE, "Stopping!");
}
void processLEDstate()
{
int i=0;
int byte;
int bit;
for (byte=0;byte<5;byte++)
{
for (bit=0; bit<8; bit+=2)
{
if ( ((_aqualink_data.raw_status[byte] >> (bit+1) ) & 1) == 1 )
_aqualink_data.aqualinkleds[i].state = FLASH;
else if ( ((_aqualink_data.raw_status[byte] >> bit) & 1) == 1 )
_aqualink_data.aqualinkleds[i].state = ON;
else
_aqualink_data.aqualinkleds[i].state = OFF;
//logMessage(LOG_DEBUG,"Led %d state %d",i+1,_aqualink_data.aqualinkleds[i].state);
i++;
}
}
// Reset enabled state for heaters, as they take 2 led states
if ( _aqualink_data.aqualinkleds[POOL_HTR_LED_INDEX-1].state == OFF && _aqualink_data.aqualinkleds[POOL_HTR_LED_INDEX].state == ON)
_aqualink_data.aqualinkleds[POOL_HTR_LED_INDEX-1].state = ENABLE;\
if ( _aqualink_data.aqualinkleds[SPA_HTR_LED_INDEX-1].state == OFF && _aqualink_data.aqualinkleds[SPA_HTR_LED_INDEX].state == ON)
_aqualink_data.aqualinkleds[SPA_HTR_LED_INDEX-1].state = ENABLE;
if ( _aqualink_data.aqualinkleds[SOLAR_HTR_LED_INDEX-1].state == OFF && _aqualink_data.aqualinkleds[SOLAR_HTR_LED_INDEX].state == ON)
_aqualink_data.aqualinkleds[SOLAR_HTR_LED_INDEX-1].state = ENABLE;
}
bool checkAqualinkTime()
{
static time_t last_checked;
time_t now = time(0); // get time now
int time_difference;
struct tm aq_tm;
time_t aqualink_time;
time_difference = (int)difftime(now, last_checked);
if (time_difference < TIME_CHECK_INTERVAL) {
logMessage(LOG_DEBUG, "time not checked, will check in %d seconds", TIME_CHECK_INTERVAL - time_difference);
return TRUE;
} else {
last_checked = now;
//return FALSE;
}
char datestr[DATE_STRING_LEN];
strcpy(&datestr[0], _aqualink_data.date);
strcpy(&datestr[12], " ");
strcpy(&datestr[13], _aqualink_data.time);
if (strptime(datestr, "%m/%d/%y %a %I:%M %p", &aq_tm) == NULL) {
logMessage(LOG_ERR, "Could not convert RS time string '%s'", datestr);
last_checked = (time_t)NULL;
return TRUE;
}
aq_tm.tm_isdst = -1; // Force mktime to use local timezone
aqualink_time = mktime(&aq_tm);
time_difference = (int)difftime(now, aqualink_time);
logMessage(LOG_INFO, "Aqualink time is off by %d seconds...\n", time_difference);
if(abs(time_difference) <= ACCEPTABLE_TIME_DIFF) {
// Time difference is less than or equal to 90 seconds (1 1/2 minutes).
// Set the return value to TRUE.
return TRUE;
}
/*
char datestring[256];
//time_t t = time(0); // get time now
struct tm * tm = localtime( & now );
strftime (datestring, sizeof(datestring), "%m/%d/%y %a %I:%M %p %z", tm);
printf("Computer '%s'\n",datestring);
strftime (datestring, sizeof(datestring), "%m/%d/%y %a %I:%M %p %z", &aq_tm);
printf("Aqualink '%s'\n",datestring);
printf("test '%s'\n",datestring);
*/
return FALSE;
}
/*
void aqualink_strcpy(char *dest, char *src)
{
int i;
for (i=0; i<strlen(src); i++) {
if (src[i] > 125 || src[i] < 32 || src[i] == 34 || src[i] == 42 || src[i] == 96 || src[i] == 39 || src[i] == 92)
dest[i] = 32;
else
dest[i] = src[i];
}
dest[i] = '\0';
//dest[10] = '\0';
}
*/
void queueGetProgramData()
{
//aq_programmer(AQ_GET_DIAGNOSTICS_MODEL, NULL, &_aqualink_data);
// Init string good time to get setpoints
aq_programmer(AQ_GET_POOL_SPA_HEATER_TEMPS, NULL, &_aqualink_data);
aq_programmer(AQ_GET_FREEZE_PROTECT_TEMP, NULL, &_aqualink_data);
//aq_programmer(AQ_GET_PROGRAMS, NULL, &_aqualink_data); // only displays to log at present, also seems to confuse getting set_points
}
void processMessage(char *message)
{
char *msg;
static bool _initWithRS = false;
static bool _gotREV = false;
// NSF replace message with msg
msg = cleanwhitespace(message);
_aqualink_data.last_message = msg;
//aqualink_strcpy(_aqualink_data.message, msg);
logMessage(LOG_DEBUG, "RS Message :- '%s'\n",msg);
// Check long messages in this if/elseif block first, as some messages are similar.
// ie "POOL TEMP" and "POOL TEMP IS SET TO" so want correct match first.
//
if(strstr(message, LNG_MSG_BATTERY_LOW) != NULL) {
_aqualink_data.battery = LOW;
}
else if(strstr(message, LNG_MSG_POOL_TEMP_SET) != NULL) {
//logMessage(LOG_DEBUG, "pool htr long message: %s", &message[20]);
_aqualink_data.pool_htr_set_point = atoi(message+20);
}
else if(strstr(message, LNG_MSG_SPA_TEMP_SET) != NULL) {
//logMessage(LOG_DEBUG, "spa htr long message: %s", &message[19]);
_aqualink_data.spa_htr_set_point = atoi(message+19);
}
else if(strstr(message, LNG_MSG_FREEZE_PROTECTION_SET) != NULL) {
//logMessage(LOG_DEBUG, "frz protect long message: %s", &message[28]);
_aqualink_data.frz_protect_set_point = atoi(message+28);
}
else if(strncmp(msg, MSG_AIR_TEMP, MSG_AIR_TEMP_LEN) == 0) {
_aqualink_data.air_temp = atoi(msg+8);
if (msg[strlen(msg)-1] == 'F')
_aqualink_data.temp_units = FAHRENHEIT;
else if (msg[strlen(msg)-1] == 'C')
_aqualink_data.temp_units = CELSIUS;
else
_aqualink_data.temp_units = UNKNOWN;
}
else if(strncmp(message, MSG_POOL_TEMP, MSG_POOL_TEMP_LEN) == 0) {
_aqualink_data.pool_temp = atoi(message+9);
}
else if(strncmp(message, MSG_SPA_TEMP, MSG_SPA_TEMP_LEN) == 0) {
_aqualink_data.spa_temp = atoi(message+8);
}
else if(msg[2] == '/' && msg[5] == '/' && msg[8] == ' ') {// date in format '08/29/16 MON'
strcpy(_aqualink_data.date, msg);
}
else if( (msg[1] == ':' || msg[2] == ':') && msg[strlen(msg)-1] == 'M') { // time in format '9:45 AM'
strcpy(_aqualink_data.time, msg);
// Setting time takes a long time, so don't try until we have all other programmed data.
if ( (_initWithRS == true) && strlen(_aqualink_data.date) > 1 && checkAqualinkTime() != TRUE ) {
logMessage(LOG_NOTICE, "RS time is NOT accurate '%s %s', re-setting on controller!\n", _aqualink_data.time, _aqualink_data.date);
aq_programmer(AQ_SET_TIME, NULL, &_aqualink_data);
} else {
logMessage(LOG_DEBUG, "RS time is accurate '%s %s'\n", _aqualink_data.time, _aqualink_data.date);
}
// If we get a time message before REV, the controller didn't see us as we started too quickly.
if ( _gotREV == false ) {
logMessage(LOG_NOTICE, "Getting control panel information\n",msg);
aq_programmer(AQ_GET_DIAGNOSTICS_MODEL, NULL, &_aqualink_data);
_gotREV = true; // Force it to true just incase we don't understand the model#
}
}
else if(strstr(msg, " REV ") != NULL) { // '8157 REV MMM'
// A master firmware revision message.
strcpy(_aqualink_data.version, msg);
_gotREV = true;
logMessage(LOG_NOTICE, "Control Panel %s\n",msg);
if ( _initWithRS == false) {
queueGetProgramData();
_initWithRS = true;
}
}
else if(strstr(msg, " TURNS ON") != NULL) {
logMessage(LOG_NOTICE, "Program data '%s'\n",msg);
}
else {
logMessage(LOG_DEBUG, "Ignoring '%s'\n",msg);
}
// We processed the next message, kick any threads waiting on the message.
kick_aq_program_thread(&_aqualink_data);
}
bool process_packet(unsigned char* packet, int length)
{
bool rtn = FALSE;
static unsigned char last_packet[AQ_MAXPKTLEN];
static char message[AQ_MSGLONGLEN+1];
static int processing_long_msg = 0;
// Check packet against last check if different.
if (memcmp(packet, last_packet, length) == 0) {
//logMessage(LOG_DEBUG, "RS Received duplicate, ignoring.\n",length);
return rtn;
} else {
memcpy(last_packet, packet, length);
rtn = true;
}
if (processing_long_msg > 0 && packet[PKT_CMD] != CMD_MSG_LONG) {
processing_long_msg = 0;
//logMessage(LOG_ERR, "RS failed to receive complete long message, received '%s'\n",message);
//logMessage(LOG_DEBUG, "RS Finished receiving of MSG_LONG '%s'\n",message);
processMessage(message);
}
switch (packet[PKT_CMD]) {
case CMD_ACK:
logMessage(LOG_DEBUG, "RS Received ACK length %d.\n",length);
break;
case CMD_STATUS:
logMessage(LOG_DEBUG, "RS Received STATUS length %d.\n",length);
memcpy(_aqualink_data.raw_status, packet+4, AQ_PSTLEN);
processLEDstate();
if (_aqualink_data.aqbuttons[PUMP_INDEX].led->state == OFF ) {
_aqualink_data.pool_temp = TEMP_UNKNOWN;
_aqualink_data.spa_temp = TEMP_UNKNOWN;
} else if (_aqualink_data.aqbuttons[SPA_INDEX].led->state == OFF ) {
_aqualink_data.spa_temp = TEMP_UNKNOWN;
} else if (_aqualink_data.aqbuttons[SPA_INDEX].led->state == ON ) {
_aqualink_data.pool_temp = TEMP_UNKNOWN;
}
//logMessage(LOG_DEBUG, "RS Pool temp set to %d | Spa temp %d\n",_aqualink_data.pool_temp,_aqualink_data.spa_temp);
break;
case CMD_MSG:
memset(message, 0, AQ_MSGLONGLEN+1);
strncpy(message, (char*)packet+PKT_DATA+1, AQ_MSGLEN);
if (packet[PKT_DATA] == 1) // Start of long message, get them all before processing
break;
processMessage(message);
break;
case CMD_MSG_LONG:
// First in sequence is normal message.
processing_long_msg++;
strncpy(&message[processing_long_msg*AQ_MSGLEN], (char*)packet+PKT_DATA+1, AQ_MSGLEN);
if (processing_long_msg == 3) {
//logMessage(LOG_DEBUG, "RS Finished receiving of MSG_LONG '%s'\n",message);
processMessage(message);
processing_long_msg=0;
}
break;
case CMD_PROBE:
logMessage(LOG_DEBUG, "RS Received PROBE length %d.\n",length);
//logMessage(LOG_INFO, "Synch'ing with Aqualink master device...\n");
rtn = false;
break;
default:
logMessage(LOG_INFO, "RS Received unknown packet.\n",length);
rtn = false;
break;
}
return rtn;
}
void action_delayed_request()
{
char sval[10];
snprintf(sval, 9, "%d", _aqualink_data.unactioned.value);
if (_aqualink_data.unactioned.type == POOL_HTR_SETOINT) {
aq_programmer(AQ_SET_POOL_HEATER_TEMP, sval, &_aqualink_data);
logMessage(LOG_NOTICE, "Setting pool heater setpoint to %d\n",_aqualink_data.unactioned.value);
} else if (_aqualink_data.unactioned.type == SPA_HTR_SETOINT) {
aq_programmer(AQ_SET_SPA_HEATER_TEMP, sval, &_aqualink_data);
logMessage(LOG_NOTICE, "Setting spa heater setpoint to %d\n",_aqualink_data.unactioned.value);
} else if (_aqualink_data.unactioned.type == FREEZE_SETPOINT) {
aq_programmer(AQ_SET_FRZ_PROTECTION_TEMP, sval, &_aqualink_data);
logMessage(LOG_NOTICE, "Setting freeze protect to %d\n",_aqualink_data.unactioned.value);
}
_aqualink_data.unactioned.type = NO_ACTION;
_aqualink_data.unactioned.value = -1;
_aqualink_data.unactioned.requested = 0;
}
int main(int argc, char *argv[]) {
// main_loop ();
int i;
char *cfgFile = DEFAULT_CONFIG_FILE;
// struct lws_context_creation_info info;
// Log only NOTICE messages and above. Debug and info messages
// will not be logged to syslog.
setlogmask(LOG_UPTO(LOG_NOTICE));
if (getuid() != 0) {
//logMessage(LOG_ERR, "%s Can only be run as root\n", argv[0]);
fprintf(stderr, "ERROR %s Can only be run as root\n", argv[0]);
return EXIT_FAILURE;
}
// Initialize the daemon's parameters.
init_parameters(&_config_parameters);
for (i = 1; i < argc; i++) {
if (strcmp(argv[i], "-d") == 0) {
_config_parameters.deamonize = false;
} else if (strcmp(argv[i], "-c") == 0) {
cfgFile = argv[++i];
}
}
initButtons(&_aqualink_data);
readCfg(&_config_parameters, &_aqualink_data, cfgFile);
setLoggingPrms(_config_parameters.log_level, _config_parameters.deamonize, _config_parameters.log_file);
logMessage(LOG_NOTICE, "%s v%s\n", AQUALINKD_NAME, AQUALINKD_VERSION);
logMessage(LOG_NOTICE, "Config log_level = %d\n", _config_parameters.log_level);
logMessage(LOG_NOTICE, "Config socket_port = %s\n", _config_parameters.socket_port);
logMessage(LOG_NOTICE, "Config serial_port = %s\n", _config_parameters.serial_port);
logMessage(LOG_NOTICE, "Config web_directory = %s\n", _config_parameters.web_directory);
logMessage(LOG_NOTICE, "Config device_id = 0x%02hhx\n", _config_parameters.device_id);
#ifndef MG_DISABLE_MQTT
logMessage(LOG_NOTICE, "Config mqtt_server = %s\n", _config_parameters.mqtt_server);
logMessage(LOG_NOTICE, "Config mqtt_dz_sub_topic = %s\n", _config_parameters.mqtt_dz_sub_topic);
logMessage(LOG_NOTICE, "Config mqtt_dz_pub_topic = %s\n", _config_parameters.mqtt_dz_pub_topic);
logMessage(LOG_NOTICE, "Config mqtt_aq_topic = %s\n", _config_parameters.mqtt_aq_topic);
logMessage(LOG_NOTICE, "Config mqtt_user = %s\n", _config_parameters.mqtt_user);
logMessage(LOG_NOTICE, "Config mqtt_passwd = %s\n", _config_parameters.mqtt_passwd);
logMessage(LOG_NOTICE, "Config mqtt_ID = %s\n", _config_parameters.mqtt_ID);
logMessage(LOG_NOTICE, "Config idx water temp = %d\n", _config_parameters.dzidx_air_temp);
logMessage(LOG_NOTICE, "Config idx pool temp = %d\n", _config_parameters.dzidx_pool_water_temp);
logMessage(LOG_NOTICE, "Config idx spa temp = %d\n", _config_parameters.dzidx_spa_water_temp);
/* removed until domoticz has a better virtual thermostat
logMessage(LOG_NOTICE, "Config idx pool thermostat = %d\n", _config_parameters.dzidx_pool_thermostat);
logMessage(LOG_NOTICE, "Config idx spa thermostat = %d\n", _config_parameters.dzidx_spa_thermostat);
*/
#endif // MG_DISABLE_MQTT
logMessage(LOG_NOTICE, "Config deamonize = %s\n", bool2text(_config_parameters.deamonize));
logMessage(LOG_NOTICE, "Config log_file = %s\n", _config_parameters.log_file);
logMessage(LOG_NOTICE, "Config light_pgm_mode = %.2f\n", _config_parameters.light_programming_mode);
// logMessage (LOG_NOTICE, "Config serial_port = %s\n", config_parameters->serial_port);
for (i=0; i < TOTAL_BUTONS; i++) {
logMessage(LOG_NOTICE, "Config BTN %-13s = label %-15s | dzidx %d\n", _aqualink_data.aqbuttons[i].name, _aqualink_data.aqbuttons[i].label , _aqualink_data.aqbuttons[i].dz_idx);
//logMessage(LOG_NOTICE, "Button %d\n", i+1, _aqualink_data.aqbuttons[i].label , _aqualink_data.aqbuttons[i].dz_idx);
}
if (_config_parameters.deamonize == true) {
char pidfile[256];
// sprintf(pidfile, "%s/%s.pid",PIDLOCATION, basename(argv[0]));
sprintf(pidfile, "%s/%s.pid", "/run", basename(argv[0]));
daemonise(pidfile, main_loop);
} else {
main_loop();
}
exit(EXIT_SUCCESS);
}
void main_loop() {
struct mg_mgr mgr;
int rs_fd;
int packet_length;
unsigned char packet_buffer[AQ_MAXPKTLEN];
// NSF need to find a better place to init this.
_aqualink_data.aq_command = 0x00;
_aqualink_data.active_thread.thread_id = 0;
_aqualink_data.air_temp = TEMP_UNKNOWN;
_aqualink_data.pool_temp = TEMP_UNKNOWN;
_aqualink_data.spa_temp = TEMP_UNKNOWN;
_aqualink_data.frz_protect_set_point = TEMP_UNKNOWN;
_aqualink_data.pool_htr_set_point = TEMP_UNKNOWN;
_aqualink_data.spa_htr_set_point = TEMP_UNKNOWN;
_aqualink_data.unactioned.type = NO_ACTION;
if (!start_net_services(&mgr, &_aqualink_data, &_config_parameters)) {
logMessage(LOG_ERR, "Can not start webserver on port %s.\n", _config_parameters.socket_port);
exit(EXIT_FAILURE);
}
signal(SIGINT, intHandler);
signal(SIGTERM, intHandler);
int blank_read = 0;
rs_fd = init_serial_port(_config_parameters.serial_port);
logMessage(LOG_NOTICE, "Listening to Aqualink RS8 on serial port: %s\n", _config_parameters.serial_port);
while (_keepRunning == true) {
while ((rs_fd < 0 || blank_read >= MAX_ZERO_READ_BEFORE_RECONNECT) && _keepRunning == true) {
if (rs_fd < 0) {
// sleep(1);
logMessage(LOG_ERR, "Aqualink daemon attempting to connect to master device...\n");
broadcast_aqualinkstate_error(mgr.active_connections, "No connection to RS control panel");
mg_mgr_poll(&mgr, 1000); // Sevice messages
mg_mgr_poll(&mgr, 3000); // should donothing for 3 seconds.
// broadcast_aqualinkstate_error(mgr.active_connections, "No connection to RS control panel");
} else {
logMessage(LOG_ERR, "Aqualink daemon looks like serial error, resetting.\n");
}
rs_fd = init_serial_port(_config_parameters.serial_port);
blank_read = 0;
}
packet_length = get_packet(rs_fd, packet_buffer);
if (packet_length == -1) {
// Unrecoverable read error. Force an attempt to reconnect.
logMessage(LOG_ERR, "Bad packet length, reconnecting\n");
blank_read = MAX_ZERO_READ_BEFORE_RECONNECT;
} else if (packet_length == 0) {
//logMessage(LOG_NOTICE, "Nothing read on serial\n");
blank_read++;
} else if (packet_length > 0) {
blank_read = 0;
if (packet_length > 0 && packet_buffer[PKT_DEST] == _config_parameters.device_id) {
send_ack(rs_fd, _aqualink_data.aq_command);
_aqualink_data.aq_command = NUL;
// Process the packet. This includes deriving general status, and identifying
// warnings and errors. If something changed, notify any listeners
if (process_packet(packet_buffer, packet_length) != false) {
broadcast_aqualinkstate(mgr.active_connections);
}
} else if (packet_length > 0) {
// printf("packet not for us %02x\n",packet_buffer[PKT_DEST]);
}
}
mg_mgr_poll(&mgr, 0);
// Any unactioned commands
if (_aqualink_data.unactioned.type != NO_ACTION) {
time_t now;
time(&now);
if (difftime(now, _aqualink_data.unactioned.requested) > 2){
logMessage(LOG_DEBUG, "Actioning delayed request\n");
action_delayed_request();
}
}
#ifdef BLOCKING_MODE
#else
tcdrain(rs_fd); // Make sure buffer has been sent.
delay(10);
#endif
//}
}
// Reset and close the port.
close_serial_port(rs_fd);
// Clear webbrowser
mg_mgr_free(&mgr);
// NSF need to run through config memory and clean up.
logMessage(LOG_NOTICE, "Exit!\n");
exit(EXIT_FAILURE);
}

BIN
aqualinkd.o Normal file

Binary file not shown.

84
aqualinkd.test.conf Executable file
View File

@ -0,0 +1,84 @@
# aqualinkd.conf
#
# Created on: Aug 17, 2012
#
# The directory where the web files are stored
web_directory=/nas/data/Development/Raspberry/aqualink/aqualinkd/web
#web_directory=/var/www/aqualinkd/
# Log to file, comment out if you do not want to log to file
#log_file=/var/log/aqualinkd.log
# The log level. [DEBUG, INFO, NOTICE, WARNING, ERROR]
#log_level=DEBUG
log_level=INFO
#log_level=NOTICE
# The socket port that the daemon listens to
# If you change this from 80, remember to update aqualink.service.avahi
socket_port=80
# The serial port the daemon access to read the Aqualink RS8
serial_port=/dev/ttyUSB0
# mqtt stuff
mqtt_address = trident:1883
#mqtt_user = someusername
#mqtt_passwd = somepassword
mqtt_dz_pub_topic = domoticz/in
mqtt_dz_sub_topic = domoticz/out
mqtt_aq_topic = aqualinkd
#mqtt_sub_topic = aqualinkd/#
#mqtt_aq_topic = aqualinkd/#
# The id of the Aqualink terminal device. Devices probed by RS8 master are:
# 08-0b, 10-13, 18-1b, 20-23,
#device_id=0a
device_id=0x0a
# Domoticz ID's for temps.
air_temp_dzidx=13
pool_water_temp_dzidx=14
spa_water_temp_dzidx=15
#pool_thermostat_dzidx=45
#spa_thermostat_dzidx=46
button_01_label=Filter Pump
button_01_dzidx=37
button_02_label=Spa Mode
button_02_dzidx=38
button_03_label=Cleaner
button_03_dzidx=39
button_04_label=Waterfall
button_04_dzidx=40
button_05_label=Spa Blower
button_05_dzidx=41
button_06_label=Pool Light
button_06_dzidx=42
button_07_label=Spa Light
button_07_dzidx=43
button_08_label=NONE
button_08_dzidx=NONE
button_09_label=NONE
button_09_dzidx=NONE
button_10_label=Pool Heater
button_10_dzidx=44
button_11_label=Spa Heater
button_11_dzidx=56
button_12_label=Solar Heater
button_12_dzidx=NONE

4459
code Normal file

File diff suppressed because it is too large Load Diff

273
config.c Normal file
View File

@ -0,0 +1,273 @@
/*
* Copyright (c) 2017 Shaun Feakes - All rights reserved
*
* You may use redistribute and/or modify this code under the terms of
* the GNU General Public License version 2 as published by the
* Free Software Foundation. For the terms of this license,
* see <http://www.gnu.org/licenses/>.
*
* You are free to use this software under the terms of the GNU General
* Public License, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* https://github.com/sfeakes/aqualinkd
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <libgen.h>
#include <sys/ioctl.h>
//#include <sys/socket.h>
//#include <sys/time.h>
//#include <syslog.h>
//#include <unistd.h>
#include <netdb.h>
//#include <linux/if.h>
//#include <sys/types.h>
#include <unistd.h>
#include <net/if.h>
#include "config.h"
#include "utils.h"
#include "aq_serial.h"
#define MAXCFGLINE 256
char *generate_mqtt_id(char *buf, int len);
/*
* initialize data to default values
*/
void init_parameters (struct aqconfig * parms)
{
//char *p;
parms->serial_port = DEFAULT_SERIALPORT;
parms->log_level = DEFAULT_LOG_LEVEL;
parms->socket_port = DEFAULT_WEBPORT;
parms->web_directory = DEFAULT_WEBROOT;
//parms->device_id = strtoul(DEFAULT_DEVICE_ID, &p, 16);
parms->device_id = strtoul(DEFAULT_DEVICE_ID, NULL, 16);
//sscanf(DEFAULT_DEVICE_ID, "0x%x", &parms->device_id);
parms->mqtt_dz_sub_topic = DEFAULT_MQTT_DZ_OUT;
parms->mqtt_dz_pub_topic = DEFAULT_MQTT_DZ_IN;
parms->mqtt_aq_topic = DEFAULT_MQTT_AQ_TP;
parms->mqtt_server = DEFAULT_MQTT_SERVER;
parms->mqtt_user = DEFAULT_MQTT_USER;
parms->mqtt_passwd = DEFAULT_MQTT_PASSWD;
parms->dzidx_air_temp = TEMP_UNKNOWN;
parms->dzidx_pool_water_temp = TEMP_UNKNOWN;
parms->dzidx_spa_water_temp = TEMP_UNKNOWN;
//parms->dzidx_pool_thermostat = TEMP_UNKNOWN; // removed until domoticz has a better virtual thermostat
//parms->dzidx_spa_thermostat = TEMP_UNKNOWN; // removed until domoticz has a better virtual thermostat
parms->light_programming_mode = 0;
parms->deamonize = true;
parms->log_file = '\0';
generate_mqtt_id(parms->mqtt_ID, MQTT_ID_LEN);
}
char *cleanalloc(char*str)
{
char *result;
str = cleanwhitespace(str);
result = (char*)malloc(strlen(str)+1);
strcpy ( result, str );
//printf("Result=%s\n",result);
return result;
}
char *cleanallocindex(char*str, int index)
{
char *result;
int i;
int found = 1;
int loc1=0;
int loc2=strlen(str);
for(i=0;i<loc2;i++) {
if ( str[i] == ';' ) {
found++;
if (found == index)
loc1 = i;
else if (found == (index+1))
loc2 = i;
}
}
if (found < index)
return NULL;
// Trim leading & trailing spaces
loc1++;
while(isspace(str[loc1])) loc1++;
loc2--;
while(isspace(str[loc2])) loc2--;
// Allocate and copy
result = (char*)malloc(loc2-loc1+2*sizeof(char));
strncpy ( result, &str[loc1], loc2-loc1+1 );
result[loc2-loc1+1] = '\0';
return result;
}
// Find the first network interface with valid MAC and put mac address into buffer upto length
bool mac(char *buf, int len)
{
struct ifreq s;
int fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_IP);
struct if_nameindex *if_nidxs, *intf;
if_nidxs = if_nameindex();
if (if_nidxs != NULL)
{
for (intf = if_nidxs; intf->if_index != 0 || intf->if_name != NULL; intf++)
{
strcpy(s.ifr_name, intf->if_name);
if (0 == ioctl(fd, SIOCGIFHWADDR, &s))
{
int i;
if ( s.ifr_addr.sa_data[0] == 0 &&
s.ifr_addr.sa_data[1] == 0 &&
s.ifr_addr.sa_data[2] == 0 &&
s.ifr_addr.sa_data[3] == 0 &&
s.ifr_addr.sa_data[4] == 0 &&
s.ifr_addr.sa_data[5] == 0 ) {
continue;
}
for (i = 0; i < 6 && i * 2 < len; ++i)
{
sprintf(&buf[i * 2], "%02x", (unsigned char)s.ifr_addr.sa_data[i]);
}
return true;
}
}
}
return false;
}
char *generate_mqtt_id(char *buf, int len) {
extern char *__progname; // glibc populates this
int i;
strncpy(buf, basename(__progname), len);
i = strlen(buf);
if (i < len) {
buf[i++] = '_';
// If we can't get MAC to pad mqtt id then use PID
if (!mac(&buf[i], len - i)) {
sprintf(&buf[i], "%.*d", (len-i), getpid());
}
}
buf[len] = '\0';
return buf;
}
//void readCfg (char *cfgFile)
void readCfg (struct aqconfig *config_parameters, struct aqualinkdata *aqdata, char *cfgFile)
{
FILE * fp ;
char bufr[MAXCFGLINE];
//const char delim[2] = ";";
//char *buf;
//int line = 0;
//int tokenindex = 0;
char *b_ptr;
if( (fp = fopen(cfgFile, "r")) != NULL){
while(! feof(fp)){
if (fgets(bufr, MAXCFGLINE, fp) != NULL)
{
b_ptr = &bufr[0];
char *indx;
// Eat leading whitespace
while(isspace(*b_ptr)) b_ptr++;
if ( b_ptr[0] != '\0' && b_ptr[0] != '#')
{
indx = strchr(b_ptr, '=');
if ( indx != NULL)
{
if (strncasecmp (b_ptr, "socket_port", 11) == 0) {
//config_parameters->socket_port = cleanint(indx+1);
config_parameters->socket_port = cleanalloc(indx+1);
} else if (strncasecmp (b_ptr, "serial_port", 11) == 0) {
config_parameters->serial_port = cleanalloc(indx+1);
} else if (strncasecmp (b_ptr, "log_level", 9) == 0) {
config_parameters->log_level = text2elevel(cleanalloc(indx+1));
// should fee mem here
} else if (strncasecmp (b_ptr, "device_id", 9) == 0) {
config_parameters->device_id = strtoul(cleanalloc(indx+1), NULL, 16);
// should fee mem here
} else if (strncasecmp (b_ptr, "web_directory", 13) == 0) {
config_parameters->web_directory = cleanalloc(indx+1);
} else if (strncasecmp (b_ptr, "log_file", 8) == 0) {
config_parameters->log_file = cleanalloc(indx+1);
} else if (strncasecmp (b_ptr, "mqtt_address", 12) == 0) {
config_parameters->mqtt_server = cleanalloc(indx+1);
} else if (strncasecmp (b_ptr, "mqtt_dz_sub_topic", 17) == 0) {
config_parameters->mqtt_dz_sub_topic = cleanalloc(indx+1);
} else if (strncasecmp (b_ptr, "mqtt_dz_pub_topic", 17) == 0) {
config_parameters->mqtt_dz_pub_topic = cleanalloc(indx+1);
} else if (strncasecmp (b_ptr, "mqtt_aq_topic", 13) == 0) {
config_parameters->mqtt_aq_topic = cleanalloc(indx+1);
} else if (strncasecmp (b_ptr, "mqtt_user", 9) == 0) {
config_parameters->mqtt_user = cleanalloc(indx+1);
} else if (strncasecmp (b_ptr, "mqtt_passwd", 11) == 0) {
config_parameters->mqtt_passwd = cleanalloc(indx+1);
} else if (strncasecmp (b_ptr, "air_temp_dzidx", 14) == 0) {
config_parameters->dzidx_air_temp = strtoul(indx+1, NULL, 10);
} else if (strncasecmp (b_ptr, "pool_water_temp_dzidx", 21) == 0) {
config_parameters->dzidx_pool_water_temp = strtoul(indx+1, NULL, 10);
} else if (strncasecmp (b_ptr, "spa_water_temp_dzidx", 20) == 0) {
config_parameters->dzidx_spa_water_temp = strtoul(indx+1, NULL, 10);
} else if (strncasecmp (b_ptr, "light_programming_mode", 21) == 0) {
config_parameters->light_programming_mode = atof(cleanalloc(indx+1)); // should free this
}/*else if (strncasecmp (b_ptr, "pool_thermostat_dzidx", 21) == 0) { // removed until domoticz has a better virtual thermostat
config_parameters->dzidx_pool_thermostat = strtoul(indx+1, NULL, 10);
} else if (strncasecmp (b_ptr, "spa_thermostat_dzidx", 20) == 0) {
config_parameters->dzidx_spa_thermostat = strtoul(indx+1, NULL, 10);
} */else if (strncasecmp (b_ptr, "button_", 7) == 0) {
int num = strtoul(b_ptr+7, NULL, 10) - 1;
//logMessage (LOG_DEBUG, "Button %d\n", strtoul(b_ptr+7, NULL, 10));
if (strncasecmp (b_ptr+9, "_label", 6) == 0) {
//logMessage (LOG_DEBUG, " Label %s\n", cleanalloc(indx+1));
aqdata->aqbuttons[num].label = cleanalloc(indx+1);
} else if (strncasecmp (b_ptr+9, "_dzidx", 6) == 0) {
//logMessage (LOG_DEBUG, " dzidx %d\n", strtoul(indx+1, NULL, 10));
aqdata->aqbuttons[num].dz_idx = strtoul(indx+1, NULL, 10);
}
}
}
//line++;
}
}
}
fclose(fp);
} else {
/* error processing, couldn't open file */
displayLastSystemError(cfgFile);
exit (EXIT_FAILURE);
}
}

56
config.h Normal file
View File

@ -0,0 +1,56 @@
#ifndef CONFIG_H_
#define CONFIG_H_
#include "utils.h"
#include "aq_serial.h"
#include "aqualink.h"
#define DEFAULT_LOG_LEVEL 10
#define DEFAULT_WEBPORT "6580"
#define DEFAULT_WEBROOT "./"
#define DEFAULT_SERIALPORT "/dev/ttyUSB0"
#define DEFAULT_DEVICE_ID "0x0a"
#define DEFAULT_MQTT_DZ_IN "domoticz/in"
#define DEFAULT_MQTT_DZ_OUT "domoticz/out"
#define DEFAULT_MQTT_AQ_TP "aqualinkd"
#define DEFAULT_MQTT_SERVER "trident:1883"
#define DEFAULT_MQTT_USER NULL
#define DEFAULT_MQTT_PASSWD NULL
#define MQTT_ID_LEN 20
struct aqconfig
{
char *serial_port;
unsigned int log_level;
char *socket_port;
char *web_directory;
unsigned char device_id;
bool deamonize;
char *log_file;
char *mqtt_dz_sub_topic;
char *mqtt_dz_pub_topic;
char *mqtt_aq_topic;
char *mqtt_server;
char *mqtt_user;
char *mqtt_passwd;
char mqtt_ID[MQTT_ID_LEN];
int dzidx_air_temp;
int dzidx_pool_water_temp;
int dzidx_spa_water_temp;
float light_programming_mode;
//int dzidx_pool_thermostat; // Domoticz virtual thermostats are crap removed until better
//int dzidx_spa_thermostat; // Domoticz virtual thermostats are crap removed until better
//char mqtt_pub_topic[250];
//char *mqtt_pub_tp_ptr = mqtt_pub_topic[];
};
void init_parameters (struct aqconfig * parms);
//bool parse_config (struct aqconfig * parms, char *cfgfile);
//void readCfg (struct aqconfig *config_parameters, char *cfgFile);
void readCfg (struct aqconfig *config_parameters, struct aqualinkdata *aqualink_data, char *cfgFile);
#endif

BIN
config.o Normal file

Binary file not shown.

11
domoticz.h Normal file
View File

@ -0,0 +1,11 @@
#ifndef DOMOTICZ_H_
#define DOMOTICZ_H_
#define DZ_OFF 0
#define DZ_ON 1
#define DZ_NULL_IDX 0
#define DZ_SVALUE_LEN 20
#endif

94
extras/aqualinkd.init-d Executable file
View File

@ -0,0 +1,94 @@
#!/bin/sh
### BEGIN INIT INFO
# Provides: aqualinkd
# Required-Start: $time $local_fs $networking $syslog
# Required-Stop: $time $local_fs $networking $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: aqualink daemon
# Description: aqualink daemon
# read aqualink serial device outputs to web
### END INIT INFO
# Using the lsb functions to perform the operations.
. /lib/lsb/init-functions
# Process name ( For display )
NAME=aqualinkd
# Daemon name, where is the actual executable
DAEMON=/usr/local/bin/aqualinkd
OPTIONS="-c /etc/aqualinkd/aqualinkd.conf"
# pid file for the daemon
PIDFILE=/run/aqualinkd.pid
# If the daemon is not there, then exit.
#if [ test -x $DAEMON ]; then
# log_daemon_msg "Missing $DAEMON"
# log_end_msg 1
#fi
case $1 in
start)
# Checked the PID file exists and check the actual status of process
if [ -e $PIDFILE ]; then
status_of_proc -p $PIDFILE $DAEMON "$NAME process" && status="0" || status="$?"
# If the status is SUCCESS then don't need to start again.
if [ $status = "0" ]; then
exit # Exit
fi
fi
# Start the daemon.
log_daemon_msg "Starting the process" "$NAME"
# Start the daemon with the help of start-stop-daemon
# Log the message appropriately
log_daemon_msg "Starting process"
# if start-stop-daemon --start --quiet --oknodo --pidfile $PIDFILE --exec $DAEMON -- $OPTIONS; then
if start-stop-daemon --start --oknodo --exec $DAEMON -- $OPTIONS; then
log_end_msg 0
else
log_end_msg 1
fi
;;
stop)
# Stop the daemon.
if [ -e $PIDFILE ]; then
status_of_proc -p $PIDFILE $DAEMON "Stoppping the $NAME process" && status="0" || status="$?"
if [ "$status" = 0 ]; then
start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE
/bin/rm -rf $PIDFILE
fi
else
log_daemon_msg "$NAME process is not running"
log_end_msg 0
fi
;;
restart)
# Restart the daemon.
$0 stop && sleep 2 && $0 start
;;
status)
# Check the status of the process.
if [ -e $PIDFILE ]; then
status_of_proc -p $PIDFILE $DAEMON "$NAME process" && exit 0 || exit $?
else
log_daemon_msg "$NAME Process is not running"
log_end_msg 0
fi
;;
# reload)
# # Reload the process. Basically sending some signal to a daemon to reload
# # it configurations.
# if [ -e $PIDFILE ]; then
# start-stop-daemon --stop --signal USR1 --quiet --pidfile $PIDFILE --name $NAME
# log_success_msg "$NAME process reloaded successfully"
# else
# log_failure_msg "$PIDFILE does not exists"
# fi
# ;;
*)
# For invalid arguments, print the usage message.
echo "Usage: $0 {start|stop|restart|status}"
exit 2
;;
esac

207
extras/homekit2mqtt.json Normal file
View File

@ -0,0 +1,207 @@
{
"Aqualinkd Pool Air Temperature": {
"id": "AqualinkdPoolAirTemperatureSensor",
"name": "Pool Air Temp",
"service": "TemperatureSensor",
"manufacturer": "Feakes Inc",
"model": "AqualinkDTemperatureSensor",
"topic": {
"statusTemperature": "aqualinkd/Temperature/Air"
},
"payload": {},
"config": {}
},
"Aqualinkd Pool Water Temperature": {
"id": "AqualinkdPoolWaterTemperatureSensor",
"name": "Pool Water Temp",
"service": "TemperatureSensor",
"manufacturer": "Feakes Inc",
"model": "AqualinkDTemperatureSensor",
"topic": {
"statusTemperature": "aqualinkd/Temperature/Pool"
},
"payload": {},
"config": {}
},
"Aqualinkd Spa Water Temperature": {
"id": "AqualinkdSpaWaterTemperatureSensor",
"name": "Spa Water Temp",
"service": "TemperatureSensor",
"manufacturer": "Feakes Inc",
"model": "AqualinkDTemperatureSensor",
"topic": {
"statusTemperature": "aqualinkd/Temperature/Spa"
},
"payload": {},
"config": {}
},
"Aqualinkd Filter Pump": {
"id": "AqualinkdFilterPump",
"name": "Filter Pump",
"service": "Switch",
"manufacturer": "Feakes Inc",
"model": "AqualinkDSwitch",
"topic": {
"setOn": "aqualinkd/Filter_Pump/set",
"statusOn": "aqualinkd/Filter_Pump"
},
"payload": {
"onFalse": 0,
"onTrue": 1
},
"config": {}
},
"Aqualinkd Spa Mode": {
"id": "AqualinkdSpaMode",
"name": "Spa Mode",
"service": "Switch",
"manufacturer": "Feakes Inc",
"model": "AqualinkDSwitch",
"topic": {
"setOn": "aqualinkd/Spa_Mode/set",
"statusOn": "aqualinkd/Spa_Mode"
},
"payload": {
"onFalse": 0,
"onTrue": 1
},
"config": {}
},
"Aqualinkd Aux1": {
"id": "AqualinkdAux1Button",
"name": "Aux1",
"service": "Switch",
"manufacturer": "Feakes Inc",
"model": "AqualinkdAuxButton",
"topic": {
"setOn": "aqualinkd/Aux_1/set",
"statusOn": "aqualinkd/Aux_1"
},
"payload": {
"onTrue": 1,
"onFalse": 0
}
},
"Aqualinkd Aux2": {
"id": "AqualinkdAux2Button",
"name": "Aux2",
"service": "Switch",
"manufacturer": "Feakes Inc",
"model": "AqualinkdAuxButton",
"topic": {
"setOn": "aqualinkd/Aux_2/set",
"statusOn": "aqualinkd/Aux_2"
},
"payload": {
"onTrue": 1,
"onFalse": 0
}
},
"Aqualinkd Aux3": {
"id": "AqualinkdAux3Button",
"name": "Aux3",
"service": "Switch",
"manufacturer": "Feakes Inc",
"model": "AqualinkdAuxButton",
"topic": {
"setOn": "aqualinkd/Aux_3/set",
"statusOn": "aqualinkd/Aux_3"
},
"payload": {
"onTrue": 1,
"onFalse": 0
}
},
"Aqualinkd Aux4": {
"id": "AqualinkdAux4Button",
"name": "Aux4 ",
"service": "Switch",
"manufacturer": "Feakes Inc",
"model": "AqualinkdAuxButton",
"topic": {
"setOn": "aqualinkd/Aux_4/set",
"statusOn": "aqualinkd/Aux_4"
},
"payload": {
"onTrue": 1,
"onFalse": 0
}
},
"Aqualinkd Aux5": {
"id": "AqualinkdAux5Button",
"name": "Aux5",
"service": "Switch",
"manufacturer": "Feakes Inc",
"model": "AqualinkdAuxButton",
"topic": {
"setOn": "aqualinkd/Aux_5/set",
"statusOn": "aqualinkd/Aux_5"
},
"payload": {
"onTrue": 1,
"onFalse": 0
}
},
"Aqualinkd Aux6": {
"id": "AqualinkdAux6Button",
"name": "Aux6",
"service": "Switch",
"manufacturer": "Feakes Inc",
"model": "AqualinkdAuxButton",
"topic": {
"setOn": "aqualinkd/Aux_6/set",
"statusOn": "aqualinkd/Aux_6"
},
"payload": {
"onTrue": 1,
"onFalse": 0
}
},
"Aqualinkd Aux7": {
"id": "AqualinkdAux7Button",
"name": "Aux7",
"service": "Switch",
"manufacturer": "Feakes Inc",
"model": "AqualinkdAuxButton",
"topic": {
"setOn": "aqualinkd/Aux_7/set",
"statusOn": "aqualinkd/Aux_7"
},
"payload": {
"onTrue": 1,
"onFalse": 0
}
},
"Aqualinkd PoolHeater": {
"id": "AqualinkdPoolHeater",
"name": "Pool Heater",
"service": "Thermostat",
"manufacturer": "Feakes Inc",
"model": "AqualinkDThermostat",
"topic": {
"setTargetTemperature": "aqualinkd/Pool_Heater/setpoint/set",
"statusTargetTemperature": "aqualinkd/Pool_Heater/setpoint",
"statusCurrentTemperature": "aqualinkd/Temperature/Pool",
"setTargetHeatingCoolingState": "aqualinkd/Pool_Heater/set",
"statusTargetHeatingCoolingState": "aqualinkd/Pool_Heater"
},
"payload": {},
"config": {}
},
"Aqualinkd SpaHeater": {
"id": "AqualinkdSpaHeater",
"name": "Spa Heater",
"service": "Thermostat",
"manufacturer": "Feakes Inc",
"model": "AqualinkDThermostat",
"topic": {
"setTargetTemperature": "aqualinkd/Spa_Heater/setpoint/set",
"statusTargetTemperature": "aqualinkd/Spa_Heater/setpoint",
"statusCurrentTemperature": "aqualinkd/Temperature/Spa",
"setTargetHeatingCoolingState": "aqualinkd/Spa_Heater/set",
"statusTargetHeatingCoolingState": "aqualinkd/Spa_Heater"
},
"payload": {},
"config": {}
}
}

View File

@ -0,0 +1,7 @@
#!/bin/sh
while :
do
wget -O /dev/stdout 'http://aqualink.ip.address/?command=mhstatus' 2>/dev/null
sync
sleep 60
done

25
extras/show_mem.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
#
PROCESSNAME=aqualinkd
MYPID=`pidof $PROCESSNAME`
echo "=======";
echo PID:$MYPID
echo "--------"
Rss=`echo 0 $(cat /proc/$MYPID/smaps | grep Rss | awk '{print $2}' | sed 's#^#+#') | bc;`
Shared=`echo 0 $(cat /proc/$MYPID/smaps | grep Shared | awk '{print $2}' | sed 's#^#+#') | bc;`
Private=`echo 0 $(cat /proc/$MYPID/smaps | grep Private | awk '{print $2}' | sed 's#^#+#') | bc;`
Swap=`echo 0 $(cat /proc/$MYPID/smaps | grep Swap | awk '{print $2}' | sed 's#^#+#') | bc;`
Pss=`echo 0 $(cat /proc/$MYPID/smaps | grep Pss | awk '{print $2}' | sed 's#^#+#') | bc;`
Mem=`echo "$Rss + $Shared + $Private + $Swap + $Pss"|bc -l`
echo "Rss " $Rss
echo "Shared " $Shared
echo "Private " $Private
echo "Swap " $Swap
echo "Pss " $Pss
echo "=================";
echo "Mem " $Mem
echo "=================";

133
init_buttons.c Normal file
View File

@ -0,0 +1,133 @@
/*
* Copyright (c) 2017 Shaun Feakes - All rights reserved
*
* You may use redistribute and/or modify this code under the terms of
* the GNU General Public License version 2 as published by the
* Free Software Foundation. For the terms of this license,
* see <http://www.gnu.org/licenses/>.
*
* You are free to use this software under the terms of the GNU General
* Public License, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* https://github.com/sfeakes/aqualinkd
*/
#include "config.h"
#include "domoticz.h"
/*
* Link LED numbers to buttons, this is valid for RS8 and below, RS10 and above are different
* need to update this code in future.
*/
void initButtons(struct aqualinkdata *aqdata)
{
aqdata->aqbuttons[0].led = &aqdata->aqualinkleds[7-1];
aqdata->aqbuttons[0].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[0].label = BTN_PUMP;
//aqdata->aqbuttons[0].label = "Filter Pump";
aqdata->aqbuttons[0].name = BTN_PUMP;
//aqdata->aqbuttons[0].code = (unsigned char *)KEY_PUMP;
aqdata->aqbuttons[0].code = KEY_PUMP;
aqdata->aqbuttons[0].dz_idx = 37;
aqdata->aqbuttons[1].led = &aqdata->aqualinkleds[6-1];
aqdata->aqbuttons[1].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[1].label = BTN_SPA;
//aqdata->aqbuttons[1].label = "Spa Mode";
aqdata->aqbuttons[1].name = BTN_SPA;
//aqdata->aqbuttons[1].code = (unsigned char *)KEY_SPA;
aqdata->aqbuttons[1].code = KEY_SPA;
aqdata->aqbuttons[1].dz_idx = 38;
aqdata->aqbuttons[2].led = &aqdata->aqualinkleds[5-1];
aqdata->aqbuttons[2].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[2].label = BTN_AUX1;
//aqdata->aqbuttons[2].label = "Cleaner";
aqdata->aqbuttons[2].name = BTN_AUX1;
//aqdata->aqbuttons[2].code = (unsigned char *)KEY_AUX1;
aqdata->aqbuttons[2].code = KEY_AUX1;
aqdata->aqbuttons[2].dz_idx = 39;
aqdata->aqbuttons[3].led = &aqdata->aqualinkleds[4-1];
aqdata->aqbuttons[3].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[3].label = BTN_AUX2;
//aqdata->aqbuttons[3].label = "Waterfall";
aqdata->aqbuttons[3].name = BTN_AUX2;
//aqdata->aqbuttons[3].code = (unsigned char *)KEY_AUX2;
aqdata->aqbuttons[3].code = KEY_AUX2;
aqdata->aqbuttons[3].dz_idx = 40;
aqdata->aqbuttons[4].led = &aqdata->aqualinkleds[3-1];
aqdata->aqbuttons[4].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[4].label = BTN_AUX3;
//aqdata->aqbuttons[4].label = "Spa Blower";
aqdata->aqbuttons[4].name = BTN_AUX3;
//aqdata->aqbuttons[4].code = (unsigned char *)KEY_AUX3;
aqdata->aqbuttons[4].code = KEY_AUX3;
aqdata->aqbuttons[4].dz_idx = 41;
aqdata->aqbuttons[5].led = &aqdata->aqualinkleds[9-1];
aqdata->aqbuttons[5].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[5].label = BTN_AUX4;
//aqdata->aqbuttons[5].label = "Pool Light";
aqdata->aqbuttons[5].name = BTN_AUX4;
//aqdata->aqbuttons[5].code = (unsigned char *)KEY_AUX4;
aqdata->aqbuttons[5].code = KEY_AUX4;
aqdata->aqbuttons[5].dz_idx = 42;
aqdata->aqbuttons[6].led = &aqdata->aqualinkleds[8-1];
aqdata->aqbuttons[6].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[6].label = BTN_AUX5;
//aqdata->aqbuttons[6].label = "Spa Light";
aqdata->aqbuttons[6].name = BTN_AUX5;
//aqdata->aqbuttons[6].code = (unsigned char *)KEY_AUX5;
aqdata->aqbuttons[6].code = KEY_AUX5;
aqdata->aqbuttons[6].dz_idx = 43;
aqdata->aqbuttons[7].led = &aqdata->aqualinkleds[12-1];
aqdata->aqbuttons[7].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[7].label = BTN_AUX6;
aqdata->aqbuttons[7].name = BTN_AUX6;
//aqdata->aqbuttons[7].code = (unsigned char *)KEY_AUX6;
aqdata->aqbuttons[7].code = KEY_AUX6;
aqdata->aqbuttons[7].dz_idx = DZ_NULL_IDX;
aqdata->aqbuttons[8].led = &aqdata->aqualinkleds[1-1];
aqdata->aqbuttons[8].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[8].label = BTN_AUX7;
aqdata->aqbuttons[8].name = BTN_AUX7;
//aqdata->aqbuttons[8].code = (unsigned char *)KEY_AUX7;
aqdata->aqbuttons[8].code = KEY_AUX7;
aqdata->aqbuttons[8].dz_idx = DZ_NULL_IDX;
aqdata->aqbuttons[9].led = &aqdata->aqualinkleds[15-1];
aqdata->aqbuttons[9].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[9].label = BTN_POOL_HTR;
//aqdata->aqbuttons[9].label = "Heater";
aqdata->aqbuttons[9].name = BTN_POOL_HTR;
//aqdata->aqbuttons[9].code = (unsigned char *)KEY_POOL_HTR;
aqdata->aqbuttons[9].code = KEY_POOL_HTR;
aqdata->aqbuttons[9].dz_idx = 44;
aqdata->aqbuttons[10].led = &aqdata->aqualinkleds[17-1];
aqdata->aqbuttons[10].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[10].label = BTN_SPA_HTR;
//aqdata->aqbuttons[10].label = "Heater";
aqdata->aqbuttons[10].name = BTN_SPA_HTR;
//aqdata->aqbuttons[10].code = (unsigned char *)KEY_SPA_HTR;
aqdata->aqbuttons[10].code = KEY_SPA_HTR;
aqdata->aqbuttons[10].dz_idx = 44;
aqdata->aqbuttons[11].led = &aqdata->aqualinkleds[19-1];
aqdata->aqbuttons[11].led->state = LED_S_UNKNOWN;
aqdata->aqbuttons[11].label = BTN_SOLAR_HTR;
//aqdata->aqbuttons[11].label = "Solar Heater";
aqdata->aqbuttons[11].name = BTN_SOLAR_HTR;
//aqdata->aqbuttons[11].code = (unsigned char *)KEY_SOLAR_HTR;
aqdata->aqbuttons[11].code = KEY_SOLAR_HTR;
aqdata->aqbuttons[11].dz_idx = DZ_NULL_IDX;
}

12
init_buttons.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef INIT_BUTTONS_H_
#define INIT_BUTTONS_H_
#define PUMP_INDEX 0
#define SPA_INDEX 1
void initButtons(struct aqualinkdata *aqdata);
#define TOTAL_BUTONS 12
#endif

BIN
init_buttons.o Normal file

Binary file not shown.

331
json_messages.c Normal file
View File

@ -0,0 +1,331 @@
/*
* Copyright (c) 2017 Shaun Feakes - All rights reserved
*
* You may use redistribute and/or modify this code under the terms of
* the GNU General Public License version 2 as published by the
* Free Software Foundation. For the terms of this license,
* see <http://www.gnu.org/licenses/>.
*
* You are free to use this software under the terms of the GNU General
* Public License, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* https://github.com/sfeakes/aqualinkd
*/
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include "aqualink.h"
#include "config.h"
//#include "aq_programmer.h"
#include "utils.h"
//#include "web_server.h"
#include "json_messages.h"
#include "domoticz.h"
//#define test_message "{\"type\": \"status\",\"version\": \"8157 REV MMM\",\"date\": \"09/01/16 THU\",\"time\": \"1:16 PM\",\"temp_units\": \"F\",\"air_temp\": \"96\",\"pool_temp\": \"86\",\"spa_temp\": \" \",\"battery\": \"ok\",\"pool_htr_set_pnt\": \"85\",\"spa_htr_set_pnt\": \"99\",\"freeze_protection\": \"off\",\"frz_protect_set_pnt\": \"0\",\"leds\": {\"pump\": \"on\",\"spa\": \"off\",\"aux1\": \"off\",\"aux2\": \"off\",\"aux3\": \"off\",\"aux4\": \"off\",\"aux5\": \"off\",\"aux6\": \"off\",\"aux7\": \"off\",\"pool_heater\": \"off\",\"spa_heater\": \"off\",\"solar_heater\": \"off\"}}"
//#define test_labels "{\"type\": \"aux_labels\",\"aux1_label\": \"Cleaner\",\"aux2_label\": \"Waterfall\",\"aux3_label\": \"Spa Blower\",\"aux4_label\": \"Pool Light\",\"aux5_label\": \"Spa Light\",\"aux6_label\": \"Unassigned\",\"aux7_label\": \"Unassigned\"}"
//#define test_message "{\"type\": \"status\",\"version\":\"xx\",\"time\":\"xx\",\"air_temp\":\"0\",\"pool_temp\":\"0\",\"spa_temp\":\"0\",\"pool_htr_set_pnt\":\"0\",\"spa_htr_set_pnt\":\"0\",\"frz_protect_set_pnt\":\"0\",\"temp_units\":\"f\",\"battery\":\"ok\",\"leds\":{\"Filter_Pump\": \"on\",\"Spa_Mode\": \"on\",\"Aux_1\": \"on\",\"Aux_2\": \"on\",\"Aux_3\": \"on\",\"Aux_4\": \"on\",\"Aux_5\": \"on\",\"Aux_6\": \"on\",\"Aux_7\": \"on\",\"Pool_Heater\": \"on\",\"Spa_Heater\": \"on\",\"Solar_Heater\": \"on\"}}"
//{"type": "aux_labels","Pool Pump": "Pool Pump","Spa Mode": "Spa Mode","Cleaner": "Aux 1","Waterfall": "Aux 2","Spa Blower": "Aux 2","Pool Light": "Aux 4","Spa Light ": "Aux 5","Aux 6": "Aux 6","Aux 7": "Aux 7","Heater": "Heater","Heater": "Heater","Solar Heater": "Solar Heater","(null)": "(null)"}
const char* getStatus(struct aqualinkdata *aqdata)
{
if (aqdata->active_thread.thread_id != 0) {
return JSON_PROGRAMMING;
}
if (aqdata->last_message != NULL && strstr(aqdata->last_message, "SERVICE") != NULL ) {
return JSON_SERVICE;
}
return JSON_READY;
}
int build_mqtt_status_JSON(char* buffer, int size, int idx, int nvalue, float setpoint/*char *svalue*/)
{
memset(&buffer[0], 0, size);
int length = 0;
if (setpoint == TEMP_UNKNOWN) {
length = sprintf(buffer, "{\"idx\":%d,\"nvalue\":%d,\"svalue\":\"\"}", idx, nvalue);
} else {
length = sprintf(buffer, "{\"idx\":%d,\"nvalue\":%d,\"stype\":\"SetPoint\",\"svalue\":\"%.2f\"}", idx, nvalue, setpoint+0.005);
}
buffer[length] = '\0';
return strlen(buffer);
}
int build_aqualink_error_status_JSON(char* buffer, int size, char *msg)
{
//return snprintf(buffer, size, "{\"type\": \"error\",\"status\":\"%s\"}", msg);
return snprintf(buffer, size, "{\"type\": \"status\",\"status\":\"%s\",\"version\":\"xx\",\"time\":\"xx\",\"air_temp\":\"0\",\"pool_temp\":\"0\",\"spa_temp\":\"0\",\"pool_htr_set_pnt\":\"0\",\"spa_htr_set_pnt\":\"0\",\"frz_protect_set_pnt\":\"0\",\"temp_units\":\"f\",\"battery\":\"ok\",\"leds\":{\"Filter_Pump\": \"off\",\"Spa_Mode\": \"off\",\"Aux_1\": \"off\",\"Aux_2\": \"off\",\"Aux_3\": \"off\",\"Aux_4\": \"off\",\"Aux_5\": \"off\",\"Aux_6\": \"off\",\"Aux_7\": \"off\",\"Pool_Heater\": \"off\",\"Spa_Heater\": \"off\",\"Solar_Heater\": \"off\"}}", msg);
}
int build_aqualink_status_JSON(struct aqualinkdata *aqdata, char* buffer, int size)
{
//strncpy(buffer, test_message, strlen(test_message)+1);
//return strlen(test_message);
//char buffer2[600];
memset(&buffer[0], 0, size);
int length = 0;
int i;
length += sprintf(buffer+length, "{\"type\": \"status\"");
length += sprintf(buffer+length, ",\"status\":\"%s\"",getStatus(aqdata) );
//length += sprintf(buffer+length, ",\"message\":\"%s\"",aqdata->message );
length += sprintf(buffer+length, ",\"version\":\"%s\"",aqdata->version );//8157 REV MMM",
length += sprintf(buffer+length, ",\"date\":\"%s\"",aqdata->date );//"09/01/16 THU",
length += sprintf(buffer+length, ",\"time\":\"%s\"",aqdata->time );//"1:16 PM",
//length += sprintf(buffer+length, ",\"air_temp\":\"%d\"",aqdata->air_temp );//"96",
//length += sprintf(buffer+length, ",\"pool_temp\":\"%d\"",aqdata->pool_temp );//"86",
//length += sprintf(buffer+length, ",\"spa_temp\":\"%d\"",aqdata->spa_temp );//" ",
length += sprintf(buffer+length, ",\"pool_htr_set_pnt\":\"%d\"",aqdata->pool_htr_set_point );//"85",
length += sprintf(buffer+length, ",\"spa_htr_set_pnt\":\"%d\"",aqdata->spa_htr_set_point );//"99",
//length += sprintf(buffer+length, ",\"freeze_protection":\"%s\"",aqdata->frz_protect_set_point );//"off",
length += sprintf(buffer+length, ",\"frz_protect_set_pnt\":\"%d\"",aqdata->frz_protect_set_point );//"0",
if ( aqdata->air_temp == TEMP_UNKNOWN )
length += sprintf(buffer+length, ",\"air_temp\":\" \"");
else
length += sprintf(buffer+length, ",\"air_temp\":\"%d\"",aqdata->air_temp );
if ( aqdata->pool_temp == TEMP_UNKNOWN )
length += sprintf(buffer+length, ",\"pool_temp\":\" \"");
else
length += sprintf(buffer+length, ",\"pool_temp\":\"%d\"",aqdata->pool_temp );
if ( aqdata->spa_temp == TEMP_UNKNOWN )
length += sprintf(buffer+length, ",\"spa_temp\":\" \"");
else
length += sprintf(buffer+length, ",\"spa_temp\":\"%d\"",aqdata->spa_temp );
if ( aqdata->temp_units == FAHRENHEIT )
length += sprintf(buffer+length, ",\"temp_units\":\"%s\"",JSON_FAHRENHEIT );
else if ( aqdata->temp_units == CELSIUS )
length += sprintf(buffer+length, ",\"temp_units\":\"%s\"", JSON_CELSIUS);
else
length += sprintf(buffer+length, ",\"temp_units\":\"%s\"",JSON_UNKNOWN );
if (aqdata->battery == OK)
length += sprintf(buffer+length, ",\"battery\":\"%s\"",JSON_OK );//"ok",
else
length += sprintf(buffer+length, ",\"battery\":\"%s\"",JSON_LOW );//"ok",
length += sprintf(buffer+length, ",\"leds\":{" );
for (i=0; i < TOTAL_BUTTONS; i++)
{
char *state;
switch (aqdata->aqbuttons[i].led->state)
{
case ON:
state = JSON_ON;
break;
case OFF:
case LED_S_UNKNOWN:
state = JSON_OFF;
break;
case FLASH:
state = JSON_FLASH;
break;
case ENABLE:
state = JSON_ENABLED;
break;
}
length += sprintf(buffer+length, "\"%s\": \"%s\"", aqdata->aqbuttons[i].name, state);
if (i+1 < TOTAL_BUTTONS)
length += sprintf(buffer+length, "," );
}
length += sprintf(buffer+length, "}}" );
buffer[length] = '\0';
/*
buffer[length] = '\0';
strncpy(buffer2, test_message, strlen(test_message)+1);
for (i=0; i < strlen(buffer); i++) {
logMessage (LOG_DEBUG, "buffer[%d] = '%c' | '%c'\n",i,buffer[i],buffer2[i]);
}
logMessage (LOG_DEBUG, "JSON Size %d\n",strlen(buffer));
printf("%s\n",buffer);
for (i=strlen(buffer); i > strlen(buffer)-10; i--) {
logMessage (LOG_DEBUG, "buffer[%d] = '%c'\n",i,buffer[i]);
}
for (i=10; i >= 0; i--) {
logMessage (LOG_DEBUG, "buffer[%d] = '%c'\n",i,buffer[i]);
}
//return length-1;
logMessage (LOG_DEBUG, "JSON Size %d\n",strlen(buffer2));
printf("%s\n",buffer2);
for (i=strlen(buffer2); i > strlen(buffer2)-10; i--) {
logMessage (LOG_DEBUG, "buffer[%d] = '%c'\n",i,buffer2[i]);
}
for (i=10; i >= 0; i--) {
logMessage (LOG_DEBUG, "buffer[%d] = '%c'\n",i,buffer2[i]);
}
//return strlen(test_message);
*/
//printf("Buffer = %d, JSON = %d",size ,strlen(buffer));
return strlen(buffer);
}
int build_aux_labels_JSON(struct aqualinkdata *aqdata, char* buffer, int size)
{
memset(&buffer[0], 0, size);
int length = 0;
int i;
length += sprintf(buffer+length, "{\"type\": \"aux_labels\"");
for (i=0; i < TOTAL_BUTTONS; i++)
{
length += sprintf(buffer+length, ",\"%s\": \"%s\"", aqdata->aqbuttons[i].name, aqdata->aqbuttons[i].label);
}
length += sprintf(buffer+length, "}");
return length;
//printf("%s\n",buffer);
//return strlen(buffer);
}
// WS Received '{"parameter":"SPA_HTR","value":99}'
// WS Received '{"command":"KEY_HTR_POOL"}'
// WS Received '{"command":"GET_AUX_LABELS"}'
bool parseJSONwebrequest(char *buffer, struct JSONwebrequest *request)
{
int i=0;
int found=0;
bool reading = false;
request->first.key = NULL;
request->first.value = NULL;
request->second.key = NULL;
request->second.value = NULL;
int length = strlen(buffer);
while ( i < length )
{
//printf ("Reading %c",buffer[i]);
switch (buffer[i]) {
case '{':
case '"':
case '}':
case ':':
case ',':
case ' ':
// Ignore space , : if reading a string
if (reading == true && buffer[i] != ' ' && buffer[i] != ',' && buffer[i] != ':'){
//printf (" <- END");
reading = false;
buffer[i] = '\0';
found++;
}
break;
default:
if (reading == false) {
//printf (" <- START");
reading = true;
switch(found) {
case 0:
request->first.key = &buffer[i];
break;
case 1:
request->first.value = &buffer[i];
break;
case 2:
request->second.key = &buffer[i];
break;
case 3:
request->second.value = &buffer[i];
break;
}
}
break;
}
//printf ("\n");
if (found >= 4)
break;
i++;
}
return true;
}
bool parseJSONmqttrequest(const char *str, size_t len, int *idx, int *nvalue, char *svalue) {
int i = 0;
int found = 0;
svalue[0] = '\0';
for (i = 0; i < len && str[i] != '\0'; i++) {
if (str[i] == '"') {
if (strncmp("\"idx\"", (char *)&str[i], 5) == 0) {
i = i + 5;
for (; str[i] != ',' && str[i] != '\0'; i++) {
if (str[i] == ':') {
*idx = atoi(&str[i + 1]);
found++;
}
}
//if (*idx == 45)
// printf("%s\n",str);
} else if (strncmp("\"nvalue\"", (char *)&str[i], 8) == 0) {
i = i + 8;
for (; str[i] != ',' && str[i] != '\0'; i++) {
if (str[i] == ':') {
*nvalue = atoi(&str[i + 1]);
found++;
}
}
} else if (strncmp("\"svalue1\"", (char *)&str[i], 9) == 0) {
i = i + 9;
for (; str[i] != ',' && str[i] != '\0'; i++) {
if (str[i] == ':') {
while(str[i] == ':' || str[i] == ' ' || str[i] == '"' || str[i] == '\'') i++;
int j=i+1;
while(str[j] != '"' && str[j] != '\'' && str[j] != ',' && str[j] != '}') j++;
strncpy(svalue, &str[i], ((j-i)>DZ_SVALUE_LEN?DZ_SVALUE_LEN:(j-i)));
svalue[((j-i)>DZ_SVALUE_LEN?DZ_SVALUE_LEN:(j-i))] = '\0'; // Simply force the last termination
found++;
}
}
}
if (found >= 4) {
return true;
}
}
}
// Just incase svalue is not found, we really don;t care for most devices.
if (found >= 2) {
return true;
}
return false;
}

101
json_messages.h Normal file
View File

@ -0,0 +1,101 @@
#ifndef JSON_MESSAGES_H_
#define JSON_MESSAGES_H_
//FUNCTION PROTOTYPES
#define JSON_LABEL_SIZE 300
#define JSON_STATUS_SIZE 800
#define JSON_MQTT_MSG_SIZE 100
#define JSON_ON "on"
#define JSON_OFF "off"
#define JSON_FLASH "flash"
#define JSON_ENABLED "enabled"
#define JSON_FAHRENHEIT "f"
#define JSON_CELSIUS "c"
#define JSON_UNKNOWN "u"
#define JSON_OK "ok"
#define JSON_LOW "low"
#define JSON_PROGRAMMING "Programming"
#define JSON_SERVICE "Service"
#define JSON_READY "Ready"
struct JSONkeyvalue{
char *key;
char *value;
};
struct JSONwebrequest {
struct JSONkeyvalue first;
struct JSONkeyvalue second;
struct JSONkeyvalue third;
};
int build_aqualink_status_JSON(struct aqualinkdata *aqdata, char* buffer, int size);
int build_aux_labels_JSON(struct aqualinkdata *aqdata, char* buffer, int size);
bool parseJSONwebrequest(char *buffer, struct JSONwebrequest *request);
int build_mqtt_status_JSON(char* buffer, int size, int idx, int nvalue, float setpoint/*char *svalue*/);
bool parseJSONmqttrequest(const char *str, size_t len, int *idx, int *nvalue, char *svalue);
int build_aqualink_error_status_JSON(char* buffer, int size, char *msg);
#endif /* JSON_MESSAGES_H_ */
/*
{\"type\": \"status\",\"version\": \"8157 REV MMM\",\"date\": \"09/01/16 THU\",\"time\": \"1:16 PM\",\"temp_units\": \"F\",\"air_temp\": \"96\",\"pool_temp\": \"86\",\"spa_temp\": \" \",\"battery\": \"ok\",\"pool_htr_set_pnt\": \"85\",\"spa_htr_set_pnt\": \"99\",\"freeze_protection\": \"off\",\"frz_protect_set_pnt\": \"0\",\"leds\": {\"pump\": \"on\",\"spa\": \"off\",\"aux1\": \"off\",\"aux2\": \"off\",\"aux3\": \"off\",\"aux4\": \"off\",\"aux5\": \"off\",\"aux6\": \"off\",\"aux7\": \"off\",\"pool_heater\": \"off\",\"spa_heater\": \"off\",\"solar_heater\": \"off\"}}
{\"type\": \"aux_labels\",\"aux1_label\": \"Cleaner\",\"aux2_label\": \"Waterfall\",\"aux3_label\": \"Spa Blower\",\"aux4_label\": \"Pool Light\",\"aux5_label\": \"Spa Light\",\"aux6_label\": \"Unassigned\",\"aux7_label\": \"Unassigned\"}
{"type": "status","version": "8157 REV MMM","date": "09/01/16 THU","time": "1:16 PM","temp_units": "F","air_temp": "96","pool_temp": "86","spa_temp": " ","battery": "ok","pool_htr_set_pnt": "85","spa_htr_set_pnt": "99","freeze_protection": "off","frz_protect_set_pnt": "0","leds": {"pump": "on","spa": "off","aux1": "off","aux2": "off","aux3": "off","aux4": "off","aux5": "off","aux6": "off","aux7": "off","pool_heater": "off","spa_heater": "off","solar_heater": "off"}}
{"type": "aux_labels","aux1_label": "Cleaner","aux2_label": "Waterfall","aux3_label": "Spa Blower","aux4_label": "Pool Light","aux5_label": "Spa Light","aux6_label": "Unassigned","aux7_label": "Unassigned"}
{
"type":"aux_labels",
"aux1_label":"Cleaner",
"aux2_label":"Waterfall",
"aux3_label":"Spa Blower",
"aux4_label":"Pool Light",
"aux5_label":"Spa Light",
"aux6_label":"Unassigned",
"aux7_label":"Unassigned"
}
{
"type":"status",
"version":"8157 REV MMM",
"date":"09/01/16 THU",
"time":"1:16 PM",
"temp_units":"F",
"air_temp":"96",
"pool_temp":"86",
"spa_temp":" ",
"battery":"ok",
"pool_htr_set_pnt":"85",
"spa_htr_set_pnt":"99",
"freeze_protection":"off",
"frz_protect_set_pnt":"0",
"leds":{
"pump":"on",
"spa":"off",
"aux1":"off",
"aux2":"off",
"aux3":"off",
"aux4":"off",
"aux5":"off",
"aux6":"off",
"aux7":"off",
"pool_heater":"off",
"spa_heater":"off",
"solar_heater":"off"
}
}
*/

BIN
json_messages.o Normal file

Binary file not shown.

16361
mongoose.c Normal file

File diff suppressed because it is too large Load Diff

6138
mongoose.h Normal file

File diff suppressed because it is too large Load Diff

BIN
mongoose.o Normal file

Binary file not shown.

739
net_services.c Normal file
View File

@ -0,0 +1,739 @@
/*
* Copyright (c) 2017 Shaun Feakes - All rights reserved
*
* You may use redistribute and/or modify this code under the terms of
* the GNU General Public License version 2 as published by the
* Free Software Foundation. For the terms of this license,
* see <http://www.gnu.org/licenses/>.
*
* You are free to use this software under the terms of the GNU General
* Public License, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* https://github.com/sfeakes/aqualinkd
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <string.h>
#include <sys/time.h>
#include <syslog.h>
#include "mongoose.h"
#include "aqualink.h"
#include "config.h"
#include "aq_programmer.h"
#include "utils.h"
#include "net_services.h"
#include "json_messages.h"
#include "domoticz.h"
#include "aq_mqtt.h"
static struct aqconfig *_aqualink_config;
static struct aqualinkdata *_aqualink_data;
static char *_web_root;
static int _mqtt_exit_flag = false;
/*
static const char *s_address = "trident:1883";
static const char *s_user_name = NULL;
static const char *s_password = NULL;
static const char *s_topic = "domoticz/out";
*/
//static struct mg_mqtt_topic_expression s_topic_expr = {NULL, 0};
#ifndef MG_DISABLE_MQTT
void start_mqtt(struct mg_mgr *mgr);
static struct aqualinkdata _last_mqtt_aqualinkdata;
void mqtt_broadcast_aqualinkstate(struct mg_connection *nc);
#endif
static sig_atomic_t s_signal_received = 0;
//static const char *s_http_port = "8080";
static struct mg_serve_http_opts s_http_server_opts;
static void signal_handler(int sig_num) {
signal(sig_num, signal_handler); // Reinstantiate signal handler
s_signal_received = sig_num;
}
static int is_websocket(const struct mg_connection *nc) {
return nc->flags & MG_F_IS_WEBSOCKET;
}
static int is_mqtt(const struct mg_connection *nc) {
return nc->flags & MG_F_USER_1;
}
static void set_mqtt(struct mg_connection *nc) {
nc->flags |= MG_F_USER_1;
}
static void ws_send(struct mg_connection *nc, char *msg)
{
int size = strlen(msg);
mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT, msg, size);
//logMessage (LOG_DEBUG, "WS: Sent %d characters '%s'\n",size, msg);
}
void broadcast_aqualinkstate_error(struct mg_connection *nc, char *msg)
{
struct mg_connection *c;
char data[JSON_STATUS_SIZE];
build_aqualink_error_status_JSON(data, JSON_STATUS_SIZE, msg);
for (c = mg_next(nc->mgr, NULL); c != NULL; c = mg_next(nc->mgr, c)) {
if (is_websocket(c))
ws_send(c, data);
}
// Maybe enhacment in future to sent error messages to MQTT
}
void broadcast_aqualinkstate(struct mg_connection *nc)
{
static int mqtt_count=0;
struct mg_connection *c;
char data[JSON_STATUS_SIZE];
build_aqualink_status_JSON(_aqualink_data, data, JSON_STATUS_SIZE);
#ifndef MG_DISABLE_MQTT
if (_mqtt_exit_flag == true) {
mqtt_count++;
if (mqtt_count >= 10) {
start_mqtt(nc->mgr);
mqtt_count = 0;
}
}
#endif
for (c = mg_next(nc->mgr, NULL); c != NULL; c = mg_next(nc->mgr, c)) {
if (is_websocket(c))
ws_send(c, data);
#ifndef MG_DISABLE_MQTT
else if (is_mqtt(c))
mqtt_broadcast_aqualinkstate(c);
#endif
}
return;
}
#ifndef MG_DISABLE_MQTT
/*
int fahrenheit2celsius(int fahrenheit)
{
return (int)((fahrenheit - 32) * 5 / 9)+0.5f;
}
*/
void send_mqtt(struct mg_connection *nc, char *toppic, char *message)
{
static uint16_t msg_id = 0;
if (msg_id >= 65535){msg_id=1;}else{msg_id++;}
//mg_mqtt_publish(nc, toppic, msg_id, MG_MQTT_QOS(0), message, strlen(message));
mg_mqtt_publish(nc, toppic, msg_id, MG_MQTT_RETAIN | MG_MQTT_QOS(1), message, strlen(message));
logMessage(LOG_INFO, "MQTT: Published id=%d: %s %s\n", msg_id, toppic, message);
}
// ******************** FIX THIS *******************************
// NSF this doesn't work for tempratures, we are badly rounding.
void send_domoticz_mqtt_msg(struct mg_connection *nc, int idx, int value)
{
if (idx <= 0)
return;
char mqtt_msg[JSON_MQTT_MSG_SIZE];
build_mqtt_status_JSON(mqtt_msg ,JSON_MQTT_MSG_SIZE, idx, value, TEMP_UNKNOWN);
send_mqtt(nc, _aqualink_config->mqtt_dz_pub_topic, mqtt_msg);
}
void send_domoticz_mqtt_msg_setpoint(struct mg_connection *nc, int idx, int value, float setpoint)
{
if (idx <= 0)
return;
char mqtt_msg[JSON_MQTT_MSG_SIZE];
build_mqtt_status_JSON(mqtt_msg ,JSON_MQTT_MSG_SIZE, idx, value, setpoint);
send_mqtt(nc, _aqualink_config->mqtt_dz_pub_topic, mqtt_msg);
}
void send_mqtt_state_msg(struct mg_connection *nc, char *dev_name, aqledstate state)
{
static char mqtt_pub_topic[250];
sprintf(mqtt_pub_topic, "%s/%s",_aqualink_config->mqtt_aq_topic, dev_name);
send_mqtt(nc, mqtt_pub_topic, (state==OFF?"0":"1"));
}
void send_mqtt_temp_msg(struct mg_connection *nc, char *dev_name, long value)
{
static char mqtt_pub_topic[250];
static char degC[5];
sprintf(degC, "%.2f", (_aqualink_data->temp_units==FAHRENHEIT)?degFtoC(value):value );
sprintf(mqtt_pub_topic, "%s/%s", _aqualink_config->mqtt_aq_topic, dev_name);
send_mqtt(nc, mqtt_pub_topic, degC);
}
void send_mqtt_setpoint_msg(struct mg_connection *nc, char *dev_name, long value)
{
static char mqtt_pub_topic[250];
static char degC[5];
sprintf(degC, "%.2f", (_aqualink_data->temp_units==FAHRENHEIT)?degFtoC(value):value );
sprintf(mqtt_pub_topic, "%s/%s/setpoint", _aqualink_config->mqtt_aq_topic, dev_name);
send_mqtt(nc, mqtt_pub_topic, degC);
}
void mqtt_broadcast_aqualinkstate(struct mg_connection *nc)
{
int i;
//logMessage(LOG_INFO, "mqtt_broadcast_aqualinkstate: START\n");
if (_aqualink_data->air_temp != TEMP_UNKNOWN && _aqualink_data->air_temp != _last_mqtt_aqualinkdata.air_temp) {
_last_mqtt_aqualinkdata.air_temp = _aqualink_data->air_temp;
send_mqtt_temp_msg(nc, AIR_TEMP_TOPIC, _aqualink_data->air_temp);
send_domoticz_mqtt_msg(nc, _aqualink_config->dzidx_air_temp, (_aqualink_data->temp_units==FAHRENHEIT)?degFtoC(_aqualink_data->air_temp):_aqualink_data->air_temp);
}
if (_aqualink_data->pool_temp != TEMP_UNKNOWN && _aqualink_data->pool_temp != _last_mqtt_aqualinkdata.pool_temp) {
_last_mqtt_aqualinkdata.pool_temp = _aqualink_data->pool_temp;
send_mqtt_temp_msg(nc, POOL_TEMP_TOPIC, _aqualink_data->pool_temp);
send_domoticz_mqtt_msg(nc, _aqualink_config->dzidx_pool_water_temp, (_aqualink_data->temp_units==FAHRENHEIT)?degFtoC(_aqualink_data->pool_temp):_aqualink_data->pool_temp);
}
if (_aqualink_data->spa_temp != TEMP_UNKNOWN && _aqualink_data->spa_temp != _last_mqtt_aqualinkdata.spa_temp) {
_last_mqtt_aqualinkdata.spa_temp = _aqualink_data->spa_temp;
send_mqtt_temp_msg(nc, SPA_TEMP_TOPIC, _aqualink_data->spa_temp);
send_domoticz_mqtt_msg(nc, _aqualink_config->dzidx_spa_water_temp, (_aqualink_data->temp_units==FAHRENHEIT)?degFtoC(_aqualink_data->spa_temp):_aqualink_data->pool_temp);
}
if (_aqualink_data->pool_htr_set_point != TEMP_UNKNOWN && _aqualink_data->pool_htr_set_point != _last_mqtt_aqualinkdata.pool_htr_set_point) {
_last_mqtt_aqualinkdata.pool_htr_set_point = _aqualink_data->pool_htr_set_point;
send_mqtt_setpoint_msg(nc, BTN_POOL_HTR, _aqualink_data->pool_htr_set_point);
// removed until domoticz has a better virtuel thermostat
//send_domoticz_mqtt_msg_setpoint(nc, _aqualink_config->dzidx_pool_thermostat, 0, degFtoC(_aqualink_data->pool_htr_set_point));
}
if (_aqualink_data->spa_htr_set_point != TEMP_UNKNOWN && _aqualink_data->spa_htr_set_point != _last_mqtt_aqualinkdata.spa_htr_set_point) {
_last_mqtt_aqualinkdata.spa_htr_set_point = _aqualink_data->spa_htr_set_point;
send_mqtt_setpoint_msg(nc, BTN_SPA_HTR, _aqualink_data->spa_htr_set_point);
// removed until domoticz has a better virtuel thermostat
//send_domoticz_mqtt_msg_setpoint(nc, _aqualink_config->dzidx_spa_thermostat, 0, degFtoC(_aqualink_data->spa_htr_set_point));
}
if (_aqualink_data->frz_protect_set_point != TEMP_UNKNOWN && _aqualink_data->frz_protect_set_point != _last_mqtt_aqualinkdata.frz_protect_set_point) {
_last_mqtt_aqualinkdata.frz_protect_set_point = _aqualink_data->frz_protect_set_point;
send_mqtt_setpoint_msg(nc, FREEZE_PROTECT, _aqualink_data->frz_protect_set_point);
}
//logMessage(LOG_INFO, "mqtt_broadcast_aqualinkstate: START LEDs\n");
//if (time(NULL) % 2) {} <-- use to determin odd/even second in time to make state flash on enabled.
// Loop over LED's and send any changes.
for (i=0; i < TOTAL_BUTTONS; i++) {
//logMessage(LOG_INFO, "LED %d : new state %d | old state %d\n", i, _aqualink_data->aqbuttons[i].led->state, _last_mqtt_aqualinkdata.aqualinkleds[i].state);
if (_last_mqtt_aqualinkdata.aqualinkleds[i].state != _aqualink_data->aqbuttons[i].led->state){
_last_mqtt_aqualinkdata.aqualinkleds[i].state = _aqualink_data->aqbuttons[i].led->state;
if (_aqualink_data->aqbuttons[i].dz_idx != DZ_NULL_IDX) {
send_mqtt_state_msg(nc, _aqualink_data->aqbuttons[i].name, _aqualink_data->aqbuttons[i].led->state);
send_domoticz_mqtt_msg(nc, _aqualink_data->aqbuttons[i].dz_idx, (_aqualink_data->aqbuttons[i].led->state==OFF?DZ_OFF:DZ_ON));
}
// Send mqtt
}
}
//logMessage(LOG_INFO, "mqtt_broadcast_aqualinkstate: END\n");
}
#endif //MG_DISABLE_MQTT
//
int getTempforMeteohub(char *buffer)
{
int length = 0;
if (_aqualink_data->air_temp != TEMP_UNKNOWN)
length += sprintf(buffer+length, "t0 %d\n",(int)degFtoC(_aqualink_data->air_temp)*10);
else
length += sprintf(buffer+length, "t0 \n");
if (_aqualink_data->pool_temp != TEMP_UNKNOWN)
length += sprintf(buffer+length, "t1 %d\n",(int)degFtoC(_aqualink_data->pool_temp)*10);
else
length += sprintf(buffer+length, "t1 \n");
return strlen(buffer);
}
void set_light_mode(char *value)
{
char buf[20];
// 5 below is light index, need to look this up so it's not hard coded.
sprintf(buf, "%-5s%-5d%.2f",value, 5, _aqualink_config->light_programming_mode );
aq_programmer(AQ_SET_COLORMODE, buf, _aqualink_data);
}
void action_web_request(struct mg_connection *nc, struct http_message *http_msg) {
// struct http_message *http_msg = (struct http_message *)ev_data;
if (getLogLevel() >= LOG_INFO) { // Simply for log message, check we are at
// this log level before running all this
// junk
char *uri = (char *)malloc(http_msg->uri.len + http_msg->query_string.len + 2);
strncpy(uri, http_msg->uri.p, http_msg->uri.len + http_msg->query_string.len + 1);
uri[http_msg->uri.len + http_msg->query_string.len + 1] = '\0';
logMessage(LOG_INFO, "URI request: '%s'\n", uri);
free(uri);
}
// If we have a get request, pass it
if (strstr(http_msg->method.p, "GET") && http_msg->query_string.len > 0) {
char command[20];
mg_get_http_var(&http_msg->query_string, "command", command, sizeof(command));
logMessage(LOG_INFO, "WEB: Message command='%s'\n", command);
// if (strstr(http_msg->query_string.p, "command=status")) {
if (strcmp(command, "status") == 0) {
char data[JSON_STATUS_SIZE];
int size = build_aqualink_status_JSON(_aqualink_data, data, JSON_STATUS_SIZE);
mg_send_head(nc, 200, size, "Content-Type: application/json");
mg_send(nc, data, size);
//} else if (strstr(http_msg->query_string.p, "command=mhstatus")) {
} else if (strcmp(command, "mhstatus") == 0) {
char data[20];
int size = getTempforMeteohub(data);
mg_send_head(nc, 200, size, "Content-Type: text/plain");
mg_send(nc, data, size);
} else if (strcmp(command, "poollightmode") == 0) {
char value[20];
mg_get_http_var(&http_msg->query_string, "value", value, sizeof(value));
//aq_programmer(AQ_SET_COLORMODE, value, _aqualink_data);
set_light_mode(value);
mg_send_head(nc, 200, strlen(GET_RTN_OK), "Content-Type: text/plain");
mg_send(nc, GET_RTN_OK, strlen(GET_RTN_OK));
} else {
int i;
for (i = 0; i < TOTAL_BUTTONS; i++) {
if (strcmp(command, _aqualink_data->aqbuttons[i].name) == 0) {
char value[20];
char *rtn;
mg_get_http_var(&http_msg->query_string, "value", value, sizeof(value));
// logMessage (LOG_INFO, "Web Message command='%s'\n",command);
// aq_programmer(AQ_SEND_CMD, (char
// *)&_aqualink_data->aqbuttons[i].code, _aqualink_data);
logMessage(LOG_DEBUG, "WEB: Message request '%s' change state to '%s'\n", command, value);
if (strcmp(value, "on") == 0) {
if (_aqualink_data->aqbuttons[i].led->state == OFF || _aqualink_data->aqbuttons[i].led->state == FLASH) {
aq_programmer(AQ_SEND_CMD, (char *)&_aqualink_data->aqbuttons[i].code, _aqualink_data);
rtn = GET_RTN_OK;
logMessage(LOG_INFO, "WEB: turn ON '%s' changed state to '%s'\n", command, value);
} else {
rtn = GET_RTN_NOT_CHANGED;
logMessage(LOG_INFO, "WEB: '%s' is already on '%s', current state %d\n", command, value, _aqualink_data->aqbuttons[i].led->state);
}
} else if (strcmp(value, "off") == 0) {
if (_aqualink_data->aqbuttons[i].led->state == ON ||
_aqualink_data->aqbuttons[i].led->state == ENABLE ||
_aqualink_data->aqbuttons[i].led->state == FLASH) {
aq_programmer(AQ_SEND_CMD, (char *)&_aqualink_data->aqbuttons[i].code, _aqualink_data);
rtn = GET_RTN_OK;
logMessage(LOG_INFO, "WEB: turn Off '%s' changed state to '%s'\n", command, value);
} else {
rtn = GET_RTN_NOT_CHANGED;
logMessage(LOG_INFO, "WEB: '%s' is already off '%s', current state %d\n", command, value, _aqualink_data->aqbuttons[i].led->state);
}
} else { // Blind switch
aq_programmer(AQ_SEND_CMD, (char *)&_aqualink_data->aqbuttons[i].code, _aqualink_data);
rtn = GET_RTN_OK;
logMessage(LOG_INFO, "WEB: '%s' blindly changed state\n", command, value);
}
logMessage(LOG_DEBUG, "WEB: On=%d, Off=%d, Enable=%d, Flash=%d\n", ON, OFF, ENABLE, FLASH);
// NSF change OK and 2 below to a constant
mg_send_head(nc, 200, strlen(rtn), "Content-Type: text/plain");
mg_send(nc, rtn, strlen(rtn));
// NSF place check we found command here
}
}
}
// If we get here, got a bad query
mg_send_head(nc, 200, sizeof(GET_RTN_UNKNOWN), "Content-Type: text/plain");
mg_send(nc, GET_RTN_UNKNOWN, sizeof(GET_RTN_UNKNOWN));
} else {
struct mg_serve_http_opts opts;
memset(&opts, 0, sizeof(opts)); // Reset all options to defaults
opts.document_root = _web_root; // Serve files from the current directory
// logMessage (LOG_DEBUG, "Doc root=%s\n",opts.document_root);
mg_serve_http(nc, http_msg, s_http_server_opts);
}
}
void action_websocket_request(struct mg_connection *nc, struct websocket_message *wm) {
char buffer[50];
struct JSONwebrequest request;
strncpy(buffer, (char *)wm->data, wm->size);
buffer[wm->size] = '\0';
// logMessage (LOG_DEBUG, "buffer '%s'\n", buffer);
parseJSONwebrequest(buffer, &request);
logMessage(LOG_INFO, "WS: Message - Key '%s' Value '%s' | Key2 '%s' Value2 '%s'\n", request.first.key, request.first.value, request.second.key,
request.second.value);
if (strcmp(request.first.key, "command") == 0) {
if (strcmp(request.first.value, "GET_AUX_LABELS") == 0) {
char labels[JSON_LABEL_SIZE];
build_aux_labels_JSON(_aqualink_data, labels, JSON_LABEL_SIZE);
ws_send(nc, labels);
} else { // Search for value in command list
int i;
for (i = 0; i < TOTAL_BUTTONS; i++) {
if (strcmp(request.first.value, _aqualink_data->aqbuttons[i].name) == 0) {
logMessage (LOG_INFO, "WS: button '%s' pressed\n",_aqualink_data->aqbuttons[i].name);
// send_command( (unsigned char)_aqualink_data->aqbuttons[i].code);
aq_programmer(AQ_SEND_CMD, (char *)&_aqualink_data->aqbuttons[i].code, _aqualink_data);
break;
// NSF place check we found command here
}
}
}
} else if (strcmp(request.first.key, "parameter") == 0) {
if (strcmp(request.first.value, "FRZ_PROTECT") == 0) {
aq_programmer(AQ_SET_FRZ_PROTECTION_TEMP, request.second.value, _aqualink_data);
} else if (strcmp(request.first.value, "POOL_HTR") == 0) {
aq_programmer(AQ_SET_POOL_HEATER_TEMP, request.second.value, _aqualink_data);
} else if (strcmp(request.first.value, "SPA_HTR") == 0) {
aq_programmer(AQ_SET_SPA_HEATER_TEMP, request.second.value, _aqualink_data);
} else if (strcmp(request.first.value, "POOL_LIGHT_MODE") == 0) {
//aq_programmer(AQ_SET_COLORMODE, request.second.value, _aqualink_data);
set_light_mode(request.second.value);
} else {
logMessage(LOG_DEBUG, "WS: Unknown parameter %s\n", request.first.value);
}
}
}
void action_mqtt_message(struct mg_connection *nc, struct mg_mqtt_message *msg) {
int i;
//printf("Topic %.*s\n",msg->topic.len, msg->topic.p);
// get the parts from the topic
char *pt1 = (char *)&msg->topic.p[strlen(_aqualink_config->mqtt_aq_topic)+1];
char *pt2 = NULL;
char *pt3 = NULL;
for (i=10; i < msg->topic.len; i++) {
if ( msg->topic.p[i] == '/' ) {
if (pt2 == NULL) {
pt2 = (char *)&msg->topic.p[++i];
} else if (pt3 == NULL) {
pt3 = (char *)&msg->topic.p[++i];
break;
}
}
}
logMessage(LOG_DEBUG, "MQTT: topic %.*s %.2f\n",msg->topic.len, msg->topic.p, atof(msg->payload.p));
//only care about topics with set at the end.
//aqualinkd/Freeze/setpoint/set
//aqualinkd/Filter_Pump/set
//aqualinkd/Pool_Heater/setpoint/set
//aqualinkd/Pool_Heater/set
if (pt3 != NULL && (strncmp(pt2, "setpoint", 8) == 0) && (strncmp(pt3, "set", 3) == 0)) {
int val = _aqualink_data->unactioned.value = (_aqualink_data->temp_units == FAHRENHEIT) ? round(degCtoF(atof(msg->payload.p))) : round(atof(msg->payload.p));
if (strncmp(pt1, BTN_POOL_HTR, strlen(BTN_POOL_HTR)) == 0) {
if (val <= HEATER_MAX && val >= MEATER_MIN) {
logMessage(LOG_INFO, "MQTT: request to set pool heater setpoint to %.2fc\n", atof(msg->payload.p));
_aqualink_data->unactioned.type = POOL_HTR_SETOINT;
} else {
logMessage(LOG_ERR, "MQTT: request to set pool heater setpoint to %.2fc is outside of range\n", atof(msg->payload.p));
send_mqtt_setpoint_msg(nc, BTN_POOL_HTR, _aqualink_data->pool_htr_set_point);
}
} else if (strncmp(pt1, BTN_SPA_HTR, strlen(BTN_SPA_HTR)) == 0) {
if (val <= HEATER_MAX && val >= MEATER_MIN) {
logMessage(LOG_INFO, "MQTT: request to set spa heater setpoint to %.2fc\n", atof(msg->payload.p));
_aqualink_data->unactioned.type = SPA_HTR_SETOINT;
} else {
logMessage(LOG_ERR, "MQTT: request to set spa heater setpoint to %.2fc is outside of range\n", atof(msg->payload.p));
send_mqtt_setpoint_msg(nc, BTN_SPA_HTR, _aqualink_data->spa_htr_set_point);
}
} else if (strncmp(pt1, FREEZE_PROTECT, strlen(FREEZE_PROTECT)) == 0) {
if (val <= FREEZE_PT_MAX && val >= FREEZE_PT_MIN) {
logMessage(LOG_INFO, "MQTT: request to set freeze protect to %.2fc\n", atof(msg->payload.p));
_aqualink_data->unactioned.type = FREEZE_SETPOINT;
} else {
logMessage(LOG_ERR, "MQTT: request to set freeze protect to %.2fc is outside of range\n", atof(msg->payload.p));
}
} else {
// Not sure what the setpoint is, ignore.
logMessage(LOG_DEBUG, "MQTT: ignoring %.*s don't recognise button setpoint\n", msg->topic.len, msg->topic.p);
return;
}
// logMessage(LOG_INFO, "MQTT: topic %.*s %.2f, setting %s\n",msg->topic.len, msg->topic.p, atof(msg->payload.p));
time(&_aqualink_data->unactioned.requested);
} else if (pt2 != NULL && (strncmp(pt2, "set", 3) == 0) && (strncmp(pt2, "setpoint", 8) != 0)) {
// Must be a switch on / off
for (i=0; i < TOTAL_BUTTONS; i++) {
if (strncmp(pt1, _aqualink_data->aqbuttons[i].name, strlen(_aqualink_data->aqbuttons[i].name)) == 0 ){
logMessage(LOG_INFO, "MQTT: MATCH %s to topic %.*s\n",_aqualink_data->aqbuttons[i].name,msg->topic.len, msg->topic.p);
// Message is either a 1 or 0 for on or off
int status = atoi(msg->payload.p);
if ( status > 1 || status < 0) {
logMessage(LOG_INFO, "MQTT: received unknown status of '%.*s' for '%s', Ignoring!\n", msg->payload.len, msg->payload.p, status, _aqualink_data->aqbuttons[i].name);
}
else if ( (_aqualink_data->aqbuttons[i].led->state == OFF && status==0) ||
(status == 1 && (_aqualink_data->aqbuttons[i].led->state == ON ||
_aqualink_data->aqbuttons[i].led->state == FLASH ||
_aqualink_data->aqbuttons[i].led->state == ENABLE))) {
logMessage(LOG_INFO, "MQTT: received '%s' for '%s', already '%s', Ignoring\n", (status==0?"OFF":"ON"), _aqualink_data->aqbuttons[i].name, (status==0?"OFF":"ON"));
} else {
logMessage(LOG_INFO, "MQTT: received '%s' for '%s', turning '%s'\n", (status==0?"OFF":"ON"), _aqualink_data->aqbuttons[i].name,(status==0?"OFF":"ON"));
aq_programmer(AQ_SEND_CMD, (char *)&_aqualink_data->aqbuttons[i].code, _aqualink_data);
}
break;
}
}
} else {
// We don's care
logMessage(LOG_DEBUG, "MQTT: ignoring topic %.*s\n",msg->topic.len, msg->topic.p);
}
}
void action_domoticz_mqtt_message(struct mg_connection *nc, struct mg_mqtt_message *msg) {
int idx = -1;
int nvalue = -1;
int i;
char svalue[DZ_SVALUE_LEN];
if (parseJSONmqttrequest(msg->payload.p, msg->payload.len, &idx, &nvalue, svalue)) {
logMessage(LOG_DEBUG, "MQTT: DZ: Received message IDX=%d nValue=%d sValue=%s\n", idx, nvalue, svalue);
for (i=0; i < TOTAL_BUTTONS; i++) {
if (_aqualink_data->aqbuttons[i].dz_idx == idx){
//NSF, should try to simplify this if statment, but not easy since AQ ON and DZ ON are different, and AQ has other states.
if ( (_aqualink_data->aqbuttons[i].led->state == OFF && nvalue==DZ_OFF) ||
(nvalue == DZ_ON && (_aqualink_data->aqbuttons[i].led->state == ON ||
_aqualink_data->aqbuttons[i].led->state == FLASH ||
_aqualink_data->aqbuttons[i].led->state == ENABLE))) {
logMessage(LOG_INFO, "MQTT: DZ: received '%s' for '%s', already '%s', Ignoring\n", (nvalue==DZ_OFF?"OFF":"ON"), _aqualink_data->aqbuttons[i].name, (nvalue==DZ_OFF?"OFF":"ON"));
} else {
// NSF Below if needs to check that the button pressed is actually a light. Add this later
if (_aqualink_data->active_thread.ptype == AQ_SET_COLORMODE ) {
logMessage(LOG_NOTICE, "MQTT: DZ: received '%s' for '%s', IGNORING as we are programming light mode\n", (nvalue==DZ_OFF?"OFF":"ON"), _aqualink_data->aqbuttons[i].name);
} else {
logMessage(LOG_INFO, "MQTT: DZ: received '%s' for '%s', turning '%s'\n", (nvalue==DZ_OFF?"OFF":"ON"), _aqualink_data->aqbuttons[i].name,(nvalue==DZ_OFF?"OFF":"ON"));
aq_programmer(AQ_SEND_CMD, (char *)&_aqualink_data->aqbuttons[i].code, _aqualink_data);
}
}
break; // no need to continue in for loop, we found button.
}
}
/* removed until domoticz has a better virtual thermostat
if (idx == _aqualink_config->dzidx_pool_thermostat) {
float degC = atof(svalue);
int degF = (int)degCtoF(degC);
if (degC > 0.0 && 1 < (degF - _aqualink_data->pool_htr_set_point)) {
logMessage(LOG_INFO, "MQTT: DZ: received temp setting '%s' for 'pool heater setpoint' old value %d(f) setting to %f(c) %d(f)\n",svalue, _aqualink_data->pool_htr_set_point, degC, degF);
//aq_programmer(AQ_SET_POOL_HEATER_TEMP, (int)degCtoF(degl), _aqualink_data);
} else {
logMessage(LOG_INFO, "MQTT: DZ: received temp setting '%s' for 'pool heater setpoint' matched current setting (or was zero), ignoring!\n", svalue);
}
} else if (idx == _aqualink_config->dzidx_spa_thermostat) {
float degC = atof(svalue);
int degF = (int)degCtoF(degC);
if (degC > 0.0 && 1 < (degF - _aqualink_data->spa_htr_set_point)) {
logMessage(LOG_INFO, "MQTT: DZ: received temp setting '%s' for 'spa heater setpoint' old value %d(f) setting to %f(c) %d(f)\n",svalue, _aqualink_data->spa_htr_set_point, degC, degF);
//aq_programmer(AQ_SET_POOL_HEATER_TEMP, (int)degCtoF(degl), _aqualink_data);
} else {
logMessage(LOG_INFO, "MQTT: DZ: received temp setting '%s' for 'spa heater setpoint' matched current setting (or was zero), ignoring!\n", svalue);
}
}*/
//printf("Finished checking IDX for setpoint\n");
}
// NSF Need to check idx against ours and decide if we are interested in it.
}
static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
struct mg_mqtt_message *mqtt_msg;
struct http_message *http_msg;
struct websocket_message *ws_msg;
//static double last_control_time;
// logMessage (LOG_DEBUG, "Event\n");
switch (ev) {
case MG_EV_HTTP_REQUEST:
//nc->user_data = WEB;
http_msg = (struct http_message *)ev_data;
action_web_request(nc, http_msg);
break;
case MG_EV_WEBSOCKET_HANDSHAKE_DONE:
//nc->user_data = WS;
logMessage(LOG_DEBUG, "++ Websocket joined\n");
break;
case MG_EV_WEBSOCKET_FRAME:
ws_msg = (struct websocket_message *)ev_data;
action_websocket_request(nc, ws_msg);
break;
case MG_EV_CLOSE:
if (is_websocket(nc)) {
logMessage(LOG_DEBUG, "-- Websocket left\n");
} else if (is_mqtt(nc)) {
logMessage(LOG_WARNING, "MQTT Connection closed\n");
_mqtt_exit_flag = true;
}
break;
case MG_EV_CONNECT: {
//nc->user_data = MQTT;
//nc->flags |= MG_F_USER_1; // NFS Need to readup on this
set_mqtt(nc);
_mqtt_exit_flag = false;
//char *MQTT_id = "AQUALINK_MQTT_TEST_ID";
struct mg_send_mqtt_handshake_opts opts;
memset(&opts, 0, sizeof(opts));
opts.user_name = _aqualink_config->mqtt_user;
opts.password = _aqualink_config->mqtt_passwd;
opts.keep_alive = 5;
opts.flags |= MG_MQTT_CLEAN_SESSION; // NFS Need to readup on this
mg_set_protocol_mqtt(nc);
mg_send_mqtt_handshake_opt(nc, _aqualink_config->mqtt_ID, opts);
logMessage(LOG_INFO, "MQTT: Subscribing mqtt with id of: %s\n", _aqualink_config->mqtt_ID);
//last_control_time = mg_time();
} break;
case MG_EV_MQTT_CONNACK:
{
struct mg_mqtt_topic_expression topics[2];
char aq_topic[30];
int qos=0;// can't be bothered with ack, so set to 0
logMessage(LOG_INFO, "MQTT: Connection acknowledged\n");
mqtt_msg = (struct mg_mqtt_message *)ev_data;
if (mqtt_msg->connack_ret_code != MG_EV_MQTT_CONNACK_ACCEPTED) {
logMessage(LOG_WARNING, "Got mqtt connection error: %d\n", mqtt_msg->connack_ret_code);
_mqtt_exit_flag = true;
}
snprintf(aq_topic, 29, "%s/#", _aqualink_config->mqtt_aq_topic);
if (_aqualink_config->mqtt_aq_topic != NULL && _aqualink_config->mqtt_dz_sub_topic != NULL) {
topics[0].topic = aq_topic;
topics[0].qos = qos;
topics[1].topic = _aqualink_config->mqtt_dz_sub_topic;
topics[1].qos = qos;
mg_mqtt_subscribe(nc, topics, 2, 42);
logMessage(LOG_INFO, "MQTT: Subscribing to '%s'\n", aq_topic);
logMessage(LOG_INFO, "MQTT: Subscribing to '%s'\n", _aqualink_config->mqtt_dz_sub_topic);
}
else if (_aqualink_config->mqtt_aq_topic != NULL) {
topics[0].topic = aq_topic;
topics[0].qos = qos;
mg_mqtt_subscribe(nc, topics, 1, 42);
logMessage(LOG_INFO, "MQTT: Subscribing to '%s'\n", aq_topic);
}
else if (_aqualink_config->mqtt_aq_topic != NULL) {
topics[0].topic = _aqualink_config->mqtt_dz_sub_topic;;
topics[0].qos = qos;
mg_mqtt_subscribe(nc, topics, 1, 42);
logMessage(LOG_INFO, "MQTT: Subscribing to '%s'\n", _aqualink_config->mqtt_dz_sub_topic);
}
}
break;
case MG_EV_MQTT_PUBACK:
mqtt_msg = (struct mg_mqtt_message *)ev_data;
logMessage(LOG_INFO, "MQTT: Message publishing acknowledged (msg_id: %d)\n", mqtt_msg->message_id);
break;
case MG_EV_MQTT_SUBACK:
logMessage(LOG_INFO, "MQTT: Subscription(s) acknowledged\n");
break;
case MG_EV_MQTT_PUBLISH:
mqtt_msg = (struct mg_mqtt_message *)ev_data;
if (mqtt_msg->message_id != 0) {
logMessage(LOG_INFO, "MQTT: received (msg_id: %d), looks like my own message, ignoring\n", mqtt_msg->message_id);
}
// NSF Need to change strlen to a global so it's not executed every time we check a topic
if (strncmp(mqtt_msg->topic.p, _aqualink_config->mqtt_aq_topic, strlen(_aqualink_config->mqtt_aq_topic)) == 0) {
action_mqtt_message(nc, mqtt_msg);
}
if (strncmp(mqtt_msg->topic.p, _aqualink_config->mqtt_dz_sub_topic, strlen(_aqualink_config->mqtt_dz_sub_topic)) == 0) {
action_domoticz_mqtt_message(nc, mqtt_msg);
}
break;
/*
// MQTT ping wasn't working in previous versions.
case MG_EV_POLL: {
struct mg_mqtt_proto_data *pd = (struct mg_mqtt_proto_data *)nc->proto_data;
double now = mg_time();
if (pd->keep_alive > 0 && last_control_time > 0 && (now - last_control_time) > pd->keep_alive) {
logMessage(LOG_INFO, "MQTT: Sending MQTT ping\n");
mg_mqtt_ping(nc);
last_control_time = now;
}
}
break;*/
}
}
void start_mqtt(struct mg_mgr *mgr) {
logMessage (LOG_NOTICE, "Starting MQTT client to %s\n", _aqualink_config->mqtt_server);
if (mg_connect(mgr, _aqualink_config->mqtt_server, ev_handler) == NULL) {
logMessage (LOG_ERR, "Failed to create MQTT listener to %s\n", _aqualink_config->mqtt_server);
} else {
int i;
for (i=0; i < TOTAL_BUTTONS; i++) {
_last_mqtt_aqualinkdata.aqualinkleds[i].state = LED_S_UNKNOWN;
}
_mqtt_exit_flag = false; // set here to stop multiple connects, if it fails truley fails it will get set to false.
}
}
//bool start_web_server(struct mg_mgr *mgr, struct aqualinkdata *aqdata, char *port, char* web_root) {
bool start_net_services(struct mg_mgr *mgr, struct aqualinkdata *aqdata, struct aqconfig *aqconfig) {
struct mg_connection *nc;
_aqualink_data = aqdata;
_aqualink_config = aqconfig;
signal(SIGTERM, signal_handler);
signal(SIGINT, signal_handler);
setvbuf(stdout, NULL, _IOLBF, 0);
setvbuf(stderr, NULL, _IOLBF, 0);
mg_mgr_init(mgr, NULL);
logMessage (LOG_NOTICE, "Starting web server on port %s\n", _aqualink_config->socket_port);
nc = mg_bind(mgr, _aqualink_config->socket_port, ev_handler);
if (nc == NULL) {
logMessage (LOG_ERR, "Failed to create listener\n");
return false;
}
// Set up HTTP server parameters
mg_set_protocol_http_websocket(nc);
s_http_server_opts.document_root = _aqualink_config->web_directory; // Serve current directory
s_http_server_opts.enable_directory_listing = "yes";
#ifndef MG_DISABLE_MQTT
// Start MQTT
start_mqtt(mgr);
#endif
return true;
}

18
net_services.h Normal file
View File

@ -0,0 +1,18 @@
#ifndef NET_SERVICES_H_
#define NET_SERVICES_H_
#define GET_RTN_OK "Ok"
#define GET_RTN_UNKNOWN "Unknown command"
#define GET_RTN_NOT_CHANGED "Not Changed"
#define FREEZE_PROTECT "Freeze_Protect"
//void main_server();
//void main_server_TEST(struct aqualinkdata *aqdata, char *s_http_port);
//bool start_web_server(struct mg_mgr *mgr, struct aqualinkdata *aqdata, char *port, char* web_root);
bool start_net_services(struct mg_mgr *mgr, struct aqualinkdata *aqdata, struct aqconfig *aqconfig);
void broadcast_aqualinkstate(struct mg_connection *nc);
void broadcast_aqualinkstate_error(struct mg_connection *nc, char *msg);
#endif // WEB_SERVER_H_

BIN
net_services.o Normal file

Binary file not shown.

BIN
release/aqualinkd Executable file

Binary file not shown.

89
release/aqualinkd.conf Executable file
View File

@ -0,0 +1,89 @@
# aqualinkd.conf
#
# Created on: Aug 17, 2012
#
# The directory where the web files are stored
#web_directory=/nas/data/Development/Raspberry/aqualink/aqualinkd/web
web_directory=/var/www/aqualinkd/
# Log to file, comment out if you do not want to log to file
#log_file=/var/log/aqualinkd.log
# The log level. [DEBUG, INFO, NOTICE, WARNING, ERROR]
# Pick the highest level, and all levels below will be sent to syslog.
# your syslog settings may be set to only display messages above a certian level
# in which case make sure you use the log_file settings to capture everything
# you want when debugging
# so, NOTICE also prints WARNING & ERROR
# DEBUG would print everything possible
#log_level=DEBUG
log_level=INFO
#log_level=NOTICE
# The socket port that the daemon listens to
# If you change this from 80, remember to update aqualink.service.avahi
socket_port=80
# The serial port the daemon access to read the Aqualink RS8
serial_port=/dev/ttyUSB0
# mqtt stuff
mqtt_address = localhost:1883
#mqtt_user = someusername
#mqtt_passwd = somepassword
mqtt_dz_pub_topic = domoticz/in
mqtt_dz_sub_topic = domoticz/out
mqtt_aq_topic = aqualinkd
# The id of the Aqualink terminal device. Devices probed by RS8 master are:
# 08-0b, 10-13, 18-1b, 20-23,
#device_id=0a
device_id=0x0a
# Light probramming mode. 0=safe mode, but slow.
# any number greater is seconds to wait between button presses. 0.6 seems to be safe, but dill sepend on hardware
light_programming_mode=0.6
# Domoticz ID's for temps.
air_temp_dzidx=13
pool_water_temp_dzidx=14
spa_water_temp_dzidx=15
# Labels for standard butons (shown in web UI), and domoticz idx's
button_01_label=Filter Pump
button_01_dzidx=37
button_02_label=Spa Mode
button_02_dzidx=38
button_03_label=Cleaner
button_03_dzidx=39
button_04_label=Waterfall
button_04_dzidx=40
button_05_label=Spa Blower
button_05_dzidx=41
button_06_label=Pool Light
button_06_dzidx=42
button_07_label=Spa Light
button_07_dzidx=43
button_08_label=NONE
button_08_dzidx=NONE
button_09_label=NONE
button_09_dzidx=NONE
button_10_label=Pool Heater
button_10_dzidx=44
button_11_label=Spa Heater
button_11_dzidx=56
button_12_label=Solar Heater
button_12_dzidx=NONE

View File

@ -0,0 +1,5 @@
# Defaults / Configuration options for aqualinkd
#
# -c config file
OPTS = -c /etc/aqualinkd.conf

21
release/aqualinkd.service Executable file
View File

@ -0,0 +1,21 @@
[Unit]
Description=Aqualink RS daemon
ConditionPathExists=/etc/aqualinkd.conf
After=network.target multi-user.target
Requires=network.target
[Service]
Type=forking
#RemainAfterExit=no
#StartLimitInterval=0
PIDFile=/run/aqualinkd.pid
EnvironmentFile=/etc/default/aqualinkd
ExecStart=/usr/local/bin/aqualinkd $OPTS
ExecReload=/bin/kill -HUP $MAINPID
#Restart=on-failure
KillMode=process
RestartSec=2
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,9 @@
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">%h HTTP</name>
<service>
<type>_http._tcp</type>
<port>80</port>
</service>
</service-group>

View File

@ -0,0 +1,32 @@
# aqualinkd - aqualinkd job file
#
# check with `initctl check-config aqualinkd`
#
description "aqualink RS daemon service"
author "Me <myself@i.com>"
# Stanzas
#
# Stanzas control when and how a process is started and stopped
# See a list of stanzas here: http://upstart.ubuntu.com/wiki/Stanzas#respawn
# When to start the service
start on runlevel [2345]
# When to stop the service
stop on runlevel [016]
# Automatically restart process if crashed
respawn
# Essentially lets upstart know the process will detach itself to the background
expect fork
# Run before process
#pre-start script
# [ -d /var/run/myservice ] || mkdir -p /var/run/myservice
# echo "Put bash code here"
#end script
# Start the process
exec /usr/local/bin/aqualinkd -c /etc/aqualinkd/aqualinkd.conf

81
release/install.sh Executable file
View File

@ -0,0 +1,81 @@
#!/bin/bash
#
# ROOT=/nas/data/Development/Raspberry/gpiocrtl/test-install
#
BUILD="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
SERVICE="aqualinkd"
BIN="aqualinkd"
CFG="aqualinkd.conf"
SRV="aqualinkd.service"
DEF="aqualinkd"
MDNS="aqualinkd.service"
BINLocation="/usr/local/bin"
CFGLocation="/etc"
SRVLocation="/etc/systemd/system"
DEFLocation="/etc/default"
WEBLocation="/var/www/aqualinkd/"
MDNSLocation="/etc/avahi/services/"
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi
if [[ $(mount | grep " / " | grep "(ro,") ]]; then
echo "Root filesystem is readonly, can't install"
exit 1
fi
# Exit if we can't find systemctl
command -v systemctl >/dev/null 2>&1 || { echo "This script needs systemd's systemctl manager, Please check path or install manually" >&2; exit 1; }
# stop service, hide any error, as the service may not be installed yet
systemctl stop $SERVICE > /dev/null 2>&1
SERVICE_EXISTS=$(echo $?)
# copy files to locations, but only copy cfg if it doesn;t already exist
cp $BUILD/$BIN $BINLocation/$BIN
cp $BUILD/$SRV $SRVLocation/$SRV
if [ -f $CFGLocation/$CFG ]; then
echo "Config exists, did not copy new config, you may need to edit existing! $CFGLocation/$CFG"
else
cp $BUILD/$CFG $CFGLocation/$CFG
fi
if [ -f $DEFLocation/$DEF ]; then
echo "Defaults exists, did not copy new defaults to $DEFLocation/$DEF"
else
cp $BUILD/$DEF.defaults $DEFLocation/$DEF
fi
if [ -f $MDNSLocation/$MDNS ]; then
echo "Avahi/mDNS defaults exists, did not copy new defaults to $MDNSLocation/$MDNS"
else
if [ -d "$MDNSLocation" ]; then
cp $BUILD/$MDNS.avahi $MDNSLocation/$MDNS
else
echo "Avahi/mDNS may not be installed, not copying $MDNSLocation/$MDNS"
fi
fi
if [ ! -d "$WEBLocation" ]; then
mkdir -p $WEBLocation
fi
cp -r $BUILD/../web/* $WEBLocation
systemctl enable $SERVICE
systemctl daemon-reload
if [ $SERVICE_EXISTS -eq 0 ]; then
echo "Starting daemon $SERVICE"
systemctl start $SERVICE
fi

422
utils.c Normal file
View File

@ -0,0 +1,422 @@
/*
* Copyright (c) 2017 Shaun Feakes - All rights reserved
*
* You may use redistribute and/or modify this code under the terms of
* the GNU General Public License version 2 as published by the
* Free Software Foundation. For the terms of this license,
* see <http://www.gnu.org/licenses/>.
*
* You are free to use this software under the terms of the GNU General
* Public License, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* https://github.com/sfeakes/aqualinkd
*/
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <syslog.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <ctype.h>
#ifndef _UTILS_C_
#define _UTILS_C_
#endif
#include "utils.h"
//#define MAXCFGLINE 265
#define TIMESTAMP_LENGTH 30
static bool _daemonise = false;
static bool _log2file = false;
static int _log_level = -1;
static char *_log_filename = NULL;
//static char _log_filename[256];
void setLoggingPrms(int level , bool deamonized, char* log_file)
{
_log_level = level;
_daemonise = deamonized;
if (log_file == NULL || strlen(log_file) <= 0) {
_log2file = false;
} else {
_log2file = true;
_log_filename = log_file;
//strcpy(_log_filename, log_file);
}
}
int getLogLevel()
{
return _log_level;
}
/*
* This function reports the error and
* exits back to the shell:
*/
void displayLastSystemError (const char *on_what)
{
fputs (strerror (errno), stderr);
fputs (": ", stderr);
fputs (on_what, stderr);
fputc ('\n', stderr);
if (_daemonise == TRUE)
{
logMessage (LOG_ERR, "%d : %s", errno, on_what);
closelog ();
}
}
/*
From -- syslog.h --
#define LOG_EMERG 0 // system is unusable
#define LOG_ALERT 1 // action must be taken immediately
#define LOG_CRIT 2 // critical conditions
#define LOG_ERR 3 // error conditions
#define LOG_WARNING 4 // warning conditions
#define LOG_NOTICE 5 // normal but significant condition
#define LOG_INFO 6 // informational
#define LOG_DEBUG 7 // debug-level messages
*/
char *elevel2text(int level)
{
switch(level) {
case LOG_ERR:
return "Error:";
break;
case LOG_WARNING:
return "Warning:";
break;
case LOG_NOTICE:
return "Notice:";
break;
case LOG_INFO:
return "Info:";
break;
case LOG_DEBUG:
default:
return "Debug:";
break;
}
return "";
}
int text2elevel(char* level)
{
if (strcmp(level, "DEBUG") == 0) {
return LOG_DEBUG;
}
else if (strcmp(level, "INFO") == 0) {
return LOG_INFO;
}
else if (strcmp(level, "WARNING") == 0) {
return LOG_WARNING;
}
else if (strcmp(level, "NOTICE") == 0) {
return LOG_NOTICE;
}
else if (strcmp(level, "INFO") == 0) {
return LOG_INFO;
}
return LOG_ERR;
}
void timestamp(char* time_string)
{
time_t now;
struct tm *tmptr;
time(&now);
tmptr = localtime(&now);
strftime(time_string, TIMESTAMP_LENGTH, "%b-%d-%y %H:%M:%S %p ", tmptr);
}
void trimwhitespace(char *str)
{
char *end;
end = str + strlen(str) - 1;
while(end > str && isspace(*end)) end--;
*(end+1) = 0;
}
char *cleanwhitespace(char *str)
{
char *end;
// Trim leading space
while(isspace(*str)) str++;
if(*str == 0) // All spaces?
return str;
// Trim trailing space
end = str + strlen(str) - 1;
while(end > str && isspace(*end)) end--;
// Write new null terminator
*(end+1) = 0;
return str;
}
/*
char *cleanquotes(char *str)
{
char *end;
// Trim leading whitespace
//while(isspace(*str)) str++;
//if(*str == 0) // All spaces?
// return str;
syslog(LOG_INFO, "String to clean %s\n", str);
while(*str=='"' || *str== '\'' || *str==' ') str++;
if(*str == 0) // All spaces
return str;
end = str + strlen(str) - 1;
while(end > str && (*end=='"' || *end== '\'' || *end==' ')) end--;
// Write new null terminator
*(end+1) = 0;
syslog(LOG_INFO, "String cleaned %s\n", str);
return str;
}
*/
int cleanint(char*str)
{
if (str == NULL)
return 0;
str = cleanwhitespace(str);
return atoi(str);
}
void test(int msg_level, char *msg)
{
char buffer[256];
sprintf(buffer,"Level %d | MsgLvl %d | Dmn %d | LF %d | %s - %s",_log_level,msg_level,_daemonise,_log2file,_log_filename,msg);
if ( buffer[strlen(buffer)-1] != '\n') {
strcat(buffer, "\n");
}
int fp = open("/var/log/aqualink.log", O_WRONLY | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if (fp != -1) {
write(fp, buffer, strlen(buffer) );
close(fp);
} else {
syslog(LOG_ERR, "Can't open file /var/log/aqualink.log");
}
}
void logMessage(int msg_level, char *format, ...)
{
char buffer[512];
va_list args;
va_start(args, format);
strncpy(buffer, " ", 8);
vsprintf (&buffer[8], format, args);
va_end(args);
//test(msg_level, buffer);
//fprintf (stderr, buffer);
if (_log_level == -1) {
fprintf (stderr, buffer);
syslog (msg_level, "%s", &buffer[8]);
closelog ();
} else if (msg_level > _log_level) {
return;
}
if (_daemonise == TRUE)
{
syslog (msg_level, "%s", &buffer[8]);
closelog ();
//return;
}
if (_log2file == TRUE && _log_filename != NULL) {
int len;
char *strLevel = elevel2text(msg_level);
strncpy(buffer, strLevel, strlen(strLevel));
len = strlen(buffer);
//printf( " '%s' last chrs '%d''%d'\n", buffer, buffer[len-1],buffer[len]);
if ( buffer[len-1] != '\n') {
strcat(buffer, "\n");
}
char time[TIMESTAMP_LENGTH];
int fp = open(_log_filename, O_WRONLY | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if (fp != -1) {
timestamp(time);
write(fp, time, strlen(time) );
write(fp, buffer, strlen(buffer) );
close(fp);
} else {
if (_daemonise == TRUE)
syslog(LOG_ERR, "Can't open log file\n %s", buffer);
else
fprintf (stderr, "Can't open debug log\n %s", buffer);
}
}
if (_daemonise == FALSE) {
if (msg_level == LOG_ERR) {
fprintf(stderr, "%s", buffer);
} else {
printf("%s", buffer);
}
}
}
void daemonise (char *pidFile, void (*main_function) (void))
{
FILE *fp = NULL;
pid_t process_id = 0;
pid_t sid = 0;
_daemonise = true;
/* Check we are root */
if (getuid() != 0)
{
logMessage(LOG_ERR,"Can only be run as root\n");
exit(EXIT_FAILURE);
}
int pid_file = open (pidFile, O_CREAT | O_RDWR, 0666);
int rc = flock (pid_file, LOCK_EX | LOCK_NB);
if (rc)
{
if (EWOULDBLOCK == errno)
; // another instance is running
//fputs ("\nAnother instance is already running\n", stderr);
logMessage(LOG_ERR,"\nAnother instance is already running\n");
exit (EXIT_FAILURE);
}
process_id = fork ();
// Indication of fork() failure
if (process_id < 0)
{
displayLastSystemError ("fork failed!");
// Return failure in exit status
exit (EXIT_FAILURE);
}
// PARENT PROCESS. Need to kill it.
if (process_id > 0)
{
fp = fopen (pidFile, "w");
if (fp == NULL)
logMessage(LOG_ERR,"can't write to PID file %s",pidFile);
else
fprintf(fp, "%d", process_id);
fclose (fp);
logMessage (LOG_DEBUG, "process_id of child process %d \n", process_id);
// return success in exit status
exit (EXIT_SUCCESS);
}
//unmask the file mode
umask (0);
//set new session
sid = setsid ();
if (sid < 0)
{
// Return failure
displayLastSystemError("Failed to fork process");
exit (EXIT_FAILURE);
}
// Change the current working directory to root.
chdir ("/");
// Close stdin. stdout and stderr
close (STDIN_FILENO);
close (STDOUT_FILENO);
close (STDERR_FILENO);
// this is the first instance
(*main_function) ();
return;
}
int count_characters(const char *str, char character)
{
const char *p = str;
int count = 0;
do {
if (*p == character)
count++;
} while (*(p++));
return count;
}
bool text2bool(char *str)
{
str = cleanwhitespace(str);
if (strcasecmp (str, "YES") == 0 || strcasecmp (str, "ON") == 0)
return TRUE;
else
return FALSE;
}
char *bool2text(bool val)
{
if(val == TRUE)
return "YES";
else
return "NO";
}
// (50°F - 32) x .5556 = 10°C
float degFtoC(float degF)
{
return ((degF-32) / 1.8);
}
// 30°C x 1.8 + 32 = 86°F
float degCtoF(float degC)
{
return (degC * 1.8 + 32);
}
#include <time.h>
void delay (unsigned int howLong) // Microseconds (1000000 = 1 second)
{
struct timespec sleeper, dummy ;
sleeper.tv_sec = (time_t)(howLong / 1000) ;
sleeper.tv_nsec = (long)(howLong % 1000) * 1000000 ;
nanosleep (&sleeper, &dummy) ;
}

54
utils.h Normal file
View File

@ -0,0 +1,54 @@
#include <syslog.h>
#include <stdbool.h>
#ifndef UTILS_H_
#define UTILS_H_
#ifndef EXIT_SUCCESS
#define EXIT_FAILURE 1
#define EXIT_SUCCESS 0
#endif
#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif
#define MAXLEN 256
#define round(a) (int) (a+0.5)
/*
typedef enum
{
false = FALSE, true = TRUE
} bool;
*/
void setLoggingPrms(int level , bool deamonized, char* log_file);
int getLogLevel();
void daemonise ( char *pidFile, void (*main_function)(void) );
//void debugPrint (char *format, ...);
void displayLastSystemError (const char *on_what);
void logMessage(int level, char *format, ...);
int count_characters(const char *str, char character);
//void readCfg (char *cfgFile);
int text2elevel(char* level);
char *elevel2text(int level);
char *cleanwhitespace(char *str);
//char *cleanquotes(char *str);
void trimwhitespace(char *str);
int cleanint(char*str);
bool text2bool(char *str);
char *bool2text(bool val);
void delay (unsigned int howLong);
float degFtoC(float degF);
float degCtoF(float degC);
//#ifndef _UTILS_C_
extern bool _daemon_;
extern bool _debuglog_;
extern bool _debug2file_;
//#endif
#endif /* UTILS_H_ */

BIN
utils.o Normal file

Binary file not shown.

4
version.h Normal file
View File

@ -0,0 +1,4 @@
#define AQUALINKD_NAME "Aqualink Daemon"
#define AQUALINKD_VERSION "0.8a"

BIN
web/aqualink-black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
web/aqualinkd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

196
web/controller.html Normal file
View File

@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset=utf-8 />
<title>Feakes Inc, Aqualink RS control daemon</title>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<!--<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />-->
<!--<meta name="viewport" content="width=device-width, user-scalable=yes, width=800"/>-->
<!--<meta name="viewport" content="width=device-width, user-scalable=yes"/>-->
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0"/>
<!--<link href="aqualinkd.png" rel="apple-touch-icon" sizes="180x180" />-->
<!--<link href="aqualinkd.png" rel="icon" sizes="128x128" />-->
<link href="aqualinkd.png" rel="apple-touch-icon">
<link href="aqualinkd.png" rel="icon">
<link rel="stylesheet" href="css/small_ff.css" type="text/css">
</head>
<body ontouchmove="BlockMove(event);" style="background-color:black;">
<div id="main_div">
<div id="header_div" class='bgimg'>
<div id="line1">
<div id="title_div">Aqualink RS8</div>
<div id="air_temp_title_div">Air Temp:</div>
<div id="air_temp">000 F</div>
<div id="battery_ok_div" class="battery_image_div"><img id="battery_ok" src="" style="visibility:hidden"></div>
<div id="battery_low_div" class="battery_image_div"><img id="battery_low" src="" style="visibility:hidden"></div>
<div id="battery_blank_div" class="battery_image_div"><img id="battery_blank" src=""></div>
</div>
<div id="line2">
<div id="net_off_div" class="net_activity_div"><img id="net_off" src="" style="visibility:visible"></div>
<div id="net_green_div" class="net_activity_div"><img id="net_green" src="" style="visibility:hidden"></div>
<div id="net_yellow_div" class="net_activity_div"><img id="net_yellow" src="" style="visibility:hidden"></div>
<div id="net_red_div" class="net_activity_div"><img id="net_red" src="" style="visibility:hidden"></div>
<div id="messages_div" align="center"></div>
<div id="date_time">00:00 PM 00/00/00 DDD</div>
</div>
</div>
<div id="body_div" class="bgimg_body">
<div id="pool_temp_div" class="body_sub_div">
<div id="pool_temp_title" class="temp_title_div">Pool:</div>
<div id="pool_temp" class="temp_val_div">000 F</div>
<div class="control_button_div">
<div class="led_button_div" id="filter_pump">
<div class="led_div"><img id="Filter_Pump_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="Filter_Pump_on_status" src="" style="visibility:hidden"></div>
<div class="led_div"><img id="Filter_Pump_en_status" src="" style="visibility:hidden"></div>
<div id="Filter_Pump" class='button black button_div' type="button" onclick="queue_command('Filter_Pump')">Filter Pump</div>
</div>
<div class="led_button_div" id="pool_htr">
<div class="led_div"><img id="Pool_Heater_on_status" src="" style="visibility:hidden"></div>
<div class="led_div"><img id="Pool_Heater_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="Pool_Heater_en_status" src="" style="visibility:hidden"></div>
<div id="Pool_Heater" class='button black button_div' type="button" onclick="queue_command('Pool_Heater')">Heater</div>
</div>
</div>
<div id="pool_htr_set_pnt" class="set_pnt_val_div">000 F</div>
<div class="set_pnt_button_div">
<div class='button medium black set_pnt_button' type="button" onclick="incr_pool_htr('+')">+</div>
<div class='button medium black set_pnt_button' type="button" onclick="incr_pool_htr('-')">-</div>
</div>
<div class="set_button_div"><div class='button black' type="button" onclick="set_temperature('POOL_HTR')">Set</div></div>
</div>
<div id="spa_temp_div" class="body_sub_div">
<div id="spa_temp_title" class="temp_title_div">Spa:</div>
<div id="spa_temp" class="temp_val_div">000 F</div>
<div class="control_button_div">
<div class="led_button_div" id="spa_mode">
<div class="led_div"><img id="Spa_Mode_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="Spa_Mode_on_status" src="" style="visibility:hidden"></div>
<div class="led_div"><img id="Spa_Mode_en_status" src="" style="visibility:hidden"></div>
<div id="Spa_Mode" class="button black button_div" type="button" onclick="queue_command('Spa_Mode')">Spa Mode</div>
</div>
<!--
<div class="led_button_div" id="aux2">
<div class="led_div"><img id="aux2_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="aux2_on_status" src="" style="visibility:hidden"></div>
<div id="Aux_2" class="button black button_div" type="button" onclick="queue_command('Aux_3')">Aux 2</div>
</div>
-->
<div class="led_button_div" id="aux3">
<div class="led_div"><img id="Aux_3_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="Aux_3_on_status" src="" style="visibility:hidden"></div>
<div class="led_div"><img id="Aux_3_en_status" src="" style="visibility:hidden"></div>
<div id="Aux_3" class="button black button_div" type="button" onclick="queue_command('Aux_3')">Aux 3</div>
</div>
<div class="led_button_div" id="pool_htr">
<div class="led_div"><img id="Spa_Heater_on_status" src="" style="visibility:hidden"></div>
<div class="led_div"><img id="Spa_Heater_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="Spa_Heater_en_status" src="" style="visibility:hidden"></div>
<div id="Spa_Heater" class="button black button_div" type="button" onclick="queue_command('Spa_Heater')">Heater</div>
</div>
</div>
<div id="spa_htr_set_pnt" class="set_pnt_val_div">000 F</div>
<div class="set_pnt_button_div">
<div class='button medium black set_pnt_button' type="button" onclick="incr_spa_htr('+')">+</div>
<div class='button medium black set_pnt_button' type="button" onclick="incr_spa_htr('-')">-</div>
</div>
<div class="set_button_div"><div class='button black' type="button" onclick="set_temperature('SPA_HTR')">Set</div></div>
</div>
<div id="solar_htr_div" class="body_sub_div">
<!--
<div class="solar_led_div"><img id="Solar_Heater_off_status" src="" style="visibility:visible"></div>
<div class="solar_led_div"><img id="Solar_Heater_on_status" src="" style="visibility:hidden"></div>
<div class="solar_led_div"><img id="Solar_Heater_en_status" src="" style="visibility:hidden"></div>
<div id="Solar_Heater" onclick="queue_command('Solar_Heater')" class="button black" type="button">Solar Heater</div>
-->
<div class="select_pl_div">
<select id="Light_Mode" class="select_pl_dd button black" onchange="set_light_mode(this.options[this.selectedIndex].value)">
<option value="0">Pool Light Program</option>
<option value="1">Voodoo Lounge - Show</option>
<option value="2">Blue Sea</option>
<option value="3">Royal Blue</option>
<option value="4">Afternoon Skies</option>
<option value="5">Aqua Green</option>
<option value="6">Emerald</option>
<option value="7">Cloud White</option>
<option value="8">Warm Red</option>
<option value="9">Flamingo</option>
<option value="10">Vivid Violet</option>
<option value="11">Sangria</option>
<option value="12">Twilight - Show</option>
<option value="13">Tranquility - Show</option>
<option value="14">Gemstone - Show</option>
<option value="15">USA - Show</option>
<option value="16">Mardi Gras - Show</option>
<option value="17">Cool Cabaret - Show</option>
</select>
</div>
</div>
<div id="frz_protect_div" class="body_sub_div">
<div id="frz_protect_title_div">Freeze Protection:</div>
<div id="frz_protect_set_pnt">000 F</div>
<div id="fp_set_pnt_button_div">
<div class='button medium black set_pnt_button' type="button" onclick="incr_frz_protect('+')">+</div>
<div class='button medium black set_pnt_button' type="button" onclick="incr_frz_protect('-')">-</div>
</div>
<div id="fp_set_button_div"><div class='button black' type="button" onclick="set_temperature('FRZ_PROTECT')">Set</div></div>
</div>
<div id="leds_div" class="body_sub_div">
<div id="accessory_title" class="temp_title_div">Accessories:</div>
<div class="control_button_div">
<div class="led_button_div" id="aux1">
<div class="led_div"><img id="Aux_1_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="Aux_1_on_status" src="" style="visibility:hidden"></div>
<div class="led_div"><img id="Aux_1_en_status" src="" style="visibility:hidden"></div>
<div id="Aux_1" class="button black button_div" type="button" onclick="queue_command('Aux_1')">Aux 1</div>
</div>
<div class="led_button_div" id="aux2">
<div class="led_div"><img id="Aux_2_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="Aux_2_on_status" src="" style="visibility:hidden"></div>
<div class="led_div"><img id="Aux_2_en_status" src="" style="visibility:hidden"></div>
<div id="Aux_2" class="button black button_div" type="button" onclick="queue_command('Aux_2')">Aux 2</div>
</div>
<div class="led_button_div" id="aux4">
<div class="led_div"><img id="Aux_4_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="Aux_4_on_status" src="" style="visibility:hidden"></div>
<div class="led_div"><img id="Aux_4_en_status" src="" style="visibility:hidden"></div>
<div id="Aux_4" class="button black button_div" type="button" onclick="queue_command('Aux_4')">Aux 4</div>
</div>
<div class="led_button_div" id="aux5">
<div class="led_div"><img id="Aux_5_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="Aux_5_on_status" src="" style="visibility:hidden"></div>
<div class="led_div"><img id="Aux_5_en_status" src="" style="visibility:hidden"></div>
<div id="Aux_5" class="button black button_div" type="button" onclick="queue_command('Aux_5')">Aux 5</div>
</div>
<!--
<div class="led_button_div" id="aux6">
<div class="led_div"><img id="Aux_6_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="Aux_6_on_status" src="" style="visibility:hidden"></div>
<div id="aux6_button" class="button black button_div" type="button" onclick="queue_command('KEY_AUX6')">Aux 6</div>
</div>
<div class="led_button_div" id="aux7">
<div class="led_div"><img id="Aux_7_off_status" src="" style="visibility:visible"></div>
<div class="led_div"><img id="Aux_7_on_status" src="" style="visibility:hidden"></div>
<div id="aux7_button" class="button black button_div" type="button" onclick="queue_command('KEY_AUX7')">Aux 7</div>
</div>
-->
</div>
</div>
</div>
<div id="footer_div" class="bgimg">
</div><!-- /footer -->
</div><!-- /page -->
<script src="js/aqualink.js"></script>
</body>
</html>

166
web/css/small_ff.css Normal file
View File

@ -0,0 +1,166 @@
/* portrait mode (default) */
@media all and (orientation:portrait) {
#main_div{width:540px;height:550px;margin-left:0;padding:0px;}
#body_div{position:relative;height:450px;}
#header_div{position:relative;height:70px;}
#footer_div{height:45px;}
#title_div{position:absolute;top:0px;left:0px;padding:5px;}
#air_temp_title_div{position:absolute;top:0px;right:120px;font-size:150%;}
#air_temp{position:absolute;top:0px;right:45px;font-size:150%;}
#messages_div{position:absolute;top:30px;left:5px;width:500px;font-size:100%;}
#date_time{position:absolute;bottom:2px;right:5px;}
#pool_temp_div{position:absolute;top:5px;height:190px;width:250px;left:10px;}
#spa_temp_div{position:absolute;top:5px;height:280px;width:250px;left:270px;}
#leds_div{position:absolute;top:205px;height:230px;width:250px;left:10px}
#frz_protect_div{position:absolute;top:295px;height:85px;width:250px;left:270px;}
#solar_htr_div{position:absolute;top:390px;height:45px;width:250px;left:270px;}
#Solar_Heater{position:absolute;bottom:5px;right:10px;width:140px;}
#frz_protect_title_div{position:absolute;top:0px;height:50px;left:5px;font-size:125%;}
#frz_protect_set_pnt{position:absolute;bottom:10px;height:50px;right:60px;font-size:250%;}
#fp_set_pnt_button_div{position:absolute;bottom:20px;height:50px;right:45px;width:10px;}
#fp_set_button_div{position:absolute;bottom:3px;left:3px;width:65px;}
}
/*@media only screen and (orientation:portrait){*/
/*@media only screen and (orientation:landscape){*/
@media all and (orientation:landscape){
#main_div{width:800px;height:420px;margin-left:0;padding:0px;}
#header_div{position:relative;height:70px;}
#footer_div{height:45px;}
#title_div{position:absolute;top:0;left:0;padding:5px;}
#air_temp_title_div{position:absolute;top:0;right:215px;font-size:200%;}
#air_temp{position:absolute;top:0;right:75px;font-size:250%;}
#messages_div{position:absolute;bottom:2px;left:35px;width:570px;}
#date_time{position:absolute;bottom:2px;right:5px;}
#body_div{position:relative;height:295px;}
#leds_div{position:absolute;top:5px;right:10px;height:230px;width:245px;}
#pool_temp_div{position:absolute;top:5px;height:190px;width:250px;left:10px;}
#spa_temp_div{position:absolute;top:5px;height:280px;width:250px;left:275px;}
#solar_htr_div{position:absolute;bottom:3px;height:45px;width:245px;right:10px;}
/*#solar_button_div{position:absolute;bottom:5px;right:10px;width:140px;}*/
#Solar_Heater{position:absolute;bottom:5px;right:10px;width:140px;}
#frz_protect_div{position:absolute;bottom:5px;height:85px;width:250px;left:10px;}
#frz_protect_title_div{position:absolute;top:0px;height:50px;left:5px;font-size:125%;}
#frz_protect_set_pnt{position:absolute;bottom:10px;height:50px;right:60px;font-size:250%;}
#fp_set_pnt_button_div{position:absolute;bottom:20px;height:50px;right:45px;width:10px;}
#fp_set_button_div{position:absolute;bottom:3px;left:3px;width:65px;}
}
.battery_image_div{position:absolute;top:5px;right:5px;width:30px;}
.net_activity_div{position:absolute;bottom:2px;left:0;}
.led_button_div{position:relative;height:37px;width:240px}
.led_div{position:absolute;top:2px;left:15px;padding:0;}
.button_div{position:absolute;right:0px;width:140px;}
.control_button_div{position:absolute;top:40px;height:50px;width:245px;right:5px;}
/*
.htr_led_div{position:absolute;top:55px;left:15px;padding:0;}
.htr_button_div{position:absolute;top:55px;height:45px;right:55px;width:110px;padding:0;margin:0px auto;}
*/
.body_sub_div{border:2px solid #a1a1a1;border-radius:15px;}
.temp_title_div{position:absolute;top:0px;height:50px;left:5px;font-size:175%;}
.temp_val_div{position:absolute;top:0px;height:55px;right:5px;font-size:225%;}
.set_pnt_val_div{position:absolute;bottom:15px;height:50px;right:60px;font-size:250%;}
.set_pnt_button_div{position:absolute;bottom:5px;height:65px;right:5px;width:50px;padding:0;}
.set_pnt_button{position:relative;right:0px;width:5px;}
.set_button_div{position:absolute;bottom:5px;left:5px;width:65px;}
.solar_led_div{position:absolute;top:8px;left:10px;}
.select_pl_div{position:absolute;top:8px;left:10px;height:33px;width:227px;}
.select_pl_dd{width:227px; height:33px;}
.button {
display: inline-block;
vertical-align: baseline;
outline: none;
cursor: pointer;
text-align: center;
text-decoration: none;
font: 16px/100% Arial, Helvetica, sans-serif;
padding: .50em 1.5em;
text-shadow: 0 1px 1px rgba(0, 0, 0, .3);
-webkit-border-radius: .5em;
-moz-border-radius: .5em;
border-radius: .5em;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
-moz-box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
}
.button:hover {
text-decoration: none;
}
.button:active {
top: 1px;
}
/*
.bigrounded {
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
}
*/
.medium {
font-size: 14px;
padding: .8em 1.5em .42em;
}
/*
.small {
font-size: 11px;
padding: .2em 1em .275em;
}
*/
.black {
color: #d7d7d7;
border: solid 1px #333;
background: #333;
background: -webkit-gradient(linear, left top, left bottom, from(#666), to(#000));
background: -moz-linear-gradient(top, #666, #000);
}
.black:hover {
background: #000;
background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#000));
background: -moz-linear-gradient(top, #444, #000);
}
.black:active {
color: #666;
background: -webkit-gradient(linear, left top, left bottom, from(#000), to(#444));
background: -moz-linear-gradient(top, #000, #444);
}
.bgimg {
width: 100%;
height: 250px;
background-image: -webkit-gradient( linear, left top, left bottom, color-stop(0.15, rgb(50, 50, 50)), color-stop(0.58, rgb(0, 0, 0)), color-stop(0.79, rgb(0, 0, 0)));
background-image: -moz-linear-gradient(top, #444, #000);
}
.bgimg_body {
width: 100%;
height: 250px;
background-image: -webkit-gradient( linear, left top, left bottom, color-stop(0.15, rgb(75, 75, 75)), color-stop(0.58, rgb(50, 50, 50)), color-stop(0.79, rgb(50, 50, 50)));
background-image: -moz-linear-gradient(top, #666, #333);
}
/*
.debug_border {
border: 2px solid #a1a1a1;
border-radius: 15px;
}
*/
div {
border-width: 0px;
border-style: solid;
padding: 0px;
}
body, html {
margin: 0;
padding: 0;
color: #f4f4f4;
font-family: Arial, Helvetica, sans-serif;
-webkit-touch-callout: none;
-webkit-user-select: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
web/images/battery_low.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
web/images/battery_ok.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
web/images/led_enabled.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
web/images/led_off.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
web/images/led_on.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
web/images/net_green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
web/images/net_off.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
web/images/net_red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
web/images/net_yellow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

1
web/index.html Symbolic link
View File

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

533
web/js/aqualink.js Normal file
View File

@ -0,0 +1,533 @@
if(window.innerHeight > window.innerWidth){
orientation("portrait");
} else {
orientation("landscape");
}
// Listen for orientation changes
window.addEventListener("orientationchange", function() {
// Announce the new orientation number
//alert(window.orientation);
if(window.innerHeight > window.innerWidth)
orientation("portrait");
else
orientation("landscape");
}, false);
function orientation(or){
//alert(or);
viewport = document.querySelector("meta[name=viewport]");
if (or == "landscape") {
viewport.setAttribute('content', 'content="width=device-width, user-scalable=yes, width=800"');
} else {
viewport.setAttribute('content', 'content="width=device-width, user-scalable=yes, width=540"');
}
}
// aqualink.js
function BlockMove(event) {
// Tell Safari not to move the window.
event.preventDefault();
}
// Preload the images.
var delay = 10;
if (document.images) {
//setTimeout(get_batt_ok_img, delay);
setTimeout(loadImages, delay);
}
function loadImages() {
document.getElementById('battery_ok').src = "images/battery_ok.png";
document.getElementById('battery_low').src = "images/battery_low.png";
document.getElementById('battery_blank').src = "images/battery_blank.png";
for (var obj in aqualink_data.leds) {
setButtonElementImage(obj+'_on_status', "images/led_on.png");
setButtonElementImage(obj+'_off_status', "images/led_off.png");
setButtonElementImage(obj+'_en_status', "images/led_enabled.png");
}
document.getElementById('net_off').src = "images/net_off.png";
document.getElementById('net_yellow').src = "images/net_yellow.png";
document.getElementById('net_green').src = "images/net_green.png";
document.getElementById('net_red').src = "images/net_red.png";
}
//function set_button_led_images
function setButtonElementImage(element, image){
el = document.getElementById(element);
if (el != null) {
el.src = image;
}
}
function LedsData() {
this.Filter_Pump = "off";
this.Spa_Mode = "off";
this.Aux_1 = "off";
//this.aux1 = "flash";
this.Aux_2 = "off";
this.Aux_3 = "off";
this.Aux_4 = "off";
this.Aux_5 = "off";
this.Aux_6 = "off";
this.Aux_7 = "off";
this.Pool_Heater = "off";
this.Spa_Heater = "off";
this.Solar_Heater = "off";
}
function AqualinkData() {
this.version = "";
this.date = "09/11/12 TUE";
this.time = "10:33 AM";
this.temp_units = "F";
this.air_temp = "75";
this.battery = "low";
this.leds = new LedsData();
}
function TemperatureSetPoint() {
this.modified = false;
this.value = 0;
}
var pool_htr_set_point = new TemperatureSetPoint();
var spa_htr_set_point = new TemperatureSetPoint();
var freeze_protect_set_point = new TemperatureSetPoint();
var aqualink_data = new AqualinkData();
var net_connection = 'red';
function interval(duration, fn){
this.baseline = undefined
this.run = function(){
if(this.baseline === undefined){
this.baseline = new Date().getTime()
}
fn()
var end = new Date().getTime()
this.baseline += duration
var nextTick = duration - (end - this.baseline)
if(nextTick<0){
nextTick = 0
}
(function(i){
i.timer = setTimeout(function(){
i.run(end)
}, nextTick)
}(this))
}
this.stop = function(){
clearTimeout(this.timer)
}
}
var timer = new interval(1000, function(){
net_connection_icon();
flash_battery_icon();
//blink_filter_pump();
blink_button_leds();
})
timer.run()
function net_connection_icon() {
if (net_connection == "red") {
document.getElementById('messages_div').innerHTML = "Not connected!";
document.getElementById('net_green').style.visibility = 'hidden';
document.getElementById('net_yellow').style.visibility = 'hidden';
if (document.getElementById('net_red').style.visibility == 'visible') {
document.getElementById('net_red').style.visibility = 'hidden';
} else {
document.getElementById('net_red').style.visibility = 'visible';
}
} else if (net_connection == "green") {
document.getElementById('net_green').style.visibility = 'visible';
document.getElementById('net_red').style.visibility = 'hidden';
document.getElementById('net_yellow').style.visibility = 'hidden';
} else if (net_connection == "yellow") {
document.getElementById('net_green').style.visibility = 'hidden';
document.getElementById('net_red').style.visibility = 'hidden';
document.getElementById('net_yellow').style.visibility = 'visible';
}
}
//setInterval(net_connection_icon, 500);
function net_activity() {
// Set the icon yellow.
document.getElementById('net_green').style.visibility = 'hidden';
document.getElementById('net_red').style.visibility = 'hidden';
document.getElementById('net_yellow').style.visibility = 'visible';
// Wait 10mS then set to green.
setTimeout(function() {
document.getElementById('net_green').style.visibility = 'visible';
document.getElementById('net_red').style.visibility = 'hidden';
document.getElementById('net_yellow').style.visibility = 'hidden';
}, 50);
}
function flash_battery_icon() {
if (aqualink_data.battery == "low") {
if (document.getElementById('battery_low').style.visibility == 'visible') {
document.getElementById('battery_low').style.visibility = 'hidden';
} else {
document.getElementById('battery_low').style.visibility = 'visible';
}
}
}
//setInterval(flash_battery_icon, 500);
/*
function blink_filter_pump() {
if (aqualink_data.leds.pump == "enabled") {
document.getElementById("pump_status").src = (document.getElementById("pump_status").src.indexOf("images/led_on.png") == -1) ? "images/led_on.png" : "images/led_off.png";
}
}
*/
//setInterval(blink_filter_pump, 500);
function blink_button_leds() {
for (var obj in aqualink_data.leds) {
if (aqualink_data.leds[obj] == "flash" && document.getElementById(obj+'_on_status') != null ) {
if (document.getElementById(obj+'_on_status').style.visibility == 'hidden') {
setElementVisibility(obj+'_on_status', 'visible');
setElementVisibility(obj+'_off_status', 'hidden');
} else {
setElementVisibility(obj+'_on_status', 'hidden');
setElementVisibility(obj+'_off_status', 'visible');
}
}
}
}
//setInterval(blink_aux_leds, 1500);
// End - Methods to Manage Flashing Images
// Display Update Methods
function update_temp(id) {
var el = document.getElementById(id);
var temp_string = "";
if (id == "air_temp") {
temp_string = aqualink_data.air_temp;
} else if (id == "pool_temp") {
temp_string = aqualink_data.pool_temp;
if (temp_string == " ") {
temp_string = "---"
}
} else if (id == "spa_temp") {
temp_string = aqualink_data.spa_temp;
if (temp_string == " ") {
temp_string = "---"
}
} else if (id == "pool_htr_set_pnt") {
if (pool_htr_set_point.modified == true) {
temp_string = pool_htr_set_point.value.toString();
el.style.color = "PowderBlue";
} else {
temp_string = aqualink_data.pool_htr_set_pnt;
pool_htr_set_point.value = parseInt(temp_string);
el.style.color = "#f4f4f4";
}
} else if (id == "spa_htr_set_pnt") {
if (spa_htr_set_point.modified == true) {
temp_string = spa_htr_set_point.value.toString();
el.style.color = "PowderBlue";
} else {
temp_string = aqualink_data.spa_htr_set_pnt;
spa_htr_set_point.value = parseInt(temp_string);
el.style.color = "#f4f4f4";
}
} else if (id == "frz_protect_set_pnt") {
if (freeze_protect_set_point.modified == true) {
temp_string = freeze_protect_set_point.value.toString();
el.style.color = "PowderBlue";
} else {
temp_string = aqualink_data.frz_protect_set_pnt;
freeze_protect_set_point.value = parseInt(temp_string);
el.style.color = "#f4f4f4";
}
}
el.innerHTML = temp_string + "&deg;" + aqualink_data.temp_units;
}
// Adjust the Pool Heater set point. Note that its maximum value
// is 104 and its minimum value is 0.
function incr_pool_htr(direction) {
if (direction == '+') {
if (pool_htr_set_point.value < 104) {
pool_htr_set_point.value++;
}
} else {
if (pool_htr_set_point.value > 0) {
pool_htr_set_point.value--;
}
}
if (pool_htr_set_point.value == parseInt(aqualink_data.pool_htr_set_pnt)) {
pool_htr_set_point.modified = false;
} else {
pool_htr_set_point.modified = true;
}
update_temp("pool_htr_set_pnt");
}
// Adjust the Spa Heater set point. Note that its maximum value
// is 104 and its minimum value is 0.
function incr_spa_htr(direction) {
if (direction == '+') {
if (spa_htr_set_point.value < 104) {
spa_htr_set_point.value++;
}
} else {
if (spa_htr_set_point.value > 0) {
spa_htr_set_point.value--;
}
}
if (spa_htr_set_point.value == parseInt(aqualink_data.spa_htr_set_pnt)) {
spa_htr_set_point.modified = false;
} else {
spa_htr_set_point.modified = true;
}
update_temp("spa_htr_set_pnt");
}
// Adjust the Freeze Protection set point. Note that its maximum value
// is 42 and its minimum value is 36.
function incr_frz_protect(direction) {
if (direction == '+') {
if (freeze_protect_set_point.value < 42) {
freeze_protect_set_point.value++;
}
} else {
if (freeze_protect_set_point.value > 36) {
freeze_protect_set_point.value--;
}
}
if (freeze_protect_set_point.value == parseInt(aqualink_data.frz_protect_set_pnt)) {
freeze_protect_set_point.modified = false;
} else {
freeze_protect_set_point.modified = true;
}
update_temp("frz_protect_set_pnt");
}
function update_date_time() {
var el = document.getElementById("date_time");
el.innerHTML = aqualink_data.time + " - " + aqualink_data.date;
}
function setElementVisibility(element, visibility){
//console.log("set "+element+" to "+visibility);
el = document.getElementById(element);
if (el != null) {
el.style.visibility = visibility;
//console.log("set "+element+" to "+visibility);
}
}
function update_leds() {
//console.log("*****UPDATE LED*****");
for (var obj in aqualink_data.leds) {
//console.log("*****setting "+obj+" to "+aqualink_data.leds[obj]);
if (aqualink_data.leds[obj] == "on") {
setElementVisibility(obj+'_on_status', 'visible');
setElementVisibility(obj+'_off_status', 'hidden');
setElementVisibility(obj+'_en_status', 'hidden');
} else if (aqualink_data.leds[obj] == "enabled") {
setElementVisibility(obj+'_on_status', 'hidden');
setElementVisibility(obj+'_off_status', 'hidden');
setElementVisibility(obj+'_en_status', 'visible');
} else if (aqualink_data.leds[obj] == "off") {
setElementVisibility(obj+'_on_status', 'hidden');
setElementVisibility(obj+'_off_status', 'visible');
setElementVisibility(obj+'_en_status', 'hidden');
} else if (aqualink_data.leds[obj] == "flash") {
// Don't set any other visibility or it'll mess up the flasher timer.
setElementVisibility(obj+'_en_status', 'hidden');
}
}
}
function update_status(data) {
aqualink_data = data;
//console.log(aqualink_data.version);
//console.log('updating status...');
if (aqualink_data.battery == "ok") {
document.getElementById('battery_low').style.visibility = 'hidden';
document.getElementById('battery_ok').style.visibility = 'visible';
} else {
document.getElementById('battery_ok').style.visibility = 'hidden';
}
if (aqualink_data.status == "Ready") {
document.getElementById('messages_div').innerHTML = "";
} else {
document.getElementById('messages_div').innerHTML = aqualink_data.status+" (please wait!)";
}
update_temp("air_temp");
update_temp("pool_temp");
update_temp("spa_temp");
update_temp("pool_htr_set_pnt");
update_temp("spa_htr_set_pnt");
update_temp("frz_protect_set_pnt");
update_date_time();
update_leds();
}
function set_aux_button_labels(data) {
//console.log("Aux 1=" + data.Aux_1);
for (var obj in data) {
//console.log("sent "+obj+" to "+data[obj]);
button = document.getElementById(obj);
if (button != null) {
button.innerHTML = data[obj];
}
}
}
//alert(BrowserDetect.browser + ' : ' + BrowserDetect.version);
function get_appropriate_ws_url() {
var pcol;
var u = document.URL;
/*
* We open the websocket encrypted if this page came on an
* https:// url itself, otherwise unencrypted
*/
if (u.substring(0, 5) == "https") {
pcol = "wss://";
u = u.substr(8);
} else {
pcol = "ws://";
if (u.substring(0, 4) == "http")
u = u.substr(7);
}
u = u.split('/');
//alert (pcol + u[0] + ":6500");
return pcol + u[0];
}
/* dumb increment protocol */
var socket_di;
function startWebsockets() {
socket_di = new WebSocket(get_appropriate_ws_url());
/*
if (BrowserDetect.browser == "Firefox" && BrowserDetect.version < 12) {
//socket_di = new MozWebSocket(get_appropriate_ws_url(), "dumb-increment-protocol");
socket_di = new MozWebSocket(get_appropriate_ws_url());
} else {
//socket_di = new WebSocket(get_appropriate_ws_url(), "dumb-increment-protocol");
socket_di = new WebSocket(get_appropriate_ws_url());
}
*/
try {
socket_di.onopen = function() {
// success!
get_aux_labels();
net_connection = 'green';
}
socket_di.onmessage = function got_packet(msg) {
net_activity();
var data = JSON.parse(msg.data);
if (data.type == 'status') {
update_status(data);
} else if (data.type == 'aux_labels') {
set_aux_button_labels(data);
}
}
socket_di.onclose = function() {
// something went wrong
net_connection = 'red';
// Try to reconnect every 5 seconds.
setTimeout(function() {
startWebsockets()
}, 5000);
}
} catch (exception) {
alert('<p>Error' + exception);
}
}
function reset() {
socket_di.send("reset\n");
}
function get_aux_labels() {
//socket_di.send("GET_AUX_LABELS");
var msg = {command: "GET_AUX_LABELS"};
// Send the msg object as a JSON-formatted string.
socket_di.send(JSON.stringify(msg));
}
function queue_command(cmd) {
var _cmd = {};
_cmd.command = cmd;
socket_di.send(JSON.stringify(_cmd));
}
function set_temperature(type) {
var temperature = {};
if (type == "POOL_HTR") {
temperature.parameter = type;
temperature.value = pool_htr_set_point.value;
pool_htr_set_point.modified = false;
} else if (type == "SPA_HTR") {
temperature.parameter = type;
temperature.value = spa_htr_set_point.value;
spa_htr_set_point.modified = false;
} else if (type == "FRZ_PROTECT") {
temperature.parameter = type;
temperature.value = freeze_protect_set_point.value;
freeze_protect_set_point.modified = false;
}
socket_di.send(JSON.stringify(temperature));
}
function set_light_mode(value) {
var mode = {};
mode.parameter = 'POOL_LIGHT_MODE';
mode.value = value;
socket_di.send(JSON.stringify(mode));
}
window.onload = function() {
startWebsockets();
}