diff --git a/.docker/Dockerfile.armv6 b/.docker/Dockerfile.armv6 new file mode 100644 index 00000000..ac77f1f3 --- /dev/null +++ b/.docker/Dockerfile.armv6 @@ -0,0 +1,29 @@ +# Use a base image compatible with ARMv6 architecture +FROM arm32v6/node:14-alpine + +# Install necessary build dependencies +RUN apk add --no-cache make gcc g++ python3 linux-headers udev tzdata git + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies (both development and production) +RUN npm ci + +# Copy the rest of the application +COPY . . + +# Build the application +RUN npm run build + +# Set environment variables +ENV NODE_ENV=production + +# Expose the port (if applicable) +EXPOSE 4200 + +# Command to run the application +CMD ["node", "dist/app.js"] diff --git a/.docker/Dockerfile.armv7 b/.docker/Dockerfile.armv7 new file mode 100644 index 00000000..2adef10a --- /dev/null +++ b/.docker/Dockerfile.armv7 @@ -0,0 +1,29 @@ +# Use a base image compatible with ARMv7 architecture +FROM arm32v7/node:14-alpine + +# Install necessary build dependencies +RUN apk add --no-cache make gcc g++ python3 linux-headers udev tzdata git + +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies (both development and production) +RUN npm ci + +# Copy the rest of the application +COPY . . + +# Build the application +RUN npm run build + +# Set environment variables +ENV NODE_ENV=production + +# Expose the port (if applicable) +EXPOSE 4200 + +# Command to run the application +CMD ["node", "dist/app.js"] diff --git a/.docker/Dockerfile.linux b/.docker/Dockerfile.linux new file mode 100644 index 00000000..81998e0f --- /dev/null +++ b/.docker/Dockerfile.linux @@ -0,0 +1,62 @@ +# Use a Linux-based image as the build stage for the first application +FROM node:lts-alpine AS build-njspc + +# Install necessary build dependencies +RUN apk add --no-cache make gcc g++ python3 linux-headers udev tzdata + +# Create the app directory and set ownership for the first application +RUN mkdir -p /app/nodejs-poolcontroller && chown node:node /app/nodejs-poolcontroller + +# Set the working directory for the first application +WORKDIR /app/nodejs-poolcontroller + +# Copy the source code and necessary files for the first application +COPY ./nodejs-poolcontroller/defaultConfig.json ./config.json +COPY ./nodejs-poolcontroller ./ +COPY ./.docker/ecosystem.config.js ./ecosystem.config.js + +# Install dependencies and build the first application +RUN npm ci +RUN npm run build + +# Second stage for the second application +FROM node:lts-alpine AS build-njspc-dp + +# Create the app directory and set ownership for the second application +RUN mkdir -p /app/nodejs-poolcontroller-dashpanel && chown node:node /app/nodejs-poolcontroller-dashpanel + +# Set the working directory for the second application +WORKDIR /app/nodejs-poolcontroller-dashpanel + +# Copy the source code and necessary files for the second application +COPY ./nodejs-poolcontroller-dashpanel ./ + +# Install dependencies and build the second application +RUN npm ci +RUN npm run build + +# Fourth stage for the final combined image +FROM node:lts-alpine AS final + +# Install PM2 globally +RUN npm install pm2 -g + +# Create the app directory and set ownership +RUN mkdir -p /app && chown node:node /app + +# Set the working directory for the final combined image +WORKDIR /app + +# Copy built applications from the previous stages into the final image +COPY --from=build-njspc /app/nodejs-poolcontroller ./nodejs-poolcontroller +COPY --from=build-njspc-dp /app/nodejs-poolcontroller-dashpanel ./nodejs-poolcontroller-dashpanel +# COPY --from=build-rem /app/relayEquipmentManager ./relayEquipmentManager + +# Copy the ecosystem configuration file from the build stage +COPY --from=build-njspc /app/nodejs-poolcontroller/ecosystem.config.js ./ecosystem.config.js + +# Expose any necessary ports +EXPOSE 4200 5150 + +# Define the command to run both applications using PM2 +CMD ["pm2-runtime", "start", "ecosystem.config.js"] diff --git a/.docker/Dockerfile.windows b/.docker/Dockerfile.windows new file mode 100644 index 00000000..5ebb178b --- /dev/null +++ b/.docker/Dockerfile.windows @@ -0,0 +1,43 @@ +# Use a Windows-based image as the build stage +FROM mcr.microsoft.com/windows/servercore:ltsc2019 AS build + +# Set the working directory +WORKDIR C:\app + +# Copy package.json and package-lock.json files +COPY package*.json ./ +COPY defaultConfig.json config.json + +# Install Node.js +RUN curl -sL https://nodejs.org/dist/v14.17.6/node-v14.17.6-x64.msi -o node.msi +RUN msiexec /i node.msi /quiet + +# Install npm dependencies +RUN npm ci + +# Copy the rest of the application files +COPY . . + +# Build the application +RUN npm run build + +# Second stage for the production image +FROM mcr.microsoft.com/windows/servercore:ltsc2019 as prod + +# Install git +RUN powershell -Command "Invoke-WebRequest -Uri https://github.com/git-for-windows/git/releases/download/v2.33.1.windows.1/Git-2.33.1-64-bit.exe -OutFile git.exe" && .\git.exe /VERYSILENT /NORESTART + +# Create the app directory +RUN mkdir C:\app + +# Set the working directory +WORKDIR C:\app + +# Copy built application from the build stage +COPY --from=build C:\app . + +# Set environment variables +ENV NODE_ENV=production + +# Define the entrypoint +CMD ["node", "dist/app.js"] diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml new file mode 100644 index 00000000..dd10ea07 --- /dev/null +++ b/.docker/docker-compose.yml @@ -0,0 +1,47 @@ +version: '3.8' + +services: + poolcontroller: + image: tagyoureit/nodejs-poolcontroller:latest + container_name: poolcontroller + restart: always + group_add: + - dialout + devices: + - /dev/ttyUSB0 + ports: + - "4200:4200" + volumes: + - /data/poolcontroller/config.json:/app/nodejs-poolcontroller/config.json + - /data/poolcontroller/data:/app/nodejs-poolcontroller/data + - /data/poolcontroller/logs:/app/nodejs-poolcontroller/logs + poolcontroller-dashpanel: + restart: always + container_name: poolcontroller-dashpanel + ports: + - "5150:5150" + volumes: + - /data/poolcontroller-dashpanel/config.json:/app/nodejs-poolcontroller-dashpanel/config.json + image: rstrouse/nodejs-poolcontroller-dashpanel:latest + depends_on: + - poolcontroller + + # poolcontroller-armv7: + # image: tagyoureit/nodejs-poolcontroller-armv7:latest + # container_name: poolcontroller-armv7 + # restart: always + # ports: + # - "4200:4200" + # volumes: + # - /data/poolcontroller/config.json:/app/config.json + # - /data/poolcontroller/data:/app/data + + # poolcontroller-armv6: + # image: tagyoureit/nodejs-poolcontroller-armv6:latest + # container_name: poolcontroller-armv6 + # restart: always + # ports: + # - "4200:4200" + # volumes: + # - /data/poolcontroller/config.json:/app/config.json + # - /data/poolcontroller/data:/app/data diff --git a/.docker/ecosystem.config.js b/.docker/ecosystem.config.js new file mode 100644 index 00000000..9061ea3a --- /dev/null +++ b/.docker/ecosystem.config.js @@ -0,0 +1,35 @@ +module.exports = { + apps: [ + { + name: "dashPanel", + script: "npm", + args: ["start"], + cwd: "/app/nodejs-poolcontroller-dashpanel", + restart_delay: 10000, + watch: [ + "pages", + "scripts", + "server", + "package.json" + ], + watch_delay: 5000, + kill_timeout: 15000 + }, + { + name: "njsPC", + script: "npm", + args: ["start"], + cwd: "/app/nodejs-poolcontroller", + restart_delay: 10000, + watch: [ + "config", + "controller", + "logger", + "web", + "package.json" + ], + watch_delay: 5000, + kill_timeout: 15000 + } + ] +}; diff --git a/.github/workflows/docker-publish-njsPC-linux.yml b/.github/workflows/docker-publish-njsPC-linux.yml new file mode 100644 index 00000000..78dfe831 --- /dev/null +++ b/.github/workflows/docker-publish-njsPC-linux.yml @@ -0,0 +1,81 @@ +name: Publish Docker Image - Ubuntu + +on: + push: + branches: + - master + - docker + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Checkout code from tagyoureit/nodejs-poolcontroller + uses: actions/checkout@v3 + with: + repository: tagyoureit/nodejs-poolcontroller + ref: master # or specify the branch or tag to pull from + path: nodejs-poolcontroller + + - name: Checkout code from rstrouse/nodejs-poolcontroller-dashpanel + uses: actions/checkout@v3 + with: + repository: rstrouse/nodejs-poolcontroller-dashpanel # + ref: master # Specify the branch or tag to pull from + path: nodejs-poolcontroller-dashpanel # Specify the directory to checkout the code into + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push combined Docker image + uses: docker/build-push-action@v4 + with: + context: . + file: ./.docker/Dockerfile.linux + push: true + tags: | + tagyoureit/njspc-dp-combined-app:latest + + + # - name: Build and push njsPC Linux Docker image (x86_64) + # uses: docker/build-push-action@v4 + # with: + # context: . + # file: ./.docker/Dockerfile.linux + # push: true + # tags: | + # tagyoureit/nodejs-poolcontroller:latest + + # - name: Build and push njsPC-dP Linux Docker image (x86_64) + # uses: docker/build-push-action@v4 + # with: + # context: nodejs-poolcontroller-dashpanel + # file: ./.docker/Dockerfile.linux + # push: true + # tags: | + # rstrouse/nodejs-poolcontroller-dashpanel:latest + + # - name: Build and push ARMv7 Docker image + # uses: docker/build-push-action@v4 + # with: + # context: . + # file: ./.docker/Dockerfile.armv7 # Adjust the path to your ARMv7 Dockerfile + # push: true + # tags: | + # tagyoureit/nodejs-poolcontroller-armv7:latest + + # - name: Build and push ARMv6 Docker image + # uses: docker/build-push-action@v4 + # with: + # context: . + # file: ./.docker/Dockerfile.armv6 # Adjust the path to your ARMv6 Dockerfile + # push: true + # tags: | + # tagyoureit/nodejs-poolcontroller-armv6:latest diff --git a/.github/workflows/docker-publish-njsPC-windows.yml b/.github/workflows/docker-publish-njsPC-windows.yml new file mode 100644 index 00000000..597c4777 --- /dev/null +++ b/.github/workflows/docker-publish-njsPC-windows.yml @@ -0,0 +1,41 @@ +name: Publish Docker Image - Windows + +on: + push: + branches: + - docker + workflow_dispatch: + + +jobs: + build-and-push-windows: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - + # Add support for more platforms with QEMU (optional) + # https://github.com/docker/setup-qemu-action + name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Windows Docker image + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile.windows + push: true + tags: | + tagyoureit/nodejs-poolcontroller:windows-latest + platforms: windows/amd64 diff --git a/README.md b/README.md index cf36869e..4e8941d3 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ```diff -- INTELLICENTER USERS: Do not upgrade to 2.006. Rollback to 1.064 to use this application. +- INTELLICENTER USERS: Do not upgrade Intellicenter to 2.006. Rollback to 1.064 to use this application. ``` # nodejs-poolController - Version 8.0 @@ -52,7 +52,7 @@ This is only the server code. See [clients](#module_nodejs-poolController--clie ### Prerequisites If you don't know anything about NodeJS, these directions might be helpful. -1. Install Nodejs (v12 recommended). (https://nodejs.org/en/download/) +1. Install Nodejs (v16+ required). (https://nodejs.org/en/download/) 1. Update NPM (https://docs.npmjs.com/getting-started/installing-node). 1. It is recommended to clone the source code as updates are frequently pushed while releases are infrequent clone with `git clone https://github.com/tagyoureit/nodejs-poolController.git` diff --git a/controller/Constants.ts b/controller/Constants.ts index c55555c6..22c168b4 100755 --- a/controller/Constants.ts +++ b/controller/Constants.ts @@ -18,10 +18,11 @@ along with this program. If not, see . import { EventEmitter } from 'events'; import { logger } from "../logger/Logger"; import * as util from 'util'; -export class Heliotrope { - constructor() { - this.isCalculated = false; - this._zenith = 90 + 50 / 60; +class HeliotropeContext { + constructor(longitude: number, latitude: number, zenith: number) { + this._zenith = typeof zenith !== 'undefined' ? zenith : 90 + 50 / 60; + this._longitude = longitude; + this._latitude = latitude; } private dMath = { sin: function (deg) { return Math.sin(deg * (Math.PI / 180)); }, @@ -31,42 +32,14 @@ export class Heliotrope { acos: function (x) { return (180 / Math.PI) * Math.acos(x); }, atan: function (x) { return (180 / Math.PI) * Math.atan(x); } } - public isCalculated: boolean = false; - public isValid: boolean = false; - public get date() { return this.dt; } - public set date(dt: Date) { - if (typeof this.dt === 'undefined' || - this.dt.getFullYear() !== dt.getFullYear() || - this.dt.getMonth() !== dt.getMonth() || - this.dt.getDate() !== dt.getDate()) { - this.isCalculated = false; - // Always store a copy since we don't want to create instances where the change doesn't get reflected. This - // also could hold onto references that we don't want held for garbage cleanup. - this.dt = typeof dt !== 'undefined' && typeof dt.getMonth === 'function' ? new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), - dt.getHours(), dt.getMinutes(), dt.getSeconds(), dt.getMilliseconds()) : undefined; - } - } - public get longitude() { return this._longitude; } - public set longitude(lon: number) { - if (this._longitude !== lon) this.isCalculated = false; - this._longitude = lon; - } - public get latitude() { return this._latitude; } - public set latitude(lat: number) { - if (this._latitude !== lat) this.isCalculated = false; - this._latitude = lat; - } - public get zenith() { return this._zenith; } - public set zenith(zen: number) { - if (this._zenith !== zen) this.isCalculated = false; - this._zenith = zen; - } private dt: Date; private _longitude: number; private _latitude: number; private _zenith: number; - private _dtSunrise: Date; - private _dtSunset: Date; + public get longitude() { return this._longitude; } + public get latitude() { return this._latitude; } + public get zenith() { return this._zenith; } + public get isValid(): boolean { return typeof this._longitude === 'number' && typeof this._latitude === 'number'; } private get longitudeHours(): number { return this.longitude / 15.0; } private get doy(): number { return Math.ceil((this.dt.getTime() - new Date(this.dt.getFullYear(), 0, 1).getTime()) / 8.64e7); } private get sunriseApproxTime(): number { return this.doy + ((6.0 - this.longitudeHours) / 24.0); } @@ -125,57 +98,175 @@ export class Heliotrope { dtLocal.setFullYear(this.dt.getFullYear(), this.dt.getMonth(), this.dt.getDate()); return dtLocal; } - public get isNight(): boolean { - let times = this.calculatedTimes; + public calculate(dt: Date): { dt: Date, sunrise: Date, sunset: Date } { + let times = { dt: this.dt = dt, sunrise: undefined, sunset: undefined }; if (this.isValid) { - let time = new Date().getTime(); - if (time >= times.sunset.getTime() && time < times.sunrise.getTime()) return true; - } - return false; - } - public calculate() { - if (typeof this.dt !== 'undefined' - && typeof this._latitude !== 'undefined' - && typeof this._longitude !== 'undefined' - && typeof this._zenith !== 'undefined') { let sunriseLocal = this.sunriseLocalTime; let sunsetLocal = this.sunsetLocalTime; if (typeof sunriseLocal !== 'undefined') { sunriseLocal = (sunriseLocal - this.longitudeHours); while (sunriseLocal >= 24) sunriseLocal -= 24; while (sunriseLocal < 0) sunriseLocal += 24; - this._dtSunrise = this.toLocalTime(sunriseLocal); + times.sunrise = this.toLocalTime(sunriseLocal); } - else this._dtSunrise = undefined; + else times.sunrise = undefined; if (typeof sunsetLocal !== 'undefined') { sunsetLocal = (sunsetLocal - this.longitudeHours); while (sunsetLocal >= 24) sunsetLocal -= 24; while (sunsetLocal < 0) sunsetLocal += 24; - this._dtSunset = this.toLocalTime(sunsetLocal); + times.sunset = this.toLocalTime(sunsetLocal); } - else this._dtSunset = undefined; - logger.verbose(`sunriseLocal:${sunriseLocal} sunsetLocal:${sunsetLocal} Calculating Heliotrope Valid`); - this.isValid = typeof this._dtSunrise !== 'undefined' && typeof this._dtSunset !== 'undefined'; - this.isCalculated = true; + else times.sunset = undefined; } - else { - logger.warn(`dt:${this.dt} lat:${this._latitude} lon:${this._longitude} Not enough information to calculate Heliotrope. See https://github.com/tagyoureit/nodejs-poolController/issues/245`); - this.isValid = false; - this._dtSunset = undefined; - this._dtSunrise = undefined; + return times; + } + +} +export class Heliotrope { + constructor() { + this.isCalculated = false; + this._zenith = 90 + 50 / 60; + } + public isCalculated: boolean = false; + public get isValid(): boolean { return typeof this.dt !== 'undefined' && typeof this.dt.getMonth === 'function' && typeof this._longitude === 'number' && typeof this._latitude === 'number'; } + public get date() { return this.dt; } + public set date(dt: Date) { + if (typeof this.dt === 'undefined' || + this.dt.getFullYear() !== dt.getFullYear() || + this.dt.getMonth() !== dt.getMonth() || + this.dt.getDate() !== dt.getDate()) { this.isCalculated = false; + // Always store a copy since we don't want to create instances where the change doesn't get reflected. This + // also could hold onto references that we don't want held for garbage cleanup. + this.dt = typeof dt !== 'undefined' && typeof dt.getMonth === 'function' ? new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), + dt.getHours(), dt.getMinutes(), dt.getSeconds(), dt.getMilliseconds()) : undefined; } - + } + public get longitude() { return this._longitude; } + public set longitude(lon: number) { + if (this._longitude !== lon) { + this.isCalculated = false; + } + this._longitude = lon; + } + public get latitude() { return this._latitude; } + public set latitude(lat: number) { + if (this._latitude !== lat) { + this.isCalculated = false; + } + this._latitude = lat; + } + public get zenith() { return this._zenith; } + public set zenith(zen: number) { + if (this._zenith !== zen) this.isCalculated = false; + this._zenith = zen; + } + private dt: Date; + private _longitude: number; + private _latitude: number; + private _zenith: number; + private _dtSunrise: Date; + private _dtSunset: Date; + private _dtNextSunrise: Date; + private _dtNextSunset: Date; + private _dtPrevSunrise: Date; + private _dtPrevSunset: Date; + public get isNight(): boolean { + let times = this.calculatedTimes; + if (this.isValid) { + let time = new Date().getTime(); + if (time >= times.sunset.getTime()) // We are after sunset. + return time < times.nextSunrise.getTime(); // It is night so long as we are less than the next sunrise. + // Normally this would be updated on 1 min after midnight so it should never be true. + else + return time < times.sunrise.getTime(); // If the Heliotrope is updated then we need to declare pre-sunrise to be night. + // This is the normal condition where it would be night since at 1 min after midnight the sunrise/sunset + // will get updated. So it will be before sunrise that it will still be night. + } + return false; + } + public calculate(dt: Date): { isValid: boolean, dt: Date, sunrise: Date, sunset: Date, nextSunrise: Date, nextSunset: Date, prevSunrise: Date, prevSunset: Date } { + let ctx = new HeliotropeContext(this.longitude, this.latitude, this.zenith); + let ret = { isValid: ctx.isValid, dt: dt, sunrise: undefined, sunset: undefined, nextSunrise: undefined, nextSunset: undefined, prevSunrise: undefined, prevSunset: undefined }; + if (ctx.isValid) { + let tToday = ctx.calculate(dt); + let tTom = ctx.calculate(new Date(dt.getTime() + 86400000)); + let tYesterday = ctx.calculate(new Date(dt.getTime() - 86400000)); + ret.sunrise = tToday.sunrise; + ret.sunset = tToday.sunset; + ret.nextSunrise = tTom.sunrise; + ret.nextSunset = tTom.sunset; + ret.prevSunrise = tYesterday.sunrise; + ret.prevSunset = tYesterday.sunset; + } + return ret; + } + private calcInternal() { + if (this.isValid) { + let times = this.calculate(this.dt); + this._dtSunrise = times.sunrise; + this._dtSunset = times.sunset; + this._dtNextSunrise = times.nextSunrise; + this._dtNextSunset = times.nextSunset; + this._dtPrevSunrise = times.prevSunrise; + this._dtPrevSunset = times.prevSunset; + this.isCalculated = true; + logger.verbose(`Calculated Heliotrope: sunrise:${Timestamp.toISOLocal(this._dtSunrise)} sunset:${Timestamp.toISOLocal(this._dtSunset)}`); + } + else + logger.warn(`dt:${this.dt} lat:${this._latitude} lon:${this._longitude} Not enough information to calculate Heliotrope. See https://github.com/tagyoureit/nodejs-poolController/issues/245`); } public get sunrise(): Date { - if (!this.isCalculated) this.calculate(); + if (!this.isCalculated) this.calcInternal(); return this._dtSunrise; } public get sunset(): Date { - if (!this.isCalculated) this.calculate(); + if (!this.isCalculated) this.calcInternal(); return this._dtSunset; } - public get calculatedTimes(): any { return { sunrise: this.sunrise, sunset: this.sunset, isValid: this.isValid }; } + public get nextSunrise(): Date { + if (!this.isCalculated) this.calcInternal(); + return this._dtNextSunrise; + } + public get nextSunset(): Date { + if (!this.isCalculated) this.calcInternal(); + return this._dtNextSunset; + } + public get prevSunrise(): Date { + if (!this.isCalculated) this.calcInternal(); + return this._dtPrevSunrise; + } + public get prevSunset(): Date { + if (!this.isCalculated) this.calcInternal(); + return this._dtPrevSunset; + } + public get calculatedTimes(): { sunrise?: Date, sunset?: Date, nextSunrise?: Date, nextSunset?: Date, prevSunrise?: Date, prevSunset: Date, isValid: boolean } { return { sunrise: this.sunrise, sunset: this.sunset, nextSunrise: this.nextSunrise, nextSunset: this.nextSunset, prevSunrise: this.prevSunrise, prevSunset: this.prevSunset, isValid: this.isValid }; } + public calcAdjustedTimes(dt: Date, hours = 0, min = 0): { sunrise?: Date, sunset?: Date, nextSunrise?: Date, nextSunset?: Date, prevSunrise?: Date, prevSunset: Date, isValid: boolean } { + if (this.dt.getFullYear() === dt.getFullYear() && this.dt.getMonth() === dt.getMonth() && this.dt.getDate() === dt.getDate()) return this.getAdjustedTimes(hours, min); + let ms = (hours * 3600000) + (min * 60000); + let times = this.calculate(dt); + return { + sunrise: new Date(times.sunrise.getTime() + ms), + sunset: new Date(times.sunset.getTime() + ms), + nextSunrise: new Date(times.nextSunrise.getTime() + ms), + nextSunset: new Date(times.nextSunset.getTime() + ms), + prevSunrise: new Date(times.prevSunrise.getTime() + ms), + prevSunset: new Date(times.prevSunset.getTime() + ms), + isValid: this.isValid + } + } + public getAdjustedTimes(hours = 0, min = 0): { sunrise?: Date, sunset?: Date, nextSunrise?: Date, nextSunset?: Date, prevSunrise?: Date, prevSunset: Date, isValid: boolean } { + let ms = (hours * 3600000) + (min * 60000); + return { + sunrise: new Date(this.sunrise.getTime() + ms), + sunset: new Date(this.sunset.getTime() + ms), + nextSunrise: new Date(this.nextSunrise.getTime() + ms), + nextSunset: new Date(this.nextSunset.getTime() + ms), + prevSunrise: new Date(this.prevSunrise.getTime() + ms), + prevSunset: new Date(this.prevSunset.getTime() + ms), + isValid: this.isValid + } + } } export class Timestamp { private static dateTextISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/; @@ -190,7 +281,7 @@ export class Timestamp { } private _isUpdating: boolean = false; public static get now(): Timestamp { return new Timestamp(); } - public toDate() { return this._dt; } + public toDate(): Date { return this._dt; } public get isValid() { return this._dt instanceof Date && !isNaN(this._dt.getTime()); } @@ -225,7 +316,8 @@ export class Timestamp { public set fullYear(val: number) { this._dt.setFullYear(val); } public get year(): number { return this._dt.getFullYear(); } public set year(val: number) { - let y = val < 100 ? (Math.floor(this._dt.getFullYear() / 100) * 100) + val : val; + let dt = new Date(); + let y = val < 100 ? (Math.floor(dt.getFullYear() / 100) * 100) + val : val; if (y !== this.year) { this._dt.setFullYear(y); this.emitter.emit('change'); @@ -248,7 +340,8 @@ export class Timestamp { public getDay(): number { return this._dt.getDay(); } public getTime() { return this._dt.getTime(); } public format(): string { return Timestamp.toISOLocal(this._dt); } - public static toISOLocal(dt): string { + public static toISOLocal(dt: Date): string { + if (typeof dt === 'undefined' || typeof dt.getTime !== 'function' || isNaN(dt.getTime())) return ''; let tzo = dt.getTimezoneOffset(); var pad = function (n) { var t = Math.floor(Math.abs(n)); diff --git a/controller/Equipment.ts b/controller/Equipment.ts index 396f3a7f..e9911458 100644 --- a/controller/Equipment.ts +++ b/controller/Equipment.ts @@ -1185,6 +1185,8 @@ export class Schedule extends EqItem { if (typeof this.data.startTimeType === 'undefined') this.data.startTimeType = 0; if (typeof this.data.endTimeType === 'undefined') this.data.endTimeType = 0; if (typeof this.data.display === 'undefined') this.data.display = 0; + if (typeof this.data.startTimeOffset === 'undefined') this.data.startTimeOffset = 0; + if (typeof this.data.endTimeOffset === 'undefined') this.data.endTimeOffset = 0; } // todo: investigate schedules having startDate and _startDate @@ -1196,6 +1198,11 @@ export class Schedule extends EqItem { public set startTime(val: number) { this.setDataVal('startTime', val); } public get endTime(): number { return this.data.endTime; } public set endTime(val: number) { this.setDataVal('endTime', val); } + public get startTimeOffset(): number { return this.data.startTimeOffset || 0; } + public set startTimeOffset(val: number) { this.setDataVal('startTimeOffset', val); } + public get endTimeOffset(): number { return this.data.endTimeOffset || 0; } + public set endTimeOffset(val: number) { this.setDataVal('endTimeOffset', val); } + public get scheduleDays(): number { return this.data.scheduleDays; } public set scheduleDays(val: number) { this.setDataVal('scheduleDays', val); } public get circuit(): number { return this.data.circuit; } @@ -1218,7 +1225,10 @@ export class Schedule extends EqItem { public set startDay(val: number) { if (typeof this._startDate === 'undefined') this._startDate = new Date(); this._startDate.setDate(val); this._saveStartDate(); } public get startYear(): number { if (typeof this._startDate === 'undefined') this._startDate = new Date(); return this._startDate.getFullYear(); } public set startYear(val: number) { if (typeof this._startDate === 'undefined') this._startDate = new Date(); this._startDate.setFullYear(val < 100 ? val + 2000 : val); this._saveStartDate(); } - public get startDate(): Date { return typeof this._startDate === 'undefined' ? this._startDate = new Date() : this._startDate; } + public get startDate(): Date { + this._startDate = typeof this._startDate === 'undefined' ? new Date(this.data.startDate) : this._startDate; + return typeof this._startDate === 'undefined' || isNaN(this._startDate.getTime()) ? new Date() : this._startDate; + } public set startDate(val: Date) { this._startDate = val; } public get scheduleType(): number | any { return this.data.scheduleType; } public set scheduleType(val: number | any) { this.setDataVal('scheduleType', sys.board.valueMaps.scheduleTypes.encode(val)); } diff --git a/controller/Lockouts.ts b/controller/Lockouts.ts index a6268240..646c3fd1 100644 --- a/controller/Lockouts.ts +++ b/controller/Lockouts.ts @@ -135,6 +135,21 @@ export class ManualPriorityDelay extends EquipmentDelay { logger.info(`Manual Operation Priority cancelled for ${this.circuitState.name}`); this._delayTimer = undefined; this.circuitState.manualPriorityActive = false; + // Rip through all the schedules and clear the manual priority. + let sscheds = state.schedules.getActiveSchedules(); + let circIds = []; + for (let i = 0; i < sscheds.length; i++) { + let ssched = sscheds[i]; + ssched.manualPriorityActive = false; + if (!circIds.includes(ssched.circuit)) circIds.push(ssched.circuit); + } + for (let i = 0; i < circIds.length; i++) { + let circ = sys.circuits.getInterfaceById(circIds[i]); + if (!circ.isActive) continue; + let cstate = state.circuits.getInterfaceById(circ.id); + sys.board.circuits.setEndTime(circ, cstate, cstate.isOn, true); + } + delayMgr.deleteDelay(this.id); } public clearDelay() { diff --git a/controller/State.ts b/controller/State.ts index df676c94..5727ee68 100644 --- a/controller/State.ts +++ b/controller/State.ts @@ -23,10 +23,11 @@ import * as util from 'util'; import { logger } from '../logger/Logger'; import { webApp } from '../web/Server'; import { ControllerType, Timestamp, utils, Heliotrope } from './Constants'; -import { sys, Chemical, ChemController, ChemicalTank, ChemicalPump } from './Equipment'; +import { sys, Chemical, ChemController, ChemicalTank, ChemicalPump, Schedule } from './Equipment'; import { versionCheck } from '../config/VersionCheck'; import { DataLogger, DataLoggerEntry } from '../logger/DataLogger'; import { delayMgr } from './Lockouts'; +import { time } from 'console'; export class State implements IState { statePath: string; @@ -212,6 +213,8 @@ export class State implements IState { model: sys.equipment.model, sunrise: self.data.sunrise || '', sunset: self.data.sunset || '', + nextSunrise: self.data.nextSunrise || '', + nextSunset: self.data.nextSunset || '', alias: sys.general.alias, freeze: utils.makeBool(self.data.freeze), valveMode: self.data.valveMode || {}, @@ -368,8 +371,16 @@ export class State implements IState { EqStateCollection.removeNullIds(sdata.chemControllers); EqStateCollection.removeNullIds(sdata.chemDosers); EqStateCollection.removeNullIds(sdata.filters); + // Initialize the schedules. + if (typeof sdata.schedules !== 'undefined') { + for (let i = 0; i < sdata.schedules.length; i++) { + let ssched = sdata.schedules[i]; + ssched.manualPriorityActive = ssched.isOn = ssched.triggered = false; + if (typeof ssched.scheduleTime !== 'undefined') ssched.scheduleTime.calculated = false; + } + } var self = this; - let pnlTime = typeof sdata.time !== 'undefined' ? new Date(sdata.time) : new Date(); + let pnlTime = typeof sdata.time !== 'undefined' && sdata.time !== '' ? new Date(sdata.time) : new Date(); if (isNaN(pnlTime.getTime())) pnlTime = new Date(); this._dt = new Timestamp(pnlTime); this._dt.milliseconds = 0; @@ -387,6 +398,10 @@ export class State implements IState { let times = self.heliotrope.calculatedTimes; self.data.sunrise = times.isValid ? Timestamp.toISOLocal(times.sunrise) : ''; self.data.sunset = times.isValid ? Timestamp.toISOLocal(times.sunset) : ''; + self.data.nextSunrise = times.isValid ? Timestamp.toISOLocal(times.nextSunrise) : ''; + self.data.nextSunset = times.isValid ? Timestamp.toISOLocal(times.nextSunset) : ''; + self.data.prevSunrise = times.isValid ? Timestamp.toISOLocal(times.prevSunrise) : ''; + self.data.prevSunset = times.isValid ? Timestamp.toISOLocal(times.prevSunset) : ''; versionCheck.checkGitRemote(); }); this.status = 0; // Initializing @@ -510,6 +525,7 @@ export interface ICircuitState { isOn: boolean; startTime?: Timestamp; endTime: Timestamp; + priority?: string, lightingTheme?: number; action?: number; emitEquipmentChange(); @@ -1067,6 +1083,224 @@ export class PumpState extends EqState { } export class ScheduleStateCollection extends EqStateCollection { public createItem(data: any): ScheduleState { return new ScheduleState(data); } + public getActiveSchedules(): ScheduleState[] { + let activeScheds: ScheduleState[] = []; + for (let i = 0; i < this.length; i++) { + let ssched = this.getItemByIndex(i); + let st = ssched.scheduleTime; + let sched = sys.schedules.getItemById(ssched.id); + if (!sched.isActive || ssched.disabled) { + continue; + } + st.calcSchedule(state.time, sys.schedules.getItemById(ssched.id)); + if (typeof st.startTime === 'undefined') continue; + if (ssched.isOn || st.shouldBeOn || st.startTime.getTime() > new Date().getTime()) activeScheds.push(ssched); + } + return activeScheds; + } +} +export class ScheduleTime extends ChildEqState { + public initData() { if (typeof this.data.times !== 'undefined') delete this.data.times; } + public get calculatedDate(): Date { return typeof this.data.calculatedDate !== 'undefined' && this.data.calculatedDate !== '' ? new Date(this.data.calculatedDate) : new Date(1970, 0, 1); } + public set calculatedDate(val: Date) { this._saveTimestamp(val, 'calculatedDate', false); } + public get startTime(): Date { return typeof this.data.startTime !== 'undefined' && this.data.startTime !== '' ? new Date(this.data.startTime) : null; } + public set startTime(val: Date) { this._saveTimestamp(val, 'startTime'); } + public get endTime(): Date { return typeof this.data.endTime !== 'undefined' && this.data.endTime !== '' ? new Date(this.data.endTime) : null; } + public set endTime(val: Date) { this._saveTimestamp(val, 'endTime'); } + private _saveTimestamp(dt, prop, persist:boolean = true) { + if (typeof dt === 'undefined' || !dt) this.setDataVal(prop, ''); + else this.setDataVal(prop, Timestamp.toISOLocal(dt)); + } + private _calcShouldBeOn(time: number) : boolean { + let tmStart = this.startTime ? this.startTime.getTime() : NaN; + let tmEnd = this.endTime ? this.endTime.getTime() : NaN; + if (isNaN(tmStart) || isNaN(tmEnd) || time < tmStart || time > tmEnd) return false; + return true; + } + public get shouldBeOn(): boolean { + let shouldBeOn = this._calcShouldBeOn(state.time.getTime()); + if (this.data.shouldBeOn !== shouldBeOn) this.setDataVal('shouldBeOn', shouldBeOn); + return this.data.shouldBeOn || false; + } + protected set shouldBeOn(val: boolean) { this.setDataVal('shouldBeOn', val); } + public get calculated(): boolean { return this.data.calculated; } + public set calculated(val: boolean) { this.setDataVal('calculated', val); } + public calcScheduleDate(ts: Timestamp, sched: Schedule): { startTime: Date, endTime: Date } { + let times: { startTime: Date, endTime: Date } = { startTime: null, endTime: null }; + try { + let sod = ts.clone().startOfDay(); + let ysod = ts.clone().addHours(-24).startOfDay(); + let nsod = ts.clone().addHours(-24).startOfDay(); + let ytimes: { startTime: Date, endTime: Date } = { startTime: null, endTime: null }; // Yesterday + let ttimes: { startTime: Date, endTime: Date } = { startTime: null, endTime: null }; // Today + let ntimes: { startTime: Date, endTime: Date } = { startTime: null, endTime: null }; // Tomorrow + let tt = sys.board.valueMaps.scheduleTimeTypes.transform(sched.startTimeType); + // Add the range for today and yesterday. + switch (tt.name) { + case 'sunrise': + let sr = state.heliotrope.calcAdjustedTimes(sod.toDate(), 0, sched.startTimeOffset); + ytimes.startTime = sr.prevSunrise; + ttimes.startTime = sr.sunrise; + ntimes.startTime = sr.nextSunrise; + break; + case 'sunset': + let ss = state.heliotrope.calcAdjustedTimes(sod.toDate(), 0, sched.startTimeOffset); + ytimes.startTime = ss.prevSunset; + ttimes.startTime = ss.sunset; + ntimes.startTime = ss.nextSunset; + break; + default: + ytimes.startTime = ysod.clone().addMinutes(sched.startTime).toDate(); + ttimes.startTime = sod.clone().addMinutes(sched.startTime).toDate(); + ntimes.startTime = nsod.clone().addMinutes(sched.startTime).toDate(); + break; + } + tt = sys.board.valueMaps.scheduleTimeTypes.transform(sched.endTimeType); + switch (tt.name) { + case 'sunrise': + let sr = state.heliotrope.calcAdjustedTimes(sod.toDate(), 0, sched.endTimeOffset); + // If the start time of the previous window is greater than the previous sunrise then we use the sunrise for today. + ytimes.endTime = ytimes.startTime >= sr.prevSunrise ? sr.sunrise : sr.prevSunrise; + // If ths start time of the current window is greater than the current sunrise then we use the sunrise for tomorrow. + ttimes.endTime = ttimes.startTime >= sr.sunrise ? sr.nextSunrise : sr.sunrise; + ntimes.endTime = ntimes.startTime >= sr.nextSunrise ? new Timestamp(sr.nextSunrise).addHours(24).toDate() : sr.nextSunrise; + break; + case 'sunset': + let ss = state.heliotrope.calcAdjustedTimes(sod.toDate(), 0, sched.endTimeOffset); + // If the start time of the previous window is greater than the previous sunset then we use the sunset for today. + ytimes.endTime = ytimes.startTime >= ss.prevSunset ? ss.sunset : ss.prevSunset; + ttimes.endTime = ttimes.startTime >= ss.sunset ? ss.nextSunset : ss.nextSunset; + ntimes.endTime = ntimes.startTime >= ss.nextSunset ? new Timestamp(ss.nextSunset).addHours(24).toDate() : ss.nextSunset; + break; + default: + ytimes.endTime = ysod.clone().addMinutes(sched.endTime).toDate(); + if (ytimes.endTime <= ytimes.startTime) ytimes.endTime = ysod.clone().addHours(24).addMinutes(sched.endTime).toDate(); + ttimes.endTime = sod.clone().addMinutes(sched.endTime).toDate(); + if (ttimes.endTime <= ttimes.startTime) ttimes.endTime = sod.clone().addHours(24).addMinutes(sched.endTime).toDate(); + ntimes.endTime = nsod.clone().addMinutes(sched.endTime).toDate(); + if (ntimes.endTime <= ntimes.startTime) ntimes.endTime = nsod.clone().addHours(24).addMinutes(sched.endTime).toDate(); + break; + } + ttimes.startTime.setSeconds(0, 0); // Set the start time to the beginning of the minute. + ttimes.endTime.setSeconds(59, 999); // Set the end time to the end of the minute. + ytimes.startTime.setSeconds(0, 0); + ytimes.endTime.setSeconds(59, 999); + ntimes.startTime.setSeconds(0, 0); + ntimes.endTime.setSeconds(59, 999); + // Now check the dow for each range. If the start time for the dow matches then include it. If not then do not. + let schedDays = sys.board.valueMaps.scheduleDays.toArray(); + let fnInRange = (time, times) => { + let tmStart = times.startTime ? times.startTime.getTime() : NaN; + let tmEnd = times.endTime ? times.endTime.getTime() : NaN; + if (isNaN(tmStart) || isNaN(tmEnd) || time < tmStart || time > tmEnd) return false; + return true; + } + let tm = ts.getTime(); + if (fnInRange(tm, ttimes)) { + // Check the dow. + let sd = schedDays.find(elem => elem.dow === ttimes.startTime.getDay()); + if (typeof sd !== 'undefined' && (sched.scheduleDays & sd.bitval) !== 0) { + times.startTime = ttimes.startTime; + times.endTime = ttimes.endTime; + return times; + } + } + // First check if we are still running yesterday. This will ensure we have + // the first runtime. + if (fnInRange(tm, ytimes)) { + // Check the dow. + let sd = schedDays.find(elem => elem.dow === ytimes.startTime.getDay()); + if (typeof sd !== 'undefined' && (sched.scheduleDays & sd.bitval) !== 0) { + times.startTime = ytimes.startTime; + times.endTime = ytimes.startTime; + return times; + } + } + // Then check if we are running today. If we have already run then get net next run + // time. + if (tm <= ttimes.startTime.getTime()) { + let sd = schedDays.find(elem => elem.dow === ttimes.startTime.getDay()); + if (typeof sd !== 'undefined' && (sched.scheduleDays & sd.bitval) !== 0) { + times.startTime = ttimes.startTime; + times.endTime = ttimes.endTime; + return times; + } + } + // Then look for tomorrow. + if (tm <= ntimes.startTime.getTime()) { + let sd = schedDays.find(elem => elem.dow === ntimes.startTime.getDay()); + if (typeof sd !== 'undefined' && (sched.scheduleDays & sd.bitval) !== 0) { + times.startTime = ntimes.startTime; + times.endTime = ntimes.endTime; + return times; + } + } + return times; + } catch (err) { + logger.error(`Error calculating date for schedule ${sched.id}: ${err.message}`); + } + finally { return times; } + } + public calcSchedule(currentTime: Timestamp, sched: Schedule): boolean { + try { + let sod = currentTime.clone().startOfDay(); + + // There are 3 conditions where the schdedule will be recalculated. The first + // 1. The calculated flag is false + // 2. The calculated flag is true and the calculated date < the current start of day + // 3. Regardless of the calculated date the current end time has passed and the start time is + // from a prior date. This will happen when the schedule is complete and we need to calculate the + // next run time. + let dtCalc = typeof this.calculatedDate !== 'undefined' && typeof this.calculatedDate.getTime === 'function' ? new Date(this.calculatedDate.getTime()).setHours(0, 0, 0, 0) : new Date(1970, 0, 1, 0, 0).getTime(); + let recalc = !this.calculated; + if (!recalc && sod.getTime() !== dtCalc) recalc = true; + if (!recalc && (this.endTime.getTime() < new Date().getTime() && this.startTime.getTime() < dtCalc)) { + recalc = true; + logger.info(`Recalculating expired schedule ${sched.id}`); + } + if (!recalc) return this.shouldBeOn; + //if (this.calculated && sod.getTime() === dtCalc) return this.shouldBeOn; + this.calculatedDate = new Date(new Date().setHours(0, 0, 0, 0)); + if (sched.isActive === false || sched.disabled) return false; + let tt = sys.board.valueMaps.scheduleTimeTypes.transform(sched.startTimeType); + // If this is a runonce schedule we need to check for the rundate + let type = sys.board.valueMaps.scheduleTypes.transform(sched.scheduleType); + let times = type.name === 'runonce' ? this.calcScheduleDate(new Timestamp(sched.startDate), sched) : this.calcScheduleDate(state.time.clone(), sched); + if (times.startTime && times.endTime.getTime() > currentTime.getTime()) { + // Check to see if it should be on. + this.startTime = times.startTime; + this.endTime = times.endTime; + this.calculated = true; + return this.shouldBeOn; + } + else { + // Chances are that the current dow is not valid. Fast forward until we get a day that works. That will + // be the next scheduled run date. + if (type.name !== 'runonce' && sched.scheduleDays > 0) { + let schedDays = sys.board.valueMaps.scheduleDays.toArray(); + let day = sod.clone().addHours(24); + let dow = day.getDay(); + while (dow !== sod.getDay()) { + let sd = schedDays.find(elem => elem.dow === day.getDay()); + if (typeof sd !== 'undefined' && (sched.scheduleDays & sd.bitval) !== 0) { + times = this.calcScheduleDate(day, sched); + break; + } + else day.addHours(24); + } + } + this.startTime = times.startTime; + this.endTime = times.endTime; + this.calculated = true; + } + return this.shouldBeOn; + } catch (err) { + this.calculated = true; + this.calculatedDate = new Date(new Date().setHours(0, 0, 0, 0)); + this.startTime = null; + this.endTime = null; + } + } } export class ScheduleState extends EqState { constructor(data: any, dataName?: string) { super(data, dataName); } @@ -1095,10 +1329,17 @@ export class ScheduleState extends EqState { public set startTime(val: number) { this.setDataVal('startTime', val); } public get endTime(): number { return this.data.endTime; } public set endTime(val: number) { this.setDataVal('endTime', val); } + public get startTimeOffset(): number { return this.data.startTimeOffset || 0; } + public set startTimeOffset(val: number) { this.setDataVal('startTimeOffset', val); } + public get endTimeOffset(): number { return this.data.endTimeOffset || 0; } + public set endTimeOffset(val: number) { this.setDataVal('endTimeOffset', val); } + public get circuit(): number { return this.data.circuit; } public set circuit(val: number) { this.setDataVal('circuit', val); } public get disabled(): boolean { return this.data.disabled; } public set disabled(val: boolean) { this.setDataVal('disabled', val); } + public get triggered(): boolean { return this.data.triggered || false; } + public set triggered(val: boolean) { this.setDataVal('triggered', val); } public get scheduleType(): number { return typeof (this.data.scheduleType) !== 'undefined' ? this.data.scheduleType.val : undefined; } public set scheduleType(val: number) { if (this.scheduleType !== val) { @@ -1152,11 +1393,18 @@ export class ScheduleState extends EqState { public set isOn(val: boolean) { this.setDataVal('isOn', val); } public get manualPriorityActive(): boolean { return this.data.manualPriorityActive; } public set manualPriorityActive(val: boolean) { this.setDataVal('manualPriorityActive', val); } + public get scheduleTime(): ScheduleTime { return new ScheduleTime(this.data, 'scheduleTime', this); } + public recalculate(force?: boolean) { + if (force === true) this.scheduleTime.calculated = false; + this.scheduleTime.calcSchedule(state.time, sys.schedules.getItemById(this.id)); + } public getExtended() { let sched = this.get(true); // Always operate on a copy. //if (typeof this.circuit !== 'undefined') sched.circuit = state.circuits.getInterfaceById(this.circuit).get(true); - //else sched.circuit = {}; + sched.clockMode = sys.board.valueMaps.clockModes.transform(sys.general.options.clockMode) || {}; + //let times = this.calcScheduleTimes(sched); + //sched.times = { shouldBeOn: times.shouldBeOn, startTime: times.shouldBeOn ? Timestamp.toISOLocal(times.startTime) : '', endTime: times.shouldBeOn ? Timestamp.toISOLocal(times.endTime) : '' }; return sched; } public emitEquipmentChange() { @@ -1227,6 +1475,8 @@ export class CircuitGroupState extends EqState implements ICircuitGroupState, IC } public get isOn(): boolean { return this.data.isOn; } public set isOn(val: boolean) { this.setDataVal('isOn', val); } + public get priority(): string { return this.data.priority || 'manual' } + public set priority(val: string) { this.setDataVal('priority', val); } public get endTime(): Timestamp { if (typeof this.data.endTime === 'undefined') return undefined; return new Timestamp(this.data.endTime); @@ -1316,6 +1566,8 @@ export class LightGroupState extends EqState implements ICircuitGroupState, ICir this.hasChanged = true; } } + public get priority(): string { return this.data.priority || 'manual' } + public set priority(val: string) { this.setDataVal('priority', val); } public get endTime(): Timestamp { if (typeof this.data.endTime === 'undefined') return undefined; return new Timestamp(this.data.endTime); @@ -1581,6 +1833,13 @@ export class HeaterState extends EqState { this.hasChanged = true; } } + public get prevHeaterOffTemp(): number { return this.data.prevHeaterOffTemp; } + public set prevHeaterOffTemp(val: number) { + if (this.prevHeaterOffTemp !== val) { + this.data.prevHeaterOffTemp = val; + if (typeof val === 'undefined') delete this.data.prevHeaterOffTemp; + } + } public get startupDelay(): boolean { return this.data.startupDelay; } public set startupDelay(val: boolean) { this.setDataVal('startupDelay', val); } public get shutdownDelay(): boolean { return this.data.shutdownDelay; } @@ -1633,6 +1892,8 @@ export class FeatureState extends EqState implements ICircuitState { } public get showInFeatures(): boolean { return this.data.showInFeatures; } public set showInFeatures(val: boolean) { this.setDataVal('showInFeatures', val); } + public get priority(): string { return this.data.priority || 'manual' } + public set priority(val: string) { this.setDataVal('priority', val); } public get endTime(): Timestamp { if (typeof this.data.endTime === 'undefined') return undefined; return new Timestamp(this.data.endTime); @@ -1657,6 +1918,8 @@ export class VirtualCircuitState extends EqState implements ICircuitState { public set nameId(val: number) { this.setDataVal('nameId', val); } public get isOn(): boolean { return this.data.isOn; } public set isOn(val: boolean) { this.setDataVal('isOn', val); } + public get priority(): string { return 'manual' } // These are always manual priority + public set priority(val: string) { ; } public get type() { return typeof this.data.type !== 'undefined' ? this.data.type.val : -1; } public set type(val: number) { if (this.type !== val) { @@ -1732,6 +1995,8 @@ export class CircuitState extends EqState implements ICircuitState { this.hasChanged = true; } } + public get priority(): string { return this.data.priority; } + public set priority(val: string) { this.setDataVal('priority', val); } public get showInFeatures(): boolean { return this.data.showInFeatures; } public set showInFeatures(val: boolean) { this.setDataVal('showInFeatures', val); } public get isOn(): boolean { return this.data.isOn; } @@ -1765,6 +2030,8 @@ export class CircuitState extends EqState implements ICircuitState { this.hasChanged = true; } } + public get scheduled(): boolean { return this.data.scheduled || false } + public set scheduled(val: boolean) { this.setDataVal('scheduled', val); } public get startTime(): Timestamp { if (typeof this.data.startTime === 'undefined') return undefined; return new Timestamp(this.data.startTime); @@ -2367,6 +2634,7 @@ export class ChemDoserState extends EqState implements IChemicalState, IChemCont export class ChemControllerState extends EqState implements IChemControllerState { public initData() { + if (typeof this.activeBodyId === 'undefined') this.data.activeBodyId = 0; if (typeof this.data.saturationIndex === 'undefined') this.data.saturationIndex = 0; if (typeof this.data.flowDetected === 'undefined') this.data.flowDetected = false; if (typeof this.data.orp === 'undefined') this.data.orp = {}; @@ -2470,6 +2738,8 @@ export class ChemControllerState extends EqState implements IChemControllerState public set address(val: number) { this.setDataVal('address', val); } public get isBodyOn(): boolean { return this.data.isBodyOn; } public set isBodyOn(val: boolean) { this.data.isBodyOn = val; } + public get activeBodyId(): number { return this.data.activeBodyId || 0; } + public set activeBodyId(val: number) { this.data.activeBodyId = val; } public get flowDetected(): boolean { return this.data.flowDetected; } public set flowDetected(val: boolean) { this.data.flowDetected = val; } public get status(): number { @@ -3454,8 +3724,9 @@ export class FilterState extends EqState { } public get pressureUnits(): number { return typeof this.data.pressureUnits === 'undefined' ? 0 : this.data.pressureUnits.val; } public set pressureUnits(val: number) { - if (this.pressureUnits !== val) { + if (this.pressureUnits !== val || typeof this.data.pressureUnits === 'undefined') { this.setDataVal('pressureUnits', sys.board.valueMaps.pressureUnits.transform(val)); + this.hasChanged = true; } } public get pressure(): number { return this.data.pressure; } diff --git a/controller/boards/EasyTouchBoard.ts b/controller/boards/EasyTouchBoard.ts index 31e3adee..41193110 100644 --- a/controller/boards/EasyTouchBoard.ts +++ b/controller/boards/EasyTouchBoard.ts @@ -721,7 +721,8 @@ export class TouchScheduleCommands extends ScheduleCommands { let schedDays = sys.board.schedules.transformDays(typeof data.scheduleDays !== 'undefined' ? data.scheduleDays : sched.scheduleDays || 255); // default to all days let changeHeatSetpoint = typeof (data.changeHeatSetpoint !== 'undefined') ? utils.makeBool(data.changeHeatSetpoint) : sched.changeHeatSetpoint; let display = typeof data.display !== 'undefined' ? data.display : sched.display || 0; - + let endTimeOffset = typeof data.endTimeOffset !== 'undefined' ? data.endTimeOffset : sched.endTimeOffset; + let startTimeOffset = typeof data.startTimeOffset !== 'undefined' ? data.startTimeOffset : sched.startTimeOffset; // Ensure all the defaults. // if (isNaN(startDate.getTime())) startDate = new Date(); if (typeof startTime === 'undefined') startTime = 480; // 8am @@ -766,10 +767,10 @@ export class TouchScheduleCommands extends ScheduleCommands { if (state.heliotrope.isCalculated) { const sunrise = state.heliotrope.sunrise.getHours() * 60 + state.heliotrope.sunrise.getMinutes(); const sunset = state.heliotrope.sunset.getHours() * 60 + state.heliotrope.sunset.getMinutes(); - if (startTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunrise')) startTime = sunrise; - else if (startTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunset')) startTime = sunset; - if (endTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunrise')) endTime = sunrise; - else if (endTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunset')) endTime = sunset; + if (startTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunrise')) startTime = (sunrise + startTimeOffset); + else if (startTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunset')) startTime = (sunset + startTimeOffset); + if (endTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunrise')) endTime = (sunrise + endTimeOffset); + else if (endTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunset')) endTime = (sunset + endTimeOffset); } @@ -813,6 +814,8 @@ export class TouchScheduleCommands extends ScheduleCommands { sched.endTimeType = ssched.endTimeType = endTimeType; sched.isActive = ssched.isActive = true; ssched.display = sched.display = display; + sched.startTimeOffset = ssched.startTimeOffset = startTimeOffset; + sched.endTimeOffset = ssched.endTimeOffset = endTimeOffset; ssched.emitEquipmentChange(); // For good measure russ is sending out a config request for // the schedule in question. If there was a failure on the @@ -969,19 +972,19 @@ export class TouchScheduleCommands extends ScheduleCommands { let sUpdated = false; let sched = sys.schedules.getItemByIndex(i); if (sched.startTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunrise') && sched.startTime !== sunrise) { - sched.startTime = sunrise; + sched.startTime = sunrise + (sched.startTimeOffset || 0); anyUpdated = sUpdated = true; } else if (sched.startTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunset') && sched.startTime !== sunset) { - sched.startTime = sunset; + sched.startTime = sunset + (sched.startTimeOffset || 0); anyUpdated = sUpdated = true; } if (sched.endTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunrise') && sched.endTime !== sunrise) { - sched.endTime = sunrise; + sched.endTime = sunrise + (sched.endTimeOffset || 0); anyUpdated = sUpdated = true; } else if (sched.endTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunset') && sched.endTime !== sunset) { - sched.endTime = sunset; + sched.endTime = sunset + (sched.endTimeOffset || 0); anyUpdated = sUpdated = true; } if (sUpdated) { @@ -1963,14 +1966,18 @@ class TouchChlorinatorCommands extends ChlorinatorCommands { if (typeof superChlorinate === 'undefined') superChlorinate = utils.makeBool(chlor.superChlor); } if (typeof obj.disabled !== 'undefined') chlor.disabled = utils.makeBool(obj.disabled); + if (typeof obj.body !== 'undefined') chlor.body = parseInt(obj.body, 10); if (typeof chlor.body === 'undefined') chlor.body = parseInt(obj.body, 10) || 32; // Verify the data. - let body = sys.board.bodies.mapBodyAssociation(chlor.body).val; - if (typeof body === 'undefined') { + let bdy = sys.board.bodies.mapBodyAssociation(chlor.body || 0); + let body; + if (typeof bdy === 'undefined') { if (sys.equipment.shared) body = 32; else if (!sys.equipment.dual) body = 0; else return Promise.reject(new InvalidEquipmentDataError(`Chlorinator body association is not valid: ${body}`, 'chlorinator', body)); } + else + body = bdy.val; if (poolSetpoint > 100 || poolSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator poolSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.poolSetpoint)); if (spaSetpoint > 100 || spaSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator spaSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.spaSetpoint)); if (typeof obj.ignoreSaltReading !== 'undefined') chlor.ignoreSaltReading = utils.makeBool(obj.ignoreSaltReading); @@ -2032,7 +2039,7 @@ class TouchChlorinatorCommands extends ChlorinatorCommands { let id = parseInt(obj.id, 10); if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator id is not valid: ${obj.id}`, 'chlorinator', obj.id)); let chlor = sys.chlorinators.getItemById(id); - if (chlor.master === 1) return await super.deleteChlorAsync(obj); + if (chlor.master >= 1) return await super.deleteChlorAsync(obj); if (sl.enabled) { await sl.chlor.setChlorEnabledAsync(false); } diff --git a/controller/boards/IntelliCenterBoard.ts b/controller/boards/IntelliCenterBoard.ts index 62a6d989..f6ac328a 100644 --- a/controller/boards/IntelliCenterBoard.ts +++ b/controller/boards/IntelliCenterBoard.ts @@ -26,6 +26,7 @@ import { state, ChlorinatorState, LightGroupState, VirtualCircuitState, ICircuit import { utils } from '../../controller/Constants'; import { InvalidEquipmentIdError, InvalidEquipmentDataError, EquipmentNotFoundError, MessageError, InvalidOperationError } from '../Errors'; import { ncp } from '../nixie/Nixie'; +import { Timestamp } from "../Constants" export class IntelliCenterBoard extends SystemBoard { public needsConfigChanges: boolean = false; constructor(system: PoolSystem) { @@ -389,6 +390,10 @@ export class IntelliCenterBoard extends SystemBoard { for (let i = 0; i < sys.circuits.length; i++) { let c = sys.circuits.getItemByIndex(i); if (c.id <= 40) c.master = 0; + if (typeof sys.board.valueMaps.circuitFunctions.get(c.type).isLight) { + let s = state.circuits.getItemById(c.id); + if (s.action !== 0) s.action = 0; + } } for (let i = 0; i < sys.valves.length; i++) { let v = sys.valves.getItemByIndex(i); @@ -2168,6 +2173,7 @@ class IntelliCenterCircuitCommands extends CircuitCommands { 255, 255, 0, 0, 0, 0], // 30-35 3); + // Circuits are always contiguous so we don't have to worry about // them having a strange offset like features and groups. However, in // single body systems they start with 2. @@ -2175,13 +2181,14 @@ class IntelliCenterCircuitCommands extends CircuitCommands { // We are using the index and setting the circuits based upon // the index. This way it doesn't matter what the sort happens to // be and whether there are gaps in the ids or not. The ordinal is the bit number. - let circuit = state.circuits.getItemByIndex(i); - let ordinal = circuit.id - 1; + let cstate = state.circuits.getItemByIndex(i); + let ordinal = cstate.id - 1; + if (ordinal >= 40) continue; let ndx = Math.floor(ordinal / 8); let byte = out.payload[ndx + 3]; let bit = ordinal - (ndx * 8); - if (circuit.id === id) byte = isOn ? byte = byte | (1 << bit) : byte; - else if (circuit.isOn) byte = byte | (1 << bit); + if (cstate.id === id) byte = isOn ? byte = byte | (1 << bit) : byte; + else if (cstate.isOn) byte = byte | (1 << bit); out.payload[ndx + 3] = byte; } // Set the bits for the features. @@ -2191,6 +2198,7 @@ class IntelliCenterCircuitCommands extends CircuitCommands { // be and whether there are gaps in the ids or not. The ordinal is the bit number. let feature = state.features.getItemByIndex(i); let ordinal = feature.id - sys.board.equipmentIds.features.start; + if (ordinal >= 32) continue; let ndx = Math.floor(ordinal / 8); let byte = out.payload[ndx + 9]; let bit = ordinal - (ndx * 8); @@ -2202,6 +2210,7 @@ class IntelliCenterCircuitCommands extends CircuitCommands { for (let i = 0; i < state.data.circuitGroups.length; i++) { let group = state.circuitGroups.getItemByIndex(i); let ordinal = group.id - sys.board.equipmentIds.circuitGroups.start; + if (ordinal >= 16) continue; let ndx = Math.floor(ordinal / 8); let byte = out.payload[ndx + 13]; let bit = ordinal - (ndx * 8); @@ -2213,6 +2222,7 @@ class IntelliCenterCircuitCommands extends CircuitCommands { for (let i = 0; i < state.data.lightGroups.length; i++) { let group = state.lightGroups.getItemByIndex(i); let ordinal = group.id - sys.board.equipmentIds.circuitGroups.start; + if (ordinal >= 16) continue; let ndx = Math.floor(ordinal / 8); let byte = out.payload[ndx + 13]; let bit = ordinal - (ndx * 8); @@ -2248,6 +2258,7 @@ class IntelliCenterCircuitCommands extends CircuitCommands { for (let i = 0; i < state.data.schedules.length; i++) { let sched = state.schedules.getItemByIndex(i); let ordinal = sched.id - 1; + if (ordinal >= 100) continue; let ndx = Math.floor(ordinal / 8); let byte = out.payload[ndx + 15]; let bit = ordinal - (ndx * 8); @@ -2258,9 +2269,9 @@ class IntelliCenterCircuitCommands extends CircuitCommands { let dow = dt.getDay(); // Convert the dow to the bit value. let sd = sys.board.valueMaps.scheduleDays.toArray().find(elem => elem.dow === dow); - let dayVal = sd.bitVal || sd.val; // The bitval allows mask overrides. + //let dayVal = sd.bitVal || sd.val; // The bitval allows mask overrides. let ts = dt.getHours() * 60 + dt.getMinutes(); - if ((sched.scheduleDays & dayVal) > 0 && ts >= sched.startTime && ts <= sched.endTime) byte = byte | (1 << bit); + if ((sched.scheduleDays & sd.bitval) > 0 && ts >= sched.startTime && ts <= sched.endTime) byte = byte | (1 << bit); } } else if (sched.isOn) byte = byte | (1 << bit); @@ -2817,6 +2828,7 @@ class IntelliCenterBodyCommands extends BodyCommands { body2: { heatMode: number, heatSetpoint: number, coolSetpoint: number } }; private async queueBodyHeatSettings(bodyId?: number, byte?: number, data?: any): Promise { + logger.debug(`queueBodyHeatSettings: ${JSON.stringify(this.bodyHeatSettings)}`); // remove this line if #848 is fixed if (typeof this.bodyHeatSettings === 'undefined') { let body1 = sys.bodies.getItemById(1); let body2 = sys.bodies.getItemById(2); @@ -2862,21 +2874,30 @@ class IntelliCenterBodyCommands extends BodyCommands { retries: 2, response: IntelliCenterBoard.getAckResponse(168) }); - await out.sendAsync(); - let body1 = sys.bodies.getItemById(1); - let sbody1 = state.temps.bodies.getItemById(1); - body1.heatMode = sbody1.heatMode = bhs.body1.heatMode; - body1.heatSetpoint = sbody1.heatSetpoint = bhs.body1.heatSetpoint; - body1.coolSetpoint = sbody1.coolSetpoint = bhs.body1.coolSetpoint; - if (sys.equipment.dual || sys.equipment.shared) { - let body2 = sys.bodies.getItemById(2); - let sbody2 = state.temps.bodies.getItemById(2); - body2.heatMode = sbody2.heatMode = bhs.body2.heatMode; - body2.heatSetpoint = sbody2.heatSetpoint = bhs.body2.heatSetpoint; - body2.coolSetpoint = sbody2.coolSetpoint = bhs.body2.coolSetpoint; - } - bhs.processing = false; - state.emitEquipmentChanges(); + try { + await out.sendAsync(); + let body1 = sys.bodies.getItemById(1); + let sbody1 = state.temps.bodies.getItemById(1); + body1.heatMode = sbody1.heatMode = bhs.body1.heatMode; + body1.heatSetpoint = sbody1.heatSetpoint = bhs.body1.heatSetpoint; + body1.coolSetpoint = sbody1.coolSetpoint = bhs.body1.coolSetpoint; + if (sys.equipment.dual || sys.equipment.shared) { + let body2 = sys.bodies.getItemById(2); + let sbody2 = state.temps.bodies.getItemById(2); + body2.heatMode = sbody2.heatMode = bhs.body2.heatMode; + body2.heatSetpoint = sbody2.heatSetpoint = bhs.body2.heatSetpoint; + body2.coolSetpoint = sbody2.coolSetpoint = bhs.body2.coolSetpoint; + } + state.emitEquipmentChanges(); + } catch (err) { + bhs.processing = false; + bhs.bytes = []; + throw (err); + } + finally { + bhs.processing = false; + bhs.bytes = []; + } return true; } else { @@ -2885,7 +2906,10 @@ class IntelliCenterBodyCommands extends BodyCommands { setTimeout(async () => { try { await this.queueBodyHeatSettings(); - } catch (err) { logger.error(`Error sending queued body setpoint message: ${err.message}`); } + } catch (err) { + logger.error(`Error sending queued body setpoint message: ${err.message}`); + throw (err); + } }, 3000); } else bhs.processing = false; @@ -3119,6 +3143,7 @@ class IntelliCenterBodyCommands extends BodyCommands { } } class IntelliCenterScheduleCommands extends ScheduleCommands { + _lastScheduleCheck: number = 0; public async setScheduleAsync(data: any): Promise { if (typeof data.id !== 'undefined') { let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); @@ -3141,6 +3166,8 @@ class IntelliCenterScheduleCommands extends ScheduleCommands { let endTime = typeof data.endTime !== 'undefined' ? data.endTime : sched.endTime; let schedDays = sys.board.schedules.transformDays(typeof data.scheduleDays !== 'undefined' ? data.scheduleDays : sched.scheduleDays); let display = typeof data.display !== 'undefined' ? data.display : sched.display || 0; + let endTimeOffset = typeof data.endTimeOffset !== 'undefined' ? data.endTimeOffset : sched.endTimeOffset; + let startTimeOffset = typeof data.startTimeOffset !== 'undefined' ? data.startTimeOffset : sched.startTimeOffset; // Ensure all the defaults. if (isNaN(startDate.getTime())) startDate = new Date(); @@ -3216,11 +3243,45 @@ class IntelliCenterScheduleCommands extends ScheduleCommands { ssched.isActive = sched.isActive = true; ssched.display = sched.display = display; ssched.emitEquipmentChange(); + ssched.startTimeOffset = sched.startTimeOffset = startTimeOffset; + ssched.endTimeOffset = sched.endTimeOffset = endTimeOffset; return sched; } else return Promise.reject(new InvalidEquipmentIdError('No schedule information provided', undefined, 'Schedule')); } + public syncScheduleStates() { + // This is triggered from the 204 message in IntelliCenter. We will + // be checking to ensure it does not load the server so we only do this every 10 seconds. + if (this._lastScheduleCheck > new Date().getTime() - 10000) return; + try { + // The call below also calculates the schedule window either the current or next. + ncp.schedules.triggerSchedules(); // At this point we are not adding Nixie schedules to IntelliCenter but this will trigger + // the proper time windows if they exist. + // Check each running circuit/feature to see when it will be going off. + let scheds = state.schedules.getActiveSchedules(); + let circs: { state: ICircuitState, endTime: number }[] = []; + for (let i = 0; i < scheds.length; i++) { + let ssched = scheds[i]; + if (!ssched.isOn || ssched.disabled || !ssched.isActive) continue; + let c = circs.find(x => x.state.id === ssched.circuit); + if (typeof c === 'undefined') { + let cstate = state.circuits.getInterfaceById(ssched.circuit); + c = { state: cstate, endTime: ssched.scheduleTime.endTime.getTime() }; + circs.push; + } + if (c.endTime < ssched.scheduleTime.endTime.getTime()) c.endTime = ssched.scheduleTime.endTime.getTime(); + } + for (let i = 0; i < circs.length; i++) { + let c = circs[i]; + if (c.state.endTime.getTime() !== c.endTime) { + c.state.endTime = new Timestamp(new Date(c.endTime)); + c.state.emitEquipmentChange(); + } + } + this._lastScheduleCheck = new Date().getTime(); + } catch (err) { logger.error(`Error synchronizing schedule states`); } + } public async deleteScheduleAsync(data: any): Promise { if (typeof data.id !== 'undefined') { let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); @@ -3484,7 +3545,7 @@ class IntelliCenterHeaterCommands extends HeaterCommands { if (gasHeaterInstalled) sys.board.valueMaps.heatSources.merge([[2, { name: 'heater', desc: 'Heater' }]]); if (mastertempInstalled) sys.board.valueMaps.heatSources.merge([[11, { name: 'mtheater', desc: 'MasterTemp' }]]); if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar Only', hasCoolSetpoint: htypes.hasCoolSetpoint }], [4, { name: 'solarpref', desc: 'Solar Preferred', hasCoolSetpoint: htypes.hasCoolSetpoint }]]); - else if (solarInstalled) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar', hasCoolsetpoint: htypes.hasCoolSetpoint }]]); + else if (solarInstalled) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar', hasCoolSetpoint: htypes.hasCoolSetpoint }]]); if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Pref' }]]); else if (heatPumpInstalled) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heat Pump' }]]); if (ultratempInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[5, { name: 'ultratemp', desc: 'UltraTemp Only', hasCoolSetpoint: htypes.hasCoolSetpoint }], [6, { name: 'ultratemppref', desc: 'UltraTemp Pref', hasCoolSetpoint: htypes.hasCoolSetpoint }]]); diff --git a/controller/boards/NixieBoard.ts b/controller/boards/NixieBoard.ts index 31a4d6e5..e6a45eb8 100644 --- a/controller/boards/NixieBoard.ts +++ b/controller/boards/NixieBoard.ts @@ -18,7 +18,7 @@ along with this program. If not, see . import * as extend from 'extend'; import { ncp } from "../nixie/Nixie"; import { NixieHeaterBase } from "../nixie/heaters/Heater"; -import { utils } from '../Constants'; +import { Timestamp, utils } from '../Constants'; import {SystemBoard, byteValueMap, BodyCommands, FilterCommands, PumpCommands, SystemCommands, CircuitCommands, FeatureCommands, ValveCommands, HeaterCommands, ChlorinatorCommands, ChemControllerCommands, EquipmentIdRange} from './SystemBoard'; import { logger } from '../../logger/Logger'; import { state, CircuitState, ICircuitState, ICircuitGroupState, LightGroupState, ValveState, FilterState, BodyTempState, FeatureState } from '../State'; @@ -221,6 +221,21 @@ export class NixieBoard extends SystemBoard { [53, { name: 'greenblue', desc: 'Green-Blue', types: ['pooltone'], sequence: 14 }], [54, { name: 'redgreen', desc: 'Red-Green', types: ['pooltone'], sequence: 15 }], [55, { name: 'bluered', desc: 'Blue-red', types: ['pooltone'], sequence: 16 }], + // Jandy Pro Series WaterColors Themes + [56, { name: 'alpinewhite', desc: 'Alpine White', types: ['watercolors'], sequence: 1 }], + [57, { name: 'skyblue', desc: 'Sky Blue', types: ['watercolors'], sequence: 2 }], + [58, { name: 'cobaltblue', desc: 'Cobalt Blue', types: ['watercolors'], sequence: 3 }], + [59, { name: 'caribbeanblue', desc: 'Caribbean Blue', types: ['watercolors'], sequence: 4 }], + [60, { name: 'springgreen', desc: 'Spring Green', types: ['watercolors'], sequence: 5 }], + [61, { name: 'emeraldgreen', desc: 'Emerald Green', types: ['watercolors'], sequence: 6 }], + [62, { name: 'emeraldrose', desc: 'Emerald Rose', types: ['watercolors'], sequence: 7 }], + [63, { name: 'magenta', desc: 'Magenta', types: ['watercolors'], sequence: 8 }], + [64, { name: 'violet', desc: 'Violet', types: ['watercolors'], sequence: 9 }], + [65, { name: 'slowcolorsplash', desc: 'Slow Color Splash', types: ['watercolors'], sequence: 10 }], + [66, { name: 'fastcolorsplash', desc: 'Fast Color Splash', types: ['watercolors'], sequence: 11 }], + [67, { name: 'americathebeautiful', desc: 'America the Beautiful', types: ['watercolors'], sequence: 12 }], + [68, { name: 'fattuesday', desc: 'Fat Tuesday', types: ['watercolors'], sequence: 13 }], + [69, { name: 'discotech', desc: 'Disco Tech', types: ['watercolors'], sequence: 14 }], [255, { name: 'none', desc: 'None' }] ]); this.valueMaps.lightColors = new byteValueMap([ @@ -416,7 +431,7 @@ export class NixieBoard extends SystemBoard { sys.circuits.removeItemById(6); state.circuits.removeItemById(6); } - + sys.equipment.setEquipmentIds(); sys.board.bodies.initFilters(); state.status = sys.board.valueMaps.controllerStatus.transform(2, 0); @@ -556,11 +571,11 @@ export class NixieSystemCommands extends SystemCommands { state.delay = sys.board.valueMaps.delay.getValue('nodelay'); return Promise.resolve(state.data.delay); } - public setManualOperationPriority(id: number): Promise { + public setManualOperationPriority(id: number): Promise { let cstate = state.circuits.getInterfaceById(id); delayMgr.setManualPriorityDelay(cstate); - return Promise.resolve(cstate); - } + return Promise.resolve(cstate); + } public setDateTimeAsync(obj: any): Promise { return Promise.resolve(); } public getDOW() { return this.board.valueMaps.scheduleDays.toArray(); } public async setGeneralAsync(obj: any): Promise { @@ -696,7 +711,6 @@ export class NixieCircuitCommands extends CircuitCommands { break; default: await ncp.circuits.setCircuitStateAsync(circ, newState); - await sys.board.processStatusAsync(); break; } // Let the main nixie controller set the circuit state and affect the relays if it needs to. @@ -707,6 +721,7 @@ export class NixieCircuitCommands extends CircuitCommands { state.emitEquipmentChanges(); ncp.pumps.syncPumpStates(); sys.board.suspendStatus(false); + await sys.board.processStatusAsync(); } } protected async setCleanerCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { @@ -769,8 +784,9 @@ export class NixieCircuitCommands extends CircuitCommands { } if (sys.general.options.cleanerStartDelay && sys.general.options.cleanerStartDelayTime) { let bcstate = state.circuits.getItemById(bstate.circuit); + let stime = typeof bcstate.startTime === 'undefined' ? dtNow : (dtNow - bcstate.startTime.getTime()); // So we should be started. Lets determine whethere there should be any delay. - delayTime = Math.max(Math.round(((sys.general.options.cleanerStartDelayTime * 1000) - (dtNow - bcstate.startTime.getTime())) / 1000), delayTime); + delayTime = Math.max(Math.round(((sys.general.options.cleanerStartDelayTime * 1000) - stime) / 1000), delayTime); logger.info(`Cleaner delay time calculated to ${delayTime}`); } } @@ -891,6 +907,7 @@ export class NixieCircuitCommands extends CircuitCommands { // circuit is already under delay it should have been processed out earlier. delayMgr.cancelPumpValveDelays(); delayMgr.cancelHeaterStartupDelays(); + sys.board.heaters.clearPrevHeaterOffTemp(); if (cstate.startDelay) delayMgr.clearBodyStartupDelay(bstate); await this.turnOffCleanerCircuits(bstate); if (sys.equipment.shared && bstate.id === 2) await this.turnOffDrainCircuits(ignoreDelays); @@ -994,7 +1011,7 @@ export class NixieCircuitCommands extends CircuitCommands { return cstate; } catch (err) { logger.error(`Nixie: Error setDrainCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setDrainCircuitStateAsync ${err.message}`, 'setDrainCircuitStateAsync')); } } - + public toggleCircuitStateAsync(id: number): Promise { let circ = state.circuits.getInterfaceById(id); return this.setCircuitStateAsync(id, !(circ.isOn || false)); @@ -1139,7 +1156,7 @@ export class NixieCircuitCommands extends CircuitCommands { if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440); if (typeof obj.showInFeatures !== 'undefined') sgroup.showInFeatures = group.showInFeatures = utils.makeBool(obj.showInFeatures); sgroup.type = group.type; - + group.dontStop = group.eggTimer === 1440; group.isActive = sgroup.isActive = true; @@ -1206,7 +1223,7 @@ export class NixieCircuitCommands extends CircuitCommands { // group.circuits.length = obj.circuits.length; // RSG - removed as this will delete circuits that were not changed group.circuits.length = obj.circuits.length; sgroup.emitEquipmentChange(); - + } resolve(group); }); @@ -1293,7 +1310,7 @@ export class NixieCircuitCommands extends CircuitCommands { //grp.lightingTheme = sgrp.lightingTheme = theme; let thm = sys.board.valueMaps.lightThemes.transform(theme); sgrp.action = sys.board.valueMaps.circuitActions.getValue('lighttheme'); - + try { // Go through and set the theme for all lights in the group. for (let i = 0; i < grp.circuits.length; i++) { @@ -1407,7 +1424,7 @@ export class NixieCircuitCommands extends CircuitCommands { else if (circuit.desiredState === 5){ // off/ignore if (val) cval = false; - else continue; + else continue; } await sys.board.circuits.setCircuitStateAsync(circuit.circuit, cval); //arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, cval)); @@ -1429,13 +1446,23 @@ export class NixieCircuitCommands extends CircuitCommands { let arr = []; for (let i = 0; i < circuits.length; i++) { let circuit = circuits[i]; - arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, val)); + // RSG 4/3/24 - This function was executing and returing the results to the array; not pushing the fn to the array. + //arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, val)); + arr.push(async () => { await sys.board.circuits.setCircuitStateAsync(circuit.circuit, val) }); } + // return new Promise(async (resolve, reject) => { + // await Promise.all(arr).catch((err) => { reject(err) }); + // resolve(gstate); + // }); return new Promise(async (resolve, reject) => { - await Promise.all(arr).catch((err) => { reject(err) }); - resolve(gstate); + try { + Promise.all(arr.map(async func => await func())); + resolve(gstate); + } catch (err) { + reject(err); + }; }); - } + }; } export class NixieFeatureCommands extends FeatureCommands { public async setFeatureAsync(obj: any): Promise { @@ -1511,7 +1538,7 @@ export class NixieFeatureCommands extends FeatureCommands { if (!val){ if (fstate.manualPriorityActive) delayMgr.cancelManualPriorityDelay(fstate.id); fstate.manualPriorityActive = false; // if the delay was previously cancelled, still need to turn this off - } + } state.emitEquipmentChanges(); return fstate; } catch (err) { return Promise.reject(new Error(`Error setting feature state ${err.message}`)); } @@ -1599,7 +1626,7 @@ export class NixieFeatureCommands extends FeatureCommands { let circuit: CircuitGroupCircuit = grp.circuits.getItemByIndex(j); let cstate = state.circuits.getInterfaceById(circuit.circuit); // RSG: desiredState for Nixie is 1=on, 2=off, 3=ignore - if (circuit.desiredState === 1 || circuit.desiredState === 4) { + if (circuit.desiredState === 1 || circuit.desiredState === 4) { // The circuit should be on if the value is 1. // If we are on 'ignore' we should still only treat the circuit as // desiredstate = 1. @@ -1610,12 +1637,15 @@ export class NixieFeatureCommands extends FeatureCommands { } } let sgrp = state.circuitGroups.getItemById(grp.id); + if (bIsOn && typeof sgrp.endTime === 'undefined') { + sys.board.circuits.setEndTime(grp, sgrp, bIsOn, true); + } sgrp.isOn = bIsOn; - if (sgrp.isOn && typeof sgrp.endTime === 'undefined') sys.board.circuits.setEndTime(grp, sgrp, sgrp.isOn, true); + if (!sgrp.isOn && sgrp.manualPriorityActive){ delayMgr.cancelManualPriorityDelays(); sgrp.manualPriorityActive = false; // if the delay was previously cancelled, still need to turn this off - } + } } sys.board.valves.syncValveStates(); } @@ -1637,9 +1667,9 @@ export class NixieFeatureCommands extends FeatureCommands { if (!sgrp.isOn && sgrp.manualPriorityActive){ delayMgr.cancelManualPriorityDelay(grp.id); sgrp.manualPriorityActive = false; // if the delay was previously cancelled, still need to turn this off - } + } } - + sys.board.valves.syncValveStates(); } state.emitEquipmentChanges(); @@ -1671,8 +1701,8 @@ export class NixiePumpCommands extends PumpCommands { // to bodies. console.log(`Body: ${pump.body} Pump: ${pump.name} Pool: ${circuitIds.includes(6)} `); if ((pump.body === 255 && (circuitIds.includes(6) || circuitIds.includes(1))) || - (pump.body === 0 && circuitIds.includes(6)) || - (pump.body === 101 && circuitIds.includes(1))) { + (pump.body === 0 && circuitIds.includes(6)) || + (pump.body === 101 && circuitIds.includes(1))) { delayMgr.setPumpValveDelay(pstate); } break; @@ -1844,14 +1874,14 @@ export class NixieHeaterCommands extends HeaterCommands { if (gasHeaterInstalled) sys.board.valueMaps.heatSources.merge([[2, { name: 'heater', desc: 'Heater' }]]); if (mastertempInstalled) sys.board.valueMaps.heatSources.merge([[11, { name: 'mtheater', desc: 'MasterTemp' }]]); if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar Only', hasCoolSetpoint: htypes.hasCoolSetpoint }], [4, { name: 'solarpref', desc: 'Solar Preferred', hasCoolSetpoint: htypes.hasCoolSetpoint }]]); - else if (solarInstalled) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar', hasCoolsetpoint: htypes.hasCoolSetpoint }]]); + else if (solarInstalled) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar', hasCoolSetpoint: htypes.hasCoolSetpoint }]]); if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Pref' }]]); else if (heatPumpInstalled) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heat Pump' }]]); if (ultratempInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[5, { name: 'ultratemp', desc: 'UltraTemp Only', hasCoolSetpoint: htypes.hasCoolSetpoint }], [6, { name: 'ultratemppref', desc: 'UltraTemp Pref', hasCoolSetpoint: htypes.hasCoolSetpoint }]]); else if (ultratempInstalled) sys.board.valueMaps.heatSources.merge([[5, { name: 'ultratemp', desc: 'UltraTemp', hasCoolSetpoint: htypes.hasCoolSetpoint }]]); sys.board.valueMaps.heatSources.merge([[0, { name: 'nochange', desc: 'No Change' }]]); - + if (gasHeaterInstalled) sys.board.valueMaps.heatModes.merge([[2, { name: 'heater', desc: 'Heater' }]]); if (mastertempInstalled) sys.board.valueMaps.heatModes.merge([[11, { name: 'mtheater', desc: 'MasterTemp' }]]); if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar Only' }], [4, { name: 'solarpref', desc: 'Solar Preferred' }]]); diff --git a/controller/boards/SunTouchBoard.ts b/controller/boards/SunTouchBoard.ts index 334e3a45..b9a2bf3e 100644 --- a/controller/boards/SunTouchBoard.ts +++ b/controller/boards/SunTouchBoard.ts @@ -222,8 +222,10 @@ class SunTouchConfigQueue extends TouchConfigQueue { // 196 - [0-2] // 198 - [0-2] // 199 - [0-2] + // 200 - Heat/Temperature Status // 201 - [0-2] // 202 - [0-2] - Custom Names + // 203 - Circuit Functions // 204 - [0-2] // 205 - [0-2] // 206 - [0-2] @@ -238,13 +240,16 @@ class SunTouchConfigQueue extends TouchConfigQueue { // 218 - [0-2] // 219 - [0-2] // 220 - [0-2] + // 221 - Valve Assignments // 223 - [0-2] // 224 - [1-2] - // 226 - [0] + // 225 - Spa side remote + // 226 - [0] - Solar/HeatPump config // 228 - [0-2] // 229 - [0-2] // 230 - [0-2] // 231 - [0-2] + // 232 - Settings (Amazed that there is none of this) // 233 - [0-2] // 234 - [0-2] // 235 - [0-2] @@ -264,6 +269,7 @@ class SunTouchConfigQueue extends TouchConfigQueue { // 249 - [0-2] // 250 - [0-2] // 251 - [0-2] + // 253 - Software Version this.queueItems(GetTouchConfigCategories.version); // 252 this.queueItems(GetTouchConfigCategories.dateTime, [0]); //197 diff --git a/controller/boards/SystemBoard.ts b/controller/boards/SystemBoard.ts index 5313136e..34a022e2 100644 --- a/controller/boards/SystemBoard.ts +++ b/controller/boards/SystemBoard.ts @@ -22,455 +22,455 @@ import { Timestamp, utils } from '../Constants'; import { Body, ChemController, ChemDoser, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, ControllerType, CustomName, CustomNameCollection, EggTimer, Equipment, Feature, Filter, General, Heater, ICircuit, ICircuitGroup, ICircuitGroupCircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, TempSensorCollection, Valve } from '../Equipment'; import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, BoardProcessError, InvalidOperationError } from '../Errors'; import { ncp } from "../nixie/Nixie"; -import { BodyTempState, ChemControllerState, ChemDoserState, ChlorinatorState, CircuitGroupState, FilterState, ICircuitGroupState, ICircuitState, LightGroupState, ScheduleState, state, TemperatureState, ValveState, VirtualCircuitState } from '../State'; +import { HeaterState, BodyTempState, ChemControllerState, ChemDoserState, ChlorinatorState, CircuitGroupState, FilterState, ICircuitGroupState, ICircuitState, LightGroupState, ScheduleState, state, TemperatureState, ValveState, VirtualCircuitState } from '../State'; import { RestoreResults } from '../../web/Server'; import { setTimeout } from 'timers/promises'; import { setTimeout as setTimeoutSync } from 'timers'; export class byteValueMap extends Map { - public transform(byte: number, ext?: number) { return extend(true, { val: byte || 0 }, this.get(byte) || this.get(0)); } - public toArray(): any[] { - let arrKeys = Array.from(this.keys()); - let arr = []; - for (let i = 0; i < arrKeys.length; i++) arr.push(this.transform(arrKeys[i])); - return arr; - } - public transformByName(name: string) { - let arr = this.toArray(); - for (let i = 0; i < arr.length; i++) { - if (typeof (arr[i].name) !== 'undefined' && arr[i].name === name) return arr[i]; + public transform(byte: number, ext?: number) { return extend(true, { val: byte || 0 }, this.get(byte) || this.get(0)); } + public toArray(): any[] { + let arrKeys = Array.from(this.keys()); + let arr = []; + for (let i = 0; i < arrKeys.length; i++) arr.push(this.transform(arrKeys[i])); + return arr; } - return { name: name }; - } - public getValue(name: string): number { return this.transformByName(name).val; } - public getName(val: number): string { return val >= 0 && typeof this.get(val) !== 'undefined' ? this.get(val).name : ''; } // added default return as this was erroring out by not finding a name - public merge(vals) { - for (let val of vals) { - this.set(val[0], val[1]); + public transformByName(name: string) { + let arr = this.toArray(); + for (let i = 0; i < arr.length; i++) { + if (typeof (arr[i].name) !== 'undefined' && arr[i].name === name) return arr[i]; + } + return { name: name }; } - } - public valExists(val: number) { - let arrKeys = Array.from(this.keys()); - return typeof arrKeys.find(elem => elem === val) !== 'undefined'; - } - public encode(val: string | number | { val: any, name: string }, def?: number) { - let v = this.findItem(val); - if (typeof v === 'undefined') logger.debug(`Invalid enumeration: val = ${val} map = ${JSON.stringify(this)}`); - return typeof v === 'undefined' ? def : v.val; - } - public findItem(val: string | number | { val: any, name: string }) { - if (val === null || typeof val === 'undefined') return; - else if (typeof val === 'number') return this.transform(val); - else if (typeof val === 'string') { - let v = parseInt(val, 10); - if (!isNaN(v)) return this.transform(v); - else return this.transformByName(val); - } - else if (typeof val === 'object') { - if (typeof val.val !== 'undefined') return this.transform(parseInt(val.val, 10)); - else if (typeof val.name !== 'undefined') return this.transformByName(val.name); + public getValue(name: string): number { return this.transformByName(name).val; } + public getName(val: number): string { return val >= 0 && typeof this.get(val) !== 'undefined' ? this.get(val).name : ''; } // added default return as this was erroring out by not finding a name + public merge(vals) { + for (let val of vals) { + this.set(val[0], val[1]); + } + } + public valExists(val: number) { + let arrKeys = Array.from(this.keys()); + return typeof arrKeys.find(elem => elem === val) !== 'undefined'; + } + public encode(val: string | number | { val: any, name: string }, def?: number) { + let v = this.findItem(val); + if (typeof v === 'undefined') logger.debug(`Invalid enumeration: val = ${val} map = ${JSON.stringify(this)}`); + return typeof v === 'undefined' ? def : v.val; + } + public findItem(val: string | number | { val: any, name: string }) { + if (val === null || typeof val === 'undefined') return; + else if (typeof val === 'number') return this.transform(val); + else if (typeof val === 'string') { + let v = parseInt(val, 10); + if (!isNaN(v)) return this.transform(v); + else return this.transformByName(val); + } + else if (typeof val === 'object') { + if (typeof val.val !== 'undefined') return this.transform(parseInt(val.val, 10)); + else if (typeof val.name !== 'undefined') return this.transformByName(val.name); + } } - } } export class EquipmentIdRange { - constructor(start: number | Function, end: number | Function) { - this._start = start; - this._end = end; - } - private _start: any = 0; - private _end: any = 0; - public get start(): number { return typeof this._start === 'function' ? this._start() : this._start; } - public set start(val: number) { this._start = val; } - public get end(): number { return typeof this._end === 'function' ? this._end() : this._end; } - public set end(val: number) { this._end = val; } - public isInRange(id: number) { return id >= this.start && id <= this.end; } + constructor(start: number | Function, end: number | Function) { + this._start = start; + this._end = end; + } + private _start: any = 0; + private _end: any = 0; + public get start(): number { return typeof this._start === 'function' ? this._start() : this._start; } + public set start(val: number) { this._start = val; } + public get end(): number { return typeof this._end === 'function' ? this._end() : this._end; } + public set end(val: number) { this._end = val; } + public isInRange(id: number) { return id >= this.start && id <= this.end; } } export class InvalidEquipmentIdArray { - constructor(data: number[]) { this._data = data; } - private _data: number[]; + constructor(data: number[]) { this._data = data; } + private _data: number[]; - public get() { return this._data; } - public set(val: number[]) { this._data = val; } - public add(val: number) { - if (!this._data.includes(val)) { - this._data.push(val); - this._data.sort(((a, b) => a - b)); + public get() { return this._data; } + public set(val: number[]) { this._data = val; } + public add(val: number) { + if (!this._data.includes(val)) { + this._data.push(val); + this._data.sort(((a, b) => a - b)); + } } - } - public merge(arr: number[]) { - for (let i = 0; i < arr.length; i++) { - if (!this._data.includes(arr[i])) this._data.push(arr[i]); + public merge(arr: number[]) { + for (let i = 0; i < arr.length; i++) { + if (!this._data.includes(arr[i])) this._data.push(arr[i]); + } + this._data.sort((a, b) => a - b); + } + public remove(val: number) { + this._data = this._data.filter(el => el !== val); + } + public isValidId(val: number) { + return !this._data.includes(val); } - this._data.sort((a, b) => a - b); - } - public remove(val: number) { - this._data = this._data.filter(el => el !== val); - } - public isValidId(val: number) { - return !this._data.includes(val); - } } export class EquipmentIds { - public circuits: EquipmentIdRange = new EquipmentIdRange(1, 6); - public features: EquipmentIdRange = new EquipmentIdRange(7, function () { return this.start + sys.equipment.maxFeatures; }); - public pumps: EquipmentIdRange = new EquipmentIdRange(1, function () { return this.start + sys.equipment.maxPumps; }); - public circuitGroups: EquipmentIdRange = new EquipmentIdRange(50, function () { return this.start + sys.equipment.maxCircuitGroups; }); - public virtualCircuits: EquipmentIdRange = new EquipmentIdRange(128, 136); - public invalidIds: InvalidEquipmentIdArray = new InvalidEquipmentIdArray([]); + public circuits: EquipmentIdRange = new EquipmentIdRange(1, 6); + public features: EquipmentIdRange = new EquipmentIdRange(7, function () { return this.start + sys.equipment.maxFeatures; }); + public pumps: EquipmentIdRange = new EquipmentIdRange(1, function () { return this.start + sys.equipment.maxPumps; }); + public circuitGroups: EquipmentIdRange = new EquipmentIdRange(50, function () { return this.start + sys.equipment.maxCircuitGroups; }); + public virtualCircuits: EquipmentIdRange = new EquipmentIdRange(128, 136); + public invalidIds: InvalidEquipmentIdArray = new InvalidEquipmentIdArray([]); } export class byteValueMaps { - constructor() { - this.pumpStatus.transform = function (byte) { - // if (byte === 0) return this.get(0); - if (byte === 0) return extend(true, {}, this.get(0), { val: byte }); - for (let b = 16; b > 0; b--) { - let bit = (1 << (b - 1)); - if ((byte & bit) > 0) { - let v = this.get(b); - if (typeof v !== 'undefined') { - return extend(true, {}, v, { val: byte }); - } - } - } - return { val: byte, name: 'error' + byte, desc: 'Unspecified Error ' + byte }; - }; - this.chlorinatorStatus.transform = function (byte) { - if (byte === 128) return { val: 128, name: 'commlost', desc: 'Communication Lost' }; - else if (byte === 0) return { val: 0, name: 'ok', desc: 'Ok' }; - for (let b = 8; b > 0; b--) { - let bit = (1 << (b - 1)); - if ((byte & bit) > 0) { - let v = this.get(b); - if (typeof v !== "undefined") { - return extend(true, {}, v, { val: byte & 0x00FF }); - } - } - } - return { val: byte, name: 'unknown' + byte, desc: 'Unknown status ' + byte }; - }; - this.scheduleTypes.transform = function (byte) { - return (byte & 128) > 0 ? extend(true, { val: 128 }, this.get(128)) : extend(true, { val: 0 }, this.get(0)); - }; - this.scheduleDays.transform = function (byte) { - let days = []; - let b = byte & 0x007F; - for (let bit = 7; bit >= 0; bit--) { - if ((byte & (1 << (bit - 1))) > 0) days.push(extend(true, {}, this.get(bit))); - } - return { val: b, days: days }; - }; - this.scheduleDays.toArray = function () { - let arrKeys = Array.from(this.keys()); - let arr = []; - for (let i = 0; i < arrKeys.length; i++) arr.push(extend(true, { val: arrKeys[i] }, this.get(arrKeys[i]))); - return arr; - }; - this.virtualCircuits.transform = function (byte) { - return extend(true, {}, { val: byte, name: 'Unknown ' + byte }, this.get(byte), { val: byte }); - }; - this.tempUnits.transform = function (byte) { return extend(true, {}, { val: byte & 0x04 }, this.get(byte & 0x04)); }; - this.panelModes.transform = function (byte) { return extend(true, { val: byte & 0x83 }, this.get(byte & 0x83)); }; - this.controllerStatus.transform = function (byte: number, percent?: number) { - let v = extend(true, {}, this.get(byte) || this.get(0)); - if (typeof percent !== 'undefined') v.percent = percent; - return v; - }; - this.lightThemes.transform = function (byte) { return typeof byte === 'undefined' ? this.get(255) : extend(true, { val: byte }, this.get(byte) || this.get(255)); }; - this.timeZones.findItem = function (val: string | number | { val: any, name: string }) { - if (typeof val === null || typeof val === 'undefined') return; - else if (typeof val === 'number') { - if (val <= 12) { // We are looking for timezones based upon the utcOffset. - let arr = this.toArray(); - let tz = arr.find(elem => elem.utcOffset === val); - return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined; - } - return this.transform(val); - } - else if (typeof val === 'string') { - let v = parseInt(val, 10); - if (!isNaN(v)) { - if (v <= 12) { - let arr = this.toArray(); - let tz = arr.find(elem => elem.utcOffset === val); - return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined; - } - return this.transform(v); - } - else { - let arr = this.toArray(); - let tz = arr.find(elem => elem.abbrev === val || elem.name === val); - return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined; + constructor() { + this.pumpStatus.transform = function (byte) { + // if (byte === 0) return this.get(0); + if (byte === 0) return extend(true, {}, this.get(0), { val: byte }); + for (let b = 16; b > 0; b--) { + let bit = (1 << (b - 1)); + if ((byte & bit) > 0) { + let v = this.get(b); + if (typeof v !== 'undefined') { + return extend(true, {}, v, { val: byte }); + } + } + } + return { val: byte, name: 'error' + byte, desc: 'Unspecified Error ' + byte }; + }; + this.chlorinatorStatus.transform = function (byte) { + if (byte === 128) return { val: 128, name: 'commlost', desc: 'Communication Lost' }; + else if (byte === 0) return { val: 0, name: 'ok', desc: 'Ok' }; + for (let b = 8; b > 0; b--) { + let bit = (1 << (b - 1)); + if ((byte & bit) > 0) { + let v = this.get(b); + if (typeof v !== "undefined") { + return extend(true, {}, v, { val: byte & 0x00FF }); + } + } + } + return { val: byte, name: 'unknown' + byte, desc: 'Unknown status ' + byte }; + }; + this.scheduleTypes.transform = function (byte) { + return (byte & 128) > 0 ? extend(true, { val: 128 }, this.get(128)) : extend(true, { val: 0 }, this.get(0)); + }; + this.scheduleDays.transform = function (byte) { + let days = []; + let b = byte & 0x007F; + for (let bit = 7; bit >= 0; bit--) { + if ((byte & (1 << (bit - 1))) > 0) days.push(extend(true, {}, this.get(bit))); + } + return { val: b, days: days }; + }; + this.scheduleDays.toArray = function () { + let arrKeys = Array.from(this.keys()); + let arr = []; + for (let i = 0; i < arrKeys.length; i++) arr.push(extend(true, { val: arrKeys[i] }, this.get(arrKeys[i]))); + return arr; + }; + this.virtualCircuits.transform = function (byte) { + return extend(true, {}, { val: byte, name: 'Unknown ' + byte }, this.get(byte), { val: byte }); + }; + this.tempUnits.transform = function (byte) { return extend(true, {}, { val: byte & 0x04 }, this.get(byte & 0x04)); }; + this.panelModes.transform = function (byte) { return extend(true, { val: byte & 0x83 }, this.get(byte & 0x83)); }; + this.controllerStatus.transform = function (byte: number, percent?: number) { + let v = extend(true, {}, this.get(byte) || this.get(0)); + if (typeof percent !== 'undefined') v.percent = percent; + return v; + }; + this.lightThemes.transform = function (byte) { return typeof byte === 'undefined' ? this.get(255) : extend(true, { val: byte }, this.get(byte) || this.get(255)); }; + this.timeZones.findItem = function (val: string | number | { val: any, name: string }) { + if (typeof val === null || typeof val === 'undefined') return; + else if (typeof val === 'number') { + if (val <= 12) { // We are looking for timezones based upon the utcOffset. + let arr = this.toArray(); + let tz = arr.find(elem => elem.utcOffset === val); + return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined; + } + return this.transform(val); + } + else if (typeof val === 'string') { + let v = parseInt(val, 10); + if (!isNaN(v)) { + if (v <= 12) { + let arr = this.toArray(); + let tz = arr.find(elem => elem.utcOffset === val); + return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined; + } + return this.transform(v); + } + else { + let arr = this.toArray(); + let tz = arr.find(elem => elem.abbrev === val || elem.name === val); + return typeof tz !== 'undefined' ? this.transform(tz.val) : undefined; + } + } + else if (typeof val === 'object') { + if (typeof val.val !== 'undefined') return this.transform(parseInt(val.val, 10)); + else if (typeof val.name !== 'undefined') return this.transformByName(val.name); + } } - } - else if (typeof val === 'object') { - if (typeof val.val !== 'undefined') return this.transform(parseInt(val.val, 10)); - else if (typeof val.name !== 'undefined') return this.transformByName(val.name); - } } - } - public expansionBoards: byteValueMap = new byteValueMap(); - // Identifies which controller manages the underlying equipment. - public equipmentMaster: byteValueMap = new byteValueMap([ - [0, { val: 0, name: 'ocp', desc: 'Outdoor Control Panel' }], - [1, { val: 1, name: 'ncp', desc: 'Nixie Control Panel' }], - [2, { val: 2, name: 'ext', desc: 'External Control Panel' }] - ]); - public equipmentCommStatus: byteValueMap = new byteValueMap([ - [0, { val: 0, name: 'ready', desc: 'Ready' }], - [1, { val: 1, name: 'commerr', desc: 'Communication Error' }] - ]); - public panelModes: byteValueMap = new byteValueMap([ - [0, { val: 0, name: 'auto', desc: 'Auto' }], - // [1, { val: 1, name: 'service', desc: 'Service' }], - // [8, { val: 8, name: 'freeze', desc: 'Freeze' }], - // [128, { val: 128, name: 'timeout', desc: 'Timeout' }], - // [129, { val: 129, name: 'service-timeout', desc: 'Service/Timeout' }], - [255, { name: 'error', desc: 'System Error' }] - ]); - public controllerStatus: byteValueMap = new byteValueMap([ - [0, { val: 0, name: 'initializing', desc: 'Initializing', percent: 0 }], - [1, { val: 1, name: 'ready', desc: 'Ready', percent: 100 }], - [2, { val: 2, name: 'loading', desc: 'Loading', percent: 0 }], - [3, { val: 255, name: 'Error', desc: 'Error', percent: 0 }] - ]); + public expansionBoards: byteValueMap = new byteValueMap(); + // Identifies which controller manages the underlying equipment. + public equipmentMaster: byteValueMap = new byteValueMap([ + [0, { val: 0, name: 'ocp', desc: 'Outdoor Control Panel' }], + [1, { val: 1, name: 'ncp', desc: 'Nixie Control Panel' }], + [2, { val: 2, name: 'ext', desc: 'External Control Panel' }] + ]); + public equipmentCommStatus: byteValueMap = new byteValueMap([ + [0, { val: 0, name: 'ready', desc: 'Ready' }], + [1, { val: 1, name: 'commerr', desc: 'Communication Error' }] + ]); + public panelModes: byteValueMap = new byteValueMap([ + [0, { val: 0, name: 'auto', desc: 'Auto' }], + // [1, { val: 1, name: 'service', desc: 'Service' }], + // [8, { val: 8, name: 'freeze', desc: 'Freeze' }], + // [128, { val: 128, name: 'timeout', desc: 'Timeout' }], + // [129, { val: 129, name: 'service-timeout', desc: 'Service/Timeout' }], + [255, { name: 'error', desc: 'System Error' }] + ]); + public controllerStatus: byteValueMap = new byteValueMap([ + [0, { val: 0, name: 'initializing', desc: 'Initializing', percent: 0 }], + [1, { val: 1, name: 'ready', desc: 'Ready', percent: 100 }], + [2, { val: 2, name: 'loading', desc: 'Loading', percent: 0 }], + [3, { val: 255, name: 'Error', desc: 'Error', percent: 0 }] + ]); - public circuitFunctions: byteValueMap = new byteValueMap([ - [0, { name: 'generic', desc: 'Generic' }], - [1, { name: 'spa', desc: 'Spa', hasHeatSource: true, body: 2 }], - [2, { name: 'pool', desc: 'Pool', hasHeatSource: true, body: 1 }], - [5, { name: 'mastercleaner', desc: 'Master Cleaner', body: 1 }], - [7, { name: 'light', desc: 'Light', isLight: true }], - [9, { name: 'samlight', desc: 'SAM Light', isLight: true }], - [10, { name: 'sallight', desc: 'SAL Light', isLight: true }], - [11, { name: 'photongen', desc: 'Photon Gen', isLight: true }], - [12, { name: 'colorwheel', desc: 'Color Wheel', isLight: true }], - [13, { name: 'valve', desc: 'Valve' }], - [14, { name: 'spillway', desc: 'Spillway' }], - [15, { name: 'floorcleaner', desc: 'Floor Cleaner', body: 1 }], // This circuit function does not seem to exist in IntelliTouch. - [16, { name: 'intellibrite', desc: 'Intellibrite', isLight: true, theme: 'intellibrite' }], - [17, { name: 'magicstream', desc: 'Magicstream', isLight: true, theme: 'magicstream' }], - [19, { name: 'notused', desc: 'Not Used' }], - [65, { name: 'lotemp', desc: 'Lo-Temp' }], - [66, { name: 'hightemp', desc: 'Hi-Temp' }] - ]); + public circuitFunctions: byteValueMap = new byteValueMap([ + [0, { name: 'generic', desc: 'Generic' }], + [1, { name: 'spa', desc: 'Spa', hasHeatSource: true, body: 2 }], + [2, { name: 'pool', desc: 'Pool', hasHeatSource: true, body: 1 }], + [5, { name: 'mastercleaner', desc: 'Master Cleaner', body: 1 }], + [7, { name: 'light', desc: 'Light', isLight: true }], + [9, { name: 'samlight', desc: 'SAM Light', isLight: true }], + [10, { name: 'sallight', desc: 'SAL Light', isLight: true }], + [11, { name: 'photongen', desc: 'Photon Gen', isLight: true }], + [12, { name: 'colorwheel', desc: 'Color Wheel', isLight: true }], + [13, { name: 'valve', desc: 'Valve' }], + [14, { name: 'spillway', desc: 'Spillway' }], + [15, { name: 'floorcleaner', desc: 'Floor Cleaner', body: 1 }], // This circuit function does not seem to exist in IntelliTouch. + [16, { name: 'intellibrite', desc: 'Intellibrite', isLight: true, theme: 'intellibrite' }], + [17, { name: 'magicstream', desc: 'Magicstream', isLight: true, theme: 'magicstream' }], + [19, { name: 'notused', desc: 'Not Used' }], + [65, { name: 'lotemp', desc: 'Lo-Temp' }], + [66, { name: 'hightemp', desc: 'Hi-Temp' }] + ]); - // Feature functions are used as the available options to define a circuit. - public featureFunctions: byteValueMap = new byteValueMap([[0, { name: 'generic', desc: 'Generic' }], [1, { name: 'spillway', desc: 'Spillway' }]]); - public virtualCircuits: byteValueMap = new byteValueMap([ - [128, { name: 'solar', desc: 'Solar', assignableToPumpCircuit: true }], - [129, { name: 'heater', desc: 'Either Heater', assignableToPumpCircuit: true }], - [130, { name: 'poolHeater', desc: 'Pool Heater', assignableToPumpCircuit: true }], - [131, { name: 'spaHeater', desc: 'Spa Heater', assignableToPumpCircuit: true }], - [132, { name: 'freeze', desc: 'Freeze', assignableToPumpCircuit: true }], - [133, { name: 'heatBoost', desc: 'Heat Boost', assignableToPumpCircuit: false }], - [134, { name: 'heatEnable', desc: 'Heat Enable', assignableToPumpCircuit: false }], - [135, { name: 'pumpSpeedUp', desc: 'Pump Speed +', assignableToPumpCircuit: false }], - [136, { name: 'pumpSpeedDown', desc: 'Pump Speed -', assignableToPumpCircuit: false }], - [255, { name: 'notused', desc: 'NOT USED', assignableToPumpCircuit: true }], - [258, { name: 'anyHeater', desc: 'Any Heater' }], - ]); - public lightThemes: byteValueMap = new byteValueMap([ - [0, { name: 'off', desc: 'Off' }], - [1, { name: 'on', desc: 'On' }], - [128, { name: 'colorsync', desc: 'Color Sync' }], - [144, { name: 'colorswim', desc: 'Color Swim' }], - [160, { name: 'colorset', desc: 'Color Set' }], - [177, { name: 'party', desc: 'Party', types: ['intellibrite'], sequence: 2 }], - [178, { name: 'romance', desc: 'Romance', types: ['intellibrite'], sequence: 3 }], - [179, { name: 'caribbean', desc: 'Caribbean', types: ['intellibrite'], sequence: 4 }], - [180, { name: 'american', desc: 'American', types: ['intellibrite'], sequence: 5 }], - [181, { name: 'sunset', desc: 'Sunset', types: ['intellibrite'], sequence: 6 }], - [182, { name: 'royal', desc: 'Royal', types: ['intellibrite'], sequence: 7 }], - [190, { name: 'save', desc: 'Save', types: ['intellibrite'], sequence: 13 }], - [191, { name: 'recall', desc: 'Recall', types: ['intellibrite'], sequence: 14 }], - [193, { name: 'blue', desc: 'Blue', types: ['intellibrite'], sequence: 8 }], - [194, { name: 'green', desc: 'Green', types: ['intellibrite'], sequence: 9 }], - [195, { name: 'red', desc: 'Red', types: ['intellibrite'], sequence: 10 }], - [196, { name: 'white', desc: 'White', types: ['intellibrite'], sequence: 11 }], - [197, { name: 'magenta', desc: 'Magenta', types: ['intellibrite'], sequence: 12 }], - [208, { name: 'thumper', desc: 'Thumper', types: ['magicstream'] }], - [209, { name: 'hold', desc: 'Hold', types: ['magicstream'] }], - [210, { name: 'reset', desc: 'Reset', types: ['magicstream'] }], - [211, { name: 'mode', desc: 'Mode', types: ['magicstream'] }], - [254, { name: 'unknown', desc: 'unknown' }], - [255, { name: 'none', desc: 'None' }] - ]); - public colorLogicThemes = new byteValueMap([ - [0, { name: 'cloudwhite', desc: 'Cloud White', types: ['colorlogic'], sequence: 7 }], - [1, { name: 'deepsea', desc: 'Deep Sea', types: ['colorlogic'], sequence: 2 }], - [2, { name: 'royalblue', desc: 'Royal Blue', types: ['colorlogic'], sequence: 3 }], - [3, { name: 'afernoonskies', desc: 'Afternoon Skies', types: ['colorlogic'], sequence: 4 }], - [4, { name: 'aquagreen', desc: 'Aqua Green', types: ['colorlogic'], sequence: 5 }], - [5, { name: 'emerald', desc: 'Emerald', types: ['colorlogic'], sequence: 6 }], - [6, { name: 'warmred', desc: 'Warm Red', types: ['colorlogic'], sequence: 8 }], - [7, { name: 'flamingo', desc: 'Flamingo', types: ['colorlogic'], sequence: 9 }], - [8, { name: 'vividviolet', desc: 'Vivid Violet', types: ['colorlogic'], sequence: 10 }], - [9, { name: 'sangria', desc: 'Sangria', types: ['colorlogic'], sequence: 11 }], - [10, { name: 'twilight', desc: 'Twilight', types: ['colorlogic'], sequence: 12 }], - [11, { name: 'tranquility', desc: 'Tranquility', types: ['colorlogic'], sequence: 13 }], - [12, { name: 'gemstone', desc: 'Gemstone', types: ['colorlogic'], sequence: 14 }], - [13, { name: 'usa', desc: 'USA', types: ['colorlogic'], sequence: 15 }], - [14, { name: 'mardigras', desc: 'Mardi Gras', types: ['colorlogic'], sequence: 16 }], - [15, { name: 'cabaret', desc: 'Cabaret', types: ['colorlogic'], sequence: 17 }], - [255, { name: 'none', desc: 'None' }] - ]); - public lightCommands = new byteValueMap([ - [4, { name: 'colorhold', desc: 'Hold', types: ['intellibrite', 'magicstream'], command: 'colorHold', sequence: 13 }], - [5, { name: 'colorrecall', desc: 'Recall', types: ['intellibrite', 'magicstream'], command: 'colorRecall', sequence: 14 }], - [6, { - name: 'lightthumper', desc: 'Thumper', types: ['magicstream'], command: 'lightThumper', message: 'Toggling Thumper', - sequence: [ // Cycle party mode 3 times. - { isOn: false, timeout: 100 }, - { isOn: true, timeout: 100 }, - { isOn: false, timeout: 100 }, - { isOn: true, timeout: 5000 }, - { isOn: false, timeout: 100 }, - { isOn: true, timeout: 100 }, - { isOn: false, timeout: 100 }, - { isOn: true, timeout: 5000 }, - { isOn: false, timeout: 100 }, - { isOn: true, timeout: 100 }, - { isOn: false, timeout: 100 } - ] - }], - [7, { - name: 'colorsync', desc: 'Sync', types: ['colorlogic'], command: 'colorSync', message: 'Synchronizing Lights', endingTheme: 'voodoolounge', - sequence: [ - { isOn: true, timeout: 1000 }, - { isOn: false, timeout: 12000 }, - { isOn: true } - ] - }], - [100, { name: 'settheme', types: ['all'], desc: 'Set Theme', message: 'Sequencing Theme' }] - ]); - public lightGroupCommands = new byteValueMap([ - [1, { name: 'colorsync', desc: 'Sync', types: ['intellibrite'], command: 'colorSync', message:'Synchronizing' }], - [2, { name: 'colorset', desc: 'Set', types: ['intellibrite'], command: 'colorSet', message: 'Sequencing Set Operation' }], - [3, { name: 'colorswim', desc: 'Swim', types: ['intellibrite'], command: 'colorSwim', message:'Sequencing Swim Operation' }], - [4, { name: 'colorhold', desc: 'Hold', types: ['intellibrite', 'magicstream'], command: 'colorHold', message: 'Saving Current Colors', sequence: 13 }], - [5, { name: 'colorrecall', desc: 'Recall', types: ['intellibrite', 'magicstream'], command: 'colorRecall', message: 'Recalling Saved Colors', sequence: 14 }], - [6, { - name: 'lightthumper', desc: 'Thumper', types: ['magicstream'], command: 'lightThumper', message: 'Toggling Thumper', - sequence: [ // Cycle party mode 3 times. - { isOn: false, timeout: 100 }, - { isOn: true, timeout: 100 }, - { isOn: false, timeout: 100 }, - { isOn: true, timeout: 5000 }, - { isOn: false, timeout: 100 }, - { isOn: true, timeout: 100 }, - { isOn: false, timeout: 100 }, - { isOn: true, timeout: 5000 }, - { isOn: false, timeout: 100 }, - { isOn: true, timeout: 100 }, - { isOn: false, timeout: 100 }, - { isOn: true, timeout: 1000 }, - ] - }] - ]); - public circuitActions: byteValueMap = new byteValueMap([ - [0, { name: 'ready', desc: 'Ready' }], - [1, { name: 'colorsync', desc: 'Synchronizing' }], - [2, { name: 'colorset', desc: 'Sequencing Set Operation' }], - [3, { name: 'colorswim', desc: 'Sequencing Swim Operation' }], - [4, { name: 'lighttheme', desc: 'Sequencing Theme/Color Operation' }], - [5, { name: 'colorhold', desc: 'Saving Current Color' }], - [6, { name: 'colorrecall', desc: 'Recalling Saved Color' }], - [7, { name: 'lightthumper', desc: 'Setting Light Thumper' }], - [100, { name: 'settheme', desc: 'Setting Light Theme' }] - ]); - public lightColors: byteValueMap = new byteValueMap([ - [0, { name: 'white', desc: 'White' }], - [2, { name: 'lightgreen', desc: 'Light Green' }], - [4, { name: 'green', desc: 'Green' }], - [6, { name: 'cyan', desc: 'Cyan' }], - [8, { name: 'blue', desc: 'Blue' }], - [10, { name: 'lavender', desc: 'Lavender' }], - [12, { name: 'magenta', desc: 'Magenta' }], - [14, { name: 'lightmagenta', desc: 'Light Magenta' }] - ]); - public scheduleDays: byteValueMap = new byteValueMap([ - [1, { name: 'sat', desc: 'Saturday', dow: 6 }], - [2, { name: 'fri', desc: 'Friday', dow: 5 }], - [3, { name: 'thu', desc: 'Thursday', dow: 4 }], - [4, { name: 'wed', desc: 'Wednesday', dow: 3 }], - [5, { name: 'tue', desc: 'Tuesday', dow: 2 }], - [6, { name: 'mon', desc: 'Monday', dow: 1 }], - [7, { name: 'sun', desc: 'Sunday', dow: 0 }] - ]); - public scheduleTimeTypes: byteValueMap = new byteValueMap([ - [0, { name: 'manual', desc: 'Manual' }] - ]); - public scheduleDisplayTypes: byteValueMap = new byteValueMap([ - [0, { name: 'always', desc: 'Always' }], - [1, { name: 'active', desc: 'When Active' }], - [2, { name: 'never', desc: 'Never' }] - ]); - public pumpTypes: byteValueMap = new byteValueMap([ - [1, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }], - [64, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }], - [65, { name: 'ds', desc: 'Two-Speed', maxCircuits: 40, hasAddress: false, hasBody: true }], - [128, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, maxCircuits: 8, hasAddress: true }], - [169, { name: 'vssvrs', desc: 'IntelliFlo VS+SVRS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, maxCircuits: 8, hasAddress: true }] - ]); - public pumpSSModels: byteValueMap = new byteValueMap([ - [0, { name: 'unspecified', desc: 'Unspecified', amps: 0, pf: 0, volts: 0, watts: 0 }], - [1, { name: 'wf1hpE', desc: '1hp WhisperFlo E+', amps: 7.4, pf: .9, volts: 230, watts: 1532 }], - [2, { name: 'wf1hpMax', desc: '1hp WhisperFlo Max', amps: 9, pf: .87, volts: 230, watts: 1600 }], - [3, { name: 'generic15hp', desc: '1.5hp Pump', amps: 9.3, pf: .9, volts: 230, watts: 1925 }], - [4, { name: 'generic2hp', desc: '2hp Pump', amps: 12, pf: .9, volts: 230, watts: 2484 }], - [5, { name: 'generic25hp', desc: '2.5hp Pump', amps: 12.5, pf: .9, volts: 230, watts: 2587 }], - [6, { name: 'generic3hp', desc: '3hp Pump', amps: 13.5, pf: .9, volts: 230, watts: 2794 }] - ]); - public pumpDSModels: byteValueMap = new byteValueMap([ - [0, { name: 'unspecified', desc: 'Unspecified', loAmps: 0, hiAmps: 0, pf: 0, volts: 0, loWatts: 0, hiWatts: 0 }], - [1, { name: 'generic1hp', desc: '1hp Pump', loAmps: 2.4, hiAmps: 6.5, pf: .9, volts: 230, loWatts: 497, hiWatts: 1345 }], - [2, { name: 'generic15hp', desc: '1.5hp Pump', loAmps: 2.7, hiAmps: 9.3, pf: .9, volts: 230, loWatts: 558, hiWatts: 1925 }], - [3, { name: 'generic2hp', desc: '2hp Pump', loAmps: 2.9, hiAmps: 12, pf: .9, volts: 230, loWatts: 600, hiWatts: 2484 }], - [4, { name: 'generic25hp', desc: '2.5hp Pump', loAmps: 3.1, hiAmps: 12.5, pf: .9, volts: 230, loWatts: 642, hiWatts: 2587 }], - [5, { name: 'generic3hp', desc: '3hp Pump', loAmps: 3.3, hiAmps: 13.5, pf: .9, volts: 230, loWatts: 683, hiWatts: 2794 }] - ]); - public pumpVSModels: byteValueMap = new byteValueMap([ - [0, { name: 'intelliflovs', desc: 'IntelliFlo VS' }] - ]); - public pumpVFModels: byteValueMap = new byteValueMap([ - [0, { name: 'intelliflovf', desc: 'IntelliFlo VF' }] - ]); - public pumpVSFModels: byteValueMap = new byteValueMap([ - [0, { name: 'intelliflovsf', desc: 'IntelliFlo VSF' }] - ]); - public pumpVSSVRSModels: byteValueMap = new byteValueMap([ - [0, { name: 'intelliflovssvrs', desc: 'IntelliFlo VS+SVRS' }] - ]); - // These are used for single-speed pump definitions. Essentially the way this works is that when - // the body circuit is running the single speed pump is on. - public pumpBodies: byteValueMap = new byteValueMap([ - [0, { name: 'pool', desc: 'Pool' }], - [101, { name: 'spa', desc: 'Spa' }], - [255, { name: 'poolspa', desc: 'Pool/Spa' }] - ]); - public heaterTypes: byteValueMap = new byteValueMap([ - [1, { name: 'gas', desc: 'Gas Heater', hasAddress: false }], - [2, { name: 'solar', desc: 'Solar Heater', hasAddress: false, hasCoolSetpoint: true, hasPreference: true }], - [3, { name: 'heatpump', desc: 'Heat Pump', hasAddress: true, hasPreference: true }], - [4, { name: 'ultratemp', desc: 'UltraTemp', hasAddress: true, hasCoolSetpoint: true, hasPreference: true }], - [5, { name: 'hybrid', desc: 'Hybrid', hasAddress: true }], - [6, { name: 'mastertemp', desc: 'MasterTemp', hasAddress: true }], - [7, { name: 'maxetherm', desc: 'Max-E-Therm', hasAddress: true }], - ]); - public heatModes: byteValueMap = new byteValueMap([ - [0, { name: 'off', desc: 'Off' }], - [3, { name: 'heater', desc: 'Heater' }], - [5, { name: 'solar', desc: 'Solar Only' }], - [12, { name: 'solarpref', desc: 'Solar Preferred' }] - ]); - public heatSources: byteValueMap = new byteValueMap([ - [0, { name: 'off', desc: 'No Heater' }], - [3, { name: 'heater', desc: 'Heater' }], - [5, { name: 'solar', desc: 'Solar Only' }], - [21, { name: 'solarpref', desc: 'Solar Preferred' }], - [32, { name: 'nochange', desc: 'No Change' }] - ]); + // Feature functions are used as the available options to define a circuit. + public featureFunctions: byteValueMap = new byteValueMap([[0, { name: 'generic', desc: 'Generic' }], [1, { name: 'spillway', desc: 'Spillway' }]]); + public virtualCircuits: byteValueMap = new byteValueMap([ + [128, { name: 'solar', desc: 'Solar', assignableToPumpCircuit: true }], + [129, { name: 'heater', desc: 'Either Heater', assignableToPumpCircuit: true }], + [130, { name: 'poolHeater', desc: 'Pool Heater', assignableToPumpCircuit: true }], + [131, { name: 'spaHeater', desc: 'Spa Heater', assignableToPumpCircuit: true }], + [132, { name: 'freeze', desc: 'Freeze', assignableToPumpCircuit: true }], + [133, { name: 'heatBoost', desc: 'Heat Boost', assignableToPumpCircuit: false }], + [134, { name: 'heatEnable', desc: 'Heat Enable', assignableToPumpCircuit: false }], + [135, { name: 'pumpSpeedUp', desc: 'Pump Speed +', assignableToPumpCircuit: false }], + [136, { name: 'pumpSpeedDown', desc: 'Pump Speed -', assignableToPumpCircuit: false }], + [255, { name: 'notused', desc: 'NOT USED', assignableToPumpCircuit: true }], + [258, { name: 'anyHeater', desc: 'Any Heater' }], + ]); + public lightThemes: byteValueMap = new byteValueMap([ + [0, { name: 'off', desc: 'Off' }], + [1, { name: 'on', desc: 'On' }], + [128, { name: 'colorsync', desc: 'Color Sync' }], + [144, { name: 'colorswim', desc: 'Color Swim' }], + [160, { name: 'colorset', desc: 'Color Set' }], + [177, { name: 'party', desc: 'Party', types: ['intellibrite'], sequence: 2 }], + [178, { name: 'romance', desc: 'Romance', types: ['intellibrite'], sequence: 3 }], + [179, { name: 'caribbean', desc: 'Caribbean', types: ['intellibrite'], sequence: 4 }], + [180, { name: 'american', desc: 'American', types: ['intellibrite'], sequence: 5 }], + [181, { name: 'sunset', desc: 'Sunset', types: ['intellibrite'], sequence: 6 }], + [182, { name: 'royal', desc: 'Royal', types: ['intellibrite'], sequence: 7 }], + [190, { name: 'save', desc: 'Save', types: ['intellibrite'], sequence: 13 }], + [191, { name: 'recall', desc: 'Recall', types: ['intellibrite'], sequence: 14 }], + [193, { name: 'blue', desc: 'Blue', types: ['intellibrite'], sequence: 8 }], + [194, { name: 'green', desc: 'Green', types: ['intellibrite'], sequence: 9 }], + [195, { name: 'red', desc: 'Red', types: ['intellibrite'], sequence: 10 }], + [196, { name: 'white', desc: 'White', types: ['intellibrite'], sequence: 11 }], + [197, { name: 'magenta', desc: 'Magenta', types: ['intellibrite'], sequence: 12 }], + [208, { name: 'thumper', desc: 'Thumper', types: ['magicstream'] }], + [209, { name: 'hold', desc: 'Hold', types: ['magicstream'] }], + [210, { name: 'reset', desc: 'Reset', types: ['magicstream'] }], + [211, { name: 'mode', desc: 'Mode', types: ['magicstream'] }], + [254, { name: 'unknown', desc: 'unknown' }], + [255, { name: 'none', desc: 'None' }] + ]); + public colorLogicThemes = new byteValueMap([ + [0, { name: 'cloudwhite', desc: 'Cloud White', types: ['colorlogic'], sequence: 7 }], + [1, { name: 'deepsea', desc: 'Deep Sea', types: ['colorlogic'], sequence: 2 }], + [2, { name: 'royalblue', desc: 'Royal Blue', types: ['colorlogic'], sequence: 3 }], + [3, { name: 'afernoonskies', desc: 'Afternoon Skies', types: ['colorlogic'], sequence: 4 }], + [4, { name: 'aquagreen', desc: 'Aqua Green', types: ['colorlogic'], sequence: 5 }], + [5, { name: 'emerald', desc: 'Emerald', types: ['colorlogic'], sequence: 6 }], + [6, { name: 'warmred', desc: 'Warm Red', types: ['colorlogic'], sequence: 8 }], + [7, { name: 'flamingo', desc: 'Flamingo', types: ['colorlogic'], sequence: 9 }], + [8, { name: 'vividviolet', desc: 'Vivid Violet', types: ['colorlogic'], sequence: 10 }], + [9, { name: 'sangria', desc: 'Sangria', types: ['colorlogic'], sequence: 11 }], + [10, { name: 'twilight', desc: 'Twilight', types: ['colorlogic'], sequence: 12 }], + [11, { name: 'tranquility', desc: 'Tranquility', types: ['colorlogic'], sequence: 13 }], + [12, { name: 'gemstone', desc: 'Gemstone', types: ['colorlogic'], sequence: 14 }], + [13, { name: 'usa', desc: 'USA', types: ['colorlogic'], sequence: 15 }], + [14, { name: 'mardigras', desc: 'Mardi Gras', types: ['colorlogic'], sequence: 16 }], + [15, { name: 'cabaret', desc: 'Cabaret', types: ['colorlogic'], sequence: 17 }], + [255, { name: 'none', desc: 'None' }] + ]); + public lightCommands = new byteValueMap([ + [4, { name: 'colorhold', desc: 'Hold', types: ['intellibrite', 'magicstream'], command: 'colorHold', sequence: 13 }], + [5, { name: 'colorrecall', desc: 'Recall', types: ['intellibrite', 'magicstream'], command: 'colorRecall', sequence: 14 }], + [6, { + name: 'lightthumper', desc: 'Thumper', types: ['magicstream'], command: 'lightThumper', message: 'Toggling Thumper', + sequence: [ // Cycle party mode 3 times. + { isOn: false, timeout: 100 }, + { isOn: true, timeout: 100 }, + { isOn: false, timeout: 100 }, + { isOn: true, timeout: 5000 }, + { isOn: false, timeout: 100 }, + { isOn: true, timeout: 100 }, + { isOn: false, timeout: 100 }, + { isOn: true, timeout: 5000 }, + { isOn: false, timeout: 100 }, + { isOn: true, timeout: 100 }, + { isOn: false, timeout: 100 } + ] + }], + [7, { + name: 'colorsync', desc: 'Sync', types: ['colorlogic'], command: 'colorSync', message: 'Synchronizing Lights', endingTheme: 'voodoolounge', + sequence: [ + { isOn: true, timeout: 1000 }, + { isOn: false, timeout: 12000 }, + { isOn: true } + ] + }], + [100, { name: 'settheme', types: ['all'], desc: 'Set Theme', message: 'Sequencing Theme' }] + ]); + public lightGroupCommands = new byteValueMap([ + [1, { name: 'colorsync', desc: 'Sync', types: ['intellibrite'], command: 'colorSync', message: 'Synchronizing' }], + [2, { name: 'colorset', desc: 'Set', types: ['intellibrite'], command: 'colorSet', message: 'Sequencing Set Operation' }], + [3, { name: 'colorswim', desc: 'Swim', types: ['intellibrite'], command: 'colorSwim', message: 'Sequencing Swim Operation' }], + [4, { name: 'colorhold', desc: 'Hold', types: ['intellibrite', 'magicstream'], command: 'colorHold', message: 'Saving Current Colors', sequence: 13 }], + [5, { name: 'colorrecall', desc: 'Recall', types: ['intellibrite', 'magicstream'], command: 'colorRecall', message: 'Recalling Saved Colors', sequence: 14 }], + [6, { + name: 'lightthumper', desc: 'Thumper', types: ['magicstream'], command: 'lightThumper', message: 'Toggling Thumper', + sequence: [ // Cycle party mode 3 times. + { isOn: false, timeout: 100 }, + { isOn: true, timeout: 100 }, + { isOn: false, timeout: 100 }, + { isOn: true, timeout: 5000 }, + { isOn: false, timeout: 100 }, + { isOn: true, timeout: 100 }, + { isOn: false, timeout: 100 }, + { isOn: true, timeout: 5000 }, + { isOn: false, timeout: 100 }, + { isOn: true, timeout: 100 }, + { isOn: false, timeout: 100 }, + { isOn: true, timeout: 1000 }, + ] + }] + ]); + public circuitActions: byteValueMap = new byteValueMap([ + [0, { name: 'ready', desc: 'Ready' }], + [1, { name: 'colorsync', desc: 'Synchronizing' }], + [2, { name: 'colorset', desc: 'Sequencing Set Operation' }], + [3, { name: 'colorswim', desc: 'Sequencing Swim Operation' }], + [4, { name: 'lighttheme', desc: 'Sequencing Theme/Color Operation' }], + [5, { name: 'colorhold', desc: 'Saving Current Color' }], + [6, { name: 'colorrecall', desc: 'Recalling Saved Color' }], + [7, { name: 'lightthumper', desc: 'Setting Light Thumper' }], + [100, { name: 'settheme', desc: 'Setting Light Theme' }] + ]); + public lightColors: byteValueMap = new byteValueMap([ + [0, { name: 'white', desc: 'White' }], + [2, { name: 'lightgreen', desc: 'Light Green' }], + [4, { name: 'green', desc: 'Green' }], + [6, { name: 'cyan', desc: 'Cyan' }], + [8, { name: 'blue', desc: 'Blue' }], + [10, { name: 'lavender', desc: 'Lavender' }], + [12, { name: 'magenta', desc: 'Magenta' }], + [14, { name: 'lightmagenta', desc: 'Light Magenta' }] + ]); + public scheduleDays: byteValueMap = new byteValueMap([ + [1, { name: 'sat', desc: 'Saturday', dow: 6 }], + [2, { name: 'fri', desc: 'Friday', dow: 5 }], + [3, { name: 'thu', desc: 'Thursday', dow: 4 }], + [4, { name: 'wed', desc: 'Wednesday', dow: 3 }], + [5, { name: 'tue', desc: 'Tuesday', dow: 2 }], + [6, { name: 'mon', desc: 'Monday', dow: 1 }], + [7, { name: 'sun', desc: 'Sunday', dow: 0 }] + ]); + public scheduleTimeTypes: byteValueMap = new byteValueMap([ + [0, { name: 'manual', desc: 'Manual' }] + ]); + public scheduleDisplayTypes: byteValueMap = new byteValueMap([ + [0, { name: 'always', desc: 'Always' }], + [1, { name: 'active', desc: 'When Active' }], + [2, { name: 'never', desc: 'Never' }] + ]); + public pumpTypes: byteValueMap = new byteValueMap([ + [1, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }], + [64, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, minFlow: 15, maxFlow: 130, flowStepSize: 1, maxCircuits: 8, hasAddress: true }], + [65, { name: 'ds', desc: 'Two-Speed', maxCircuits: 40, hasAddress: false, hasBody: true }], + [128, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, maxCircuits: 8, hasAddress: true }], + [169, { name: 'vssvrs', desc: 'IntelliFlo VS+SVRS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, speedStepSize: 10, maxCircuits: 8, hasAddress: true }] + ]); + public pumpSSModels: byteValueMap = new byteValueMap([ + [0, { name: 'unspecified', desc: 'Unspecified', amps: 0, pf: 0, volts: 0, watts: 0 }], + [1, { name: 'wf1hpE', desc: '1hp WhisperFlo E+', amps: 7.4, pf: .9, volts: 230, watts: 1532 }], + [2, { name: 'wf1hpMax', desc: '1hp WhisperFlo Max', amps: 9, pf: .87, volts: 230, watts: 1600 }], + [3, { name: 'generic15hp', desc: '1.5hp Pump', amps: 9.3, pf: .9, volts: 230, watts: 1925 }], + [4, { name: 'generic2hp', desc: '2hp Pump', amps: 12, pf: .9, volts: 230, watts: 2484 }], + [5, { name: 'generic25hp', desc: '2.5hp Pump', amps: 12.5, pf: .9, volts: 230, watts: 2587 }], + [6, { name: 'generic3hp', desc: '3hp Pump', amps: 13.5, pf: .9, volts: 230, watts: 2794 }] + ]); + public pumpDSModels: byteValueMap = new byteValueMap([ + [0, { name: 'unspecified', desc: 'Unspecified', loAmps: 0, hiAmps: 0, pf: 0, volts: 0, loWatts: 0, hiWatts: 0 }], + [1, { name: 'generic1hp', desc: '1hp Pump', loAmps: 2.4, hiAmps: 6.5, pf: .9, volts: 230, loWatts: 497, hiWatts: 1345 }], + [2, { name: 'generic15hp', desc: '1.5hp Pump', loAmps: 2.7, hiAmps: 9.3, pf: .9, volts: 230, loWatts: 558, hiWatts: 1925 }], + [3, { name: 'generic2hp', desc: '2hp Pump', loAmps: 2.9, hiAmps: 12, pf: .9, volts: 230, loWatts: 600, hiWatts: 2484 }], + [4, { name: 'generic25hp', desc: '2.5hp Pump', loAmps: 3.1, hiAmps: 12.5, pf: .9, volts: 230, loWatts: 642, hiWatts: 2587 }], + [5, { name: 'generic3hp', desc: '3hp Pump', loAmps: 3.3, hiAmps: 13.5, pf: .9, volts: 230, loWatts: 683, hiWatts: 2794 }] + ]); + public pumpVSModels: byteValueMap = new byteValueMap([ + [0, { name: 'intelliflovs', desc: 'IntelliFlo VS' }] + ]); + public pumpVFModels: byteValueMap = new byteValueMap([ + [0, { name: 'intelliflovf', desc: 'IntelliFlo VF' }] + ]); + public pumpVSFModels: byteValueMap = new byteValueMap([ + [0, { name: 'intelliflovsf', desc: 'IntelliFlo VSF' }] + ]); + public pumpVSSVRSModels: byteValueMap = new byteValueMap([ + [0, { name: 'intelliflovssvrs', desc: 'IntelliFlo VS+SVRS' }] + ]); + // These are used for single-speed pump definitions. Essentially the way this works is that when + // the body circuit is running the single speed pump is on. + public pumpBodies: byteValueMap = new byteValueMap([ + [0, { name: 'pool', desc: 'Pool' }], + [101, { name: 'spa', desc: 'Spa' }], + [255, { name: 'poolspa', desc: 'Pool/Spa' }] + ]); + public heaterTypes: byteValueMap = new byteValueMap([ + [1, { name: 'gas', desc: 'Gas Heater', hasAddress: false }], + [2, { name: 'solar', desc: 'Solar Heater', hasAddress: false, hasCoolSetpoint: true, hasPreference: true }], + [3, { name: 'heatpump', desc: 'Heat Pump', hasAddress: true, hasPreference: true }], + [4, { name: 'ultratemp', desc: 'UltraTemp', hasAddress: true, hasCoolSetpoint: true, hasPreference: true }], + [5, { name: 'hybrid', desc: 'Hybrid', hasAddress: true }], + [6, { name: 'mastertemp', desc: 'MasterTemp', hasAddress: true }], + [7, { name: 'maxetherm', desc: 'Max-E-Therm', hasAddress: true }], + ]); + public heatModes: byteValueMap = new byteValueMap([ + [0, { name: 'off', desc: 'Off' }], + [3, { name: 'heater', desc: 'Heater' }], + [5, { name: 'solar', desc: 'Solar Only' }], + [12, { name: 'solarpref', desc: 'Solar Preferred' }] + ]); + public heatSources: byteValueMap = new byteValueMap([ + [0, { name: 'off', desc: 'No Heater' }], + [3, { name: 'heater', desc: 'Heater' }], + [5, { name: 'solar', desc: 'Solar Only' }], + [21, { name: 'solarpref', desc: 'Solar Preferred' }], + [32, { name: 'nochange', desc: 'No Change' }] + ]); public heatStatus: byteValueMap = new byteValueMap([ [0, { name: 'off', desc: 'Off' }], [1, { name: 'heater', desc: 'Heater' }], @@ -480,203 +480,205 @@ export class byteValueMaps { [5, { name: 'dual', desc: 'Dual' }], [128, { name: 'cooldown', desc: 'Cooldown' }] ]); - public pumpStatus: byteValueMap = new byteValueMap([ - [0, { name: 'off', desc: 'Off' }], // When the pump is disconnected or has no power then we simply report off as the status. This is not the recommended wiring - // for a VS/VF pump as is should be powered at all times. When it is, the status will always report a value > 0. - [1, { name: 'ok', desc: 'Ok' }], // Status is always reported when the pump is not wired to a relay regardless of whether it is on or not - // as is should be if this is a VS / VF pump. However if it is wired to a relay most often filter, the pump will report status - // 0 if it is not running. Essentially this is no error but it is not a status either. - [2, { name: 'filter', desc: 'Filter warning' }], - [3, { name: 'overcurrent', desc: 'Overcurrent condition' }], - [4, { name: 'priming', desc: 'Priming' }], - [5, { name: 'blocked', desc: 'System blocked' }], - [6, { name: 'general', desc: 'General alarm' }], - [7, { name: 'overtemp', desc: 'Overtemp condition' }], - [8, { name: 'power', dec: 'Power outage' }], - [9, { name: 'overcurrent2', desc: 'Overcurrent condition 2' }], - [10, { name: 'overvoltage', desc: 'Overvoltage condition' }], - [11, { name: 'error11', desc: 'Unspecified Error 11' }], - [12, { name: 'error12', desc: 'Unspecified Error 12' }], - [13, { name: 'error13', desc: 'Unspecified Error 13' }], - [14, { name: 'error14', desc: 'Unspecified Error 14' }], - [15, { name: 'error15', desc: 'Unspecified Error 15' }], - [16, { name: 'commfailure', desc: 'Communication failure' }] - ]); - public pumpUnits: byteValueMap = new byteValueMap([ - [0, { name: 'rpm', desc: 'RPM' }], - [1, { name: 'gpm', desc: 'GPM' }] - ]); - public bodyTypes: byteValueMap = new byteValueMap([ - [0, { name: 'pool', desc: 'Pool' }], - [1, { name: 'spa', desc: 'Spa' }], - [2, { name: 'spa', desc: 'Spa' }], - [3, { name: 'spa', desc: 'Spa' }] - ]); - public bodies: byteValueMap = new byteValueMap([ - [0, { name: 'pool', desc: 'Pool' }], - [1, { name: 'spa', desc: 'Spa' }], - [2, { name: 'body3', desc: 'Body 3' }], - [3, { name: 'body4', desc: 'Body 4' }], - [32, { name: 'poolspa', desc: 'Pool/Spa' }] - ]); - public chlorinatorStatus: byteValueMap = new byteValueMap([ - [0, { name: 'ok', desc: 'Ok' }], - [1, { name: 'lowflow', desc: 'Low Flow' }], - [2, { name: 'lowsalt', desc: 'Low Salt' }], - [3, { name: 'verylowsalt', desc: 'Very Low Salt' }], - [4, { name: 'highcurrent', desc: 'High Current' }], - [5, { name: 'clean', desc: 'Clean Cell' }], - [6, { name: 'lowvoltage', desc: 'Low Voltage' }], - [7, { name: 'lowtemp', desc: 'Water Temp Low' }], - [8, { name: 'commlost', desc: 'Communication Lost' }] - ]); - public chlorinatorType: byteValueMap = new byteValueMap([ - [0, { name: 'pentair', desc: 'Pentair' }], - [1, { name: 'unknown', desc: 'unknown' }], - [2, { name: 'aquarite', desc: 'Aquarite' }], - [3, { name: 'unknown', desc: 'unknown' }] - ]); - public chlorinatorModel: byteValueMap = new byteValueMap([ - [0, { name: 'unknown', desc: 'unknown', capacity: 0, chlorinePerDay: 0, chlorinePerSec: 0 }], - [1, { name: 'intellichlor--15', desc: 'IntelliChlor IC15', capacity: 15000, chlorinePerDay: 0.60, chlorinePerSec: 0.60 / 86400 }], - [2, { name: 'intellichlor--20', desc: 'IntelliChlor IC20', capacity: 20000, chlorinePerDay: 0.70, chlorinePerSec: 0.70 / 86400 }], - [3, { name: 'intellichlor--40', desc: 'IntelliChlor IC40', capacity: 40000, chlorinePerDay: 1.40, chlorinePerSec: 1.4 / 86400 }], - [4, { name: 'intellichlor--60', desc: 'IntelliChlor IC60', capacity: 60000, chlorinePerDay: 2, chlorinePerSec: 2 / 86400 }], - [5, { name: 'aquarite-t15', desc: 'AquaRite T15', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }], - [6, { name: 'aquarite-t9', desc: 'AquaRite T9', capacity: 30000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }], - [7, { name: 'aquarite-t5', desc: 'AquaRite T5', capacity: 20000, chlorinePerDay: 0.735, chlorinePerSec: 0.735 / 86400 }], - [8, { name: 'aquarite-t3', desc: 'AquaRite T3', capacity: 15000, chlorinePerDay: 0.53, chlorinePerSec: 0.53 / 86400 }], - [9, { name: 'aquarite-925', desc: 'AquaRite 925', capacity: 25000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }], - [10, { name: 'aquarite-940', desc: 'AquaRite 940', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }] - ]) - public customNames: byteValueMap = new byteValueMap(); - public circuitNames: byteValueMap = new byteValueMap(); - public scheduleTypes: byteValueMap = new byteValueMap([ - [0, { name: 'runonce', desc: 'Run Once', startDate: true, startTime: true, endTime: true, days: false, heatSource: true, heatSetpoint: true }], - [128, { name: 'repeat', desc: 'Repeats', startDate: false, startTime: true, endTime: true, days: 'multi', heatSource: true, heatSetpoint: true }] - ]); - public circuitGroupTypes: byteValueMap = new byteValueMap([ - [0, { name: 'none', desc: 'Unspecified' }], - [1, { name: 'light', desc: 'Light' }], - [2, { name: 'circuit', desc: 'Circuit' }], - [3, { name: 'intellibrite', desc: 'IntelliBrite' }] - ]); - public groupCircuitStates: byteValueMap = new byteValueMap([ - [0, { name: 'off', desc: 'Off' }], - [1, { name: 'on', desc: 'On' }] - ]); - public systemUnits: byteValueMap = new byteValueMap([ - [0, { name: 'english', desc: 'English' }], - [4, { name: 'metric', desc: 'Metric' }] - ]); - public tempUnits: byteValueMap = new byteValueMap([ - [0, { name: 'F', desc: 'Fahrenheit' }], - [4, { name: 'C', desc: 'Celsius' }] - ]); - public valveTypes: byteValueMap = new byteValueMap([ - [0, { name: 'standard', desc: 'Standard' }], - [1, { name: 'intellivalve', desc: 'IntelliValve' }] - ]); - public valveModes: byteValueMap = new byteValueMap([ - [0, { name: 'off', desc: 'Off' }], - [1, { name: 'pool', desc: 'Pool' }], - [2, { name: 'spa', dest: 'Spa' }], - [3, { name: 'spillway', desc: 'Spillway' }], - [4, { name: 'spadrain', desc: 'Spa Drain' }] - ]); - public msgBroadcastActions: byteValueMap = new byteValueMap([ - [2, { name: 'status', desc: 'Equipment Status' }], - [82, { name: 'ivstatus', desc: 'IntelliValve Status' }] - ]); + public pumpStatus: byteValueMap = new byteValueMap([ + [0, { name: 'off', desc: 'Off' }], // When the pump is disconnected or has no power then we simply report off as the status. This is not the recommended wiring + // for a VS/VF pump as is should be powered at all times. When it is, the status will always report a value > 0. + [1, { name: 'ok', desc: 'Ok' }], // Status is always reported when the pump is not wired to a relay regardless of whether it is on or not + // as is should be if this is a VS / VF pump. However if it is wired to a relay most often filter, the pump will report status + // 0 if it is not running. Essentially this is no error but it is not a status either. + [2, { name: 'filter', desc: 'Filter warning' }], + [3, { name: 'overcurrent', desc: 'Overcurrent condition' }], + [4, { name: 'priming', desc: 'Priming' }], + [5, { name: 'blocked', desc: 'System blocked' }], + [6, { name: 'general', desc: 'General alarm' }], + [7, { name: 'overtemp', desc: 'Overtemp condition' }], + [8, { name: 'power', dec: 'Power outage' }], + [9, { name: 'overcurrent2', desc: 'Overcurrent condition 2' }], + [10, { name: 'overvoltage', desc: 'Overvoltage condition' }], + [11, { name: 'error11', desc: 'Unspecified Error 11' }], + [12, { name: 'error12', desc: 'Unspecified Error 12' }], + [13, { name: 'error13', desc: 'Unspecified Error 13' }], + [14, { name: 'error14', desc: 'Unspecified Error 14' }], + [15, { name: 'error15', desc: 'Unspecified Error 15' }], + [16, { name: 'commfailure', desc: 'Communication failure' }] + ]); + public pumpUnits: byteValueMap = new byteValueMap([ + [0, { name: 'rpm', desc: 'RPM' }], + [1, { name: 'gpm', desc: 'GPM' }] + ]); + public bodyTypes: byteValueMap = new byteValueMap([ + [0, { name: 'pool', desc: 'Pool' }], + [1, { name: 'spa', desc: 'Spa' }], + [2, { name: 'spa', desc: 'Spa' }], + [3, { name: 'spa', desc: 'Spa' }] + ]); + public bodies: byteValueMap = new byteValueMap([ + [0, { name: 'pool', desc: 'Pool' }], + [1, { name: 'spa', desc: 'Spa' }], + [2, { name: 'body3', desc: 'Body 3' }], + [3, { name: 'body4', desc: 'Body 4' }], + [32, { name: 'poolspa', desc: 'Pool/Spa' }] + ]); + public chlorinatorStatus: byteValueMap = new byteValueMap([ + [0, { name: 'ok', desc: 'Ok' }], + [1, { name: 'lowflow', desc: 'Low Flow' }], + [2, { name: 'lowsalt', desc: 'Low Salt' }], + [3, { name: 'verylowsalt', desc: 'Very Low Salt' }], + [4, { name: 'highcurrent', desc: 'High Current' }], + [5, { name: 'clean', desc: 'Clean Cell' }], + [6, { name: 'lowvoltage', desc: 'Low Voltage' }], + [7, { name: 'lowtemp', desc: 'Water Temp Low' }], + [8, { name: 'commlost', desc: 'Communication Lost' }] + ]); + public chlorinatorType: byteValueMap = new byteValueMap([ + [0, { name: 'pentair', desc: 'Pentair' }], + [1, { name: 'unknown', desc: 'unknown' }], + [2, { name: 'aquarite', desc: 'Aquarite' }], + [3, { name: 'unknown', desc: 'unknown' }] + ]); + public chlorinatorModel: byteValueMap = new byteValueMap([ + [0, { name: 'unknown', desc: 'unknown', capacity: 0, chlorinePerDay: 0, chlorinePerSec: 0 }], + [1, { name: 'intellichlor--15', desc: 'IntelliChlor IC15', capacity: 15000, chlorinePerDay: 0.60, chlorinePerSec: 0.60 / 86400 }], + [2, { name: 'intellichlor--20', desc: 'IntelliChlor IC20', capacity: 20000, chlorinePerDay: 0.70, chlorinePerSec: 0.70 / 86400 }], + [3, { name: 'intellichlor--40', desc: 'IntelliChlor IC40', capacity: 40000, chlorinePerDay: 1.40, chlorinePerSec: 1.4 / 86400 }], + [4, { name: 'intellichlor--60', desc: 'IntelliChlor IC60', capacity: 60000, chlorinePerDay: 2.0, chlorinePerSec: 2.0 / 86400 }], + [5, { name: 'aquarite-t15', desc: 'AquaRite T15', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }], + [6, { name: 'aquarite-t9', desc: 'AquaRite T9', capacity: 30000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }], + [7, { name: 'aquarite-t5', desc: 'AquaRite T5', capacity: 20000, chlorinePerDay: 0.735, chlorinePerSec: 0.735 / 86400 }], + [8, { name: 'aquarite-t3', desc: 'AquaRite T3', capacity: 15000, chlorinePerDay: 0.53, chlorinePerSec: 0.53 / 86400 }], + [9, { name: 'aquarite-925', desc: 'AquaRite 925', capacity: 25000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }], + [10, { name: 'aquarite-940', desc: 'AquaRite 940', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }], + [11, { name: 'ichlor-ic15', desc: 'iChlor IC15', capacity: 15000, chlorinePerDay: 0.6, chlorinePerSec: 0.6 / 86400 }], + [12, { name: 'ichlor-ic30', desc: 'iChlor IC30', capacity: 30000, chlorinePerDay: 1.0, chlorinePerSec: 1.0 / 86400 }] + ]); + public customNames: byteValueMap = new byteValueMap(); + public circuitNames: byteValueMap = new byteValueMap(); + public scheduleTypes: byteValueMap = new byteValueMap([ + [0, { name: 'runonce', desc: 'Run Once', startDate: true, startTime: true, endTime: true, days: false, heatSource: true, heatSetpoint: true }], + [128, { name: 'repeat', desc: 'Repeats', startDate: false, startTime: true, endTime: true, days: 'multi', heatSource: true, heatSetpoint: true }] + ]); + public circuitGroupTypes: byteValueMap = new byteValueMap([ + [0, { name: 'none', desc: 'Unspecified' }], + [1, { name: 'light', desc: 'Light' }], + [2, { name: 'circuit', desc: 'Circuit' }], + [3, { name: 'intellibrite', desc: 'IntelliBrite' }] + ]); + public groupCircuitStates: byteValueMap = new byteValueMap([ + [0, { name: 'off', desc: 'Off' }], + [1, { name: 'on', desc: 'On' }] + ]); + public systemUnits: byteValueMap = new byteValueMap([ + [0, { name: 'english', desc: 'English' }], + [4, { name: 'metric', desc: 'Metric' }] + ]); + public tempUnits: byteValueMap = new byteValueMap([ + [0, { name: 'F', desc: 'Fahrenheit' }], + [4, { name: 'C', desc: 'Celsius' }] + ]); + public valveTypes: byteValueMap = new byteValueMap([ + [0, { name: 'standard', desc: 'Standard' }], + [1, { name: 'intellivalve', desc: 'IntelliValve' }] + ]); + public valveModes: byteValueMap = new byteValueMap([ + [0, { name: 'off', desc: 'Off' }], + [1, { name: 'pool', desc: 'Pool' }], + [2, { name: 'spa', dest: 'Spa' }], + [3, { name: 'spillway', desc: 'Spillway' }], + [4, { name: 'spadrain', desc: 'Spa Drain' }] + ]); + public msgBroadcastActions: byteValueMap = new byteValueMap([ + [2, { name: 'status', desc: 'Equipment Status' }], + [82, { name: 'ivstatus', desc: 'IntelliValve Status' }] + ]); public chemDoserTypes: byteValueMap = new byteValueMap([ [0, { name: 'acid', desc: 'Acid' }], [1, { name: 'chlor', desc: 'Chlorine' }] ]); - public chemControllerTypes: byteValueMap = new byteValueMap([ - [0, { name: 'none', desc: 'None', ph: { min: 6.8, max: 7.6 }, orp: { min: 400, max: 800 }, hasAddress: false }], - [1, { name: 'unknown', desc: 'Unknown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }], - [2, { name: 'intellichem', desc: 'IntelliChem', ph: { min: 7.2, max: 7.6 }, orp: { min: 400, max: 800 }, hasAddress: true }], - // [3, { name: 'homegrown', desc: 'Homegrown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }], - [4, { name: 'rem', desc: 'REM Chem', ph: { min: 6.8, max: 8.0 }, hasAddress: false }] - ]); - public siCalcTypes: byteValueMap = new byteValueMap([ - [0, { name: 'lsi', desc: 'Langelier Saturation Index' }], - [1, { name: 'csi', desc: 'Calcite Saturation Index' }] - ]); - public chemPumpTypes: byteValueMap = new byteValueMap([ - [0, { name: 'none', desc: 'No Pump', ratedFlow: false, tank: false, remAddress: false }], - [1, { name: 'relay', desc: 'Relay Pump', ratedFlow: true, tank: true, remAddress: true }], - [2, { name: 'ezo-pmp', desc: 'Altas EZO-PMP', ratedFlow: true, tank: true, remAddress: true }] - ]); - public chemPhProbeTypes: byteValueMap = new byteValueMap([ - [0, { name: 'none', desc: 'No Probe' }], - [1, { name: 'ezo-ph', desc: 'Atlas EZO-PH', remAddress: true }], - [2, { name: 'other', desc: 'Other' }] - ]); - public chemORPProbeTypes: byteValueMap = new byteValueMap([ - [0, { name: 'none', desc: 'No Probe' }], - [1, { name: 'ezo-orp', desc: 'Atlas EZO-ORP', remAddress: true }], - [2, { name: 'other', desc: 'Other' }] - ]); - public flowSensorTypes: byteValueMap = new byteValueMap([ - [0, { name: 'none', desc: 'No Sensor' }], - [1, { name: 'switch', desc: 'Flow Switch', remAddress: true }], - [2, { name: 'rate', desc: 'Rate Sensor', remAddress: true }], - [4, { name: 'pressure', desc: 'Pressure Sensor', remAddress: true }], - ]); - public chemDosingMethods: byteValueMap = new byteValueMap([ - [0, { name: 'manual', desc: 'Manual' }], - [1, { name: 'time', desc: 'Time' }], - [2, { name: 'volume', desc: 'Volume' }] - ]); - public chemChlorDosingMethods: byteValueMap = new byteValueMap([ - [0, { name: 'chlor', desc: 'Use Chlorinator Settings' }], - [1, { name: 'target', desc: 'Dynamic based on ORP Setpoint' }] - ]); - public phSupplyTypes: byteValueMap = new byteValueMap([ - [0, { name: 'base', desc: 'Base pH+' }], - [1, { name: 'acid', desc: 'Acid pH-' }] - ]); - public phDoserTypes: byteValueMap = new byteValueMap([ - [0, { name: 'none', desc: 'No Doser Attached' }], - [1, { name: 'extrelay', desc: 'External Relay' }], - [2, { name: 'co2', desc: 'CO2 Tank' }], - [3, { name: 'intrelay', desc: 'Internal Relay'}] - ]); - public orpDoserTypes: byteValueMap = new byteValueMap([ - [0, { name: 'none', desc: 'No Doser Attached' }], - [1, { name: 'extrelay', desc: 'External Relay' }], - [2, { name: 'chlorinator', desc: 'Chlorinator'}], - [3, { name: 'intrelay', desc: 'Internal Relay'}] - ]) - public volumeUnits: byteValueMap = new byteValueMap([ - [0, { name: '', desc: 'No Units' }], - [1, { name: 'gal', desc: 'Gallons' }], - [2, { name: 'L', desc: 'Liters' }], - [3, { name: 'mL', desc: 'Milliliters' }], - [4, { name: 'cL', desc: 'Centiliters' }], - [5, { name: 'oz', desc: 'Ounces' }], - [6, { name: 'qt', desc: 'Quarts' }], - [7, { name: 'pt', desc: 'Pints' }] - ]); - public pressureUnits: byteValueMap = new byteValueMap([ - [0, { name: 'psi', desc: 'Pounds per Sqare Inch' }], - [1, { name: 'Pa', desc: 'Pascal' }], - [2, { name: 'kPa', desc: 'Kilo-pascals' }], - [3, { name: 'atm', desc: 'Atmospheres' }], - [4, { name: 'bar', desc: 'Barometric' }] - ]); + public chemControllerTypes: byteValueMap = new byteValueMap([ + [0, { name: 'none', desc: 'None', ph: { min: 6.8, max: 7.6 }, orp: { min: 400, max: 800 }, hasAddress: false }], + [1, { name: 'unknown', desc: 'Unknown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }], + [2, { name: 'intellichem', desc: 'IntelliChem', ph: { min: 7.2, max: 7.6 }, orp: { min: 400, max: 800 }, hasAddress: true }], + // [3, { name: 'homegrown', desc: 'Homegrown', ph: { min: 6.8, max: 7.6 }, hasAddress: false }], + [4, { name: 'rem', desc: 'REM Chem', ph: { min: 6.8, max: 8.0 }, hasAddress: false }] + ]); + public siCalcTypes: byteValueMap = new byteValueMap([ + [0, { name: 'lsi', desc: 'Langelier Saturation Index' }], + [1, { name: 'csi', desc: 'Calcite Saturation Index' }] + ]); + public chemPumpTypes: byteValueMap = new byteValueMap([ + [0, { name: 'none', desc: 'No Pump', ratedFlow: false, tank: false, remAddress: false }], + [1, { name: 'relay', desc: 'Relay Pump', ratedFlow: true, tank: true, remAddress: true }], + [2, { name: 'ezo-pmp', desc: 'Altas EZO-PMP', ratedFlow: true, tank: true, remAddress: true }] + ]); + public chemPhProbeTypes: byteValueMap = new byteValueMap([ + [0, { name: 'none', desc: 'No Probe' }], + [1, { name: 'ezo-ph', desc: 'Atlas EZO-PH', remAddress: true }], + [2, { name: 'other', desc: 'Other' }] + ]); + public chemORPProbeTypes: byteValueMap = new byteValueMap([ + [0, { name: 'none', desc: 'No Probe' }], + [1, { name: 'ezo-orp', desc: 'Atlas EZO-ORP', remAddress: true }], + [2, { name: 'other', desc: 'Other' }] + ]); + public flowSensorTypes: byteValueMap = new byteValueMap([ + [0, { name: 'none', desc: 'No Sensor' }], + [1, { name: 'switch', desc: 'Flow Switch', remAddress: true }], + [2, { name: 'rate', desc: 'Rate Sensor', remAddress: true }], + [4, { name: 'pressure', desc: 'Pressure Sensor', remAddress: true }], + ]); + public chemDosingMethods: byteValueMap = new byteValueMap([ + [0, { name: 'manual', desc: 'Manual' }], + [1, { name: 'time', desc: 'Time' }], + [2, { name: 'volume', desc: 'Volume' }] + ]); + public chemChlorDosingMethods: byteValueMap = new byteValueMap([ + [0, { name: 'chlor', desc: 'Use Chlorinator Settings' }], + [1, { name: 'target', desc: 'Dynamic based on ORP Setpoint' }] + ]); + public phSupplyTypes: byteValueMap = new byteValueMap([ + [0, { name: 'base', desc: 'Base pH+' }], + [1, { name: 'acid', desc: 'Acid pH-' }] + ]); + public phDoserTypes: byteValueMap = new byteValueMap([ + [0, { name: 'none', desc: 'No Doser Attached' }], + [1, { name: 'extrelay', desc: 'External Relay' }], + [2, { name: 'co2', desc: 'CO2 Tank' }], + [3, { name: 'intrelay', desc: 'Internal Relay' }] + ]); + public orpDoserTypes: byteValueMap = new byteValueMap([ + [0, { name: 'none', desc: 'No Doser Attached' }], + [1, { name: 'extrelay', desc: 'External Relay' }], + [2, { name: 'chlorinator', desc: 'Chlorinator' }], + [3, { name: 'intrelay', desc: 'Internal Relay' }] + ]) + public volumeUnits: byteValueMap = new byteValueMap([ + [0, { name: '', desc: 'No Units' }], + [1, { name: 'gal', desc: 'Gallons' }], + [2, { name: 'L', desc: 'Liters' }], + [3, { name: 'mL', desc: 'Milliliters' }], + [4, { name: 'cL', desc: 'Centiliters' }], + [5, { name: 'oz', desc: 'Ounces' }], + [6, { name: 'qt', desc: 'Quarts' }], + [7, { name: 'pt', desc: 'Pints' }] + ]); + public pressureUnits: byteValueMap = new byteValueMap([ + [0, { name: 'psi', desc: 'Pounds per Sqare Inch' }], + [1, { name: 'Pa', desc: 'Pascal' }], + [2, { name: 'kPa', desc: 'Kilo-pascals' }], + [3, { name: 'atm', desc: 'Atmospheres' }], + [4, { name: 'bar', desc: 'Barometric' }] + ]); - public areaUnits: byteValueMap = new byteValueMap([ - [0, { name: '', desc: 'No Units' }], - [1, { name: 'sqft', desc: 'Square Feet' }], - [2, { name: 'sqM', desc: 'Square Meters' }] - ]); - public chemControllerStatus: byteValueMap = new byteValueMap([ - [0, { name: 'ok', desc: 'Ok' }], - [1, { name: 'nocomms', desc: 'No Communication' }], - [2, { name: 'config', desc: 'Invalid Configuration' }] - ]); + public areaUnits: byteValueMap = new byteValueMap([ + [0, { name: '', desc: 'No Units' }], + [1, { name: 'sqft', desc: 'Square Feet' }], + [2, { name: 'sqM', desc: 'Square Meters' }] + ]); + public chemControllerStatus: byteValueMap = new byteValueMap([ + [0, { name: 'ok', desc: 'Ok' }], + [1, { name: 'nocomms', desc: 'No Communication' }], + [2, { name: 'config', desc: 'Invalid Configuration' }] + ]); public chemDoserStatus: byteValueMap = new byteValueMap([ [0, { name: 'ok', desc: 'Ok' }], [1, { name: 'nocomms', desc: 'No Communication' }], @@ -714,226 +716,226 @@ export class byteValueMaps { ]); - public chemControllerAlarms: byteValueMap = new byteValueMap([ - [0, { name: 'ok', desc: 'Ok - No alarm' }], - [1, { name: 'noflow', desc: 'No Flow Detected' }], - [2, { name: 'phhigh', desc: 'pH Level High' }], - [4, { name: 'phlow', desc: 'pH Level Low' }], - [8, { name: 'orphigh', desc: 'orp Level High' }], - [16, { name: 'orplow', desc: 'orp Level Low' }], - [32, { name: 'phtankempty', desc: 'pH Tank Empty' }], - [64, { name: 'orptankempty', desc: 'orp Tank Empty' }], - [128, { name: 'probefault', desc: 'Probe Fault' }], - [129, { name: 'phtanklow', desc: 'pH Tank Low' }], - [130, { name: 'orptanklow', desc: 'orp Tank Low' }], - [131, { name: 'freezeprotect', desc: 'Freeze Protection Lockout'}] - ]); - public chemControllerHardwareFaults: byteValueMap = new byteValueMap([ - [0, { name: 'ok', desc: 'Ok - No Faults' }], - [1, { name: 'phprobe', desc: 'pH Probe Fault' }], - [2, { name: 'phpump', desc: 'pH Pump Fault' }], - [3, { name: 'orpprobe', desc: 'ORP Probe Fault' }], - [4, { name: 'orppump', desc: 'ORP Pump Fault' }], - [5, { name: 'chlormismatch', desc: 'Chlorinator body mismatch' }], - [6, { name: 'invalidbody', desc: 'Body capacity not valid' }], - [7, { name: 'flowsensor', desc: 'Flow Sensor Fault' }] - ]); - public chemControllerWarnings: byteValueMap = new byteValueMap([ - [0, { name: 'ok', desc: 'Ok - No Warning' }], - [1, { name: 'corrosive', desc: 'Corrosion May Occur' }], - [2, { name: 'scaling', desc: 'Scaling May Occur' }], - [8, { name: 'invalidsetup', desc: 'Invalid Setup' }], - [16, { name: 'chlorinatorComms', desc: 'Chlorinator Comms Error' }] - ]); - public chemControllerLimits: byteValueMap = new byteValueMap([ - [0, { name: 'ok', desc: 'Ok - No limits reached' }], - [1, { name: 'phlockout', desc: 'pH Lockout - ORP will not dose' }], - [2, { name: 'phdailylimit', desc: 'pH Daily Limit Reached' }], - [4, { name: 'orpdailylimit', desc: 'orp Daily Limit Reached' }], - [128, { name: 'commslost', desc: 'Communications with Chem Controller Lost' }] // to be verified - ]); - public chemControllerDosingStatus: byteValueMap = new byteValueMap([ - [0, { name: 'dosing', desc: 'Dosing' }], - [1, { name: 'mixing', desc: 'Mixing' }], - [2, { name: 'monitoring', desc: 'Monitoring' }] - ]); - public acidTypes: byteValueMap = new byteValueMap([ - [0, { name: 'a34.6', desc: '34.6% - 22 Baume', dosingFactor: 0.909091 }], - [1, { name: 'a31.45', desc: '31.45% - 20 Baume', dosingFactor: 1 }], - [2, { name: 'a29', desc: '29% - 19 Baume', dosingFactor: 1.08448 }], - [3, { name: 'a28', desc: '28.3% - 18 Baume', dosingFactor: 1.111111 }], - [4, { name: 'a15.7', desc: '15.7% - 10 Baume', dosingFactor: 2.0 }], - [5, { name: 'a14.5', desc: '14.5% - 9.8 Baume', dosingFactor: 2.16897 }], - ]); - public filterTypes: byteValueMap = new byteValueMap([ - [0, { name: 'sand', desc: 'Sand', hasBackwash: true }], - [1, { name: 'cartridge', desc: 'Cartridge', hasBackwash: false }], - [2, { name: 'de', desc: 'Diatom Earth', hasBackwash: true }], - [3, { name: 'unknown', desc: 'Unknown' }] - ]); + public chemControllerAlarms: byteValueMap = new byteValueMap([ + [0, { name: 'ok', desc: 'Ok - No alarm' }], + [1, { name: 'noflow', desc: 'No Flow Detected' }], + [2, { name: 'phhigh', desc: 'pH Level High' }], + [4, { name: 'phlow', desc: 'pH Level Low' }], + [8, { name: 'orphigh', desc: 'orp Level High' }], + [16, { name: 'orplow', desc: 'orp Level Low' }], + [32, { name: 'phtankempty', desc: 'pH Tank Empty' }], + [64, { name: 'orptankempty', desc: 'orp Tank Empty' }], + [128, { name: 'probefault', desc: 'Probe Fault' }], + [129, { name: 'phtanklow', desc: 'pH Tank Low' }], + [130, { name: 'orptanklow', desc: 'orp Tank Low' }], + [131, { name: 'freezeprotect', desc: 'Freeze Protection Lockout' }] + ]); + public chemControllerHardwareFaults: byteValueMap = new byteValueMap([ + [0, { name: 'ok', desc: 'Ok - No Faults' }], + [1, { name: 'phprobe', desc: 'pH Probe Fault' }], + [2, { name: 'phpump', desc: 'pH Pump Fault' }], + [3, { name: 'orpprobe', desc: 'ORP Probe Fault' }], + [4, { name: 'orppump', desc: 'ORP Pump Fault' }], + [5, { name: 'chlormismatch', desc: 'Chlorinator body mismatch' }], + [6, { name: 'invalidbody', desc: 'Body capacity not valid' }], + [7, { name: 'flowsensor', desc: 'Flow Sensor Fault' }] + ]); + public chemControllerWarnings: byteValueMap = new byteValueMap([ + [0, { name: 'ok', desc: 'Ok - No Warning' }], + [1, { name: 'corrosive', desc: 'Corrosion May Occur' }], + [2, { name: 'scaling', desc: 'Scaling May Occur' }], + [8, { name: 'invalidsetup', desc: 'Invalid Setup' }], + [16, { name: 'chlorinatorComms', desc: 'Chlorinator Comms Error' }] + ]); + public chemControllerLimits: byteValueMap = new byteValueMap([ + [0, { name: 'ok', desc: 'Ok - No limits reached' }], + [1, { name: 'phlockout', desc: 'pH Lockout - ORP will not dose' }], + [2, { name: 'phdailylimit', desc: 'pH Daily Limit Reached' }], + [4, { name: 'orpdailylimit', desc: 'orp Daily Limit Reached' }], + [128, { name: 'commslost', desc: 'Communications with Chem Controller Lost' }] // to be verified + ]); + public chemControllerDosingStatus: byteValueMap = new byteValueMap([ + [0, { name: 'dosing', desc: 'Dosing' }], + [1, { name: 'mixing', desc: 'Mixing' }], + [2, { name: 'monitoring', desc: 'Monitoring' }] + ]); + public acidTypes: byteValueMap = new byteValueMap([ + [0, { name: 'a34.6', desc: '34.6% - 22 Baume', dosingFactor: 0.909091 }], + [1, { name: 'a31.45', desc: '31.45% - 20 Baume', dosingFactor: 1 }], + [2, { name: 'a29', desc: '29% - 19 Baume', dosingFactor: 1.08448 }], + [3, { name: 'a28', desc: '28.3% - 18 Baume', dosingFactor: 1.111111 }], + [4, { name: 'a15.7', desc: '15.7% - 10 Baume', dosingFactor: 2.0 }], + [5, { name: 'a14.5', desc: '14.5% - 9.8 Baume', dosingFactor: 2.16897 }], + ]); + public filterTypes: byteValueMap = new byteValueMap([ + [0, { name: 'sand', desc: 'Sand', hasBackwash: true }], + [1, { name: 'cartridge', desc: 'Cartridge', hasBackwash: false }], + [2, { name: 'de', desc: 'Diatom Earth', hasBackwash: true }], + [3, { name: 'unknown', desc: 'Unknown' }] + ]); - // public filterPSITargetTypes: byteValueMap = new byteValueMap([ - // [0, { name: 'none', desc: 'Do not use filter PSI' }], - // [1, { name: 'value', desc: 'Change filter at value' }], - // [2, { name: 'percent', desc: 'Change filter with % increase' }], - // [3, { name: 'increase', desc: 'Change filter with psi increase' }] - // ]); - public countries: byteValueMap = new byteValueMap([ - [1, { name: 'US', desc: 'United States' }], - [2, { name: 'CA', desc: 'Canada' }], - [3, { name: 'MX', desc: 'Mexico' }] - ]); - public timeZones: byteValueMap = new byteValueMap([ - [128, { name: 'Samoa Standard Time', loc: 'Pacific', abbrev: 'SST', utcOffset: -11 }], - [129, { name: 'Tahiti Time', loc: 'Pacific', abbrev: 'TAHT', utcOffset: -10 }], - [130, { name: 'Alaska Standard Time', loc: 'North America', abbrev: 'AKST', utcOffset: -9 }], - [131, { name: 'Pacific Standard Time', loc: 'North America', abbrev: 'PST', utcOffset: -8 }], - [132, { name: 'Mountain Standard Time', loc: 'North America', abbrev: 'MST', utcOffset: -7 }], - [133, { name: 'Central Standard Time', loc: 'North America', abbrev: 'CST', utcOffset: -6 }], - [134, { name: 'Eastern Standard Time', loc: 'North America', abbrev: 'EST', utcOffset: -5 }], - [135, { name: 'Chile Standard Time', loc: 'South America', abbrev: 'CLT', utcOffset: -4 }], - [136, { name: 'French Guiana Time', loc: 'South America', abbrev: 'GFT', utcOffset: -3 }], - [137, { name: 'Fernando de Noronha Time', loc: 'South America', abbrev: 'FNT', utcOffset: -2 }], - [138, { name: 'Azores Time', loc: 'Atlantic', abbrev: 'AZOST', utcOffset: -1 }], - [139, { name: 'Greenwich Mean Time', loc: 'Europe', abbrev: 'GMT', utcOffset: 0 }], - [140, { name: 'Central European Time', loc: 'Europe', abbrev: 'CET', utcOffset: 1 }], - [141, { name: 'Eastern European Time', loc: 'Europe', abbrev: 'EET', utcOffset: 2 }], - [142, { name: 'Eastern Africa Time', loc: 'Africa', abbrev: 'EAT', utcOffset: 3 }], - [143, { name: 'Georgia Standard Time', loc: 'Europe/Asia', abbrev: 'GET', utcOffset: 4 }], - [144, { name: 'Pakistan Standard Time', loc: 'Asia', abbrev: 'PKT', utcOffset: 5 }], - [145, { name: 'Bangladesh Standard Time', loc: 'Asia', abbrev: 'BST', utcOffset: 6 }], - [146, { name: 'Western Indonesian Time', loc: 'Asia', abbrev: 'WIB', utcOffset: 7 }], - [147, { name: 'Australian Western Standard Time', loc: 'Australia', abbrev: 'AWST', utcOffset: 8 }], - [148, { name: 'Japan Standard Time', loc: 'Asia', abbrev: 'JST', utcOffset: 9 }], - [149, { name: 'Australian Eastern Standard Time', loc: 'Australia', abbrev: 'AEST', utcOffset: 10 }], - [150, { name: 'Solomon Islands Time', loc: 'Pacific', abbrev: 'SBT', utcOffset: 11 }], - [151, { name: 'Marshall Islands Time', loc: 'Pacific', abbrev: 'MHT', utcOffset: 12 }], - [191, { name: 'Fiji Time', loc: 'Pacific', abbrev: 'FJT', utcOffset: 12 }] - ]); - public clockSources: byteValueMap = new byteValueMap([ - [3, { name: 'server', desc: 'Server' }] - ]); - public clockModes: byteValueMap = new byteValueMap([ - [12, { name: '12 Hour' }], - [24, { name: '24 Hour' }] - ]); - public virtualControllerStatus: byteValueMap = new byteValueMap([ - [-1, { name: 'notapplicable', desc: 'Not Applicable' }], - [0, { name: 'stopped', desc: 'Stopped' }], - [1, { name: 'running', desc: 'Running' }] - ]); - public eqMessageSeverities: byteValueMap = new byteValueMap([ - [-1, { name: 'unspecified', desc: 'Unspecified' }], - [0, { name: 'info', desc: 'Information', icon: 'fas fa-circle-info' }], - [1, { name: 'reminder', desc: 'Reminder', icon: 'fas fa-bell' }], - [2, { name: 'alert', desc: 'Alert', icon: 'fas fa-circle-exclamation' }], - [3, { name: 'warning', desc: 'Warning', icon: 'fas fa-circle-exclamation' }], - [4, { name: 'error', desc: 'Error', icon: 'fas fa-triangle-exclamation' }], - [5, { name: 'fatal', desc: 'Fatal', icon: 'fas fa-skull-crossbones' }] - ]); - // need to validate these... - public delay: byteValueMap = new byteValueMap([ - [0, { name: 'nodelay', desc: 'No Delay' }], - [32, { name: 'nodelay', desc: 'No Delay' }], - [34, { name: 'heaterdelay', desc: 'Heater Delay' }], - [36, { name: 'cleanerdelay', desc: 'Cleaner Delay' }] - ]); - public remoteTypes: byteValueMap = new byteValueMap([ - [0, { name: 'none', desc: 'Not Installed', maxButtons: 0 }], - [1, { name: 'is4', desc: 'iS4 Spa-Side Remote', maxButtons: 4 }], - [2, { name: 'is10', desc: 'iS10 Spa-Side Remote', maxButtons: 10 }], - [6, { name: 'quickTouch', desc: 'Quick Touch Remote', maxButtons: 4 }], - [7, { name: 'spaCommand', desc: 'Spa Command', maxButtons: 10 }] - ]); - public appVersionStatus: byteValueMap = new byteValueMap([ - [-1, { name: 'unknown', desc: 'Unable to compare versions' }], - [0, { name: 'current', desc: 'On current version' }], - [1, { name: 'behind', desc: 'New version available' }], - [2, { name: 'ahead', desc: 'Ahead of published version' }] - ]); + // public filterPSITargetTypes: byteValueMap = new byteValueMap([ + // [0, { name: 'none', desc: 'Do not use filter PSI' }], + // [1, { name: 'value', desc: 'Change filter at value' }], + // [2, { name: 'percent', desc: 'Change filter with % increase' }], + // [3, { name: 'increase', desc: 'Change filter with psi increase' }] + // ]); + public countries: byteValueMap = new byteValueMap([ + [1, { name: 'US', desc: 'United States' }], + [2, { name: 'CA', desc: 'Canada' }], + [3, { name: 'MX', desc: 'Mexico' }] + ]); + public timeZones: byteValueMap = new byteValueMap([ + [128, { name: 'Samoa Standard Time', loc: 'Pacific', abbrev: 'SST', utcOffset: -11 }], + [129, { name: 'Tahiti Time', loc: 'Pacific', abbrev: 'TAHT', utcOffset: -10 }], + [130, { name: 'Alaska Standard Time', loc: 'North America', abbrev: 'AKST', utcOffset: -9 }], + [131, { name: 'Pacific Standard Time', loc: 'North America', abbrev: 'PST', utcOffset: -8 }], + [132, { name: 'Mountain Standard Time', loc: 'North America', abbrev: 'MST', utcOffset: -7 }], + [133, { name: 'Central Standard Time', loc: 'North America', abbrev: 'CST', utcOffset: -6 }], + [134, { name: 'Eastern Standard Time', loc: 'North America', abbrev: 'EST', utcOffset: -5 }], + [135, { name: 'Chile Standard Time', loc: 'South America', abbrev: 'CLT', utcOffset: -4 }], + [136, { name: 'French Guiana Time', loc: 'South America', abbrev: 'GFT', utcOffset: -3 }], + [137, { name: 'Fernando de Noronha Time', loc: 'South America', abbrev: 'FNT', utcOffset: -2 }], + [138, { name: 'Azores Time', loc: 'Atlantic', abbrev: 'AZOST', utcOffset: -1 }], + [139, { name: 'Greenwich Mean Time', loc: 'Europe', abbrev: 'GMT', utcOffset: 0 }], + [140, { name: 'Central European Time', loc: 'Europe', abbrev: 'CET', utcOffset: 1 }], + [141, { name: 'Eastern European Time', loc: 'Europe', abbrev: 'EET', utcOffset: 2 }], + [142, { name: 'Eastern Africa Time', loc: 'Africa', abbrev: 'EAT', utcOffset: 3 }], + [143, { name: 'Georgia Standard Time', loc: 'Europe/Asia', abbrev: 'GET', utcOffset: 4 }], + [144, { name: 'Pakistan Standard Time', loc: 'Asia', abbrev: 'PKT', utcOffset: 5 }], + [145, { name: 'Bangladesh Standard Time', loc: 'Asia', abbrev: 'BST', utcOffset: 6 }], + [146, { name: 'Western Indonesian Time', loc: 'Asia', abbrev: 'WIB', utcOffset: 7 }], + [147, { name: 'Australian Western Standard Time', loc: 'Australia', abbrev: 'AWST', utcOffset: 8 }], + [148, { name: 'Japan Standard Time', loc: 'Asia', abbrev: 'JST', utcOffset: 9 }], + [149, { name: 'Australian Eastern Standard Time', loc: 'Australia', abbrev: 'AEST', utcOffset: 10 }], + [150, { name: 'Solomon Islands Time', loc: 'Pacific', abbrev: 'SBT', utcOffset: 11 }], + [151, { name: 'Marshall Islands Time', loc: 'Pacific', abbrev: 'MHT', utcOffset: 12 }], + [191, { name: 'Fiji Time', loc: 'Pacific', abbrev: 'FJT', utcOffset: 12 }] + ]); + public clockSources: byteValueMap = new byteValueMap([ + [3, { name: 'server', desc: 'Server' }] + ]); + public clockModes: byteValueMap = new byteValueMap([ + [12, { name: '12 Hour' }], + [24, { name: '24 Hour' }] + ]); + public virtualControllerStatus: byteValueMap = new byteValueMap([ + [-1, { name: 'notapplicable', desc: 'Not Applicable' }], + [0, { name: 'stopped', desc: 'Stopped' }], + [1, { name: 'running', desc: 'Running' }] + ]); + public eqMessageSeverities: byteValueMap = new byteValueMap([ + [-1, { name: 'unspecified', desc: 'Unspecified' }], + [0, { name: 'info', desc: 'Information', icon: 'fas fa-circle-info' }], + [1, { name: 'reminder', desc: 'Reminder', icon: 'fas fa-bell' }], + [2, { name: 'alert', desc: 'Alert', icon: 'fas fa-circle-exclamation' }], + [3, { name: 'warning', desc: 'Warning', icon: 'fas fa-circle-exclamation' }], + [4, { name: 'error', desc: 'Error', icon: 'fas fa-triangle-exclamation' }], + [5, { name: 'fatal', desc: 'Fatal', icon: 'fas fa-skull-crossbones' }] + ]); + // need to validate these... + public delay: byteValueMap = new byteValueMap([ + [0, { name: 'nodelay', desc: 'No Delay' }], + [32, { name: 'nodelay', desc: 'No Delay' }], + [34, { name: 'heaterdelay', desc: 'Heater Delay' }], + [36, { name: 'cleanerdelay', desc: 'Cleaner Delay' }] + ]); + public remoteTypes: byteValueMap = new byteValueMap([ + [0, { name: 'none', desc: 'Not Installed', maxButtons: 0 }], + [1, { name: 'is4', desc: 'iS4 Spa-Side Remote', maxButtons: 4 }], + [2, { name: 'is10', desc: 'iS10 Spa-Side Remote', maxButtons: 10 }], + [6, { name: 'quickTouch', desc: 'Quick Touch Remote', maxButtons: 4 }], + [7, { name: 'spaCommand', desc: 'Spa Command', maxButtons: 10 }] + ]); + public appVersionStatus: byteValueMap = new byteValueMap([ + [-1, { name: 'unknown', desc: 'Unable to compare versions' }], + [0, { name: 'current', desc: 'On current version' }], + [1, { name: 'behind', desc: 'New version available' }], + [2, { name: 'ahead', desc: 'Ahead of published version' }] + ]); } // SystemBoard is a mechanism to abstract the underlying pool system from specific functionality // managed by the personality board. This also provides a way to override specific functions for // acquiring state and configuration data. export class SystemBoard { - protected _statusTimer: NodeJS.Timeout; - protected _statusCheckRef: number = 0; - protected _statusInterval: number = 3000; + protected _statusTimer: NodeJS.Timeout; + protected _statusCheckRef: number = 0; + protected _statusInterval: number = 3000; - // TODO: (RSG) Do we even need to pass in system? We don't seem to be using it and we're overwriting the var with the SystemCommands anyway. + // TODO: (RSG) Do we even need to pass in system? We don't seem to be using it and we're overwriting the var with the SystemCommands anyway. constructor(system: PoolSystem) { } public async closeAsync() { }; - protected _modulesAcquired: boolean = true; - public needsConfigChanges: boolean = false; - public valueMaps: byteValueMaps = new byteValueMaps(); - public checkConfiguration() { } - public requestConfiguration(ver?: ConfigVersion) { } - public equipmentMaster = 0; - public async stopAsync() { - // turn off chlor - console.log(`Stopping sys`); - //sys.board.virtualChlorinatorController.stop(); - if (sys.controllerType === ControllerType.Nixie) this.turnOffAllCircuits(); - // sys.board.virtualChemControllers.stop(); - this.killStatusCheck(); - await ncp.closeAsync(); - // return sys.board.virtualPumpControllers.stopAsync() - } - public async turnOffAllCircuits() { - // turn off all circuits/features - for (let i = 0; i < state.circuits.length; i++) { - let s = state.circuits.getItemByIndex(i) - s.isOn = s.manualPriorityActive = false; - } - for (let i = 0; i < state.features.length; i++) { - let s = state.features.getItemByIndex(i) - s.isOn = s.manualPriorityActive = false; + protected _modulesAcquired: boolean = true; + public needsConfigChanges: boolean = false; + public valueMaps: byteValueMaps = new byteValueMaps(); + public checkConfiguration() { } + public requestConfiguration(ver?: ConfigVersion) { } + public equipmentMaster = 0; + public async stopAsync() { + // turn off chlor + console.log(`Stopping sys`); + //sys.board.virtualChlorinatorController.stop(); + if (sys.controllerType === ControllerType.Nixie) this.turnOffAllCircuits(); + // sys.board.virtualChemControllers.stop(); + this.killStatusCheck(); + await ncp.closeAsync(); + // return sys.board.virtualPumpControllers.stopAsync() } - for (let i = 0; i < state.lightGroups.length; i++) { - let s = state.lightGroups.getItemByIndex(i) - s.isOn = s.manualPriorityActive = false; - } - for (let i = 0; i < state.temps.bodies.length; i++) { - state.temps.bodies.getItemByIndex(i).isOn = false; + public async turnOffAllCircuits() { + // turn off all circuits/features + for (let i = 0; i < state.circuits.length; i++) { + let s = state.circuits.getItemByIndex(i) + s.isOn = s.manualPriorityActive = false; + } + for (let i = 0; i < state.features.length; i++) { + let s = state.features.getItemByIndex(i) + s.isOn = s.manualPriorityActive = false; + } + for (let i = 0; i < state.lightGroups.length; i++) { + let s = state.lightGroups.getItemByIndex(i) + s.isOn = s.manualPriorityActive = false; + } + for (let i = 0; i < state.temps.bodies.length; i++) { + state.temps.bodies.getItemByIndex(i).isOn = false; + } + // sys.board.virtualPumpControllers.setTargetSpeed(); + state.emitEquipmentChanges(); } - // sys.board.virtualPumpControllers.setTargetSpeed(); - state.emitEquipmentChanges(); - } - public system: SystemCommands = new SystemCommands(this); - public bodies: BodyCommands = new BodyCommands(this); - public pumps: PumpCommands = new PumpCommands(this); - public circuits: CircuitCommands = new CircuitCommands(this); - public valves: ValveCommands = new ValveCommands(this); - public features: FeatureCommands = new FeatureCommands(this); - public chlorinator: ChlorinatorCommands = new ChlorinatorCommands(this); - public heaters: HeaterCommands = new HeaterCommands(this); - public filters: FilterCommands = new FilterCommands(this); + public system: SystemCommands = new SystemCommands(this); + public bodies: BodyCommands = new BodyCommands(this); + public pumps: PumpCommands = new PumpCommands(this); + public circuits: CircuitCommands = new CircuitCommands(this); + public valves: ValveCommands = new ValveCommands(this); + public features: FeatureCommands = new FeatureCommands(this); + public chlorinator: ChlorinatorCommands = new ChlorinatorCommands(this); + public heaters: HeaterCommands = new HeaterCommands(this); + public filters: FilterCommands = new FilterCommands(this); public chemControllers: ChemControllerCommands = new ChemControllerCommands(this); public chemDosers: ChemDoserCommands = new ChemDoserCommands(this); - public schedules: ScheduleCommands = new ScheduleCommands(this); - public equipmentIds: EquipmentIds = new EquipmentIds(); - //public virtualChlorinatorController = new VirtualChlorinatorController(this); - // public virtualPumpControllers = new VirtualPumpController(this); - // public virtualChemControllers = new VirtualChemController(this); + public schedules: ScheduleCommands = new ScheduleCommands(this); + public equipmentIds: EquipmentIds = new EquipmentIds(); + //public virtualChlorinatorController = new VirtualChlorinatorController(this); + // public virtualPumpControllers = new VirtualPumpController(this); + // public virtualChemControllers = new VirtualChemController(this); - // We need this here so that we don't inadvertently start processing 2 messages before we get to a 204 in IntelliCenter. This message tells - // us all of the installed modules on the panel and the status is worthless until we know the equipment on the board. For *Touch this is always true but the - // virtual controller may need to make use of it after it looks for pumps and chlorinators. - public get modulesAcquired(): boolean { return this._modulesAcquired; } - public set modulesAcquired(value: boolean) { this._modulesAcquired = value; } - public reloadConfig() { - state.status = 0; - sys.resetData(); - this.checkConfiguration(); - } - public get commandSourceAddress(): number { return Message.pluginAddress; } - public get commandDestAddress(): number { return 16; } - public get statusInterval(): number { return this._statusInterval } - protected killStatusCheck() { - if (typeof this._statusTimer !== 'undefined' && this._statusTimer) clearTimeout(this._statusTimer); - this._statusTimer = undefined; - this._statusCheckRef = 0; - } + // We need this here so that we don't inadvertently start processing 2 messages before we get to a 204 in IntelliCenter. This message tells + // us all of the installed modules on the panel and the status is worthless until we know the equipment on the board. For *Touch this is always true but the + // virtual controller may need to make use of it after it looks for pumps and chlorinators. + public get modulesAcquired(): boolean { return this._modulesAcquired; } + public set modulesAcquired(value: boolean) { this._modulesAcquired = value; } + public reloadConfig() { + state.status = 0; + sys.resetData(); + this.checkConfiguration(); + } + public get commandSourceAddress(): number { return Message.pluginAddress; } + public get commandDestAddress(): number { return 16; } + public get statusInterval(): number { return this._statusInterval } + protected killStatusCheck() { + if (typeof this._statusTimer !== 'undefined' && this._statusTimer) clearTimeout(this._statusTimer); + this._statusTimer = undefined; + this._statusCheckRef = 0; + } public suspendStatus(bSuspend: boolean) { // The way status suspension works is by using a reference value that is incremented and decremented // the status check is only performed when the reference value is 0. So suspending the status check 3 times and un-suspending @@ -942,29 +944,31 @@ export class SystemBoard { else this._statusCheckRef = Math.max(0, this._statusCheckRef - 1); if (this._statusCheckRef > 1) logger.verbose(`Suspending status check: ${bSuspend} -- ${this._statusCheckRef}`); } - /// This method processes the status message periodically. The role of this method is to verify the circuit, valve, and heater - /// relays. This method does not control RS485 operations such as pumps and chlorinators. These are done through the respective - /// equipment polling functions. - public async processStatusAsync() { - let self = this; - try { - if (this._statusCheckRef > 0) return; - this.suspendStatus(true); - if (typeof this._statusTimer !== 'undefined' && this._statusTimer) clearTimeout(this._statusTimer); - // Go through all the assigned equipment and verify the current state. - sys.board.system.keepManualTime(); - await sys.board.bodies.syncFreezeProtection(); - await sys.board.syncEquipmentItems(); - await sys.board.schedules.syncScheduleStates(); - await sys.board.circuits.checkEggTimerExpirationAsync(); - state.emitControllerChange(); - state.emitEquipmentChanges(); - } catch (err) { state.status = 255; logger.error(`Error performing processStatusAsync ${err.message}`); } - finally { - this.suspendStatus(false); - if (this.statusInterval > 0) this._statusTimer = setTimeoutSync(async () => await self.processStatusAsync(), this.statusInterval); + /// This method processes the status message periodically. The role of this method is to verify the circuit, valve, and heater + /// relays. This method does not control RS485 operations such as pumps and chlorinators. These are done through the respective + /// equipment polling functions. + public async processStatusAsync() { + let self = this; + try { + if (this._statusCheckRef > 0) return; + this.suspendStatus(true); + if (typeof this._statusTimer !== 'undefined' && this._statusTimer) clearTimeout(this._statusTimer); + // Go through all the assigned equipment and verify the current state. + sys.board.system.keepManualTime(); + await sys.board.bodies.syncFreezeProtection(); + await sys.board.syncEquipmentItems(); + await sys.board.schedules.syncScheduleStates(); + await sys.board.circuits.checkEggTimerExpirationAsync(); + state.emitControllerChange(); + state.emitEquipmentChanges(); + // RSG 4.3.24 - suspendStatus(false) should not be in the finally because it would decrement the _statusCheckRef + // when it should be the job of the calling function (eg setCircuitStateAsync) + this.suspendStatus(false); + } catch (err) { this.suspendStatus(false); state.status = 255; logger.error(`Error performing processStatusAsync ${err.message}`); } + finally { + if (this._statusCheckRef === 0) this._statusTimer = setTimeoutSync(async () => await self.processStatusAsync(), this.statusInterval); + } } - } public async syncEquipmentItems() { try { await sys.board.circuits.syncCircuitRelayStates(); @@ -976,81 +980,81 @@ export class SystemBoard { } catch (err) { logger.error(`Error synchronizing equipment items: ${err.message}`); } } - public async setControllerType(obj): Promise { - try { - if (obj.controllerType !== sys.controllerType) - return Promise.reject(new InvalidEquipmentDataError(`You may not change the controller type data for ${sys.controllerType} controllers`, 'controllerType', obj.controllerType)); - return sys.equipment; - } catch (err) { } - } + public async setControllerType(obj): Promise { + try { + if (obj.controllerType !== sys.controllerType) + return Promise.reject(new InvalidEquipmentDataError(`You may not change the controller type data for ${sys.controllerType} controllers`, 'controllerType', obj.controllerType)); + return sys.equipment; + } catch (err) { } + } } export class ConfigRequest { - public failed: boolean = false; - public version: number = 0; // maybe not used for intellitouch - public items: number[] = []; - public acquired: number[] = []; // used? - public oncomplete: Function; - public name: string; - public category: number; - public setcategory: number; - public fillRange(start: number, end: number) { - for (let i = start; i <= end; i++) this.items.push(i); - } - public get isComplete(): boolean { - return this.items.length === 0; - } - public removeItem(byte: number) { - for (let i = this.items.length - 1; i >= 0; i--) - if (this.items[i] === byte) this.items.splice(i, 1); + public failed: boolean = false; + public version: number = 0; // maybe not used for intellitouch + public items: number[] = []; + public acquired: number[] = []; // used? + public oncomplete: Function; + public name: string; + public category: number; + public setcategory: number; + public fillRange(start: number, end: number) { + for (let i = start; i <= end; i++) this.items.push(i); + } + public get isComplete(): boolean { + return this.items.length === 0; + } + public removeItem(byte: number) { + for (let i = this.items.length - 1; i >= 0; i--) + if (this.items[i] === byte) this.items.splice(i, 1); - } + } } export class ConfigQueue { - public queue: ConfigRequest[] = []; - public curr: ConfigRequest = null; - public closed: boolean = false; - public close() { - this.closed = true; - this.queue.length = 0; - } - public reset() { - this.closed = false; - this.queue.length = 0; - this.totalItems = 0; - } - public removeItem(cat: number, itm: number) { - for (let i = this.queue.length - 1; i >= 0; i--) { - if (this.queue[i].category === cat) this.queue[i].removeItem(itm); - if (this.queue[i].isComplete) this.queue.splice(i, 1); + public queue: ConfigRequest[] = []; + public curr: ConfigRequest = null; + public closed: boolean = false; + public close() { + this.closed = true; + this.queue.length = 0; } - } - public totalItems: number = 0; - public get remainingItems(): number { - let c = this.queue.reduce((prev: number, curr: ConfigRequest): number => { - return prev += curr.items.length; - }, 0); - c = c + (this.curr ? this.curr.items.length : 0); - return c; - } - public get percent(): number { - return this.totalItems !== 0 ? - 100 - Math.round(this.remainingItems / this.totalItems * 100) : - 100; - } - public push(req: ConfigRequest) { - this.queue.push(req); - this.totalItems += req.items.length; - } - // following overridden in extended class - processNext(msg?: Outbound) { } - protected queueItems(cat: number, items?: number[]) { } - protected queueRange(cat: number, start: number, end: number) { } + public reset() { + this.closed = false; + this.queue.length = 0; + this.totalItems = 0; + } + public removeItem(cat: number, itm: number) { + for (let i = this.queue.length - 1; i >= 0; i--) { + if (this.queue[i].category === cat) this.queue[i].removeItem(itm); + if (this.queue[i].isComplete) this.queue.splice(i, 1); + } + } + public totalItems: number = 0; + public get remainingItems(): number { + let c = this.queue.reduce((prev: number, curr: ConfigRequest): number => { + return prev += curr.items.length; + }, 0); + c = c + (this.curr ? this.curr.items.length : 0); + return c; + } + public get percent(): number { + return this.totalItems !== 0 ? + 100 - Math.round(this.remainingItems / this.totalItems * 100) : + 100; + } + public push(req: ConfigRequest) { + this.queue.push(req); + this.totalItems += req.items.length; + } + // following overridden in extended class + processNext(msg?: Outbound) { } + protected queueItems(cat: number, items?: number[]) { } + protected queueRange(cat: number, start: number, end: number) { } } export class BoardCommands { - protected board: SystemBoard = null; - constructor(parent: SystemBoard) { this.board = parent; } + protected board: SystemBoard = null; + constructor(parent: SystemBoard) { this.board = parent; } } export class SystemCommands extends BoardCommands { public async restore(rest: { poolConfig: any, poolState: any }): Promise { @@ -1469,56 +1473,56 @@ export class SystemCommands extends BoardCommands { public async setPanelModeAsync(data: any): Promise { return { mode: state.mode }; } } export class BodyCommands extends BoardCommands { - public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { - try { - // First delete the bodies that should be removed. - for (let i = 0; i < ctx.bodies.remove.length; i++) { - let body = ctx.bodies.remove[i]; - try { - sys.bodies.removeItemById(body.id); - state.temps.bodies.removeItemById(body.id); - res.addModuleSuccess('body', `Remove: ${body.id}-${body.name}`); - } catch (err) { res.addModuleError('body', `Remove: ${body.id}-${body.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.bodies.update.length; i++) { - let body = ctx.bodies.update[i]; + public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { try { - await sys.board.bodies.setBodyAsync(body); - res.addModuleSuccess('body', `Update: ${body.id}-${body.name}`); - } catch (err) { res.addModuleError('body', `Update: ${body.id}-${body.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.bodies.add.length; i++) { - let body = ctx.bodies.add[i]; + // First delete the bodies that should be removed. + for (let i = 0; i < ctx.bodies.remove.length; i++) { + let body = ctx.bodies.remove[i]; + try { + sys.bodies.removeItemById(body.id); + state.temps.bodies.removeItemById(body.id); + res.addModuleSuccess('body', `Remove: ${body.id}-${body.name}`); + } catch (err) { res.addModuleError('body', `Remove: ${body.id}-${body.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.bodies.update.length; i++) { + let body = ctx.bodies.update[i]; + try { + await sys.board.bodies.setBodyAsync(body); + res.addModuleSuccess('body', `Update: ${body.id}-${body.name}`); + } catch (err) { res.addModuleError('body', `Update: ${body.id}-${body.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.bodies.add.length; i++) { + let body = ctx.bodies.add[i]; + try { + // pull a little trick to first add the data then perform the update. + sys.bodies.getItemById(body.id, true); + await sys.board.bodies.setBodyAsync(body); + } catch (err) { res.addModuleError('body', `Add: ${body.id}-${body.name}: ${err.message}`); } + } + return true; + } catch (err) { logger.error(`Error restoring bodies: ${err.message}`); res.addModuleError('system', `Error restoring bodies: ${err.message}`); return false; } + } + public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> { try { - // pull a little trick to first add the data then perform the update. - sys.bodies.getItemById(body.id, true); - await sys.board.bodies.setBodyAsync(body); - } catch (err) { res.addModuleError('body', `Add: ${body.id}-${body.name}: ${err.message}`); } - } - return true; - } catch (err) { logger.error(`Error restoring bodies: ${err.message}`); res.addModuleError('system', `Error restoring bodies: ${err.message}`); return false; } - } - public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any}> { - try { - let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; - // Look at bodies. - let cfg = rest.poolConfig; - for (let i = 0; i < cfg.bodies.length; i++) { - let r = cfg.bodies[i]; - let c = sys.bodies.find(elem => r.id === elem.id); - if (typeof c === 'undefined') ctx.add.push(r); - else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); - } - for (let i = 0; i < sys.bodies.length; i++) { - let c = sys.bodies.getItemByIndex(i); - let r = cfg.bodies.find(elem => elem.id == c.id); - if (typeof r === 'undefined') ctx.remove.push(c.get(true)); - } - return ctx; - } catch (err) { logger.error(`Error validating bodies for restore: ${err.message}`); } - } - public freezeProtectBodyOn: Date; - public freezeProtectStart: Date; + let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; + // Look at bodies. + let cfg = rest.poolConfig; + for (let i = 0; i < cfg.bodies.length; i++) { + let r = cfg.bodies[i]; + let c = sys.bodies.find(elem => r.id === elem.id); + if (typeof c === 'undefined') ctx.add.push(r); + else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); + } + for (let i = 0; i < sys.bodies.length; i++) { + let c = sys.bodies.getItemByIndex(i); + let r = cfg.bodies.find(elem => elem.id == c.id); + if (typeof r === 'undefined') ctx.remove.push(c.get(true)); + } + return ctx; + } catch (err) { logger.error(`Error validating bodies for restore: ${err.message}`); } + } + public freezeProtectBodyOn: Date; + public freezeProtectStart: Date; public async syncFreezeProtection() { try { // Go through all the features and circuits to make sure we have the freeze protect set appropriately. The freeze @@ -1639,145 +1643,147 @@ export class BodyCommands extends BoardCommands { catch (err) { logger.error(`syncFreezeProtection: Error synchronizing freeze protection states: ${err.message}`); } } - public async initFilters() { - try { - let filter: Filter; - let sFilter: FilterState; - if (sys.equipment.maxBodies > 0) { - filter = sys.filters.getItemById(1, true, { filterType: 3, name: sys.equipment.shared ? 'Filter' : 'Filter 1' }); - sFilter = state.filters.getItemById(1, true, { id: 1, name: filter.name }); - filter.isActive = true; - filter.master = sys.board.equipmentMaster; - filter.body = sys.equipment.shared ? sys.board.valueMaps.bodies.transformByName('poolspa') : 0; - //sFilter = state.filters.getItemById(1, true); - sFilter.body = filter.body; - sFilter.filterType = filter.filterType; - sFilter.name = filter.name; - if (sys.equipment.dual) { - filter = sys.filters.getItemById(2, true, { filterType: 3, name: 'Filter 2' }); - filter.isActive = true; - filter.master = sys.board.equipmentMaster; - filter.body = 1; - sFilter = state.filters.getItemById(2, true); - sFilter.body = filter.body; - sFilter.filterType = filter.filterType; - sFilter.name = filter.name; + public async initFilters() { + try { + let filter: Filter; + let sFilter: FilterState; + if (sys.equipment.maxBodies > 0) { + filter = sys.filters.getItemById(1, true, { filterType: 3, name: sys.equipment.shared ? 'Filter' : 'Filter 1' }); + sFilter = state.filters.getItemById(1, true, { id: 1, name: filter.name }); + filter.isActive = true; + filter.master = sys.board.equipmentMaster; + filter.body = sys.equipment.shared ? sys.board.valueMaps.bodies.transformByName('poolspa') : 0; + //sFilter = state.filters.getItemById(1, true); + sFilter.body = filter.body; + sFilter.filterType = filter.filterType; + sFilter.name = filter.name; + if (sys.equipment.dual) { + filter = sys.filters.getItemById(2, true, { filterType: 3, name: 'Filter 2' }); + filter.isActive = true; + filter.master = sys.board.equipmentMaster; + filter.body = 1; + sFilter = state.filters.getItemById(2, true); + sFilter.body = filter.body; + sFilter.filterType = filter.filterType; + sFilter.name = filter.name; + } + else { + sys.filters.removeItemById(2); + state.filters.removeItemById(2); + } + } + else { + sys.filters.removeItemById(1); + state.filters.removeItemById(1); + sys.filters.removeItemById(2); + state.filters.removeItemById(2); + } + } catch (err) { logger.error(`Error initializing filters`); } + } + public async setBodyAsync(obj: any): Promise { + return new Promise(function (resolve, reject) { + let id = parseInt(obj.id, 10); 1 + if (isNaN(id)) reject(new InvalidEquipmentIdError('Body Id has not been defined', obj.id, 'Body')); + let body = sys.bodies.getItemById(id, false); + let sbody = state.temps.bodies.getItemById(id, false); + body.set(obj); + sbody.name = body.name; + sbody.showInDashboard = body.showInDashboard; + resolve(body); + }); + } + public mapBodyAssociation(val: any): any { + if (typeof val === 'undefined') return; + let ass = sys.board.bodies.getBodyAssociations(); + let nval = parseInt(val, 10); + if (!isNaN(nval)) { + return ass.find(elem => elem.val === nval); } - else { - sys.filters.removeItemById(2); - state.filters.removeItemById(2); + else if (typeof val === 'string') return ass.find(elem => elem.name === val); + else if (typeof val.val !== 'undefined') { + nval = parseInt(val.val); + return ass.find(elem => elem.val === val) !== undefined; } - } - else { - sys.filters.removeItemById(1); - state.filters.removeItemById(1); - sys.filters.removeItemById(2); - state.filters.removeItemById(2); - } - } catch (err) { logger.error(`Error initializing filters`); } - } - public async setBodyAsync(obj: any): Promise { - return new Promise(function (resolve, reject) { - let id = parseInt(obj.id, 10); 1 - if (isNaN(id)) reject(new InvalidEquipmentIdError('Body Id has not been defined', obj.id, 'Body')); - let body = sys.bodies.getItemById(id, false); - let sbody = state.temps.bodies.getItemById(id, false); - body.set(obj); - sbody.name = body.name; - sbody.showInDashboard = body.showInDashboard; - resolve(body); - }); - } - public mapBodyAssociation(val: any): any { - if (typeof val === 'undefined') return; - let ass = sys.board.bodies.getBodyAssociations(); - let nval = parseInt(val, 10); - if (!isNaN(nval)) { - return ass.find(elem => elem.val === nval); - } - else if (typeof val === 'string') return ass.find(elem => elem.name === val); - else if (typeof val.val !== 'undefined') { - nval = parseInt(val.val); - return ass.find(elem => elem.val === val) !== undefined; - } - else if (typeof val.name !== 'undefined') return ass.find(elem => elem.name === val.name); - } - // This method provides a list of enumerated values for configuring associations - // tied to the current configuration. It is used to supply only the valid values - // for tying things like heaters, chem controllers, ss & ds pumps to a particular body within - // the plumbing. - public getBodyAssociations() { - let ass = []; - let assoc = sys.board.valueMaps.bodies.toArray(); - for (let i = 0; i < assoc.length; i++) { - let body; - let code = assoc[i]; - switch (code.name) { - case 'body1': - case 'pool': - body = sys.bodies.getItemById(1); - code.desc = body.name; - ass.push(code); - break; - case 'body2': - case 'spa': - if (sys.equipment.maxBodies >= 2) { - body = sys.bodies.getItemById(2); - code.desc = body.name; - ass.push(code); - } - break; - case 'body3': - if (sys.equipment.maxBodies >= 3) { - body = sys.bodies.getItemById(3); - code.desc = body.name; - ass.push(code); - } - break; - case 'body4': - if (sys.equipment.maxBodies >= 4) { - body = sys.bodies.getItemById(3); - code.desc = body.name; - ass.push(code); - } - break; - case 'poolspa': - if (sys.equipment.shared && sys.equipment.maxBodies >= 2) { - body = sys.bodies.getItemById(1); - let body2 = sys.bodies.getItemById(2); - code.desc = `${body.name}/${body2.name}`; - ass.push(code); - } - break; - } - } - return ass; - } - public async setHeatModeAsync(body: Body, mode: number): Promise { - let bdy = sys.bodies.getItemById(body.id); - let bstate = state.temps.bodies.getItemById(body.id); - bdy.heatMode = bstate.heatMode = mode; - sys.board.heaters.syncHeaterStates(); - state.emitEquipmentChanges(); - return Promise.resolve(bstate); - } - public async setHeatSetpointAsync(body: Body, setPoint: number): Promise { - let bdy = sys.bodies.getItemById(body.id); - let bstate = state.temps.bodies.getItemById(body.id); - bdy.setPoint = bstate.setPoint = setPoint; - state.emitEquipmentChanges(); - sys.board.heaters.syncHeaterStates(); - return Promise.resolve(bstate); - } - public async setCoolSetpointAsync(body: Body, setPoint: number): Promise { - let bdy = sys.bodies.getItemById(body.id); - let bstate = state.temps.bodies.getItemById(body.id); - bdy.coolSetpoint = bstate.coolSetpoint = setPoint; - state.emitEquipmentChanges(); - sys.board.heaters.syncHeaterStates(); - return Promise.resolve(bstate); - } + else if (typeof val.name !== 'undefined') return ass.find(elem => elem.name === val.name); + } + // This method provides a list of enumerated values for configuring associations + // tied to the current configuration. It is used to supply only the valid values + // for tying things like heaters, chem controllers, ss & ds pumps to a particular body within + // the plumbing. + public getBodyAssociations() { + let ass = []; + let assoc = sys.board.valueMaps.bodies.toArray(); + for (let i = 0; i < assoc.length; i++) { + let body; + let code = assoc[i]; + switch (code.name) { + case 'body1': + case 'pool': + body = sys.bodies.getItemById(1); + code.desc = body.name; + ass.push(code); + break; + case 'body2': + case 'spa': + if (sys.equipment.maxBodies >= 2) { + body = sys.bodies.getItemById(2); + code.desc = body.name; + ass.push(code); + } + break; + case 'body3': + if (sys.equipment.maxBodies >= 3) { + body = sys.bodies.getItemById(3); + code.desc = body.name; + ass.push(code); + } + break; + case 'body4': + if (sys.equipment.maxBodies >= 4) { + body = sys.bodies.getItemById(3); + code.desc = body.name; + ass.push(code); + } + break; + case 'poolspa': + if (sys.equipment.shared && sys.equipment.maxBodies >= 2) { + body = sys.bodies.getItemById(1); + let body2 = sys.bodies.getItemById(2); + code.desc = `${body.name}/${body2.name}`; + ass.push(code); + } + break; + } + } + return ass; + } + public async setHeatModeAsync(body: Body, mode: number): Promise { + let bdy = sys.bodies.getItemById(body.id); + let bstate = state.temps.bodies.getItemById(body.id); + bdy.heatMode = bstate.heatMode = mode; + sys.board.heaters.clearPrevHeaterOffTemp(); + sys.board.heaters.syncHeaterStates(); + state.emitEquipmentChanges(); + return Promise.resolve(bstate); + } + public async setHeatSetpointAsync(body: Body, setPoint: number): Promise { + let bdy = sys.bodies.getItemById(body.id); + let bstate = state.temps.bodies.getItemById(body.id); + bdy.setPoint = bstate.setPoint = setPoint; + sys.board.heaters.clearPrevHeaterOffTemp(); + state.emitEquipmentChanges(); + sys.board.heaters.syncHeaterStates(); + return Promise.resolve(bstate); + } + public async setCoolSetpointAsync(body: Body, setPoint: number): Promise { + let bdy = sys.bodies.getItemById(body.id); + let bstate = state.temps.bodies.getItemById(body.id); + bdy.coolSetpoint = bstate.coolSetpoint = setPoint; + state.emitEquipmentChanges(); + sys.board.heaters.syncHeaterStates(); + return Promise.resolve(bstate); + } public getHeatSources(bodyId: number) { let heatSources = []; let heatTypes = this.board.heaters.getInstalledHeaterTypes(bodyId); @@ -1845,72 +1851,93 @@ export class BodyCommands extends BoardCommands { } return heatModes; } - public getPoolStates(): BodyTempState[] { - let arrPools = []; - for (let i = 0; i < state.temps.bodies.length; i++) { - let bstate = state.temps.bodies.getItemByIndex(i); - if (bstate.circuit === 6) - arrPools.push(bstate); + public getPoolStates(): BodyTempState[] { + let arrPools = []; + for (let i = 0; i < state.temps.bodies.length; i++) { + let bstate = state.temps.bodies.getItemByIndex(i); + if (bstate.circuit === 6) + arrPools.push(bstate); + } + return arrPools; } - return arrPools; - } - public getSpaStates(): BodyTempState[] { - let arrSpas = []; - for (let i = 0; i < state.temps.bodies.length; i++) { - let bstate = state.temps.bodies.getItemByIndex(i); - if (bstate.circuit === 1) { - arrSpas.push(bstate); - } - } - return arrSpas; - } - public getBodyState(bodyCode: number): BodyTempState { - let assoc = sys.board.valueMaps.bodies.transform(bodyCode); - switch (assoc.name) { - case 'body1': - case 'pool': - return state.temps.bodies.getItemById(1); - case 'body2': - case 'spa': - return state.temps.bodies.getItemById(2); - case 'body3': - return state.temps.bodies.getItemById(3); - case 'body4': - return state.temps.bodies.getItemById(4); - case 'poolspa': - if (sys.equipment.shared && sys.equipment.maxBodies >= 2) { - let body = state.temps.bodies.getItemById(1); - if (body.isOn) return body; - body = state.temps.bodies.getItemById(2); - if (body.isOn) return body; - return state.temps.bodies.getItemById(1); + public getSpaStates(): BodyTempState[] { + let arrSpas = []; + for (let i = 0; i < state.temps.bodies.length; i++) { + let bstate = state.temps.bodies.getItemByIndex(i); + if (bstate.circuit === 1) { + arrSpas.push(bstate); + } } - else - return state.temps.bodies.getItemById(1); + return arrSpas; } - } - public isBodyOn(bodyCode: number): boolean { - let assoc = sys.board.valueMaps.bodies.transform(bodyCode); - switch (assoc.name) { - case 'body1': - case 'pool': - return state.temps.bodies.getItemById(1).isOn; - case 'body2': - case 'spa': - return state.temps.bodies.getItemById(2).isOn; - case 'body3': - return state.temps.bodies.getItemById(3).isOn; - case 'body4': - return state.temps.bodies.getItemById(4).isOn; - case 'poolspa': - if (sys.equipment.shared && sys.equipment.maxBodies >= 2) { - return state.temps.bodies.getItemById(1).isOn === true || state.temps.bodies.getItemById(2).isOn === true; + public getBodyState(bodyCode: number): BodyTempState { + let assoc = sys.board.valueMaps.bodies.transform(bodyCode); + switch (assoc.name) { + case 'body1': + case 'pool': + return state.temps.bodies.getItemById(1); + case 'body2': + case 'spa': + return state.temps.bodies.getItemById(2); + case 'body3': + return state.temps.bodies.getItemById(3); + case 'body4': + return state.temps.bodies.getItemById(4); + case 'poolspa': + if (sys.equipment.shared && sys.equipment.maxBodies >= 2) { + let body = state.temps.bodies.getItemById(1); + if (body.isOn) return body; + body = state.temps.bodies.getItemById(2); + if (body.isOn) return body; + return state.temps.bodies.getItemById(1); + } + else + return state.temps.bodies.getItemById(1); } - else - return state.temps.bodies.getItemById(1).isOn; } - return false; - } + public isBodyOn(bodyCode: number): boolean { + let assoc = sys.board.valueMaps.bodies.transform(bodyCode); + switch (assoc.name) { + case 'body1': + case 'pool': + return state.temps.bodies.getItemById(1).isOn; + case 'body2': + case 'spa': + return state.temps.bodies.getItemById(2).isOn; + case 'body3': + return state.temps.bodies.getItemById(3).isOn; + case 'body4': + return state.temps.bodies.getItemById(4).isOn; + case 'poolspa': + if (sys.equipment.shared && sys.equipment.maxBodies >= 2) { + return state.temps.bodies.getItemById(1).isOn === true || state.temps.bodies.getItemById(2).isOn === true; + } + else + return state.temps.bodies.getItemById(1).isOn; + } + return false; + } + public getActiveBody(bodyCode: number): number { + let assoc = sys.board.valueMaps.bodies.transform(bodyCode); + switch (assoc.name) { + case 'body1': + case 'pool': + return 1; + case 'body2': + case 'spa': + return 2; + case 'body3': + return 3; + case 'body4': + return 4; + case 'poolspa': + if (sys.equipment.shared && sys.equipment.maxBodies >= 2) { + return state.temps.bodies.getItemById(2).isOn ? 2 : 1; + } + else return 1; // Always default to pool. + } + return 0; + } } export class PumpCommands extends BoardCommands { public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { @@ -1937,7 +1964,7 @@ export class PumpCommands extends BoardCommands { try { // pull a little trick to first add the data then perform the update. This way we won't get a new id or // it won't error out. - sys.pumps.getItemById(p.id, true, {id: parseInt(p.id, 10), type: parseInt(p.type, 10) }); + sys.pumps.getItemById(p.id, true, { id: parseInt(p.id, 10), type: parseInt(p.type, 10) }); await sys.board.pumps.setPumpAsync(p); res.addModuleSuccess('pump', `Add: ${p.id}-${p.name}`); } catch (err) { res.addModuleError('pump', `Add: ${p.id}-${p.name}: ${err.message}`); } @@ -1965,261 +1992,261 @@ export class PumpCommands extends BoardCommands { } catch (err) { logger.error(`Error validating pumps for restore: ${err.message}`); } } - public getPumpTypes() { return this.board.valueMaps.pumpTypes.toArray(); } - public getCircuitUnits(pump?: Pump) { - if (typeof pump === 'undefined') - return this.board.valueMaps.pumpUnits.toArray(); - else { - let pumpType = sys.board.valueMaps.pumpTypes.getName(pump.type); - let val; - if (pumpType.includes('vsf')) val = this.board.valueMaps.pumpUnits.toArray(); - else if (pumpType.includes('vs')) val = this.board.valueMaps.pumpUnits.getValue('rpm'); - else if (pumpType.includes('vf')) val = this.board.valueMaps.pumpUnits.getValue('gpm'); - else return {}; - return this.board.valueMaps.pumpUnits.transform(val); + public getPumpTypes() { return this.board.valueMaps.pumpTypes.toArray(); } + public getCircuitUnits(pump?: Pump) { + if (typeof pump === 'undefined') + return this.board.valueMaps.pumpUnits.toArray(); + else { + let pumpType = sys.board.valueMaps.pumpTypes.getName(pump.type); + let val; + if (pumpType.includes('vsf')) val = this.board.valueMaps.pumpUnits.toArray(); + else if (pumpType.includes('vs')) val = this.board.valueMaps.pumpUnits.getValue('rpm'); + else if (pumpType.includes('vf')) val = this.board.valueMaps.pumpUnits.getValue('gpm'); + else return {}; + return this.board.valueMaps.pumpUnits.transform(val); + } } - } - public async setPumpAsync(data: any, send: boolean = true): Promise { - try { - let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); - if (id <= 0) id = sys.pumps.filter(elem => elem.master === 1).getMaxId(false, 49) + 1; - data.id = id; - if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid pump id: ${data.id}`, data.id, 'Pump')); - let pump = sys.pumps.getItemById(id, true); - await ncp.pumps.setPumpAsync(pump, data); - let spump = state.pumps.getItemById(id, true); - spump.emitData('pumpExt', spump.getExtended()); - spump.emitEquipmentChange(); - return pump; - } - catch (err) { - logger.error(`Error setting pump: ${err}`); - return Promise.reject(err); + public async setPumpAsync(data: any, send: boolean = true): Promise { + try { + let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); + if (id <= 0) id = sys.pumps.filter(elem => elem.master === 1).getMaxId(false, 49) + 1; + data.id = id; + if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid pump id: ${data.id}`, data.id, 'Pump')); + let pump = sys.pumps.getItemById(id, true); + await ncp.pumps.setPumpAsync(pump, data); + let spump = state.pumps.getItemById(id, true); + spump.emitData('pumpExt', spump.getExtended()); + spump.emitEquipmentChange(); + return pump; + } + catch (err) { + logger.error(`Error setting pump: ${err}`); + return Promise.reject(err); + } } - } - public async deletePumpAsync(data: any): Promise { - if (typeof data.id !== 'undefined') { - try { - let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); - if (isNaN(id) || id <= 0) return Promise.reject(new InvalidEquipmentIdError(`Invalid pump id: ${data.id}`, data.id, 'Pump')); - let pump = sys.pumps.getItemById(id, false); - let spump = state.pumps.getItemById(id, false); - await ncp.pumps.deletePumpAsync(pump.id); - spump.isActive = pump.isActive = false; - sys.pumps.removeItemById(id); - state.pumps.removeItemById(id); - spump.emitEquipmentChange(); - return Promise.resolve(pump); - } - catch (err) { - return Promise.reject(err); - } - } - else - return Promise.reject(new InvalidEquipmentIdError('No pump information provided', undefined, 'Pump')); - } - public deletePumpCircuit(pump: Pump, pumpCircuitId: number) { - pump.circuits.removeItemById(pumpCircuitId); - let spump = state.pumps.getItemById(pump.id); - spump.emitData('pumpExt', spump.getExtended()); - } - public availableCircuits() { - let _availCircuits = []; - for (let i = 0; i < sys.circuits.length; i++) { - let circ = sys.circuits.getItemByIndex(i); - if (circ.isActive) _availCircuits.push({ type: 'circuit', id: circ.id, name: circ.name }); - } - for (let i = 0; i < sys.features.length; i++) { - let circ = sys.features.getItemByIndex(i); - if (circ.isActive) _availCircuits.push({ type: 'feature', id: circ.id, name: circ.name }); - } - let arrCircuits = sys.board.valueMaps.virtualCircuits.toArray(); - for (let i = 0; i < arrCircuits.length; i++) { - let vc = arrCircuits[i]; - switch (vc.name) { - case 'poolHeater': - case 'spaHeater': - case 'freeze': - case 'poolSpa': - case 'solarHeat': - case 'solar': - case 'heater': - _availCircuits.push({ type: 'virtual', id: vc.val, name: vc.desc }); - } - } - // what is "not used" on Intellicenter? Hardcoded for *Touch for now. - _availCircuits.push({ type: 'none', id: 255, name: 'Remove' }); - return _availCircuits; - } - public setPumpValveDelays(circuitIds: number[], delay?: number) {} + public async deletePumpAsync(data: any): Promise { + if (typeof data.id !== 'undefined') { + try { + let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); + if (isNaN(id) || id <= 0) return Promise.reject(new InvalidEquipmentIdError(`Invalid pump id: ${data.id}`, data.id, 'Pump')); + let pump = sys.pumps.getItemById(id, false); + let spump = state.pumps.getItemById(id, false); + await ncp.pumps.deletePumpAsync(pump.id); + spump.isActive = pump.isActive = false; + sys.pumps.removeItemById(id); + state.pumps.removeItemById(id); + spump.emitEquipmentChange(); + return Promise.resolve(pump); + } + catch (err) { + return Promise.reject(err); + } + } + else + return Promise.reject(new InvalidEquipmentIdError('No pump information provided', undefined, 'Pump')); + } + public deletePumpCircuit(pump: Pump, pumpCircuitId: number) { + pump.circuits.removeItemById(pumpCircuitId); + let spump = state.pumps.getItemById(pump.id); + spump.emitData('pumpExt', spump.getExtended()); + } + public availableCircuits() { + let _availCircuits = []; + for (let i = 0; i < sys.circuits.length; i++) { + let circ = sys.circuits.getItemByIndex(i); + if (circ.isActive) _availCircuits.push({ type: 'circuit', id: circ.id, name: circ.name }); + } + for (let i = 0; i < sys.features.length; i++) { + let circ = sys.features.getItemByIndex(i); + if (circ.isActive) _availCircuits.push({ type: 'feature', id: circ.id, name: circ.name }); + } + let arrCircuits = sys.board.valueMaps.virtualCircuits.toArray(); + for (let i = 0; i < arrCircuits.length; i++) { + let vc = arrCircuits[i]; + switch (vc.name) { + case 'poolHeater': + case 'spaHeater': + case 'freeze': + case 'poolSpa': + case 'solarHeat': + case 'solar': + case 'heater': + _availCircuits.push({ type: 'virtual', id: vc.val, name: vc.desc }); + } + } + // what is "not used" on Intellicenter? Hardcoded for *Touch for now. + _availCircuits.push({ type: 'none', id: 255, name: 'Remove' }); + return _availCircuits; + } + public setPumpValveDelays(circuitIds: number[], delay?: number) { } } export class CircuitCommands extends BoardCommands { - public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { - try { - // First delete the circuit/lightGroups that should be removed. - for (let i = 0; i < ctx.circuitGroups.remove.length; i++) { - let c = ctx.circuitGroups.remove[i]; - try { - await sys.board.circuits.deleteCircuitGroupAsync(c); - res.addModuleSuccess('circuitGroup', `Remove: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('circuitGroup', `Remove: ${c.id}-${c.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.lightGroups.remove.length; i++) { - let c = ctx.lightGroups.remove[i]; - try { - await sys.board.circuits.deleteLightGroupAsync(c); - res.addModuleSuccess('lightGroup', `Remove: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('lightGroup', `Remove: ${c.id}-${c.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.circuits.remove.length; i++) { - let c = ctx.circuits.remove[i]; - try { - await sys.board.circuits.deleteCircuitAsync(c); - res.addModuleSuccess('circuit', `Remove: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('circuit', `Remove: ${c.id}-${c.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.circuits.add.length; i++) { - let c = ctx.circuits.add[i]; - try { - await sys.board.circuits.setCircuitAsync(c); - res.addModuleSuccess('circuit', `Add: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('circuit', `Add: ${c.id}-${c.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.circuitGroups.add.length; i++) { - let c = ctx.circuitGroups.add[i]; - try { - await sys.board.circuits.setCircuitGroupAsync(c); - res.addModuleSuccess('circuitGroup', `Add: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('circuitGroup', `Add: ${c.id}-${c.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.lightGroups.add.length; i++) { - let c = ctx.lightGroups.add[i]; + public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { try { - await sys.board.circuits.setLightGroupAsync(c); - res.addModuleSuccess('lightGroup', `Add: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('lightGroup', `Add: ${c.id}-${c.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.circuits.update.length; i++) { - let c = ctx.circuits.update[i]; + // First delete the circuit/lightGroups that should be removed. + for (let i = 0; i < ctx.circuitGroups.remove.length; i++) { + let c = ctx.circuitGroups.remove[i]; + try { + await sys.board.circuits.deleteCircuitGroupAsync(c); + res.addModuleSuccess('circuitGroup', `Remove: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('circuitGroup', `Remove: ${c.id}-${c.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.lightGroups.remove.length; i++) { + let c = ctx.lightGroups.remove[i]; + try { + await sys.board.circuits.deleteLightGroupAsync(c); + res.addModuleSuccess('lightGroup', `Remove: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('lightGroup', `Remove: ${c.id}-${c.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.circuits.remove.length; i++) { + let c = ctx.circuits.remove[i]; + try { + await sys.board.circuits.deleteCircuitAsync(c); + res.addModuleSuccess('circuit', `Remove: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('circuit', `Remove: ${c.id}-${c.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.circuits.add.length; i++) { + let c = ctx.circuits.add[i]; + try { + await sys.board.circuits.setCircuitAsync(c); + res.addModuleSuccess('circuit', `Add: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('circuit', `Add: ${c.id}-${c.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.circuitGroups.add.length; i++) { + let c = ctx.circuitGroups.add[i]; + try { + await sys.board.circuits.setCircuitGroupAsync(c); + res.addModuleSuccess('circuitGroup', `Add: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('circuitGroup', `Add: ${c.id}-${c.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.lightGroups.add.length; i++) { + let c = ctx.lightGroups.add[i]; + try { + await sys.board.circuits.setLightGroupAsync(c); + res.addModuleSuccess('lightGroup', `Add: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('lightGroup', `Add: ${c.id}-${c.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.circuits.update.length; i++) { + let c = ctx.circuits.update[i]; + try { + await sys.board.circuits.setCircuitAsync(c); + res.addModuleSuccess('circuit', `Update: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('circuit', `Update: ${c.id}-${c.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.circuitGroups.update.length; i++) { + let c = ctx.circuitGroups.update[i]; + try { + await sys.board.circuits.setCircuitGroupAsync(c); + res.addModuleSuccess('circuitGroup', `Update: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('circuitGroup', `Update: ${c.id}-${c.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.lightGroups.update.length; i++) { + let c = ctx.lightGroups.update[i]; + try { + await sys.board.circuits.setLightGroupAsync(c); + res.addModuleSuccess('lightGroup', `Update: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('lightGroup', `Update: ${c.id}-${c.name}: ${err.message}`); } + } + return true; + } catch (err) { logger.error(`Error restoring circuits: ${err.message}`); res.addModuleError('system', `Error restoring circuits/features: ${err.message}`); return false; } + } + public async validateRestore(rest: { poolConfig: any, poolState: any }, ctxRoot): Promise { try { - await sys.board.circuits.setCircuitAsync(c); - res.addModuleSuccess('circuit', `Update: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('circuit', `Update: ${c.id}-${c.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.circuitGroups.update.length; i++) { - let c = ctx.circuitGroups.update[i]; + let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; + // Look at circuits. + let cfg = rest.poolConfig; + for (let i = 0; i < cfg.circuits.length; i++) { + let r = cfg.circuits[i]; + let c = sys.circuits.find(elem => r.id === elem.id); + if (typeof c === 'undefined') ctx.add.push(r); + else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); + } + for (let i = 0; i < sys.circuits.length; i++) { + let c = sys.circuits.getItemByIndex(i); + let r = cfg.circuits.find(elem => elem.id == c.id); + if (typeof r === 'undefined') ctx.remove.push(c.get(true)); + } + ctxRoot.circuits = ctx; + ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; + for (let i = 0; i < cfg.circuitGroups.length; i++) { + let r = cfg.circuitGroups[i]; + let c = sys.circuitGroups.find(elem => r.id === elem.id); + if (typeof c === 'undefined') ctx.add.push(r); + else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); + } + for (let i = 0; i < sys.circuitGroups.length; i++) { + let c = sys.circuitGroups.getItemByIndex(i); + let r = cfg.circuitGroups.find(elem => elem.id == c.id); + if (typeof r === 'undefined') ctx.remove.push(c.get(true)); + } + ctxRoot.circuitGroups = ctx; + ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; + for (let i = 0; i < cfg.lightGroups.length; i++) { + let r = cfg.lightGroups[i]; + let c = sys.lightGroups.find(elem => r.id === elem.id); + if (typeof c === 'undefined') ctx.add.push(r); + else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); + } + for (let i = 0; i < sys.lightGroups.length; i++) { + let c = sys.lightGroups.getItemByIndex(i); + let r = cfg.lightGroups.find(elem => elem.id == c.id); + if (typeof r === 'undefined') ctx.remove.push(c.get(true)); + } + ctxRoot.lightGroups = ctx; + return true; + } catch (err) { logger.error(`Error validating circuits for restore: ${err.message}`); } + } + public async checkEggTimerExpirationAsync() { + // turn off any circuits that have reached their egg timer; + // Nixie circuits we have 100% control over; + // but features/cg/lg may override OCP control try { - await sys.board.circuits.setCircuitGroupAsync(c); - res.addModuleSuccess('circuitGroup', `Update: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('circuitGroup', `Update: ${c.id}-${c.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.lightGroups.update.length; i++) { - let c = ctx.lightGroups.update[i]; + for (let i = 0; i < sys.circuits.length; i++) { + let c = sys.circuits.getItemByIndex(i); + let cstate = state.circuits.getItemByIndex(i); + if (!cstate.isActive || !cstate.isOn || typeof cstate.endTime === 'undefined') continue; + if (c.master === 1) { + await ncp.circuits.checkCircuitEggTimerExpirationAsync(cstate); + } + } + for (let i = 0; i < sys.features.length; i++) { + let fstate = state.features.getItemByIndex(i); + if (!fstate.isActive || !fstate.isOn || typeof fstate.endTime === 'undefined') continue; + if (fstate.endTime.toDate() < new Timestamp().toDate()) { + await sys.board.circuits.setCircuitStateAsync(fstate.id, false); + fstate.emitEquipmentChange(); + } + } + for (let i = 0; i < sys.circuitGroups.length; i++) { + let cgstate = state.circuitGroups.getItemByIndex(i); + if (!cgstate.isActive || !cgstate.isOn || typeof cgstate.endTime === 'undefined') continue; + if (cgstate.endTime.toDate() < new Timestamp().toDate()) { + await sys.board.circuits.setCircuitGroupStateAsync(cgstate.id, false); + cgstate.emitEquipmentChange(); + } + } + for (let i = 0; i < sys.lightGroups.length; i++) { + let lgstate = state.lightGroups.getItemByIndex(i); + if (!lgstate.isActive || !lgstate.isOn || typeof lgstate.endTime === 'undefined') continue; + if (lgstate.endTime.toDate() < new Timestamp().toDate()) { + await sys.board.circuits.setLightGroupStateAsync(lgstate.id, false); + lgstate.emitEquipmentChange(); + } + } + } catch (err) { logger.error(`checkEggTimerExpiration: Error synchronizing circuit relays ${err.message}`); } + } + public async syncCircuitRelayStates() { try { - await sys.board.circuits.setLightGroupAsync(c); - res.addModuleSuccess('lightGroup', `Update: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('lightGroup', `Update: ${c.id}-${c.name}: ${err.message}`); } - } - return true; - } catch (err) { logger.error(`Error restoring circuits: ${err.message}`); res.addModuleError('system', `Error restoring circuits/features: ${err.message}`); return false; } - } - public async validateRestore(rest: { poolConfig: any, poolState: any }, ctxRoot): Promise { - try { - let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; - // Look at circuits. - let cfg = rest.poolConfig; - for (let i = 0; i < cfg.circuits.length; i++) { - let r = cfg.circuits[i]; - let c = sys.circuits.find(elem => r.id === elem.id); - if (typeof c === 'undefined') ctx.add.push(r); - else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); - } - for (let i = 0; i < sys.circuits.length; i++) { - let c = sys.circuits.getItemByIndex(i); - let r = cfg.circuits.find(elem => elem.id == c.id); - if (typeof r === 'undefined') ctx.remove.push(c.get(true)); - } - ctxRoot.circuits = ctx; - ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; - for (let i = 0; i < cfg.circuitGroups.length; i++) { - let r = cfg.circuitGroups[i]; - let c = sys.circuitGroups.find(elem => r.id === elem.id); - if (typeof c === 'undefined') ctx.add.push(r); - else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); - } - for (let i = 0; i < sys.circuitGroups.length; i++) { - let c = sys.circuitGroups.getItemByIndex(i); - let r = cfg.circuitGroups.find(elem => elem.id == c.id); - if (typeof r === 'undefined') ctx.remove.push(c.get(true)); - } - ctxRoot.circuitGroups = ctx; - ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; - for (let i = 0; i < cfg.lightGroups.length; i++) { - let r = cfg.lightGroups[i]; - let c = sys.lightGroups.find(elem => r.id === elem.id); - if (typeof c === 'undefined') ctx.add.push(r); - else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); - } - for (let i = 0; i < sys.lightGroups.length; i++) { - let c = sys.lightGroups.getItemByIndex(i); - let r = cfg.lightGroups.find(elem => elem.id == c.id); - if (typeof r === 'undefined') ctx.remove.push(c.get(true)); - } - ctxRoot.lightGroups = ctx; - return true; - } catch (err) { logger.error(`Error validating circuits for restore: ${err.message}`); } - } - public async checkEggTimerExpirationAsync() { - // turn off any circuits that have reached their egg timer; - // Nixie circuits we have 100% control over; - // but features/cg/lg may override OCP control - try { - for (let i = 0; i < sys.circuits.length; i++) { - let c = sys.circuits.getItemByIndex(i); - let cstate = state.circuits.getItemByIndex(i); - if (!cstate.isActive || !cstate.isOn || typeof cstate.endTime === 'undefined') continue; - if (c.master === 1) { - await ncp.circuits.checkCircuitEggTimerExpirationAsync(cstate); - } - } - for (let i = 0; i < sys.features.length; i++) { - let fstate = state.features.getItemByIndex(i); - if (!fstate.isActive || !fstate.isOn || typeof fstate.endTime === 'undefined') continue; - if (fstate.endTime.toDate() < new Timestamp().toDate()) { - await sys.board.circuits.setCircuitStateAsync(fstate.id, false); - fstate.emitEquipmentChange(); - } - } - for (let i = 0; i < sys.circuitGroups.length; i++) { - let cgstate = state.circuitGroups.getItemByIndex(i); - if (!cgstate.isActive || !cgstate.isOn || typeof cgstate.endTime === 'undefined') continue; - if (cgstate.endTime.toDate() < new Timestamp().toDate()) { - await sys.board.circuits.setCircuitGroupStateAsync(cgstate.id, false); - cgstate.emitEquipmentChange(); - } - } - for (let i = 0; i < sys.lightGroups.length; i++) { - let lgstate = state.lightGroups.getItemByIndex(i); - if (!lgstate.isActive || !lgstate.isOn || typeof lgstate.endTime === 'undefined') continue; - if (lgstate.endTime.toDate() < new Timestamp().toDate()) { - await sys.board.circuits.setLightGroupStateAsync(lgstate.id, false); - lgstate.emitEquipmentChange(); - } - } - } catch (err) { logger.error(`checkEggTimerExpiration: Error synchronizing circuit relays ${err.message}`); } - } - public async syncCircuitRelayStates() { - try { - for (let i = 0; i < sys.circuits.length; i++) { - // Run through all the controlled circuits to see whether they should be triggered or not. - let circ = sys.circuits.getItemByIndex(i); - if (circ.master === 1 && circ.isActive) { - let cstate = state.circuits.getItemById(circ.id); - if (cstate.isOn) await ncp.circuits.setCircuitStateAsync(cstate, cstate.isOn); - } - } - } catch (err) { logger.error(`syncCircuitRelayStates: Error synchronizing circuit relays ${err.message}`); } - } + for (let i = 0; i < sys.circuits.length; i++) { + // Run through all the controlled circuits to see whether they should be triggered or not. + let circ = sys.circuits.getItemByIndex(i); + if (circ.master === 1 && circ.isActive) { + let cstate = state.circuits.getItemById(circ.id); + if (cstate.isOn) await ncp.circuits.setCircuitStateAsync(cstate, cstate.isOn); + } + } + } catch (err) { logger.error(`syncCircuitRelayStates: Error synchronizing circuit relays ${err.message}`); } + } public syncVirtualCircuitStates() { try { let arrCircuits = sys.board.valueMaps.virtualCircuits.toArray(); @@ -2231,6 +2258,8 @@ export class CircuitCommands extends BoardCommands { // This also removes virtual circuits depending on whether heaters exsits on the bodies. Not sure why we are doing this // as the body data contains whether a body is heated or not. Perhapse some attached interface is using // the virtual circuit list as a means to determine whether solar is available. That is totally flawed if that is the case. + let solarType = sys.board.valueMaps.heaterTypes.encode('solar', -1); + for (let i = 0; i < arrCircuits.length; i++) { let vc = arrCircuits[i]; let remove = false; @@ -2247,7 +2276,7 @@ export class CircuitCommands extends BoardCommands { // Determine whether the pool heater is on. for (let j = 0; j < poolStates.length; j++) { let hstatus = sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus); - if (hstatus !== 'off' && hstatus !== 'solar') { + if (hstatus !== 'off' && hstatus !== 'solar' && hstatus !== 'cooling') { // In this instance we may have a delay underway. let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name !== 'solar'); bState = typeof hstate === 'undefined'; @@ -2264,7 +2293,7 @@ export class CircuitCommands extends BoardCommands { // Determine whether the spa heater is on. for (let j = 0; j < spaStates.length; j++) { let hstatus = sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus); - if (hstatus !== 'off' && hstatus !== 'solar') { + if (hstatus !== 'off' && hstatus !== 'solar' && hstatus !== 'cooling') { // In this instance we may have a delay underway. let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name !== 'solar'); bState = typeof hstate === 'undefined'; @@ -2329,112 +2358,57 @@ export class CircuitCommands extends BoardCommands { } if (!remove) { for (let j = 0; j < poolStates.length && !bState; j++) { - if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') bState = true; - } - for (let j = 0; j < spaStates.length && !bState; j++) { - if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') bState = true; + let bodyState = poolStates[j]; + let hstatus = sys.board.valueMaps.heatStatus.getName(bodyState.heatStatus); + let hstate: HeaterState; + if (hstatus === 'solar') hstate = state.heaters.find(x => x.bodyId === bodyState.id && x.startupDelay !== true && x.type.val === solarType && x.isOn === true); + else if (hstatus === 'cooling') hstate = state.heaters.find(x => x.bodyId === bodyState.id && x.startupDelay !== true && x.type.val === solarType && x.isCooling === true); + bState = typeof hstate !== 'undefined'; + if (bState) break; } - } - break; - case 'solar1': - remove = true; - for (let j = 0; j < poolStates.length; j++) { - if (poolStates[j].id === 1 && poolStates[j].heaterOptions.solar) { - remove = false; - vc.desc = `${poolStates[j].name} Solar`; - if (sys.board.valueMaps.heatStatus.getName(poolStates[j].heatStatus) === 'solar') { - // In this instance we may have a delay underway. - let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name === 'solar'); - bState = typeof hstate === 'undefined'; + if (!bState) { + for (let j = 0; j < spaStates.length && !bState; j++) { + let bodyState = spaStates[j]; + let hstatus = sys.board.valueMaps.heatStatus.getName(bodyState.heatStatus); + let hstate: HeaterState; + if (hstatus === 'solar') hstate = state.heaters.find(x => x.bodyId === bodyState.id && x.startupDelay !== true && x.type.val === solarType && x.isOn === true); + else if (hstatus === 'cooling') hstate = state.heaters.find(x => x.bodyId === bodyState.id && x.startupDelay !== true && x.type.val === solarType && x.isCooling === true); + bState = typeof hstate !== 'undefined'; + if (bState) break; } } } - for (let j = 0; j < spaStates.length; j++) { - if (spaStates[j].id === 1 && spaStates[j].heaterOptions.solar) { - remove = false; - vc.desc = `${spaStates[j].name} Solar`; - if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') { - // In this instance we may have a delay underway. - let hstate = state.heaters.find(x => x.bodyId === 1 && x.startupDelay === true && x.type.name === 'solar'); - bState = typeof hstate === 'undefined'; - } - } - } - break; + case 'solar1': case 'solar2': - remove = true; - for (let j = 0; j < poolStates.length; j++) { - if (poolStates[j].id === 2 && poolStates[j].heaterOptions.solar) { - remove = false; - vc.desc = `${poolStates[j].name} Solar`; - if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') { - // In this instance we may have a delay underway. - let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name === 'solar'); - bState = typeof hstate === 'undefined'; - } - } - } - for (let j = 0; j < spaStates.length; j++) { - if (spaStates[j].id === 2 && spaStates[j].heaterOptions.solar) { - remove = false; - vc.desc = `${spaStates[j].name} Solar`; - if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') { - // In this instance we may have a delay underway. - let hstate = state.heaters.find(x => x.bodyId === 2 && x.startupDelay === true && x.type.name === 'solar'); - bState = typeof hstate === 'undefined'; - } - } - } - break; case 'solar3': - remove = true; - for (let j = 0; j < poolStates.length; j++) { - if (poolStates[j].id === 3 && poolStates[j].heaterOptions.solar) { - remove = false; - vc.desc = `${poolStates[j].name} Solar`; - if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') { - // In this instance we may have a delay underway. - let hstate = state.heaters.find(x => x.bodyId === 3 && x.startupDelay === true && x.type.name === 'solar'); - bState = typeof hstate === 'undefined'; - } - } - } - for (let j = 0; j < spaStates.length; j++) { - if (spaStates[j].id === 3 && spaStates[j].heaterOptions.solar) { - remove = false; - vc.desc = `${spaStates[j].name} Solar`; - if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') { - // In this instance we may have a delay underway. - let hstate = state.heaters.find(x => x.bodyId === 3 && x.startupDelay === true && x.type.name === 'solar'); - bState = typeof hstate === 'undefined'; - } - } - } - - break; case 'solar4': remove = true; + let solarBody = parseInt(vc.name.substring(5), 10); for (let j = 0; j < poolStates.length; j++) { - if (poolStates[j].id === 4 && poolStates[j].heaterOptions.solar) { + let bodyState = poolStates[j]; + if (bodyState.id === solarBody && bodyState.heaterOptions.solar) { remove = false; - vc.desc = `${poolStates[j].name} Solar`; - if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') { - // In this instance we may have a delay underway. - let hstate = state.heaters.find(x => x.bodyId === 4 && x.startupDelay === true && x.type.name === 'solar'); - bState = typeof hstate === 'undefined'; - } + let hstatus = sys.board.valueMaps.heatStatus.getName(bodyState.heatStatus); + vc.desc = `${bodyState.name} Solar`; + let hstate: HeaterState; + if (hstatus === 'solar') hstate = state.heaters.find(x => x.bodyId === bodyState.id && x.startupDelay !== true && x.type.val === solarType && x.isOn === true); + else if (hstatus === 'cooling') hstate = state.heaters.find(x => x.bodyId === bodyState.id && x.startupDelay !== true && x.type.val === solarType && x.isCooling === true); + bState = typeof hstate !== 'undefined'; + if (bState) break; } } for (let j = 0; j < spaStates.length; j++) { - if (spaStates[j].id === 4 && spaStates[j].heaterOptions.solar) { + let bodyState = spaStates[j]; + if (bodyState.id === solarBody && bodyState.heaterOptions.solar) { remove = false; - vc.desc = `${spaStates[j].name} Solar`; - if (sys.board.valueMaps.heatStatus.getName(spaStates[j].heatStatus) === 'solar') { - // In this instance we may have a delay underway. - let hstate = state.heaters.find(x => x.bodyId === 4 && x.startupDelay === true && x.type.name === 'solar'); - bState = typeof hstate === 'undefined'; - } + let hstatus = sys.board.valueMaps.heatStatus.getName(bodyState.heatStatus); + vc.desc = `${bodyState.name} Solar`; + let hstate: HeaterState; + if (hstatus === 'solar') hstate = state.heaters.find(x => x.bodyId === bodyState.id && x.startupDelay !== true && x.type.val === solarType && x.isOn === true); + else if (hstatus === 'cooling') hstate = state.heaters.find(x => x.bodyId === bodyState.id && x.startupDelay !== true && x.type.val === solarType && x.isCooling === true); + bState = typeof hstate !== 'undefined'; + if (bState) break; } } break; @@ -2484,94 +2458,94 @@ export class CircuitCommands extends BoardCommands { } } catch (err) { logger.error(`Error synchronizing virtual circuits`); } } - public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { - sys.board.suspendStatus(true); - try { - // We need to do some routing here as it is now critical that circuits, groups, and features - // have their own processing. The virtual controller used to only deal with one circuit. - if (sys.board.equipmentIds.circuitGroups.isInRange(id)) - return await sys.board.circuits.setCircuitGroupStateAsync(id, val); - else if (sys.board.equipmentIds.features.isInRange(id)) - return await sys.board.features.setFeatureStateAsync(id, val); - let circuit: ICircuit = sys.circuits.getInterfaceById(id, false, { isActive: false }); - if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit or Feature id ${id} not valid`, id, 'Circuit')); - let circ = state.circuits.getInterfaceById(id, circuit.isActive !== false); - let newState = utils.makeBool(val); - // First, if we are turning the circuit on, lets determine whether the circuit is a pool or spa circuit and if this is a shared system then we need - // to turn off the other body first. - //[12, { name: 'pool', desc: 'Pool', hasHeatSource: true }], - //[13, { name: 'spa', desc: 'Spa', hasHeatSource: true }] - let func = sys.board.valueMaps.circuitFunctions.get(circuit.type); - if (newState && (func.name === 'pool' || func.name === 'spa') && sys.equipment.shared === true) { - // If we are shared we need to turn off the other circuit. - let offType = func.name === 'pool' ? sys.board.valueMaps.circuitFunctions.getValue('spa') : sys.board.valueMaps.circuitFunctions.getValue('pool'); - let off = sys.circuits.get().filter(elem => elem.type === offType); - // Turn the circuits off that are part of the shared system. We are going back to the board - // just in case we got here for a circuit that isn't on the current defined panel. - for (let i = 0; i < off.length; i++) { - let coff = off[i]; - await sys.board.circuits.setCircuitStateAsync(coff.id, false); + public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { + sys.board.suspendStatus(true); + try { + // We need to do some routing here as it is now critical that circuits, groups, and features + // have their own processing. The virtual controller used to only deal with one circuit. + if (sys.board.equipmentIds.circuitGroups.isInRange(id)) + return await sys.board.circuits.setCircuitGroupStateAsync(id, val); + else if (sys.board.equipmentIds.features.isInRange(id)) + return await sys.board.features.setFeatureStateAsync(id, val); + let circuit: ICircuit = sys.circuits.getInterfaceById(id, false, { isActive: false }); + if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit or Feature id ${id} not valid`, id, 'Circuit')); + let circ = state.circuits.getInterfaceById(id, circuit.isActive !== false); + let newState = utils.makeBool(val); + // First, if we are turning the circuit on, lets determine whether the circuit is a pool or spa circuit and if this is a shared system then we need + // to turn off the other body first. + //[12, { name: 'pool', desc: 'Pool', hasHeatSource: true }], + //[13, { name: 'spa', desc: 'Spa', hasHeatSource: true }] + let func = sys.board.valueMaps.circuitFunctions.get(circuit.type); + if (newState && (func.name === 'pool' || func.name === 'spa') && sys.equipment.shared === true) { + // If we are shared we need to turn off the other circuit. + let offType = func.name === 'pool' ? sys.board.valueMaps.circuitFunctions.getValue('spa') : sys.board.valueMaps.circuitFunctions.getValue('pool'); + let off = sys.circuits.get().filter(elem => elem.type === offType); + // Turn the circuits off that are part of the shared system. We are going back to the board + // just in case we got here for a circuit that isn't on the current defined panel. + for (let i = 0; i < off.length; i++) { + let coff = off[i]; + await sys.board.circuits.setCircuitStateAsync(coff.id, false); + } + } + if (id === 6) state.temps.bodies.getItemById(1, true).isOn = val; + else if (id === 1) state.temps.bodies.getItemById(2, true).isOn = val; + // Let the main nixie controller set the circuit state and affect the relays if it needs to. + await ncp.circuits.setCircuitStateAsync(circ, newState); + await sys.board.syncEquipmentItems(); + return state.circuits.getInterfaceById(circ.id); + } + catch (err) { return Promise.reject(`Nixie: Error setCircuitStateAsync ${err.message}`); } + finally { + ncp.pumps.syncPumpStates(); + sys.board.suspendStatus(false); + state.emitEquipmentChanges(); } - } - if (id === 6) state.temps.bodies.getItemById(1, true).isOn = val; - else if (id === 1) state.temps.bodies.getItemById(2, true).isOn = val; - // Let the main nixie controller set the circuit state and affect the relays if it needs to. - await ncp.circuits.setCircuitStateAsync(circ, newState); - await sys.board.syncEquipmentItems(); - return state.circuits.getInterfaceById(circ.id); - } - catch (err) { return Promise.reject(`Nixie: Error setCircuitStateAsync ${err.message}`); } - finally { - ncp.pumps.syncPumpStates(); - sys.board.suspendStatus(false); - state.emitEquipmentChanges(); } - } - public async toggleCircuitStateAsync(id: number): Promise { - let circ = state.circuits.getInterfaceById(id); - return await this.setCircuitStateAsync(id, !(circ.isOn || false)); - } - public async runLightGroupCommandAsync(obj: any): Promise { - // Do all our validation. - try { - let id = parseInt(obj.id, 10); - let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightGroupCommands.findItem(obj.command) : { val: 0, name: 'undefined' }; - if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light group command ${cmd.name} does not exist`, 'runLightGroupCommandAsync')); - if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light group ${id} does not exist`, 'runLightGroupCommandAsync')); - let grp = sys.lightGroups.getItemById(id); - let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name); - let sgrp = state.lightGroups.getItemById(grp.id); - sgrp.action = nop; - sgrp.emitEquipmentChange(); - // So here we are now we can run the command against all lights in the group that match the command so get a list of the lights. - let arrCircs = []; - for (let i = 0; i < grp.circuits.length; i++) { - let circ = sys.circuits.getItemById(grp.circuits.getItemByIndex(i).circuit); - let type = sys.board.valueMaps.circuitFunctions.transform(circ.type); - if (type.isLight && cmd.types.includes(type.theme)) arrCircs.push(circ); - } - // So now we should hav a complete list of the lights that are part of the command list so start them off on their sequence. We want all the lights - // to be doing their thing at the same time so in the lieu of threads we will ceate a promise all. - let proms = []; - for (let i = 0; i < arrCircs.length; i++) { - await ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence); - //proms.push(ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence)); - } - for (let i = 0; i < arrCircs.length; i++) { - await sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, false); - //proms.push(ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence)); - } - await setTimeout(10000); - for (let i = 0; i < arrCircs.length; i++) { - await sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, true); - //proms.push(ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence)); - } - sgrp.action = 0; - sgrp.emitEquipmentChange(); - return state.lightGroups.getItemById(id); - } - catch (err) { return Promise.reject(`Error runLightGroupCommandAsync ${err.message}`); } - } + public async toggleCircuitStateAsync(id: number): Promise { + let circ = state.circuits.getInterfaceById(id); + return await this.setCircuitStateAsync(id, !(circ.isOn || false)); + } + public async runLightGroupCommandAsync(obj: any): Promise { + // Do all our validation. + try { + let id = parseInt(obj.id, 10); + let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightGroupCommands.findItem(obj.command) : { val: 0, name: 'undefined' }; + if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light group command ${cmd.name} does not exist`, 'runLightGroupCommandAsync')); + if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light group ${id} does not exist`, 'runLightGroupCommandAsync')); + let grp = sys.lightGroups.getItemById(id); + let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name); + let sgrp = state.lightGroups.getItemById(grp.id); + sgrp.action = nop; + sgrp.emitEquipmentChange(); + // So here we are now we can run the command against all lights in the group that match the command so get a list of the lights. + let arrCircs = []; + for (let i = 0; i < grp.circuits.length; i++) { + let circ = sys.circuits.getItemById(grp.circuits.getItemByIndex(i).circuit); + let type = sys.board.valueMaps.circuitFunctions.transform(circ.type); + if (type.isLight && cmd.types.includes(type.theme)) arrCircs.push(circ); + } + // So now we should hav a complete list of the lights that are part of the command list so start them off on their sequence. We want all the lights + // to be doing their thing at the same time so in the lieu of threads we will ceate a promise all. + let proms = []; + for (let i = 0; i < arrCircs.length; i++) { + await ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence); + //proms.push(ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence)); + } + for (let i = 0; i < arrCircs.length; i++) { + await sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, false); + //proms.push(ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence)); + } + await setTimeout(10000); + for (let i = 0; i < arrCircs.length; i++) { + await sys.board.circuits.setCircuitStateAsync(arrCircs[i].id, true); + //proms.push(ncp.circuits.sendOnOffSequenceAsync(arrCircs[i].id, cmd.sequence)); + } + sgrp.action = 0; + sgrp.emitEquipmentChange(); + return state.lightGroups.getItemById(id); + } + catch (err) { return Promise.reject(`Error runLightGroupCommandAsync ${err.message}`); } + } public async runLightCommandAsync(obj: any): Promise { // Do all our validation. try { @@ -2605,109 +2579,109 @@ export class CircuitCommands extends BoardCommands { } catch (err) { return Promise.reject(`Error runLightCommandAsync ${err.message}`); } } - public async setLightThemeAsync(id: number, theme: number): Promise { - let cstate = state.circuits.getItemById(id); - let circ = sys.circuits.getItemById(id); - let thm = sys.board.valueMaps.lightThemes.findItem(theme); - let nop = sys.board.valueMaps.circuitActions.getValue('lighttheme'); - cstate.action = nop; - cstate.emitEquipmentChange(); - try { - if (typeof thm !== 'undefined' && typeof thm.sequence !== 'undefined' && circ.master === 1) { - await sys.board.circuits.setCircuitStateAsync(id, true); - await ncp.circuits.setLightThemeAsync(id, thm); - } - cstate.lightingTheme = theme; - return cstate; - } catch (err) { return Promise.reject(new InvalidOperationError(err.message, 'setLightThemeAsync')); } - finally { cstate.action = 0; cstate.emitEquipmentChange(); } - } - public async setColorHoldAsync(id: number): Promise { - try { - let circ = sys.circuits.getItemById(id); - if (!circ.isActive) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id ${id}`, id, 'circuit')); - let cstate = state.circuits.getItemById(circ.id); - let cmd = sys.board.valueMaps.lightCommands.findItem('colorhold'); - await sys.board.circuits.setCircuitStateAsync(id, true); - if (circ.master === 1) await ncp.circuits.sendOnOffSequenceAsync(id, cmd.sequence); - return cstate; - } - catch (err) { return Promise.reject(`Nixie: Error setColorHoldAsync ${err.message}`); } - } - public async setColorRecallAsync(id: number): Promise { - try { - let circ = sys.circuits.getItemById(id); - if (!circ.isActive) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id ${id}`, id, 'circuit')); - let cstate = state.circuits.getItemById(circ.id); - let cmd = sys.board.valueMaps.lightCommands.findItem('colorrecall'); - await sys.board.circuits.setCircuitStateAsync(id, true); - if (circ.master === 1) await ncp.circuits.sendOnOffSequenceAsync(id, cmd.sequence); - return cstate; - } - catch (err) { return Promise.reject(`Nixie: Error setColorHoldAsync ${err.message}`); } - } - public async setLightThumperAsync(id: number): Promise { return state.circuits.getItemById(id); } + public async setLightThemeAsync(id: number, theme: number): Promise { + let cstate = state.circuits.getItemById(id); + let circ = sys.circuits.getItemById(id); + let thm = sys.board.valueMaps.lightThemes.findItem(theme); + let nop = sys.board.valueMaps.circuitActions.getValue('lighttheme'); + cstate.action = nop; + cstate.emitEquipmentChange(); + try { + if (typeof thm !== 'undefined' && typeof thm.sequence !== 'undefined' && circ.master === 1) { + await sys.board.circuits.setCircuitStateAsync(id, true); + await ncp.circuits.setLightThemeAsync(id, thm); + } + cstate.lightingTheme = theme; + return cstate; + } catch (err) { return Promise.reject(new InvalidOperationError(err.message, 'setLightThemeAsync')); } + finally { cstate.action = 0; cstate.emitEquipmentChange(); } + } + public async setColorHoldAsync(id: number): Promise { + try { + let circ = sys.circuits.getItemById(id); + if (!circ.isActive) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id ${id}`, id, 'circuit')); + let cstate = state.circuits.getItemById(circ.id); + let cmd = sys.board.valueMaps.lightCommands.findItem('colorhold'); + await sys.board.circuits.setCircuitStateAsync(id, true); + if (circ.master === 1) await ncp.circuits.sendOnOffSequenceAsync(id, cmd.sequence); + return cstate; + } + catch (err) { return Promise.reject(`Nixie: Error setColorHoldAsync ${err.message}`); } + } + public async setColorRecallAsync(id: number): Promise { + try { + let circ = sys.circuits.getItemById(id); + if (!circ.isActive) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id ${id}`, id, 'circuit')); + let cstate = state.circuits.getItemById(circ.id); + let cmd = sys.board.valueMaps.lightCommands.findItem('colorrecall'); + await sys.board.circuits.setCircuitStateAsync(id, true); + if (circ.master === 1) await ncp.circuits.sendOnOffSequenceAsync(id, cmd.sequence); + return cstate; + } + catch (err) { return Promise.reject(`Nixie: Error setColorHoldAsync ${err.message}`); } + } + public async setLightThumperAsync(id: number): Promise { return state.circuits.getItemById(id); } - public setDimmerLevelAsync(id: number, level: number): Promise { - let circ = state.circuits.getItemById(id); - circ.level = level; - return Promise.resolve(circ as ICircuitState); - } - public getCircuitReferences(includeCircuits?: boolean, includeFeatures?: boolean, includeVirtual?: boolean, includeGroups?: boolean) { - let arrRefs = []; - if (includeCircuits) { - // RSG: converted this to getItemByIndex because hasHeatSource isn't actually stored as part of the data - for (let i = 0; i < sys.circuits.length; i++) { - let c = sys.circuits.getItemByIndex(i); - arrRefs.push({ id: c.id, name: c.name, type: c.type, equipmentType: 'circuit', nameId: c.nameId, hasHeatSource: c.hasHeatSource }); - } - } - if (includeFeatures) { - let features = sys.features.get(); - for (let i = 0; i < sys.features.length; i++) { - let c = features[i]; - arrRefs.push({ id: c.id, name: c.name, type: c.type, equipmentType: 'feature', nameId: c.nameId }); - } - } - if (includeVirtual) { - let vcs = sys.board.valueMaps.virtualCircuits.toArray(); - for (let i = 0; i < vcs.length; i++) { - let c = vcs[i]; - arrRefs.push({ id: c.val, name: c.desc, equipmentType: 'virtual', assignableToPumpCircuit: c.assignableToPumpCircuit }); - } - } - if (includeGroups) { - let groups = sys.circuitGroups.get(); - for (let i = 0; i < groups.length; i++) { - let c = groups[i]; - arrRefs.push({ id: c.id, name: c.name, equipmentType: 'circuitGroup', nameId: c.nameId }); - } - groups = sys.lightGroups.get(); - for (let i = 0; i < groups.length; i++) { - let c = groups[i]; - arrRefs.push({ id: c.id, name: c.name, equipmentType: 'lightGroup', nameId: c.nameId }); - } - } - arrRefs.sort((a, b) => { return a.id > b.id ? 1 : a.id === b.id ? 0 : -1; }); - return arrRefs; - } - public getLightReferences() { - let circuits = sys.circuits.get(); - let arrRefs = []; - for (let i = 0; i < circuits.length; i++) { - let c = circuits[i]; - let type = sys.board.valueMaps.circuitFunctions.transform(c.type); - if (type.isLight) arrRefs.push({ id: c.id, name: c.name, type: c.type, equipmentType: 'circuit', nameId: c.nameId }); - } - return arrRefs; - } - public getLightThemes(type?: number) { return sys.board.valueMaps.lightThemes.toArray(); } - public getCircuitFunctions() { - let cf = sys.board.valueMaps.circuitFunctions.toArray(); - if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' }); - return cf; - } - public getCircuitNames() { return [...sys.board.valueMaps.circuitNames.toArray(), ...sys.board.valueMaps.customNames.toArray()]; } + public setDimmerLevelAsync(id: number, level: number): Promise { + let circ = state.circuits.getItemById(id); + circ.level = level; + return Promise.resolve(circ as ICircuitState); + } + public getCircuitReferences(includeCircuits?: boolean, includeFeatures?: boolean, includeVirtual?: boolean, includeGroups?: boolean) { + let arrRefs = []; + if (includeCircuits) { + // RSG: converted this to getItemByIndex because hasHeatSource isn't actually stored as part of the data + for (let i = 0; i < sys.circuits.length; i++) { + let c = sys.circuits.getItemByIndex(i); + arrRefs.push({ id: c.id, name: c.name, type: c.type, equipmentType: 'circuit', nameId: c.nameId, hasHeatSource: c.hasHeatSource }); + } + } + if (includeFeatures) { + let features = sys.features.get(); + for (let i = 0; i < sys.features.length; i++) { + let c = features[i]; + arrRefs.push({ id: c.id, name: c.name, type: c.type, equipmentType: 'feature', nameId: c.nameId }); + } + } + if (includeVirtual) { + let vcs = sys.board.valueMaps.virtualCircuits.toArray(); + for (let i = 0; i < vcs.length; i++) { + let c = vcs[i]; + arrRefs.push({ id: c.val, name: c.desc, equipmentType: 'virtual', assignableToPumpCircuit: c.assignableToPumpCircuit }); + } + } + if (includeGroups) { + let groups = sys.circuitGroups.get(); + for (let i = 0; i < groups.length; i++) { + let c = groups[i]; + arrRefs.push({ id: c.id, name: c.name, equipmentType: 'circuitGroup', nameId: c.nameId }); + } + groups = sys.lightGroups.get(); + for (let i = 0; i < groups.length; i++) { + let c = groups[i]; + arrRefs.push({ id: c.id, name: c.name, equipmentType: 'lightGroup', nameId: c.nameId }); + } + } + arrRefs.sort((a, b) => { return a.id > b.id ? 1 : a.id === b.id ? 0 : -1; }); + return arrRefs; + } + public getLightReferences() { + let circuits = sys.circuits.get(); + let arrRefs = []; + for (let i = 0; i < circuits.length; i++) { + let c = circuits[i]; + let type = sys.board.valueMaps.circuitFunctions.transform(c.type); + if (type.isLight) arrRefs.push({ id: c.id, name: c.name, type: c.type, equipmentType: 'circuit', nameId: c.nameId }); + } + return arrRefs; + } + public getLightThemes(type?: number) { return sys.board.valueMaps.lightThemes.toArray(); } + public getCircuitFunctions() { + let cf = sys.board.valueMaps.circuitFunctions.toArray(); + if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' }); + return cf; + } + public getCircuitNames() { return [...sys.board.valueMaps.circuitNames.toArray(), ...sys.board.valueMaps.customNames.toArray()]; } public async setCircuitAsync(data: any, send: boolean = true): Promise { try { let id = parseInt(data.id, 10); @@ -2761,465 +2735,454 @@ export class CircuitCommands extends BoardCommands { } catch (err) { logger.error(`setCircuitAsync error with ${data}. ${err}`); return Promise.reject(err); } } - public async setCircuitGroupAsync(obj: any): Promise { - let group: CircuitGroup = null; - let sgroup: CircuitGroupState = null; - let type = 0; - let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1; - let isAdd = false; - if (id <= 0) { - // We are adding a circuit group so we need to get the next equipment id. For circuit groups and light groups, they share ids. - let range = sys.board.equipmentIds.circuitGroups; - for (let i = range.start; i <= range.end; i++) { - if (!sys.lightGroups.find(elem => elem.id === i) && !sys.circuitGroups.find(elem => elem.id === i)) { - id = i; - break; + public async setCircuitGroupAsync(obj: any): Promise { + let group: CircuitGroup = null; + let sgroup: CircuitGroupState = null; + let type = 0; + let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1; + let isAdd = false; + if (id <= 0) { + // We are adding a circuit group so we need to get the next equipment id. For circuit groups and light groups, they share ids. + let range = sys.board.equipmentIds.circuitGroups; + for (let i = range.start; i <= range.end; i++) { + if (!sys.lightGroups.find(elem => elem.id === i) && !sys.circuitGroups.find(elem => elem.id === i)) { + id = i; + break; + } + } + type = parseInt(obj.type, 10) || 2; + group = sys.circuitGroups.getItemById(id, true); + sgroup = state.circuitGroups.getItemById(id, true); + isAdd = true; } - } - type = parseInt(obj.type, 10) || 2; - group = sys.circuitGroups.getItemById(id, true); - sgroup = state.circuitGroups.getItemById(id, true); - isAdd = true; - } - else { - group = sys.circuitGroups.getItemById(id, false); - sgroup = state.circuitGroups.getItemById(id, false); - type = group.type; - } - if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit group ids exceeded: ${id}`, id, 'circuitGroup')); - if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'circuitGroup')); - return new Promise((resolve, reject) => { - if (typeof obj.nameId !== 'undefined') { - group.nameId = obj.nameId; - group.name = sys.board.valueMaps.circuitNames.transform(obj.nameId).desc; - } - else if (typeof obj.name !== 'undefined') group.name = obj.name; - if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440; - if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440); - if (typeof obj.showInFeatures !== 'undefined') group.showInFeatures = utils.makeBool(obj.showInFeatures); - group.dontStop = group.eggTimer === 1440; - group.isActive = true; - // group.type = 2; - if (typeof obj.circuits !== 'undefined') { - for (let i = 0; i < obj.circuits.length; i++) { - let c = group.circuits.getItemByIndex(i, true, { id: i + 1 }); - let cobj = obj.circuits[i]; - if (typeof cobj.circuit !== 'undefined') c.circuit = cobj.circuit; - if (typeof cobj.desiredState !== 'undefined') - c.desiredState = parseInt(cobj.desiredState, 10); - else if (typeof cobj.desiredStateOn !== 'undefined') { - c.desiredState = utils.makeBool(cobj.desiredStateOn) ? 0 : 1; - } + else { + group = sys.circuitGroups.getItemById(id, false); + sgroup = state.circuitGroups.getItemById(id, false); + type = group.type; } - } - let sgroup = state.circuitGroups.getItemById(id, true); - sgroup.name = group.name; - sgroup.type = group.type; - sgroup.showInFeatures = group.showInFeatures; - sgroup.isActive = group.isActive; - sgroup.type = group.type; - sys.board.features.syncGroupStates(); - resolve(group); - }); + if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit group ids exceeded: ${id}`, id, 'circuitGroup')); + if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'circuitGroup')); + return new Promise((resolve, reject) => { + if (typeof obj.nameId !== 'undefined') { + group.nameId = obj.nameId; + group.name = sys.board.valueMaps.circuitNames.transform(obj.nameId).desc; + } + else if (typeof obj.name !== 'undefined') group.name = obj.name; + if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440; + if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440); + if (typeof obj.showInFeatures !== 'undefined') group.showInFeatures = utils.makeBool(obj.showInFeatures); + group.dontStop = group.eggTimer === 1440; + group.isActive = true; + // group.type = 2; + if (typeof obj.circuits !== 'undefined') { + for (let i = 0; i < obj.circuits.length; i++) { + let c = group.circuits.getItemByIndex(i, true, { id: i + 1 }); + let cobj = obj.circuits[i]; + if (typeof cobj.circuit !== 'undefined') c.circuit = cobj.circuit; + if (typeof cobj.desiredState !== 'undefined') + c.desiredState = parseInt(cobj.desiredState, 10); + else if (typeof cobj.desiredStateOn !== 'undefined') { + c.desiredState = utils.makeBool(cobj.desiredStateOn) ? 0 : 1; + } + } + } + let sgroup = state.circuitGroups.getItemById(id, true); + sgroup.name = group.name; + sgroup.type = group.type; + sgroup.showInFeatures = group.showInFeatures; + sgroup.isActive = group.isActive; + sgroup.type = group.type; + sys.board.features.syncGroupStates(); + resolve(group); + }); - } - public async setLightGroupAsync(obj: any, send: boolean = true): Promise { - let group: LightGroup = null; - let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1; - if (id <= 0) { - // We are adding a circuit group. - id = sys.circuitGroups.getNextEquipmentId(sys.board.equipmentIds.circuitGroups); - } - if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit light group id exceeded`, id, 'LightGroup')); - if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'LightGroup')); - group = sys.lightGroups.getItemById(id, true); - let sgroup = state.lightGroups.getItemById(id, true); - return new Promise((resolve, reject) => { - if (typeof obj.name !== 'undefined') sgroup.name = group.name = obj.name; - if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440; - if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440); - group.dontStop = group.eggTimer === 1440; - group.isActive = true; - if (typeof obj.circuits !== 'undefined') { - for (let i = 0; i < obj.circuits.length; i++) { - let cobj = obj.circuits[i]; - let c: LightGroupCircuit; - if (typeof cobj.id !== 'undefined') c = group.circuits.getItemById(parseInt(cobj.id, 10), true); - else if (typeof cobj.circuit !== 'undefined') c = group.circuits.getItemByCircuitId(parseInt(cobj.circuit, 10), true); - else c = group.circuits.getItemByIndex(i, true, { id: i + 1 }); - if (typeof cobj.circuit !== 'undefined') c.circuit = cobj.circuit; - if (typeof cobj.lightingTheme !== 'undefined') c.lightingTheme = parseInt(cobj.lightingTheme, 10); - if (typeof cobj.color !== 'undefined') c.color = parseInt(cobj.color, 10); - if (typeof cobj.swimDelay !== 'undefined') c.swimDelay = parseInt(cobj.swimDelay, 10); - if (typeof cobj.position !== 'undefined') c.position = parseInt(cobj.position, 10); - } - // group.circuits.length = obj.circuits.length; // RSG - removed as this will delete circuits that were not changed - } - resolve(group); - }); - } - public async deleteCircuitGroupAsync(obj: any): Promise { - let id = parseInt(obj.id, 10); - if (isNaN(id)) return Promise.reject(new EquipmentNotFoundError(`Invalid group id: ${obj.id}`, 'CircuitGroup')); - //if (!sys.board.equipmentIds.circuitGroups.isInRange(id)) return; - if (typeof id !== 'undefined') { - let group = sys.circuitGroups.getItemById(id, false); - let sgroup = state.circuitGroups.getItemById(id, false); - sys.circuitGroups.removeItemById(id); - state.circuitGroups.removeItemById(id); - group.isActive = false; - sgroup.isOn = false; - sgroup.isActive = false; - sgroup.showInFeatures = false; - sgroup.emitEquipmentChange(); - return new Promise((resolve, reject) => { resolve(group); }); - } - else - return Promise.reject(new InvalidEquipmentIdError('Group id has not been defined', id, 'CircuitGroup')); - } - public async deleteLightGroupAsync(obj: any): Promise { - let id = parseInt(obj.id, 10); - if (isNaN(id)) return Promise.reject(new EquipmentNotFoundError(`Invalid group id: ${obj.id}`, 'LightGroup')); - if (!sys.board.equipmentIds.circuitGroups.isInRange(id)) return; - if (typeof obj.id !== 'undefined') { - let group = sys.lightGroups.getItemById(id, false); - let sgroup = state.lightGroups.getItemById(id, false); - sys.lightGroups.removeItemById(id); - state.lightGroups.removeItemById(id); - group.isActive = false; - sgroup.isOn = false; - sgroup.isActive = false; - sgroup.emitEquipmentChange(); - return new Promise((resolve, reject) => { resolve(group); }); - } - else - return Promise.reject(new InvalidEquipmentIdError('Group id has not been defined', id, 'LightGroup')); - } - public async deleteCircuitAsync(data: any): Promise { - if (typeof data.id === 'undefined') return Promise.reject(new InvalidEquipmentIdError('You must provide an id to delete a circuit', data.id, 'Circuit')); - let circuit = sys.circuits.getInterfaceById(data.id); - if (circuit.master === 1) await ncp.circuits.deleteCircuitAsync(circuit.id); - if (circuit instanceof Circuit) { - sys.circuits.removeItemById(data.id); - state.circuits.removeItemById(data.id); - } - if (circuit instanceof Feature) { - sys.features.removeItemById(data.id); - state.features.removeItemById(data.id); - } - return new Promise((resolve, reject) => { resolve(circuit); }); - } - public deleteCircuit(data: any) { - if (typeof data.id !== 'undefined') { - let circuit = sys.circuits.getInterfaceById(data.id); - if (circuit instanceof Circuit) { - sys.circuits.removeItemById(data.id); - state.circuits.removeItemById(data.id); - return; - } - if (circuit instanceof Feature) { - sys.features.removeItemById(data.id); - state.features.removeItemById(data.id); - return; - } } - } - public getNameById(id: number) { - if (id < 200) - return sys.board.valueMaps.circuitNames.transform(id).desc; - else - return sys.customNames.getItemById(id - 200).name; - } - public async setLightGroupThemeAsync(id: number, theme: number): Promise { - const grp = sys.lightGroups.getItemById(id); - const sgrp = state.lightGroups.getItemById(id); - grp.lightingTheme = sgrp.lightingTheme = theme; - for (let i = 0; i < grp.circuits.length; i++) { - let c = grp.circuits.getItemByIndex(i); - let cstate = state.circuits.getItemById(c.circuit); - // if theme is 'off' light groups should not turn on - if (cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) === 'off') - await sys.board.circuits.setCircuitStateAsync(c.circuit, false); - else if (!cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) !== 'off') await sys.board.circuits.setCircuitStateAsync(c.circuit, true); - } - let isOn = sys.board.valueMaps.lightThemes.getName(theme) === 'off' ? false : true; - sys.board.circuits.setEndTime(grp, sgrp, isOn); - sgrp.isOn = isOn; - // If we truly want to support themes in lightGroups we probably need to program - // the specific on/off toggles to enable that. For now this will go through the motions but it's just a pretender. - switch (theme) { - case 0: // off - case 1: // on - break; - case 128: // sync - setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'sync'); }); - break; - case 144: // swim - setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'swim'); }); - break; - case 160: // swim - setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'set'); }); - break; - case 190: // save - case 191: // recall - setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'other'); }); - break; - default: - setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'color'); }); - // other themes for magicstream? - } - sgrp.hasChanged = true; // Say we are dirty but we really are pure as the driven snow. - state.emitEquipmentChanges(); - return Promise.resolve(sgrp); - } - public async setLightGroupAttribsAsync(group: LightGroup): Promise { - let grp = sys.lightGroups.getItemById(group.id); - try { - grp.circuits.clear(); - for (let i = 0; i < group.circuits.length; i++) { - let circuit = grp.circuits.getItemByIndex(i); - grp.circuits.add({ id: i, circuit: circuit.circuit, color: circuit.color, position: i, swimDelay: circuit.swimDelay }); - } - let sgrp = state.lightGroups.getItemById(group.id); - sgrp.hasChanged = true; // Say we are dirty but we really are pure as the driven snow. - return Promise.resolve(grp); - } - catch (err) { return Promise.reject(err); } - } - public async sequenceLightGroupAsync(id: number, operation: string): Promise { - let sgroup = state.lightGroups.getItemById(id); - // This is the default action which really does nothing. - try { - let nop = sys.board.valueMaps.circuitActions.getValue(operation); - if (nop > 0) { - sgroup.action = nop; - sgroup.emitEquipmentChange(); - await setTimeout(10000); - sgroup.action = 0; - state.emitAllEquipmentChanges(); - } - return sgroup; - } catch (err) { return Promise.reject(new InvalidOperationError(`Error sequencing light group ${err.message}`, 'sequenceLightGroupAsync')); } - } - public async setCircuitGroupStateAsync(id: number, val: boolean): Promise { - let grp = sys.circuitGroups.getItemById(id, false, { isActive: false }); - logger.info(`Setting Circuit Group State`); - let gstate = (grp.dataName === 'circuitGroupConfig') ? state.circuitGroups.getItemById(grp.id, grp.isActive !== false) : state.lightGroups.getItemById(grp.id, grp.isActive !== false); - let circuits = grp.circuits.toArray(); - let arr = []; - for (let i = 0; i < circuits.length; i++) { - let circuit = circuits[i]; - // if the circuit group is turned on, we want the desired state of the individual circuits; - // if the circuit group is turned off, we want the opposite of the desired state - arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, val ? circuit.desiredState : !circuit.desiredState)); - } - return new Promise(async (resolve, reject) => { - await Promise.all(arr).catch((err) => { reject(err) }); - gstate.emitEquipmentChange(); - sys.board.circuits.setEndTime(grp, gstate, val); - gstate.isOn = val; - resolve(gstate); - }); - } - public async setLightGroupStateAsync(id: number, val: boolean): Promise { - return sys.board.circuits.setCircuitGroupStateAsync(id, val); - } - public setEndTime(thing: ICircuit, thingState: ICircuitState, isOn: boolean, bForce: boolean = false) { - /* - this is a generic fn for circuits, features, circuitGroups, lightGroups - to set the end time based on the egg timer. - it will be called from set[]StateAsync calls as well as when then state is - eval'ed from status packets/external messages and schedule changes. - instead of maintaining timers here which would increase the amount of - emits substantially, let the clients keep their own local timers - or just display the end time. - - bForce is an override sent by the syncScheduleStates. It gets set after the circuit gets set but we need to know if the sched is on. This allows the circuit end time to be - re-evaluated even though it already has an end time. - - Logic gets fun here... - 0. If the circuit is off, or has don't stop enabled, don't set an end time - 0.1. If the circuit state hasn't changed, abort (unless bForce is true). - 1. If the schedule is on, the egg timer does not come into play - 2. If the schedule is off... - 2.1. and the egg timer will turn off the circuit off before the schedule starts, use egg timer time - 2.2. else if the schedule will start before the egg timer turns it off, use the schedule end time - 3. Iterate over each schedule for 1-2 above; nearest end time wins - */ - try { - if (thing.dontStop || !isOn) { - thingState.endTime = undefined; - } - else if (!thingState.isOn && isOn || bForce) { - let endTime: Timestamp; - let eggTimerEndTime: Timestamp; - // let remainingDuration: number; - if (typeof thing.eggTimer !== 'undefined') { - eggTimerEndTime = state.time.clone().addHours(0, thing.eggTimer); + public async setLightGroupAsync(obj: any, send: boolean = true): Promise { + let group: LightGroup = null; + let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1; + if (id <= 0) { + // We are adding a circuit group. + id = sys.circuitGroups.getNextEquipmentId(sys.board.equipmentIds.circuitGroups); } - // egg timers don't come into play if a schedule will control the circuit - // schedules don't come into play if the circuit is in manualPriority - if (!thingState.manualPriorityActive) { - - for (let i = 0; i < sys.schedules.length; i++) { - let sched = sys.schedules.getItemByIndex(i); - let ssched = state.schedules.getItemById(sched.id); - if (sched.isActive && sys.board.schedules.includesCircuit(sched, thing.id)) { - let nearestStartTime = sys.board.schedules.getNearestStartTime(sched); - let nearestEndTime = sys.board.schedules.getNearestEndTime(sched); - // if the schedule doesn't have an end date (eg no days)... - if (nearestEndTime.getTime() === 0) continue; - if (ssched.isOn) { - if (typeof endTime === 'undefined' || nearestEndTime.getTime() < endTime.getTime()) { - endTime = nearestEndTime.clone(); - eggTimerEndTime = undefined; - } - } - else { - if (typeof eggTimerEndTime !== 'undefined' && eggTimerEndTime.getTime() < nearestStartTime.getTime()) { - if (typeof endTime === 'undefined' || eggTimerEndTime.getTime() < endTime.getTime()) endTime = eggTimerEndTime.clone(); + if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit light group id exceeded`, id, 'LightGroup')); + if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'LightGroup')); + group = sys.lightGroups.getItemById(id, true); + let sgroup = state.lightGroups.getItemById(id, true); + return new Promise((resolve, reject) => { + if (typeof obj.name !== 'undefined') sgroup.name = group.name = obj.name; + if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440; + if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440); + group.dontStop = group.eggTimer === 1440; + group.isActive = true; + if (typeof obj.circuits !== 'undefined') { + for (let i = 0; i < obj.circuits.length; i++) { + let cobj = obj.circuits[i]; + let c: LightGroupCircuit; + if (typeof cobj.id !== 'undefined') c = group.circuits.getItemById(parseInt(cobj.id, 10), true); + else if (typeof cobj.circuit !== 'undefined') c = group.circuits.getItemByCircuitId(parseInt(cobj.circuit, 10), true); + else c = group.circuits.getItemByIndex(i, true, { id: i + 1 }); + if (typeof cobj.circuit !== 'undefined') c.circuit = cobj.circuit; + if (typeof cobj.lightingTheme !== 'undefined') c.lightingTheme = parseInt(cobj.lightingTheme, 10); + if (typeof cobj.color !== 'undefined') c.color = parseInt(cobj.color, 10); + if (typeof cobj.swimDelay !== 'undefined') c.swimDelay = parseInt(cobj.swimDelay, 10); + if (typeof cobj.position !== 'undefined') c.position = parseInt(cobj.position, 10); } - else if (typeof endTime === 'undefined' || nearestEndTime.getTime() < endTime.getTime()) endTime = nearestEndTime.clone(); - } + // group.circuits.length = obj.circuits.length; // RSG - removed as this will delete circuits that were not changed } - } + resolve(group); + }); + } + public async deleteCircuitGroupAsync(obj: any): Promise { + let id = parseInt(obj.id, 10); + if (isNaN(id)) return Promise.reject(new EquipmentNotFoundError(`Invalid group id: ${obj.id}`, 'CircuitGroup')); + //if (!sys.board.equipmentIds.circuitGroups.isInRange(id)) return; + if (typeof id !== 'undefined') { + let group = sys.circuitGroups.getItemById(id, false); + let sgroup = state.circuitGroups.getItemById(id, false); + sys.circuitGroups.removeItemById(id); + state.circuitGroups.removeItemById(id); + group.isActive = false; + sgroup.isOn = false; + sgroup.isActive = false; + sgroup.showInFeatures = false; + sgroup.emitEquipmentChange(); + return new Promise((resolve, reject) => { resolve(group); }); } - if (typeof endTime !== 'undefined') thingState.endTime = endTime; - else if (typeof eggTimerEndTime !== 'undefined') thingState.endTime = eggTimerEndTime; - } + else + return Promise.reject(new InvalidEquipmentIdError('Group id has not been defined', id, 'CircuitGroup')); } - catch (err) { - logger.error(`Error setting end time for ${thing.id}: ${err}`) + public async deleteLightGroupAsync(obj: any): Promise { + let id = parseInt(obj.id, 10); + if (isNaN(id)) return Promise.reject(new EquipmentNotFoundError(`Invalid group id: ${obj.id}`, 'LightGroup')); + if (!sys.board.equipmentIds.circuitGroups.isInRange(id)) return; + if (typeof obj.id !== 'undefined') { + let group = sys.lightGroups.getItemById(id, false); + let sgroup = state.lightGroups.getItemById(id, false); + sys.lightGroups.removeItemById(id); + state.lightGroups.removeItemById(id); + group.isActive = false; + sgroup.isOn = false; + sgroup.isActive = false; + sgroup.emitEquipmentChange(); + return new Promise((resolve, reject) => { resolve(group); }); + } + else + return Promise.reject(new InvalidEquipmentIdError('Group id has not been defined', id, 'LightGroup')); } - } - public async turnOffDrainCircuits(ignoreDelays: boolean) { - try { - { - let drt = sys.board.valueMaps.circuitFunctions.getValue('spadrain'); - let drains = sys.circuits.filter(x => { return x.type === drt }); - for (let i = 0; i < drains.length; i++) { - let drain = drains.getItemByIndex(i); - let sdrain = state.circuits.getItemById(drain.id); - if (sdrain.isOn) await sys.board.circuits.setCircuitStateAsync(drain.id, false, ignoreDelays); - sdrain.startDelay = false; - sdrain.stopDelay = false; + public async deleteCircuitAsync(data: any): Promise { + if (typeof data.id === 'undefined') return Promise.reject(new InvalidEquipmentIdError('You must provide an id to delete a circuit', data.id, 'Circuit')); + let circuit = sys.circuits.getInterfaceById(data.id); + if (circuit.master === 1) await ncp.circuits.deleteCircuitAsync(circuit.id); + if (circuit instanceof Circuit) { + sys.circuits.removeItemById(data.id); + state.circuits.removeItemById(data.id); } - } - { - let drt = sys.board.valueMaps.featureFunctions.getValue('spadrain'); - let drains = sys.features.filter(x => { return x.type === drt }); - for (let i = 0; i < drains.length; i++) { - let drain = drains.getItemByIndex(i); - let sdrain = state.features.getItemById(drain.id); - if (sdrain.isOn) await sys.board.features.setFeatureStateAsync(drain.id, false, ignoreDelays); + if (circuit instanceof Feature) { + sys.features.removeItemById(data.id); + state.features.removeItemById(data.id); } - } - - } catch (err) { return Promise.reject(new BoardProcessError(`turnOffDrainCircuits: ${err.message}`)); } - } - public async turnOffCleanerCircuits(bstate: BodyTempState, ignoreDelays?: boolean) { - try { - // First we have to get all the cleaner circuits that are associated with the - // body. To do this we get the circuit functions for all cleaner types associated with the body. - // - // Cleaner ciruits can always be turned off. However, they cannot always be turned on. - let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('cleaner') !== -1 && x.body === bstate.id; }); - let cleaners = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 }); - // So now we should have all the cleaner circuits so lets make sure they are off. - for (let i = 0; i < cleaners.length; i++) { - let cleaner = cleaners.getItemByIndex(i); - if (cleaner.isActive) { - let cstate = state.circuits.getItemById(cleaner.id, true); - if (cstate.isOn || cstate.startDelay) await sys.board.circuits.setCircuitStateAsync(cleaner.id, false, ignoreDelays); + return new Promise((resolve, reject) => { resolve(circuit); }); + } + public deleteCircuit(data: any) { + if (typeof data.id !== 'undefined') { + let circuit = sys.circuits.getInterfaceById(data.id); + if (circuit instanceof Circuit) { + sys.circuits.removeItemById(data.id); + state.circuits.removeItemById(data.id); + return; + } + if (circuit instanceof Feature) { + sys.features.removeItemById(data.id); + state.features.removeItemById(data.id); + return; + } } - } - } catch (err) { return Promise.reject(new BoardProcessError(`turnOffCleanerCircuits: ${err.message}`)); } - } - public async turnOffSpillwayCircuits(ignoreDelays?: boolean) { - try { - { - let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('spillway') !== -1 }); - let spillways = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 }); - // So now we should have all the cleaner circuits so lets make sure they are off. - for (let i = 0; i < spillways.length; i++) { - let spillway = spillways.getItemByIndex(i); - if (spillway.isActive) { - let cstate = state.circuits.getItemById(spillway.id, true); - if (cstate.isOn || cstate.startDelay) await sys.board.circuits.setCircuitStateAsync(spillway.id, false, ignoreDelays); - } + } + public getNameById(id: number) { + if (id < 200) + return sys.board.valueMaps.circuitNames.transform(id).desc; + else + return sys.customNames.getItemById(id - 200).name; + } + public async setLightGroupThemeAsync(id: number, theme: number): Promise { + const grp = sys.lightGroups.getItemById(id); + const sgrp = state.lightGroups.getItemById(id); + grp.lightingTheme = sgrp.lightingTheme = theme; + for (let i = 0; i < grp.circuits.length; i++) { + let c = grp.circuits.getItemByIndex(i); + let cstate = state.circuits.getItemById(c.circuit); + // if theme is 'off' light groups should not turn on + if (cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) === 'off') + await sys.board.circuits.setCircuitStateAsync(c.circuit, false); + else if (!cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) !== 'off') await sys.board.circuits.setCircuitStateAsync(c.circuit, true); } - } - { - let arrTypes = sys.board.valueMaps.featureFunctions.toArray().filter(x => { return x.name.indexOf('spillway') !== -1 }); - let spillways = sys.features.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 }); - // So now we should have all the cleaner features so lets make sure they are off. - for (let i = 0; i < spillways.length; i++) { - let spillway = spillways.getItemByIndex(i); - if (spillway.isActive) { - let cstate = state.features.getItemById(spillway.id, true); - if (cstate.isOn) await sys.board.features.setFeatureStateAsync(spillway.id, false, ignoreDelays); - } + let isOn = sys.board.valueMaps.lightThemes.getName(theme) === 'off' ? false : true; + sys.board.circuits.setEndTime(grp, sgrp, isOn); + sgrp.isOn = isOn; + // If we truly want to support themes in lightGroups we probably need to program + // the specific on/off toggles to enable that. For now this will go through the motions but it's just a pretender. + switch (theme) { + case 0: // off + case 1: // on + break; + case 128: // sync + setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'sync'); }); + break; + case 144: // swim + setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'swim'); }); + break; + case 160: // swim + setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'set'); }); + break; + case 190: // save + case 191: // recall + setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'other'); }); + break; + default: + setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'color'); }); + // other themes for magicstream? } - } - } catch (err) { return Promise.reject(new BoardProcessError(`turnOffSpillwayCircuits: ${err.message}`)); } - } + sgrp.hasChanged = true; // Say we are dirty but we really are pure as the driven snow. + state.emitEquipmentChanges(); + return Promise.resolve(sgrp); + } + public async setLightGroupAttribsAsync(group: LightGroup): Promise { + let grp = sys.lightGroups.getItemById(group.id); + try { + grp.circuits.clear(); + for (let i = 0; i < group.circuits.length; i++) { + let circuit = grp.circuits.getItemByIndex(i); + grp.circuits.add({ id: i, circuit: circuit.circuit, color: circuit.color, position: i, swimDelay: circuit.swimDelay }); + } + let sgrp = state.lightGroups.getItemById(group.id); + sgrp.hasChanged = true; // Say we are dirty but we really are pure as the driven snow. + return Promise.resolve(grp); + } + catch (err) { return Promise.reject(err); } + } + public async sequenceLightGroupAsync(id: number, operation: string): Promise { + let sgroup = state.lightGroups.getItemById(id); + // This is the default action which really does nothing. + try { + let nop = sys.board.valueMaps.circuitActions.getValue(operation); + if (nop > 0) { + sgroup.action = nop; + sgroup.emitEquipmentChange(); + await setTimeout(10000); + sgroup.action = 0; + state.emitAllEquipmentChanges(); + } + return sgroup; + } catch (err) { return Promise.reject(new InvalidOperationError(`Error sequencing light group ${err.message}`, 'sequenceLightGroupAsync')); } + } + public async setCircuitGroupStateAsync(id: number, val: boolean): Promise { + let grp = sys.circuitGroups.getItemById(id, false, { isActive: false }); + logger.info(`Setting Circuit Group State`); + let gstate = (grp.dataName === 'circuitGroupConfig') ? state.circuitGroups.getItemById(grp.id, grp.isActive !== false) : state.lightGroups.getItemById(grp.id, grp.isActive !== false); + let circuits = grp.circuits.toArray(); + let arr = []; + for (let i = 0; i < circuits.length; i++) { + let circuit = circuits[i]; + // if the circuit group is turned on, we want the desired state of the individual circuits; + // if the circuit group is turned off, we want the opposite of the desired state + arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, val ? circuit.desiredState : !circuit.desiredState)); + } + return new Promise(async (resolve, reject) => { + await Promise.all(arr).catch((err) => { reject(err) }); + gstate.emitEquipmentChange(); + sys.board.circuits.setEndTime(grp, gstate, val); + gstate.isOn = val; + resolve(gstate); + }); + } + public async setLightGroupStateAsync(id: number, val: boolean): Promise { + return sys.board.circuits.setCircuitGroupStateAsync(id, val); + } + public setEndTime(thing: ICircuit, thingState: ICircuitState, isOn: boolean, bForce: boolean = false) { + /* + this is a generic fn for circuits, features, circuitGroups, lightGroups + to set the end time based on the egg timer. + it will be called from set[]StateAsync calls as well as when then state is + eval'ed from status packets/external messages and schedule changes. + instead of maintaining timers here which would increase the amount of + emits substantially, let the clients keep their own local timers + or just display the end time. + + bForce is an override sent by the syncScheduleStates. It gets set after the circuit gets set but we need to know if the sched is on. This allows the circuit end time to be + re-evaluated even though it already has an end time. + + Logic gets fun here... + 0. If the circuit is off, or has don't stop enabled, don't set an end time + 0.1. If the circuit state hasn't changed, abort (unless bForce is true). + 1. If the schedule is on, the egg timer does not come into play + 2. If the schedule is off... + 2.1. and the egg timer will turn off the circuit off before the schedule starts, use egg timer time + 2.2. else if the schedule will start before the egg timer turns it off, use the schedule end time + 3. Iterate over each schedule for 1-2 above; nearest end time wins + */ + try { + if (!isOn) thingState.startTime = thingState.endTime = undefined; + else if (!thingState.isOn && isOn || bForce) { + if (!thingState.isOn && isOn && typeof thingState.startTime === 'undefined') thingState.startTime = new Timestamp(new Date()); + let schedTime: Date; + let eggTime: Date = thing.dontStop ? undefined : new Date(new Date().getTime() + ((thing.eggTimer || 720) * 60000)); + if (!thingState.manualPriorityActive) { + let sscheds = state.schedules.getActiveSchedules(); + // If a schedule happens to be on for this circuit then we will be turning it off at the max end time for the + // circuit. + for (let i = 0; i < sscheds.length; i++) { + let ssched = sscheds[i]; + let st = ssched.scheduleTime; + // Don't thrown an error on uncalculable schedules. + if (typeof st === 'undefined' || typeof st.startTime === 'undefined' || typeof st.endTime === 'undefined') continue; + if (ssched.isOn || + (typeof eggTime !== 'undefined' && st.startTime.getTime() < eggTime.getTime())) { + // If the schedule is on or it will start within the egg timer then we need the max end time of the schedule. + let sched = sys.schedules.getItemById(ssched.id); + if (sys.board.schedules.includesCircuit(sched, thing.id)) { + if (typeof schedTime === 'undefined' || st.endTime.getTime() > schedTime.getTime()) schedTime = st.endTime; + } + } + } + } + //console.log({ f: bForce, isOn:isOn, eggTime: Timestamp.toISOLocal(eggTime), schedTime: Timestamp.toISOLocal(schedTime) }); + if (typeof schedTime !== 'undefined' && schedTime) thingState.endTime = new Timestamp(schedTime); + else if (typeof eggTime !== 'undefined' && eggTime) thingState.endTime = new Timestamp(eggTime); + else thingState.endTime = undefined; + } + } + catch (err) { + logger.error(`Error setting end time for ${thing.id}: ${err}`) + } + } + public async turnOffDrainCircuits(ignoreDelays: boolean) { + try { + { + let drt = sys.board.valueMaps.circuitFunctions.getValue('spadrain'); + let drains = sys.circuits.filter(x => { return x.type === drt }); + for (let i = 0; i < drains.length; i++) { + let drain = drains.getItemByIndex(i); + let sdrain = state.circuits.getItemById(drain.id); + if (sdrain.isOn) await sys.board.circuits.setCircuitStateAsync(drain.id, false, ignoreDelays); + sdrain.startDelay = false; + sdrain.stopDelay = false; + } + } + { + let drt = sys.board.valueMaps.featureFunctions.getValue('spadrain'); + let drains = sys.features.filter(x => { return x.type === drt }); + for (let i = 0; i < drains.length; i++) { + let drain = drains.getItemByIndex(i); + let sdrain = state.features.getItemById(drain.id); + if (sdrain.isOn) await sys.board.features.setFeatureStateAsync(drain.id, false, ignoreDelays); + } + } + + } catch (err) { return Promise.reject(new BoardProcessError(`turnOffDrainCircuits: ${err.message}`)); } + } + public async turnOffCleanerCircuits(bstate: BodyTempState, ignoreDelays?: boolean) { + try { + // First we have to get all the cleaner circuits that are associated with the + // body. To do this we get the circuit functions for all cleaner types associated with the body. + // + // Cleaner ciruits can always be turned off. However, they cannot always be turned on. + let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('cleaner') !== -1 && x.body === bstate.id; }); + let cleaners = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 }); + // So now we should have all the cleaner circuits so lets make sure they are off. + for (let i = 0; i < cleaners.length; i++) { + let cleaner = cleaners.getItemByIndex(i); + if (cleaner.isActive) { + let cstate = state.circuits.getItemById(cleaner.id, true); + if (cstate.isOn || cstate.startDelay) await sys.board.circuits.setCircuitStateAsync(cleaner.id, false, ignoreDelays); + } + } + } catch (err) { return Promise.reject(new BoardProcessError(`turnOffCleanerCircuits: ${err.message}`)); } + } + public async turnOffSpillwayCircuits(ignoreDelays?: boolean) { + try { + { + let arrTypes = sys.board.valueMaps.circuitFunctions.toArray().filter(x => { return x.name.indexOf('spillway') !== -1 }); + let spillways = sys.circuits.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 }); + // So now we should have all the cleaner circuits so lets make sure they are off. + for (let i = 0; i < spillways.length; i++) { + let spillway = spillways.getItemByIndex(i); + if (spillway.isActive) { + let cstate = state.circuits.getItemById(spillway.id, true); + if (cstate.isOn || cstate.startDelay) await sys.board.circuits.setCircuitStateAsync(spillway.id, false, ignoreDelays); + } + } + } + { + let arrTypes = sys.board.valueMaps.featureFunctions.toArray().filter(x => { return x.name.indexOf('spillway') !== -1 }); + let spillways = sys.features.filter(x => { return arrTypes.findIndex(t => { return t.val === x.type }) !== -1 }); + // So now we should have all the cleaner features so lets make sure they are off. + for (let i = 0; i < spillways.length; i++) { + let spillway = spillways.getItemByIndex(i); + if (spillway.isActive) { + let cstate = state.features.getItemById(spillway.id, true); + if (cstate.isOn) await sys.board.features.setFeatureStateAsync(spillway.id, false, ignoreDelays); + } + } + } + } catch (err) { return Promise.reject(new BoardProcessError(`turnOffSpillwayCircuits: ${err.message}`)); } + } } export class FeatureCommands extends BoardCommands { - public getFeatureFunctions() { - let cf = sys.board.valueMaps.featureFunctions.toArray(); - if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' }); - return cf; - } + public getFeatureFunctions() { + let cf = sys.board.valueMaps.featureFunctions.toArray(); + if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' }); + return cf; + } - public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { - try { - // First delete the features that should be removed. - for (let i = 0; i < ctx.features.remove.length; i++) { - let f = ctx.features.remove[i]; - try { - await sys.board.features.deleteFeatureAsync(f); - res.addModuleSuccess('feature', `Remove: ${f.id}-${f.name}`); - } catch (err) { res.addModuleError('feature', `Remove: ${f.id}-${f.name}: ${err.message}`) } - } - for (let i = 0; i < ctx.features.update.length; i++) { - let f = ctx.features.update[i]; + public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { try { - await sys.board.features.setFeatureAsync(f); - res.addModuleSuccess('feature', `Update: ${f.id}-${f.name}`); - } catch (err) { res.addModuleError('feature', `Update: ${f.id}-${f.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.features.add.length; i++) { - // pull a little trick to first add the data then perform the update. This way we won't get a new id or - // it won't error out. - let f = ctx.features.add[i]; + // First delete the features that should be removed. + for (let i = 0; i < ctx.features.remove.length; i++) { + let f = ctx.features.remove[i]; + try { + await sys.board.features.deleteFeatureAsync(f); + res.addModuleSuccess('feature', `Remove: ${f.id}-${f.name}`); + } catch (err) { res.addModuleError('feature', `Remove: ${f.id}-${f.name}: ${err.message}`) } + } + for (let i = 0; i < ctx.features.update.length; i++) { + let f = ctx.features.update[i]; + try { + await sys.board.features.setFeatureAsync(f); + res.addModuleSuccess('feature', `Update: ${f.id}-${f.name}`); + } catch (err) { res.addModuleError('feature', `Update: ${f.id}-${f.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.features.add.length; i++) { + // pull a little trick to first add the data then perform the update. This way we won't get a new id or + // it won't error out. + let f = ctx.features.add[i]; + try { + sys.features.getItemById(f, true); + await sys.board.features.setFeatureAsync(f); + res.addModuleSuccess('feature', `Add: ${f.id}-${f.name}`); + } catch (err) { res.addModuleError('feature', `Add: ${f.id}-${f.name}: ${err.message}`) } + } + return true; + } catch (err) { logger.error(`Error restoring features: ${err.message}`); res.addModuleError('system', `Error restoring features: ${err.message}`); return false; } + } + public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> { try { - sys.features.getItemById(f, true); - await sys.board.features.setFeatureAsync(f); - res.addModuleSuccess('feature', `Add: ${f.id}-${f.name}`); - } catch (err) { res.addModuleError('feature', `Add: ${f.id}-${f.name}: ${err.message}`) } - } - return true; - } catch (err) { logger.error(`Error restoring features: ${err.message}`); res.addModuleError('system', `Error restoring features: ${err.message}`); return false; } - } - public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> { - try { - let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; - // Look at features. - let cfg = rest.poolConfig; - for (let i = 0; i < cfg.features.length; i++) { - let r = cfg.features[i]; - let c = sys.features.find(elem => r.id === elem.id); - if (typeof c === 'undefined') ctx.add.push(r); - else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); - } - for (let i = 0; i < sys.features.length; i++) { - let c = sys.features.getItemByIndex(i); - let r = cfg.features.find(elem => elem.id == c.id); - if (typeof r === 'undefined') ctx.remove.push(c.get(true)); - } - return ctx; - } catch (err) { logger.error(`Error validating features for restore: ${err.message}`); } - } + let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; + // Look at features. + let cfg = rest.poolConfig; + for (let i = 0; i < cfg.features.length; i++) { + let r = cfg.features[i]; + let c = sys.features.find(elem => r.id === elem.id); + if (typeof c === 'undefined') ctx.add.push(r); + else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); + } + for (let i = 0; i < sys.features.length; i++) { + let c = sys.features.getItemByIndex(i); + let r = cfg.features.find(elem => elem.id == c.id); + if (typeof r === 'undefined') ctx.remove.push(c.get(true)); + } + return ctx; + } catch (err) { logger.error(`Error validating features for restore: ${err.message}`); } + } public async setFeatureAsync(obj: any): Promise { let id = parseInt(obj.id, 10); @@ -3248,508 +3211,505 @@ export class FeatureCommands extends BoardCommands { feature.dontStop = feature.eggTimer === 1440; return new Promise((resolve, reject) => { resolve(feature); }); } - public async deleteFeatureAsync(obj: any): Promise { - let id = parseInt(obj.id, 10); - if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${obj.id}`, obj.id, 'Feature')); - if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${obj.id}`, obj.id, 'Feature')); - if (typeof obj.id !== 'undefined') { - let feature = sys.features.getItemById(id, false); - let sfeature = state.features.getItemById(id, false); - sys.features.removeItemById(id); - state.features.removeItemById(id); - feature.isActive = false; - sfeature.isOn = false; - sfeature.showInFeatures = false; - sfeature.emitEquipmentChange(); - return new Promise((resolve, reject) => { resolve(feature); }); - } - else - Promise.reject(new InvalidEquipmentIdError('Feature id has not been defined', undefined, 'Feature')); - } - public async setFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { - try { - if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature')); - if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature')); - let feature = sys.features.getItemById(id); - let fstate = state.features.getItemById(feature.id, feature.isActive !== false); - sys.board.circuits.setEndTime(feature, fstate, val); - fstate.isOn = val; - sys.board.valves.syncValveStates(); - ncp.pumps.syncPumpStates(); - state.emitEquipmentChanges(); - return fstate; - } catch (err) { return Promise.reject(new Error(`Error setting feature state ${err.message}`)); } - } - public async toggleFeatureStateAsync(id: number): Promise { - let feat = state.features.getItemById(id); - return this.setFeatureStateAsync(id, !(feat.isOn || false)); - } - public syncGroupStates() { - try { - for (let i = 0; i < sys.circuitGroups.length; i++) { - let grp: CircuitGroup = sys.circuitGroups.getItemByIndex(i); - let circuits = grp.circuits.toArray(); - let bIsOn = false; - let bSyncOn = true; - // This should only show the group as on if all the states are correct. - if (grp.isActive) { - for (let j = 0; j < circuits.length; j++) { - let circuit: CircuitGroupCircuit = grp.circuits.getItemByIndex(j); - let cstate = state.circuits.getInterfaceById(circuit.circuit); - //logger.info(`Synchronizing circuit group ${cstate.name}: ${cstate.isOn} = ${circuit.desiredState}`); - if (circuit.desiredState === 1 || circuit.desiredState === 0) { - if (cstate.isOn === utils.makeBool(circuit.desiredState)) { - bIsOn = true; - } - else bSyncOn = false; - } - } - } - let sgrp = state.circuitGroups.getItemById(grp.id); - let isOn = bIsOn && bSyncOn && grp.isActive; - if (isOn !== sgrp.isOn) { - sys.board.circuits.setEndTime(grp, sgrp, isOn); - sgrp.isOn = isOn; - } - sys.board.valves.syncValveStates(); - } - // I am guessing that there will only be one here but iterate - // just in case we expand. - for (let i = 0; i < sys.lightGroups.length; i++) { - let grp: LightGroup = sys.lightGroups.getItemByIndex(i); - let bIsOn = false; - if (grp.isActive) { - let circuits = grp.circuits.toArray(); - for (let j = 0; j < circuits.length; j++) { - let circuit = grp.circuits.getItemByIndex(j).circuit; - let cstate = state.circuits.getInterfaceById(circuit); - if (cstate.isOn) bIsOn = true; - } - } - let sgrp = state.lightGroups.getItemById(grp.id); - if (bIsOn !== sgrp.isOn) { - sys.board.circuits.setEndTime(grp, sgrp, bIsOn); - sgrp.isOn = bIsOn; + public async deleteFeatureAsync(obj: any): Promise { + let id = parseInt(obj.id, 10); + if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${obj.id}`, obj.id, 'Feature')); + if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${obj.id}`, obj.id, 'Feature')); + if (typeof obj.id !== 'undefined') { + let feature = sys.features.getItemById(id, false); + let sfeature = state.features.getItemById(id, false); + sys.features.removeItemById(id); + state.features.removeItemById(id); + feature.isActive = false; + sfeature.isOn = false; + sfeature.showInFeatures = false; + sfeature.emitEquipmentChange(); + return new Promise((resolve, reject) => { resolve(feature); }); } - } - state.emitEquipmentChanges(); - } catch (err) { logger.error(`Error synchronizing group circuits. ${err}`); } - } + else + Promise.reject(new InvalidEquipmentIdError('Feature id has not been defined', undefined, 'Feature')); + } + public async setFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { + try { + if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature')); + if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature')); + let feature = sys.features.getItemById(id); + let fstate = state.features.getItemById(feature.id, feature.isActive !== false); + sys.board.circuits.setEndTime(feature, fstate, val); + fstate.isOn = val; + sys.board.valves.syncValveStates(); + ncp.pumps.syncPumpStates(); + state.emitEquipmentChanges(); + return fstate; + } catch (err) { return Promise.reject(new Error(`Error setting feature state ${err.message}`)); } + } + public async toggleFeatureStateAsync(id: number): Promise { + let feat = state.features.getItemById(id); + return this.setFeatureStateAsync(id, !(feat.isOn || false)); + } + public syncGroupStates() { + try { + for (let i = 0; i < sys.circuitGroups.length; i++) { + let grp: CircuitGroup = sys.circuitGroups.getItemByIndex(i); + let circuits = grp.circuits.toArray(); + let bIsOn = false; + let bSyncOn = true; + // This should only show the group as on if all the states are correct. + if (grp.isActive) { + for (let j = 0; j < circuits.length; j++) { + let circuit: CircuitGroupCircuit = grp.circuits.getItemByIndex(j); + let cstate = state.circuits.getInterfaceById(circuit.circuit); + //logger.info(`Synchronizing circuit group ${cstate.name}: ${cstate.isOn} = ${circuit.desiredState}`); + if (circuit.desiredState === 1 || circuit.desiredState === 0) { + if (cstate.isOn === utils.makeBool(circuit.desiredState)) { + bIsOn = true; + } + else bSyncOn = false; + } + } + } + let sgrp = state.circuitGroups.getItemById(grp.id); + let isOn = bIsOn && bSyncOn && grp.isActive; + if (isOn !== sgrp.isOn) { + sys.board.circuits.setEndTime(grp, sgrp, isOn); + sgrp.isOn = isOn; + } + sys.board.valves.syncValveStates(); + } + // I am guessing that there will only be one here but iterate + // just in case we expand. + for (let i = 0; i < sys.lightGroups.length; i++) { + let grp: LightGroup = sys.lightGroups.getItemByIndex(i); + let bIsOn = false; + if (grp.isActive) { + let circuits = grp.circuits.toArray(); + for (let j = 0; j < circuits.length; j++) { + let circuit = grp.circuits.getItemByIndex(j).circuit; + let cstate = state.circuits.getInterfaceById(circuit); + if (cstate.isOn) bIsOn = true; + } + } + let sgrp = state.lightGroups.getItemById(grp.id); + if (bIsOn !== sgrp.isOn) { + sys.board.circuits.setEndTime(grp, sgrp, bIsOn); + sgrp.isOn = bIsOn; + } + } + state.emitEquipmentChanges(); + } catch (err) { logger.error(`Error synchronizing group circuits. ${err}`); } + } } export class ChlorinatorCommands extends BoardCommands { - public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { - try { - // First delete the chlorinators that should be removed. - for (let i = 0; i < ctx.chlorinators.remove.length; i++) { - let c = ctx.chlorinators.remove[i]; + public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { try { - await sys.board.chlorinator.deleteChlorAsync(c); - res.addModuleSuccess('chlorinator', `Remove: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('chlorinator', `Remove: ${c.id}-${c.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.chlorinators.update.length; i++) { - let c = ctx.chlorinators.update[i]; + // First delete the chlorinators that should be removed. + for (let i = 0; i < ctx.chlorinators.remove.length; i++) { + let c = ctx.chlorinators.remove[i]; + try { + await sys.board.chlorinator.deleteChlorAsync(c); + res.addModuleSuccess('chlorinator', `Remove: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('chlorinator', `Remove: ${c.id}-${c.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.chlorinators.update.length; i++) { + let c = ctx.chlorinators.update[i]; + try { + await sys.board.chlorinator.setChlorAsync(c); + res.addModuleSuccess('chlorinator', `Update: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('chlorinator', `Update: ${c.id}-${c.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.chlorinators.add.length; i++) { + let c = ctx.chlorinators.add[i]; + try { + // pull a little trick to first add the data then perform the update. This way we won't get a new id or + // it won't error out. + sys.chlorinators.getItemById(c.id, true); + await sys.board.chlorinator.setChlorAsync(c); + res.addModuleSuccess('chlorinator', `Add: ${c.id}-${c.name}`); + } catch (err) { res.addModuleError('chlorinator', `Add: ${c.id}-${c.name}: ${err.message}`); } + } + return true; + } catch (err) { logger.error(`Error restoring chlorinators: ${err.message}`); res.addModuleError('system', `Error restoring chlorinators: ${err.message}`); return false; } + } + + public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> { try { - await sys.board.chlorinator.setChlorAsync(c); - res.addModuleSuccess('chlorinator', `Update: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('chlorinator', `Update: ${c.id}-${c.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.chlorinators.add.length; i++) { - let c = ctx.chlorinators.add[i]; + let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; + // Look at chlorinators. + let cfg = rest.poolConfig; + for (let i = 0; i < cfg.chlorinators.length; i++) { + let r = cfg.chlorinators[i]; + let c = sys.chlorinators.find(elem => r.id === elem.id); + if (typeof c === 'undefined') ctx.add.push(r); + else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); + } + for (let i = 0; i < sys.chlorinators.length; i++) { + let c = sys.chlorinators.getItemByIndex(i); + let r = cfg.chlorinators.find(elem => elem.id == c.id); + if (typeof r === 'undefined') ctx.remove.push(c.get(true)); + } + return ctx; + } catch (err) { logger.error(`Error validating chlorinators for restore: ${err.message}`); } + } + + public async setChlorAsync(obj: any, send: boolean = true): Promise { try { - // pull a little trick to first add the data then perform the update. This way we won't get a new id or - // it won't error out. - sys.chlorinators.getItemById(c.id, true); - await sys.board.chlorinator.setChlorAsync(c); - res.addModuleSuccess('chlorinator', `Add: ${c.id}-${c.name}`); - } catch (err) { res.addModuleError('chlorinator', `Add: ${c.id}-${c.name}: ${err.message}`); } - } - return true; - } catch (err) { logger.error(`Error restoring chlorinators: ${err.message}`); res.addModuleError('system', `Error restoring chlorinators: ${err.message}`); return false; } - } + let id = parseInt(obj.id, 10); + let chlor: Chlorinator; + let master = parseInt(obj.master, 10); + let portId = typeof obj.portId !== 'undefined' ? parseInt(obj.portId, 10) : 0; + if (isNaN(master)) master = 1; // NCP to control. + if (isNaN(id) || id <= 0) { + let body = sys.board.bodies.mapBodyAssociation(typeof obj.body !== 'undefined' ? parseInt(obj.body, 10) : 0); + if (typeof body === 'undefined') { + if (sys.equipment.shared) body = 32; + else if (!sys.equipment.dual) body = 1; + else return Promise.reject(new InvalidEquipmentDataError(`Chlorinator body association is not valid: ${body}`, 'chlorinator', body)); + } + let poolSetpoint = typeof obj.poolSetpoint !== 'undefined' ? parseInt(obj.poolSetpoint, 10) : 50; + let spaSetpoint = typeof obj.spaSetpoint !== 'undefined' ? parseInt(obj.spaSetpoint, 10) : 10; + if (isNaN(poolSetpoint) || poolSetpoint > 100 || poolSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator poolSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', poolSetpoint)); + if (isNaN(spaSetpoint) || spaSetpoint > 100 || spaSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator spaSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', spaSetpoint)); + if (master === 2) { + // We can add as many external chlorinators as we want. + id = sys.chlorinators.count(elem => elem.master === 2) + 50; + chlor = sys.chlorinators.getItemById(id, true, { id: id, master: parseInt(obj.master, 10) }); + } + else { + if (portId === 0 && sys.controllerType !== ControllerType.Nixie) return Promise.reject(new InvalidEquipmentDataError(`You may not install a chlorinator on an ${sys.controllerType} system that is assigned to the Primary Port`, 'Chlorinator', portId)); + if (sys.chlorinators.count(elem => elem.portId === portId && elem.master !== 2) > 0) return Promise.reject(new InvalidEquipmentDataError(`There is already a chlorinator using port #${portId}. Only one chlorinator may be installed per port.`, 'Chlorinator', portId)); + // We are adding so we need to see if there is another chlorinator that is not external. + if (sys.chlorinators.count(elem => elem.master !== 2) > sys.equipment.maxChlorinators) return Promise.reject(new InvalidEquipmentDataError(`The max number of chlorinators has been exceeded you may only add ${sys.equipment.maxChlorinators}`, 'Chlorinator', sys.equipment.maxChlorinators)); + id = sys.chlorinators.getMaxId(false, 0) + 1; + chlor = sys.chlorinators.getItemById(id, true, { id: id, master: 1 }); + } + } + else chlor = sys.chlorinators.getItemById(id, false); - public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> { - try { - let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; - // Look at chlorinators. - let cfg = rest.poolConfig; - for (let i = 0; i < cfg.chlorinators.length; i++) { - let r = cfg.chlorinators[i]; - let c = sys.chlorinators.find(elem => r.id === elem.id); - if (typeof c === 'undefined') ctx.add.push(r); - else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); - } - for (let i = 0; i < sys.chlorinators.length; i++) { - let c = sys.chlorinators.getItemByIndex(i); - let r = cfg.chlorinators.find(elem => elem.id == c.id); - if (typeof r === 'undefined') ctx.remove.push(c.get(true)); - } - return ctx; - } catch (err) { logger.error(`Error validating chlorinators for restore: ${err.message}`); } - } + if (chlor.master >= 1) + await ncp.chlorinators.setChlorinatorAsync(chlor, obj); + else { + let body = sys.board.bodies.mapBodyAssociation(typeof obj.body !== 'undefined' ? parseInt(obj.body, 10) : chlor.body); + if (typeof body === 'undefined') { + if (sys.equipment.shared) body = 32; + else if (!sys.equipment.dual) body = 1; + else return Promise.reject(new InvalidEquipmentDataError(`Chlorinator body association is not valid: ${body}`, 'chlorinator', body)); + } + let poolSetpoint = typeof obj.poolSetpoint !== 'undefined' ? parseInt(obj.poolSetpoint, 10) : isNaN(chlor.poolSetpoint) ? 50 : chlor.poolSetpoint; + let spaSetpoint = typeof obj.spaSetpoint !== 'undefined' ? parseInt(obj.spaSetpoint, 10) : isNaN(chlor.spaSetpoint) ? 10 : chlor.spaSetpoint; + if (poolSetpoint > 100 || poolSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator poolSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.poolSetpoint)); + if (spaSetpoint > 100 || spaSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator spaSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.spaSetpoint)); - public async setChlorAsync(obj: any, send: boolean = true): Promise { - try { - let id = parseInt(obj.id, 10); - let chlor: Chlorinator; - let master = parseInt(obj.master, 10); - let portId = typeof obj.portId !== 'undefined' ? parseInt(obj.portId, 10) : 0; - if (isNaN(master)) master = 1; // NCP to control. - if (isNaN(id) || id <= 0) { - let body = sys.board.bodies.mapBodyAssociation(typeof obj.body !== 'undefined' ? parseInt(obj.body, 10) : 0); - if (typeof body === 'undefined') { - if (sys.equipment.shared) body = 32; - else if (!sys.equipment.dual) body = 1; - else return Promise.reject(new InvalidEquipmentDataError(`Chlorinator body association is not valid: ${body}`, 'chlorinator', body)); + chlor = sys.chlorinators.getItemById(id, true); + let schlor = state.chlorinators.getItemById(chlor.id, true); + chlor.name = schlor.name = obj.name || chlor.name || 'Chlorinator --' + id; + chlor.superChlorHours = schlor.superChlorHours = typeof obj.superChlorHours !== 'undefined' ? parseInt(obj.superChlorHours, 10) : isNaN(chlor.superChlorHours) ? 8 : chlor.superChlorHours; + chlor.superChlor = schlor.superChlor = typeof obj.superChlorinate !== 'undefined' ? utils.makeBool(obj.superChlorinate) : chlor.superChlor; + chlor.superChlor = schlor.superChlor = typeof obj.superChlor !== 'undefined' ? utils.makeBool(obj.superChlor) : chlor.superChlor; + + chlor.isDosing = typeof obj.isDosing !== 'undefined' ? utils.makeBool(obj.isDosing) : chlor.isDosing || false; + chlor.disabled = typeof obj.disabled !== 'undefined' ? utils.makeBool(obj.disabled) : chlor.disabled || false; + schlor.model = chlor.model = typeof obj.model !== 'undefined' ? sys.board.valueMaps.chlorinatorModel.encode(obj.model) : chlor.model; + chlor.type = schlor.type = typeof obj.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(obj.type) : chlor.type || 0; + chlor.body = schlor.body = body.val; + chlor.address = typeof obj.address !== 'undefined' ? parseInt(obj.address, 10) : 80; + schlor.poolSetpoint = chlor.poolSetpoint = poolSetpoint; + schlor.spaSetpoint = chlor.spaSetpoint = spaSetpoint; + chlor.ignoreSaltReading = typeof obj.ignoreSaltReading !== 'undefined' ? utils.makeBool(obj.ignoreSaltReading) : utils.makeBool(chlor.ignoreSaltReading); + schlor.isActive = chlor.isActive = typeof obj.isActive !== 'undefined' ? utils.makeBool(obj.isActive) : typeof chlor.isActive !== 'undefined' ? utils.makeBool(chlor.isActive) : true; + chlor.master = 2; + schlor.currentOutput = typeof obj.currentOutput !== 'undefined' ? parseInt(obj.currentOutput, 10) : schlor.currentOutput; + schlor.lastComm = typeof obj.lastComm !== 'undefined' ? obj.lastComm : schlor.lastComm || Date.now(); + schlor.status = typeof obj.status !== 'undefined' ? sys.board.valueMaps.chlorinatorStatus.encode(obj.status) : sys.board.valueMaps.chlorinatorStatus.encode(schlor.status || 0); + if (typeof obj.superChlorRemaining !== 'undefined') schlor.superChlorRemaining = parseInt(obj.superChlorRemaining, 10); + schlor.targetOutput = typeof obj.targetOutput !== 'undefined' ? parseInt(obj.targetOutput, 10) : schlor.targetOutput; + schlor.saltLevel = typeof obj.saltLevel !== 'undefined' ? parseInt(obj.saltLevel, 10) : schlor.saltLevel; + } + state.emitEquipmentChanges(); + return Promise.resolve(state.chlorinators.getItemById(id)); } - let poolSetpoint = typeof obj.poolSetpoint !== 'undefined' ? parseInt(obj.poolSetpoint, 10) : 50; - let spaSetpoint = typeof obj.spaSetpoint !== 'undefined' ? parseInt(obj.spaSetpoint, 10) : 10; - if (isNaN(poolSetpoint) || poolSetpoint > 100 || poolSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator poolSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', poolSetpoint)); - if (isNaN(spaSetpoint) || spaSetpoint > 100 || spaSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator spaSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', spaSetpoint)); - if (master === 2) { - // We can add as many external chlorinators as we want. - id = sys.chlorinators.count(elem => elem.master === 2) + 50; - chlor = sys.chlorinators.getItemById(id, true, { id: id, master: parseInt(obj.master, 10) }); + catch (err) { + logger.error(`Error setting chlorinator: ${err}`) + return Promise.reject(err); } - else { - if (portId === 0 && sys.controllerType !== ControllerType.Nixie) return Promise.reject(new InvalidEquipmentDataError(`You may not install a chlorinator on an ${sys.controllerType} system that is assigned to the Primary Port`, 'Chlorinator', portId)); - if (sys.chlorinators.count(elem => elem.portId === portId && elem.master !== 2) > 0) return Promise.reject(new InvalidEquipmentDataError(`There is already a chlorinator using port #${portId}. Only one chlorinator may be installed per port.`, 'Chlorinator', portId)); - // We are adding so we need to see if there is another chlorinator that is not external. - if (sys.chlorinators.count(elem => elem.master !== 2) > sys.equipment.maxChlorinators) return Promise.reject(new InvalidEquipmentDataError(`The max number of chlorinators has been exceeded you may only add ${sys.equipment.maxChlorinators}`, 'Chlorinator', sys.equipment.maxChlorinators)); - id = sys.chlorinators.getMaxId(false, 0) + 1; - chlor = sys.chlorinators.getItemById(id, true, { id: id, master: 1 }); + } + public async deleteChlorAsync(obj: any): Promise { + try { + let id = parseInt(obj.id, 10); + if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator id is not valid: ${obj.id}`, 'chlorinator', obj.id)); + let chlor = state.chlorinators.getItemById(id); + chlor.isActive = false; + await ncp.chlorinators.deleteChlorinatorAsync(id); + state.chlorinators.removeItemById(id); + sys.chlorinators.removeItemById(id); + chlor.emitEquipmentChange(); + state.emitEquipmentChanges(); + return Promise.resolve(chlor); } - } - else chlor = sys.chlorinators.getItemById(id, false); - - if (chlor.master === 1) - await ncp.chlorinators.setChlorinatorAsync(chlor, obj); - else { - let body = sys.board.bodies.mapBodyAssociation(typeof obj.body !== 'undefined' ? parseInt(obj.body, 10) : chlor.body); - if (typeof body === 'undefined') { - if (sys.equipment.shared) body = 32; - else if (!sys.equipment.dual) body = 1; - else return Promise.reject(new InvalidEquipmentDataError(`Chlorinator body association is not valid: ${body}`, 'chlorinator', body)); + catch (err) { + logger.error(`Error deleting chlorinator: ${err}`) + return Promise.reject(err); } - let poolSetpoint = typeof obj.poolSetpoint !== 'undefined' ? parseInt(obj.poolSetpoint, 10) : isNaN(chlor.poolSetpoint) ? 50 : chlor.poolSetpoint; - let spaSetpoint = typeof obj.spaSetpoint !== 'undefined' ? parseInt(obj.spaSetpoint, 10) : isNaN(chlor.spaSetpoint) ? 10 : chlor.spaSetpoint; - if (poolSetpoint > 100 || poolSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator poolSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.poolSetpoint)); - if (spaSetpoint > 100 || spaSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator spaSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.spaSetpoint)); - - chlor = sys.chlorinators.getItemById(id, true); - let schlor = state.chlorinators.getItemById(chlor.id, true); - chlor.name = schlor.name = obj.name || chlor.name || 'Chlorinator --' + id; - chlor.superChlorHours = schlor.superChlorHours = typeof obj.superChlorHours !== 'undefined' ? parseInt(obj.superChlorHours, 10) : isNaN(chlor.superChlorHours) ? 8 : chlor.superChlorHours; - chlor.superChlor = schlor.superChlor = typeof obj.superChlorinate !== 'undefined' ? utils.makeBool(obj.superChlorinate) : chlor.superChlor; - chlor.superChlor = schlor.superChlor = typeof obj.superChlor !== 'undefined' ? utils.makeBool(obj.superChlor) : chlor.superChlor; - - chlor.isDosing = typeof obj.isDosing !== 'undefined' ? utils.makeBool(obj.isDosing) : chlor.isDosing || false; - chlor.disabled = typeof obj.disabled !== 'undefined' ? utils.makeBool(obj.disabled) : chlor.disabled || false; - schlor.model = chlor.model = typeof obj.model !== 'undefined' ? sys.board.valueMaps.chlorinatorModel.encode(obj.model) : chlor.model; - chlor.type = schlor.type = typeof obj.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(obj.type) : chlor.type || 0; - chlor.body = schlor.body = body.val; - chlor.address = typeof obj.address !== 'undefined' ? parseInt(obj.address,10) : 80; - schlor.poolSetpoint = chlor.poolSetpoint = poolSetpoint; - schlor.spaSetpoint = chlor.spaSetpoint = spaSetpoint; - chlor.ignoreSaltReading = typeof obj.ignoreSaltReading !== 'undefined' ? utils.makeBool(obj.ignoreSaltReading) : utils.makeBool(chlor.ignoreSaltReading); - schlor.isActive = chlor.isActive = typeof obj.isActive !== 'undefined' ? utils.makeBool(obj.isActive) : typeof chlor.isActive !== 'undefined' ? utils.makeBool(chlor.isActive) : true; - chlor.master = 2; - schlor.currentOutput = typeof obj.currentOutput !== 'undefined' ? parseInt(obj.currentOutput, 10) : schlor.currentOutput; - schlor.lastComm = typeof obj.lastComm !== 'undefined' ? obj.lastComm : schlor.lastComm || Date.now(); - schlor.status = typeof obj.status !== 'undefined' ? sys.board.valueMaps.chlorinatorStatus.encode(obj.status) : sys.board.valueMaps.chlorinatorStatus.encode(schlor.status || 0); - if (typeof obj.superChlorRemaining !== 'undefined') schlor.superChlorRemaining = parseInt(obj.superChlorRemaining, 10); - schlor.targetOutput = typeof obj.targetOutput !== 'undefined' ? parseInt(obj.targetOutput, 10) : schlor.targetOutput; - schlor.saltLevel = typeof obj.saltLevel !== 'undefined' ? parseInt(obj.saltLevel, 10) : schlor.saltLevel; - } - state.emitEquipmentChanges(); - return Promise.resolve(state.chlorinators.getItemById(id)); - } - catch (err) { - logger.error(`Error setting chlorinator: ${err}`) - return Promise.reject(err); } - } - public async deleteChlorAsync(obj: any): Promise { - try { - let id = parseInt(obj.id, 10); - if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator id is not valid: ${obj.id}`, 'chlorinator', obj.id)); - let chlor = state.chlorinators.getItemById(id); - chlor.isActive = false; - await ncp.chlorinators.deleteChlorinatorAsync(id); - state.chlorinators.removeItemById(id); - sys.chlorinators.removeItemById(id); - chlor.emitEquipmentChange(); - state.emitEquipmentChanges(); - return Promise.resolve(chlor); - } - catch (err) { - logger.error(`Error deleting chlorinator: ${err}`) - return Promise.reject(err); + public setChlorProps(chlor: Chlorinator, obj?: any) { + if (typeof obj !== 'undefined') { + for (var prop in obj) { + if (prop in chlor) chlor[prop] = obj[prop]; + } + } } - } - public setChlorProps(chlor: Chlorinator, obj?: any) { - if (typeof obj !== 'undefined') { - for (var prop in obj) { - if (prop in chlor) chlor[prop] = obj[prop]; - } +} +export class ScheduleCommands extends BoardCommands { + public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { + try { + // First delete the schedules that should be removed. + for (let i = 0; i < ctx.schedules.remove.length; i++) { + let s = ctx.schedules.remove[i]; + try { + await sys.board.schedules.deleteScheduleAsync(ctx.schedules.remove[i]); + res.addModuleSuccess('schedule', `Remove: ${s.id}-${s.circuitId}`); + } catch (err) { res.addModuleError('schedule', `Remove: ${s.id}-${s.circuitId} ${err.message}`); } + } + for (let i = 0; i < ctx.schedules.update.length; i++) { + let s = ctx.schedules.update[i]; + try { + await sys.board.schedules.setScheduleAsync(s); + res.addModuleSuccess('schedule', `Update: ${s.id}-${s.circuitId}`); + } catch (err) { res.addModuleError('schedule', `Update: ${s.id}-${s.circuitId} ${err.message}`); } + } + for (let i = 0; i < ctx.schedules.add.length; i++) { + let s = ctx.schedules.add[i]; + try { + // pull a little trick to first add the data then perform the update. This way we won't get a new id or + // it won't error out. + sys.schedules.getItemById(s.id, true); + await sys.board.schedules.setScheduleAsync(s); + res.addModuleSuccess('schedule', `Add: ${s.id}-${s.circuitId}`); + } catch (err) { res.addModuleError('schedule', `Add: ${s.id}-${s.circuitId} ${err.message}`); } + } + return true; + } catch (err) { logger.error(`Error restoring schedules: ${err.message}`); res.addModuleError('system', `Error restoring schedules: ${err.message}`); return false; } } - } -} -export class ScheduleCommands extends BoardCommands { - public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { - try { - // First delete the schedules that should be removed. - for (let i = 0; i < ctx.schedules.remove.length; i++) { - let s = ctx.schedules.remove[i]; - try { - await sys.board.schedules.deleteScheduleAsync(ctx.schedules.remove[i]); - res.addModuleSuccess('schedule', `Remove: ${s.id}-${s.circuitId}`); - } catch (err) { res.addModuleError('schedule', `Remove: ${s.id}-${s.circuitId} ${err.message}`); } - } - for (let i = 0; i < ctx.schedules.update.length; i++) { - let s = ctx.schedules.update[i]; - try { - await sys.board.schedules.setScheduleAsync(s); - res.addModuleSuccess('schedule', `Update: ${s.id}-${s.circuitId}`); - } catch (err) { res.addModuleError('schedule', `Update: ${s.id}-${s.circuitId} ${err.message}`); } - } - for (let i = 0; i < ctx.schedules.add.length; i++) { - let s = ctx.schedules.add[i]; + public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> { try { - // pull a little trick to first add the data then perform the update. This way we won't get a new id or - // it won't error out. - sys.schedules.getItemById(s.id, true); - await sys.board.schedules.setScheduleAsync(s); - res.addModuleSuccess('schedule', `Add: ${s.id}-${s.circuitId}`); - } catch (err) { res.addModuleError('schedule', `Add: ${s.id}-${s.circuitId} ${err.message}`); } - } - return true; - } catch (err) { logger.error(`Error restoring schedules: ${err.message}`); res.addModuleError('system', `Error restoring schedules: ${err.message}`); return false; } - } - public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> { - try { - let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; - // Look at schedules. - let cfg = rest.poolConfig; - for (let i = 0; i < cfg.schedules.length; i++) { - let r = cfg.schedules[i]; - let c = sys.schedules.find(elem => r.id === elem.id); - if (typeof c === 'undefined') ctx.add.push(r); - else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); - } - for (let i = 0; i < sys.schedules.length; i++) { - let c = sys.schedules.getItemByIndex(i); - let r = cfg.schedules.find(elem => elem.id == c.id); - if (typeof r === 'undefined') ctx.remove.push(c.get(true)); - } - return ctx; - } catch (err) { logger.error(`Error validating schedules for restore: ${err.message}`); } - } + let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; + // Look at schedules. + let cfg = rest.poolConfig; + for (let i = 0; i < cfg.schedules.length; i++) { + let r = cfg.schedules[i]; + let c = sys.schedules.find(elem => r.id === elem.id); + if (typeof c === 'undefined') ctx.add.push(r); + else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); + } + for (let i = 0; i < sys.schedules.length; i++) { + let c = sys.schedules.getItemByIndex(i); + let r = cfg.schedules.find(elem => elem.id == c.id); + if (typeof r === 'undefined') ctx.remove.push(c.get(true)); + } + return ctx; + } catch (err) { logger.error(`Error validating schedules for restore: ${err.message}`); } + } - public transformDays(val: any): number { - if (typeof val === 'number') return val; - let edays = sys.board.valueMaps.scheduleDays.toArray(); - let dayFromString = function (str) { - let lstr = str.toLowerCase(); - let byte = 0; - for (let i = 0; i < edays.length; i++) { - let eday = edays[i]; - switch (lstr) { - case 'weekdays': - if (eday.name === 'mon' || eday.name === 'tue' || eday.name === 'wed' || eday.name === 'thu' || eday.name === 'fri') - byte |= (1 << (eday.val - 1)); - break; - case 'weekends': - if (eday.name === 'sat' || eday.name === 'sun') - byte |= (1 << (eday.val - 1)); - break; - default: - if (lstr.startsWith(eday.name)) byte |= (1 << (eday.val - 1)); - break; - } - } - return byte; - }; - let dayFromDow = function (dow) { - let byte = 0; - for (let i = 0; i < edays.length; i++) { - let eday = edays[i]; - if (eday.dow === dow) { - byte |= (1 << (eday.val - 1)); - break; - } - } - return byte; - }; - let bdays = 0; - if (val.isArray) { - for (let i in val) { - let v = val[i]; - if (typeof v === 'string') bdays |= dayFromString(v); - else if (typeof v === 'number') bdays |= dayFromDow(v); - else if (typeof v === 'object') { - if (typeof v.name !== 'undefined') bdays |= dayFromString(v); - else if (typeof v.dow !== 'undefined') bdays |= dayFromDow(v); - else if (typeof v.desc !== 'undefined') bdays |= dayFromString(v); + public transformDays(val: any): number { + if (typeof val === 'number') return val; + let edays = sys.board.valueMaps.scheduleDays.toArray(); + let dayFromString = function (str) { + let lstr = str.toLowerCase(); + let byte = 0; + for (let i = 0; i < edays.length; i++) { + let eday = edays[i]; + switch (lstr) { + case 'weekdays': + if (eday.name === 'mon' || eday.name === 'tue' || eday.name === 'wed' || eday.name === 'thu' || eday.name === 'fri') + byte |= (1 << (eday.val - 1)); + break; + case 'weekends': + if (eday.name === 'sat' || eday.name === 'sun') + byte |= (1 << (eday.val - 1)); + break; + default: + if (lstr.startsWith(eday.name)) byte |= (1 << (eday.val - 1)); + break; + } + } + return byte; + }; + let dayFromDow = function (dow) { + let byte = 0; + for (let i = 0; i < edays.length; i++) { + let eday = edays[i]; + if (eday.dow === dow) { + byte |= (1 << (eday.val - 1)); + break; + } + } + return byte; + }; + let bdays = 0; + if (val.isArray) { + for (let i in val) { + let v = val[i]; + if (typeof v === 'string') bdays |= dayFromString(v); + else if (typeof v === 'number') bdays |= dayFromDow(v); + else if (typeof v === 'object') { + if (typeof v.name !== 'undefined') bdays |= dayFromString(v); + else if (typeof v.dow !== 'undefined') bdays |= dayFromDow(v); + else if (typeof v.desc !== 'undefined') bdays |= dayFromString(v); + } + } } - } + return bdays; } - return bdays; - } - public setSchedule(sched: Schedule | EggTimer, obj?: any) { - if (typeof obj !== undefined) { - for (var s in obj) - sched[s] = obj[s]; - } - } - public syncScheduleHeatSourceAndSetpoint(cbody: Body, tbody: BodyTempState) { - // check schedules to see if we need to adjust heat mode and setpoint. This will be in effect for the first minute of the schedule - let schedules: ScheduleState[] = state.schedules.get(true); - for (let i = 0; i < schedules.length; i++) { - let sched = schedules[i]; - // check if the id's, min, hour match - if (sched.circuit === cbody.circuit && sched.isActive && Math.floor(sched.startTime / 60) === state.time.hours && sched.startTime % 60 === state.time.minutes) { - // check day match next as we need to iterate another array - // let days = sys.board.valueMaps.scheduleDays.transform(sched.scheduleDays); - // const days = sys.board.valueMaps.scheduleDays.transform(sched.scheduleDays); - const days = (sched.scheduleDays as any).days.map(d => d.dow) - // if scheduleDays includes today - if (days.includes(state.time.toDate().getDay())) { - if (sched.changeHeatSetpoint && (sched.heatSource as any).val !== sys.board.valueMaps.heatSources.getValue('off') && sched.heatSetpoint > 0 && sched.heatSetpoint !== tbody.setPoint) { - setTimeoutSync(() => sys.board.bodies.setHeatSetpointAsync(cbody, sched.heatSetpoint), 100); - } - if ((sched.heatSource as any).val !== sys.board.valueMaps.heatSources.getValue('nochange') && sched.heatSource !== tbody.heatMode) { - setTimeoutSync(() => sys.board.bodies.setHeatModeAsync(cbody, sys.board.valueMaps.heatModes.getValue((sched.heatSource as any).name)), 100); - } + public setSchedule(sched: Schedule | EggTimer, obj?: any) { + if (typeof obj !== undefined) { + for (var s in obj) + sched[s] = obj[s]; } - } - }; - } - public async setScheduleAsync(data: any, send: boolean = true): Promise { - let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); - if (id <= 0) id = sys.schedules.getNextEquipmentId(new EquipmentIdRange(1, sys.equipment.maxSchedules)); - if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid schedule id: ${data.id}`, data.id, 'Schedule')); - let sched = sys.schedules.getItemById(id, data.id <= 0); - let ssched = state.schedules.getItemById(id, data.id <= 0); - let schedType = typeof data.scheduleType !== 'undefined' ? data.scheduleType : sched.scheduleType; - if (typeof schedType === 'undefined') schedType = 0; // Repeats + } + public syncScheduleHeatSourceAndSetpoint(cbody: Body, tbody: BodyTempState) { + // check schedules to see if we need to adjust heat mode and setpoint. This will be in effect for the first minute of the schedule + let schedules: ScheduleState[] = state.schedules.get(true); + for (let i = 0; i < schedules.length; i++) { + let sched = schedules[i]; + // check if the id's, min, hour match + if (sched.circuit === cbody.circuit && sched.isActive && Math.floor(sched.startTime / 60) === state.time.hours && sched.startTime % 60 === state.time.minutes) { + // check day match next as we need to iterate another array + // let days = sys.board.valueMaps.scheduleDays.transform(sched.scheduleDays); + // const days = sys.board.valueMaps.scheduleDays.transform(sched.scheduleDays); + const days = (sched.scheduleDays as any).days.map(d => d.dow) + // if scheduleDays includes today + if (days.includes(state.time.toDate().getDay())) { + if (sched.changeHeatSetpoint && (sched.heatSource as any).val !== sys.board.valueMaps.heatSources.getValue('off') && sched.heatSetpoint > 0 && sched.heatSetpoint !== tbody.setPoint) { + setTimeoutSync(() => sys.board.bodies.setHeatSetpointAsync(cbody, sched.heatSetpoint), 100); + } + if ((sched.heatSource as any).val !== sys.board.valueMaps.heatSources.getValue('nochange') && sched.heatSource !== tbody.heatMode) { + setTimeoutSync(() => sys.board.bodies.setHeatModeAsync(cbody, sys.board.valueMaps.heatModes.getValue((sched.heatSource as any).name)), 100); + } + } + } + }; + } + public async setScheduleAsync(data: any, send: boolean = true): Promise { + let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); + if (id <= 0) id = sys.schedules.getNextEquipmentId(new EquipmentIdRange(1, sys.equipment.maxSchedules)); + if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid schedule id: ${data.id}`, data.id, 'Schedule')); + let sched = sys.schedules.getItemById(id, data.id <= 0); + let ssched = state.schedules.getItemById(id, data.id <= 0); + ssched.scheduleTime.calculated = false; + let schedType = typeof data.scheduleType !== 'undefined' ? data.scheduleType : sched.scheduleType; + if (typeof schedType === 'undefined') schedType = 0; // Repeats - let startTimeType = typeof data.startTimeType !== 'undefined' ? data.startTimeType : sched.startTimeType; - let endTimeType = typeof data.endTimeType !== 'undefined' ? data.endTimeType : sched.endTimeType; - let startDate = typeof data.startDate !== 'undefined' ? data.startDate : sched.startDate; - if (typeof startDate.getMonth !== 'function') startDate = new Date(startDate); - let heatSource = typeof data.heatSource !== 'undefined' ? data.heatSource : sched.heatSource; - let heatSetpoint = typeof data.heatSetpoint !== 'undefined' ? data.heatSetpoint : sched.heatSetpoint; - let coolSetpoint = typeof data.coolSetpoint !== 'undefined' ? data.coolSetpoint : sched.coolSetpoint || 100; - let circuit = typeof data.circuit !== 'undefined' ? data.circuit : sched.circuit; - let startTime = typeof data.startTime !== 'undefined' ? data.startTime : sched.startTime; - let endTime = typeof data.endTime !== 'undefined' ? data.endTime : sched.endTime; - let schedDays = sys.board.schedules.transformDays(typeof data.scheduleDays !== 'undefined' ? data.scheduleDays : sched.scheduleDays); - let changeHeatSetpoint = typeof (data.changeHeatSetpoint !== 'undefined') ? data.changeHeatSetpoint : false; - let display = typeof data.display !== 'undefined' ? data.display : sched.display || 0; - let disabled = typeof data.disabled !== 'undefined' ? utils.makeBool(data.disabled) : sched.disabled; + let startTimeType = typeof data.startTimeType !== 'undefined' ? data.startTimeType : sched.startTimeType; + let endTimeType = typeof data.endTimeType !== 'undefined' ? data.endTimeType : sched.endTimeType; + let startDate = typeof data.startDate !== 'undefined' ? data.startDate : sched.startDate; + if (typeof startDate.getMonth !== 'function') startDate = new Date(startDate); + let heatSource = typeof data.heatSource !== 'undefined' ? data.heatSource : sched.heatSource; + let heatSetpoint = typeof data.heatSetpoint !== 'undefined' ? data.heatSetpoint : sched.heatSetpoint; + let coolSetpoint = typeof data.coolSetpoint !== 'undefined' ? data.coolSetpoint : sched.coolSetpoint || 100; + let circuit = typeof data.circuit !== 'undefined' ? data.circuit : sched.circuit; + let startTime = typeof data.startTime !== 'undefined' ? data.startTime : sched.startTime; + let endTime = typeof data.endTime !== 'undefined' ? data.endTime : sched.endTime; + let schedDays = sys.board.schedules.transformDays(typeof data.scheduleDays !== 'undefined' ? data.scheduleDays : sched.scheduleDays); + let changeHeatSetpoint = typeof (data.changeHeatSetpoint !== 'undefined') ? data.changeHeatSetpoint : false; + let display = typeof data.display !== 'undefined' ? data.display : sched.display || 0; + let disabled = typeof data.disabled !== 'undefined' ? utils.makeBool(data.disabled) : sched.disabled; + let endTimeOffset = typeof data.endTimeOffset !== 'undefined' ? data.endTimeOffset : sched.endTimeOffset; + let startTimeOffset = typeof data.startTimeOffset !== 'undefined' ? data.startTimeOffset : sched.startTimeOffset; - // Ensure all the defaults. - if (isNaN(startDate.getTime())) startDate = new Date(); - if (typeof startTime === 'undefined') startTime = 480; // 8am - if (typeof endTime === 'undefined') endTime = 1020; // 5pm - if (typeof startTimeType === 'undefined') startTimeType = 0; // Manual - if (typeof endTimeType === 'undefined') endTimeType = 0; // Manual + // Ensure all the defaults. + if (isNaN(startDate.getTime())) startDate = new Date(); + if (typeof startTime === 'undefined') startTime = 480; // 8am + if (typeof endTime === 'undefined') endTime = 1020; // 5pm + if (typeof startTimeType === 'undefined') startTimeType = 0; // Manual + if (typeof endTimeType === 'undefined') endTimeType = 0; // Manual - // At this point we should have all the data. Validate it. - if (!sys.board.valueMaps.scheduleTypes.valExists(schedType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid schedule type; ${schedType}`, 'Schedule', schedType)); - if (!sys.board.valueMaps.scheduleTimeTypes.valExists(startTimeType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid start time type; ${startTimeType}`, 'Schedule', startTimeType)); - if (!sys.board.valueMaps.scheduleTimeTypes.valExists(endTimeType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid end time type; ${endTimeType}`, 'Schedule', endTimeType)); - if (!sys.board.valueMaps.heatSources.valExists(heatSource)) return Promise.reject(new InvalidEquipmentDataError(`Invalid heat source: ${heatSource}`, 'Schedule', heatSource)); - if (heatSetpoint < 0 || heatSetpoint > 104) return Promise.reject(new InvalidEquipmentDataError(`Invalid heat setpoint: ${heatSetpoint}`, 'Schedule', heatSetpoint)); - if (sys.board.circuits.getCircuitReferences(true, true, false, true).find(elem => elem.id === circuit) === undefined) - return Promise.reject(new InvalidEquipmentDataError(`Invalid circuit reference: ${circuit}`, 'Schedule', circuit)); - if (schedType === 128 && schedDays === 0) return Promise.reject(new InvalidEquipmentDataError(`Invalid schedule days: ${schedDays}. You must supply days that the schedule is to run.`, 'Schedule', schedDays)); + // At this point we should have all the data. Validate it. + if (!sys.board.valueMaps.scheduleTypes.valExists(schedType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid schedule type; ${schedType}`, 'Schedule', schedType)); + if (!sys.board.valueMaps.scheduleTimeTypes.valExists(startTimeType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid start time type; ${startTimeType}`, 'Schedule', startTimeType)); + if (!sys.board.valueMaps.scheduleTimeTypes.valExists(endTimeType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid end time type; ${endTimeType}`, 'Schedule', endTimeType)); + if (!sys.board.valueMaps.heatSources.valExists(heatSource)) return Promise.reject(new InvalidEquipmentDataError(`Invalid heat source: ${heatSource}`, 'Schedule', heatSource)); + if (heatSetpoint < 0 || heatSetpoint > 104) return Promise.reject(new InvalidEquipmentDataError(`Invalid heat setpoint: ${heatSetpoint}`, 'Schedule', heatSetpoint)); + if (sys.board.circuits.getCircuitReferences(true, true, false, true).find(elem => elem.id === circuit) === undefined) + return Promise.reject(new InvalidEquipmentDataError(`Invalid circuit reference: ${circuit}`, 'Schedule', circuit)); + if (schedType === 128 && schedDays === 0) return Promise.reject(new InvalidEquipmentDataError(`Invalid schedule days: ${schedDays}. You must supply days that the schedule is to run.`, 'Schedule', schedDays)); - // If we made it to here we are valid and the schedula and it state should exist. - sched = sys.schedules.getItemById(id, true); - ssched = state.schedules.getItemById(id, true); - sched.circuit = ssched.circuit = circuit; - sched.scheduleDays = ssched.scheduleDays = schedDays; - sched.scheduleType = ssched.scheduleType = schedType; - sched.changeHeatSetpoint = ssched.changeHeatSetpoint = changeHeatSetpoint; - sched.heatSetpoint = ssched.heatSetpoint = heatSetpoint; - sched.coolSetpoint = ssched.coolSetpoint = coolSetpoint; - sched.heatSource = ssched.heatSource = heatSource; - sched.startTime = ssched.startTime = startTime; - sched.endTime = ssched.endTime = endTime; - sched.startTimeType = ssched.startTimeType = startTimeType; - sched.endTimeType = ssched.endTimeType = endTimeType; - sched.startDate = ssched.startDate = startDate; - sched.startYear = startDate.getFullYear(); - sched.startMonth = startDate.getMonth() + 1; - sched.startDay = startDate.getDate(); - ssched.isActive = sched.isActive = true; - ssched.disabled = sched.disabled = disabled; - ssched.display = sched.display = display; - if (typeof sched.startDate === 'undefined') - sched.master = 1; - await ncp.schedules.setScheduleAsync(sched, data); - // update end time in case sched is changed while circuit is on - let cstate = state.circuits.getInterfaceById(sched.circuit); - sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(sched.circuit), cstate, cstate.isOn, true); - cstate.emitEquipmentChange(); - ssched.emitEquipmentChange(); - return sched; - } - public deleteScheduleAsync(data: any): Promise { - let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); - if (isNaN(id) || id < 0) return Promise.reject(new InvalidEquipmentIdError(`Invalid schedule id: ${data.id}`, data.id, 'Schedule')); - let sched = sys.schedules.getItemById(id, false); - let ssched = state.schedules.getItemById(id, false); - ssched.isActive = false; - if (sched.master === 1) ncp.schedules.removeById(id); - sys.schedules.removeItemById(id); - state.schedules.removeItemById(id); - ssched.emitEquipmentChange(); - return new Promise((resolve, reject) => { resolve(sched); }); - } - public syncScheduleStates() { - try { - ncp.schedules.triggerSchedules(); - let dt = state.time.toDate(); - let dow = dt.getDay(); - // Convert the dow to the bit value. - let sd = sys.board.valueMaps.scheduleDays.toArray().find(elem => elem.dow === dow); - let dayVal = sd.bitVal || sd.val; // The bitval allows mask overrides. - let ts = dt.getHours() * 60 + dt.getMinutes(); - for (let i = 0; i < state.schedules.length; i++) { - let schedIsOn: boolean; - let ssched = state.schedules.getItemByIndex(i); - let scirc = state.circuits.getInterfaceById(ssched.circuit); - let mOP = sys.board.schedules.manualPriorityActive(ssched); //sys.board.schedules.manualPriorityActiveByProxy(scirc.id); - if (scirc.isOn && !mOP && - (ssched.scheduleDays & dayVal) > 0 && - ts >= ssched.startTime && ts <= ssched.endTime) schedIsOn = true - else schedIsOn = false; - if (schedIsOn !== ssched.isOn) { - // if the schedule state changes, it may affect the end time - ssched.isOn = schedIsOn; - sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(ssched.circuit), scirc, scirc.isOn, true); - } + // If we made it to here we are valid and the schedula and it state should exist. + sched = sys.schedules.getItemById(id, true); + ssched = state.schedules.getItemById(id, true); + sched.circuit = ssched.circuit = circuit; + sched.scheduleDays = ssched.scheduleDays = schedDays; + sched.scheduleType = ssched.scheduleType = schedType; + sched.changeHeatSetpoint = ssched.changeHeatSetpoint = changeHeatSetpoint; + sched.heatSetpoint = ssched.heatSetpoint = heatSetpoint; + sched.coolSetpoint = ssched.coolSetpoint = coolSetpoint; + sched.heatSource = ssched.heatSource = heatSource; + sched.startTime = ssched.startTime = startTime; + sched.endTime = ssched.endTime = endTime; + sched.startTimeType = ssched.startTimeType = startTimeType; + sched.endTimeType = ssched.endTimeType = endTimeType; + sched.startDate = ssched.startDate = startDate; + sched.startYear = startDate.getFullYear(); + sched.startMonth = startDate.getMonth() + 1; + sched.startDay = startDate.getDate(); + ssched.isActive = sched.isActive = true; + ssched.disabled = sched.disabled = disabled; + ssched.display = sched.display = display; + ssched.startTimeOffset = sched.startTimeOffset = startTimeOffset; + ssched.endTimeOffset = sched.endTimeOffset = endTimeOffset; + if (typeof sched.startDate === 'undefined') + sched.master = 1; + await ncp.schedules.setScheduleAsync(sched, data); + // update end time in case sched is changed while circuit is on + let cstate = state.circuits.getInterfaceById(sched.circuit); + sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(sched.circuit), cstate, cstate.isOn, true); + cstate.emitEquipmentChange(); ssched.emitEquipmentChange(); - } - } catch (err) { logger.error(`Error synchronizing schedule states`); } - } - public async setEggTimerAsync(data?: any, send: boolean = true): Promise { return Promise.resolve(sys.eggTimers.getItemByIndex(1)); } - public async deleteEggTimerAsync(data?: any): Promise { return Promise.resolve(sys.eggTimers.getItemByIndex(1)); } - public includesCircuit(sched: Schedule, circuit: number) { - let bIncludes = false; - if (circuit === sched.circuit) bIncludes = true; - else if (sys.board.equipmentIds.circuitGroups.isInRange(sched.circuit)) { - let circs = sys.circuitGroups.getItemById(sched.circuit).getExtended().circuits; - for (let i = 0; i < circs.length; i++) { - if (circs[i].circuit.id === circuit) bIncludes = true; - } - } - return bIncludes; - } + return sched; + } + public deleteScheduleAsync(data: any): Promise { + let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); + if (isNaN(id) || id < 0) return Promise.reject(new InvalidEquipmentIdError(`Invalid schedule id: ${data.id}`, data.id, 'Schedule')); + let sched = sys.schedules.getItemById(id, false); + let ssched = state.schedules.getItemById(id, false); + ssched.isActive = false; + sys.schedules.removeItemById(id); + state.schedules.removeItemById(id); + ssched.emitEquipmentChange(); + return new Promise((resolve, reject) => { resolve(sched); }); + } + public syncScheduleStates() { + try { + // The call below also calculates the schedule window either the current or next. + ncp.schedules.triggerSchedules(); + for (let i = 0; i < state.schedules.length; i++) { + let schedIsOn: boolean; + let ssched = state.schedules.getItemByIndex(i); + let scirc = state.circuits.getInterfaceById(ssched.circuit); + let mOP = sys.board.schedules.manualPriorityActive(ssched); //sys.board.schedules.manualPriorityActiveByProxy(scirc.id); + if (scirc.isOn && !mOP && ssched.scheduleTime.shouldBeOn) schedIsOn = true + else schedIsOn = false; + if (schedIsOn !== ssched.isOn) { + // if the schedule state changes, it may affect the end time + ssched.isOn = schedIsOn; + sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(ssched.circuit), scirc, scirc.isOn, true); + } + ssched.emitEquipmentChange(); + } + } catch (err) { logger.error(`Error synchronizing schedule states`); } + } + public async setEggTimerAsync(data?: any, send: boolean = true): Promise { return Promise.resolve(sys.eggTimers.getItemByIndex(1)); } + public async deleteEggTimerAsync(data?: any): Promise { return Promise.resolve(sys.eggTimers.getItemByIndex(1)); } + public includesCircuit(sched: Schedule, circuit: number) { + if (circuit === sched.circuit) return true; + else if (sys.board.equipmentIds.circuitGroups.isInRange(sched.circuit)) { + let circs = sys.circuitGroups.getItemById(sched.circuit).getExtended().circuits; + for (let i = 0; i < circs.length; i++) { + if (circs[i].circuit.id === circuit) return true; + } + } + return false; + } + /* RKS: 07-10-23 - Deprecated: The schedule state calculates the start/end times. public getNearestEndTime(sched: Schedule): Timestamp { let nearestEndTime = new Timestamp(new Date(0)) let today = new Timestamp().startOfDay(); @@ -3784,122 +3744,125 @@ export class ScheduleCommands extends BoardCommands { } return nearestStartTime; } - public manualPriorityForThisCircuit(circuit: number): boolean { - // This fn will test if this circuit/light group has any circuit group circuits that have manual priority active - let grp: ICircuitGroup; - let cgc: ICircuitGroupCircuit[] = []; - if (sys.board.equipmentIds.circuitGroups.isInRange(circuit) || sys.board.equipmentIds.features.isInRange(circuit)) - grp = sys.circuitGroups.getInterfaceById(circuit); - if (state.circuitGroups.getInterfaceById(circuit).manualPriorityActive) return true; - if (grp && grp.isActive) cgc = grp.circuits.toArray(); - for (let i = 0; i < cgc.length; i++) { - let c = state.circuits.getInterfaceById(cgc[i].circuit); - if (c.manualPriorityActive) return true; - } - return false; - } - public manualPriorityActive(schedule: ScheduleState): boolean { - // This method will look at all other schedules. If any of them have been resumed, - // and manualPriority (global setting) is on, and this schedule would otherwise impact - // that circuit, then we declared this schedule as being delayed due to manual override - // priority (mOP). - // We only need to check this if shouldBeOn = true; if that's false, exit. - // Rules: - // 1. If the circuit id for this schedule is in manual priority, then true - // 2. If the other schedule will turn on a body in a shared body, and it will affect - // this circuit id, return true - // 3. If this is a circuit/light group schedule, check to see if any member circuit/lights have mOP active - // 4. If this is a circuit/light/feature, is there another group that has this same id with mOP active + */ + public manualPriorityForThisCircuit(circuit: number): boolean { + // This fn will test if this circuit/light group has any circuit group circuits that have manual priority active + let grp: ICircuitGroup; + let cgc: ICircuitGroupCircuit[] = []; + if (sys.board.equipmentIds.circuitGroups.isInRange(circuit) || sys.board.equipmentIds.features.isInRange(circuit)) + grp = sys.circuitGroups.getInterfaceById(circuit); + if (state.circuitGroups.getInterfaceById(circuit).manualPriorityActive) return true; + if (grp && grp.isActive) cgc = grp.circuits.toArray(); + for (let i = 0; i < cgc.length; i++) { + let c = state.circuits.getInterfaceById(cgc[i].circuit); + if (c.manualPriorityActive) return true; + } + return false; + } + public manualPriorityActive(schedule: ScheduleState): boolean { + // This method will look at all other schedules. If any of them have been resumed, + // and manualPriority (global setting) is on, and this schedule would otherwise impact + // that circuit, then we declared this schedule as being delayed due to manual override + // priority (mOP). + // We only need to check this if shouldBeOn = true; if that's false, exit. + // Rules: + // 1. If the circuit id for this schedule is in manual priority, then true + // 2. If the other schedule will turn on a body in a shared body, and it will affect + // this circuit id, return true + // 3. If this is a circuit/light group schedule, check to see if any member circuit/lights have mOP active + // 4. If this is a circuit/light/feature, is there another group that has this same id with mOP active - if (schedule.isActive === false) return false; - if (schedule.disabled) return false; - //if (!sys.general.options.manualPriority) return false; //if we override a circuit to be mOP, this will not be true + if (schedule.isActive === false) return false; + if (schedule.disabled) return false; + if (!schedule.scheduleTime.shouldBeOn) return false; + //if (!sys.general.options.manualPriority) return false; //if we override a circuit to be mOP, this will not be true + // Check this circuit if it has the mOP on then we are done and do not need to load up all the other group stuff. + if (state.circuits.getInterfaceById(schedule.circuit).manualPriorityActive) return true; - let currGrp: ICircuitGroup; - let currSchedGrpCircs = []; - if (sys.board.equipmentIds.circuitGroups.isInRange(schedule.circuit) || sys.board.equipmentIds.features.isInRange(schedule.circuit)) - currGrp = sys.circuitGroups.getInterfaceById(schedule.circuit); - if (currGrp && currGrp.isActive) currSchedGrpCircs = currGrp.circuits.toArray(); - let circuitGrps: ICircuitGroup[] = sys.circuitGroups.toArray(); - let lightGrps: ICircuitGroup[] = sys.lightGroups.toArray(); - let currManualPriorityByProxy = sys.board.schedules.manualPriorityForThisCircuit(schedule.circuit); - // check this circuit - if (state.circuits.getInterfaceById(schedule.circuit).manualPriorityActive) return true; - // check this group, if present - if (currManualPriorityByProxy) return true; - let schedules: ScheduleState[] = state.schedules.get(true); - for (let i = 0; i < schedules.length; i++) { - let sched = schedules[i]; - // if the id of another circuit is the same as this, we should delay - let schedCState = state.circuits.getInterfaceById(sched.circuit); - if (schedule.circuit === schedCState.id && schedCState.manualPriorityActive) return true; - // if OCP includes a shared body, and this schedule affects the shared body, - // and this body is still on, we should delay - if (sys.equipment.shared && schedCState.dataName === 'circuit') { - let otherBody = sys.bodies.find(elem => elem.circuit === sched.circuit); - // let otherBodyIsOn = state.circuits.getInterfaceById(sched.circuit).isOn; - let thisBody = sys.bodies.find(elem => elem.circuit === schedule.circuit); - if (typeof otherBody !== 'undefined' && typeof thisBody !== 'undefined' && schedCState.manualPriorityActive) return true; - } - // if other circuit/schedule groups have this circ id, and it's mOP, return true - if (schedCState.dataName === 'circuitGroup') { - for (let i = 0; i < circuitGrps.length; i++) { - let grp: ICircuitGroup = circuitGrps[i]; - let sgrp: ICircuitGroupState = state.circuitGroups.getInterfaceById(grp.id); - let circuits = grp.circuits.toArray(); - if (grp.isActive) { - let manualPriorityByProxy = sys.board.schedules.manualPriorityForThisCircuit(grp.id); - for (let j = 0; j < circuits.length; j++) { - let cgc = grp.circuits.getItemByIndex(j); - let scgc = state.circuits.getInterfaceById(cgc.circuit); - // if the circuit id's match and mOP is active, we delay - if (scgc.id === schedule.circuit && manualPriorityByProxy) return true; - // check all the other cgc against this cgc - // note: circuit/light groups cannot be part of a group themselves - for (let k = 0; k < currSchedGrpCircs.length; k++) { - let currCircGrpCirc = state.circuits.getInterfaceById(currSchedGrpCircs[k].circuit); - // if either circuit in either group has mOP then delay - if (currManualPriorityByProxy || manualPriorityByProxy) { - if (currCircGrpCirc.id === schedCState.id) return true; - if (currCircGrpCirc.id === scgc.id) return true; + let currGrp: ICircuitGroup; + let currSchedGrpCircs = []; + if (sys.board.equipmentIds.circuitGroups.isInRange(schedule.circuit) || sys.board.equipmentIds.features.isInRange(schedule.circuit)) + currGrp = sys.circuitGroups.getInterfaceById(schedule.circuit); + if (currGrp && currGrp.isActive) currSchedGrpCircs = currGrp.circuits.toArray(); + let circuitGrps: ICircuitGroup[] = sys.circuitGroups.toArray(); + let lightGrps: ICircuitGroup[] = sys.lightGroups.toArray(); + let currManualPriorityByProxy = sys.board.schedules.manualPriorityForThisCircuit(schedule.circuit); + // Check this group, if present + if (currManualPriorityByProxy) return true; + + let schedules: ScheduleState[] = state.schedules.get(true); + for (let i = 0; i < schedules.length; i++) { + let sched = schedules[i]; + // if the id of another circuit is the same as this, we should delay + let schedCState = state.circuits.getInterfaceById(sched.circuit); + if (schedule.circuit === schedCState.id && schedCState.manualPriorityActive) return true; + // if OCP includes a shared body, and this schedule affects the shared body, + // and this body is still on, we should delay + if (sys.equipment.shared && schedCState.dataName === 'circuit') { + let otherBody = sys.bodies.find(elem => elem.circuit === sched.circuit); + // let otherBodyIsOn = state.circuits.getInterfaceById(sched.circuit).isOn; + let thisBody = sys.bodies.find(elem => elem.circuit === schedule.circuit); + if (typeof otherBody !== 'undefined' && typeof thisBody !== 'undefined' && schedCState.manualPriorityActive) return true; + } + // if other circuit/schedule groups have this circ id, and it's mOP, return true + if (schedCState.dataName === 'circuitGroup') { + for (let i = 0; i < circuitGrps.length; i++) { + let grp: ICircuitGroup = circuitGrps[i]; + let sgrp: ICircuitGroupState = state.circuitGroups.getInterfaceById(grp.id); + let circuits = grp.circuits.toArray(); + if (grp.isActive) { + let manualPriorityByProxy = sys.board.schedules.manualPriorityForThisCircuit(grp.id); + for (let j = 0; j < circuits.length; j++) { + let cgc = grp.circuits.getItemByIndex(j); + let scgc = state.circuits.getInterfaceById(cgc.circuit); + // if the circuit id's match and mOP is active, we delay + if (scgc.id === schedule.circuit && manualPriorityByProxy) return true; + // check all the other cgc against this cgc + // note: circuit/light groups cannot be part of a group themselves + for (let k = 0; k < currSchedGrpCircs.length; k++) { + let currCircGrpCirc = state.circuits.getInterfaceById(currSchedGrpCircs[k].circuit); + // if either circuit in either group has mOP then delay + if (currManualPriorityByProxy || manualPriorityByProxy) { + if (currCircGrpCirc.id === schedCState.id) return true; + if (currCircGrpCirc.id === scgc.id) return true; + } + } + } + } } - } } - } - } - } - if (schedCState.dataName === 'lightGroup') { - for (let i = 0; i < lightGrps.length; i++) { - let grp: ICircuitGroup = lightGrps[i]; - let sgrp: ICircuitGroupState = state.circuitGroups.getInterfaceById(grp.id); - let circuits = grp.circuits.toArray(); - if (grp.isActive) { - let manualPriorityByProxy = sys.board.schedules.manualPriorityForThisCircuit(grp.id); - for (let j = 0; j < circuits.length; j++) { - let cgc = grp.circuits.getItemByIndex(j); - let scgc = state.circuits.getInterfaceById(cgc.circuit); - // if the circuit id's match and mOP is active, we delay - if (scgc.id === schedule.circuit && scgc.manualPriorityActive) return true; - // check all the other cgc against this cgc - // note: circuit/light groups cannot be part of a group themselves - for (let k = 0; k < currSchedGrpCircs.length; k++) { - let currCircGrpCirc = state.circuits.getInterfaceById(currSchedGrpCircs[k].circuit); - // if either circuit in either group has mOP then delay - if (currManualPriorityByProxy || manualPriorityByProxy) { - if (currCircGrpCirc.id === schedCState.id) return true; - if (currCircGrpCirc.id === scgc.id) return true; + if (schedCState.dataName === 'lightGroup') { + for (let i = 0; i < lightGrps.length; i++) { + let grp: ICircuitGroup = lightGrps[i]; + let sgrp: ICircuitGroupState = state.circuitGroups.getInterfaceById(grp.id); + let circuits = grp.circuits.toArray(); + if (grp.isActive) { + let manualPriorityByProxy = sys.board.schedules.manualPriorityForThisCircuit(grp.id); + for (let j = 0; j < circuits.length; j++) { + let cgc = grp.circuits.getItemByIndex(j); + let scgc = state.circuits.getInterfaceById(cgc.circuit); + // if the circuit id's match and mOP is active, we delay + if (scgc.id === schedule.circuit && scgc.manualPriorityActive) return true; + // check all the other cgc against this cgc + // note: circuit/light groups cannot be part of a group themselves + for (let k = 0; k < currSchedGrpCircs.length; k++) { + let currCircGrpCirc = state.circuits.getInterfaceById(currSchedGrpCircs[k].circuit); + // if either circuit in either group has mOP then delay + if (currManualPriorityByProxy || manualPriorityByProxy) { + if (currCircGrpCirc.id === schedCState.id) return true; + if (currCircGrpCirc.id === scgc.id) return true; + } + } + } + } } - } } - } } - } + // if we make it this far, nothing is impacting us + return false; } - // if we make it this far, nothing is impacting us - return false; - } - public async updateSunriseSunsetAsync():Promise {return Promise.resolve(false);}; + public async updateSunriseSunsetAsync(): Promise { return Promise.resolve(false); }; } export class HeaterCommands extends BoardCommands { public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { @@ -4086,7 +4049,7 @@ export class HeaterCommands extends BoardCommands { let body = sys.bodies.getItemByIndex(i); let btemp = state.temps.bodies.getItemById(body.id, body.isActive !== false); let opts = sys.board.heaters.getInstalledHeaterTypes(body.id); - + btemp.heaterOptions = opts; } this.setActiveTempSensors(); @@ -4194,11 +4157,21 @@ export class HeaterCommands extends BoardCommands { } } } + public clearPrevHeaterOffTemp() { // #925 + for (let i = 0; i < state.heaters.length; i++) { + let heater = state.heaters.getItemByIndex(i); + let htype = sys.board.valueMaps.heaterTypes.transform(heater.type); + // only setting this for solar now; expand it to other heater types if applicable #925 + if (htype.name === 'solar'){ + heater.prevHeaterOffTemp = undefined; + } + } + } // This updates the heater states based upon the installed heaters. This is true for heaters that are tied to the OCP // and those that are not. public syncHeaterStates() { try { - + // Go through the installed heaters and bodies to determine whether they should be on. If there is a // heater that is not controlled by the OCP then we need to determine whether it should be on. let heaters = sys.heaters.toArray(); @@ -4263,20 +4236,56 @@ export class HeaterCommands extends BoardCommands { switch (htype.name) { case 'solar': if (mode === 'solar' || mode === 'solarpref') { - // Measure up against start and stop temp deltas for effective solar heating. - if (body.temp < cfgBody.heatSetpoint && - state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) { + /* + From the manual: + Screen (3/3): Solar temperature differential start up and run settings. + Start: Set the temperature differential to start heating from 3° to 9°. For example, if + “Start” is set to 3°, this ensures that the temperature has to deviate by 3° at least to the + specified set point temperature (in the Heat menu, on page 25) before it switches on. + Once the solar comes on it will start converging as it is heating. This ensures that it + will not continually be switching on and off. + Run: Set the temperature differential to stop heating from 2° to 5°. This setting sets + how close to the target set point temperature to switch off solar heat. + */ + // RSG 04.15.2024 - Updates to heater logic for start/stop deltas. #925 + // 1. For all heating cases in order for the heater to turn on, the solar temp > water temp. + // 2. If the water temp is below the set point, we want to turn on the heater + // 3. But only if the prevHeaterOffTemp - water temp > start temp delta + // 4. Also only if there is enough heat ('run') to make it worthwhile + // 5. The heater should run until it reaches the set point + stop delta + // 6. When the heater turns off, note the water temp ("prevHeaterOffTemp"). This will only live in the application, not persisted. + let hState: HeaterState = + state.heaters.getItemById(heater.id); + if (state.temps.solar > body.temp // 1 + && body.temp < cfgBody.heatSetpoint // 2 + && (typeof hState.prevHeaterOffTemp === 'undefined' || ((hState.prevHeaterOffTemp - body.temp) > heater.startTempDelta)) // 3 + && (state.temps.solar - body.temp) > heater.stopTempDelta // 4 + && body.temp < cfgBody.heatSetpoint // 5 + ) { + // if (((hstate.isOn && body.temp < cfgBody.heatSetpoint + heater.stopTempDelta) || (!hstate.isOn && body.temp > cfgBody.heatSetpoint - heater.startTempDelta)) + // && state.temps.solar > body.temp ) { isOn = true; body.heatStatus = sys.board.valueMaps.heatStatus.getValue('solar'); isHeating = true; } - else if (heater.coolingEnabled && body.temp > cfgBody.coolSetpoint && state.heliotrope.isNight && - state.temps.solar > body.temp + (hstate.isOn ? heater.stopTempDelta : heater.startTempDelta)) { + // reverse logic from heating states + else if (heater.coolingEnabled + && state.heliotrope.isNight + && state.temps.solar < body.temp // 1 + && body.temp > cfgBody.coolSetpoint // 2 + && (typeof hState.prevHeaterOffTemp === 'undefined' || ((hState.prevHeaterOffTemp - body.temp) < heater.startTempDelta)) // 3 + && (body.temp - state.temps.solar) > heater.stopTempDelta // 4 + && body.temp > (cfgBody.coolSetpoint + heater.stopTempDelta) // 5 + ) { + // else if (heater.coolingEnabled && (body.temp > cfgBody.coolSetpoint - heater.stopTempDelta && body.temp < cfgBody.coolSetpoint - heater.startTempDelta) && state.heliotrope.isNight && state.temps.solar < body.temp) { isOn = true; body.heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling'); isHeating = true; isCooling = true; } + if (hstate.isOn && !isOn) { + hState.prevHeaterOffTemp = body.temp; + } // 6 } break; case 'ultratemp': @@ -4502,185 +4511,186 @@ export class HeaterCommands extends BoardCommands { } } export class ValveCommands extends BoardCommands { - public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { - try { - // First delete the valves that should be removed. - for (let i = 0; i < ctx.valves.remove.length; i++) { - let v = ctx.valves.remove[i]; - try { - await sys.board.valves.deleteValveAsync(v); - res.addModuleSuccess('valve', `Remove: ${v.id}-${v.name}`); - } catch (err) { res.addModuleError('valve', `Remove: ${v.id}-${v.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.valves.update.length; i++) { - let v = ctx.valves.update[i]; - try { - await sys.board.valves.setValveAsync(v); - res.addModuleSuccess('valve', `Update: ${v.id}-${v.name}`); - } catch (err) { res.addModuleError('valve', `Update: ${v.id}-${v.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.valves.add.length; i++) { - let v = ctx.valves.add[i]; + public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { try { - // pull a little trick to first add the data then perform the update. This way we won't get a new id or - // it won't error out. - sys.valves.getItemById(ctx.valves.add[i].id, true); - await sys.board.valves.setValveAsync(v); - res.addModuleSuccess('valve', `Add: ${v.id}-${v.name}`); - } catch (err) { res.addModuleError('valve', `Add: ${v.id}-${v.name}: ${err.message}`); } - } - return true; - } catch (err) { logger.error(`Error restoring valves: ${err.message}`); res.addModuleError('system', `Error restoring valves: ${err.message}`); return false; } - } + // First delete the valves that should be removed. + for (let i = 0; i < ctx.valves.remove.length; i++) { + let v = ctx.valves.remove[i]; + try { + await sys.board.valves.deleteValveAsync(v); + res.addModuleSuccess('valve', `Remove: ${v.id}-${v.name}`); + } catch (err) { res.addModuleError('valve', `Remove: ${v.id}-${v.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.valves.update.length; i++) { + let v = ctx.valves.update[i]; + try { + await sys.board.valves.setValveAsync(v); + res.addModuleSuccess('valve', `Update: ${v.id}-${v.name}`); + } catch (err) { res.addModuleError('valve', `Update: ${v.id}-${v.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.valves.add.length; i++) { + let v = ctx.valves.add[i]; + try { + // pull a little trick to first add the data then perform the update. This way we won't get a new id or + // it won't error out. + sys.valves.getItemById(ctx.valves.add[i].id, true); + await sys.board.valves.setValveAsync(v); + res.addModuleSuccess('valve', `Add: ${v.id}-${v.name}`); + } catch (err) { res.addModuleError('valve', `Add: ${v.id}-${v.name}: ${err.message}`); } + } + return true; + } catch (err) { logger.error(`Error restoring valves: ${err.message}`); res.addModuleError('system', `Error restoring valves: ${err.message}`); return false; } + } - public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> { - try { - let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; - // Look at valves. - let cfg = rest.poolConfig; - for (let i = 0; i < cfg.valves.length; i++) { - let r = cfg.valves[i]; - let c = sys.valves.find(elem => r.id === elem.id); - if (typeof c === 'undefined') ctx.add.push(r); - else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); - } - for (let i = 0; i < sys.valves.length; i++) { - let c = sys.valves.getItemByIndex(i); - let r = cfg.valves.find(elem => elem.id == c.id); - if (typeof r === 'undefined') ctx.remove.push(c.get(true)); - } - return ctx; - } catch (err) { logger.error(`Error validating valves for restore: ${err.message}`); } - } + public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> { + try { + let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; + // Look at valves. + let cfg = rest.poolConfig; + for (let i = 0; i < cfg.valves.length; i++) { + let r = cfg.valves[i]; + let c = sys.valves.find(elem => r.id === elem.id); + if (typeof c === 'undefined') ctx.add.push(r); + else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); + } + for (let i = 0; i < sys.valves.length; i++) { + let c = sys.valves.getItemByIndex(i); + let r = cfg.valves.find(elem => elem.id == c.id); + if (typeof r === 'undefined') ctx.remove.push(c.get(true)); + } + return ctx; + } catch (err) { logger.error(`Error validating valves for restore: ${err.message}`); } + } - public async setValveStateAsync(valve: Valve, vstate: ValveState, isDiverted: boolean) { - if (valve.master === 1) await ncp.valves.setValveStateAsync(vstate, isDiverted); - else - vstate.isDiverted = isDiverted; - } - public async setValveAsync(obj: any, send: boolean = true): Promise { - try { - let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1; - obj.master = 1; - if (isNaN(id) || id <= 0) id = Math.max(sys.valves.getMaxId(false, 49) + 1, 50); + public async setValveStateAsync(valve: Valve, vstate: ValveState, isDiverted: boolean) { + if (valve.master === 1) await ncp.valves.setValveStateAsync(vstate, isDiverted); + else + vstate.isDiverted = isDiverted; + } + public async setValveAsync(obj: any, send: boolean = true): Promise { + try { + let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1; + obj.master = 1; + if (isNaN(id) || id <= 0) id = Math.max(sys.valves.getMaxId(false, 49) + 1, 50); - if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Nixie: Valve Id has not been defined ${id}`, obj.id, 'Valve')); - // Check the Nixie Control Panel to make sure the valve exist there. If it needs to be added then we should add it. - let valve = sys.valves.getItemById(id, true); - // Set all the valve properies. - let vstate = state.valves.getItemById(valve.id, true); - valve.isActive = true; - valve.circuit = typeof obj.circuit !== 'undefined' ? obj.circuit : valve.circuit; - valve.name = typeof obj.name !== 'undefined' ? obj.name : valve.name; - valve.connectionId = typeof obj.connectionId ? obj.connectionId : valve.connectionId; - valve.deviceBinding = typeof obj.deviceBinding !== 'undefined' ? obj.deviceBinding : valve.deviceBinding; - valve.pinId = typeof obj.pinId !== 'undefined' ? obj.pinId : valve.pinId; - await ncp.valves.setValveAsync(valve, obj); - sys.board.valves.syncValveStates(); - return valve; - } catch (err) { logger.error(`Nixie: Error setting valve definition. ${err.message}`); return Promise.reject(err); } - } + if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Nixie: Valve Id has not been defined ${id}`, obj.id, 'Valve')); + // Check the Nixie Control Panel to make sure the valve exist there. If it needs to be added then we should add it. + let valve = sys.valves.getItemById(id, true); + // Set all the valve properies. + let vstate = state.valves.getItemById(valve.id, true); + valve.isActive = true; + valve.circuit = typeof obj.circuit !== 'undefined' ? obj.circuit : valve.circuit; + valve.name = typeof obj.name !== 'undefined' ? obj.name : valve.name; + valve.connectionId = typeof obj.connectionId ? obj.connectionId : valve.connectionId; + valve.deviceBinding = typeof obj.deviceBinding !== 'undefined' ? obj.deviceBinding : valve.deviceBinding; + valve.pinId = typeof obj.pinId !== 'undefined' ? obj.pinId : valve.pinId; + await ncp.valves.setValveAsync(valve, obj); + sys.board.valves.syncValveStates(); + return valve; + } catch (err) { logger.error(`Nixie: Error setting valve definition. ${err.message}`); return Promise.reject(err); } + } - public async deleteValveAsync(obj: any): Promise { - let id = parseInt(obj.id, 10); - try { - if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Valve Id has not been defined', obj.id, 'Valve')); - let valve = sys.valves.getItemById(id, false); - let vstate = state.valves.getItemById(id); - if (valve.master === 1) await ncp.valves.deleteValveAsync(id); - valve.isActive = false; - vstate.hasChanged = true; - vstate.emitEquipmentChange(); - sys.valves.removeItemById(id); - state.valves.removeItemById(id); - return valve; - } catch (err) { return Promise.reject(new Error(`Error deleting valve: ${err.message}`)); } - // The following code will make sure we do not encroach on any valves defined by the OCP. - } - public async syncValveStates() { - try { - // Check to see if there is a drain circuit or feature on. If it is on then the intake will be diverted no mater what. - let drain = sys.equipment.shared ? typeof state.circuits.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spadrain' && elem.isOn === true) !== 'undefined' || - typeof state.features.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spadrain' && elem.isOn === true) !== 'undefined' : false; - // Check to see if there is a spillway circuit or feature on. If it is on then the return will be diverted no mater what. - let spillway = sys.equipment.shared ? typeof state.circuits.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined' || - typeof state.features.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined' : false; - let spa = sys.equipment.shared ? state.circuits.getItemById(1).isOn : false; - let pool = sys.equipment.shared ? state.circuits.getItemById(6).isOn : false; - // Set the valve mode. - if (!sys.equipment.shared) state.valveMode = sys.board.valueMaps.valveModes.getValue('off'); - else if (drain) state.valveMode = sys.board.valueMaps.valveModes.getValue('spadrain'); - else if (spillway) state.valveMode = sys.board.valueMaps.valveModes.getValue('spillway'); - else if (spa) state.valveMode = sys.board.valueMaps.valveModes.getValue('spa'); - else if (pool) state.valveMode = sys.board.valueMaps.valveModes.getValue('pool'); - else state.valveMode = sys.board.valueMaps.valveModes.getValue('off'); + public async deleteValveAsync(obj: any): Promise { + let id = parseInt(obj.id, 10); + try { + if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Valve Id has not been defined', obj.id, 'Valve')); + let valve = sys.valves.getItemById(id, false); + let vstate = state.valves.getItemById(id); + if (valve.master === 1) await ncp.valves.deleteValveAsync(id); + valve.isActive = false; + vstate.hasChanged = true; + vstate.emitEquipmentChange(); + sys.valves.removeItemById(id); + state.valves.removeItemById(id); + return valve; + } catch (err) { return Promise.reject(new Error(`Error deleting valve: ${err.message}`)); } + // The following code will make sure we do not encroach on any valves defined by the OCP. + } + public async syncValveStates() { + try { + // Check to see if there is a drain circuit or feature on. If it is on then the intake will be diverted no mater what. + let drain = sys.equipment.shared ? typeof state.circuits.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spadrain' && elem.isOn === true) !== 'undefined' || + typeof state.features.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spadrain' && elem.isOn === true) !== 'undefined' : false; + // Check to see if there is a spillway circuit or feature on. If it is on then the return will be diverted no mater what. + let spillway = sys.equipment.shared ? + typeof state.circuits.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined' || + typeof state.features.get().find(elem => typeof elem.type !== 'undefined' && elem.type.name === 'spillway' && elem.isOn === true) !== 'undefined' : false; + let spa = sys.equipment.shared ? state.circuits.getItemById(1).isOn : false; + let pool = sys.equipment.shared ? state.circuits.getItemById(6).isOn : false; + // Set the valve mode. + if (!sys.equipment.shared) state.valveMode = sys.board.valueMaps.valveModes.getValue('off'); + else if (drain) state.valveMode = sys.board.valueMaps.valveModes.getValue('spadrain'); + else if (spillway) state.valveMode = sys.board.valueMaps.valveModes.getValue('spillway'); + else if (spa) state.valveMode = sys.board.valueMaps.valveModes.getValue('spa'); + else if (pool) state.valveMode = sys.board.valueMaps.valveModes.getValue('pool'); + else state.valveMode = sys.board.valueMaps.valveModes.getValue('off'); - for (let i = 0; i < sys.valves.length; i++) { - // Run through all the valves to see whether they should be triggered or not. - let valve = sys.valves.getItemByIndex(i); - if (valve.isActive) { - let vstate = state.valves.getItemById(valve.id, true); - let isDiverted = vstate.isDiverted; - if (typeof valve.circuit !== 'undefined' && valve.circuit > 0) { - if (sys.equipment.shared && valve.isIntake === true) { - // Valve Diverted Positions - // Spa: Y - // Drain: Y - // Spillway: N - // Pool: N - isDiverted = utils.makeBool(spa || drain); // If the spa is on then the intake is diverted. - } - else if (sys.equipment.shared && valve.isReturn === true) { - // Valve Diverted Positions - // Spa: Y - // Drain: N - // Spillway: Y - // Pool: N - isDiverted = utils.makeBool((spa || spillway) && !drain); + for (let i = 0; i < sys.valves.length; i++) { + // Run through all the valves to see whether they should be triggered or not. + let valve = sys.valves.getItemByIndex(i); + if (valve.isActive) { + let vstate = state.valves.getItemById(valve.id, true); + let isDiverted = vstate.isDiverted; + if (typeof valve.circuit !== 'undefined' && valve.circuit > 0) { + if (sys.equipment.shared && valve.isIntake === true) { + // Valve Diverted Positions + // Spa: Y + // Drain: Y + // Spillway: N + // Pool: N + isDiverted = utils.makeBool(spa || drain); // If the spa is on then the intake is diverted. + } + else if (sys.equipment.shared && valve.isReturn === true) { + // Valve Diverted Positions + // Spa: Y + // Drain: N + // Spillway: Y + // Pool: N + isDiverted = utils.makeBool((spa || spillway) && !drain); + } + else { + let circ = state.circuits.getInterfaceById(valve.circuit); + isDiverted = utils.makeBool(circ.isOn); + } + } + else + isDiverted = false; + vstate.type = valve.type; + vstate.name = valve.name; + await sys.board.valves.setValveStateAsync(valve, vstate, isDiverted); + } } - else { - let circ = state.circuits.getInterfaceById(valve.circuit); - isDiverted = utils.makeBool(circ.isOn); - } - } - else - isDiverted = false; - vstate.type = valve.type; - vstate.name = valve.name; - await sys.board.valves.setValveStateAsync(valve, vstate, isDiverted); - } - } - } catch (err) { logger.error(`syncValveStates: Error synchronizing valves ${err.message}`); } - } - public getBodyValveCircuitIds(isOn?: boolean): number[] { - let arrIds: number[] = []; - if (sys.equipment.shared !== true) return arrIds; + } catch (err) { logger.error(`syncValveStates: Error synchronizing valves ${err.message}`); } + } + public getBodyValveCircuitIds(isOn?: boolean): number[] { + let arrIds: number[] = []; + if (sys.equipment.shared !== true) return arrIds; - { - let dtype = sys.board.valueMaps.circuitFunctions.getValue('spadrain'); - let stype = sys.board.valueMaps.circuitFunctions.getValue('spillway'); - let ptype = sys.board.valueMaps.circuitFunctions.getValue('pool'); - let sptype = sys.board.valueMaps.circuitFunctions.getValue('spa'); - for (let i = 0; i < state.circuits.length; i++) { - let cstate = state.circuits.getItemByIndex(i); - if (typeof isOn === 'undefined' || cstate.isOn === isOn) { - if (cstate.id === 1 || cstate.id === 6) arrIds.push(cstate.id); - if (cstate.type === dtype || cstate.type === stype || cstate.type === ptype || cstate.type === sptype) arrIds.push(cstate.id); + { + let dtype = sys.board.valueMaps.circuitFunctions.getValue('spadrain'); + let stype = sys.board.valueMaps.circuitFunctions.getValue('spillway'); + let ptype = sys.board.valueMaps.circuitFunctions.getValue('pool'); + let sptype = sys.board.valueMaps.circuitFunctions.getValue('spa'); + for (let i = 0; i < state.circuits.length; i++) { + let cstate = state.circuits.getItemByIndex(i); + if (typeof isOn === 'undefined' || cstate.isOn === isOn) { + if (cstate.id === 1 || cstate.id === 6) arrIds.push(cstate.id); + if (cstate.type === dtype || cstate.type === stype || cstate.type === ptype || cstate.type === sptype) arrIds.push(cstate.id); + } + } } - } - } - { - let dtype = sys.board.valueMaps.featureFunctions.getValue('spadrain'); - let stype = sys.board.valueMaps.featureFunctions.getValue('spillway'); - for (let i = 0; i < state.features.length; i++) { - let fstate = state.features.getItemByIndex(i); - if (typeof isOn === 'undefined' || fstate.isOn === isOn) { - if (fstate.type === dtype || fstate.type === stype) arrIds.push(fstate.id); + { + let dtype = sys.board.valueMaps.featureFunctions.getValue('spadrain'); + let stype = sys.board.valueMaps.featureFunctions.getValue('spillway'); + for (let i = 0; i < state.features.length; i++) { + let fstate = state.features.getItemByIndex(i); + if (typeof isOn === 'undefined' || fstate.isOn === isOn) { + if (fstate.type === dtype || fstate.type === stype) arrIds.push(fstate.id); + } + } } - } + return arrIds; } - return arrIds; - } } export class ChemDoserCommands extends BoardCommands { public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { @@ -4901,20 +4911,20 @@ export class ChemControllerCommands extends BoardCommands { } catch (err) { logger.error(`Error validating chemControllers for restore: ${err.message}`); } } - public async deleteChemControllerAsync(data: any): Promise { - try { - let id = typeof data.id !== 'undefined' ? parseInt(data.id, 10) : -1; - if (typeof id === 'undefined' || isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid Chem Controller Id`, id, 'chemController')); - let chem = sys.chemControllers.getItemById(id); - let schem = state.chemControllers.getItemById(id); - schem.isActive = chem.isActive = false; - await ncp.chemControllers.removeById(id); - sys.chemControllers.removeItemById(id); - state.chemControllers.removeItemById(id); - sys.emitEquipmentChange(); - return Promise.resolve(chem); - } catch (err) { logger.error(`Error deleting chem controller ${err.message}`); } - } + public async deleteChemControllerAsync(data: any): Promise { + try { + let id = typeof data.id !== 'undefined' ? parseInt(data.id, 10) : -1; + if (typeof id === 'undefined' || isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid Chem Controller Id`, id, 'chemController')); + let chem = sys.chemControllers.getItemById(id); + let schem = state.chemControllers.getItemById(id); + schem.isActive = chem.isActive = false; + await ncp.chemControllers.removeById(id); + sys.chemControllers.removeItemById(id); + state.chemControllers.removeItemById(id); + sys.emitEquipmentChange(); + return Promise.resolve(chem); + } catch (err) { logger.error(`Error deleting chem controller ${err.message}`); } + } public async manualDoseAsync(data: any): Promise { try { let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined; @@ -4944,311 +4954,311 @@ export class ChemControllerCommands extends BoardCommands { } catch (err) { return Promise.reject(err); } } - public async cancelDosingAsync(data: any): Promise { - try { - let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined; - if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel dosing: Invalid chem controller id was provided ${data.id}`, 'chemController', data.id)); - let chem = sys.chemControllers.find(elem => elem.id === id); - if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel dosing: Chem controller was not found ${data.id}`, 'chemController', data.id)); - // Let's check the type. AFAIK you cannot manual dose an IntelliChem. - let type = sys.board.valueMaps.chemControllerTypes.transform(chem.type); - if (type.name !== 'rem') return Promise.reject(new InvalidEquipmentDataError(`You can only cancel dosing on REM Chem controllers. Cannot cancel ${type.desc}`, 'chemController', data.id)); - // We are down to the nitty gritty. Let REM Chem do its thing. - await ncp.chemControllers.cancelDoseAsync(chem.id, data); - return Promise.resolve(state.chemControllers.getItemById(id)); - } catch (err) { return Promise.reject(err); } - } - public async manualMixAsync(data: any): Promise { - try { - let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined; - if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot begin mixing: Invalid chem controller id was provided ${data.id}`, 'chemController', data.id)); - let chem = sys.chemControllers.find(elem => elem.id === id); - if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot begin mixing: Chem controller was not found ${data.id}`, 'chemController', data.id)); - // Let's check the type. AFAIK you cannot manual dose an IntelliChem. - let type = sys.board.valueMaps.chemControllerTypes.transform(chem.type); - if (type.name !== 'rem') return Promise.reject(new InvalidEquipmentDataError(`You can only perform manual mixing REM Chem controllers. Cannot manually dose ${type.desc}`, 'chemController', data.id)); - // We are down to the nitty gritty. Let REM Chem do its thing. - await ncp.chemControllers.manualMixAsync(chem.id, data); - return Promise.resolve(state.chemControllers.getItemById(id)); - } catch (err) { return Promise.reject(err); } - } - public async cancelMixingAsync(data: any): Promise { - try { - let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined; - if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel mixing: Invalid chem controller id was provided ${data.id}`, 'chemController', data.id)); - let chem = sys.chemControllers.find(elem => elem.id === id); - if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel mixing: Chem controller was not found ${data.id}`, 'chemController', data.id)); - // Let's check the type. AFAIK you cannot manual dose an IntelliChem. - let type = sys.board.valueMaps.chemControllerTypes.transform(chem.type); - if (type.name !== 'rem') return Promise.reject(new InvalidEquipmentDataError(`You can only cancel mixing on REM Chem controllers. Cannot cancel ${type.desc}`, 'chemController', data.id)); - // We are down to the nitty gritty. Let REM Chem do its thing. - await ncp.chemControllers.cancelMixingAsync(chem.id, data); - return Promise.resolve(state.chemControllers.getItemById(id)); - } catch (err) { return Promise.reject(err); } - } + public async cancelDosingAsync(data: any): Promise { + try { + let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined; + if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel dosing: Invalid chem controller id was provided ${data.id}`, 'chemController', data.id)); + let chem = sys.chemControllers.find(elem => elem.id === id); + if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel dosing: Chem controller was not found ${data.id}`, 'chemController', data.id)); + // Let's check the type. AFAIK you cannot manual dose an IntelliChem. + let type = sys.board.valueMaps.chemControllerTypes.transform(chem.type); + if (type.name !== 'rem') return Promise.reject(new InvalidEquipmentDataError(`You can only cancel dosing on REM Chem controllers. Cannot cancel ${type.desc}`, 'chemController', data.id)); + // We are down to the nitty gritty. Let REM Chem do its thing. + await ncp.chemControllers.cancelDoseAsync(chem.id, data); + return Promise.resolve(state.chemControllers.getItemById(id)); + } catch (err) { return Promise.reject(err); } + } + public async manualMixAsync(data: any): Promise { + try { + let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined; + if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot begin mixing: Invalid chem controller id was provided ${data.id}`, 'chemController', data.id)); + let chem = sys.chemControllers.find(elem => elem.id === id); + if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot begin mixing: Chem controller was not found ${data.id}`, 'chemController', data.id)); + // Let's check the type. AFAIK you cannot manual dose an IntelliChem. + let type = sys.board.valueMaps.chemControllerTypes.transform(chem.type); + if (type.name !== 'rem') return Promise.reject(new InvalidEquipmentDataError(`You can only perform manual mixing REM Chem controllers. Cannot manually dose ${type.desc}`, 'chemController', data.id)); + // We are down to the nitty gritty. Let REM Chem do its thing. + await ncp.chemControllers.manualMixAsync(chem.id, data); + return Promise.resolve(state.chemControllers.getItemById(id)); + } catch (err) { return Promise.reject(err); } + } + public async cancelMixingAsync(data: any): Promise { + try { + let id = typeof data.id !== 'undefined' ? parseInt(data.id) : undefined; + if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel mixing: Invalid chem controller id was provided ${data.id}`, 'chemController', data.id)); + let chem = sys.chemControllers.find(elem => elem.id === id); + if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Cannot cancel mixing: Chem controller was not found ${data.id}`, 'chemController', data.id)); + // Let's check the type. AFAIK you cannot manual dose an IntelliChem. + let type = sys.board.valueMaps.chemControllerTypes.transform(chem.type); + if (type.name !== 'rem') return Promise.reject(new InvalidEquipmentDataError(`You can only cancel mixing on REM Chem controllers. Cannot cancel ${type.desc}`, 'chemController', data.id)); + // We are down to the nitty gritty. Let REM Chem do its thing. + await ncp.chemControllers.cancelMixingAsync(chem.id, data); + return Promise.resolve(state.chemControllers.getItemById(id)); + } catch (err) { return Promise.reject(err); } + } - // If we land here then this is definitely a non-OCP implementation. Pass this off to nixie to do her thing. - protected async setIntelliChemAsync(data: any): Promise { - try { - let chem = sys.chemControllers.getItemById(data.id); - return chem.master === 1 ? await ncp.chemControllers.setControllerAsync(chem, data) : chem; - } catch (err) { return Promise.reject(err); } - } - public findChemController(data: any) { - let address = parseInt(data.address, 10); - let id = parseInt(data.id, 10); - if (!isNaN(id)) return sys.chemControllers.find(x => x.id === id); - else if (!isNaN(address)) return sys.chemControllers.find(x => x.address === address); - } - public async setChemControllerAsync(data: any, send: boolean = true): Promise { - // The following are the rules related to when an OCP is present. - // ============================================================== - // 1. IntelliChem cannot be controlled/polled via Nixie, since there is no enable/disable from the OCP at this point we don't know who is in control of polling. - // 2. With *Touch Commands will be sent directly to the IntelliChem controller in the hopes that the OCP will pick it up. Turns out this is not correct. The TouchBoard now has the proper interface. - // 3. njspc will communicate to the OCP for IntelliChem control via the configuration interface. + // If we land here then this is definitely a non-OCP implementation. Pass this off to nixie to do her thing. + protected async setIntelliChemAsync(data: any): Promise { + try { + let chem = sys.chemControllers.getItemById(data.id); + return chem.master === 1 ? await ncp.chemControllers.setControllerAsync(chem, data) : chem; + } catch (err) { return Promise.reject(err); } + } + public findChemController(data: any) { + let address = parseInt(data.address, 10); + let id = parseInt(data.id, 10); + if (!isNaN(id)) return sys.chemControllers.find(x => x.id === id); + else if (!isNaN(address)) return sys.chemControllers.find(x => x.address === address); + } + public async setChemControllerAsync(data: any, send: boolean = true): Promise { + // The following are the rules related to when an OCP is present. + // ============================================================== + // 1. IntelliChem cannot be controlled/polled via Nixie, since there is no enable/disable from the OCP at this point we don't know who is in control of polling. + // 2. With *Touch Commands will be sent directly to the IntelliChem controller in the hopes that the OCP will pick it up. Turns out this is not correct. The TouchBoard now has the proper interface. + // 3. njspc will communicate to the OCP for IntelliChem control via the configuration interface. - // The following are the rules related to when no OCP is present. - // ============================================================= - // 1. All chemControllers will be controlled via Nixie (IntelliChem, REM Chem). - try { - // let c1 = sys.chemControllers.getItemById(1); - let chem = sys.board.chemControllers.findChemController(data); - let isAdd = typeof chem === 'undefined' || typeof chem.isActive === 'undefined'; - let type = sys.board.valueMaps.chemControllerTypes.encode(isAdd ? data.type : chem.type); - if (typeof type === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`The chem controller type could not be determined ${data.type || type}`, 'chemController', type)); - if (isAdd && sys.equipment.maxChemControllers <= sys.chemControllers.length) return Promise.reject(new InvalidEquipmentDataError(`The maximum number of chem controllers have been added to your controller`, 'chemController', sys.equipment.maxChemControllers)); - let address = typeof data.address !== 'undefined' ? parseInt(data.address, 10) : isAdd ? undefined : chem.address; - let t = sys.board.valueMaps.chemControllerTypes.transform(type); - if (t.hasAddress) { - // First lets make sure the user supplied an address. - if (isNaN(address)) return Promise.reject(new InvalidEquipmentDataError(`${t.desc} chem controllers require a valid address`, 'chemController', data.address)); - if (typeof sys.chemControllers.find(x => x.address === address && x.id !== (isAdd ? -1 : chem.id)) !== 'undefined') return Promise.reject(new InvalidEquipmentDataError(`${type.desc} chem controller addresses must be unique`, 'chemController', data.address)); - } - if (isAdd) { - // At this point we are going to add the chem controller no matter what. - data.id = sys.chemControllers.getNextControllerId(type); - chem = sys.chemControllers.getItemById(data.id, true); - chem.type = type; - if (t.hasAddress) chem.address = address; - } - chem.isActive = true; - // So here is the thing. If you have an OCP then the IntelliChem must be controlled by that. - // the messages on the bus will talk back to the OCP so if you do not do this mayhem will ensue. - if (t.name === 'intellichem') { - logger.info(`${chem.name} - ${chem.id} routing IntelliChem to OCP`); - await sys.board.chemControllers.setIntelliChemAsync(data); - } - else - await ncp.chemControllers.setControllerAsync(chem, data); - return Promise.resolve(chem); - } - catch (err) { return Promise.reject(err); } - } - public async setChemControllerStateAsync(data: any): Promise { - // For the most part all of the settable settings for IntelliChem are config settings. REM is a bit of a different story so that - // should map to the ncp - let chem = sys.board.chemControllers.findChemController(data); - if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`A valid chem controller could not be found for id:${data.id} or address ${data.address}`, data.id || data.address, 'chemController')); - data.id = chem.id; - logger.info(`Setting ${chem.name} data ${chem.master}`); - if (chem.master === 1) await ncp.chemControllers.setControllerAsync(chem, data); - else await sys.board.chemControllers.setChemControllerAsync(data); - let schem = state.chemControllers.getItemById(chem.id, true); - return Promise.resolve(schem); - } + // The following are the rules related to when no OCP is present. + // ============================================================= + // 1. All chemControllers will be controlled via Nixie (IntelliChem, REM Chem). + try { + // let c1 = sys.chemControllers.getItemById(1); + let chem = sys.board.chemControllers.findChemController(data); + let isAdd = typeof chem === 'undefined' || typeof chem.isActive === 'undefined'; + let type = sys.board.valueMaps.chemControllerTypes.encode(isAdd ? data.type : chem.type); + if (typeof type === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`The chem controller type could not be determined ${data.type || type}`, 'chemController', type)); + if (isAdd && sys.equipment.maxChemControllers <= sys.chemControllers.length) return Promise.reject(new InvalidEquipmentDataError(`The maximum number of chem controllers have been added to your controller`, 'chemController', sys.equipment.maxChemControllers)); + let address = typeof data.address !== 'undefined' ? parseInt(data.address, 10) : isAdd ? undefined : chem.address; + let t = sys.board.valueMaps.chemControllerTypes.transform(type); + if (t.hasAddress) { + // First lets make sure the user supplied an address. + if (isNaN(address)) return Promise.reject(new InvalidEquipmentDataError(`${t.desc} chem controllers require a valid address`, 'chemController', data.address)); + if (typeof sys.chemControllers.find(x => x.address === address && x.id !== (isAdd ? -1 : chem.id)) !== 'undefined') return Promise.reject(new InvalidEquipmentDataError(`${type.desc} chem controller addresses must be unique`, 'chemController', data.address)); + } + if (isAdd) { + // At this point we are going to add the chem controller no matter what. + data.id = sys.chemControllers.getNextControllerId(type); + chem = sys.chemControllers.getItemById(data.id, true); + chem.type = type; + if (t.hasAddress) chem.address = address; + } + chem.isActive = true; + // So here is the thing. If you have an OCP then the IntelliChem must be controlled by that. + // the messages on the bus will talk back to the OCP so if you do not do this mayhem will ensue. + if (t.name === 'intellichem') { + logger.info(`${chem.name} - ${chem.id} routing IntelliChem to OCP`); + await sys.board.chemControllers.setIntelliChemAsync(data); + } + else + await ncp.chemControllers.setControllerAsync(chem, data); + return Promise.resolve(chem); + } + catch (err) { return Promise.reject(err); } + } + public async setChemControllerStateAsync(data: any): Promise { + // For the most part all of the settable settings for IntelliChem are config settings. REM is a bit of a different story so that + // should map to the ncp + let chem = sys.board.chemControllers.findChemController(data); + if (typeof chem === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`A valid chem controller could not be found for id:${data.id} or address ${data.address}`, data.id || data.address, 'chemController')); + data.id = chem.id; + logger.info(`Setting ${chem.name} data ${chem.master}`); + if (chem.master === 1) await ncp.chemControllers.setControllerAsync(chem, data); + else await sys.board.chemControllers.setChemControllerAsync(data); + let schem = state.chemControllers.getItemById(chem.id, true); + return Promise.resolve(schem); + } } export class FilterCommands extends BoardCommands { - public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { - try { - // First delete the filters that should be removed. - for (let i = 0; i < ctx.filters.remove.length; i++) { - let filter = ctx.filters.remove[i]; - try { - sys.filters.removeItemById(filter.id); - state.filters.removeItemById(filter.id); - res.addModuleSuccess('filter', `Remove: ${filter.id}-${filter.name}`); - } catch (err) { res.addModuleError('filter', `Remove: ${filter.id}-${filter.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.filters.update.length; i++) { - let filter = ctx.filters.update[i]; + public async restore(rest: { poolConfig: any, poolState: any }, ctx: any, res: RestoreResults): Promise { try { - await sys.board.filters.setFilterAsync(filter); - res.addModuleSuccess('filter', `Update: ${filter.id}-${filter.name}`); - } catch (err) { res.addModuleError('filter', `Update: ${filter.id}-${filter.name}: ${err.message}`); } - } - for (let i = 0; i < ctx.filters.add.length; i++) { - let filter = ctx.filters.add[i]; + // First delete the filters that should be removed. + for (let i = 0; i < ctx.filters.remove.length; i++) { + let filter = ctx.filters.remove[i]; + try { + sys.filters.removeItemById(filter.id); + state.filters.removeItemById(filter.id); + res.addModuleSuccess('filter', `Remove: ${filter.id}-${filter.name}`); + } catch (err) { res.addModuleError('filter', `Remove: ${filter.id}-${filter.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.filters.update.length; i++) { + let filter = ctx.filters.update[i]; + try { + await sys.board.filters.setFilterAsync(filter); + res.addModuleSuccess('filter', `Update: ${filter.id}-${filter.name}`); + } catch (err) { res.addModuleError('filter', `Update: ${filter.id}-${filter.name}: ${err.message}`); } + } + for (let i = 0; i < ctx.filters.add.length; i++) { + let filter = ctx.filters.add[i]; + try { + // pull a little trick to first add the data then perform the update. + sys.filters.getItemById(filter.id, true); + await sys.board.filters.setFilterAsync(filter); + res.addModuleSuccess('filter', `Add: ${filter.id}-${filter.name}`); + } catch (err) { res.addModuleError('filter', `Add: ${filter.id}-${filter.name}: ${err.message}`); } + } + return true; + } catch (err) { logger.error(`Error restoring filters: ${err.message}`); res.addModuleError('system', `Error restoring filters: ${err.message}`); return false; } + } + public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> { try { - // pull a little trick to first add the data then perform the update. - sys.filters.getItemById(filter.id, true); - await sys.board.filters.setFilterAsync(filter); - res.addModuleSuccess('filter', `Add: ${filter.id}-${filter.name}`); - } catch (err) { res.addModuleError('filter', `Add: ${filter.id}-${filter.name}: ${err.message}`); } - } - return true; - } catch (err) { logger.error(`Error restoring filters: ${err.message}`); res.addModuleError('system', `Error restoring filters: ${err.message}`); return false; } - } - public async validateRestore(rest: { poolConfig: any, poolState: any }): Promise<{ errors: any, warnings: any, add: any, update: any, remove: any }> { - try { - let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; - // Look at filters. - let cfg = rest.poolConfig; - for (let i = 0; i < cfg.filters.length; i++) { - let r = cfg.filters[i]; - let c = sys.filters.find(elem => r.id === elem.id); - if (typeof c === 'undefined') ctx.add.push(r); - else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); - } - for (let i = 0; i < sys.filters.length; i++) { - let c = sys.filters.getItemByIndex(i); - let r = cfg.filters.find(elem => elem.id == c.id); - if (typeof r === 'undefined') ctx.remove.push(c.get(true)); - } - return ctx; - } catch (err) { logger.error(`Error validating filters for restore: ${err.message}`); } - } + let ctx = { errors: [], warnings: [], add: [], update: [], remove: [] }; + // Look at filters. + let cfg = rest.poolConfig; + for (let i = 0; i < cfg.filters.length; i++) { + let r = cfg.filters[i]; + let c = sys.filters.find(elem => r.id === elem.id); + if (typeof c === 'undefined') ctx.add.push(r); + else if (JSON.stringify(c.get()) !== JSON.stringify(r)) ctx.update.push(r); + } + for (let i = 0; i < sys.filters.length; i++) { + let c = sys.filters.getItemByIndex(i); + let r = cfg.filters.find(elem => elem.id == c.id); + if (typeof r === 'undefined') ctx.remove.push(c.get(true)); + } + return ctx; + } catch (err) { logger.error(`Error validating filters for restore: ${err.message}`); } + } - public async syncFilterStates() { - try { - for (let i = 0; i < sys.filters.length; i++) { - // Run through all the valves to see whether they should be triggered or not. - let filter = sys.filters.getItemByIndex(i); - if (filter.isActive && !isNaN(filter.id)) { - let fstate = state.filters.getItemById(filter.id, true); - // Check to see if the associated body is on. - await sys.board.filters.setFilterStateAsync(filter, fstate, sys.board.bodies.isBodyOn(filter.body)); - } - } - } catch (err) { logger.error(`syncFilterStates: Error synchronizing filters ${err.message}`); } - } - public async setFilterPressure(id: number, pressure: number, units?: string) { - try { - let filter = sys.filters.find(elem => elem.id === id); - if (typeof filter === 'undefined' || isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`setFilterPressure: Invalid equipmentId ${id}`, id, 'Filter')); - if (isNaN(pressure)) return Promise.reject(new InvalidEquipmentDataError(`setFilterPressure: Invalid filter pressure ${pressure} for ${filter.name}`, 'Filter', pressure)); - let sfilter = state.filters.getItemById(filter.id, true); - // Convert the pressure to the units that we have set on the filter for the pressure units. - let pu = sys.board.valueMaps.pressureUnits.transform(filter.pressureUnits || 0); - if (typeof units === 'undefined' || units === '') units = pu.name; - sfilter.pressureUnits = filter.pressureUnits; - sfilter.pressure = Math.round(pressure * 1000) / 1000; // Round this to 3 decimal places just in case we are getting stupid scales. - // Check to see if our circuit is the only thing on. If it is then we will be setting our current clean pressure to the incoming pressure and calculating a percentage. - // Rules for the circuit. - // 1. The assigned circuit must be on. - // 2. There must not be a current freeze condition - // 3. No heaters can be on. - // 4. The assigned circuit must be on exclusively but we will be ignoring any of the light circuit types for the exclusivity. - let cstate = state.circuits.getInterfaceById(filter.pressureCircuitId); - if (cstate.isOn && state.freeze !== true) { - // Ok so our circuit is on. We need to check to see if any other circuits are on. This includes heaters. The reason for this is that even with - // a gas heater there may be a heater bypass that will screw up our numbers. Certainly reflow on a solar heater will skew the numbers. - let hon = state.temps.bodies.toArray().find(elem => elem.isOn && (elem.heatStatus || 0) !== 0); - if (typeof hon === 'undefined') { - // Put together the circuit types that could be lights. We don't want these. - let ctypes = []; - let funcs = sys.board.valueMaps.circuitFunctions.toArray(); - for (let i = 0; i < funcs.length; i++) { - let f = funcs[i]; - if (f.isLight) ctypes.push(f.val); - } - let con = state.circuits.find(elem => elem.isOn === true && elem.id !== filter.pressureCircuitId && elem.id !== 1 && elem.id !== 6 && !ctypes.includes(elem.type)); - if (typeof con === 'undefined') { - // This check is the one that will be the most problematic. For this reason we are only going to check features that are not generic. If they are spillway - // it definitely has to be off. - let feats = state.features.toArray(); - let fon = false; - for (let i = 0; i < feats.length && fon === false; i++) { - let f = feats[i]; - if (!f.isOn) continue; - if (f.id === filter.pressureCircuitId) continue; - if (f.type !== 0) fon = true; - // Check to see if this feature is used on a valve. This will make it - // not include this pressure either. We do not care whether the valve is diverted or not. - if (typeof sys.valves.find(elem => elem.circuit === f.id) !== 'undefined') - fon = true; - else { - // Finally if the feature happens to be used on a pump then we don't want it either. - let pumps = sys.pumps.get(); - for (let j = 0; j < pumps.length; j++) { - let pmp = pumps[j]; - if (typeof pmp.circuits !== 'undefined') { - if (typeof pmp.circuits.find(elem => elem.circuit === f.id) !== 'undefined') { - fon = true; - break; - } - } + public async syncFilterStates() { + try { + for (let i = 0; i < sys.filters.length; i++) { + // Run through all the valves to see whether they should be triggered or not. + let filter = sys.filters.getItemByIndex(i); + if (filter.isActive && !isNaN(filter.id)) { + let fstate = state.filters.getItemById(filter.id, true); + // Check to see if the associated body is on. + await sys.board.filters.setFilterStateAsync(filter, fstate, sys.board.bodies.isBodyOn(filter.body)); } - } } - if (!fon) { - // Finally we have a value we can believe in. - sfilter.refPressure = pressure; + } catch (err) { logger.error(`syncFilterStates: Error synchronizing filters ${err.message}`); } + } + public async setFilterPressure(id: number, pressure: number, units?: string) { + try { + let filter = sys.filters.find(elem => elem.id === id); + if (typeof filter === 'undefined' || isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`setFilterPressure: Invalid equipmentId ${id}`, id, 'Filter')); + if (isNaN(pressure)) return Promise.reject(new InvalidEquipmentDataError(`setFilterPressure: Invalid filter pressure ${pressure} for ${filter.name}`, 'Filter', pressure)); + let sfilter = state.filters.getItemById(filter.id, true); + // Convert the pressure to the units that we have set on the filter for the pressure units. + let pu = sys.board.valueMaps.pressureUnits.transform(filter.pressureUnits || 0); + if (typeof units === 'undefined' || units === '') units = pu.name; + sfilter.pressureUnits = filter.pressureUnits; + sfilter.pressure = Math.round(pressure * 1000) / 1000; // Round this to 3 decimal places just in case we are getting stupid scales. + // Check to see if our circuit is the only thing on. If it is then we will be setting our current clean pressure to the incoming pressure and calculating a percentage. + // Rules for the circuit. + // 1. The assigned circuit must be on. + // 2. There must not be a current freeze condition + // 3. No heaters can be on. + // 4. The assigned circuit must be on exclusively but we will be ignoring any of the light circuit types for the exclusivity. + let cstate = state.circuits.getInterfaceById(filter.pressureCircuitId); + if (cstate.isOn && state.freeze !== true) { + // Ok so our circuit is on. We need to check to see if any other circuits are on. This includes heaters. The reason for this is that even with + // a gas heater there may be a heater bypass that will screw up our numbers. Certainly reflow on a solar heater will skew the numbers. + let hon = state.temps.bodies.toArray().find(elem => elem.isOn && (elem.heatStatus || 0) !== 0); + if (typeof hon === 'undefined') { + // Put together the circuit types that could be lights. We don't want these. + let ctypes = []; + let funcs = sys.board.valueMaps.circuitFunctions.toArray(); + for (let i = 0; i < funcs.length; i++) { + let f = funcs[i]; + if (f.isLight) ctypes.push(f.val); + } + let con = state.circuits.find(elem => elem.isOn === true && elem.id !== filter.pressureCircuitId && elem.id !== 1 && elem.id !== 6 && !ctypes.includes(elem.type)); + if (typeof con === 'undefined') { + // This check is the one that will be the most problematic. For this reason we are only going to check features that are not generic. If they are spillway + // it definitely has to be off. + let feats = state.features.toArray(); + let fon = false; + for (let i = 0; i < feats.length && fon === false; i++) { + let f = feats[i]; + if (!f.isOn) continue; + if (f.id === filter.pressureCircuitId) continue; + if (f.type !== 0) fon = true; + // Check to see if this feature is used on a valve. This will make it + // not include this pressure either. We do not care whether the valve is diverted or not. + if (typeof sys.valves.find(elem => elem.circuit === f.id) !== 'undefined') + fon = true; + else { + // Finally if the feature happens to be used on a pump then we don't want it either. + let pumps = sys.pumps.get(); + for (let j = 0; j < pumps.length; j++) { + let pmp = pumps[j]; + if (typeof pmp.circuits !== 'undefined') { + if (typeof pmp.circuits.find(elem => elem.circuit === f.id) !== 'undefined') { + fon = true; + break; + } + } + } + } + } + if (!fon) { + // Finally we have a value we can believe in. + sfilter.refPressure = pressure; + } + } + else { + logger.verbose(`Circuit ${con.id}-${con.name} is currently on filter pressure for cleaning ignored.`); + } + } + else { + logger.verbose(`Heater for body ${hon.name} is currently on ${hon.heatStatus} filter pressure for cleaning skipped.`); + } } - } - else { - logger.verbose(`Circuit ${con.id}-${con.name} is currently on filter pressure for cleaning ignored.`); - } - } - else { - logger.verbose(`Heater for body ${hon.name} is currently on ${hon.heatStatus} filter pressure for cleaning skipped.`); + sfilter.emitEquipmentChange(); } - } - sfilter.emitEquipmentChange(); + catch (err) { logger.error(`setFilterPressure: Error setting filter #${id} pressure to ${pressure}${units || ''}`); } } - catch (err) { logger.error(`setFilterPressure: Error setting filter #${id} pressure to ${pressure}${units || ''}`); } - } - public async setFilterStateAsync(filter: Filter, fstate: FilterState, isOn: boolean) { fstate.isOn = isOn; } - public async setFilterAsync(data: any): Promise { - let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); - if (id <= 0) id = sys.filters.length + 1; // set max filters? - if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid filter id: ${data.id}`, data.id, 'Filter')); - let filter = sys.filters.getItemById(id, id > 0); - let sfilter = state.filters.getItemById(id, id > 0); - let filterType = typeof data.filterType !== 'undefined' ? parseInt(data.filterType, 10) : filter.filterType; - if (typeof filterType === 'undefined') filterType = sys.board.valueMaps.filterTypes.getValue('unknown'); + public async setFilterStateAsync(filter: Filter, fstate: FilterState, isOn: boolean) { fstate.isOn = isOn; } + public async setFilterAsync(data: any): Promise { + let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); + if (id <= 0) id = sys.filters.length + 1; // set max filters? + if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid filter id: ${data.id}`, data.id, 'Filter')); + let filter = sys.filters.getItemById(id, id > 0); + let sfilter = state.filters.getItemById(id, id > 0); + let filterType = typeof data.filterType !== 'undefined' ? parseInt(data.filterType, 10) : filter.filterType; + if (typeof filterType === 'undefined') filterType = sys.board.valueMaps.filterTypes.getValue('unknown'); - // The only way to delete a filter is to call deleteFilterAsync. - //if (typeof data.isActive !== 'undefined') { - // if (utils.makeBool(data.isActive) === false) { - // sys.filters.removeItemById(id); - // state.filters.removeItemById(id); - // return; - // } - //} + // The only way to delete a filter is to call deleteFilterAsync. + //if (typeof data.isActive !== 'undefined') { + // if (utils.makeBool(data.isActive) === false) { + // sys.filters.removeItemById(id); + // state.filters.removeItemById(id); + // return; + // } + //} - let body = typeof data.body !== 'undefined' ? data.body : filter.body; - let name = typeof data.name !== 'undefined' ? data.name : filter.name; - if (typeof body === 'undefined') body = 32; - // At this point we should have all the data. Validate it. - if (!sys.board.valueMaps.filterTypes.valExists(filterType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid filter type; ${filterType}`, 'Filter', filterType)); + let body = typeof data.body !== 'undefined' ? data.body : filter.body; + let name = typeof data.name !== 'undefined' ? data.name : filter.name; + if (typeof body === 'undefined') body = 32; + // At this point we should have all the data. Validate it. + if (!sys.board.valueMaps.filterTypes.valExists(filterType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid filter type; ${filterType}`, 'Filter', filterType)); - filter.pressureUnits = typeof data.pressureUnits !== 'undefined' ? data.pressureUnits || 0 : filter.pressureUnits || 0; - filter.pressureCircuitId = parseInt(data.pressureCircuitId || filter.pressureCircuitId || 6, 10); - filter.cleanPressure = parseFloat(data.cleanPressure || filter.cleanPressure || 0); - filter.dirtyPressure = parseFloat(data.dirtyPressure || filter.dirtyPressure || 0); + filter.pressureUnits = typeof data.pressureUnits !== 'undefined' ? data.pressureUnits || 0 : filter.pressureUnits || 0; + filter.pressureCircuitId = parseInt(data.pressureCircuitId || filter.pressureCircuitId || 6, 10); + filter.cleanPressure = parseFloat(data.cleanPressure || filter.cleanPressure || 0); + filter.dirtyPressure = parseFloat(data.dirtyPressure || filter.dirtyPressure || 0); - filter.filterType = sfilter.filterType = filterType; - filter.body = sfilter.body = body; - filter.name = sfilter.name = name; - filter.capacity = typeof data.capacity === 'number' ? data.capacity : filter.capacity; - filter.capacityUnits = typeof data.capacityUnits !== 'undefined' ? data.capacityUnits : filter.capacity; - filter.connectionId = typeof data.connectionId !== 'undefined' ? data.connectionId : filter.connectionId; - filter.deviceBinding = typeof data.deviceBinding !== 'undefined' ? data.deviceBinding : filter.deviceBinding; - sfilter.pressureUnits = filter.pressureUnits; - sfilter.calcCleanPercentage(); - sfilter.emitEquipmentChange(); - return filter; // Always return the config when we are dealing with the config not state. - } - public async deleteFilterAsync(data: any): Promise { - try { - let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); - let filter = sys.filters.getItemById(id); - let sfilter = state.filters.getItemById(filter.id); - filter.isActive = false; - sys.filters.removeItemById(id); - state.filters.removeItemById(id); - sfilter.emitEquipmentChange(); - return filter; - } catch (err) { logger.error(`deleteFilterAsync: Error deleting filter ${err.message}`); } - } + filter.filterType = sfilter.filterType = filterType; + filter.body = sfilter.body = body; + filter.name = sfilter.name = name; + filter.capacity = typeof data.capacity === 'number' ? data.capacity : filter.capacity; + filter.capacityUnits = typeof data.capacityUnits !== 'undefined' ? data.capacityUnits : filter.capacity; + filter.connectionId = typeof data.connectionId !== 'undefined' ? data.connectionId : filter.connectionId; + filter.deviceBinding = typeof data.deviceBinding !== 'undefined' ? data.deviceBinding : filter.deviceBinding; + sfilter.pressureUnits = filter.pressureUnits; + sfilter.calcCleanPercentage(); + sfilter.emitEquipmentChange(); + return filter; // Always return the config when we are dealing with the config not state. + } + public async deleteFilterAsync(data: any): Promise { + try { + let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); + let filter = sys.filters.getItemById(id); + let sfilter = state.filters.getItemById(filter.id); + filter.isActive = false; + sys.filters.removeItemById(id); + state.filters.removeItemById(id); + sfilter.emitEquipmentChange(); + return filter; + } catch (err) { logger.error(`deleteFilterAsync: Error deleting filter ${err.message}`); } + } } diff --git a/controller/comms/messages/Messages.ts b/controller/comms/messages/Messages.ts index c5ef54a8..b83c6bcd 100755 --- a/controller/comms/messages/Messages.ts +++ b/controller/comms/messages/Messages.ts @@ -507,7 +507,7 @@ export class Inbound extends Message { ndx = bytes.length - 5; let arr = bytes.slice(0, ndx); // Remove all but the last 4 bytes. This will result in nothing anyway. - logger.verbose(`Tossed Inbound Bytes ${arr} due to an unrecoverable collision.`); + logger.verbose(`[Port ${this.portId}] Tossed Inbound Bytes ${arr} due to an unrecoverable collision.`); } this.padding = []; break; @@ -1169,12 +1169,12 @@ export class Response extends OutboundCommon { // Scenario 1. Request for pump status. // Msg In: [165,0,16, 96, 7,15], [4,0,0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0,17,31], [1,95] // Msg Out: [165,0,96, 16, 7, 0],[1,28] - if (msgIn.source !== msgOut.dest || msgIn.dest !== msgOut.source) { return false; } + if (msgIn.source !== msgOut.dest || (msgIn.dest !== msgOut.source && msgIn.dest != 16)) { return false; } if (msgIn.action === 7 && msgOut.action === 7) { return true; } return false; default: //Scenario 2, pump messages are mimics of each other but the dest/src are swapped - if (msgIn.source !== msgOut.dest || msgIn.dest !== msgOut.source) { return false; } + if (msgIn.source !== msgOut.dest || (msgIn.dest !== msgOut.source && msgIn.dest != 16)) { return false; } // sub-case // Msg In: [165,0,16, 96, 1, 2], [3,32],[1,59] // Msg Out: [165,0,96,16, 1,4],[3,39, 3,32], [1,103] diff --git a/controller/comms/messages/config/ChlorinatorMessage.ts b/controller/comms/messages/config/ChlorinatorMessage.ts index ed54c0a1..6d1b71e0 100755 --- a/controller/comms/messages/config/ChlorinatorMessage.ts +++ b/controller/comms/messages/config/ChlorinatorMessage.ts @@ -80,6 +80,7 @@ export class ChlorinatorMessage { if (isActive) { let chlor = sys.chlorinators.getItemById(1, true); let schlor = state.chlorinators.getItemById(1, true); + chlor.master = 0; chlor.isActive = schlor.isActive = isActive; if (!chlor.disabled) { // RKS: We don't want these setpoints if our chem controller disabled the @@ -87,10 +88,16 @@ export class ChlorinatorMessage { schlor.spaSetpoint = chlor.spaSetpoint = msg.extractPayloadByte(0) >> 1; schlor.poolSetpoint = chlor.poolSetpoint = msg.extractPayloadByte(1); chlor.address = msg.dest; - schlor.body = chlor.body = sys.equipment.maxBodies >= 1 || sys.equipment.shared === true ? 32 : 0; + schlor.body = chlor.body = sys.equipment.shared === true ? 32 : 0; + } + let name = msg.extractPayloadString(6, 16).trimEnd(); + if (typeof chlor.name === 'undefined') schlor.name = chlor.name = name; + if (typeof chlor.model === 'undefined') { + chlor.model = sys.board.valueMaps.chlorinatorModel.getValue(schlor.name.toLowerCase()); + if (typeof chlor.model === 'undefined') { + if (name.startsWith('iChlor')) chlor.model = sys.board.valueMaps.chlorinatorModel.getValue('ichlor-ic30'); + } } - if (typeof chlor.name === 'undefined') schlor.name = chlor.name = msg.extractPayloadString(6, 16); - if (typeof chlor.model === 'undefined') chlor.model = sys.board.valueMaps.chlorinatorModel.getValue(schlor.name.toLowerCase()); if (typeof chlor.type === 'undefined') chlor.type = schlor.type = 0; schlor.saltLevel = msg.extractPayloadByte(3) * 50 || schlor.saltLevel; schlor.status = msg.extractPayloadByte(4) & 0x007F; // Strip off the high bit. The chlorinator does not actually report this.; diff --git a/controller/comms/messages/config/ExternalMessage.ts b/controller/comms/messages/config/ExternalMessage.ts index 44a8b253..8cf19581 100755 --- a/controller/comms/messages/config/ExternalMessage.ts +++ b/controller/comms/messages/config/ExternalMessage.ts @@ -329,6 +329,7 @@ export class ExternalMessage { case 8: // Intellibrite case 10: // Colorcascade cstate.lightingTheme = circuit.lightingTheme; + if (!isOn) cstate.action = 0; break; case 9: // Dimmer cstate.level = circuit.level; @@ -354,7 +355,8 @@ export class ExternalMessage { if (schedule.isActive) { if (schedule.circuit > 0) { // Don't get the schedule state if we haven't determined the entire config for it yet. let sstate = state.schedules.getItemById(scheduleId, schedule.isActive); - sstate.isOn = ((byte & (1 << (j))) >> j) > 0; + let isOn = ((byte & (1 << (j))) >> j) > 0; + sstate.isOn = isOn; sstate.circuit = schedule.circuit; sstate.endTime = schedule.endTime; sstate.startDate = schedule.startDate; @@ -365,6 +367,7 @@ export class ExternalMessage { sstate.heatSource = schedule.heatSource; sstate.startTimeType = schedule.startTimeType; sstate.endTimeType = schedule.endTimeType; + sstate.startDate = schedule.startDate; } } else diff --git a/controller/comms/messages/status/ChlorinatorStateMessage.ts b/controller/comms/messages/status/ChlorinatorStateMessage.ts index 48294a95..91bcc516 100755 --- a/controller/comms/messages/status/ChlorinatorStateMessage.ts +++ b/controller/comms/messages/status/ChlorinatorStateMessage.ts @@ -67,9 +67,15 @@ export class ChlorinatorStateMessage { // I n t e l l i c h l o r - - 4 0 //[16, 2, 0, 3][0, 73, 110, 116, 101, 108, 108, 105, 99, 104, 108, 111, 114, 45, 45, 52, 48][188, 16, 3] // This is the model number of the chlorinator and the address is actually the second byte. - let name = msg.extractPayloadString(1, 16); + let name = msg.extractPayloadString(1, 16).trimEnd(); if (typeof chlor.name === 'undefined' || chlor.name === '') chlor.name = cstate.name = name; - if (typeof chlor.model === 'undefined' || chlor.model === 0) chlor.model = sys.board.valueMaps.chlorinatorModel.getValue(name.toLowerCase()); + if (typeof chlor.model === 'undefined' || chlor.model === 0) { + chlor.model = sys.board.valueMaps.chlorinatorModel.getValue(name.toLowerCase()); + // With iChlor it does not report the model. + if (typeof chlor.model === 'undefined') { + if (name.startsWith('iChlor')) chlor.model = sys.board.valueMaps.chlorinatorModel.getValue('ichlor-ic30'); + } + } cstate.isActive = chlor.isActive; state.emitEquipmentChanges(); break; diff --git a/controller/comms/messages/status/EquipmentStateMessage.ts b/controller/comms/messages/status/EquipmentStateMessage.ts index 13b0a5ce..2f276a91 100644 --- a/controller/comms/messages/status/EquipmentStateMessage.ts +++ b/controller/comms/messages/status/EquipmentStateMessage.ts @@ -596,9 +596,9 @@ export class EquipmentStateMessage { case 204: // IntelliCenter only. state.batteryVoltage = msg.extractPayloadByte(2) / 50; state.comms.keepAlives = msg.extractPayloadInt(4); - state.time.date = msg.extractPayloadByte(6); - state.time.month = msg.extractPayloadByte(7); state.time.year = msg.extractPayloadByte(8); + state.time.month = msg.extractPayloadByte(7); + state.time.date = msg.extractPayloadByte(6); sys.equipment.controllerFirmware = (msg.extractPayloadByte(42) + (msg.extractPayloadByte(43) / 1000)).toString(); if (sys.chlorinators.length > 0) { if (msg.extractPayloadByte(37, 255) !== 255) { @@ -641,6 +641,7 @@ export class EquipmentStateMessage { scover2.name = cover2.name; state.temps.bodies.getItemById(cover2.body + 1).isCovered = scover2.isClosed = (msg.extractPayloadByte(30) & 0x0002) > 0; } + sys.board.schedules.syncScheduleStates(); msg.isProcessed = true; state.emitEquipmentChanges(); break; diff --git a/controller/nixie/bodies/Body.ts b/controller/nixie/bodies/Body.ts index 5c294c44..cf8fe386 100644 --- a/controller/nixie/bodies/Body.ts +++ b/controller/nixie/bodies/Body.ts @@ -79,7 +79,7 @@ export class NixieBody extends NixieEquipment { try { // Here we go we need to set the valve state. if (bstate.isOn !== isOn) { - logger.info(`Nixie: Set Body ${bstate.id}-${bstate.name} to ${isOn}`); + logger.info(`Nixie: Set Body ${bstate.id}-${bstate.name} to ${isOn}`); } bstate.isOn = isOn; } catch (err) { logger.error(`Nixie Error setting body state ${bstate.id}-${bstate.name}: ${err.message}`); } diff --git a/controller/nixie/chemistry/ChemController.ts b/controller/nixie/chemistry/ChemController.ts index c636b3ad..f4fb19b7 100644 --- a/controller/nixie/chemistry/ChemController.ts +++ b/controller/nixie/chemistry/ChemController.ts @@ -1,7 +1,7 @@ import { clearTimeout, setTimeout } from 'timers'; import { conn } from '../../../controller/comms/Comms'; import { Outbound, Protocol, Response } from '../../../controller/comms/messages/Messages'; -import { IChemical, IChemController, ChemController, ChemControllerCollection, ChemFlowSensor, Chemical, ChemicalChlor, ChemicalORP, ChemicalORPProbe, ChemicalPh, ChemicalPhProbe, ChemicalProbe, ChemicalPump, ChemicalTank, sys } from "../../../controller/Equipment"; +import { IChemical, IChemController, Chlorinator, ChemController, ChemControllerCollection, ChemFlowSensor, Chemical, ChemicalChlor, ChemicalORP, ChemicalORPProbe, ChemicalPh, ChemicalPhProbe, ChemicalProbe, ChemicalPump, ChemicalTank, sys } from "../../../controller/Equipment"; import { logger } from '../../../logger/Logger'; import { InterfaceServerResponse, webApp } from "../../../web/Server"; import { Timestamp, utils } from '../../Constants'; @@ -21,6 +21,8 @@ export interface INixieChemical extends NixieEquipment { chemController: INixieChemController; chemical: IChemical; } + +//#region export class NixieChemControllerCollection extends NixieEquipmentCollection { public async manualDoseAsync(id: number, data: any) { try { @@ -208,6 +210,7 @@ export class NixieChemControllerBase extends NixieEquipment implements INixieChe else if (!isOn) this.bodyOnTime = undefined; return isOn; } + public get activeBodyId(): number { return sys.board.bodies.getActiveBody(this.chem.body); } public async setControllerAsync(data: any) { } // This is meant to be abstract override this value public processAlarms(schem: any) { } @@ -692,6 +695,7 @@ export class NixieChemController extends NixieChemControllerBase { // to indicate this to the user. schem.alarms.flow = schem.isBodyOn && !schem.flowDetected ? 1 : 0; } + schem.activeBodyId = this.activeBodyId; schem.ph.dailyVolumeDosed = schem.ph.calcDoseHistory(); schem.orp.dailyVolumeDosed = schem.orp.calcDoseHistory(); let chem = this.chem; @@ -709,7 +713,7 @@ export class NixieChemController extends NixieChemControllerBase { else schem.alarms.orpTank = 0; // Alright we need to determine whether we need to adjust the volume any so that we get at least 3 seconds out of the pump. let padj = this.chem.orp.pump.type > 0 && !this.chem.orp.useChlorinator ? (this.chem.orp.pump.ratedFlow / 60) * 3 : 0; - if (this.chem.orp.maxDailyVolume <= schem.orp.dailyVolumeDosed && !this.chem.orp.useChlorinator) { + if (this.chem.orp.dosingMethod !== 0 && this.chem.orp.maxDailyVolume <= schem.orp.dailyVolumeDosed && !this.chem.orp.useChlorinator) { schem.warnings.orpDailyLimitReached = 4; schem.orp.dailyLimitReached = true; } @@ -721,7 +725,15 @@ export class NixieChemController extends NixieChemControllerBase { if (probeType !== 0 && chem.orp.tolerance.enabled) schem.alarms.orp = schem.orp.level < chem.orp.tolerance.low ? 16 : schem.orp.level > chem.orp.tolerance.high ? 8 : 0; else schem.alarms.orp = 0; - schem.warnings.chlorinatorCommError = useChlorinator && schem.isBodyOn && state.chlorinators.getItemById(1).status & 0xF0 ? 16 : 0; + let chlorErr = 0; + if (useChlorinator && schem.isBodyOn) { + let chlors = sys.chlorinators.getByBody(schem.activeBodyId); + let chlor = chlors.getItemByIndex(0); + let schlor = state.chlorinators.getItemById(chlor.id); + this.orp.chlor.chlorId = chlor.id; + if (schlor.status & 0xF0) chlorErr = 16; + } + schem.warnings.chlorinatorCommError = chlorErr; schem.warnings.pHLockout = useChlorinator === false && probeType !== 0 && pumpType !== 0 && schem.ph.level >= chem.orp.phLockout ? 1 : 0; } else { @@ -755,7 +767,7 @@ export class NixieChemController extends NixieChemControllerBase { schem.warnings.pHDailyLimitReached = 0; // Alright we need to determine whether we need to adjust the volume any so that we get at least 3 seconds out of the pump. let padj = this.chem.ph.pump.type > 0 ? (this.chem.ph.pump.ratedFlow / 60) * 3 : 0; - if (this.chem.ph.maxDailyVolume <= schem.ph.dailyVolumeDosed + padj) { + if (this.chem.ph.dosingMethod !== 0 && this.chem.ph.maxDailyVolume <= schem.ph.dailyVolumeDosed + padj) { schem.warnings.pHDailyLimitReached = 2; schem.ph.dailyLimitReached = true; } @@ -1055,7 +1067,7 @@ class NixieChemical extends NixieChildEquipment implements INixieChemical { schem.chlor.isDosing = schem.pump.isDosing = false; if (!this.chemical.flowOnlyMixing || (schem.chemController.isBodyOn && this.chemController.flowDetected && !schem.freezeProtect)) { if (this.chemType === 'orp' && typeof this.chemController.orp.orp.useChlorinator !== 'undefined' && this.chemController.orp.orp.useChlorinator && this.chemController.orp.orp.chlorDosingMethod > 0) { - if (state.chlorinators.getItemById(1).currentOutput !== 0) { + if (state.chlorinators.getItemById(this.chemController.orp.chlor.chlorId).currentOutput !== 0) { logger.debug(`Chem mixing ORP (chlorinator) paused waiting for chlor current output to be 0%. Mix time remaining: ${utils.formatDuration(schem.mixTimeRemaining)} `); return; } @@ -1412,6 +1424,7 @@ export class NixieChemPump extends NixieChildEquipment { export class NixieChemChlor extends NixieChildEquipment { public chlor: ChemicalChlor; public isOn: boolean; + public chlorId = 0; public _lastOnStatus: number; protected _dosingTimer: NodeJS.Timeout; private _isStopping = false; @@ -1423,7 +1436,7 @@ export class NixieChemChlor extends NixieChildEquipment { if (typeof data.chlorDosingMethod !== 'undefined' && data.chlorDosingMethod === 0) { if (schlor.chemical.dosingStatus === 0) { await this.chemical.cancelDosing(schlor.chemController.orp, 'dosing method changed'); } if (schlor.chemical.dosingStatus === 1) { await this.chemical.cancelMixing(schlor.chemController.orp); } - let chlor = sys.chlorinators.getItemById(1); + let chlor = sys.chlorinators.getItemById(this.chlorId); chlor.disabled = false; chlor.isDosing = false; } @@ -1475,7 +1488,7 @@ export class NixieChemChlor extends NixieChildEquipment { let isBodyOn = schem.chemController.flowDetected; await this.chemical.initDose(schem); let chemController = schem.getParent() - let schlor = state.chlorinators.getItemById(1); + let schlor = state.chlorinators.getItemById(this.chlorId); if (!isBodyOn) { // Make sure the chlor is off. logger.info(`Chem chlor flow not detected. Body is not running.`); @@ -1552,8 +1565,8 @@ export class NixieChemChlor extends NixieChildEquipment { public async turnOff(schem: IChemicalState): Promise { try { //logger.info(`Turning off the chlorinator`); - let chlor = sys.chlorinators.getItemById(1); - let schlor = state.chlorinators.getItemById(1); + let chlor = sys.chlorinators.getItemById(this.chlorId); + let schlor = state.chlorinators.getItemById(chlor.id); if (schlor.currentOutput === 0 && schlor.targetOutput === 0 && !schlor.superChlor && chlor.disabled && !chlor.isDosing) { this.isOn = schem.chlor.isDosing = false; return schlor; @@ -1570,8 +1583,8 @@ export class NixieChemChlor extends NixieChildEquipment { } public async turnOn(schem: ChemicalState, latchTimeout?: number): Promise { try { - let chlor = sys.chlorinators.getItemById(1); - let schlor = state.chlorinators.getItemById(1); + let chlor = sys.chlorinators.getItemById(this.chlorId); + let schlor = state.chlorinators.getItemById(chlor.id); if (schlor.currentOutput === 100 && schlor.targetOutput === 100 && !schlor.superChlor && !chlor.disabled && chlor.isDosing) { this.isOn = schem.chlor.isDosing = true; return schlor; @@ -2263,8 +2276,8 @@ export class NixieChemicalORP extends NixieChemical { } - let chlor = sys.chlorinators.getItemById(1); // Still haven't seen any systems with 2+ chlors - let schlor = state.chlorinators.getItemById(1); + let chlor = sys.chlorinators.getItemById(this.chlor.chlorId); // Still haven't seen any systems with 2+ chlors + let schlor = state.chlorinators.getItemById(chlor.id); // If someone or something is superchloring the pool, let it be if (schlor.superChlor) return; // Let's have some fun trying to figure out a dynamic approach to chlor management diff --git a/controller/nixie/circuits/Circuit.ts b/controller/nixie/circuits/Circuit.ts index c18f639e..fa653714 100644 --- a/controller/nixie/circuits/Circuit.ts +++ b/controller/nixie/circuits/Circuit.ts @@ -158,20 +158,55 @@ export class NixieCircuit extends NixieEquipment { } protected async setIntelliBriteThemeAsync(cstate: CircuitState, theme: any): Promise { let arr = []; - if (cstate.isOn) arr.push({ isOn: false, timeout: 1000 }); let count = typeof theme !== 'undefined' && theme.sequence ? theme.sequence : 0; - if (cstate.isOn) arr.push({ isOn: false, timeout: 1000 }); + + // Removing this. No need to turn the light off first. We actually need it on to start the sequence for theme setting to work correctly when the light is starting from the off state. + // if (cstate.isOn) arr.push({ isOn: false, timeout: 1000 }); + + // Start the sequence of off/on after the light is on. + arr.push({ isOn: true, timeout: 100 }); for (let i = 0; i < count; i++) { - if (i < count - 1) { - arr.push({ isOn: true, timeout: 100 }); - arr.push({ isOn: false, timeout: 100 }); - } - else arr.push({ isOn: true, timeout: 1000 }); + arr.push({ isOn: false, timeout: 100 }); + arr.push({ isOn: true, timeout: 100 }); + } + // Ensure light stays on long enough for the theme to stick (required for light group theme setting to function correctly). + // 2s was too short. + arr.push({ isOn: true, timeout: 3000 }); + + logger.debug(arr); + let res = await NixieEquipment.putDeviceService(this.circuit.connectionId, `/state/device/${this.circuit.deviceBinding}`, arr, 60000); + // Even though we ended with on we need to make sure that the relay stays on now that we are done. + if (!res.error) { + this._sequencing = false; + await this.setCircuitStateAsync(cstate, true, false); + } + return res; + } + protected async setWaterColorsThemeAsync(cstate: CircuitState, theme: any): Promise { + let ptheme = sys.board.valueMaps.lightThemes.findItem(cstate.lightingTheme) || { val: 0, sequence: 0 }; + // First check to see if we are on. If we are not then we need to emit our status as if we are initializing and busy. + let arr = []; + if (ptheme.val === 0) { + // We don't know our previous theme so we are going to sync the lights to get a starting point. + arr.push({ isOn: true, timeout: 1000 }); // Turn on for 1 second + arr.push({ isOn: false, timeout: 5000 }); // Turn off for 5 seconds + arr.push({ isOn: true, timeout: 1000 }); + ptheme = sys.board.valueMaps.lightThemes.findItem('eveningsea'); + } + let count = theme.sequence - ptheme.sequence; + if (count < 0) count = count + 16; + for (let i = 0; i < count; i++) { + arr.push({ isOn: true, timeout: 200 }); + arr.push({ isOn: false, timeout: 200 }); } console.log(arr); + if (arr.length === 0) return new InterfaceServerResponse(200, 'Success'); let res = await NixieEquipment.putDeviceService(this.circuit.connectionId, `/state/device/${this.circuit.deviceBinding}`, arr, 60000); // Even though we ended with on we need to make sure that the relay stays on now that we are done. if (!res.error) { + cstate.lightingTheme = ptheme.val; + cstate.isOn = true; // At this point the relay will be off but we want the process + // to assume that the relay state is not actually changing. this._sequencing = false; await this.setCircuitStateAsync(cstate, true, false); } @@ -237,6 +272,9 @@ export class NixieCircuit extends NixieEquipment { case 'colorlogic': res = await this.setColorLogicThemeAsync(cstate, theme); break; + case 'watercolors': + res = await this.setWaterColorsThemeAsync(cstate, theme); + break; } cstate.action = 0; // Make sure clients know that we are done. diff --git a/controller/nixie/heaters/Heater.ts b/controller/nixie/heaters/Heater.ts index 28a68601..3ea9666c 100644 --- a/controller/nixie/heaters/Heater.ts +++ b/controller/nixie/heaters/Heater.ts @@ -287,11 +287,15 @@ export class NixieSolarHeater extends NixieHeaterBase { if (typeof this._lastState === 'undefined' || target || this._lastState !== target) { if (utils.isNullOrEmpty(this.heater.connectionId) || utils.isNullOrEmpty(this.heater.deviceBinding)) { this._lastState = hstate.isOn = target; + hstate.isCooling = target && isCooling; } else { let res = await NixieEquipment.putDeviceService(this.heater.connectionId, `/state/device/${this.heater.deviceBinding}`, { isOn: target, latch: target ? 10000 : undefined }); - if (res.status.code === 200) this._lastState = hstate.isOn = target; + if (res.status.code === 200) { + this._lastState = hstate.isOn = target; + hstate.isCooling = target && isCooling; + } else logger.error(`Nixie Error setting heater state: ${res.status.code} -${res.status.message} ${res.error.message}`); } if (target) { @@ -578,6 +582,7 @@ export class NixieMastertemp extends NixieGasHeater { // Set the polling interval to 3 seconds. this.pollEquipmentAsync(); this.pollingInterval = 3000; + } /* public getCooldownTime(): number { // Delays are always in terms of seconds so convert the minute to seconds. @@ -589,12 +594,26 @@ export class NixieMastertemp extends NixieGasHeater { public async setHeaterStateAsync(hstate: HeaterState, isOn: boolean) { try { // Initialize the desired state. + this.isOn = isOn; this.isCooling = false; - // Here we go we need to set the firemans switch state. - if (hstate.isOn !== isOn) { - logger.info(`Nixie: Set Heater ${hstate.id}-${hstate.name} to ${isOn}`); + let target = hstate.startupDelay === false && isOn; + if (target && typeof hstate.endTime !== 'undefined') { + // Calculate a short cycle time so that the gas heater does not cycle + // too often. For gas heaters this is 60 seconds. This gives enough time + // for the heater control circuit to make a full cycle. + if (new Date().getTime() - hstate.endTime.getTime() < this.heater.minCycleTime * 60000) { + logger.verbose(`${hstate.name} short cycle detected deferring turn on state`); + target = false; + } + } + // Here we go we need to set the state. + if (hstate.isOn !== target) { + logger.info(`Nixie: Set Mastertemp ${hstate.id}-${hstate.name} to ${isOn}`); + } + if (typeof this._lastState === 'undefined' || target || this._lastState !== target) { + this._lastState = hstate.isOn = target; + if (target) this.lastHeatCycle = new Date(); } - if (isOn && !hstate.startupDelay) this.lastHeatCycle = new Date(); hstate.isOn = isOn; } catch (err) { return logger.error(`Nixie Error setting heater state ${hstate.id}-${hstate.name}: ${err.message}`); } } diff --git a/controller/nixie/pumps/Pump.ts b/controller/nixie/pumps/Pump.ts index 04cd3ea0..79dd2c1f 100644 --- a/controller/nixie/pumps/Pump.ts +++ b/controller/nixie/pumps/Pump.ts @@ -791,8 +791,7 @@ export class NixiePumpVSF extends NixiePumpRS485 { let _newSpeed = 0; let maxRPM = 0; let maxGPM = 0; - let flows = 0; - let speeds = 0; + let useFlow = false; if (!pState.pumpOnDelay) { let pumpCircuits = this.pump.circuits.get(); let pt = sys.board.valueMaps.pumpTypes.get(this.pump.type); @@ -800,13 +799,19 @@ export class NixiePumpVSF extends NixiePumpRS485 { // if there is a mix in the circuit array then they will not work. In IntelliCenter if there is an RPM setting in the mix it will use RPM by converting // the GPM to RPM but if there is none then it will use GPM. let toRPM = (flowRate: number, minSpeed: number = 450, maxSpeed: number = 3450) => { + // eff = 114.4365 + // gpm = 80 + // speed = 2412 let eff = .03317 * maxSpeed; - let rpm = Math.min((flowRate * maxSpeed) / eff, maxSpeed); + let rpm = Math.min(Math.round((flowRate * maxSpeed) / eff), maxSpeed); return rpm > 0 ? Math.max(rpm, minSpeed) : 0; }; let toGPM = (speed: number, maxSpeed: number = 3450, minFlow: number = 15, maxFlow: number = 140) => { + // eff = 114.4365 + // speed = 1100 + // gpm = (114.4365 * 1100)/3450 = 36 let eff = .03317 * maxSpeed; - let gpm = Math.min((eff * speed) / maxSpeed, maxFlow); + let gpm = Math.min(Math.round((eff * speed) / maxSpeed), maxFlow); return gpm > 0 ? Math.max(gpm, minFlow) : 0; } for (let i = 0; i < pumpCircuits.length; i++) { @@ -814,23 +819,24 @@ export class NixiePumpVSF extends NixiePumpRS485 { let pc = pumpCircuits[i]; if (circ.isOn) { if (pc.units > 0) { + let rpm = toRPM(pc.flow, pt.minSpeed, pt.MaxSpeed); + if (rpm > maxRPM) useFlow = true; maxGPM = Math.max(maxGPM, pc.flow); - // Calculate an RPM from this flow. - maxRPM = Math.max(maxGPM, toRPM(pc.flow, pt.minSpeed, pt.maxSpeed)); - flows++; + rpm = Math.max(maxRPM, rpm); } else { + let gpm = toGPM(pc.speed, pt.maxSpeed, pt.minFlow, pt.maxFlow); + if (gpm > maxGPM) useFlow = false; maxRPM = Math.max(maxRPM, pc.speed); - maxGPM = Math.max(maxGPM, toGPM(pc.speed, pt.maxSpeed, pt.minFlow, pt.maxFlow)); - speeds++; + maxGPM = Math.max(maxGPM, gpm); } } } - _newSpeed = speeds > 0 || flows === 0 ? maxRPM : maxGPM; + _newSpeed = useFlow ? maxGPM : maxRPM; } if (isNaN(_newSpeed)) _newSpeed = 0; // Send the flow message if it is flow and the rpm message if it is rpm. - if (this._targetSpeed !== _newSpeed) logger.info(`NCP: Setting Pump ${this.pump.name} to ${_newSpeed} ${flows > 0 ? 'GPM' : 'RPM'}.`); + if (this._targetSpeed !== _newSpeed) logger.info(`NCP: Setting Pump ${this.pump.name} to ${_newSpeed} ${useFlow ? 'GPM' : 'RPM'}.`); this._targetSpeed = _newSpeed; } protected async setPumpRPMAsync() { diff --git a/controller/nixie/schedules/Schedule.ts b/controller/nixie/schedules/Schedule.ts index 88029923..664632c9 100644 --- a/controller/nixie/schedules/Schedule.ts +++ b/controller/nixie/schedules/Schedule.ts @@ -4,11 +4,12 @@ import { logger } from '../../../logger/Logger'; import { NixieEquipment, NixieChildEquipment, NixieEquipmentCollection, INixieControlPanel } from "../NixieEquipment"; import { CircuitGroup, CircuitGroupCircuit, ICircuitGroup, ICircuitGroupCircuit, LightGroup, LightGroupCircuit, Schedule, ScheduleCollection, sys } from "../../../controller/Equipment"; -import { CircuitGroupState, ICircuitGroupState, ScheduleState, state, } from "../../State"; +import { ICircuitState, CircuitGroupState, ICircuitGroupState, ScheduleState, ScheduleTime, state, } from "../../State"; import { setTimeout, clearTimeout } from 'timers'; import { NixieControlPanel } from '../Nixie'; import { webApp, InterfaceServerResponse } from "../../../web/Server"; import { delayMgr } from '../../../controller/Lockouts'; +import { time } from 'console'; export class NixieScheduleCollection extends NixieEquipmentCollection { @@ -23,9 +24,8 @@ export class NixieScheduleCollection extends NixieEquipmentCollection elem.circuitId === sscheds[i].circuit); + let sched = sys.schedules.getItemById(sscheds[i].id) + if (typeof circ === 'undefined') circuits.push({ + circuitId: sscheds[i].circuit, + cstate: state.circuits.getInterfaceById(sscheds[i].circuit), hasNixie: sched.master !== 0, sscheds: [sscheds[i]] + }); + else { + if (sched.master !== 0) circ.hasNixie = true; + circ.sscheds.push(sscheds[i]); + } } - // Alright now that we are done with that we need to set all the circuit states that need changing. - for (let i = 0; i < ctx.circuits.length; i++) { - let circuit = ctx.circuits[i]; - await sys.board.circuits.setCircuitStateAsync(circuit.id, circuit.isOn); + // Sort this so that body circuits are evaluated first. This is required when there are schedules for things like cleaner + // or delay circuits. If we do not do this then a schedule that requires the pool to be on for instance will never + // get triggered. + circuits.sort((x, y) => y.circuitId === 6 || y.circuitId === 1 ? 1 : y.circuitId - x.circuitId); + + + /* + RSG 5-8-22 + Manual OP needs to play a role here.From the IC manual: + # Manual OP General + + From the manual: + Manual OP Priority: ON: This feature allows for a circuit to be manually switched OFF and switched ON within a scheduled program, + the circuit will continue to run for a maximum of 12 hours or whatever that circuit Egg Timer is set to, after which the scheduled + program will resume. This feature will turn off any scheduled program to allow manual pump override.The Default setting is OFF. + + ## When on + 1. If a schedule should be on and the user turns the schedule off then the schedule expires until such time as the time off has + expired.When that occurs the schedule should be reset to run at the designated time.If the user resets the schedule by turning the + circuit back on again ~~then the schedule will resume and turn off at the specified time~~then the schedule will be ignored and + the circuit will run until the egg timer expires or the circuit / feature is manually turned off.This setting WILL affect + other schedules that may impact this circuit. + + ## When off + 1. "Normal" = If a schedule should be on and the user turns the schedule off then the schedule expires until such time as the time + off has expired.When that occurs the schedule should be reset to run at the designated time.If the user resets the schedule by + turning the circuit back on again then the schedule will resume and turn off at the specified time. + */ + + let mOP = sys.general.options.manualPriority; + + // Now lets evaluate the schedules by virtue of their state related to the circuits. + for (let i = 0; i < circuits.length; i++) { + let c = circuits[i]; + if (!c.hasNixie) continue; // If this has nothing to do with Nixie move on. + let shouldBeOn = typeof c.sscheds.find(elem => elem.scheduleTime.shouldBeOn === true) !== 'undefined'; + // 1. If the feature is currently running and the schedule is not on then it will set the priority for the circuit to [scheduled]. + // 2. If the feature is currently running but there are overlapping schedules then this will catch any schedules that need to be turned off. + if (c.cstate.isOn && shouldBeOn) { + c.cstate.priority = shouldBeOn ? 'scheduled' : 'manual'; + for (let j = 0; j < c.sscheds.length; j++) { + let ssched = c.sscheds[j]; + ssched.triggered = ssched.scheduleTime.shouldBeOn; + if (mOP && ssched.manualPriorityActive) { + ssched.isOn = false; + // Not sure what setting a delay for this does but ok. + if (!c.cstate.manualPriorityActive) delayMgr.setManualPriorityDelay(c.cstate); + } + else ssched.isOn = ssched.scheduleTime.shouldBeOn && !ssched.manualPriorityActive; + } + } + // 3. If the schedule should be on and it isn't and the schedule has not been triggered then we need to + // turn the schedule and circuit on. + else if (!c.cstate.isOn && shouldBeOn) { + // The circuit is not on but it should be. Check to ensure all schedules have been triggered. + let untriggered = false; + // If this schedule has been triggered then mOP comes into play if manualPriority has been set in the config. + for (let j = 0; j < c.sscheds.length; j++) { + let ssched = c.sscheds[j]; + // If this schedule is turned back on then the egg timer will come into play. This is all that is required + // for the mOP function. The setEndDate for the circuit makes the determination as to when off will occur. + if (mOP && ssched.scheduleTime.shouldBeOn && ssched.triggered) { + ssched.manualPriorityActive = true; + } + // The reason we check to see if anything has not been triggered is so we do not have to perform the circuit changes + // if the schedule has already been triggered. + else if (!ssched.triggered) untriggered = true; + } + let heatSource = { heatMode: 'nochange', heatSetpoint: undefined, coolSetpoint: undefined }; + // Check to see if any of the schedules have not been triggered. If they haven't then trigger them and turn the circuit on. + if (untriggered) { + // Get the heat modes and temps for all the schedules that have not been triggered. + let body = sys.bodies.find(elem => elem.circuit === c.circuitId); + if (typeof body !== 'undefined') { + // If this is a body circuit then we need to set the heat mode and the temperature but only do this once. If + // the user changes it later then that is on them. + for (let j = 0; j < c.sscheds.length; j++) { + if (sscheds[j].triggered) continue; + let ssched = sscheds[j]; + let hs = sys.board.valueMaps.heatSources.transform(c.sscheds[i].heatSource); + switch (hs.name) { + case 'nochange': + case 'dontchange': + break; + case 'off': + // If the heatsource setting is off only change it if it is currently don't change. + if (heatSource.heatMode === 'nochange') heatSource.heatMode = hs.name; + break; + default: + switch (heatSource.heatMode) { + case 'off': + case 'nochange': + case 'dontchange': + heatSource.heatMode = hs.name; + heatSource.heatSetpoint = ssched.heatSetpoint; + heatSource.coolSetpoint = hs.hasCoolSetpoint ? ssched.coolSetpoint : undefined; + break; + } + break; + } + // Ok if we need to change the setpoint or the heatmode then lets do it. + if (heatSource.heatMode !== 'nochange') { + await sys.board.bodies.setHeatModeAsync(body, sys.board.valueMaps.heatSources.getValue(heatSource.heatMode)); + if (typeof heatSource.heatSetpoint !== 'undefined') await sys.board.bodies.setHeatSetpointAsync(body, heatSource.heatSetpoint); + if (typeof heatSource.coolSetpoint !== 'undefined') await sys.board.bodies.setCoolSetpointAsync(body, heatSource.coolSetpoint); + } + } + } + // By now we have everything we need to turn on the circuit. + for (let j = 0; j < c.sscheds.length; j++) { + let ssched = c.sscheds[j]; + if (!ssched.triggered && ssched.scheduleTime.shouldBeOn) { + if (!c.cstate.isOn) { + await sys.board.circuits.setCircuitStateAsync(c.circuitId, true); + } + let ssched = c.sscheds[j]; + c.cstate.priority = 'scheduled'; + ssched.triggered = ssched.isOn = ssched.scheduleTime.shouldBeOn; + ssched.manualPriorityActive = false; + } + } + } + } + else if (c.cstate.isOn && !shouldBeOn) { + // Time to turn off the schedule. + for (let j = 0; j < c.sscheds.length; j++) { + let ssched = c.sscheds[j]; + // Only turn off the schedule if it is not actively mOP. + if (c.cstate.isOn && !ssched.manualPriorityActive) await sys.board.circuits.setCircuitStateAsync(c.circuitId, false); + c.cstate.priority = 'manual'; + // The schedule has expired we need to clear all the info for it. + ssched.manualPriorityActive = ssched.triggered = ssched.isOn = c.sscheds[j].scheduleTime.shouldBeOn; + } + } + else if (!c.cstate.isOn && !shouldBeOn) { + // Everything is off so lets clear it all. + for (let j = 0; j < c.sscheds.length; j++) { + let ssched = c.sscheds[j]; + ssched.isOn = ssched.manualPriorityActive = ssched.triggered = false; + } + } + state.emitEquipmentChanges(); } - } catch (err) { logger.error(`Error triggering schedules: ${err}`); } + } catch (err) { logger.error(`Error triggering nixie schedules: ${err.message}`); } } } export class NixieSchedule extends NixieEquipment { @@ -95,24 +239,26 @@ export class NixieSchedule extends NixieEquipment { public async setScheduleAsync(data: any) { try { let schedule = this.schedule; + let sschedule = state.schedules.getItemById(schedule.id); + sschedule.scheduleTime.calculated = false; } catch (err) { logger.error(`Nixie setScheduleAsync: ${err.message}`); return Promise.reject(err); } } - public async pollEquipmentAsync() { - let self = this; - try { - if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer); - this._pollTimer = null; - let success = false; - } - catch (err) { logger.error(`Nixie Error polling Schedule - ${err}`); } - finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); } - } + public async pollEquipmentAsync() {} public async validateSetupAsync(Schedule: Schedule, temp: ScheduleState) { try { // The validation will be different if the Schedule is on or not. So lets get that information. } catch (err) { logger.error(`Nixie Error checking Schedule Hardware ${this.schedule.id}: ${err.message}`); return Promise.reject(err); } } + public async closeAsync() { + try { + if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer); + this._pollTimer = null; + } + catch (err) { logger.error(`Nixie Schedule closeAsync: ${err.message}`); return Promise.reject(err); } + } + public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); } + /* public async triggerScheduleAsync(ctx: NixieScheduleContext) { try { if (this.schedule.isActive === false) return; @@ -122,22 +268,20 @@ export class NixieSchedule extends NixieEquipment { // Schedules can be overridden so it is important that when the // state is changed for the schedule if it is currently active that // Nixie does not override the state of the scheduled circuit or feature. - /* - RSG 5-8-22 - Manual OP needs to play a role here. From the IC manual: - # Manual OP General + //RSG 5-8-22 + //Manual OP needs to play a role here. From the IC manual: + //# Manual OP General - From the manual: - Manual OP Priority: ON: This feature allows for a circuit to be manually switched OFF and switched ON within a scheduled program, the circuit will continue to run for a maximum of 12 hours or whatever that circuit Egg Timer is set to, after which the scheduled program will resume. This feature will turn off any scheduled program to allow manual pump override. The Default setting is OFF. + //From the manual: + //Manual OP Priority: ON: This feature allows for a circuit to be manually switched OFF and switched ON within a scheduled program, the circuit will continue to run for a maximum of 12 hours or whatever that circuit Egg Timer is set to, after which the scheduled program will resume. This feature will turn off any scheduled program to allow manual pump override. The Default setting is OFF. - ## When on - 1. If a schedule should be on and the user turns the schedule off then the schedule expires until such time as the time off has expired. When that occurs the schedule should be reset to run at the designated time. If the user resets the schedule by turning the circuit back on again ~~then the schedule will resume and turn off at the specified time~~ then the schedule will be ignored and the circuit will run until the egg timer expires or the circuit/feature is manually turned off. This setting WILL affect other schedules that may impact this circuit. + //## When on + //1. If a schedule should be on and the user turns the schedule off then the schedule expires until such time as the time off has expired. When that occurs the schedule should be reset to run at the designated time. If the user resets the schedule by turning the circuit back on again ~~then the schedule will resume and turn off at the specified time~~ then the schedule will be ignored and the circuit will run until the egg timer expires or the circuit/feature is manually turned off. This setting WILL affect other schedules that may impact this circuit. - ## When off - 1. "Normal" = If a schedule should be on and the user turns the schedule off then the schedule expires until such time as the time off has expired. When that occurs the schedule should be reset to run at the designated time. If the user resets the schedule by turning the circuit back on again then the schedule will resume and turn off at the specified time. + //## When off + //1. "Normal" = If a schedule should be on and the user turns the schedule off then the schedule expires until such time as the time off has expired. When that occurs the schedule should be reset to run at the designated time. If the user resets the schedule by turning the circuit back on again then the schedule will resume and turn off at the specified time. - Interestingly, there also seems to be a schedule level setting for this. We will ignore that for now as the logic could get much more complicated. - */ + //Interestingly, there also seems to be a schedule level setting for this. We will ignore that for now as the logic could get much more complicated. // 1. If the feature happens to be running and the schedule is not yet turned on then // it should not override what the user says. // 2. If a schedule is running and the state of the circuit changes to off then the new state should suspend the schedule @@ -167,8 +311,7 @@ export class NixieSchedule extends NixieEquipment { ssched.isOn = false; return; } - let shouldBeOn = this.shouldBeOn(); // This should also set the validity for the schedule if there are errors. - + let shouldBeOn = ssched.shouldBeOn(); // This should also set the validity for the schedule if there are errors. let manualPriorityActive: boolean = shouldBeOn ? sys.board.schedules.manualPriorityActive(ssched) : false; //console.log(`Processing schedule ${this.schedule.id} - ${circuit.name} : ShouldBeOn: ${shouldBeOn} ManualPriorityActive: ${manualPriorityActive} Running: ${this.running} Suspended: ${this.suspended} Resumed: ${this.resumed}`); @@ -212,6 +355,11 @@ export class NixieSchedule extends NixieEquipment { this.running = true; } else if (shouldBeOn && this.running) { + // Check to see if circuit is on, if not turn it on. + // RKS: 07-09-23 - This was in PR#819 buut this needs further review since the circuit states are not to be set here. This would + // trash delays and manualPriority. + // if(!cstate.isOn) ctx.setCircuit(circuit.id, true); + // With mOP, we need to see if the schedule will come back into play and also set the circut if (this.suspended && cstate.isOn) { if (sys.general.options.manualPriority) { @@ -221,13 +369,13 @@ export class NixieSchedule extends NixieEquipment { this.resumed = true; } this.suspended = !cstate.isOn; - if (manualPriorityActive){ + if (manualPriorityActive) { ssched.isOn = false; ssched.manualPriorityActive = true; } else { ssched.isOn = cstate.isOn; - ssched.manualPriorityActive = false; + ssched.manualPriorityActive = false; } } // Our schedule has expired it is time to turn it off, but only if !manualPriorityActive. @@ -248,86 +396,6 @@ export class NixieSchedule extends NixieEquipment { } ssched.emitEquipmentChange(); } catch (err) { logger.error(`Error processing schedule: ${err.message}`); } - } - protected calcTime(dt: Timestamp, type: number, offset: number): Timestamp { - let tt = sys.board.valueMaps.scheduleTimeTypes.transform(type); - switch (tt.name) { - case 'sunrise': - return new Timestamp(state.heliotrope.sunrise); - case 'sunset': - return new Timestamp(state.heliotrope.sunset); - default: - return dt.startOfDay().addMinutes(offset); - } - } - protected shouldBeOn(): boolean { - if (this.schedule.isActive === false) return false; - if (this.schedule.disabled) return false; - // Be careful with toDate since this returns a mutable date object from the state timestamp. startOfDay makes it immutable. - let sod = state.time.startOfDay() - let dow = sod.toDate().getDay(); - let type = sys.board.valueMaps.scheduleTypes.transform(this.schedule.scheduleType); - if (type.name === 'runonce') { - // If we are not matching up with the day then we shouldn't be running. - if (sod.fullYear !== this.schedule.startYear || sod.month + 1 !== this.schedule.startMonth || sod.date !== this.schedule.startDay) return false; - } - else { - // Convert the dow to the bit value. - let sd = sys.board.valueMaps.scheduleDays.toArray().find(elem => elem.dow === dow); - let dayVal = sd.bitVal || sd.val; // The bitval allows mask overrides. - // First check to see if today is one of our days. - if ((this.schedule.scheduleDays & dayVal) === 0) return false; - } - // Next normalize our start and end times. Fortunately, the start and end times are normalized here so that - // [0, {name: 'manual', desc: 'Manual }] - // [1, { name: 'sunrise', desc: 'Sunrise' }], - // [2, { name: 'sunset', desc: 'Sunset' }] - let tmStart = this.calcTime(sod, this.schedule.startTimeType, this.schedule.startTime).getTime(); - let tmEnd = this.calcTime(sod, this.schedule.endTimeType, this.schedule.endTime).getTime(); - - if (isNaN(tmStart)) return false; - if (isNaN(tmEnd)) return false; - // If we are past our window we should be off. - let tm = state.time.getTime(); - if (tm >= tmEnd) return false; - if (tm <= tmStart) return false; - - // Let's now check to see - - // If we make it here we should be on. - return true; - } - - - public async closeAsync() { - try { - if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer); - this._pollTimer = null; - } - catch (err) { logger.error(`Nixie Schedule closeAsync: ${err.message}`); return Promise.reject(err); } - } - public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); } + */ } -class NixieScheduleContext { - constructor() { - - } - public circuits: { id: number, isOn: boolean }[] = []; - public heatModes: { id: number, heatMode: number, heatSetpoint?: number, coolSetpoint?: number }[] = []; - public setCircuit(id: number, isOn: boolean) { - let c = this.circuits.find(elem => elem.id === id); - if (typeof c === 'undefined') this.circuits.push({ id: id, isOn: isOn }); - else c.isOn = isOn; - } - public setHeatMode(id: number, heatMode: string, heatSetpoint?: number, coolSetpoint?: number) { - let mode = sys.board.valueMaps.heatModes.transformByName(heatMode); - let hm = this.heatModes.find(elem => elem.id == id); - if (typeof hm === 'undefined') this.heatModes.push({ id: id, heatMode: mode.val, heatSetpoint: heatSetpoint, coolSetpoint: coolSetpoint }); - else { - hm.heatMode = mode.val; - if (typeof heatSetpoint !== 'undefined') hm.heatSetpoint = heatSetpoint; - if (typeof coolSetpoint !== 'undefined') hm.coolSetpoint = coolSetpoint; - } - } -} \ No newline at end of file diff --git a/defaultConfig.json b/defaultConfig.json index 46e7e603..659ab468 100755 --- a/defaultConfig.json +++ b/defaultConfig.json @@ -164,7 +164,7 @@ "username": "", "password": "", "selfSignedCertificate": false, - "rootTopic": "@bind=(state.equipment.model).replace(' ','-').replace('/','').toLowerCase();", + "rootTopic": "@bind=(state.equipment.model).replace(/ /g,'-').replace('/','').toLowerCase();", "retain": true, "qos": 0, "changesOnly": true @@ -204,7 +204,7 @@ "port": 1883, "username": "", "password": "", - "rootTopic": "@bind=(state.equipment.model).replace(' ','-').replace('/','').toLowerCase();Alt", + "rootTopic": "@bind=(state.equipment.model).replace(/ /,'-').replace('/','').toLowerCase();Alt", "retain": true, "qos": 0, "changesOnly": true @@ -230,7 +230,7 @@ "vars": { "_note": "hassTopic is the topic that HASS reads for configuration and should not be changed. mqttTopic should match the topic in the MQTT binding (do not use MQTTAlt for HASS).", "hassTopic": "homeassistant", - "mqttTopic": "@bind=(state.equipment.model).replace(' ','-').replace('/','').toLowerCase();" + "mqttTopic": "@bind=(state.equipment.model).replace(/ /g,'-').replace('/','').toLowerCase();" } }, "rem": { diff --git a/package-lock.json b/package-lock.json index 6c45d221..2aed123d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nodejs-poolcontroller", - "version": "8.0.1", + "version": "8.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nodejs-poolcontroller", - "version": "8.0.1", + "version": "8.0.2", "license": "GNU Affero General Public License v3.0", "dependencies": { "@influxdata/influxdb-client": "^1.32.0", diff --git a/package.json b/package.json index 161889c9..110fcab0 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nodejs-poolcontroller", - "version": "8.0.1", + "version": "8.0.2", "description": "nodejs-poolController", "main": "app.js", "author": { diff --git a/sendSocket.js b/sendSocket.js new file mode 100644 index 00000000..7ea7f031 --- /dev/null +++ b/sendSocket.js @@ -0,0 +1,32 @@ +// Import Socket.IO client +const io = require('socket.io-client'); + +// Connect to the server +const socket = io('http://localhost:4200'); + +// Event handler for successful connection +socket.on('connect', () => { + console.log('Connected to server.'); + + // Emit data to the server + socket.emit('message', 'Hello, server!'); + socket.emit('echo', `testing 123`); + socket.on('echo', (string)=>{ + console.log(string); + }) + //const hexData = "02 10 01 01 14 00 03 10 02 10 01 01 14 00 03 10 02 10 01 01 14 00 03 10 02 10 02 01 80 20 00 00 00 00 00 00 b5 00 03 10 02 10 03 01 20 20 6f 50 6c 6f 54 20 6d 65 20 70 37 20 5f 34 20 46 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 07 00 10 d6 10 03 01 02 00 01 10 14 10 03 01 02 00 01 10 14 10 03 01 02 00 01 10 14"; + const hexData = "10 02 01 80 20 00 00 00 00 00 00 b5 00 03 10 02 10 03"; + const formattedHexData = hexData.split(' ').map(hex => parseInt(hex, 16)); + //socket.emit('rawbytes', Buffer.from([1, 2, 3])); + socket.emit('rawbytes', formattedHexData); +}); + +// Event handler for receiving data from the server +socket.on('message', (data) => { + console.log('Received from server:', data); +}); + +// Event handler for disconnection +socket.on('disconnect', () => { + console.log('Disconnected from server.'); +}); diff --git a/web/Server.ts b/web/Server.ts index 612e6a1b..d2998c6a 100755 --- a/web/Server.ts +++ b/web/Server.ts @@ -681,6 +681,10 @@ export class HttpServer extends ProtoServer { logger.error(`Error replaying packet: ${err.message}`); } }); + sock.on('rawbytes', (data:any)=>{ + let port = conn.findPortById(0); + port.pushIn(Buffer.from(data)); + }) sock.on('sendLogMessages', function (sendMessages: boolean) { console.log(`sendLogMessages set to ${sendMessages}`); if (!sendMessages) sock.leave('msgLogger'); diff --git a/web/bindings/homeassistant.json b/web/bindings/homeassistant.json index 19a1aba1..50de3d3f 100644 --- a/web/bindings/homeassistant.json +++ b/web/bindings/homeassistant.json @@ -34,7 +34,7 @@ } ], "rootTopic-DIRECTIONS": "rootTopic in config.json is ingored. Instead set the two topic variables in the vars section", - "_rootTopic": "@bind=(state.equipment.model).replace(' ','-').replace(' / ','').toLowerCase();", + "_rootTopic": "@bind=(state.equipment.model).replace(/ /g,'-').replace(' / ','').toLowerCase();", "clientId": "@bind=`hass_njsPC_${webApp.mac().replace(/:/g, '_'}-${webApp.httpPort()}`;" } }, @@ -434,4 +434,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/web/services/state/State.ts b/web/services/state/State.ts index 4828aa2b..4e57a5ef 100755 --- a/web/services/state/State.ts +++ b/web/services/state/State.ts @@ -415,9 +415,16 @@ export class StateRoute { let val; if (isNaN(mode)) mode = parseInt(req.body.heatMode, 10); if (!isNaN(mode)) val = sys.board.valueMaps.heatModes.transform(mode); - else val = sys.board.valueMaps.heatModes.transformByName(req.body.mode || req.body.heatMode); - if (typeof val.val === 'undefined') { - return next(new ServiceParameterError(`Invalid value for heatMode: ${req.body.mode}`, 'body', 'heatMode', mode)); + else { + let smode = req.body.mode || req.body.heatMode; + if (typeof smode === 'string') smode = smode.toLowerCase(); + else { + return next(new ServiceParameterError(`Invalid mode supplied ${req.body.mode || req.body.heatMode}.`, 'body', 'heatmode', smode)); + } + val = sys.board.valueMaps.heatModes.transformByName(smode); + if (typeof val.val === 'undefined') { + return next(new ServiceParameterError(`Invalid value for heatMode: ${req.body.mode}`, 'body', 'heatMode', mode)); + } } mode = val.val; let body = sys.bodies.findByObject(req.body); diff --git a/web/services/state/StateSocket.ts b/web/services/state/StateSocket.ts index 902488c7..1e6b4c9a 100755 --- a/web/services/state/StateSocket.ts +++ b/web/services/state/StateSocket.ts @@ -193,7 +193,9 @@ export class StateSocket { try { data = JSON.parse(data); let id = parseInt(data.id, 10); - if (!isNaN(id) && (typeof data.isOn !== 'undefined' || typeof data.state !== 'undefined')) { + if (!isNaN(id) && typeof data.toggle !== 'undefined') + if (utils.makeBool(data.toggle)) await sys.board.circuits.toggleCircuitStateAsync(id); + else if (!isNaN(id) && (typeof data.isOn !== 'undefined' || typeof data.state !== 'undefined')) { await sys.board.circuits.setCircuitStateAsync(id, utils.makeBool(data.isOn || typeof data.state)); } } @@ -203,6 +205,8 @@ export class StateSocket { try { data = JSON.parse(data); let id = parseInt(data.id, 10); + if (!isNaN(id) && typeof data.toggle !== 'undefined') + if (utils.makeBool(data.toggle)) await sys.board.features.toggleFeatureStateAsync(id); if (!isNaN(id) && (typeof data.isOn !== 'undefined' || typeof data.state !== 'undefined')) { await sys.board.features.setFeatureStateAsync(id, utils.makeBool(data.isOn || typeof data.state)); } diff --git a/web/services/utilities/Utilities.ts b/web/services/utilities/Utilities.ts index 2229ad27..cc34125f 100644 --- a/web/services/utilities/Utilities.ts +++ b/web/services/utilities/Utilities.ts @@ -103,7 +103,7 @@ export class UtilitiesRoute { options: { protocol: 'mqtt://', host: '', port: 1883, username: '', password: '', selfSignedCertificate: false, - rootTopic: "pool/@bind=(state.equipment.model).replace(' ','-').replace(' / ','').toLowerCase();", + rootTopic: "pool/@bind=(state.equipment.model).replace(/ /g,'-').replace(' / ','').toLowerCase();", retain: true, qos: 0, changesOnly: true } } @@ -230,4 +230,4 @@ export class UtilitiesRoute { }); } -} \ No newline at end of file +}