Updates 2.6.2

master
sfeakes 2025-04-13 12:03:42 -05:00
parent 4faa2e0db0
commit a410d31ad6
24 changed files with 556 additions and 187 deletions

View File

@ -84,7 +84,7 @@ endif
SRCS = aqualinkd.c utils.c config.c aq_serial.c aq_panel.c aq_programmer.c allbutton.c allbutton_aq_programmer.c net_services.c json_messages.c rs_msg_utils.c\
onetouch.c onetouch_aq_programmer.c iaqtouch.c iaqtouch_aq_programmer.c iaqualink.c\
devices_jandy.c packetLogger.c devices_pentair.c color_lights.c serialadapter.c aq_timer.c aq_scheduler.c web_config.c\
serial_logger.c mongoose.c hassio.c simulator.c sensors.c aq_filesystem.c timespec_subtract.c
serial_logger.c mongoose.c hassio.c simulator.c sensors.c aq_systemutils.c timespec_subtract.c
AQ_FLAGS =

View File

@ -11,23 +11,29 @@ If you like this project, you can buy me a cup of coffee :)
<br>
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SEGN9UNS38TXJ)
## AqualinkD new home
AqualinkD has grown over the years and now has multiple repositories for software / hardware. We are brining them all together under one organization. [AqualinkD home page (under construction)](https://www.aqualinkd.com) -or- [AqualinkD organization](https://github.com/aqualinkd/AqualinkD).<br>
AqualinkD will always be open source and so will every associated repository. Nothing will change from that perspective. You will always be able to run this on cheap off the shelf hardware.
## AqualinkD discussions
* Please use github Discussions for questions (Link at top of page).
https://github.com/sfeakes/AqualinkD/discussions
https://github.com/aqualinkd/AqualinkD/discussions
* For Bugs, please use issues link on top of page. ( please add AqualinkD version to posts )
https://github.com/sfeakes/AqualinkD/issues
https://github.com/aqualinkd/AqualinkD/issues
## Please see Wiki for installation instructions
https://github.com/sfeakes/AqualinkD/wiki
https://github.com/aqualinkd/AqualinkD/wiki
<!--
For information on Control panel versions and upgrading the chips.<br>
https://github.com/sfeakes/AqualinkD/wiki/Upgrading-Jandy-Aqualink-PDA-to-RS-panel
https://github.com/aqualinkd/AqualinkD/wiki/Upgrading-Jandy-Aqualink-PDA-to-RS-panel
-->
<!--
Here's where I started to document what I know about the Jandy RS485 protocol.<br>
https://github.com/sfeakes/AqualinkD/wiki/Jandy-Aqualink-RS485-protocol
https://github.com/aqualinkd/AqualinkD/wiki/Jandy-Aqualink-RS485-protocol
-->
## AqualinkD built in WEB Interface(s).
@ -80,9 +86,11 @@ Designed to mimic AqualinkRS devices, used to fully configure the master control
* http://aqualink.ip/onetouch_sim.html <- (One Touch Simulator)
* http://aqualink.ip/aquapda_sim.html <- (PDA simulator)
#<a name="release"></a>
# ToDo (future release)
* Create iAqualink Touch Simulator
* AuqlinkD to self configure. (Done for ID's, need to do for Panel type/size)
* AqualinkD to self configure. (Done for ID's, need to do for Panel type/size)
<!--
@ -100,7 +108,7 @@ Designed to mimic AqualinkRS devices, used to fully configure the master control
-->
# Call for Help.
* The only Jandy devices I have not decoded yet are LX heater & Chemical Feeder. If you have either of these devices and are willing to post some logs, please let me know, or post in the [Discussions area](https://github.com/sfeakes/AqualinkD/discussions)
* The only Jandy devices I have not decoded yet are LX heater & Chemical Feeder. If you have either of these devices and are willing to post some logs, please let me know, or post in the [Discussions area](https://github.com/aqualinkd/AqualinkD/discussions)
<!--
@ -111,10 +119,18 @@ NEED TO FIX FOR THIS RELEASE.
* update documentation on how vbutton / vpump / pump_max & min / enable_iauqalink2
* check rs serial adapter is active if light color mode 11 is used.
* Check SWG messages like "#1 TruClear", see log in this post https://github.com/sfeakes/AqualinkD/discussions/388
* Check SWG messages like "#1 TruClear", see log in this post https://github.com/aqualinkd/AqualinkD/discussions/388
* Heat Pump / Chiller for OneTouch
* Try an auto-update
* Update Mongoose
-->
# Updates in 2.6.2
* AqualinkD can how self-update directly from github. use `Upgrade AqualinkD` button in Aqmanager
* New install and remote_install scripts.
* Changed MQTT posting frequency when Timers are enabled for better
* Updates for new repo location. [AqualinkD organization](https://github.com/aqualinkd/AqualinkD)
# Updates in 2.6.1
* Added External Sensors to Web UI & HomeKit.
@ -123,7 +139,7 @@ NEED TO FIX FOR THIS RELEASE.
* Link device/virtual/onetouch button with SWG BOOST. (Allows you to set VSP RPM when in Boost mode)
# Updates in 2.6.0
* Added configuration editor in aqmanager. [Wiki - AQManager](https://github.com/sfeakes/AqualinkD/wiki#AQManager)
* Added configuration editor in aqmanager. [Wiki - AQManager](https://github.com/aqualinkd/AqualinkD/wiki#AQManager)
* Can now self-configure on startup. set `device_id=0xFF`
* Added scheduling of pump after events (Power On, Freeze Protect, Boost)
* Fixed HA bug for thermostats not converting to °C when HA is set to display °C.
@ -195,10 +211,10 @@ NEED TO FIX FOR THIS RELEASE.
# Update in Release 2.3.5
* Added Home Assistant integration through MQTT discover
* Please read the Home Assistant section of the [Wiki - HASSIO](https://github.com/sfeakes/AqualinkD/wiki#HASSIO)
* Please read the Home Assistant section of the [Wiki - HASSIO](https://github.com/aqualinkd/AqualinkD/wiki#HASSIO)
* There are still some enhacments to come on this.
* Included Docker into main releases
* Please read Docker section of the [Wiki - Docker](https://github.com/sfeakes/AqualinkD/wiki#Docker)
* Please read Docker section of the [Wiki - Docker](https://github.com/aqualinkd/AqualinkD/wiki#Docker)
* Added support for reading extended information for Jandy JXi heaters.
* Added Color Light to iAqualinkTouch protocol.
* Fixed issue mqtt_timed_update (1~2 min rather than between 2 & 20 min)
@ -214,11 +230,11 @@ NEED TO FIX FOR THIS RELEASE.
# Update in Release 2.3.3
* Introduced Aqualink Manager UI http://aqualink.ip/aqmanager.html
* [AqualinkD Manager](https://github.com/sfeakes/AqualinkD/wiki#AQManager)
* [AqualinkD Manager](https://github.com/aqualinkd/AqualinkD/wiki#AQManager)
* Moved logging into systemd/journal (journalctl) from syslog
* [AqualinkD Log](https://github.com/sfeakes/AqualinkD/wiki#Log)
* [AqualinkD Log](https://github.com/aqualinkd/AqualinkD/wiki#Log)
* Updated to scheduler
* [AqualinkD Scheduler](https://github.com/sfeakes/AqualinkD/wiki#Scheduler)
* [AqualinkD Scheduler](https://github.com/aqualinkd/AqualinkD/wiki#Scheduler)
* Introduced RS485 frame delay / timer.
* Improve PDA panels reliability (PDA pannels are slower than RS panels)
* Potentially fixed Pentair VSP / SWG problems since Pentair VSP use a different protocol, this will allow a timed delay for the VSP to post a status messages. Seems to only effect RS485 bus when both a Pentair VSP and Jandy SWG are present.
@ -275,7 +291,7 @@ NEED TO FIX FOR THIS RELEASE.
* Fixed RS-4 panel bug.
* Fixed some AqualinkTouch programming issues.
* Increased timeout for startup probe.
* This release WILL require you to make aqualinkd.conf changes. <b>Make sure to read wiki section https://github.com/sfeakes/AqualinkD/wiki#Version_2</b>
* This release WILL require you to make aqualinkd.conf changes. <b>Make sure to read wiki section https://github.com/aqualinkd/AqualinkD/wiki#Version_2</b>
* Extensive work to reduce CPU cycles and unnesessary logic.
* iAqualink Touch protocol supported for VSP & extended programming.
* This protocol is a lot faster for programming, ID's are between 0x38 & 0x3B `extended_device_id`, use Serial_logger to find valid ID.
@ -292,7 +308,7 @@ NEED TO FIX FOR THIS RELEASE.
* AqualinkD startup changed to fix some 'systemctl restart' issues.
* More detailed API replys.
# Update in Release 2.1.0
* Big update, lots of core changes, <b>please read wiki section https://github.com/sfeakes/AqualinkD/wiki#Version_2</b>
* Big update, lots of core changes, <b>please read wiki section https://github.com/aqualinkd/AqualinkD/wiki#Version_2</b>
* Full Variable Speed Pump support. (Can read,set & change RPM,GPM)
* Full support for all Colored Lights (even if Jandy Control Panel doesn't support them)
* Chemlink pH & ORP now supported. (along with posting MQTT information)
@ -438,7 +454,7 @@ NEED TO FIX FOR THIS RELEASE.
* Freeze protect, heater temperature and SWG set-points have been added to support for standard HTTP requests (MQTT & WS always had support).
# Please see Wiki for install instructions
https://github.com/sfeakes/AqualinkD/wiki
https://github.com/aqualinkd/AqualinkD/wiki
#

View File

@ -19,9 +19,9 @@ RUN mkdir /home/AqualinkD
WORKDIR /home/AqualinkD
ARG AQUALINKD_VERSION
RUN curl -sL "https://github.com/sfeakes/AqualinkD/archive/refs/tags/$AQUALINKD_VERSION.tar.gz" | tar xz --strip-components=1
RUN curl -sL "https://github.com/aqualinkd/AqualinkD/archive/refs/tags/$AQUALINKD_VERSION.tar.gz" | tar xz --strip-components=1
# Get latest release
#RUN curl -sL $(curl -s https://api.github.com/repos/sfeakes/AqualinkD/releases/latest | grep "tarball_url" | cut -d'"' -f4) | tar xz --strip-components=1
#RUN curl -sL $(curl -s https://api.github.com/repos/aqualinkd/AqualinkD/releases/latest | grep "tarball_url" | cut -d'"' -f4) | tar xz --strip-components=1
# Build aqualinkd
RUN make clean && \
@ -48,9 +48,9 @@ RUN sed -i '/EXTRA_OPTS=.-l./s/^#//g' /etc/default/cron
# See: https://github.com/opencontainers/image-spec/blob/main/annotations.md
LABEL org.opencontainers.image.title="AqualinkD"
LABEL org.opencontainers.image.url="https://hub.docker.com/repository/docker/sfeakes/aqualinkd/general"
LABEL org.opencontainers.image.source="https://github.com/sfeakes/AqualinkD"
LABEL org.opencontainers.image.documentation="https://github.com/sfeakes/AqualinkD"
LABEL org.opencontainers.image.url="https://hub.docker.com/repository/docker/aqualinkd/aqualinkd/general"
LABEL org.opencontainers.image.source="https://github.com/aqualinkd/AqualinkD"
LABEL org.opencontainers.image.documentation="https://github.com/aqualinkd/AqualinkD"
LABEL org.opencontainers.image.version=$AQUALINKD_VERSION
@ -60,7 +60,7 @@ COPY --from=aqualinkd-build /home/AqualinkD/web/ /var/www/aqualinkd/
COPY --from=aqualinkd-build /home/AqualinkD/release/aqualinkd.conf /etc/aqualinkd.conf
#COPY --from=aqualinkd-build /home/AqualinkD/docker/aqualinkd-docker.cmd /usr/local/bin/aqualinkd-docker
RUN curl -s -o /usr/local/bin/aqualinkd-docker https://raw.githubusercontent.com/sfeakes/AqualinkD/master/docker/aqualinkd-docker.cmd && \
RUN curl -s -o /usr/local/bin/aqualinkd-docker https://raw.githubusercontent.com/aqualinkd/AqualinkD/master/docker/aqualinkd-docker.cmd && \
chmod +x /usr/local/bin/aqualinkd-docker
CMD ["sh", "-c", "/usr/local/bin/aqualinkd-docker"]

View File

@ -59,9 +59,9 @@ WORKDIR /home/AqualinkD
ARG AQUALINKD_VERSION
RUN curl -sL "https://github.com/sfeakes/AqualinkD/archive/refs/tags/$AQUALINKD_VERSION.tar.gz" | tar xz --strip-components=1
RUN curl -sL "https://github.com/aqualinkd/AqualinkD/archive/refs/tags/$AQUALINKD_VERSION.tar.gz" | tar xz --strip-components=1
# Get latest release
#RUN curl -sL $(curl -s https://api.github.com/repos/sfeakes/AqualinkD/releases/latest | grep "tarball_url" | cut -d'"' -f4) | tar xz --strip-components=1
#RUN curl -sL $(curl -s https://api.github.com/repos/aqualinkd/AqualinkD/releases/latest | grep "tarball_url" | cut -d'"' -f4) | tar xz --strip-components=1
# Make AqualinkD
@ -87,8 +87,8 @@ RUN sed -i '/EXTRA_OPTS=.-l./s/^#//g' /etc/default/cron
LABEL org.opencontainers.image.title="AqualinkD"
LABEL org.opencontainers.image.url="https://hub.docker.com/repository/docker/sfeakes/aqualinkd/general"
LABEL org.opencontainers.image.source="https://github.com/sfeakes/AqualinkD"
LABEL org.opencontainers.image.documentation="https://github.com/sfeakes/AqualinkD"
LABEL org.opencontainers.image.source="https://github.com/aqualinkd/AqualinkD"
LABEL org.opencontainers.image.documentation="https://github.com/aqualinkd/AqualinkD"
LABEL org.opencontainers.image.version=$AQUALINKD_VERSION
EXPOSE 80/tcp

View File

@ -2,7 +2,7 @@ services:
aqualinkd:
image: sfeakes/aqualinkd:latest
#build:
# context: https://github.com/sfeakes/AqualinkD.git#master:docker
# context: https://github.com/aqualinkd/AqualinkD.git#master:docker
# args:
# AQUALINKD_VERSION: v2.3.6 # Make sure to change to correct version
# tags:

View File

@ -10,6 +10,25 @@
# curl --silent "https://raw.githubusercontent.com/sfeakes/AqualinkD/master/version.h" | grep AQUALINKD_VERSION | cut -d '"' -f 2
#
echo "This script has been updated!"
echo "Please use 'curl -fsSL https://raw.githubusercontent.com/aqualinkd/AqualinkD/master/release/remote_install.sh | sudo bash -s -- latest' to install AqualinkD"
while true; do
read -p "Continue install with this script? (y/n): " response
case $response in
[yY] )
echo "Continuing..."
break
;;
[nN] )
echo "Exiting..."
exit 0
;;
* )
echo "Invalid input. Please enter y or n."
;;
esac
done
NAME="AqualinkD"
SOURCE_LOCATION="/extras/aqua.sh"

View File

@ -13,7 +13,7 @@
# * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# * See the GNU General Public License for more details.
# *
# * https://github.com/sfeakes/aqualinkd
# * https://github.com/aqualinkd/aqualinkd
# */

View File

@ -13,7 +13,7 @@
# * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# * See the GNU General Public License for more details.
# *
# * https://github.com/sfeakes/aqualinkd
# * https://github.com/aqualinkd/aqualinkd
# */
MAX_TEMP=78

Binary file not shown.

Binary file not shown.

0
release/aqualinkd.service Executable file → Normal file
View File

View File

@ -24,14 +24,41 @@ MDNSLocation="/etc/avahi/services/"
SOURCEBIN=$BIN
LOG_SYSTEMD=1 # 1=false in bash, 0=true
REMOUNT_RO=1
log()
{
echo "$*"
if [[ $LOG_SYSTEMD -eq 0 ]]; then
logger -p local0.notice -t aqualinkd "Upgrade: $*"
# Below is same as above but will only wotrk on journald (leaving it here if we use that rater then file)
#echo $* | systemd-cat -t aqualinkd_upgrade -p info
#echo "$*" >> "$OUTPUT"
fi
}
if ! tty > /dev/null 2>&1 || [ "$1" = "syslog" ]; then
# No stdin, probably called from upgrade script
LOG_SYSTEMD=0 # Set logger to systemd
fi
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
log "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
if mount / -o remount,rw &>/dev/null; then
# can mount RW.
#mount / -o remount,rw &>/dev/null
log "Root filesystem is readonly, remounted RW"
REMOUNT_RO=0;
else
log "Root filesystem is readonly, can't install"
exit 1
fi
fi
# Figure out what system we are on and set correct binary.
@ -41,7 +68,7 @@ if [ "$PARENT_COMMAND" != "make" ] && [ "$1" != "from-make" ] && [ "$1" != "igno
# dpkg --print-architecture
# Exit if we can't find systemctl
command -v dpkg >/dev/null 2>&1 || { echo -e "Can't detect system architecture, Please check path to 'dpkg' or install manually.\n"\
command -v dpkg >/dev/null 2>&1 || { log -e "Can't detect system architecture, Please check path to 'dpkg' or install manually.\n"\
"Or run '$0 ignorearch'" >&2; exit 1; }
ARCH=$(dpkg --print-architecture)
@ -49,19 +76,19 @@ if [ "$PARENT_COMMAND" != "make" ] && [ "$1" != "from-make" ] && [ "$1" != "igno
case $ARCH in
arm64)
echo "Arch is $ARCH, Using 64bit AqualinkD"
log "Arch is $ARCH, Using 64bit AqualinkD"
BINEXT="-arm64"
;;
armhf)
echo "Arch is $ARCH, Using 32bit AqualinkD"
log "Arch is $ARCH, Using 32bit AqualinkD"
BINEXT="-armhf"
;;
*)
if [ -f $BUILD/$SOURCEBIN-$ARCH ]; then
echo "Arch $ARCH is not officially supported, but we found a suitable binary"
log "Arch $ARCH is not officially supported, but we found a suitable binary"
BINEXT="-$ARCH"
else
echo "Arch $ARCH is unknown, Default to using 32bit HF AqualinkD, you may need to manually try ./release/aqualnkd_arm64"
log "Arch $ARCH is unknown, Default to using 32bit HF AqualinkD, you may need to manually try ./release/aqualnkd_arm64"
BINEXT=""
fi
;;
@ -72,18 +99,18 @@ if [ "$PARENT_COMMAND" != "make" ] && [ "$1" != "from-make" ] && [ "$1" != "igno
SOURCEBIN=$BIN$BINEXT
elif [ -f $BUILD/$SOURCEBIN ]; then
# Not good
echo "Can't find correct aqualnkd binary for $ARCH, '$BUILD/$SOURCEBIN$BINEXT' using '$BUILD/$SOURCEBIN' ";
log "Can't find correct aqualnkd binary for $ARCH, '$BUILD/$SOURCEBIN$BINEXT' using '$BUILD/$SOURCEBIN' ";
fi
fi
# Exit if we can't find binary
if [ ! -f $BUILD/$SOURCEBIN ]; then
echo "Can't find aqualnkd binary `$BUILD/$SOURCEBIN` ";
log "Can't find aqualnkd binary `$BUILD/$SOURCEBIN` ";
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; }
command -v systemctl >/dev/null 2>&1 || { log "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
@ -91,7 +118,7 @@ SERVICE_EXISTS=$(echo $?)
# Clean everything if requested.
if [ "$1" == "clean" ]; then
echo "Deleting install"
log "Deleting install"
systemctl disable $SERVICE > /dev/null 2>&1
if [ -f $BINLocation/$BIN ]; then
rm -f $BINLocation/$BIN
@ -105,6 +132,9 @@ if [ "$1" == "clean" ]; then
if [ -f $DEFLocation/$DEF ]; then
rm -f $DEFLocation/$DEF
fi
if [ -f /etc/cron.d/aqualinkd ]; then
rm -f /etc/cron.d/aqualinkd
fi
if [ -d $WEBLocation ]; then
rm -rf $WEBLocation
fi
@ -115,17 +145,17 @@ fi
# Check cron.d options
if [ ! -d "/etc/cron.d" ]; then
echo "The version of Cron may not support chron.d, if so AqualinkD Scheduler will not work"
echo "Please check before starting"
log "The version of Cron may not support chron.d, if so AqualinkD Scheduler will not work"
log "Please check before starting"
else
if [ -f "/etc/default/cron" ]; then
CD=$(cat /etc/default/cron | grep -v ^# | grep "\-l")
if [ -z "$CD" ]; then
echo "Please enabled cron.d support, if not AqualinkD Scheduler will not work"
echo "Edit /etc/default/cron and look for the -l option, usually in EXTRA_OPTS"
log "Please enabled cron.d support, if not AqualinkD Scheduler will not work"
log "Edit /etc/default/cron and look for the -l option, usually in EXTRA_OPTS"
fi
else
echo "Please make sure the version if Cron supports chron.d, if not the AqualinkD Scheduler will not work"
log "Please make sure the version if Cron supports chron.d, if not the AqualinkD Scheduler will not work"
fi
fi
@ -134,10 +164,12 @@ fi
if [ -f "$WEBLocation/config.js" ]; then
# Test is if has AUX_V1 in file AND "Spa" is in file (Spa_mode changed to Spa)
# Version 2.6.0 added Chiller as well
if ! grep -q 'Aux_V1' $WEBLocation/$file || ! grep -q '"Spa"' $WEBLocation/$file || ! grep -q '"Chiller"' $WEBLocation/$file; then
if ! grep -q '"Aux_V1"' "$WEBLocation/config.js" ||
! grep -q '"Spa"' "$WEBLocation/config.js" ||
! grep -q '"Chiller"' "$WEBLocation/config.js"; then
dateext=`date +%Y%m%d_%H_%M_%S`
echo "AqualinkD web config is old, making copy to $WEBLocation/config.js.$dateext"
echo "Please make changes to new version $WEBLocation/config.js"
log "AqualinkD web config is old, making copy to $WEBLocation/config.js.$dateext"
log "Please make changes to new version $WEBLocation/config.js"
mv $WEBLocation/config.js $WEBLocation/config.js.$dateext
fi
fi
@ -150,24 +182,25 @@ cp $BUILD/$SOURCEBIN $BINLocation/$BIN
cp $BUILD/$SRV $SRVLocation/$SRV
if [ -f $CFGLocation/$CFG ]; then
echo "AqualinkD config exists, did not copy new config, you may need to edit existing! $CFGLocation/$CFG"
log "AqualinkD 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 "AqualinkD defaults exists, did not copy new defaults to $DEFLocation/$DEF"
log "AqualinkD 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"
log "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"
log "Avahi/mDNS may not be installed, not copying $MDNSLocation/$MDNS"
fi
fi
@ -176,20 +209,38 @@ if [ ! -d "$WEBLocation" ]; then
fi
if [ -f "$WEBLocation/config.js" ]; then
echo "AqualinkD web config exists, did not copy new config, you may need to edit existing $WEBLocation/config.js "
rsync -avq --exclude='config.js' $BUILD/../web/* $WEBLocation
log "AqualinkD web config exists, did not copy new config, you may need to edit existing $WEBLocation/config.js "
if command -v "rsync" &>/dev/null; then
rsync -avq --exclude='config.js' $BUILD/../web/* $WEBLocation
else
# This isn;t the right way to do it, but seems to work.
shopt -s extglob
`cp -r "$BUILD/../web/"!(*config.js) "$WEBLocation"`
shopt -u extglob
# Below should work, but doesn't.
#shopt -s extglob
#cp -r "$BUILD/../web/"!(*config.js) "$WEBLocation"
#shopt -u extglob
fi
else
cp -r $BUILD/../web/* $WEBLocation
fi
# remount root ro
if [[ $REMOUNT_RO -eq 0 ]]; then
mount / -o remount,ro &>/dev/null
fi
systemctl enable $SERVICE
systemctl daemon-reload
if [ $SERVICE_EXISTS -eq 0 ]; then
echo "Starting daemon $SERVICE"
log "Starting daemon $SERVICE"
systemctl start $SERVICE
else
echo "Please edit $CFGLocation/$CFG, then start AqualinkD service"
log "Please edit $CFGLocation/$CFG, then start AqualinkD service with `sudo systemctl start aqualinkd`"
fi

View File

@ -1,114 +0,0 @@
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mount.h>
#include <sys/statvfs.h>
#include "aqualink.h"
#include "aq_scheduler.h"
bool remount_root_ro(bool readonly)
{
#ifdef AQ_CONTAINER
// In container this is pointless
return false;
#endif
if (readonly)
{
LOG(AQUA_LOG, LOG_INFO, "reMounting root RO\n");
mount(NULL, "/", NULL, MS_REMOUNT | MS_RDONLY, NULL);
return true;
}
else
{
struct statvfs fsinfo;
statvfs("/", &fsinfo);
if ((fsinfo.f_flag & ST_RDONLY) == 0) // We are readwrite, ignore
return false;
LOG(AQUA_LOG, LOG_INFO, "reMounting root RW\n");
mount(NULL, "/", NULL, MS_REMOUNT, NULL);
return true;
}
}
FILE *aq_open_file(char *filename, bool *ro_root, bool *created_file)
{
FILE *fp;
*ro_root = remount_root_ro(false);
if (access(filename, F_OK) == 0)
{
*created_file = true;
}
fp = fopen(filename, "w");
if (fp == NULL)
{
remount_root_ro(*ro_root);
}
return fp;
}
bool aq_close_file(FILE *file, bool ro_root)
{
if (file != NULL)
fclose(file);
return remount_root_ro(ro_root);
}
#define BUFFER_SIZE 4096
bool copy_file(const char *source_path, const char *destination_path) {
bool ro_root = remount_root_ro(false);
FILE *source_file = fopen(source_path, "rb");
if (source_file == NULL) {
LOG(AQUA_LOG,LOG_ERR, "Error opening source file: %s\n",source_path);
remount_root_ro(ro_root);
return false;
}
FILE *destination_file = fopen(destination_path, "wb");
if (destination_file == NULL) {
LOG(AQUA_LOG,LOG_ERR, "Error opening source file: %s\n",destination_path);
fclose(source_file);
remount_root_ro(ro_root);
return false;
}
char buffer[BUFFER_SIZE];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, source_file)) > 0) {
if (fwrite(buffer, 1, bytes_read, destination_file) != bytes_read) {
LOG(AQUA_LOG,LOG_ERR, "Error writing to destination file: %s\n",destination_path);
fclose(source_file);
fclose(destination_file);
remount_root_ro(ro_root);
return false;
}
}
if (ferror(source_file)) {
LOG(AQUA_LOG,LOG_ERR, "Error reading from source: %s\n",source_path);
fclose(source_file);
fclose(destination_file);
remount_root_ro(ro_root);
return false;
}
fclose(source_file);
fclose(destination_file);
remount_root_ro(ro_root);
return true;
}

View File

@ -29,7 +29,7 @@
#include "config.h"
#include "aq_panel.h"
//#include "utils.h"
#include "aq_filesystem.h"
#include "aq_systemutils.h"

247
source/aq_systemutils.c Normal file
View File

@ -0,0 +1,247 @@
/*
* 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 <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mount.h>
#include <sys/statvfs.h>
#include <sys/wait.h>
#include "aqualink.h"
#include "aq_scheduler.h"
bool remount_root_ro(bool readonly)
{
#ifdef AQ_CONTAINER
// In container this is pointless
return false;
#endif
if (readonly)
{
LOG(AQUA_LOG, LOG_INFO, "reMounting root RO\n");
mount(NULL, "/", NULL, MS_REMOUNT | MS_RDONLY, NULL);
return true;
}
else
{
struct statvfs fsinfo;
statvfs("/", &fsinfo);
if ((fsinfo.f_flag & ST_RDONLY) == 0) // We are readwrite, ignore
return false;
LOG(AQUA_LOG, LOG_INFO, "reMounting root RW\n");
mount(NULL, "/", NULL, MS_REMOUNT, NULL);
return true;
}
}
FILE *aq_open_file(char *filename, bool *ro_root, bool *created_file)
{
FILE *fp;
*ro_root = remount_root_ro(false);
if (access(filename, F_OK) == 0)
{
*created_file = true;
}
fp = fopen(filename, "w");
if (fp == NULL)
{
remount_root_ro(*ro_root);
}
return fp;
}
bool aq_close_file(FILE *file, bool ro_root)
{
if (file != NULL)
fclose(file);
return remount_root_ro(ro_root);
}
#define BUFFER_SIZE 4096
bool copy_file(const char *source_path, const char *destination_path)
{
bool ro_root = remount_root_ro(false);
FILE *source_file = fopen(source_path, "rb");
if (source_file == NULL)
{
LOG(AQUA_LOG, LOG_ERR, "Error opening source file: %s\n", source_path);
remount_root_ro(ro_root);
return false;
}
FILE *destination_file = fopen(destination_path, "wb");
if (destination_file == NULL)
{
LOG(AQUA_LOG, LOG_ERR, "Error opening source file: %s\n", destination_path);
fclose(source_file);
remount_root_ro(ro_root);
return false;
}
char buffer[BUFFER_SIZE];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, source_file)) > 0)
{
if (fwrite(buffer, 1, bytes_read, destination_file) != bytes_read)
{
LOG(AQUA_LOG, LOG_ERR, "Error writing to destination file: %s\n", destination_path);
fclose(source_file);
fclose(destination_file);
remount_root_ro(ro_root);
return false;
}
}
if (ferror(source_file))
{
LOG(AQUA_LOG, LOG_ERR, "Error reading from source: %s\n", source_path);
fclose(source_file);
fclose(destination_file);
remount_root_ro(ro_root);
return false;
}
fclose(source_file);
fclose(destination_file);
remount_root_ro(ro_root);
return true;
}
bool run_aqualinkd_upgrade(bool onlycheck)
{
int pipe_curl_to_bash[2];
pid_t pid_curl, pid_bash;
//char *curl_args[] = {"curl", "-fsSl", "http://tiger/scratch/remote_install.sh", NULL};
//char *curl_args[] = {"curl", "-fsSl", "-H", "Accept: application/vnd.github.raw", "https://api.github.com/repos/sfeakes/AqualinkD/contents/release/remote_install.sh", NULL};
char *curl_args[] = {"curl", "-fsSl", "-H", "Accept: application/vnd.github.raw", "https://api.github.com/repos/AqualinkD/AqualinkD/contents/release/remote_install.sh", NULL};
char *bash_args[] = {"bash", "-s", "--", "check", NULL};
int status_curl, status_bash;
if (!onlycheck) {
bash_args[3] = NULL;
}
if (pipe(pipe_curl_to_bash) == -1)
{
LOG(AQUA_LOG, LOG_ERR,"Upgrade error, opening pipe");
return false;
}
// Fork for curl
pid_curl = fork();
if (pid_curl == -1)
{
LOG(AQUA_LOG, LOG_ERR,"Upgrade error, fork (curl)");
return false;
}
if (pid_curl == 0)
{ // Child process (curl)
close(pipe_curl_to_bash[0]);
dup2(pipe_curl_to_bash[1], STDOUT_FILENO);
close(pipe_curl_to_bash[1]);
execvp("curl", curl_args);
LOG(AQUA_LOG, LOG_ERR,"Upgrade error, execvp (curl)");
return false;
}
// Fork for bash
pid_bash = fork();
if (pid_bash == -1)
{
LOG(AQUA_LOG, LOG_ERR,"Upgrade error, fork (bash)");
return false;
}
if (pid_bash == 0)
{ // Child process (bash)
close(pipe_curl_to_bash[1]);
dup2(pipe_curl_to_bash[0], STDIN_FILENO);
close(pipe_curl_to_bash[0]);
execvp("bash", bash_args);
LOG(AQUA_LOG, LOG_ERR,"Upgrade error, execvp (bash)");
return false;
}
// Parent process
close(pipe_curl_to_bash[0]);
close(pipe_curl_to_bash[1]);
// Wait for curl and get its exit status
if (waitpid(pid_curl, &status_curl, 0) == -1)
{
LOG(AQUA_LOG, LOG_ERR,"Upgrade error, waitpid (curl)");
return false;
}
// Wait for bash and get its exit status
if (waitpid(pid_bash, &status_bash, 0) == -1)
{
LOG(AQUA_LOG, LOG_ERR,"Upgrade error, waitpid (bash)");
return false;
}
// Check the exit status of curl
if (WIFEXITED(status_curl))
{
//printf("curl exited with status: %d\n", WEXITSTATUS(status_curl));
if (WEXITSTATUS(status_curl) != 0) {
LOG(AQUA_LOG, LOG_ERR,"Upgrade error, curl exited with status: %d\n", WEXITSTATUS(status_curl));
return false;
}
}
else if (WIFSIGNALED(status_curl))
{
//printf("curl terminated by signal: %d\n", WTERMSIG(status_curl));
LOG(AQUA_LOG, LOG_ERR,"Upgrade error, curl terminated by signal: %d\n", WTERMSIG(status_curl));
return false;
}
// Check the exit status of bash
if (WIFEXITED(status_bash))
{
//printf("bash exited with status: %d\n", WEXITSTATUS(status_bash));
if (WEXITSTATUS(status_bash) != 0) {
LOG(AQUA_LOG, LOG_ERR,"Upgrade error, bash exited with status: %d\n", WEXITSTATUS(status_bash));
return false;
}
}
else if (WIFSIGNALED(status_bash))
{
//printf("bash terminated by signal: %d\n", WTERMSIG(status_bash));
LOG(AQUA_LOG, LOG_ERR,"Upgrade error, bash terminated by signal: %d\n", WTERMSIG(status_bash));
return false;
}
//printf("Command execution complete.\n");
LOG(AQUA_LOG, LOG_NOTICE, "AqualinkD is upgrading!");
return true;
}

View File

@ -5,7 +5,7 @@
FILE *aq_open_file( char *filename, bool *ro_root, bool* created_file);
bool aq_close_file(FILE *file, bool ro_root);
bool copy_file(const char *source_path, const char *destination_path);
bool run_aqualinkd_upgrade(bool onlycheck);

View File

@ -14,7 +14,8 @@
#define setMASK(bitmask, mask) (bitmask |= mask)
#define removeMASK(bitmask, mask) (bitmask &= ~mask)
#define SIGRESTART SIGUSR1
#define SIGRESTART SIGUSR1
#define SIGRUPGRADE SIGUSR2
#ifdef AQ_NO_THREAD_NETSERVICE
#define DEFAULT_POLL_SPEED -1

View File

@ -57,6 +57,7 @@
#include "debug_timer.h"
#include "aq_scheduler.h"
#include "json_messages.h"
#include "aq_systemutils.h"
#ifdef AQ_MANAGER
#include "serial_logger.h"
@ -102,11 +103,19 @@ bool isAqualinkDStopping() {
void intHandler(int sig_num)
{
if (sig_num == SIGRUPGRADE) {
if (! run_aqualinkd_upgrade(false)) {
LOG(AQUA_LOG,LOG_ERR, "AqualinkD upgrade failed!\n");
}
return; // Let the upgrade process terminate us.
}
LOG(AQUA_LOG,LOG_WARNING, "Stopping!\n");
_keepRunning = false;
if (sig_num == SIGRESTART) {
LOG(AQUA_LOG,LOG_WARNING, "Restarting AqualinkD!\n");
// If we are deamonized, we need to use the system
if (_aqconfig_.deamonize) {
if(fork() == 0) {
@ -502,6 +511,32 @@ int main(int argc, char *argv[])
return startup(argv[0], cfgFile);
}
void check_upgrade_log()
{
FILE *fp;
size_t len = 0;
ssize_t read_size;
char *line = NULL;
fp = fopen("/tmp/aqualinkd_upgrade.log", "r");
if (fp == NULL)
{
// No upgrade file
return;
}
LOG(AQUA_LOG,LOG_NOTICE, "--- AqualinkD Upgrade log ----\n");
while ((read_size = getline(&line, &len, fp)) != -1) {
LOG(AQUA_LOG,LOG_NOTICE, "%s", line);
}
LOG(AQUA_LOG,LOG_NOTICE, "--- End AqualinkD Upgrade log ----\n");
free(line);
fclose(fp);
remove("/tmp/aqualinkd_upgrade.log");
// Need to delete the file here.
}
int startup(char *self, char *cfgFile)
@ -539,6 +574,8 @@ int startup(char *self, char *cfgFile)
LOG(AQUA_LOG,LOG_NOTICE, "Starting %s v%s !\n", AQUALINKD_NAME, AQUALINKD_VERSION);
check_upgrade_log();
check_print_config(&_aqualink_data);
@ -938,6 +975,7 @@ void main_loop()
signal(SIGTERM, intHandler);
signal(SIGQUIT, intHandler);
signal(SIGRESTART, intHandler);
signal(SIGRUPGRADE, intHandler);
if (!start_net_services(&_aqualink_data))
{

View File

@ -46,7 +46,7 @@
#include "aq_scheduler.h"
#include "aq_panel.h"
#include "rs_msg_utils.h"
#include "aq_filesystem.h"
#include "aq_systemutils.h"
#define MAXCFGLINE 256

View File

@ -610,6 +610,8 @@ int build_aqualink_aqmanager_JSON(struct aqualinkdata *aqdata, char* buffer, int
length += sprintf(buffer+length, ",\"config_editor\": \"yes\"");
}
length += sprintf(buffer+length, ",\"aqualinkd_version\":\"%s\"",AQUALINKD_VERSION);
/*
length += sprintf(buffer+length, ",\"panel_type\":\"%s\"",getPanelString());
@ -655,6 +657,7 @@ int build_aqualink_aqmanager_JSON(struct aqualinkdata *aqdata, char* buffer, int
return length;
}
int build_aqualink_status_JSON(struct aqualinkdata *aqdata, char* buffer, int size)
{
//strncpy(buffer, test_message, strlen(test_message)+1);

View File

@ -691,6 +691,8 @@ void mqtt_broadcast_aqualinkstate(struct mg_connection *nc)
int i;
const char *status;
int pumpStatus;
static struct timespec last_update_timestamp = {0, 0};
// We get called about every second, so check time every MQTT_TIMED_UDATE / 2
if (_aqconfig_.mqtt_timed_update) {
@ -926,8 +928,18 @@ void mqtt_broadcast_aqualinkstate(struct mg_connection *nc)
//send_mqtt_timer_duration_msg(nc, _aqualink_data->aqbuttons[i].name, &_aqualink_data->aqbuttons[i]);
// send_mqtt_timer_state_msg will call send_mqtt_timer_duration_msg so no need to do it here.
// Have to use send_mqtt_timer_state_msg due to a timer being set on a device that's already on, (ir no state change so above code does't get hit)
send_mqtt_timer_state_msg(nc, _aqualink_data->aqbuttons[i].name, &_aqualink_data->aqbuttons[i]);
struct timespec current_time;
clock_gettime(CLOCK_MONOTONIC, &current_time);
// Calculate the time difference in nanoseconds
long long time_difference_ns = (current_time.tv_sec - last_update_timestamp.tv_sec) * 1000000000LL + (current_time.tv_nsec - last_update_timestamp.tv_nsec);
// Check if 10 seconds (10 * 10^9 nanoseconds) have passed
if (time_difference_ns >= 10000000000LL || last_update_timestamp.tv_sec == 0) {
send_mqtt_timer_state_msg(nc, _aqualink_data->aqbuttons[i].name, &_aqualink_data->aqbuttons[i]);
last_update_timestamp = current_time;
}
}
}
@ -1174,6 +1186,10 @@ uriAtype action_URI(request_source from, const char *URI, int uri_length, float
LOG(NET_LOG,LOG_NOTICE, "Received restart request!\n");
raise(SIGRESTART);
return uActioned;
} else if (strncmp(ri1, "upgrade", 7) == 0 && from == NET_WS) { // Only valid from websocket.
LOG(NET_LOG,LOG_NOTICE, "Received upgrade request!\n");
raise(SIGRUPGRADE);
return uActioned;
} else if (strncmp(ri1, "seriallogger", 12) == 0 && from == NET_WS) { // Only valid from websocket.
LOG(NET_LOG,LOG_NOTICE, "Received request to run serial_logger!\n");
//LOG(NET_LOG,LOG_NOTICE, "Received request ri1=%s, ri2=%s, ri3=%s value=%f\n",ri1,ri2,ri3,value);
@ -1647,14 +1663,13 @@ void action_web_request(struct mg_connection *nc, struct http_message *http_msg)
// If we have a get request, pass it
if (strncmp(http_msg->uri.p, "/api", 4 ) != 0) {
if (strstr(http_msg->method.p, "GET") && http_msg->query_string.len > 0) {
//LOG(NET_LOG,LOG_ERR, "WEB: Old API stanza requested, ignoring client request\n");
log_http_request(LOG_ERR, "Old API stanza requested, ignoring request :", http_msg);
} else {
//if (strstr(http_msg->method.p, "GET") && http_msg->query_string.len > 0) {
// log_http_request(LOG_ERR, "Old API stanza requested, ignoring request :", http_msg);
//} else {
DEBUG_TIMER_START(&tid);
mg_serve_http(nc, http_msg, _http_server_opts);
DEBUG_TIMER_STOP(tid, NET_LOG, "action_web_request() serve file took");
}
//}
//} else if (strstr(http_msg->method.p, "PUT")) {
} else {
char buf[JSON_BUFFER_SIZE];

View File

@ -4,4 +4,4 @@
#define AQUALINKD_SHORT_NAME "AqualinkD"
// Use Magor . Minor . Patch
#define AQUALINKD_VERSION "2.6.1"
#define AQUALINKD_VERSION "2.6.2"

View File

@ -337,6 +337,9 @@
var _config = {};
const _urlParams = new URLSearchParams(window.location.search);
// if loading confighelp.js failes, create a blank variable that it creates.
if (typeof _confighelp === 'undefined') {
var _confighelp = {};
@ -1284,6 +1287,7 @@
read panel_message, panel_type, version, aqualinkd_version
*/
/*
if (data['aqualinkd_version']) {
document.getElementById("aqualinkdversion").innerHTML = data['aqualinkd_version'];
if (data['aqualinkd_version'] == _latestVersionAvailable ) {
@ -1291,7 +1295,16 @@
} else {
document.getElementById("latesversionavailable").classList.add("newversion");
}
}
//const upgrade = _urlParams.get('upgrade');
//console.log("upgrade = "+upgrade);
if ( isNewerVersion(_latestVersionAvailable, data['aqualinkd_version'] ) || _urlParams.get('upgrade') != null) {
enablebutton("upgrade");
} else {
disablebutton("upgrade");
}
}*/
if (data['version']) {
document.getElementById("panelversion").innerHTML = data['version'];
}
@ -1335,7 +1348,25 @@
if (data['deamonized'] == 'off') {
//console.log("deamonized=" + data['deamonized'] + " Need to rename Restart to Reload");
disablebutton("restart");
disablebutton("upgrade");
} else {
enablebutton("restart");
if (data['aqualinkd_version']) {
document.getElementById("aqualinkdversion").innerHTML = data['aqualinkd_version'];
if (data['aqualinkd_version'] == _latestVersionAvailable ) {
document.getElementById("latesversionavailable").classList.add("hidden");
} else {
document.getElementById("latesversionavailable").classList.add("newversion");
}
if ( isNewerVersion(_latestVersionAvailable, data['aqualinkd_version'] ) || _urlParams.get('upgrade') != null) {
enablebutton("upgrade");
} else {
disablebutton("upgrade");
}
}
}
if (data['config_editor'] == 'yes') {
enablebutton("editconfig");
} else {
@ -1346,6 +1377,8 @@
}, 10000);
}
/*
if (data['logfilename'] == "(null)")
enabletoggle("logfile");
@ -1517,7 +1550,25 @@
socket_di.send(JSON.stringify(msg));
}
function isNewerVersion(version1, version2) {
try{
let [major1, minor1, patch1] = version1.split('.').map(Number => parseInt(Number, 10));
let [major2, minor2, patch2] = version2.split('.').map(Number => parseInt(Number, 10));
if (patch1 === undefined) { patch1 = 0; }
if (patch2 === undefined) { patch2 = 0; }
if (major1 > major2) return true;
if (major1 < major2) return false;
if (minor1 > minor2) return true;
if (minor1 < minor2) return false;
return patch1 > patch2;
} catch (e) {
return false;
}
}
/*
function reset() {
socket_di.send("reset\n");
@ -1526,9 +1577,12 @@
function init() {
// Resize log container
var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
console.log("Height="+h+" Setting to "+(h-290) );
//console.log("Height="+h+" Setting to "+(h-290) );
document.documentElement.style.setProperty('--logcontainer-height', (h-290)+'px');
disablebutton("restart");
disablebutton("upgrade");
startWebsockets();
getLatestVersion();
}
@ -1566,6 +1620,10 @@
var cmd = {};
//cmd.uri = "rawcommand"
switch (source.id) {
case "upgrade":
cmd.uri = "upgrade"
// NEED TO REGET aqmanager after restart.
break;
case "restart":
cmd.uri = "restart"
// NEED TO REGET aqmanager after restart.
@ -1640,7 +1698,8 @@
<td colspan="2" align="center"><label id="statusmsg" class="statusmsg">status</label></td>
</tr>
<tr>
<td colspan="2" align="center"><input id="restart" type="button" onclick="send(this);" value="Restart AqualinkD"></td>
<td colspan="1" align="center"><input id="restart" type="button" onclick="send(this);" value="Restart AqualinkD"></td>
<td colspan="1" align="center"><input id="upgrade" type="button" onclick="send(this);" value="Upgrade AqualinkD"></td>
</tr>
<!--
<tr>

View File

@ -647,6 +647,32 @@
showTileOptions(false);
startWebsockets();
resetBackgroundSize();
// Test for mobile-app mode
if (window.navigator.standalone === true || window.matchMedia('(display-mode: standalone)').matches) {
//console.log("Running in standalone mode");
//alert("Standalone");
// in webapp mode, remove the aqmanager link
try{
document.getElementById('td_cog').classList.add("hide");
} catch (e){}
} else {
//console.log("Not running in standalone mode");
//alert("NOT Standalone");
}
// Test for iframe
if (window.self !== window.top) {
//console.log("The code is running inside an iframe.");
// Change the link under the cog/hamburger icon
ele = document.getElementById('cog_link');
ele.setAttribute("href", "/");
ele.setAttribute("target", "_parent");
} else {
//console.log("The code is not running inside an iframe.");
}
}
function populateLightProgram(type=0, current_mode="") {
@ -2726,6 +2752,14 @@
<div id="header" class="head" onclick="showBackground();">
<table border='0' width='100%' cellpadding='0' cellspacing='0'>
<tr>
<td width="25px" align='left' id='td_cog' style="vertical-align:middle">
<a id="cog_link" href="/aqmanager.html">
<svg viewBox='0 0 10 8' width='18' height='18'>
<path d='M1 1h8M1 4h 8M1 7h8' stroke='#FFF' stroke-width='1.2' stroke-linecap='round'/>
</svg>
</a>
</td>
</td>
<td width="70px" align='left' id='td_name'><span class='title' id='name'
onclick="showVersion(this);event.stopPropagation();">AqualinkD</span>
</td>