├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── 1-bug-report.yml
│ ├── 2-docs.md
│ ├── 3-proposal.md
│ └── config.yml
└── workflows
│ └── docker-publish-njsPC-linux.yml
├── .gitignore
├── .npmrc
├── CONTRIBUTING.md
├── Changelog
├── Dockerfile
├── Gruntfile.js
├── LICENSE
├── README.md
├── anslq25
├── MessagesMock.ts
├── boards
│ ├── MockBoardFactory.ts
│ ├── MockEasyTouchBoard.ts
│ └── MockSystemBoard.ts
├── chemistry
│ └── MockChlorinator.ts
└── pumps
│ └── MockPump.ts
├── app.ts
├── config
├── Config.ts
└── VersionCheck.ts
├── controller
├── Constants.ts
├── Equipment.ts
├── Errors.ts
├── Lockouts.ts
├── State.ts
├── boards
│ ├── AquaLinkBoard.ts
│ ├── BoardFactory.ts
│ ├── EasyTouchBoard.ts
│ ├── IntelliCenterBoard.ts
│ ├── IntelliComBoard.ts
│ ├── IntelliTouchBoard.ts
│ ├── NixieBoard.ts
│ ├── SunTouchBoard.ts
│ └── SystemBoard.ts
├── comms
│ ├── Comms.ts
│ ├── ScreenLogic.ts
│ └── messages
│ │ ├── Messages.ts
│ │ ├── config
│ │ ├── ChlorinatorMessage.ts
│ │ ├── CircuitGroupMessage.ts
│ │ ├── CircuitMessage.ts
│ │ ├── ConfigMessage.ts
│ │ ├── CoverMessage.ts
│ │ ├── CustomNameMessage.ts
│ │ ├── EquipmentMessage.ts
│ │ ├── ExternalMessage.ts
│ │ ├── FeatureMessage.ts
│ │ ├── GeneralMessage.ts
│ │ ├── HeaterMessage.ts
│ │ ├── IntellichemMessage.ts
│ │ ├── OptionsMessage.ts
│ │ ├── PumpMessage.ts
│ │ ├── RemoteMessage.ts
│ │ ├── ScheduleMessage.ts
│ │ ├── SecurityMessage.ts
│ │ └── ValveMessage.ts
│ │ └── status
│ │ ├── ChlorinatorStateMessage.ts
│ │ ├── EquipmentStateMessage.ts
│ │ ├── HeaterStateMessage.ts
│ │ ├── IntelliChemStateMessage.ts
│ │ ├── IntelliValveStateMessage.ts
│ │ ├── PumpStateMessage.ts
│ │ └── VersionMessage.ts
└── nixie
│ ├── Nixie.ts
│ ├── NixieEquipment.ts
│ ├── bodies
│ ├── Body.ts
│ └── Filter.ts
│ ├── chemistry
│ ├── ChemController.ts
│ ├── ChemDoser.ts
│ └── Chlorinator.ts
│ ├── circuits
│ └── Circuit.ts
│ ├── heaters
│ └── Heater.ts
│ ├── pumps
│ └── Pump.ts
│ ├── schedules
│ └── Schedule.ts
│ └── valves
│ └── Valve.ts
├── defaultConfig.json
├── logger
├── DataLogger.ts
└── Logger.ts
├── package-lock.json
├── package.json
├── sendSocket.js
├── tsconfig.json
└── web
├── Server.ts
├── bindings
├── aqualinkD.json
├── homeassistant.json
├── influxDB.json
├── mqtt.json
├── mqttAlt.json
├── rulesManager.json
├── smartThings-Hubitat.json
├── valveRelays.json
└── vera.json
├── interfaces
├── baseInterface.ts
├── httpInterface.ts
├── influxInterface.ts
├── mqttInterface.ts
└── ruleInterface.ts
└── services
├── config
├── Config.ts
└── ConfigSocket.ts
├── state
├── State.ts
└── StateSocket.ts
└── utilities
└── Utilities.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es2021": true,
4 | "node": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended"
9 | ],
10 | "parser": "@typescript-eslint/parser",
11 | "parserOptions": {
12 | "ecmaVersion": "latest",
13 | "sourceType": "module"
14 | },
15 | "plugins": [
16 | "@typescript-eslint"
17 | ],
18 | "rules": {
19 | "indent": [
20 | "error",
21 | "tab"
22 | ],
23 | "linebreak-style": [
24 | "error",
25 | "windows"
26 | ],
27 | "quotes": [
28 | "error",
29 | "single"
30 | ],
31 | "semi": [
32 | "error",
33 | "always"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/1-bug-report.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F41B Bug report"
2 | description: Create a report to help us improve
3 | title: '[BUG] '
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thank you for reporting an issue. Most issues can be more readily resolved by attaching a Packet Capture. Follow the instructions to complete a [packet capture](https://github.com/tagyoureit/nodejs-poolController/wiki/How-to-capture-all-packets-for-issue-resolution) and attach the resulting zip/log files.
9 |
10 | If you require more general support please file an start a discussion on our discussion board https://github.com/tagyoureit/nodejs-poolController/discussions
11 |
12 | Having trouble installing? Be sure to check out the wiki! https://github.com/tagyoureit/nodejs-poolController/wiki
13 |
14 | Please fill in as much of the form below as you're able.
15 | - type: input
16 | attributes:
17 | label: nodejs-poolController Version/commit
18 | description: can be viewed under dashPanel. Hamburger menu => System.
19 | validations:
20 | required: true
21 | - type: input
22 | attributes:
23 | label: nodejs-poolController-dashPanel Version/commit
24 | description: if applicable
25 | - type: input
26 | attributes:
27 | label: relayEquipmentManager Version/commit
28 | description: if applicable
29 | - type: input
30 | attributes:
31 | label: Node Version
32 | description: Output of `node -v`
33 | - type: input
34 | attributes:
35 | label: Platform
36 | description: |
37 | UNIX: output of `uname -a`
38 | Windows: output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in PowerShell console
39 | - type: input
40 | attributes:
41 | label: RS485 Adapter
42 | description: Elfin? JBTek?
43 | - type: checkboxes
44 | attributes:
45 | label: Are you using Docker?
46 | options:
47 | - label: Yes.
48 | - type: input
49 | attributes:
50 | label: OCP
51 | description: Outdoor Control Panel. Eg EasyTouch2 8P, Intellicenter i5PS, none.
52 | placeholder: None / Nixie (standalone setup)
53 | - type: input
54 | attributes:
55 | label: Pump(s)
56 | description: Please list all pumps. EG Intelliflo 2 VST, Intelliflo VS
57 | placeholder: Intelliflo VS
58 | - type: input
59 | attributes:
60 | label: Chlorinator(s)
61 | description: Please list all chlorinators. EG Intellichlor IC-40, Aquarite, None
62 | placeholder: None
63 | - type: textarea
64 | attributes:
65 | label: What steps will reproduce the bug?
66 | description: Enter details about your bug, preferably a simple code snippet that can be run using `node` directly without installing third-party dependencies.
67 | validations:
68 | required: true
69 | - type: textarea
70 | attributes:
71 | label: What happens?
72 | description: If possible please provide textual output instead of screenshots.
73 | validations:
74 | required: true
75 | - type: textarea
76 | attributes:
77 | label: What should have happened?
78 | description: If possible please provide textual output instead of screenshots.
79 | validations:
80 | required: true
81 | - type: textarea
82 | attributes:
83 | label: Additional information
84 | description: Tell us anything else you think we should know.
85 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/2-docs.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 📚 Documentation
3 | about: Report an issue related to documentation
4 | ---
5 |
6 | ## 📚 Documentation
7 |
8 | (A clear and concise description of how the docs could be better, with links if possible)
9 |
10 | ### Have you read the [Contributing Guidelines on docs](https://github.com/serialport/node-serialport/blob/master/CONTRIBUTING.md#writing-documentation)?
11 |
12 | (Write your answer here.)
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/3-proposal.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 💥 Proposal / Feature
3 | about: Propose a non-trivial change or new feature for SerialPort
4 | ---
5 |
6 | ## 💥 Proposal
7 |
8 | ### What feature you'd like to see
9 |
10 | (A clear and concise description of what the proposal is.)
11 |
12 | ## Motivation
13 |
14 | (Please outline the motivation for the proposal. It's interesting knowing what people are working on and also could help community members make suggestions for work-arounds until the feature is built)
15 |
16 | ## Pitch
17 |
18 | (Please explain why this feature should be implemented and how it would be used.)
19 |
20 |
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: ⁉️ Need help with nodejs-poolController?
4 | url: https://github.com/tagyoureit/nodejs-poolController/discussions
5 | about: Please start a discussion before opening a bug.
6 | - name: Gitter
7 | url: https://gitter.im/nodejs-poolController/Lobby
8 | about: Legacy Gitter discussion forums
9 |
--------------------------------------------------------------------------------
/.github/workflows/docker-publish-njsPC-linux.yml:
--------------------------------------------------------------------------------
1 | name: Publish Docker Image - Ubuntu
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | tags:
8 | - "v*.*.*"
9 | workflow_dispatch:
10 |
11 | jobs:
12 | build-and-push:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Docker meta
16 | id: meta
17 | uses: docker/metadata-action@v5
18 | with:
19 | # list of Docker images to use as base name for tags
20 | images: |
21 | tagyoureit/njspc
22 | # generate Docker tags based on the following events/attributes
23 | tags: |
24 | type=schedule
25 | type=ref,event=branch
26 | type=ref,event=pr
27 | type=semver,pattern={{version}}
28 | type=semver,pattern={{major}}.{{minor}}
29 | type=semver,pattern={{major}}
30 | type=sha
31 |
32 | - name: Set up QEMU
33 | uses: docker/setup-qemu-action@v3
34 |
35 | - name: Set up Docker Buildx
36 | uses: docker/setup-buildx-action@v3
37 |
38 | - name: Login to Docker Hub
39 | uses: docker/login-action@v3
40 | with:
41 | username: ${{ secrets.DOCKERHUB_USERNAME }}
42 | password: ${{ secrets.DOCKERHUB_TOKEN }}
43 |
44 | - name: Build and push combined Docker image
45 | uses: docker/build-push-action@v6
46 | with:
47 | push: true
48 | platforms: linux/amd64,linux/arm64,linux/arm/v7
49 | tags: ${{ steps.meta.outputs.tags }}
50 | labels: ${{ steps.meta.outputs.labels }}
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /nbproject/private/
2 | /logs
3 | *.log
4 | .coveralls*
5 | .idea/
6 | .cache
7 | .snyk
8 | .travis.yml
9 | node_modules/
10 | docs/
11 | out/
12 | # ignore coverage output
13 | .nyc_output
14 | coverage/
15 | # ignore dist, it is for production files created by npm scripts
16 | dist/
17 | # ignore any replay files
18 | replay/
19 | # ignore the config.json; it will get recreated
20 | config.json
21 | # data files
22 | data/
23 | backups/
24 | poolConfig.json
25 | poolState.json
26 | *.crt
27 | *.key
28 | users.htpasswd
29 | # ignore visual studio
30 | *.csproj
31 | *.njsproj
32 | *.sln
33 | *.njsproj.user
34 | obj/
35 | bin/
36 | .vs/
37 | .vscode/
38 | web/bindings/custom/
39 | # end
40 |
41 | /web/bindings/njsPCAux.json
42 | /web/bindings/njsPCMaster.json
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | # .npmrc
2 | engine-strict=true
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | Third-party patches are essential for keeping nodejs-poolController great. We
4 | simply can't access the huge number of platforms and myriad configurations for
5 | running different pools and their equipment. We want to keep it as easy as
6 | possible to contribute changes that get things working in your environment.
7 | There are a few guidelines that we need contributors to follow so that we can
8 | have a chance of keeping on top of things.
9 |
10 |
11 | ## Getting Started
12 |
13 | * Make sure you have a [GitHub account](https://github.com/signup/free)
14 | * Submit a ticket for your issue, assuming one does not already exist.
15 | * Clearly describe the issue including steps to reproduce when it is a bug.
16 | * Make sure you fill in the earliest version that you know has the issue.
17 | * Fork the repository on GitHub
18 |
19 | ## Making Changes
20 |
21 | * Create a topic branch from where you want to base your work.
22 | * This is usually the master branch.
23 | * Only target release branches if you are certain your fix must be on that
24 | branch.
25 | * To quickly create a topic branch based on master; `git checkout -b
26 | fix/master/my_contribution master`. Please avoid working directly on the
27 | `master` branch.
28 | * Make commits of logical units.
29 | * Check for unnecessary whitespace with `git diff --check` before committing.
30 |
31 | * Make sure you have added the necessary tests for your changes.
32 | * Run _all_ the tests to assure nothing else was accidentally broken.
33 |
34 | ## Submitting Changes
35 |
36 | * By submitting any changes, you agree that any contributions provided will become the sole property of the Copyright holders subject to the terms below.
37 | 1. Definitions.
38 |
39 | "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Russell Goldin. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
40 |
41 | "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Russell Goldin for inclusion in, or documentation of, any of the products owned or managed by Russell Goldin (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Russell Goldin or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Russell Goldin for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
42 |
43 | 2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Russell Goldin and to recipients of software distributed by Russell Goldin a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
44 |
45 | 3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Russell Goldin and to recipients of software distributed by Russell Goldin a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
46 |
47 | 4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Russell Goldin, or that your employer has executed a separate Corporate CLA with Russell Goldin.
48 |
49 | 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
50 |
51 | 6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
52 |
53 | 7. Should You wish to submit work that is not Your original creation, You may submit it to Russell Goldin separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
54 |
55 | 8. You agree to notify Russell Goldin of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
56 |
57 | * Push your changes to a topic branch in your fork of the repository.
58 | * Submit a pull request to the repository.
59 | * The core team looks at Pull Requests on a regular basis.
60 |
61 | ## Revert Policy
62 | By running tests in advance and by engaging with peer review for prospective
63 | changes, your contributions have a high probability of becoming long lived
64 | parts of the the project. After being merged, the code will run through a
65 | series of testing pipelines on a large number of operating system
66 | environments. These pipelines can reveal incompatibilities that are difficult
67 | to detect in advance.
68 |
69 | If the code change results in a test failure, we will make our best effort to
70 | correct the error.
71 |
72 |
73 | ### Summary
74 | * Changes resulting in failures will be reverted.
75 |
--------------------------------------------------------------------------------
/Changelog:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 8.1.0
4 | 1. Support for dual chlorinators with REM chem controllers. It is now possible to have two separate chlorinators controlled in 'dynamic' mode by two separate REM chems. Note: In order for REM chem to control each chlorinator, each needs to be on a dedicated RS-485 port (not shared with an OCP or any other chlorinator).
5 |
6 | ## 8.0.1-8.0.5
7 | 1. Bug fixes including:
8 | a. schedule end time errors
9 | b. manual priority
10 | c. screenlogic recurring schedules
11 | d. VF pump message sequences
12 | e. intellibrite themes
13 | f. schedules are evaluated by bodies first and then everything else
14 | g. solar stop/start delta logic
15 | 2. Jandy WaterColors support
16 | 3. Initial docker support (github docker actions)
17 |
18 | ## 8.0.0
19 | 1. Refactor comms code to Async
20 | 2. Update dependencies and Node >16
21 | 3. EasyTouch v1 support
22 | 4. Screenlogic support
23 | 5. Anslq25 (Mock controller)
24 |
25 | ## 7.7.0
26 | 1. Aqualink-D MQTT Interface
27 | 2. Manual Priority for Schedules
28 | 3. Add multiple RS-485 ports
29 | 4. Support for Hayward Pumps
30 | 5. Display remote chlorinators
31 | 6. Updates for ETi hybrid
32 | 7. Stop temp deltas for Nixie
33 | 8. Batch write influx points
34 | 9. MQTT & Influx updates
35 | 10. Ability to hide bodies in dashPanel
36 | 11. Florida Sunseeker Pooltone lighting
37 | 12. Proper uPNP formatting
38 |
39 |
40 | ## 7.6.1
41 | 1. Many bugfixes: Intellichem, Touch body capacities, Ultratemp heaters, Nixie, more...
42 | 2. Add env variables for Docker setup: POOL_RS485_PORT, POOL_NET_CONNECT, POOL_NET_HOST, POOL_NET_PORT, SOURCE_COMMIT, SOURCE_BRANCH
43 | 3. Update dependencies
44 | ## 7.6
45 | 1. MasterTemp RS485 support for Nixie and IntelliCenter
46 | 2. Nixie Valve Rotation delay
47 | 3. Nixie Heater Cooldown delay
48 | 4. Nixie Cleaner Start delay
49 | 5. Nixie Cleaner Shutdown on Solar
50 | 6. Nixie Delay Cancel
51 |
52 | ## 7.5.1
53 | 1. Backup/restore fixes
54 | 2. Egg timer expiration
55 | 3. Bug Fixes
56 | 4. dashPanel/messageManager Filter
57 | 5. RS485 refactor
58 |
59 | ## 7.5
60 | 1. Backup/restore
61 | 2. Intellitouch add expansion modules
62 | ## 7.4
63 | 1. Filter object, emit, monitoring
64 |
65 | ## 7.3.1
66 | 1. Influx 2.0 support
67 |
68 | ## 7.3
69 | 1. Dynamic chlorinating % based on ORP demand for Nixie
70 | 2. Docker creation updates
71 |
72 | ## 7.2
73 | 1. Refactor Intellichem and Chem Controllers
74 |
75 | ## 7.1.1
76 | 1. Added end time for circuits to show eggtimer/schedule off times
77 | 2. Ultratemp updates
78 | 3. Heater logic refactored
79 | 4. Message response logic refactored
80 | 5. Intellichem updates
81 |
82 |
83 | ## 7.1.0
84 | 1. Moved virtual chlorinator code and control to Nixie
85 | 2. Moved virtual pump code and control to Nixie; Nixie supports SS, DS, SuperFlo, VS, VF, VSF
86 | 3. MQTT changes
87 | 4. Outbound processing for packets now has a scope. Previously if an outbound packet would receive a response we would clear all of the similar packets off the queue. EG if a user requests circuit 2 and 3 to be turned on, we would clear out the outbound message for 3. Now the code is more selective about what "scope" is considered for a successful response.
88 |
89 | ## 7.0.0
90 | 1. Upgrades to setup/sync between njsPC and REM
91 | 2. Significant steps to njsPC (Nixie) acting as a standalone pool controller (virtual controller heaters, move virtual controller code, etc.)
92 | 3. Dependency updates (Typescript 4, Socket.io 4, etc)
93 |
94 | ## 6.5.2
95 | 1. Bug Fixes
96 | 2. Schedule updates
97 | 3. MQTT Binding updates
98 | 4. LSI calcs for REM (in addition to CSI)
99 |
100 | ## 6.5.1
101 | 1. Init Touch bodies upon startup
102 | 2. *Touch chlorinator fixes
103 | 3. MQTT updates
104 |
105 | ## 6.5.0
106 | 1. Full compatibility with REM (Relay Equipment Manager) for hardware control (ph sensors, orp sensors, pumps, relays, flow sensors)
107 | 1. Upgrades to Influx binding
108 | 1. MQTT alternate bindings
109 | 1. Many, many bug fixes
110 |
111 | ## 6.1.0
112 | 1. Chem controller
113 | 1. MQTT native support
114 | 1. Server based time for *Touch and other non-internet based OCP
115 | 1. Version notifications
116 | 1. IntelliCenter updates for dual bodies, 1.045/1.047 (partial) support
117 | 1. Many bug fixes
118 |
119 | ## 6.0.1
120 | 1. Implement https (no basic auth yet)
121 | 1. API documentation @ https://tagyoureit.github.io/nodejs-poolcontroller-api/
122 | 1. Add timestamp to logs for API calls
123 | 1. #200, #202
124 |
125 | ## 6.0
126 | What's new in 6.0?s
127 |
128 | In short, everything! 6.0 is a complete re-write of the application. Huge props to @rstrouse for his wisdom and guidance in refactoring the code.
129 |
130 | 1. IntelliCenter - now supported
131 | 1. Configuring and running the app - all new. Start over with the Installation instructions.
132 | 1. Automatic detection of your pool equipment. Previous versions of the app would detect the configuration of your pool but you still had to tell the app if you had IntelliTouch/EasyTouch/IntelliCom. This is now done automatically.
133 | 1. Configuration and state information. Config.json now only stores information related to the configuration of the app. There are separate files in the /data directory that store (and persist) pool configuration and state information.
134 | 1. API's - completely changed. See separate API documentation (*link here)
135 | 1. Outbound Sockets - Now more granular to make the web app more responsive
136 | 1. Web app - Now a separate installion for a true client/server metaphore.
137 | 1. Node v12+
138 | 1. `Integrations` are now called `Bindings`. Any integration built on 5.3 need to be upgraded to the binding format. See Readme for a list of currently upgraded bindings.
139 |
140 | ## 5.3.3
141 | #134
142 |
143 | ## 5.3.1
144 | #132
145 |
146 | ## 5.3.0
147 | Fix for #106
148 | Fix for "Error 60" messages
149 | Improved caching of files on browsers. Thanks @arrmo! Now files will be loaded once in the browser and kept in cache instead of reloaded each time.
150 | Improved handling of sessions and graceful closing of the HTTP(s) servers.
151 |
152 | ## 5.2.0
153 | 1. Node 6+ is supported. This app no longer supports Node 4.
154 | 1. Update of modules. Make sure to run `npm i` or `npm upgrade` to get the latest.
155 | 1. Much better support of multiple Intellibrite controllers. We can read both controllers now. There are still some issues with sending changes and help is needed to debug these.
156 | 1. Chlorinator API calls (and UI) will now make changes through Intellitouch when available, or directly to the Intellichlor if it is standalone (aka using the virtual controller)
157 | 1. Decoupled serial port and processing of packets. Should help recovery upon packet errors.
158 | 1. Implementation of #89. Expansion boards are now (better) supported by setting variables in your config.json. See the [config.json](#module_nodejs-poolController--config) section below.
159 | 1. Fix for #95
160 | 1. Fix for #99
161 | 1. Fix for #100
162 |
163 | ## 5.1.1 -
164 | 1. Renamed all 'valves' items to valve to be in line with singular renaming of items
165 | 1. InfluxDB - moved some items that were in tag fields to field keys; added valves
166 | 1. Added days of week (with editing) back to the schedules. Not sure when they disappeared, but they are back now. #92
167 | 1. Added MySQL integration to log all packets to a DB
168 | 1. Fixed PR #95 to allow sub-hour egg timers
169 | 1. Fixed Intellibrite bugs
170 | 1. Started to move some of the inter-communications to emitter events for better micro-services and shorter call stacks (easier debugging; loosely coupled code).
171 | 1. Changed some Influx tags/queries.
172 |
173 | ## 5.1.0 -
174 | 1. Intellibrite support - API's, Sockets and a WebUI. Lights should have the 'Intellbrite' an their circuit function (set this up at the controller) to show up in this section.
175 | Will document more later, but...
176 | /light/mode/:mode
177 | /light/circuit/:circuit/setColor/:color
178 | /light/circuit/:circuit/setSwimDelay/:delay
179 | /light/circuit/:circuit/setPosition/:position
180 |
181 | See the constants.js file and the sections:
182 | strIntellibriteModes (for modes)
183 | lightColors (for setColor)
184 |
185 | ## 5.0.1 -
186 | 1. Fixed Influx error on startup #90
187 | 1. Fixed bad characters in custom names
188 |
189 | ## 5.0.0 -
190 | Make sure to run `npm upgrade`. There are many package updates and changes.
191 |
192 | * Added add/delete/edit schedule
193 | * All sockets/API now singular (`circuits`->`circuit`)
194 | * All sockets/API data now returned with a JSON qualifier. EG `{pump:...}`, `{circuit:...}`
195 | * Intellichem decoding and display
196 | * Changes to `/config` endpoint. It's now included with the `/all` end point since there would be quite a bit of duplication. It still exists standalone (for now) but has much less information in it.
197 | * Moved `hideAux` setting from `configClient.json` (web UI settings) to `config.json` template. In `config.json` template, moved
198 | ```
199 | {equipment: {controller: {circuitFriendlyNames:{1..20}}}}
200 |
201 | // to
202 |
203 | {equipment: {circuit: friendlyName:{1..20},
204 | hideAux: boolean
205 | },
206 | }
207 | ```
208 | to be in line with the other equipment in the pool setup and accomodate the `hideAux` setting.
209 |
210 | * Fixed issue #82
211 | * Extra info from `/config` was being added to the circuit section in `config.json`
212 | * This release includes a new mechanism for updating config.json files. See notes in [config.json](#module_nodejs-poolController--config) section.
213 | * mDNS server. Currently included for SmartThings integration, but in the future can be used for autodiscovery by other applications/devices.
214 | * New `/config` endpoint (beta) to allow applications to get a high level summary of the system.
215 | * Support for two separate (http/https) web servers, each/both with Auth, and also the option to redirect all http to https traffic. Thanks to @arrmo for driving this with #65 and #68.
216 | * A UI for standalone pumps
217 | * All sockets and API's renamed to be SINGULAR. Circuits -> circuit, Schedules->schedule, etc.
218 | * All returned JSON data (API/socket) now has the type qualifier per [#57](https://github.com/tagyoureit/nodejs-poolController/issues/57)
219 | * Make sure to run `npm upgrade`. There are many package updates and changes.
220 | * Intellichem initial support.
221 | * Inactivity timer for both internal connections and web page connections. If a connection is broken, it should re-establish itself automatically now.
222 | * SSDP for auto-discovery by SmartThings or other services
223 |
224 | ## 4.0.0 -
225 | * Changed much in the config.json file
226 | * Save pump programs and chlorinator level to config.json
227 | * Added support for GPM with pumps
228 | * Check for newer versions of the app on github, and dismiss notifications until next release
229 | * Bootstrap configuration is automatically saved in clientConfig.json via UI actions
230 | * Started to introduce some promises into the workflow (mostly with read/write operations)
231 | * Added log-to-file option
232 | * Added capture for Ctrl-C/SIGINT to have a clean exit
233 | * Added InfluxDB database capabilities
234 | * Added support for reading the data from up to 16 pumps. (You can still only control two.)
235 | * Support for up to 50 circuits, 8 pumps
236 | * Delay and Cancel Delay for circuits
237 |
238 | ## 3.1.x -
239 | * Added unit testing for certain areas
240 | * Added setDateTime API/Socket
241 | * Bootstrap panel states are now persistent
242 |
243 | ## 3.0.0 -
244 | * Upgraded pump logic
245 |
246 | ## 2.0.0 -
247 | * https, Authentication
248 | * Completely refactored code. Integrated BottleJS (https://github.com/young-steveo/bottlejs) for dependency injection and service locator functions
249 | * Integrations to loosely couple add-ons
250 |
251 | ## 1.0.0 -
252 | * Much of the code reworked and refactored
253 | * Added Bootstrap UI by @arrmo
254 | * Better standalone pump control (@bluemantwo was super-helpful here, too!)
255 | * More accurate recognition of packets
256 | * Super fast speed improvements
257 | * Outgoing packets are now sent based on a timer (previously number of incoming packets)
258 | * Added ISY support (@bluemantwo was super-helpful here, too!)
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine AS build
2 | RUN apk add --no-cache make gcc g++ python3 linux-headers udev tzdata
3 | WORKDIR /app
4 | COPY package*.json ./
5 | COPY defaultConfig.json config.json
6 | RUN npm ci
7 | COPY . .
8 | RUN npm run build
9 | RUN npm ci --omit=dev
10 |
11 | FROM node:18-alpine as prod
12 | RUN apk add git
13 | RUN mkdir /app && chown node:node /app
14 | WORKDIR /app
15 | COPY --chown=node:node --from=build /app .
16 | USER node
17 | ENV NODE_ENV=production
18 | EXPOSE 5150
19 | ENTRYPOINT ["node", "dist/app.js"]
20 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 |
3 | grunt.initConfig({
4 | usebanner: {
5 | taskName: {
6 | options: {
7 | position: 'top',
8 | banner: `/* nodejs-poolController. An application to control pool equipment.
9 | Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com
10 |
11 | This program is free software: you can redistribute it and/or modify
12 | it under the terms of the GNU Affero General Public License as
13 | published by the Free Software Foundation, either version 3 of the
14 | License, or (at your option) any later version.
15 |
16 | This program is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU Affero General Public License for more details.
20 |
21 | You should have received a copy of the GNU Affero General Public License
22 | along with this program. If not, see .
23 | */`,
24 | linebreak: true,
25 | replace: false
26 | },
27 | files: {
28 | src: [ 'config/*.ts', 'controller/**/*.ts', 'logger/*.ts', 'web/**/*.ts', 'app.ts' ]
29 | }
30 | }
31 | }
32 | })
33 |
34 | grunt.loadNpmTasks('grunt-banner');
35 |
36 | }
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/anslq25/boards/MockBoardFactory.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | // import { MockIntelliCenterBoard } from './MockIntelliCenterBoard';
19 | // import { MockIntelliTouchBoard } from './MockIntelliTouchBoard';
20 | // import { MockIntelliComBoard } from './MockIntelliComBoard';
21 | import { MockEasyTouch } from './MockEasyTouchBoard';
22 | import { MockSystemBoard } from './MockSystemBoard';
23 | import { ControllerType } from '../../controller/Constants';
24 | import { PoolSystem } from 'controller/Equipment';
25 | // import { MockAquaLinkBoard } from './MockAquaLinkBoard';
26 | // import { MockSunTouchBoard } from "./MockSunTouchBoard";
27 |
28 |
29 | export class MockBoardFactory {
30 | // Factory create the system board from the controller type. Resist storing
31 | // the pool system as this can cause a leak. The PoolSystem object already has a reference to this.
32 | public static fromControllerType(ct: ControllerType, system: PoolSystem) {
33 | switch (ct) {
34 | // case ControllerType.IntelliCenter:
35 | // return new MockIntelliCenterBoard(system);
36 | // case ControllerType.IntelliTouch:
37 | // return new MockIntelliTouchBoard(system);
38 | // case ControllerType.IntelliCom:
39 | // return new MockIntelliComBoard(system);
40 | case ControllerType.EasyTouch:
41 | return new MockEasyTouch(system);
42 | // case ControllerType.AquaLink:
43 | // return new MockAquaLinkBoard(system);
44 | // case ControllerType.SunTouch:
45 | // return new MockSunTouchBoard(system);
46 | }
47 | return new MockSystemBoard(system);
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/anslq25/boards/MockSystemBoard.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 |
19 | import { logger } from "../../logger/Logger";
20 | import { setTimeout as setTimeoutSync } from 'timers';
21 | import { Inbound, Outbound, Protocol } from "../../controller/comms/messages/Messages";
22 | import { byteValueMap, byteValueMaps, SystemBoard } from "../../controller/boards/SystemBoard";
23 | import { Anslq25, PoolSystem, sys } from "../../controller/Equipment";
24 | import { ControllerType, utils } from "../../controller/Constants";
25 | import { conn } from "../../controller/comms/Comms";
26 | import { MockEasyTouch } from "./MockEasyTouchBoard";
27 |
28 | export class MockSystemBoard {
29 | public valueMaps: byteValueMaps = new byteValueMaps();
30 | protected _statusTimer: NodeJS.Timeout;
31 | protected _statusCheckRef: number = 0;
32 | protected _statusInterval: number = 5000;
33 | constructor(system: PoolSystem) {
34 | // sys.anslq25.portId = 0; // pass this in.
35 | setTimeout(() => {
36 | this.processStatusAsync().then(() => { });
37 | }, 5000);
38 | }
39 | public expansionBoards: byteValueMap = new byteValueMap();
40 | public get statusInterval(): number { return this._statusInterval };
41 | public system: MockSystemCommands = new MockSystemCommands(this);
42 | public circuits: MockCircuitCommands = new MockCircuitCommands(this);
43 | public schedules: MockScheduleCommands = new MockScheduleCommands(this);
44 | public heaters: MockHeaterCommands = new MockHeaterCommands(this);
45 | public valves: MockValveCommands = new MockValveCommands(this);
46 | public remotes: MockRemoteCommands = new MockRemoteCommands(this);
47 | public pumps: MockPumpCommands = new MockPumpCommands(this);
48 | public static convertOutbound(outboundMsg: Outbound) { };
49 | public async sendAsync(msg: Outbound){
50 | return await msg.sendAsync();
51 | // is the controller on a real/physical port or a mock port?
52 | /* let port = conn.findPortById(sys.anslq25.portId);
53 | if (port.mockPort) {
54 | let inbound = new Inbound();
55 | inbound.protocol = msg.protocol;
56 | inbound.header = msg.header;
57 | inbound.payload = msg.payload;
58 | inbound.term = msg.term;
59 | inbound.portId = msg.portId;
60 | // don't need to wait for packet to process
61 | setTimeout(()=>{conn.sendMockPacket(inbound)}, 10);
62 | return Promise.resolve();
63 | }
64 | else {
65 | return await msg.sendAsync();
66 | } */
67 | }
68 | public process(msg: Inbound): Outbound { return new Outbound(Protocol.Broadcast,0,0,0,[]); }
69 | protected killStatusCheck() {
70 | if (typeof this._statusTimer !== 'undefined' && this._statusTimer) clearTimeout(this._statusTimer);
71 | this._statusTimer = undefined;
72 | this._statusCheckRef = 0;
73 | }
74 | public suspendStatus(bSuspend: boolean) {
75 | // The way status suspension works is by using a reference value that is incremented and decremented
76 | // the status check is only performed when the reference value is 0. So suspending the status check 3 times and un-suspending
77 | // it 2 times will still result in the status check being suspended. This method also ensures the reference never falls below 0.
78 | if (bSuspend) this._statusCheckRef++;
79 | else this._statusCheckRef = Math.max(0, this._statusCheckRef - 1);
80 | if (this._statusCheckRef > 1) logger.verbose(`Suspending ANSLQ25 status check: ${bSuspend} -- ${this._statusCheckRef}`);
81 | }
82 | public async setAnslq25Async(data: any): Promise {
83 | let self = this;
84 | try {
85 | this.suspendStatus(true);
86 | // if (typeof data.isActive === 'undefined') return Promise.reject(`Mock System Board: No isActive flag provided.`);
87 | if (typeof data.anslq25portId === 'undefined') return Promise.reject(new Error(`Mock System Board: No portId provided.`));
88 | if (typeof data.anslq25ControllerType === 'undefined') return Promise.reject(new Error(`Mock System Board: No controller type provided.`));
89 | if (typeof data.anslq25model === 'undefined') return Promise.reject(new Error(`Mock System Board: No model provided.`));
90 | //for (let i = 1; i <= )
91 | let isActive = true; // utils.makeBool(data.isActive);
92 | let portId = parseInt(data.anslq25portId, 10);
93 | let port = conn.findPortById(portId);
94 | if (typeof port === 'undefined') return Promise.reject(new Error(`Mock System Board: Invalid portId provided.`));
95 | if (portId === 0) return Promise.reject(new Error(`Please choose a port other than the primary port.`));
96 | let mockControllerType = data.anslq25ControllerType;
97 | let model = parseInt(data.anslq25model, 10);
98 | let broadcastComms = data.broadcastComms;
99 | if (typeof broadcastComms === 'undefined') return Promise.reject(new Error(`A value for broadcast comms must be provided.`));
100 | sys.anslq25.portId = portId;
101 | sys.anslq25.broadcastComms = broadcastComms;
102 | switch (mockControllerType) {
103 | case ControllerType.EasyTouch:{
104 | sys.anslq25ControllerType = ControllerType.EasyTouch;
105 | // (sys.anslq25Board as MockEasyTouch).initExpansionModules(model);
106 | break;
107 | }
108 | default: {
109 | logger.warn(`No ANSLQ25 Mock Board definiton yet for: ${mockControllerType}`);
110 | return Promise.reject(new Error(`No ANSLQ25 Mock Board definiton yet for: ${mockControllerType}`));
111 | }
112 | }
113 | sys.anslq25.isActive = isActive;
114 | sys.anslq25.model = model;
115 |
116 | } catch (err) {
117 | logger.error(`Error changing port id: ${err.message}`);
118 | }
119 | finally {
120 | this.suspendStatus(false);
121 | this._statusTimer = setTimeoutSync(async () => await self.processStatusAsync(), this.statusInterval);
122 | }
123 | }
124 | public async deleteAnslq25Async(data: any) {
125 | try {
126 |
127 | this.killStatusCheck();
128 | this.closeAsync();
129 | sys.anslq25.isActive = false;
130 | sys.anslq25.portId = undefined;
131 | sys.anslq25.model = undefined;
132 | sys.anslq25ControllerType = ControllerType.None;
133 | }
134 | catch (err){
135 |
136 | }
137 | finally {
138 | this.suspendStatus(false);
139 | }
140 |
141 | }
142 |
143 | public async processStatusAsync() {
144 | let self = this;
145 | try {
146 | if (this._statusCheckRef > 0) return;
147 | this.suspendStatus(true);
148 |
149 | await sys.anslq25Board.system.sendStatusAsync();
150 | }
151 | catch (err) {
152 | logger.error(`Error running mock processStatusAsync: ${err}`);
153 | }
154 | finally {
155 | this.suspendStatus(false);
156 | if (sys.anslq25.isActive){
157 | if (this.statusInterval > 0) this._statusTimer = setTimeoutSync(async () => await self.processStatusAsync(), this.statusInterval);
158 | }
159 |
160 | }
161 | }
162 | // public async setPortId(portId: number) {
163 | // let self = this;
164 | // try {
165 | // this.suspendStatus(true);
166 | // sys.anslq25.portId = portId;
167 |
168 | // } catch (err) {
169 | // logger.error(`Error changing port id: ${err.message}`);
170 | // }
171 | // finally {
172 | // this.suspendStatus(false);
173 | // this._statusTimer = setTimeoutSync(async () => await self.processStatusAsync(), this.statusInterval);
174 | // }
175 | // }
176 | public async closeAsync() {
177 | try {
178 | }
179 | catch (err) { logger.error(err); }
180 | }
181 | }
182 | export class MockBoardCommands {
183 | protected mockBoard: MockSystemBoard = null;
184 | constructor(parent: MockSystemBoard) { this.mockBoard = parent; }
185 | }
186 | export class MockSystemCommands extends MockBoardCommands {
187 | public sendAck(msg:Inbound) { };
188 | public async processDateTimeAsync(msg: Inbound){ };
189 | public async processCustomNameAsync(msg: Inbound){ };
190 | public async processSettingsAsync(msg: Inbound){ };
191 | public async sendStatusAsync() { };
192 | }
193 |
194 | export class MockCircuitCommands extends MockBoardCommands {
195 | public async processCircuitAsync( msg: Inbound) { };
196 | public async processLightGroupAsync( msg: Inbound) { };
197 | }
198 | export class MockScheduleCommands extends MockBoardCommands {
199 | public async processScheduleAsync( msg: Inbound) { };
200 | }
201 | export class MockHeaterCommands extends MockBoardCommands {
202 | public async processHeatModesAsync(msg: Inbound) { };
203 | public async processHeaterConfigAsync(msg: Inbound) { };
204 | }
205 | export class MockValveCommands extends MockBoardCommands {
206 | public async processValveOptionsAsync(msg: Inbound) { };
207 | public async processValveAssignmentsAsync(msg: Inbound) { };
208 | }
209 | export class MockRemoteCommands extends MockBoardCommands {
210 | public async processIS4IS10RemoteAsync(msg: Inbound) { };
211 | public async processQuickTouchRemoteAsync(msg: Inbound) { };
212 | public async processSpaCommandRemoteAsync(msg: Inbound) { };
213 | }
214 | export class MockPumpCommands extends MockBoardCommands {
215 | public async processPumpConfigAsync(msg: Inbound) { };
216 | public async processHighSpeedCircuitsAsync(msg: Inbound) { };
217 | }
--------------------------------------------------------------------------------
/anslq25/chemistry/MockChlorinator.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "../../logger/Logger";
2 | import { Inbound, Outbound } from "../../controller/comms/messages/Messages";
3 | import { conn } from "../../controller/comms/Comms";
4 |
5 | export class MockChlorinator {
6 | constructor(){}
7 |
8 | public process(inbound: Inbound){
9 | let response: Outbound = Outbound.create({
10 | portId: inbound.portId,
11 | protocol: inbound.protocol,
12 | dest: 0
13 | });
14 |
15 | switch (inbound.action){
16 | case 0: // Set control OCP->Chlorinator: [16,2,80,0][0][98,16,3]
17 | this.chlorSetControl(inbound, response);
18 | case 17: // OCP->Chlorinator set output. [16,2,80,17][15][130,16,3]
19 | this.chlorSetOutput(inbound, response);
20 | case 19: // iChlor keep alive(?) [16, 2, 80, 19][117, 16, 3]
21 | // need response
22 | break;
23 | case 20: // OCP->Chlorinator Get model [16,2,80,20][0][118,16,3]
24 | this.chlorGetModel(inbound, response);
25 | default:
26 | logger.info(`No mock chlorinator response for ${inbound.toShortPacket()} `);
27 | }
28 | }
29 |
30 | public async chlorSetControl(inbound: Inbound, response: Outbound){
31 | /*
32 | {"port":0,"id":42633,"valid":true,"dir":"out","proto":"chlorinator","pkt":[[],[], [16,2,80,0], [0],[98,16,3]],"ts":"2022-07-19T21:45:59.959-0700"}
33 | {"port":0,"id":42634,"valid":true,"dir":"in","proto":"chlorinator","for":[42633],"pkt":[[],[],[16,2,0,1],[0,0],[19,16,3]],"ts": "2022-07-19T21:45:59.999-0700"} */
34 | try {
35 |
36 | response.action = 1;
37 | response.appendPayloadBytes(0, 2);
38 | await response.sendAsync()
39 | }
40 | catch (err){
41 | logger.error(`Error sending mock chlor packet ${response.toPacket}`);
42 | }
43 | }
44 | public chlorSetOutput(inbound: Inbound, response: Outbound){
45 | /*
46 | {"port":0,"id":42639,"valid":true,"dir":"out","proto":"chlorinator","pkt":[[],[], [16,2,80,17], [100],[215,16,3]],"ts":"2022-07-19T21:46:00.302-0700"}
47 | {"port":0,"id":42640,"valid":true,"dir":"in","proto":"chlorinator","for":[42639],"pkt":[[],[],[16,2,0,18],[78,128],[242,16,3]],"ts": "2022-07-19T21:46:00.341-0700"} */
48 | response.action = 18;
49 | response.appendPayloadBytes(0, 2);
50 | // ideal high = 4500 = 90 * 50; ideal low = 2800 = 56 * 50
51 | response.setPayloadByte(0, this.random(90-56, true)+56, 75)
52 | response.setPayloadByte(1, 128);
53 | conn.queueSendMessage(response);
54 | }
55 | public chlorGetModel(inbound: Inbound, response: Outbound){
56 | /*
57 | {"port":0,"id":42645,"valid":true,"dir":"out","proto":"chlorinator","pkt":[[],[], [16,2,80,20], [0],[118,16,3]],"ts":"2022-07-19T21:46:00.645-0700"}
58 | {"port":0,"id":42646,"valid":true,"dir":"in","proto":"chlorinator","for":[42645],"pkt":[[],[],[16,2,0,3],[0,73,110,116,101,108,108,105,99,104,108,111,114,45,45,54,48],[190,16,3]],"ts": "2022-07-19T21:46:00.700-0700"} */
59 | response.action = 3;
60 | response.appendPayloadBytes(0, 17);
61 | response.insertPayloadString(1, 'INTELLICHLOR--60');
62 | conn.queueSendMessage(response);
63 | }
64 |
65 | private random(bounds: number, onlyPositive: boolean = false){
66 | let rand = Math.random() * bounds;
67 | if (!onlyPositive) {
68 | if (Math.random()<=.5) rand = rand * -1;
69 | }
70 | return rand;
71 | }
72 |
73 | }
74 |
75 | export var mockChlor: MockChlorinator = new MockChlorinator();
--------------------------------------------------------------------------------
/anslq25/pumps/MockPump.ts:
--------------------------------------------------------------------------------
1 | import { sys } from "../../controller/Equipment";
2 | import { PumpState, state } from "../../controller/State";
3 | import { Outbound } from "../../controller/comms/messages/Messages";
4 | import { conn } from "controller/comms/Comms";
5 |
6 | export class MockPump {
7 | constructor(){}
8 |
9 | public process(outboundMsg: Outbound){
10 | let response: Outbound = Outbound.create({
11 | portId: outboundMsg.portId,
12 | protocol: outboundMsg.protocol
13 | });
14 |
15 | switch (outboundMsg.action){
16 | case 7:
17 | this.pumpStatus(outboundMsg, response);
18 | default:
19 | this.pumpAck(outboundMsg, response);
20 | }
21 | }
22 |
23 | public pumpStatus(outboundMsg: Outbound, response: Outbound){
24 | let pState:PumpState = state.pumps.getItemById(outboundMsg.dest - 96);
25 | let pt = sys.board.valueMaps.pumpTypes.get(pState.type);
26 | response.action = 7;
27 | response.source = outboundMsg.dest;
28 | response.dest = outboundMsg.source;
29 | response.appendPayloadBytes(0, 15);
30 | response.setPayloadByte(0, pState.command, 2);
31 | response.setPayloadByte(1, pState.mode, 0);
32 | response.setPayloadByte(2, pState.driveState, 2);
33 | let watts = 0;
34 | if (Math.max(pState.rpm, pState.flow) > 0){
35 | if (pState.rpm > 0) watts = pState.rpm/pt.maxSpeed * 2000 + this.random(100);
36 | else if (pState.flow > 0) watts = pState.flow/pt.maxFlow * 2000 + this.random(100);
37 | else //ss, ds, etc
38 | watts = 2000 + this.random(250);
39 | }
40 | response.setPayloadByte(3, Math.floor(watts / 256), 0);
41 | response.setPayloadByte(4, watts % 256, 0);
42 | response.setPayloadByte(5, Math.floor(pState.rpm / 256), 0);
43 | response.setPayloadByte(6, pState.rpm % 256, 0);
44 | response.setPayloadByte(7, pState.flow, 0);
45 | response.setPayloadByte(8, pState.ppc, 0);
46 | // 9, 10 = unknown
47 | // 11, 12 = Status code;
48 | response.setPayloadByte(11, Math.floor(pState.status / 256), 0);
49 | response.setPayloadByte(12, pState.status % 256, 1);
50 | let time = new Date();
51 | response.setPayloadByte(13, time.getHours() * 60);
52 | response.setPayloadByte(14, time.getMinutes());
53 |
54 | conn.queueSendMessage(response);
55 | }
56 |
57 | public pumpAck(outboundMsg: Outbound, response: Outbound){
58 | response.action = outboundMsg.action;
59 | response.source = outboundMsg.dest;
60 | response.dest = outboundMsg.source;
61 | switch (outboundMsg.action){
62 | case 1:
63 | case 10: {
64 | response.appendPayloadByte(outboundMsg.payload[2]);
65 | response.appendPayloadByte(outboundMsg.payload[3]);
66 | break;
67 | }
68 | default:
69 | response.appendPayloadByte(outboundMsg.payload[0]);
70 | }
71 | conn.queueSendMessage(response);
72 | }
73 |
74 | private random(bounds: number, onlyPositive: boolean = false){
75 | let rand = Math.random() * bounds;
76 | if (!onlyPositive) {
77 | if (Math.random()<=.5) rand = rand * -1;
78 | }
79 | return rand;
80 | }
81 |
82 | }
83 |
84 | export var mockPump: MockPump = new MockPump();
--------------------------------------------------------------------------------
/app.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | // add source map support for .js to .ts files
19 | //require('source-map-support').install();
20 | import 'source-map-support/register';
21 |
22 | import { logger } from "./logger/Logger";
23 | import { config } from "./config/Config";
24 | import { conn } from "./controller/comms/Comms";
25 | import { sys } from "./controller/Equipment";
26 |
27 | import { state } from "./controller/State";
28 | import { webApp } from "./web/Server";
29 | import * as readline from 'readline';
30 | import { sl } from './controller/comms/ScreenLogic'
31 |
32 | export async function initAsync() {
33 | try {
34 | await config.init();
35 | await logger.init();
36 | await sys.init();
37 | await state.init();
38 | await webApp.init();
39 | await conn.initAsync();
40 | await sys.start();
41 | await webApp.initAutoBackup();
42 | await sl.openAsync();
43 | } catch (err) { console.log(`Error Initializing nodejs-PoolController ${err.message}`); }
44 | }
45 |
46 | export async function startPacketCapture(bResetLogs: boolean) {
47 | try {
48 | let log = config.getSection('log');
49 | log.app.captureForReplay = true;
50 | config.setSection('log', log);
51 | logger.startCaptureForReplay(bResetLogs);
52 | if (bResetLogs){
53 | sys.resetSystem();
54 | }
55 | }
56 | catch (err) {
57 | console.error(`Error starting replay: ${ err.message }`);
58 | }
59 | }
60 | export async function stopPacketCaptureAsync() {
61 | let log = config.getSection('log');
62 | log.app.captureForReplay = false;
63 | config.setSection('log', log);
64 | return logger.stopCaptureForReplayAsync();
65 | }
66 | export async function stopAsync(): Promise {
67 | try {
68 | console.log('Shutting down open processes');
69 | await webApp.stopAutoBackup();
70 | await sys.stopAsync();
71 | await state.stopAsync();
72 | await conn.stopAsync();
73 | await sl.closeAsync();
74 | await webApp.stopAsync();
75 | await config.updateAsync();
76 | await logger.stopAsync();
77 | // RKS: Uncomment below to see the shutdown process
78 | //await new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, 20000); });
79 | }
80 | catch (err) {
81 | console.error(`Error stopping processes: ${ err.message }`);
82 | }
83 | finally {
84 | process.exit();
85 | }
86 | }
87 | if (process.platform === 'win32') {
88 | let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
89 | rl.on('SIGINT', async function () {
90 | try { await stopAsync(); } catch (err) { console.log(`Error shutting down processes ${err.message}`); }
91 | });
92 | }
93 | else {
94 | process.stdin.resume();
95 | process.on('SIGINT', async function () {
96 | try { return await stopAsync(); } catch (err) { console.log(`Error shutting down processes ${err.message}`); }
97 | });
98 | }
99 | if (typeof process === 'object') {
100 | process.on('unhandledRejection', (error: Error, promise) => {
101 | console.group('unhandled rejection');
102 | console.error("== Node detected an unhandled rejection! ==");
103 | console.error(error.message);
104 | console.error(error.stack);
105 | console.groupEnd();
106 | });
107 | }
108 | ( async () => { await initAsync() })();
--------------------------------------------------------------------------------
/config/Config.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import * as path from "path";
19 | import * as fs from "fs";
20 | import { EventEmitter } from 'events';
21 | const extend = require("extend");
22 | import { logger } from "../logger/Logger";
23 | import { utils } from "../controller/Constants";
24 | import { setTimeout } from 'timers/promises';
25 | class Config {
26 | private cfgPath: string;
27 | private _cfg: any;
28 | private _isInitialized: boolean=false;
29 | private _fileTime: Date = new Date(0);
30 | private _isLoading: boolean = false;
31 | public emitter: EventEmitter;
32 | constructor() {
33 | let self=this;
34 | this.cfgPath = path.posix.join(process.cwd(), "/config.json");
35 | this.emitter = new EventEmitter();
36 | // RKS 05-18-20: This originally had multiple points of failure where it was not in the try/catch.
37 | try {
38 | this._isLoading = true;
39 | this._cfg = fs.existsSync(this.cfgPath) ? JSON.parse(fs.readFileSync(this.cfgPath, "utf8").trim()) : {};
40 | const def = JSON.parse(fs.readFileSync(path.join(process.cwd(), "/defaultConfig.json"), "utf8").trim());
41 | const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), "/package.json"), "utf8").trim());
42 | this._cfg = extend(true, {}, def, this._cfg, { appVersion: packageJson.version });
43 | this._isInitialized = true;
44 | this.updateAsync((err) => {
45 | if (typeof err === 'undefined') {
46 | fs.watch(this.cfgPath, (event, fileName) => {
47 | if (fileName && event === 'change') {
48 | if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once.
49 | console.log('Updating config file');
50 | const stats = fs.statSync(self.cfgPath);
51 | if (stats.mtime.valueOf() === self._fileTime.valueOf()) return;
52 | this._cfg = fs.existsSync(this.cfgPath) ? JSON.parse(fs.readFileSync(this.cfgPath, "utf8")) : {};
53 | this._cfg = extend(true, {}, def, this._cfg, { appVersion: packageJson.version });
54 | logger.init(); // only reload logger for now; possibly expand to other areas of app
55 | logger.info(`Reloading app config: ${fileName}`);
56 | this.emitter.emit('reloaded', this._cfg);
57 | }
58 | });
59 | }
60 | else return Promise.reject(err);
61 | });
62 | this._isLoading = false;
63 | this.getEnvVariables();
64 | } catch (err) {
65 | console.log(`Error reading configuration information. Aborting startup: ${ err }`);
66 | // Rethrow this error so we exit the app with the appropriate pause in the console.
67 | throw err;
68 | }
69 | }
70 | public async updateAsync(callback?: (err?) => void) {
71 | // Don't overwrite the configuration if we failed during the initialization.
72 | try {
73 | if (!this._isInitialized) {
74 | if (typeof callback === 'function') callback(new Error('njsPC has not been initialized.'));
75 | return;
76 | }
77 | this._isLoading = true;
78 | fs.writeFileSync(
79 | this.cfgPath,
80 | JSON.stringify(this._cfg, undefined, 2)
81 | );
82 | if (typeof callback === 'function') callback();
83 | await setTimeout(2000);
84 | this._isLoading = false;
85 | }
86 | catch (err) {
87 | logger.error("Error writing configuration file %s", err);
88 | if (typeof callback === 'function') callback(err);
89 |
90 | }
91 | }
92 | public removeSection(section: string) {
93 | let c = this._cfg;
94 | if (section.indexOf('.') !== -1) {
95 | let arr = section.split('.');
96 | for (let i = 0; i < arr.length - 1; i++) {
97 | if (typeof c[arr[i]] === 'undefined')
98 | c[arr[i]] = {};
99 | c = c[arr[i]];
100 | }
101 | section = arr[arr.length - 1];
102 | }
103 | if(typeof c[section] !== 'undefined') delete c[section];
104 | this.updateAsync();
105 | }
106 | public setSection(section: string, val) {
107 | let c = this._cfg;
108 | if (section.indexOf('.') !== -1) {
109 | let arr = section.split('.');
110 | for (let i = 0; i < arr.length - 1; i++) {
111 | if (typeof c[arr[i]] === 'undefined')
112 | c[arr[i]] = {};
113 | c = c[arr[i]];
114 | }
115 | section = arr[arr.length - 1];
116 | }
117 | c[section] = val;
118 | this.updateAsync();
119 | }
120 | // RKS: 09-21-21 - We are counting on the return from this being immutable. A copy of the data
121 | // should always be returned here.
122 | public getSection(section?: string, opts?: any): any {
123 | if (typeof section === 'undefined') return this._cfg;
124 | let c: any = this._cfg;
125 | if (section.indexOf('.') !== -1) {
126 | const arr = section.split('.');
127 | for (let i = 0; i < arr.length; i++) {
128 | if (typeof c[arr[i]] === "undefined") {
129 | c = null;
130 | break;
131 | } else c = c[arr[i]];
132 | }
133 | } else c = c[section];
134 | return extend(true, {}, opts || {}, c || {});
135 | }
136 | public init() {
137 | let baseDir = process.cwd();
138 | this.ensurePath(baseDir + '/logs/');
139 | this.ensurePath(baseDir + '/data/');
140 | this.ensurePath(baseDir + '/backups/');
141 | this.ensurePath(baseDir + '/web/bindings/custom/')
142 | // this.ensurePath(baseDir + '/replay/');
143 | //setTimeout(() => { config.update(); }, 100);
144 | }
145 | private ensurePath(dir: string) {
146 | if (!fs.existsSync(dir)) {
147 | fs.mkdir(dir, (err) => {
148 | // Logger will not be initialized by the time we reach here so we must
149 | // simply log these to the console.
150 | if (err) console.log(`Error creating directory: ${ dir } - ${ err.message }`);
151 | });
152 | }
153 | }
154 | public setInterface(obj: any){
155 | let interfaces: any = this._cfg.web.interfaces;
156 | for (var i in interfaces) {
157 | if (interfaces[i].uuid === obj.uuid) {
158 | interfaces[i] = obj;
159 | this.updateAsync();
160 | return {[i]: interfaces[i]};
161 | }
162 | }
163 | }
164 | public getInterfaceByUuid(uuid: string){
165 | let interfaces = this._cfg.web.interfaces
166 | for (var i in interfaces) {
167 | if (interfaces[i].uuid === uuid) {
168 | return interfaces[i];
169 | }
170 | }
171 | }
172 | private getEnvVariables(){
173 | // set docker env variables to config.json, if they are set
174 | let env = process.env;
175 | let bUpdate = false;
176 | if (typeof env.POOL_RS485_PORT !== 'undefined' && env.POOL_RS485_PORT !== this._cfg.controller.comms.rs485Port) {
177 | this._cfg.controller.comms.rs485Port = env.POOL_RS485_PORT;
178 | bUpdate = true;
179 | }
180 | if (typeof env.POOL_NET_CONNECT !== 'undefined' && env.POOL_NET_CONNECT !== this._cfg.controller.comms.netConnect) {
181 | this._cfg.controller.comms.netConnect = utils.makeBool(env.POOL_NET_CONNECT);
182 | bUpdate = true;
183 | }
184 | if (typeof env.POOL_NET_HOST !== 'undefined' && env.POOL_NET_HOST !== this._cfg.controller.comms.netHost) {
185 | this._cfg.controller.comms.netHost = env.POOL_NET_HOST;
186 | bUpdate = true;
187 | }
188 | if (typeof env.POOL_NET_PORT !== 'undefined' && env.POOL_NET_PORT !== this._cfg.controller.comms.netPort) {
189 | this._cfg.controller.comms.netPort = env.POOL_NET_PORT;
190 | bUpdate = true;
191 | }
192 | if (bUpdate) this.updateAsync();
193 | }
194 | }
195 | export const config: Config = new Config();
196 |
--------------------------------------------------------------------------------
/config/VersionCheck.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { logger } from "../logger/Logger";
19 | // import { https } from "follow-redirects";
20 | import * as https from 'https';
21 | import { state } from "../controller/State";
22 | import { sys } from "../controller/Equipment";
23 | import { Timestamp } from "../controller/Constants";
24 | import { execSync } from 'child_process';
25 |
26 | class VersionCheck {
27 | private userAgent: string;
28 | private gitApiHost: string;
29 | private gitLatestReleaseJSONPath: string;
30 | private redirects: number;
31 | constructor() {
32 | this.userAgent = 'tagyoureit-nodejs-poolController-app';
33 | this.gitApiHost = 'api.github.com';
34 | this.gitLatestReleaseJSONPath = '/repos/tagyoureit/nodejs-poolController/releases/latest';
35 | }
36 |
37 | public checkGitRemote() {
38 | // need to significantly rate limit this because GitHub will start to throw 'too many requests' error
39 | // and we simply don't need to check that often if the app needs to be updated
40 | if (typeof state.appVersion.nextCheckTime === 'undefined' || new Date() > new Date(state.appVersion.nextCheckTime)) setTimeout(() => { this.checkAll(); }, 100);
41 | }
42 | public checkGitLocal() {
43 | let env = process.env;
44 | // check local git version
45 | try {
46 | let out: string;
47 | if (typeof env.SOURCE_BRANCH !== 'undefined')
48 | {
49 | out = env.SOURCE_BRANCH // check for docker variable
50 | }
51 | else {
52 | let res = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' });
53 | out = res.toString().trim();
54 | }
55 | logger.info(`The current git branch output is ${out}`);
56 | switch (out) {
57 | case 'fatal':
58 | case 'command':
59 | state.appVersion.gitLocalBranch = '--';
60 | break;
61 | default:
62 | state.appVersion.gitLocalBranch = out;
63 | }
64 | }
65 | catch (err) {
66 | state.appVersion.gitLocalBranch = '--';
67 | logger.warn(`Unable to retrieve local git branch. ${err}`);
68 | }
69 | try {
70 | let out: string;
71 | if (typeof env.SOURCE_COMMIT !== 'undefined')
72 | {
73 | out = env.SOURCE_COMMIT; // check for docker variable
74 | }
75 | else {
76 | let res = execSync('git rev-parse HEAD', { stdio: 'pipe' });
77 | out = res.toString().trim();
78 | }
79 | logger.info(`The current git commit output is ${out}`);
80 | switch (out) {
81 | case 'fatal':
82 | case 'command':
83 | state.appVersion.gitLocalCommit = '--';
84 | break;
85 | default:
86 | state.appVersion.gitLocalCommit = out;
87 | }
88 | }
89 | catch (err) {
90 | state.appVersion.gitLocalCommit = '--';
91 | logger.warn(`Unable to retrieve local git commit. ${err}`);
92 | }
93 | }
94 | private checkAll() {
95 | try {
96 | this.redirects = 0;
97 | let dt = new Date();
98 | dt.setDate(dt.getDate() + 2); // check every 2 days
99 | state.appVersion.nextCheckTime = Timestamp.toISOLocal(dt);
100 | this.getLatestRelease().then((publishedVersion) => {
101 | state.appVersion.githubRelease = publishedVersion;
102 | this.compare();
103 | });
104 | }
105 | catch (err) {
106 | logger.error(`Error checking latest release: ${err.message}`);
107 | }
108 | }
109 |
110 | private async getLatestRelease(redirect?: string): Promise {
111 | var options = {
112 | method: 'GET',
113 | headers: {
114 | 'User-Agent': this.userAgent
115 | }
116 | }
117 | let url: string;
118 | if (typeof redirect === 'undefined') {
119 | url = `https://${this.gitApiHost}${this.gitLatestReleaseJSONPath}`;
120 | }
121 | else {
122 | url = redirect;
123 | this.redirects += 1;
124 | }
125 | if (this.redirects >= 20) return Promise.reject(`Too many redirects.`)
126 | return new Promise((resolve, reject) => {
127 | try {
128 | let req = https.request(url, options, async res => {
129 | if (res.statusCode > 300 && res.statusCode < 400 && res.headers.location) await this.getLatestRelease(res.headers.location);
130 | let data = '';
131 | res.on('data', d => { data += d; });
132 | res.on('end', () => {
133 | let jdata = JSON.parse(data);
134 | if (typeof jdata.tag_name !== 'undefined')
135 | resolve(jdata.tag_name.replace('v', ''));
136 | else
137 | reject(`No data returned.`)
138 | })
139 | })
140 | .end();
141 | req.on('error', (err) => {
142 | logger.error(`Error getting Github API latest release. ${err.message}`)
143 | })
144 | }
145 | catch (err) {
146 | logger.error('Error contacting Github for latest published release: ' + err);
147 | reject(err);
148 | };
149 | })
150 | }
151 | public compare() {
152 | logger.info(`Checking njsPC versions...`);
153 | if (typeof state.appVersion.githubRelease === 'undefined' || typeof state.appVersion.installed === 'undefined') {
154 | state.appVersion.status = sys.board.valueMaps.appVersionStatus.getValue('unknown');
155 | return;
156 | }
157 | let publishedVersionArr = state.appVersion.githubRelease.split('.');
158 | let installedVersionArr = state.appVersion.installed.split('.');
159 | if (installedVersionArr.length !== publishedVersionArr.length) {
160 | // this is in case local a.b.c doesn't have same # of elements as another version a.b.c.d. We should never get here.
161 | logger.warn(`Cannot check for updated app. Version length of installed app (${installedVersionArr}) and remote (${publishedVersionArr}) do not match.`);
162 | state.appVersion.status = sys.board.valueMaps.appVersionStatus.getValue('unknown');
163 | return;
164 | } else {
165 | for (var i = 0; i < installedVersionArr.length; i++) {
166 | if (publishedVersionArr[i] > installedVersionArr[i]) {
167 | state.appVersion.status = sys.board.valueMaps.appVersionStatus.getValue('behind');
168 | return;
169 | } else if (publishedVersionArr[i] < installedVersionArr[i]) {
170 | state.appVersion.status = sys.board.valueMaps.appVersionStatus.getValue('ahead');
171 | return;
172 | }
173 | }
174 | }
175 | state.appVersion.status = sys.board.valueMaps.appVersionStatus.getValue('current');
176 | }
177 | }
178 | export var versionCheck = new VersionCheck();
--------------------------------------------------------------------------------
/controller/Errors.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import extend = require("extend");
19 | import { Message, Outbound, Inbound } from "./comms/messages/Messages";
20 | import * as path from "path";
21 |
22 | // Internal abstract class for all errors.
23 | class ApiError extends Error {
24 | constructor(message: string, code?: number, httpCode?: number) {
25 | super(message);
26 | this.name = 'ApiError';
27 | this.code = code || 0;
28 | this.httpCode = httpCode || 400;
29 | let pos: any = {};
30 | if (typeof this.stack !== 'undefined') {
31 | try {
32 | // Another weirdo decision by NodeJS to not include the line numbers and source. Only a text based stack trace.
33 | let lines = this.stack.split('\n');
34 | for (let i = 0; i < lines.length; i++) {
35 | let line = lines[i];
36 | if (line.trimLeft().startsWith('at ')) {
37 | let lastParen = line.lastIndexOf(')');
38 | let firstParen = line.indexOf('(');
39 | if (lastParen >= 0 && firstParen >= 0) {
40 | let p = line.substring(firstParen + 1, lastParen);
41 | let m = /(\:\d+\:\d+)(?!.*\1)/g;
42 | let matches = p.match(m);
43 | let linecol = '';
44 | let lastIndex = -1;
45 | if (matches.length > 0) {
46 | linecol = matches[matches.length - 1];
47 | lastIndex = p.lastIndexOf(linecol);
48 | p = p.substring(0, lastIndex);
49 | if (linecol.startsWith(':')) linecol = linecol.substring(1);
50 | let lastcolon = linecol.lastIndexOf(':');
51 | if (lastcolon !== -1) {
52 | pos.column = parseInt(linecol.substring(lastcolon + 1), 10);
53 | pos.line = parseInt(linecol.substring(0, lastcolon), 10);
54 | }
55 | }
56 | let po = path.parse(p);
57 | pos.dir = po.dir;
58 | pos.file = po.base;
59 | }
60 | break;
61 | }
62 | }
63 | } catch (e) { }
64 | }
65 | this.position = pos;
66 | }
67 | public code: number = 0;
68 | public httpCode: number = 500;
69 | public position: any = {}
70 | }
71 | class EquipmentError extends ApiError {
72 | constructor(message: string, code: number, eqType: string) {
73 | super(message, 210, 400);
74 | this.name = 'EquipmentError';
75 | this.equipmentType = eqType;
76 | }
77 | public equipmentType: string;
78 | }
79 | export class EquipmentNotFoundError extends EquipmentError {
80 | constructor(message: string, eqType: string) {
81 | super(message, 204, eqType);
82 | this.name = 'EquipmentNotFound';
83 | }
84 | }
85 | export class InvalidEquipmentIdError extends EquipmentError {
86 | constructor(message: string, id: number, eqType: string) {
87 | super(message, 250, eqType);
88 | this.name = 'InvalidEquipmentId';
89 | }
90 | public id: number;
91 | }
92 | export class InvalidEquipmentDataError extends EquipmentError {
93 | constructor(message: string, eqType: string, eqData) {
94 | super(message, 270, eqType);
95 | this.name = 'InvalidEquipmentData';
96 | this.eqData = eqData;
97 | }
98 | public eqData;
99 | }
100 | export class ServiceProcessError extends ApiError {
101 | constructor(message: string, serviceName: string, process?: string) {
102 | super(message, 290, 400);
103 | this.name = 'ServiceProcessError';
104 | this.service = serviceName;
105 | this.process = process;
106 | }
107 | public process: string;
108 | public service: string;
109 | }
110 | export class ServiceParameterError extends ApiError {
111 | constructor(message: string, serviceName: string, paramName: string, value) {
112 | super(message, 280, 400);
113 | this.name = 'InvalidServiceParameter';
114 |
115 | this.value = value;
116 | this.parameter = paramName;
117 | this.service = serviceName;
118 | }
119 | public value;
120 | public parameter: string;
121 | public service: string;
122 | }
123 | export class InvalidOperationError extends ApiError {
124 | constructor(message: string, operation: string) {
125 | super(message, 100, 400);
126 | this.name = 'InvalidOperation';
127 | this.operation = operation;
128 | }
129 | public operation: string;
130 | }
131 | export class EquipmentTimeoutError extends ApiError {
132 | constructor(message: string, operation: string) {
133 | super(message, 100, 400);
134 | this.name = 'TimeoutError';
135 | this.operation = operation;
136 | }
137 | public operation: string;
138 | }
139 | export class ParameterOutOfRangeError extends InvalidOperationError {
140 | constructor(message: string, operation: string, parameter: string, value) {
141 | super(message, operation);
142 | this.name = 'ParameterOutOfRange';
143 | this.operation = operation;
144 | }
145 | public value;
146 | public parameter: string;
147 | }
148 | export class BoardProcessError extends ApiError {
149 | constructor(message: string, process?: string) {
150 | super(message, 300, 400);
151 | this.name = 'ProcessingError';
152 | this.process = process;
153 | }
154 | public process: string;
155 |
156 | }
157 |
158 | export class MessageError extends ApiError {
159 | constructor(msg: Message, message: string, code?: number, httpCode?: number) {
160 | super(message, code, httpCode);
161 | this.name = 'MessageError';
162 | this.msg = msg;
163 | this.code = code || 500;
164 | }
165 | public msg: Message;
166 | }
167 | export class OutboundMessageError extends MessageError {
168 | constructor(msg: Outbound, message: string, code?: number, httpCode?: number) {
169 | super(msg, message, code, httpCode);
170 | this.name = 'OutboundMessageError';
171 | this.code = code || 501;
172 | }
173 | }
174 | export class InboundMessageError extends MessageError {
175 | constructor(msg: Inbound, message: string, code?: number, httpCode?: number) {
176 | super(msg, message, code, httpCode);
177 | this.name = 'InboundMessageError';
178 | this.code = code || 502;
179 | }
180 | }
181 |
182 |
--------------------------------------------------------------------------------
/controller/boards/BoardFactory.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { IntelliCenterBoard } from './IntelliCenterBoard';
19 | import { IntelliTouchBoard } from './IntelliTouchBoard';
20 | import { IntelliComBoard } from './IntelliComBoard';
21 | import { EasyTouchBoard } from './EasyTouchBoard';
22 | import { SystemBoard } from './SystemBoard';
23 | import { ControllerType } from '../Constants';
24 | import { PoolSystem } from '../Equipment';
25 | import { NixieBoard } from './NixieBoard';
26 | import { AquaLinkBoard } from './AquaLinkBoard';
27 | import { SunTouchBoard } from "./SunTouchBoard";
28 |
29 |
30 | export class BoardFactory {
31 | // Factory create the system board from the controller type. Resist storing
32 | // the pool system as this can cause a leak. The PoolSystem object already has a reference to this.
33 | public static fromControllerType(ct: ControllerType, system: PoolSystem) {
34 | switch (ct) {
35 | case ControllerType.IntelliCenter:
36 | return new IntelliCenterBoard(system);
37 | case ControllerType.IntelliTouch:
38 | return new IntelliTouchBoard(system);
39 | case ControllerType.IntelliCom:
40 | return new IntelliComBoard(system);
41 | case ControllerType.EasyTouch:
42 | return new EasyTouchBoard(system);
43 | case ControllerType.Nixie:
44 | return new NixieBoard(system);
45 | case ControllerType.AquaLink:
46 | return new AquaLinkBoard(system);
47 | case ControllerType.SunTouch:
48 | return new SunTouchBoard(system);
49 | }
50 | return new SystemBoard(system);
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/controller/boards/IntelliComBoard.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import * as extend from 'extend';
19 | import { EventEmitter } from 'events';
20 | import { EasyTouchBoard } from './EasyTouchBoard';
21 | import { sys, PoolSystem } from '../Equipment';
22 | import { byteValueMap } from './SystemBoard';
23 | import { state } from '../State';
24 |
25 | export class IntelliComBoard extends EasyTouchBoard {
26 | constructor(system: PoolSystem) {
27 | super(system); // graph chain to EasyTouchBoard constructor.
28 | this.valueMaps.expansionBoards = new byteValueMap([
29 | [11, { name: 'INTCOM2', part: 'INTCOM2', desc: 'IntelliComm II', circuits: 6, shared: true }]
30 | ]);
31 | }
32 | public initExpansionModules(byte1: number, byte2: number) {
33 | switch (byte1) {
34 | case 40: // This is a SunTouch
35 | break;
36 | }
37 | console.log(`Pentair IntelliCom System Detected!`);
38 |
39 | sys.equipment.model = 'Suntouch/Intellicom';
40 |
41 | // Initialize the installed personality board.
42 | let mt = this.valueMaps.expansionBoards.transform(0);
43 | let mod = sys.equipment.modules.getItemById(0, true);
44 | mod.name = mt.name;
45 | mod.desc = mt.desc;
46 | mod.type = byte1;
47 | mod.part = mt.part;
48 | let eq = sys.equipment;
49 | let md = mod.get();
50 | eq.maxBodies = md.bodies = typeof mt.bodies !== 'undefined' ? mt.bodies : mt.shared ? 2 : 1;
51 | eq.maxCircuits = md.circuits = typeof mt.circuits !== 'undefined' ? mt.circuits : 4;
52 | eq.maxFeatures = md.features = typeof mt.features !== 'undefined' ? mt.features : 0
53 | eq.maxValves = md.valves = typeof mt.valves !== 'undefined' ? mt.valves : 2;
54 | eq.maxPumps = md.maxPumps = typeof mt.pumps !== 'undefined' ? mt.pumps : 2;
55 | eq.shared = mt.shared;
56 | eq.dual = false;
57 | eq.single = true;
58 | eq.maxChlorinators = md.chlorinators = 1;
59 | eq.maxChemControllers = md.chemControllers = 1;
60 | eq.maxCustomNames = 10;
61 | // Calculate out the invalid ids.
62 | sys.board.equipmentIds.invalidIds.set([]);
63 | sys.board.equipmentIds.invalidIds.merge([5, 7, 8, 9, 13, 14, 15, 16, 17, 18])
64 | sys.equipment.model = mt.desc;
65 | this.initBodyDefaults();
66 | state.emitControllerChange();
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/controller/comms/messages/config/ChlorinatorMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { sys, Chlorinator } from "../../../Equipment";
19 | import { Inbound } from "../Messages";
20 | import { state } from "../../../State";
21 | import { logger } from "../../../../logger/Logger"
22 | export class ChlorinatorMessage {
23 | public static process(msg: Inbound): void {
24 | var chlorId;
25 | var chlor: Chlorinator;
26 | switch (msg.extractPayloadByte(1)) {
27 | case 0:
28 | chlorId = 1;
29 | for (let i = 0; i < 4 && i + 30 < msg.payload.length; i++) {
30 | let isActive = msg.extractPayloadByte(i + 22) === 1;
31 | chlor = sys.chlorinators.getItemById(chlorId);
32 | if (chlor.master !== 0) continue; // RSG: probably never need this. See Touch chlor below.
33 | if (isActive) {
34 | chlor = sys.chlorinators.getItemById(chlorId, true);
35 | let schlor = state.chlorinators.getItemById(chlor.id, true);
36 | chlor.isActive = schlor.isActive = true;
37 | chlor.master = 0;
38 | chlor.body = msg.extractPayloadByte(i + 2);
39 | chlor.type = msg.extractPayloadByte(i + 6);
40 | if (!chlor.disabled && !chlor.isDosing) {
41 | // RKS: We don't want to change the setpoints if our chem controller disabled
42 | // the chlorinator. These should be 0.
43 | if (msg.extractPayloadByte(i + 10) === 0 && chlor.poolSetpoint > 0) logger.info(`Changing pool setpoint to 0 ${msg.extractPayloadByte(i + 10)}`);
44 |
45 | chlor.poolSetpoint = msg.extractPayloadByte(i + 10);
46 | chlor.spaSetpoint = msg.extractPayloadByte(i + 14);
47 | }
48 | chlor.superChlor = msg.extractPayloadByte(i + 18) === 1;
49 | chlor.isActive = msg.extractPayloadByte(i + 22) === 1;
50 | chlor.superChlorHours = msg.extractPayloadByte(i + 26);
51 | chlor.address = 80 + i;
52 | schlor.body = chlor.body;
53 | schlor.poolSetpoint = chlor.poolSetpoint;
54 | schlor.spaSetpoint = chlor.spaSetpoint;
55 | schlor.type = chlor.type;
56 | schlor.model = chlor.model;
57 | schlor.isActive = chlor.isActive;
58 | schlor.superChlorHours = chlor.superChlorHours;
59 | state.emitEquipmentChanges();
60 | }
61 | else {
62 | sys.chlorinators.removeItemById(chlorId);
63 | state.chlorinators.removeItemById(chlorId);
64 | }
65 | chlorId++;
66 | }
67 | msg.isProcessed = true;
68 | break;
69 | default:
70 | logger.debug(`Unprocessed Config Message ${msg.toPacket()}`)
71 | break;
72 | }
73 | }
74 | public static processTouch(msg: Inbound) {
75 | //[255, 0, 255][165, 1, 15, 16, 25, 22][1, 90, 128, 58, 128, 0, 73, 110, 116, 101, 108, 108, 105, 99, 104, 108, 111, 114, 45, 45, 54, 48][8, 50]
76 | // This is for the 25 message that is broadcast from the OCP.
77 | let chlor = sys.chlorinators.getItemById(1);
78 | if (chlor.master !== 0 && typeof chlor.master !== 'undefined') return; // Some Aquarite chlors need more frequent control (via Nixie) but will be disabled via Touch. https://github.com/tagyoureit/nodejs-poolController/issues/349
79 | let isActive = (msg.extractPayloadByte(0) & 0x01) === 1;
80 | if (isActive) {
81 | let chlor = sys.chlorinators.getItemById(1, true);
82 | let schlor = state.chlorinators.getItemById(1, true);
83 | chlor.master = 0;
84 | chlor.isActive = schlor.isActive = isActive;
85 | if (!chlor.disabled) {
86 | // RKS: We don't want these setpoints if our chem controller disabled the
87 | // chlorinator. These should be 0 anyway.
88 | schlor.spaSetpoint = chlor.spaSetpoint = msg.extractPayloadByte(0) >> 1;
89 | schlor.poolSetpoint = chlor.poolSetpoint = msg.extractPayloadByte(1);
90 | chlor.address = msg.dest;
91 | schlor.body = chlor.body = sys.equipment.shared === true ? 32 : 0;
92 | }
93 | let name = msg.extractPayloadString(6, 16).trimEnd();
94 | if (typeof chlor.name === 'undefined') schlor.name = chlor.name = name;
95 | if (typeof chlor.model === 'undefined') {
96 | chlor.model = sys.board.valueMaps.chlorinatorModel.getValue(schlor.name.toLowerCase());
97 | if (typeof chlor.model === 'undefined') {
98 | if (name.startsWith('iChlor')) chlor.model = sys.board.valueMaps.chlorinatorModel.getValue('ichlor-ic30');
99 | }
100 | }
101 | if (typeof chlor.type === 'undefined') chlor.type = schlor.type = 0;
102 | schlor.saltLevel = msg.extractPayloadByte(3) * 50 || schlor.saltLevel;
103 | schlor.status = msg.extractPayloadByte(4) & 0x007F; // Strip off the high bit. The chlorinator does not actually report this.;
104 | // Pull the hours from the 25 message.
105 | let hours = msg.extractPayloadByte(5);
106 | // If we are not currently running a superChlor cycle this will be our initial hours so
107 | // set the superChlorHours when:
108 | // 1. We are not superChlorinating and the hours > 0
109 | // 2. We don't have any superChlor hours yet. This is when superChlorHours is undefined.
110 | if ((!schlor.superChlor && hours > 0)) {
111 | schlor.superChlorHours = chlor.superChlorHours = hours;
112 | }
113 | else if (typeof chlor.superChlorHours === 'undefined') {
114 | // The hours could be 0 because Touch doesn't persist this value out of the gate so we
115 | // will initialize this to a modest 8 hours.
116 | schlor.superChlorHours = chlor.superChlorHours = hours || 8;
117 | }
118 | schlor.superChlor = chlor.superChlor = hours > 0;
119 | if (schlor.superChlor) {
120 | schlor.superChlorRemaining = hours * 3600;
121 | }
122 | else {
123 | schlor.superChlorRemaining = 0;
124 | }
125 | if (state.temps.bodies.getItemById(1).isOn) schlor.targetOutput = chlor.disabled ? 0 : chlor.poolSetpoint;
126 | else if (state.temps.bodies.getItemById(2).isOn) schlor.targetOutput = chlor.disabled ? 0 : chlor.spaSetpoint;
127 | }
128 | else {
129 | sys.chlorinators.removeItemById(1);
130 | state.chlorinators.removeItemById(1);
131 | }
132 | msg.isProcessed = true;
133 | }
134 | }
--------------------------------------------------------------------------------
/controller/comms/messages/config/CircuitGroupMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { Inbound } from "../Messages";
19 | import { sys, CircuitGroup, LightGroup, CircuitGroupCircuit, LightGroupCircuit, ICircuitGroup, CircuitGroupCircuitCollection, ControllerType } from "../../../Equipment";
20 | import { state, CircuitGroupState, LightGroupState, ICircuitGroupState } from '../../../State';
21 | import { logger } from "../../../../logger/Logger";
22 | export class CircuitGroupMessage {
23 | private static maxCircuits: number = 16;
24 | public static process(msg: Inbound): void {
25 | if (sys.controllerType === ControllerType.IntelliTouch) {
26 | CircuitGroupMessage.processITCircuitGroups(msg);
27 | return;
28 | }
29 | let groupId;
30 | let group: ICircuitGroup;
31 | let sgroup: ICircuitGroupState;
32 | let msgId = msg.extractPayloadByte(1);
33 | switch (msgId) {
34 | case 32: // Group type for the first 16.
35 | CircuitGroupMessage.processGroupType(msg);
36 | break;
37 | case 33: // Group type for second 16.
38 | CircuitGroupMessage.processGroupType(msg);
39 | break;
40 | case 34:
41 | CircuitGroupMessage.processEggTimer(msg);
42 | break;
43 | case 35:
44 | //CircuitGroupMessage.processEggTimer(msg);
45 | //CircuitGroupMessage.processColor(msg);
46 | //break;
47 | case 36:
48 | case 37:
49 | case 38:
50 | case 39:
51 | case 40:
52 | case 41:
53 | case 42:
54 | case 43:
55 | case 44:
56 | case 45:
57 | case 46:
58 | case 47:
59 | case 48:
60 | case 49:
61 | case 50:
62 | CircuitGroupMessage.processColor(msg);
63 | break;
64 | default:
65 | if(msgId <= 31) logger.debug(`Unprocessed Config Message ${msg.toPacket()}`)
66 | break;
67 |
68 | }
69 | if (msgId <= 15) {
70 | groupId = msg.extractPayloadByte(1) + sys.board.equipmentIds.circuitGroups.start;
71 | group = sys.circuitGroups.getInterfaceById(groupId);
72 | if (group.isActive) {
73 | for (let i = 0; i < msg.payload.length && i < 15; i++) {
74 | let circuitId = msg.extractPayloadByte(i + 2) + 1;
75 | if (circuitId < 255) {
76 | let circuit = group.circuits.getItemByIndex(i, true, { id: i + 1 });
77 | circuit.circuit = circuitId;
78 | if (group.type === 1) (circuit as LightGroupCircuit).swimDelay = msg.extractPayloadByte(i + 18);
79 | }
80 | else
81 | group.circuits.removeItemByIndex(i);
82 | }
83 | }
84 | msg.isProcessed = true;
85 | }
86 | else if (msgId >= 16 && msgId <= 31) {
87 | groupId = msgId - 16 + sys.board.equipmentIds.circuitGroups.start;
88 | if (sys.board.equipmentIds.circuitGroups.isInRange) {
89 | group = sys.circuitGroups.getInterfaceById(groupId);
90 | if (group.isActive) {
91 | sgroup = group.type === 1 ? state.lightGroups.getItemById(groupId) : state.circuitGroups.getItemById(groupId);
92 | group.name = msg.extractPayloadString(2, 16);
93 | sgroup.name = group.name;
94 | }
95 | msg.isProcessed = true;
96 | }
97 | }
98 | }
99 | private static processITCircuitGroups (msg: Inbound){
100 | // [41,15],[4,0,0,0,0,0,0,0,0,192,15,0,0,0,0],[1,208]
101 | // bytes 1-7 = off circuits
102 | // bytes 8-14 = on circuits
103 |
104 | // start circuitGroup range at 192; same as IntelliCenter
105 | let groupId = msg.extractPayloadByte(0) + sys.board.equipmentIds.circuitGroups.start + 1;
106 | let _isActive = msg.payload.slice(1).reduce((accumulator, currentValue) => accumulator + currentValue) > 0;
107 | if (_isActive){
108 | let group = sys.circuitGroups.getItemById(groupId, _isActive);
109 | let sgroup: CircuitGroupState = state.circuitGroups.getItemById(group.id, true);
110 | let feature = sys.circuits.getInterfaceById(msg.extractPayloadByte(0) + sys.board.equipmentIds.features.start, true);
111 | feature.isActive = true;
112 | feature.macro = true;
113 | group.name = sgroup.name = feature.name;
114 | group.nameId = sgroup.nameId = feature.nameId;
115 | group.type = sgroup.type = sys.board.valueMaps.circuitGroupTypes.getValue('circuit');
116 | group.isActive = _isActive;
117 | if (typeof group.showInFeatures === 'undefined') group.showInFeatures = true;
118 | sgroup.showInFeatures = group.showInFeatures;
119 | let circuits: CircuitGroupCircuitCollection = group.circuits;
120 | for (let byte = 1; byte <= 7; byte++){
121 | let offByte = msg.extractPayloadByte(byte);
122 | let onByte = msg.extractPayloadByte(byte + 7);
123 | for (let bit = 1; bit < 8; bit++) {
124 | let ndx = (byte - 1) * 8 + bit;
125 | if (offByte & 1) {
126 | let circuit = circuits.getItemById(ndx, true);
127 | circuit.circuit = ndx;
128 | circuit.desiredState = 0;
129 | //circuit.desiredStateOn = false;
130 | }
131 | else if (onByte & 1) {
132 | let circuit = circuits.getItemById(ndx, true);
133 | circuit.circuit = ndx;
134 | circuit.desiredState = 1;
135 | //circuit.desiredStateOn = true;
136 | }
137 | else circuits.removeItemById(ndx);
138 | offByte = offByte >> 1;
139 | onByte = onByte >> 1;
140 | }
141 | }
142 | }
143 | else {
144 | let feature = sys.circuits.getInterfaceById(msg.extractPayloadByte(0) + sys.board.equipmentIds.features.start);
145 | feature.macro = true;
146 | sys.circuitGroups.removeItemById(groupId);
147 | state.circuitGroups.removeItemById(groupId);
148 | }
149 | state.emitEquipmentChanges();
150 | msg.isProcessed = true;
151 | }
152 | private static processGroupType(msg: Inbound) {
153 | var groupId = ((msg.extractPayloadByte(1) - 32) * 16) + sys.board.equipmentIds.circuitGroups.start;
154 | let arrlightGrps = [];
155 | let arrCircuitGrps = [];
156 | for (let i = 2; i < msg.payload.length && sys.board.equipmentIds.circuitGroups.isInRange(groupId) && i <= 18; i++) {
157 | let type = msg.extractPayloadByte(i);
158 | let group: ICircuitGroup = type === 1 ? sys.lightGroups.getItemById(groupId++, true) : sys.circuitGroups.getItemById(groupId++, type !== 0);
159 | group.type = type;
160 | group.isActive = type !== 0;
161 | if (group.isActive) {
162 | if (group.type === 1) {
163 | arrlightGrps.push(group);
164 | sys.circuitGroups.removeItemById(group.id);
165 | state.circuitGroups.removeItemById(group.id);
166 | group.lightingTheme = msg.extractPayloadByte(16 + i) >> 2;
167 | }
168 | else if (group.type === 2) {
169 | arrCircuitGrps.push(group);
170 | sys.lightGroups.removeItemById(group.id);
171 | state.lightGroups.removeItemById(group.id);
172 | }
173 | }
174 | else {
175 | state.lightGroups.removeItemById(group.id);
176 | sys.lightGroups.removeItemById(group.id);
177 | state.circuitGroups.removeItemById(group.id);
178 | sys.circuitGroups.removeItemById(group.id);
179 | }
180 | }
181 | for (let i = 0; i < arrlightGrps.length; i++) {
182 | let group: LightGroup = arrlightGrps[i];
183 | let sgroup: LightGroupState = state.lightGroups.getItemById(group.id, true);
184 | group.isActive = sgroup.isActive = true;
185 | sgroup.type = group.type;
186 | sgroup.lightingTheme = group.lightingTheme;
187 | }
188 | for (let i = 0; i < arrCircuitGrps.length; i++) {
189 | let group: CircuitGroup = arrCircuitGrps[i];
190 | let sgroup: CircuitGroupState = state.circuitGroups.getItemById(group.id, true);
191 | group.isActive = sgroup.isActive = true;
192 | if (typeof group.showInFeatures === 'undefined') group.showInFeatures = true;
193 | sgroup.type = group.type;
194 | sgroup.showInFeatures = group.showInFeatures;
195 | }
196 | state.emitEquipmentChanges();
197 | msg.isProcessed = true;
198 | }
199 | private static processColor(msg: Inbound) {
200 | var groupId = ((msg.extractPayloadByte(1) - 35)) + sys.board.equipmentIds.circuitGroups.start;
201 | var group: ICircuitGroup = sys.circuitGroups.getInterfaceById(groupId++);
202 | if (group.isActive && group.type === 1) {
203 | let lg = group as LightGroup;
204 | for (let j = 0; j < 16; j++) {
205 | let circuit = lg.circuits.getItemById(j + 1);
206 | circuit.color = msg.extractPayloadByte(j + 2);
207 | }
208 | }
209 | else if (group.isActive && group.type === 2) {
210 | let g = group as CircuitGroup;
211 | for (let j = 0; j < 16 && j < msg.payload.length; j++) {
212 | let circuit = g.circuits.getItemById(j + 1);
213 | let state = msg.extractPayloadByte(j + 18);
214 | circuit.desiredState = state !== 255 ? state : 3;
215 | }
216 |
217 | }
218 | msg.isProcessed = true;
219 | }
220 | private static processEggTimer(msg: Inbound) {
221 | var groupId = ((msg.extractPayloadByte(1) - 34) * 16) + sys.board.equipmentIds.circuitGroups.start;
222 | for (let i = 2; i < msg.payload.length && sys.board.equipmentIds.circuitGroups.isInRange(groupId); i++) {
223 | var group: ICircuitGroup = sys.circuitGroups.getInterfaceById(groupId++);
224 | if (group.isActive) {
225 | let sgroup: ICircuitGroupState = group.type === 1 ? state.lightGroups.getItemById(group.id) : state.circuitGroups.getItemById(group.id);
226 | group.eggTimer = (msg.extractPayloadByte(i) * 60) + msg.extractPayloadByte(i + 16);
227 | group.dontStop = group.eggTimer === 1440;
228 | // sgroup.eggTimer = group.eggTimer;
229 | }
230 | }
231 | msg.isProcessed = true;
232 | }
233 | }
--------------------------------------------------------------------------------
/controller/comms/messages/config/ConfigMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { Inbound } from "../Messages";
19 | import { CircuitMessage } from "./CircuitMessage";
20 | import { HeaterMessage } from "./HeaterMessage";
21 | import { FeatureMessage } from "./FeatureMessage";
22 | import { ScheduleMessage } from "./ScheduleMessage";
23 | import { PumpMessage } from "./PumpMessage";
24 | import { RemoteMessage } from "./RemoteMessage";
25 | import { CircuitGroupMessage } from "./CircuitGroupMessage";
26 | import { ChlorinatorMessage } from "./ChlorinatorMessage";
27 | import { ValveMessage } from "./ValveMessage";
28 | import { GeneralMessage } from "./GeneralMessage";
29 | import { EquipmentMessage } from "./EquipmentMessage";
30 | import { SecurityMessage } from "./SecurityMessage";
31 | import { OptionsMessage } from "./OptionsMessage";
32 | import { CoverMessage } from "./CoverMessage";
33 | import { IntellichemMessage } from "./IntellichemMessage";
34 | import { ControllerType } from "../../../Constants";
35 | import { sys } from '../../../Equipment';
36 | import { ExternalMessage } from "./ExternalMessage";
37 | import { logger } from "../../../../logger/Logger";
38 |
39 | export class ConfigMessage {
40 | // Firing up the mobi after changing settings.
41 | // 1. Asked for chlorinator config (0)
42 | // 2. Asked for features (0-20)... the whole banana except 21-22. It did not send the Acks (1 for each received packet 0-20) until it had gotten all the packets.
43 | // 3. Then it asked for features (21-22) which I didn't actually get.
44 | // 4. Then it asked for a config option [222][15][0].
45 | // Response: [165, 63, 15, 16, 30, 29][15, 0, 32, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255][3, 108]
46 | public static process(msg: Inbound): void {
47 | switch (sys.controllerType) {
48 | case ControllerType.IntelliCenter:
49 | switch (msg.extractPayloadByte(0)) {
50 | case 0:
51 | OptionsMessage.process(msg);
52 | break;
53 | case 1:
54 | CircuitMessage.processIntelliCenter(msg);
55 | break;
56 | case 2:
57 | FeatureMessage.process(msg);
58 | break;
59 | case 3:
60 | ScheduleMessage.process(msg);
61 | break;
62 | case 4:
63 | PumpMessage.process(msg);
64 | break;
65 | case 5:
66 | RemoteMessage.process(msg);
67 | break;
68 | case 6:
69 | CircuitGroupMessage.process(msg);
70 | break;
71 | case 7:
72 | ChlorinatorMessage.process(msg);
73 | break;
74 | case 8:
75 | IntellichemMessage.process(msg);
76 | break;
77 | case 9:
78 | ValveMessage.process(msg);
79 | break;
80 | case 10:
81 | HeaterMessage.process(msg);
82 | break;
83 | case 11:
84 | SecurityMessage.process(msg);
85 | break;
86 | case 12:
87 | GeneralMessage.process(msg);
88 | break;
89 | case 13:
90 | EquipmentMessage.process(msg);
91 | break;
92 | case 14:
93 | CoverMessage.process(msg);
94 | break;
95 | case 15:
96 | // Send this off to the external message processor
97 | // since it knows all that it needs to know to process the config. This
98 | // is a replica of the external 15 message.
99 | ExternalMessage.processIntelliCenterState(msg);
100 | break;
101 | default:
102 | logger.debug(`Unprocessed Config Message ${msg.toPacket()}`)
103 | break;
104 | }
105 | break;
106 | case ControllerType.EasyTouch:
107 | case ControllerType.SunTouch:
108 | case ControllerType.IntelliCom:
109 | case ControllerType.IntelliTouch:
110 | // switch (msg.action) { }
111 | break;
112 | }
113 |
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/controller/comms/messages/config/CoverMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { Inbound } from '../Messages';
19 | import { sys, Cover } from '../../../Equipment';
20 | import { logger } from "../../../../logger/Logger";
21 | export class CoverMessage {
22 | public static process(msg: Inbound): void {
23 | let cover: Cover;
24 | switch (msg.extractPayloadByte(1)) {
25 | case 0: // Cover Type
26 | case 1:
27 | cover = sys.covers.getItemById(msg.extractPayloadByte(1) + 1, true);
28 | cover.name = msg.extractPayloadString(2, 16);
29 | cover.circuits.length = 0;
30 | cover.body = msg.extractPayloadByte(1) === 0 ? 0 : 1;
31 | cover.isActive = (msg.extractPayloadByte(28) & 4) === 4;
32 | cover.normallyOn = (msg.extractPayloadByte(28) & 2) === 2;
33 | for (let i = 1; i < 10; i++) {
34 | if (msg.extractPayloadByte(i + 18) !== 255) cover.circuits.push(msg.extractPayloadByte(i + 18));
35 | }
36 | msg.isProcessed = true;
37 | break;
38 | default:
39 | logger.debug(`Unprocessed Config Message ${msg.toPacket()}`)
40 | break;
41 |
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/controller/comms/messages/config/CustomNameMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { Inbound } from "../Messages";
19 | import { sys } from "../../../Equipment";
20 | export class CustomNameMessage
21 | {
22 | public static process ( msg: Inbound ): void
23 | {
24 | let customNameId = msg.extractPayloadByte( 0 );
25 | let customName = sys.customNames.getItemById( customNameId, customNameId <= sys.equipment.maxCustomNames );
26 | customName.name = msg.extractPayloadString( 1, 11 );
27 | // customName.isActive = customNameId <= sys.equipment.maxCustomNames && !customName.name.includes('USERNAME-')
28 | if (customNameId >= sys.equipment.maxCustomNames) sys.equipment.maxCustomNames = customNameId + 1;
29 | sys.board.system.syncCustomNamesValueMap();
30 | msg.isProcessed = true;
31 | }
32 | }
--------------------------------------------------------------------------------
/controller/comms/messages/config/FeatureMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { Inbound } from "../Messages";
19 | import { sys, Feature } from "../../../Equipment";
20 | import { state, FeatureState } from "../../../State";
21 | import { logger } from "../../../../logger/Logger";
22 | export class FeatureMessage {
23 | public static process(msg: Inbound): void {
24 | switch (msg.extractPayloadByte(1)) {
25 | case 0: // Feature Type
26 | FeatureMessage.processFeatureType(msg);
27 | break;
28 | case 1: // Freeze
29 | FeatureMessage.processFreezeProtect(msg);
30 | break;
31 | case 2: // Show in features
32 | FeatureMessage.processShowInFeatures(msg);
33 | break;
34 | case 3:
35 | FeatureMessage.processEggTimerHours(msg);
36 | break;
37 | case 4:
38 | FeatureMessage.processEggTimerMinutes(msg);
39 | break;
40 | case 5:
41 | FeatureMessage.processDontStop(msg); // Don't Stop
42 | break;
43 | case 6:
44 | case 7:
45 | case 8:
46 | case 9:
47 | case 10:
48 | case 11:
49 | case 12:
50 | case 13:
51 | case 14:
52 | case 15:
53 | case 16:
54 | case 17:
55 | case 18:
56 | case 19:
57 | case 20:
58 | case 21:
59 | FeatureMessage.processFeatureNames(msg);
60 | break;
61 | case 22: // Not sure what this is.
62 | msg.isProcessed = true;
63 | break;
64 | default:
65 | logger.debug(`Unprocessed Config Message ${msg.toPacket()}`)
66 | break;
67 | }
68 | }
69 | private static processDontStop(msg: Inbound) {
70 | for (let i = 1; i < msg.payload.length - 1 && i <= sys.equipment.maxFeatures; i++) {
71 | let featureId = i + sys.board.equipmentIds.features.start - 1;
72 | var feature: Feature = sys.features.getItemById(featureId, false);
73 | feature.dontStop = msg.extractPayloadByte(i + 1) == 1;
74 | }
75 | msg.isProcessed = true;
76 | }
77 | private static processFeatureType(msg: Inbound) {
78 | for (let i = 1; i < msg.payload.length - 1 && i <= sys.equipment.maxFeatures; i++) {
79 | let featureId = i + sys.board.equipmentIds.features.start - 1;
80 | let type = msg.extractPayloadByte(i + 1);
81 | let feature: Feature = sys.features.getItemById(featureId, type !== 255);
82 | let sFeature: FeatureState = state.features.getItemById(featureId, type !== 255);
83 | if (type !== 255) {
84 | let feature: Feature = sys.features.getItemById(featureId);
85 | feature.isActive = true;
86 | sFeature.type = feature.type = type;
87 | }
88 | else {
89 | feature.isActive = false;
90 | sys.features.removeItemById(featureId);
91 | state.features.removeItemById(featureId);
92 | }
93 | }
94 | msg.isProcessed = true;
95 | }
96 | private static processFreezeProtect(msg: Inbound) {
97 | for (let i = 1; i < msg.payload.length - 1 && i <= sys.equipment.maxFeatures; i++) {
98 | let featureId = i + sys.board.equipmentIds.features.start - 1;
99 | var feature: Feature = sys.features.getItemById(featureId);
100 | feature.freeze = msg.extractPayloadByte(i + 1) > 0;
101 | }
102 | msg.isProcessed = true;
103 | }
104 | private static processFeatureNames(msg: Inbound) {
105 | var featureId = ((msg.extractPayloadByte(1) - 6) * 2) + sys.board.equipmentIds.features.start;
106 | if (sys.board.equipmentIds.features.isInRange(featureId)) {
107 | let feature: Feature = sys.features.getItemById(featureId++);
108 | feature.name = msg.extractPayloadString(2, 16);
109 | if (feature.isActive) state.features.getItemById(feature.id).name = feature.name;
110 | }
111 | if (sys.board.equipmentIds.features.isInRange(featureId)) {
112 | let feature: Feature = sys.features.getItemById(featureId++);
113 | feature.name = msg.extractPayloadString(18, 16);
114 | if (feature.isActive) state.features.getItemById(feature.id).name = feature.name;
115 | }
116 | state.emitEquipmentChanges();
117 | msg.isProcessed = true;
118 | }
119 | private static processEggTimerHours(msg: Inbound) {
120 | for (let i = 1; i < msg.payload.length - 1 && i <= sys.equipment.maxFeatures; i++) {
121 | let featureId = i + sys.board.equipmentIds.features.start - 1;
122 | let feature: Feature = sys.features.getItemById(featureId);
123 | feature.eggTimer = (msg.extractPayloadByte(i + 1) * 60) + ((feature.eggTimer || 0) % 60);
124 | }
125 | msg.isProcessed = true;
126 | }
127 | private static processEggTimerMinutes(msg: Inbound) {
128 | for (let i = 1; i < msg.payload.length - 1 && i <= sys.equipment.maxFeatures; i++) {
129 | let featureId = i + sys.board.equipmentIds.features.start - 1;
130 | var feature: Feature = sys.features.getItemById(featureId);
131 | feature.eggTimer = (Math.floor(feature.eggTimer / 60) * 60) + msg.extractPayloadByte(i + 1);
132 | }
133 | msg.isProcessed = true;
134 | }
135 | private static processShowInFeatures(msg: Inbound) {
136 | for (let i = 1; i < msg.payload.length - 1 && i <= sys.equipment.maxFeatures; i++) {
137 | let featureId = i + sys.board.equipmentIds.features.start - 1;
138 | var feature: Feature = sys.features.getItemById(featureId);
139 | feature.showInFeatures = msg.extractPayloadByte(i + 1) > 0;
140 | if (feature.isActive) state.features.getItemById(featureId, feature.isActive).showInFeatures = feature.showInFeatures;
141 | }
142 | state.emitEquipmentChanges();
143 | msg.isProcessed = true;
144 | }
145 | }
--------------------------------------------------------------------------------
/controller/comms/messages/config/GeneralMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { Inbound } from "../Messages";
19 | import { sys, General } from "../../../Equipment";
20 | import { logger } from "../../../../logger/Logger";
21 | export class GeneralMessage {
22 | public static process(msg: Inbound): void {
23 | switch (msg.extractPayloadByte(1)) {
24 | case 0:
25 | sys.general.alias = msg.extractPayloadString(2, 16);
26 | sys.general.owner.name = msg.extractPayloadString(18, 16);
27 | sys.general.location.zip = msg.extractPayloadString(34, 6);
28 | msg.isProcessed = true;
29 | break;
30 | case 1:
31 | sys.general.owner.phone = msg.extractPayloadString(2, 20);
32 | sys.general.owner.phone2 = msg.extractPayloadString(21, 15);
33 | sys.general.location.latitude = ((msg.extractPayloadByte(35) * 256) + msg.extractPayloadByte(34)) / 100;
34 | msg.isProcessed = true;
35 | break;
36 | case 2:
37 | sys.general.location.address = msg.extractPayloadString(2, 32);
38 | sys.general.location.longitude = -(((msg.extractPayloadByte(35) * 256) + msg.extractPayloadByte(34)) / 100);
39 | msg.isProcessed = true;
40 | break;
41 | case 3:
42 | sys.general.owner.email = msg.extractPayloadString(2, 32);
43 | sys.general.location.timeZone = msg.extractPayloadByte(34);
44 | msg.isProcessed = true;
45 | break;
46 | case 4:
47 | sys.general.owner.email2 = msg.extractPayloadString(2, 32);
48 | msg.isProcessed = true;
49 | break;
50 | case 5:
51 | sys.general.location.country = msg.extractPayloadString(2, 32);
52 | msg.isProcessed = true;
53 | break;
54 | case 6:
55 | sys.general.location.city = msg.extractPayloadString(2, 32);
56 | msg.isProcessed = true;
57 | break;
58 | case 7:
59 | sys.general.location.state = msg.extractPayloadString(2, 32);
60 | msg.isProcessed = true;
61 | break;
62 | default:
63 | logger.debug(`Unprocessed Config Message ${msg.toPacket()}`)
64 | break;
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/controller/comms/messages/config/OptionsMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { Inbound } from "../Messages";
19 | import { sys } from "../../../Equipment";
20 | import { state } from "../../../State";
21 | import { ControllerType } from "../../../Constants";
22 | export class OptionsMessage {
23 | public static process(msg: Inbound): void {
24 | switch (sys.controllerType) {
25 | case ControllerType.IntelliCenter:
26 | OptionsMessage.processIntelliCenter(msg);
27 | break;
28 | case ControllerType.IntelliCom:
29 | case ControllerType.SunTouch:
30 | case ControllerType.EasyTouch:
31 | case ControllerType.IntelliTouch:
32 | OptionsMessage.processIntelliTouch(msg);
33 | break;
34 | }
35 | }
36 | private static processIntelliCenter(msg: Inbound) {
37 | switch (msg.action) {
38 | case 30:
39 | switch (msg.extractPayloadByte(1)) {
40 | case 0:
41 | {
42 | if ((msg.extractPayloadByte(13) & 32) === 32)
43 | sys.general.options.clockSource = 'internet';
44 | else if (sys.general.options.clockSource !== 'server')
45 | sys.general.options.clockSource = 'manual';
46 | sys.general.options.clockMode = (msg.extractPayloadByte(13) & 64) === 64 ? 24 : 12;
47 | if (sys.general.options.clockSource !== 'server' || typeof sys.general.options.adjustDST === 'undefined') sys.general.options.adjustDST = (msg.extractPayloadByte(13) & 128) === 128;
48 | // No pumpDelay
49 | //[255, 0, 255][165, 63, 15, 16, 30, 40][0, 0, 1, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 149, 29, 35, 3, 0, 0, 92, 81, 91, 81, 3, 3, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0][4, 193]
50 | // pumpDelay
51 | //[255, 0, 255][165, 63, 15, 16, 30, 40][0, 0, 1, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 149, 29, 35, 3, 0, 0, 92, 81, 91, 81, 3, 3, 0, 0, 15, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0][4, 194]
52 | sys.general.options.pumpDelay = msg.extractPayloadByte(29) === 1;
53 | // No cooldownDelay
54 | //[255, 0, 255][165, 63, 15, 16, 30, 40][0, 0, 1, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 149, 29, 35, 3, 0, 0, 92, 81, 91, 81, 3, 3, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0][4, 193]
55 | // cooldownDelay
56 | //[255, 0, 255][165, 63, 15, 16, 30, 40][0, 0, 1, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 149, 29, 35, 3, 0, 0, 92, 81, 91, 81, 3, 3, 0, 0, 15, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0][4, 194]
57 | sys.general.options.cooldownDelay = msg.extractPayloadByte(30) === 1;
58 | sys.general.options.manualPriority = msg.extractPayloadByte(38) === 1;
59 | sys.general.options.manualHeat = msg.extractPayloadByte(39) === 1;
60 | let fnTranslateByte = (byte):number => { return (byte & 0x007F) * (((byte & 0x0080) > 0) ? -1 : 1); }
61 | sys.equipment.tempSensors.setCalibration('water1', fnTranslateByte(msg.extractPayloadByte(3)));
62 | sys.equipment.tempSensors.setCalibration('solar1', fnTranslateByte(msg.extractPayloadByte(4)));
63 | sys.equipment.tempSensors.setCalibration('air', fnTranslateByte(msg.extractPayloadByte(5)));
64 | sys.equipment.tempSensors.setCalibration('water2', fnTranslateByte(msg.extractPayloadByte(6)));
65 | sys.equipment.tempSensors.setCalibration('solar2', fnTranslateByte(msg.extractPayloadByte(7)));
66 | sys.equipment.tempSensors.setCalibration('water3', fnTranslateByte(msg.extractPayloadByte(8)));
67 | sys.equipment.tempSensors.setCalibration('solar3', fnTranslateByte(msg.extractPayloadByte(9)));
68 | sys.equipment.tempSensors.setCalibration('water4', fnTranslateByte(msg.extractPayloadByte(10)));
69 | sys.equipment.tempSensors.setCalibration('solar4', fnTranslateByte(msg.extractPayloadByte(11)));
70 |
71 | // When we complete our transition for the calibration make this go away.
72 | //sys.general.options.waterTempAdj2 = (msg.extractPayloadByte(2) & 0x007F) * (((msg.extractPayloadByte(2) & 0x0080) > 0) ? -1 : 1);
73 | //sys.general.options.waterTempAdj1 = (msg.extractPayloadByte(3) & 0x007F) * (((msg.extractPayloadByte(3) & 0x0080) > 0) ? -1 : 1);
74 | //sys.general.options.solarTempAdj1 = (msg.extractPayloadByte(4) & 0x007F) * (((msg.extractPayloadByte(4) & 0x0080) > 0) ? -1 : 1);
75 | //sys.general.options.airTempAdj = (msg.extractPayloadByte(5) & 0x007F) * (((msg.extractPayloadByte(5) & 0x0080) > 0) ? -1 : 1);
76 | //sys.general.options.waterTempAdj2 = (msg.extractPayloadByte(6) & 0x007F) * (((msg.extractPayloadByte(6) & 0x0080) > 0) ? -1 : 1);
77 |
78 | // Somewhere in here are the units.
79 |
80 | let body = sys.bodies.getItemById(1, sys.equipment.maxBodies > 0);
81 | body.heatMode = msg.extractPayloadByte(24);
82 | body.heatSetpoint = msg.extractPayloadByte(20);
83 | body.coolSetpoint = msg.extractPayloadByte(21);
84 |
85 | body = sys.bodies.getItemById(2, sys.equipment.maxBodies > 1);
86 | body.heatMode = msg.extractPayloadByte(25);
87 | body.heatSetpoint = msg.extractPayloadByte(22);
88 | body.coolSetpoint = msg.extractPayloadByte(23);
89 |
90 | //body = sys.bodies.getItemById(3, sys.equipment.maxBodies > 2);
91 | //body.heatMode = msg.extractPayloadByte(26);
92 | //body.heatSetpoint = msg.extractPayloadByte(21);
93 | //body.manualHeat = sys.general.options.manualHeat;
94 | //body = sys.bodies.getItemById(4, sys.equipment.maxBodies > 3);
95 | //body.heatMode = msg.extractPayloadByte(27);
96 | //body.heatSetpoint = msg.extractPayloadByte(23);
97 | msg.isProcessed = true;
98 | break;
99 | }
100 | case 1: // Vacation mode
101 | let yy = msg.extractPayloadByte(4) + 2000;
102 | let mm = msg.extractPayloadByte(5);
103 | let dd = msg.extractPayloadByte(6);
104 | sys.general.options.vacation.startDate = new Date(yy, mm - 1, dd);
105 | yy = msg.extractPayloadByte(7) + 2000;
106 | mm = msg.extractPayloadByte(8);
107 | dd = msg.extractPayloadByte(9);
108 | sys.general.options.vacation.endDate = new Date(yy, mm - 1, dd);
109 | sys.general.options.vacation.enabled = msg.extractPayloadByte(2) > 0;
110 | sys.general.options.vacation.useTimeframe = msg.extractPayloadByte(3) > 0;
111 | msg.isProcessed = true;
112 | break;
113 | }
114 | msg.isProcessed = true;
115 | break;
116 | }
117 | }
118 | private static processIntelliTouch(msg: Inbound) {
119 | switch (msg.action) {
120 | case 30: {
121 | // sample packet
122 | // [165,33,15,16,30,16],[4,9,16,0,1,72,0,0,16,205,0,0,0,2,0,0],[2,88]
123 | // this is (I believe) to assign circuits that require high speed mode with a dual speed pump
124 |
125 | // We don't want the dual speed pump to even exist unless there are no circuit controlling it.
126 | // It should not be showing up in our pumps list or emitting state unless the user has defined
127 | // circuits to it on *Touch interfaces.
128 | // RSG 1/5/23 - Intellitouch (and Dual Body) accept 8 high speed circuits
129 | let maxCircuits = sys.controllerType === ControllerType.IntelliTouch ? 8 : 4;
130 | let arrCircuits = [];
131 | let pump = sys.pumps.getDualSpeed(true);
132 | for (let i = 0; i < maxCircuits; i++) {
133 | let val = msg.extractPayloadByte(i);
134 | if (val > 0) arrCircuits.push(val);
135 | else pump.circuits.removeItemById(i);
136 | }
137 | if (arrCircuits.length > 0) {
138 | let pump = sys.pumps.getDualSpeed(true);
139 | for (let j = 1; j <= arrCircuits.length; j++) pump.circuits.getItemById(j, true).circuit = arrCircuits[j-1];
140 | }
141 | else sys.pumps.removeItemById(10);
142 | msg.isProcessed = true;
143 | break;
144 | }
145 | case 40:
146 | case 168:
147 | {
148 |
149 | // [165,33,16,34,168,10],[0,0,0,254,0,0,0,0,0,0],[2,168 = manual heat mode off
150 | // [165,33,16,34,168,10],[0,0,0,254,1,0,0,0,0,0],[2,169] = manual heat mode on
151 | sys.general.options.manualHeat = msg.extractPayloadByte(4) === 1;
152 | // From https://github.com/tagyoureit/nodejs-poolController/issues/362 = Intellitouch
153 | // [0,0,0,0,1,x,0,0,0,0] x=0 Manual OP heat Off; x=1 Manual OP heat On
154 | sys.general.options.manualPriority = msg.extractPayloadByte(5) === 1;
155 | if ((msg.extractPayloadByte(3) & 0x01) === 1) {
156 | // only support for 1 ic with EasyTouch
157 | let chem = sys.chemControllers.getItemByAddress(144, true);
158 | //let schem = state.chemControllers.getItemById(chem.id, true);
159 | chem.ph.tank.capacity = chem.orp.tank.capacity = 6;
160 | chem.ph.tank.units = chem.orp.tank.units = '';
161 |
162 | }
163 | else {
164 | if (sys.controllerType !== ControllerType.SunTouch) {
165 | let chem = sys.chemControllers.getItemByAddress(144);
166 | state.chemControllers.removeItemById(chem.id);
167 | sys.chemControllers.removeItemById(chem.id);
168 | }
169 | }
170 | msg.isProcessed = true;
171 | break;
172 | }
173 | }
174 | }
175 | }
--------------------------------------------------------------------------------
/controller/comms/messages/config/SecurityMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { Inbound } from "../Messages";
19 | import { sys, SecurityRole } from "../../../Equipment";
20 |
21 | export class SecurityMessage {
22 | public static process(msg: Inbound): void {
23 | var role: SecurityRole;
24 | switch (msg.extractPayloadByte(1)) {
25 | case 0:
26 | sys.security.enabled = (msg.extractPayloadByte(3) & 1) === 1;
27 | sys.security.roles.clear();
28 | break;
29 | }
30 | if ((msg.extractPayloadByte(3) & 2) === 2) {
31 | role = sys.security.roles.getItemById(msg.extractPayloadByte(1) + 1, true);
32 | role.name = msg.extractPayloadString(4, 16);
33 | role.timeout = msg.extractPayloadByte(20);
34 | role.flag1 = msg.extractPayloadByte(2);
35 | role.flag2 = msg.extractPayloadByte(3);
36 | role.pin = msg.extractPayloadByte(21).toString() + msg.extractPayloadByte(22).toString() + msg.extractPayloadByte(23).toString() + msg.extractPayloadByte(24).toString();
37 | }
38 | msg.isProcessed = true;
39 | }
40 | }
--------------------------------------------------------------------------------
/controller/comms/messages/status/HeaterStateMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { Inbound, Protocol } from "../Messages";
19 | import { state, BodyTempState, HeaterState } from "../../../State";
20 | import { sys, ControllerType, Heater } from "../../../Equipment";
21 |
22 | export class HeaterStateMessage {
23 | public static process(msg: Inbound) {
24 | if (msg.protocol === Protocol.Heater) {
25 | switch (msg.action) {
26 | case 112: // This is a message from a master controlling MasterTemp or UltraTemp ETi
27 | break;
28 | case 114: // This is a message from a master controlling UltraTemp
29 | msg.isProcessed = true;
30 | break;
31 | case 113:
32 | HeaterStateMessage.processHybridStatus(msg);
33 | break;
34 | case 116:
35 | HeaterStateMessage.processMasterTempStatus(msg);
36 | break;
37 | case 115:
38 | HeaterStateMessage.processUltraTempStatus(msg);
39 | break;
40 | }
41 | }
42 | }
43 | public static processHeaterCommand(msg: Inbound) {
44 | let heater: Heater = sys.heaters.getItemByAddress(msg.source);
45 | // At this point there is no other configuration data for ET
46 | if (sys.controllerType === ControllerType.EasyTouch) {
47 | let htype = sys.board.valueMaps.heaterTypes.transform(heater.type);
48 | switch (htype.name) {
49 | case 'hybrid':
50 | heater.economyTime = msg.extractPayloadByte(3);
51 | heater.maxBoostTemp = msg.extractPayloadByte(4);
52 | break;
53 | }
54 | }
55 | }
56 | public static processHybridStatus(msg: Inbound) {
57 | //[165, 0, 16, 112, 113, 10][1, 1, 0, 0, 0, 0, 0, 0, 0, 0][1, 162]
58 | let heater: Heater = sys.heaters.getItemByAddress(msg.source);
59 | let sheater = state.heaters.getItemById(heater.id);
60 | sheater.isOn = msg.extractPayloadByte(0) > 0;
61 | if (heater.master > 0) {
62 | let sbody = sheater.bodyId > 0 ? state.temps.bodies.getItemById(sheater.bodyId) : undefined;
63 | if (typeof sbody !== 'undefined') {
64 | switch (msg.extractPayloadByte(1)) {
65 | case 1:
66 | sbody.heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
67 | break;
68 | case 2:
69 | sbody.heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
70 | break;
71 | case 3:
72 | sbody.heatStatus = sys.board.valueMaps.heatStatus.getValue('dual');
73 | break;
74 | case 4:
75 | sbody.heatStatus = sys.board.valueMaps.heatStatus.getValue('dual');
76 | break;
77 | default:
78 | sbody.heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
79 | break;
80 | }
81 | }
82 | }
83 | sheater.commStatus = 0;
84 | state.equipment.messages.removeItemByCode(`heater:${heater.id}:comms`);
85 | msg.isProcessed = true;
86 | }
87 | public static processUltraTempStatus(msg: Inbound) {
88 | // RKS: 07-03-21 - We only know byte 2 at this point for Ultratemp for the 115 message we are processing here. The
89 | // byte description
90 | // ------------------------------------------------
91 | // 0 Unknown (always seems to be 160 for response)
92 | // 1 Unknown (always 1)
93 | // 2 Current heater status 0=off, 1=heat, 2=cool
94 | // 3-9 Unknown
95 |
96 | // 114 message - outbound response
97 | //[165, 0, 112, 16, 114, 10][144, 0, 0, 0, 0, 0, 0, 0, 0, 0][2, 49] // OCP to Heater
98 | // byte description
99 | // ------------------------------------------------
100 | // 0 Unknown (always seems to be 144 for request)
101 | // 1 Current heater status 0=off, 1=heat, 2=cool
102 | // 3 Believed to be ofset temp
103 | // 4-9 Unknown
104 |
105 | // byto 0: always seems to be 144 for outbound
106 | // byte 1: Sets heater mode to 0 = Off 1 = Heat 2 = Cool
107 | //[165, 0, 16, 112, 115, 10][160, 1, 0, 3, 0, 0, 0, 0, 0, 0][2, 70] // Heater Reply
108 | let heater: Heater = sys.heaters.getItemByAddress(msg.source);
109 | let sheater = state.heaters.getItemById(heater.id);
110 | let byte = msg.extractPayloadByte(2);
111 | sheater.isOn = byte >= 1;
112 | sheater.isCooling = byte === 2;
113 | sheater.commStatus = 0;
114 | state.equipment.messages.removeItemByCode(`heater:${heater.id}:comms`);
115 | msg.isProcessed = true;
116 | }
117 | public static processMasterTempStatus(msg: Inbound) {
118 | //[255, 0, 255][165, 0, 16, 112, 116, 23][67, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 10, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0][2, 66]
119 | // Byte 1 is the indicator to which setpoint it is heating to.
120 | // Byte 8 increments over time when the heater is on.
121 | // Byte 13 looks like the mode the heater is in for instance it is in cooldown mode.
122 | // 0 = Normal
123 | // 2 = ??????
124 | // 6 = Cooldown
125 | // Byte 14 looks like the cooldown delay in minutes.
126 | let heater: Heater = sys.heaters.getItemByAddress(msg.source);
127 | let sheater = state.heaters.getItemById(heater.id);
128 | let byte = msg.extractPayloadByte(1);
129 | sheater.isOn = byte >= 1;
130 | sheater.isCooling = false;
131 | sheater.commStatus = 0;
132 | state.equipment.messages.removeItemByCode(`heater:${heater.id}:comms`);
133 | msg.isProcessed = true;
134 | }
135 |
136 | }
--------------------------------------------------------------------------------
/controller/comms/messages/status/IntelliValveStateMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { Inbound } from "../Messages";
19 | import { state } from "../../../State";
20 | import { sys, ControllerType } from "../../../Equipment";
21 | import { logger } from "../../../../logger/Logger";
22 |
23 | export class IntelliValveStateMessage {
24 | public static process(msg: Inbound) {
25 | if (sys.controllerType === ControllerType.Unknown) return;
26 | // We only want to process the messages that are coming from IntelliValve.
27 | if (msg.source !== 12) return;
28 | switch (msg.action) {
29 | case 82: // This is hail from the valve that says it is not bound yet.
30 | break;
31 | default:
32 | logger.info(`IntelliValve sent an unknown action ${msg.action}`);
33 | break;
34 | }
35 | state.emitEquipmentChanges();
36 | }
37 | }
--------------------------------------------------------------------------------
/controller/comms/messages/status/VersionMessage.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { Inbound } from "../Messages";
19 | import { sys, ConfigVersion } from "../../../Equipment";
20 | export class VersionMessage {
21 | public static process(msg: Inbound): void {
22 | var ver: ConfigVersion = new ConfigVersion({});
23 | ver.options = msg.extractPayloadInt(6);
24 | ver.circuits = msg.extractPayloadInt(8);
25 | ver.features = msg.extractPayloadInt(10);
26 | ver.schedules = msg.extractPayloadInt(12);
27 | ver.pumps = msg.extractPayloadInt(14);
28 | ver.remotes = msg.extractPayloadInt(16);
29 | ver.circuitGroups = msg.extractPayloadInt(18);
30 | ver.chlorinators = msg.extractPayloadInt(20);
31 | ver.intellichem = msg.extractPayloadInt(22);
32 | ver.valves = msg.extractPayloadInt(24);
33 | ver.heaters = msg.extractPayloadInt(26);
34 | ver.security = msg.extractPayloadInt(28);
35 | ver.general = msg.extractPayloadInt(30);
36 | ver.equipment = msg.extractPayloadInt(32);
37 | ver.covers = msg.extractPayloadInt(34);
38 | ver.systemState = msg.extractPayloadInt(36);
39 | sys.processVersionChanges(ver);
40 | msg.isProcessed = true;
41 | }
42 | }
--------------------------------------------------------------------------------
/controller/nixie/Nixie.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as fs from 'fs';
3 | import * as os from 'os';
4 | import { webApp } from "../../web/Server";
5 | import { logger } from "../../logger/Logger";
6 | import { INixieControlPanel } from "./NixieEquipment";
7 | import { NixieChemControllerCollection } from "./chemistry/ChemController";
8 | import { NixieChemDoserCollection } from "./chemistry/ChemDoser";
9 |
10 | import { sys, PoolSystem } from "../../controller/Equipment";
11 | import { NixieCircuitCollection } from './circuits/Circuit';
12 | import { NixieBodyCollection } from './bodies/Body';
13 | import { NixieValveCollection } from './valves/Valve';
14 | import { NixieHeaterCollection } from './heaters/Heater';
15 | import { config } from '../../config/Config';
16 | import { NixieFilterCollection } from './bodies/Filter';
17 | import { NixieChlorinatorCollection } from './chemistry/Chlorinator';
18 | import { NixiePump, NixiePumpCollection } from './pumps/Pump';
19 | import { NixieScheduleCollection } from './schedules/Schedule';
20 |
21 | /************************************************************************
22 | * Nixie: Nixie is a control panel that controls devices as a master. It
23 | * can extend an existing *Touch or Center control panel. Or it can run
24 | * standalone as a master controller. The NixieControlPanel will always
25 | * be instantiated to control equipment when that equipment is not
26 | * supported or controlled by a traditional OCP. However, if a traditional
27 | * OCP exists, it will be subordinate to that OCP in regard to controlling
28 | * pool bodies.
29 | *
30 | * Equipment: Equipment identified as ncp (Nixie Control Panel) will be
31 | * managed by the Nixie controller. It works hand-in-glove with the REM
32 | * (Relay Equipment Manager) to provide low level hardware support.
33 | *
34 | * RS485: RS485 occurs directly within nixie through the standard RS485
35 | * bus identified for njspc and does not route through REM. This is an
36 | * important distinction to provide robust hardware level communication.
37 | * Items Controlled on this channel include: Pumps, Chlorinators, and
38 | * IntelliChem controllers. When and if IntelliValve functions become
39 | * available these too will be performed through the njspc RS485 bus.
40 | *
41 | * Configuration and State Management:
42 | * Nixie uses the underlying njspc configuration structures. The intent
43 | * is not to replace these rather the intention is to extend them with
44 | * low level control. For this reason, only when a Nixie is the only
45 | * master controller on the pool do commands get routed through NixieBoard.
46 | * this mode is identified at startup. When a particular piece of equipment
47 | * is identified to be controlled by Nixie as a master then the command
48 | * will eventually be marshalled to the NixieControlPanel. Any connected
49 | * OCP will get first crack at it.
50 | *
51 | * LifeCycle: The Nixie controller does not persist data outside of
52 | * the PoolConfig.json and PoolState.json files. It is initialized
53 | * at startup and torn down when the application is stopped.
54 | * */
55 | export class NixieControlPanel implements INixieControlPanel {
56 | // Only equipment controlled by Nixie is represented on the controller. If interaction with
57 | // other equipment is required this should be sent back through the original controller.
58 | // Command sequence is Board -> SystemBoard -> NixieController whenever the master is not identified as Nixie.
59 | chemDosers: NixieChemDoserCollection = new NixieChemDoserCollection(this);
60 | chemControllers: NixieChemControllerCollection = new NixieChemControllerCollection(this);
61 | chlorinators: NixieChlorinatorCollection = new NixieChlorinatorCollection(this);
62 | circuits: NixieCircuitCollection = new NixieCircuitCollection(this);
63 | bodies: NixieBodyCollection = new NixieBodyCollection(this);
64 | filters: NixieFilterCollection = new NixieFilterCollection(this);
65 | valves: NixieValveCollection = new NixieValveCollection(this);
66 | heaters: NixieHeaterCollection = new NixieHeaterCollection(this);
67 | pumps: NixiePumpCollection = new NixiePumpCollection(this);
68 | schedules: NixieScheduleCollection = new NixieScheduleCollection(this);
69 | public async setServiceModeAsync() {
70 | await this.circuits.setServiceModeAsync();
71 | await this.heaters.setServiceModeAsync();
72 | await this.chlorinators.setServiceModeAsync();
73 | await this.chemControllers.setServiceModeAsync();
74 | await this.chemDosers.setServiceModeAsync();
75 | await this.pumps.setServiceModeAsync();
76 | }
77 | public async initAsync(equipment: PoolSystem) {
78 | try {
79 |
80 | // We need to tell Nixie what her place is. If there is an existing OCP she needs to be a partner. However, if
81 | // she is the only master then she needs to step up and take command. The way we will signify this is
82 | // by using settings in config.json.
83 | // The controller types define the number of bodies and whether they are shared.
84 | logger.info(`Initializing Nixie Controller`);
85 | await this.bodies.initAsync(equipment.bodies);
86 | await this.filters.initAsync(equipment.filters);
87 | await this.circuits.initAsync(equipment.circuits);
88 | await this.valves.initAsync(equipment.valves);
89 | await this.heaters.initAsync(equipment.heaters);
90 | await this.chlorinators.initAsync(equipment.chlorinators);
91 | await this.chemControllers.initAsync(equipment.chemControllers);
92 | await this.chemDosers.initAsync(equipment.chemDosers);
93 | await this.pumps.initAsync(equipment.pumps);
94 | await this.schedules.initAsync(equipment.schedules);
95 | logger.info(`Nixie Controller Initialized`)
96 | }
97 | catch (err) { return Promise.reject(err); }
98 | }
99 | public async readLogFile(logFile: string): Promise {
100 | try {
101 | let logPath = path.join(process.cwd(), '/logs');
102 | if (!fs.existsSync(logPath)) fs.mkdirSync(logPath);
103 | logPath += (`/${logFile}`);
104 | let lines = [];
105 | if (fs.existsSync(logPath)) {
106 | let buff = fs.readFileSync(logPath);
107 | lines = buff.toString().split('\n');
108 | }
109 | return lines;
110 | } catch (err) { logger.error(`Error reading log file ${logFile}: ${err.message}`); }
111 | }
112 | public async logData(logFile: string, data: any) {
113 | try {
114 | let logPath = path.join(process.cwd(), '/logs');
115 | if (!fs.existsSync(logPath)) fs.mkdirSync(logPath);
116 | logPath += (`/${logFile}`);
117 | let lines = [];
118 | if (fs.existsSync(logPath)) {
119 | let buff = fs.readFileSync(logPath);
120 | lines = buff.toString().split('\n');
121 | }
122 | if (typeof data === 'object')
123 | lines.unshift(JSON.stringify(data));
124 | else
125 | lines.unshift(data.toString());
126 | fs.writeFileSync(logPath, lines.join('\n'));
127 | } catch (err) { logger.error(`Error logging to ${logFile}: ${err.message}`); }
128 | }
129 | public async closeAsync() {
130 | // Close all the associated equipment.
131 | await this.chemDosers.closeAsync();
132 | await this.chemControllers.closeAsync();
133 | await this.chlorinators.closeAsync();
134 | await this.heaters.closeAsync();
135 | await this.circuits.closeAsync();
136 | await this.pumps.closeAsync();
137 | await this.filters.closeAsync();
138 | await this.bodies.closeAsync();
139 | await this.valves.closeAsync();
140 | }
141 | /*
142 | * This method is used to obtain a list of existing REM servers for configuration. This returns all servers and
143 | * their potential devices and is not designed to be used at run-time or to detect failure.
144 | *
145 | */
146 | public async getREMServers() {
147 | try {
148 | let srv = [];
149 | let servers = webApp.findServersByType('rem');
150 | if (typeof servers !== 'undefined') {
151 | for (let i = 0; i < servers.length; i++) {
152 | let server = servers[i];
153 | // Sometimes I hate type safety.
154 | let devices = typeof server['getDevices'] === 'function' ? await server['getDevices']() : [];
155 | let int = config.getInterfaceByUuid(servers[i].uuid);
156 | srv.push({
157 | uuid: servers[i].uuid,
158 | name: servers[i].name,
159 | type: servers[i].type,
160 | isRunning: servers[i].isRunning,
161 | isConnected: servers[i].isConnected,
162 | devices: devices,
163 | remoteConnectionId: servers[i].remoteConnectionId,
164 | interface: int
165 | });
166 | }
167 | await ncp.chemControllers.syncRemoteREMFeeds(srv);
168 | }
169 | return srv;
170 | } catch (err) { logger.error(`Error gettting REM Servers: ${err.message}`); }
171 | }
172 | }
173 |
174 | export let ncp = new NixieControlPanel();
--------------------------------------------------------------------------------
/controller/nixie/NixieEquipment.ts:
--------------------------------------------------------------------------------
1 | import { webApp, REMInterfaceServer, InterfaceServerResponse } from "../../web/Server";
2 | import { logger } from "../../logger/Logger";
3 | import e = require("express");
4 | export interface INixieControlPanel {
5 | getREMServers();
6 | logData(file: string, data: any);
7 | readLogFile(file: string);
8 | }
9 | export class NixieEquipment {
10 | protected _pmap = new WeakSet();
11 | //private _dataKey = { id: 'parent' };
12 | constructor(ncp: INixieControlPanel) { this._pmap['ncp'] = ncp; }
13 | public get controlPanel(): INixieControlPanel { return this._pmap['ncp']; }
14 | public get id(): number { return -1; }
15 | public static get isConnected(): boolean {
16 | let servers = webApp.findServersByType("rem");
17 | for (let i = 0; i < servers.length; i++) {
18 | if (!servers[0].isConnected) return false;
19 | }
20 | return true;
21 | }
22 | public static async putDeviceService(uuid: string, url: string, data?: any, timeout: number = 3600): Promise {
23 | try {
24 | let result: InterfaceServerResponse;
25 | let server = webApp.findServerByGuid(uuid);
26 | if (typeof server === 'undefined')
27 | return InterfaceServerResponse.createError(new Error(`Error sending device command: Server [${uuid}] not found.`));
28 | if (!server.isConnected) {
29 | logger.warn(`Cannot send PUT ${url} to ${server.name} server is not connected.`);
30 | return InterfaceServerResponse.createError(new Error(`Error sending device command: [${server.name}] not connected.`));
31 | }
32 | if (server.type === 'rem') {
33 | let rem = server as REMInterfaceServer;
34 | result = await rem.putApiService(url, data, timeout);
35 | // If the result code is > 200 we have an issue.
36 | //if (result.status.code > 200 || result.status.code === -1)
37 | // return Promise.reject(new Error(`putDeviceService: ${result.error.message}`));
38 | }
39 | return result;
40 | }
41 | catch (err) { return Promise.reject(err); }
42 | }
43 | public static async getDeviceService(uuid: string, url: string, data?: any, timeout:number = 3600): Promise {
44 | try {
45 | let result: InterfaceServerResponse;
46 | let server = webApp.findServerByGuid(uuid);
47 | if (typeof server === 'undefined') return Promise.reject(new Error(`Error sending device command: Server [${uuid}] not found.`));
48 | if (!server.isConnected) return Promise.reject(new Error(`Error sending device command: [${server.name}] not connected.`));
49 | if (server.type === 'rem') {
50 | let rem = server as REMInterfaceServer;
51 | //console.log(`CALLING GET FROM GETDEVSER`);
52 |
53 | result = await rem.getApiService(url, data, timeout);
54 | //console.log(`RETURNING GET FROM GETDEVSER`);
55 | // If the result code is > 200 we have an issue.
56 | if (result.status.code > 200) return Promise.reject(new Error(`putDeviceService: ${result.error.message}`));
57 | }
58 | return result;
59 | }
60 | catch (err) { return Promise.reject(err); }
61 | }
62 | public async closeAsync() {
63 | try {
64 | }
65 | catch (err) { logger.error(`Error closing Nixie Equipment: ${err.message}`); }
66 | }
67 | }
68 | export class NixieChildEquipment extends NixieEquipment {
69 | constructor(parent: NixieEquipment) {
70 | super(parent.controlPanel);
71 | this._pmap['parent'] = parent;
72 | }
73 | public getParent(): NixieEquipment { return this._pmap['parent']; }
74 | }
75 | export class NixieEquipmentCollection extends Array {
76 | private _pmap = new WeakSet();
77 | public get controlPanel(): INixieControlPanel { return this._pmap['ncp']; }
78 | constructor(ncp: INixieControlPanel) {
79 | super();
80 | this._pmap['ncp'] = ncp;
81 | }
82 | public async removeById(id: number) {
83 | try {
84 | let ndx = this.findIndex(elem => elem.id === id);
85 | if (ndx >= 0) {
86 | let eq = this[ndx];
87 | await eq.closeAsync();
88 | this.splice(ndx, 1);
89 | logger.info(`Removing chem doser id# ${id} at index ndx`);
90 | }
91 | else
92 | logger.warn(`A Nixie equipment item was not found with id ${id}. Equipment not removed.`);
93 | }
94 | catch (err) { return Promise.reject(err); }
95 | }
96 | }
97 | //export class NixieRelay extends NixieEquipment {
98 |
99 | //}
100 | //export class NixieCircuit extends NixieRelay {
101 |
102 | //}
103 | //export class NixieValve extends NixieRelay {
104 |
105 | //}
--------------------------------------------------------------------------------
/controller/nixie/bodies/Body.ts:
--------------------------------------------------------------------------------
1 | import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, ParameterOutOfRangeError } from '../../Errors';
2 | import { utils, Timestamp } from '../../Constants';
3 | import { logger } from '../../../logger/Logger';
4 |
5 | import { NixieEquipment, NixieChildEquipment, NixieEquipmentCollection, INixieControlPanel } from "../NixieEquipment";
6 | import { Body, BodyCollection, sys } from "../../../controller/Equipment";
7 | import { BodyTempState, state, } from "../../State";
8 | import { setTimeout, clearTimeout } from 'timers';
9 | import { NixieControlPanel } from '../Nixie';
10 | import { webApp, InterfaceServerResponse } from "../../../web/Server";
11 |
12 | export class NixieBodyCollection extends NixieEquipmentCollection {
13 | public async setBodyAsync(body: Body, data: any) {
14 | // By the time we get here we know that we are in control and this is a REMChem.
15 | try {
16 | let c: NixieBody = this.find(elem => elem.id === body.id) as NixieBody;
17 | if (typeof c === 'undefined') {
18 | body.master = 1;
19 | c = new NixieBody(this.controlPanel, body);
20 | this.push(c);
21 | await c.setBodyAsync(data);
22 | logger.info(`A body was not found for id #${body.id} creating body`);
23 | }
24 | else {
25 | await c.setBodyAsync(data);
26 | }
27 | }
28 | catch (err) { logger.error(`setBodyAsync: ${err.message}`); return Promise.reject(err); }
29 | }
30 | public async initAsync(bodies: BodyCollection) {
31 | try {
32 | this.length = 0;
33 | for (let i = 0; i < bodies.length; i++) {
34 | let body = bodies.getItemByIndex(i);
35 | if (body.master === 1) {
36 | if (typeof this.find(elem => elem.id === body.id) === 'undefined') {
37 | logger.info(`Initializing Nixie body ${body.name}`);
38 | let nbody = new NixieBody(this.controlPanel, body);
39 | this.push(nbody);
40 | }
41 | }
42 | }
43 | }
44 | catch (err) { logger.error(`Nixie Body initAsync: ${err.message}`); return Promise.reject(err); }
45 | }
46 | public async closeAsync() {
47 | try {
48 | for (let i = this.length - 1; i >= 0; i--) {
49 | try {
50 | await this[i].closeAsync();
51 | this.splice(i, 1);
52 | } catch (err) { logger.error(`Error stopping Nixie Body ${err}`); }
53 | }
54 |
55 | } catch (err) { } // Don't bail if we have an errror.
56 | }
57 |
58 | }
59 | export class NixieBody extends NixieEquipment {
60 | public pollingInterval: number = 10000;
61 | private _pollTimer: NodeJS.Timeout = null;
62 | public body: Body;
63 | constructor(ncp: INixieControlPanel, body: Body) {
64 | super(ncp);
65 | this.body = body;
66 | this.pollEquipmentAsync();
67 | let bs = state.temps.bodies.getItemById(body.id);
68 | bs.heaterCooldownDelay = false;
69 | bs.heatStatus = 0;
70 | }
71 | public get id(): number { return typeof this.body !== 'undefined' ? this.body.id : -1; }
72 | public async setBodyAsync(data: any) {
73 | try {
74 | let body = this.body;
75 | }
76 | catch (err) { logger.error(`Nixie setBodyAsync: ${err.message}`); return Promise.reject(err); }
77 | }
78 | public async setBodyStateAsync(bstate: BodyTempState, isOn: boolean) {
79 | try {
80 | // Here we go we need to set the valve state.
81 | if (bstate.isOn !== isOn) {
82 | logger.info(`Nixie: Set Body ${bstate.id}-${bstate.name} to ${isOn}`);
83 | }
84 | bstate.isOn = isOn;
85 | } catch (err) { logger.error(`Nixie Error setting body state ${bstate.id}-${bstate.name}: ${err.message}`); }
86 | }
87 |
88 | public async pollEquipmentAsync() {
89 | let self = this;
90 | try {
91 | if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
92 | this._pollTimer = null;
93 | let success = false;
94 | }
95 | catch (err) { logger.error(`Nixie Error polling body - ${err}`); }
96 | finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
97 | }
98 | private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
99 | try {
100 | let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
101 | return dev;
102 | } catch (err) { logger.error(`Nixie Body checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
103 | }
104 | public async validateSetupAsync(body: Body, temp: BodyTempState) {
105 | try {
106 | // The validation will be different if the body is on or not. So lets get that information.
107 | } catch (err) { logger.error(`Nixie Error checking Body Hardware ${this.body.name}: ${err.message}`); return Promise.reject(err); }
108 | }
109 | public async closeAsync() {
110 | try {
111 | if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
112 | this._pollTimer = null;
113 | let bstate = state.temps.bodies.getItemById(this.body.id);
114 | await this.setBodyStateAsync(bstate, false);
115 | bstate.emitEquipmentChange();
116 | }
117 | catch (err) { logger.error(`Nixie Body closeAsync: ${err.message}`); return Promise.reject(err); }
118 | }
119 | public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
120 | }
121 |
--------------------------------------------------------------------------------
/controller/nixie/bodies/Filter.ts:
--------------------------------------------------------------------------------
1 | import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, ParameterOutOfRangeError } from '../../Errors';
2 | import { utils, Timestamp } from '../../Constants';
3 | import { logger } from '../../../logger/Logger';
4 |
5 | import { NixieEquipment, NixieChildEquipment, NixieEquipmentCollection, INixieControlPanel } from "../NixieEquipment";
6 | import { Filter, FilterCollection, sys } from "../../../controller/Equipment";
7 | import { FilterState, state, } from "../../State";
8 | import { setTimeout, clearTimeout } from 'timers';
9 | import { NixieControlPanel } from '../Nixie';
10 | import { webApp, InterfaceServerResponse } from "../../../web/Server";
11 |
12 | export class NixieFilterCollection extends NixieEquipmentCollection {
13 | public async setFilterStateAsync(fstate: FilterState, val: boolean) {
14 | try {
15 | let f: NixieFilter = this.find(elem => elem.id === fstate.id) as NixieFilter;
16 | if (typeof f === 'undefined') return Promise.reject(new Error(`NCP: Filter ${fstate.id}-${fstate.name} could not be found to set the state to ${val}.`));
17 | await f.setFilterStateAsync(fstate, val);
18 | }
19 | catch (err) { return logger.error(`NCP: setCircuitFilterAsync ${fstate.id}-${fstate.name}: ${err.message}`); }
20 | }
21 |
22 | public async setFilterAsync(filter: Filter, data: any) {
23 | // By the time we get here we know that we are in control and this is a REMChem.
24 | try {
25 | let c: NixieFilter = this.find(elem => elem.id === filter.id) as NixieFilter;
26 | if (typeof c === 'undefined') {
27 | filter.master = 1;
28 | c = new NixieFilter(this.controlPanel, filter);
29 | this.push(c);
30 | await c.setFilterAsync(data);
31 | logger.info(`A Filter was not found for id #${filter.id} creating Filter`);
32 | }
33 | else {
34 | await c.setFilterAsync(data);
35 | }
36 | }
37 | catch (err) { logger.error(`setFilterAsync: ${err.message}`); return Promise.reject(err); }
38 | }
39 | public async initAsync(filters: FilterCollection) {
40 | try {
41 | this.length = 0;
42 | for (let i = 0; i < filters.length; i++) {
43 | let filter = filters.getItemByIndex(i);
44 | if (filter.master === 1) {
45 | if (typeof this.find(elem => elem.id === filter.id) === 'undefined') {
46 | logger.info(`Initializing Filter ${Filter.name}`);
47 | let nFilter = new NixieFilter(this.controlPanel, filter);
48 | this.push(nFilter);
49 | }
50 | }
51 | }
52 | }
53 | catch (err) { logger.error(`Nixie Filter initAsync: ${err.message}`); return Promise.reject(err); }
54 | }
55 | public async closeAsync() {
56 | try {
57 | for (let i = this.length - 1; i >= 0; i--) {
58 | try {
59 | await this[i].closeAsync();
60 | this.splice(i, 1);
61 | } catch (err) { logger.error(`Error stopping Nixie Filter ${err}`); }
62 | }
63 |
64 | } catch (err) { } // Don't bail if we have an errror.
65 | }
66 | }
67 | export class NixieFilter extends NixieEquipment {
68 | public pollingInterval: number = 10000;
69 | private _pollTimer: NodeJS.Timeout = null;
70 | private _lastState;
71 | public filter: Filter;
72 | constructor(ncp: INixieControlPanel, filter: Filter) {
73 | super(ncp);
74 | this.filter = filter;
75 | this.pollEquipmentAsync();
76 | }
77 | public get id(): number { return typeof this.filter !== 'undefined' ? this.filter.id : -1; }
78 | public async setFilterAsync(data: any) {
79 | try {
80 | let filter = this.filter;
81 | }
82 | catch (err) { logger.error(`Nixie setFilterAsync: ${err.message}`); return Promise.reject(err); }
83 | }
84 | public async setFilterStateAsync(fstate: FilterState, val: boolean): Promise {
85 | try {
86 | if (utils.isNullOrEmpty(this.filter.connectionId) || utils.isNullOrEmpty(this.filter.deviceBinding)) {
87 | fstate.isOn = val;
88 | return new InterfaceServerResponse(200, 'Success');
89 | }
90 | if (typeof this._lastState === 'undefined' || val || this._lastState !== val) {
91 | let res = await NixieEquipment.putDeviceService(this.filter.connectionId, `/state/device/${this.filter.deviceBinding}`, { isOn: val, latch: val ? 10000 : undefined });
92 | if (res.status.code === 200) this._lastState = fstate.isOn = val;
93 | return res;
94 | }
95 | else {
96 | fstate.isOn = val;
97 | return new InterfaceServerResponse(200, 'Success');
98 | }
99 | } catch (err) { logger.error(`Nixie: Error setting filter state ${fstate.id}-${fstate.name} to ${val}`); }
100 | }
101 |
102 | public async pollEquipmentAsync() {
103 | let self = this;
104 | try {
105 | if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
106 | this._pollTimer = null;
107 | let success = false;
108 | }
109 | catch (err) { logger.error(`Nixie Error polling Filter - ${err}`); }
110 | finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
111 | }
112 | private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
113 | try {
114 | let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
115 | return dev;
116 | } catch (err) { logger.error(`Nixie Filter checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
117 | }
118 | public async validateSetupAsync(Filter: Filter, temp: FilterState) {
119 | try {
120 | // The validation will be different if the Filter is on or not. So lets get that information.
121 | } catch (err) { logger.error(`Nixie Error checking Filter Hardware ${this.filter.name}: ${err.message}`); return Promise.reject(err); }
122 | }
123 | public async closeAsync() {
124 | try {
125 | if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
126 | this._pollTimer = null;
127 | let fstate = state.filters.getItemById(this.filter.id);
128 | logger.info(`Closing filter ${fstate.name}`)
129 | await this.setFilterStateAsync(fstate, false);
130 | fstate.emitEquipmentChange();
131 | }
132 | catch (err) { logger.error(`Nixie Filter closeAsync: ${err.message}`); return Promise.reject(err); }
133 | }
134 | public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
135 | }
136 |
--------------------------------------------------------------------------------
/controller/nixie/valves/Valve.ts:
--------------------------------------------------------------------------------
1 | import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, ParameterOutOfRangeError } from '../../Errors';
2 | import { utils, Timestamp } from '../../Constants';
3 | import { logger } from '../../../logger/Logger';
4 |
5 | import { NixieEquipment, NixieChildEquipment, NixieEquipmentCollection, INixieControlPanel } from "../NixieEquipment";
6 | import { Valve, ValveCollection, sys } from "../../../controller/Equipment";
7 | import { ValveState, state, } from "../../State";
8 | import { setTimeout, clearTimeout } from 'timers';
9 | import { NixieControlPanel } from '../Nixie';
10 | import { webApp, InterfaceServerResponse } from "../../../web/Server";
11 |
12 | export class NixieValveCollection extends NixieEquipmentCollection {
13 | public async deleteValveAsync(id: number) {
14 | try {
15 | for (let i = this.length - 1; i >= 0; i--) {
16 | let valve = this[i];
17 | if (valve.id === id) {
18 | await valve.closeAsync();
19 | this.splice(i, 1);
20 | }
21 | }
22 | } catch (err) { return Promise.reject(`Nixie Control Panel deleteValveAsync ${err.message}`); }
23 | }
24 | public async setValveStateAsync(vstate: ValveState, isDiverted: boolean) {
25 | try {
26 | let valve: NixieValve = this.find(elem => elem.id === vstate.id) as NixieValve;
27 | if (typeof valve === 'undefined') {
28 | vstate.isDiverted = isDiverted;
29 | return logger.error(`Nixie Control Panel Error setValveState could not find valve ${vstate.id}-${vstate.name}`);
30 | }
31 | await valve.setValveStateAsync(vstate, isDiverted);
32 | } catch (err) { return Promise.reject(new Error(`Nixie Error setting valve state ${vstate.id}-${vstate.name}: ${err.message}`)); }
33 | }
34 | public async setValveAsync(valve: Valve, data: any) {
35 | // By the time we get here we know that we are in control and this is a Nixie valve.
36 | try {
37 | let c: NixieValve = this.find(elem => elem.id === valve.id) as NixieValve;
38 | if (typeof c === 'undefined') {
39 | valve.master = 1;
40 | c = new NixieValve(this.controlPanel, valve);
41 | this.push(c);
42 | await c.setValveAsync(data);
43 | logger.info(`A valve was not found for id #${valve.id} creating valve`);
44 | }
45 | else {
46 | await c.setValveAsync(data);
47 | }
48 | }
49 | catch (err) { logger.error(`setValveAsync: ${err.message}`); return Promise.reject(err); }
50 | }
51 | public async initAsync(valves: ValveCollection) {
52 | try {
53 | for (let i = 0; i < valves.length; i++) {
54 | let valve = valves.getItemByIndex(i);
55 | if (valve.master === 1) {
56 | if (typeof this.find(elem => elem.id === valve.id) === 'undefined') {
57 | let nvalve = new NixieValve(this.controlPanel, valve);
58 | logger.info(`Initializing Nixie Valve ${nvalve.id}-${valve.name}`);
59 | this.push(nvalve);
60 | }
61 | }
62 | }
63 | }
64 | catch (err) { logger.error(`Nixie Valve initAsync Error: ${err.message}`); return Promise.reject(err); }
65 | }
66 | public async closeAsync() {
67 | try {
68 | for (let i = this.length - 1; i >= 0; i--) {
69 | try {
70 | await this[i].closeAsync();
71 | this.splice(i, 1);
72 | } catch (err) { logger.error(`Error stopping Nixie Valve ${err}`); }
73 | }
74 |
75 | } catch (err) { } // Don't bail if we have an errror.
76 | }
77 |
78 | public async initValveAsync(valve: Valve): Promise {
79 | try {
80 | let c: NixieValve = this.find(elem => elem.id === valve.id) as NixieValve;
81 | if (typeof c === 'undefined') {
82 | c = new NixieValve(this.controlPanel, valve);
83 | this.push(c);
84 | }
85 | return c;
86 | } catch (err) { return Promise.reject(logger.error(`Nixie Controller: initValveAsync Error: ${err.message}`)); }
87 | }
88 |
89 | }
90 | export class NixieValve extends NixieEquipment {
91 | public pollingInterval: number = 10000;
92 | private _pollTimer: NodeJS.Timeout = null;
93 | public valve: Valve;
94 | private _lastState;
95 | constructor(ncp: INixieControlPanel, valve: Valve) {
96 | super(ncp);
97 | this.valve = valve;
98 | this.pollEquipmentAsync();
99 | }
100 | public get id(): number { return typeof this.valve !== 'undefined' ? this.valve.id : -1; }
101 | public async setValveStateAsync(vstate: ValveState, isDiverted: boolean) {
102 | try {
103 | // Here we go we need to set the valve state.
104 | if (vstate.isDiverted !== isDiverted) {
105 | logger.verbose(`Nixie: Set valve ${vstate.id}-${vstate.name} to ${isDiverted}`);
106 | }
107 | if (utils.isNullOrEmpty(this.valve.connectionId) || utils.isNullOrEmpty(this.valve.deviceBinding)) {
108 | vstate.isDiverted = isDiverted;
109 | return new InterfaceServerResponse(200, 'Success');
110 | }
111 | if (typeof this._lastState === 'undefined' || isDiverted || this._lastState !== isDiverted) {
112 | let res = await NixieEquipment.putDeviceService(this.valve.connectionId, `/state/device/${this.valve.deviceBinding}`, { isOn: isDiverted, latch: isDiverted ? 10000 : undefined });
113 | if (res.status.code === 200) this._lastState = vstate.isDiverted = isDiverted;
114 | return res;
115 | }
116 | else {
117 | vstate.isDiverted = isDiverted;
118 | return new InterfaceServerResponse(200, 'Success');
119 | }
120 | } catch (err) { return logger.error(`Nixie Error setting valve state ${vstate.id}-${vstate.name}: ${err.message}`); }
121 | }
122 | public async setValveAsync(data: any) {
123 | try {
124 | let valve = this.valve;
125 | }
126 | catch (err) { logger.error(`Nixie setValveAsync: ${err.message}`); return Promise.reject(err); }
127 | }
128 | public async pollEquipmentAsync() {
129 | let self = this;
130 | try {
131 | if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
132 | this._pollTimer = null;
133 | let success = false;
134 | }
135 | catch (err) { logger.error(`Nixie Error polling valve - ${err}`); }
136 | finally { this._pollTimer = setTimeout(async () => await self.pollEquipmentAsync(), this.pollingInterval || 10000); }
137 | }
138 | private async checkHardwareStatusAsync(connectionId: string, deviceBinding: string) {
139 | try {
140 | let dev = await NixieEquipment.getDeviceService(connectionId, `/status/device/${deviceBinding}`);
141 | return dev;
142 | } catch (err) { logger.error(`Nixie Valve checkHardwareStatusAsync: ${err.message}`); return { hasFault: true } }
143 | }
144 | public async validateSetupAsync(valve: Valve, vstate: ValveState) {
145 | try {
146 | if (typeof valve.connectionId !== 'undefined' && valve.connectionId !== ''
147 | && typeof valve.deviceBinding !== 'undefined' && valve.deviceBinding !== '') {
148 | try {
149 | let stat = await this.checkHardwareStatusAsync(valve.connectionId, valve.deviceBinding);
150 | // If we have a status check the return.
151 | vstate.commStatus = stat.hasFault ? 1 : 0;
152 | } catch (err) { vstate.commStatus = 1; }
153 | }
154 | else
155 | vstate.commStatus = 0;
156 | // The validation will be different if the valve is on or not. So lets get that information.
157 | } catch (err) { logger.error(`Nixie Error checking Valve Hardware ${this.valve.name}: ${err.message}`); vstate.commStatus = 1; return Promise.reject(err); }
158 | }
159 | public async closeAsync() {
160 | try {
161 | if (typeof this._pollTimer !== 'undefined' || this._pollTimer) clearTimeout(this._pollTimer);
162 | this._pollTimer = null;
163 | let vstate = state.valves.getItemById(this.valve.id);
164 | this.setValveStateAsync(vstate, false);
165 | vstate.emitEquipmentChange();
166 | }
167 | catch (err) { logger.error(`Nixie Valve closeAsync: ${err.message}`); return Promise.reject(err); }
168 | }
169 | public logData(filename: string, data: any) { this.controlPanel.logData(filename, data); }
170 | }
171 |
--------------------------------------------------------------------------------
/defaultConfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "controller": {
3 | "comms": {
4 | "type": "local",
5 | "portId": 0,
6 | "enabled": true,
7 | "rs485Port": "/dev/ttyUSB0",
8 | "mock": false,
9 | "netConnect": false,
10 | "netHost": "raspberrypi",
11 | "netPort": 9801,
12 | "inactivityRetry": 10,
13 | "portSettings": {
14 | "baudRate": 9600,
15 | "dataBits": 8,
16 | "parity": "none",
17 | "stopBits": 1,
18 | "rtscts": false,
19 | "autoOpen": false,
20 | "lock": false
21 | },
22 | "netSettings": {
23 | "allowHalfOpen": false,
24 | "keepAlive": false,
25 | "keepAliveInitialDelay": 5
26 | },
27 | "screenlogic": {
28 | "connectionType": "local",
29 | "systemName": "Pentair: 00-00-00",
30 | "password": 1234
31 | }
32 | },
33 | "backups": {
34 | "automatic": false,
35 | "interval": {
36 | "days": 30,
37 | "hours": 0
38 | },
39 | "keepCount": 5,
40 | "njsPC": true,
41 | "servers": []
42 | }
43 | },
44 | "web": {
45 | "servers": {
46 | "http2": {
47 | "enabled": false
48 | },
49 | "http": {
50 | "enabled": true,
51 | "ip": "0.0.0.0",
52 | "port": 4200,
53 | "httpsRedirect": false,
54 | "authentication": "none",
55 | "authFile": "/users.htpasswd"
56 | },
57 | "https": {
58 | "enabled": false,
59 | "ip": "0.0.0.0",
60 | "port": 4201,
61 | "authentication": "none",
62 | "authFile": "/users.htpasswd",
63 | "sslKeyFile": "",
64 | "sslCertFile": ""
65 | },
66 | "mdns": {
67 | "enabled": false
68 | },
69 | "ssdp": {
70 | "enabled": true
71 | }
72 | },
73 | "services": {},
74 | "interfaces": {
75 | "smartThings": {
76 | "name": "SmartThings",
77 | "type": "rest",
78 | "enabled": false,
79 | "fileName": "smartThings-Hubitat.json",
80 | "globals": {},
81 | "options": {
82 | "host": "0.0.0.0",
83 | "port": 39500
84 | }
85 | },
86 | "hubitat": {
87 | "name": "Hubitat",
88 | "type": "rest",
89 | "enabled": false,
90 | "fileName": "smartThings-Hubitat.json",
91 | "globals": {},
92 | "options": {
93 | "host": "0.0.0.0",
94 | "port": 39501
95 | }
96 | },
97 | "vera": {
98 | "name": "Vera",
99 | "type": "rest",
100 | "enabled": false,
101 | "fileName": "vera.json",
102 | "vars": {
103 | "deviceId": 0
104 | },
105 | "options": {
106 | "host": "",
107 | "port": 3480
108 | }
109 | },
110 | "valveRelay": {
111 | "type": "rest",
112 | "name": "Valve Relays",
113 | "enabled": false,
114 | "fileName": "valveRelays.json",
115 | "vars": {
116 | "valveIds": []
117 | },
118 | "options": {
119 | "host": "0.0.0.0",
120 | "port": 8081
121 | }
122 | },
123 | "influxDB": {
124 | "name": "InfluxDB",
125 | "type": "influx",
126 | "enabled": false,
127 | "fileName": "influxDB.json",
128 | "options": {
129 | "version": 1,
130 | "protocol": "http",
131 | "host": "192.168.0.1",
132 | "port": 9999,
133 | "username": "",
134 | "password": "",
135 | "database": "pool",
136 | "retentionPolicy": "autogen"
137 | }
138 | },
139 | "influxDBv2": {
140 | "name": "InfluxDBv2",
141 | "type": "influx",
142 | "enabled": false,
143 | "fileName": "influxDB.json",
144 | "options": {
145 | "version": 2,
146 | "protocol": "http",
147 | "host": "192.168.0.1",
148 | "port": 9999,
149 | "token": "...LuyM84JJx93Qvc7tfaXPbI_mFFjRBjaA==",
150 | "org": "example-org",
151 | "bucket": "57ec4eed2d90a50b"
152 | }
153 | },
154 | "mqtt": {
155 | "name": "MQTT",
156 | "type": "mqtt",
157 | "enabled": false,
158 | "fileName": "mqtt.json",
159 | "globals": {},
160 | "options": {
161 | "protocol": "mqtt://",
162 | "host": "192.168.0.1",
163 | "port": 1883,
164 | "username": "",
165 | "password": "",
166 | "selfSignedCertificate": false,
167 | "rootTopic": "@bind=(state.equipment.model).replace(/ /g,'-').replace('/','').toLowerCase();",
168 | "retain": true,
169 | "qos": 0,
170 | "changesOnly": true
171 | }
172 | },
173 | "aqualinkD": {
174 | "name": "AquaLinkD",
175 | "type": "mqtt",
176 | "enabled": false,
177 | "fileName": "aqualinkD.json",
178 | "globals": {},
179 | "options": {
180 | "protocol": "mqtt://",
181 | "host": "192.168.0.1",
182 | "port": 1883,
183 | "username": "",
184 | "password": "",
185 | "rootTopic": "aqualinkd",
186 | "retain": true,
187 | "qos": 0,
188 | "changesOnly": true
189 | },
190 | "vars": {
191 | "tempPrecision": 2,
192 | "tempUnits": "@bind=sys.board.valueMaps.tempUnits.getName(state.temps.units);"
193 | }
194 | },
195 | "mqttAlt": {
196 | "name": "MQTTAlt",
197 | "type": "mqtt",
198 | "enabled": false,
199 | "fileName": "mqttAlt.json",
200 | "globals": {},
201 | "options": {
202 | "protocol": "mqtt://",
203 | "host": "192.168.0.1",
204 | "port": 1883,
205 | "username": "",
206 | "password": "",
207 | "rootTopic": "@bind=(state.equipment.model).replace(/ /,'-').replace('/','').toLowerCase();Alt",
208 | "retain": true,
209 | "qos": 0,
210 | "changesOnly": true
211 | }
212 | },
213 | "homeAssistant": {
214 | "name": "Home Assistant",
215 | "type": "mqtt",
216 | "enabled": false,
217 | "fileName": "homeassistant.json",
218 | "globals": {},
219 | "options": {
220 | "protocol": "mqtt://",
221 | "host": "192.168.0.1",
222 | "port": 1883,
223 | "username": "",
224 | "password": "",
225 | "selfSignedCertificate": false,
226 | "retain": true,
227 | "qos": 0,
228 | "changesOnly": true
229 | },
230 | "vars": {
231 | "_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).",
232 | "hassTopic": "homeassistant",
233 | "mqttTopic": "@bind=(state.equipment.model).replace(/ /g,'-').replace('/','').toLowerCase();"
234 | }
235 | },
236 | "rem": {
237 | "name": "Relay Equipment Manager",
238 | "type": "rem",
239 | "enabled": false,
240 | "options": {
241 | "protocol": "http://",
242 | "host": "raspberrypi",
243 | "port": 8080,
244 | "headers": {
245 | "content-type": "application/json"
246 | }
247 | },
248 | "socket": {
249 | "transports": [
250 | "websocket"
251 | ],
252 | "upgrade": false,
253 | "reconnectionDelay": 2000,
254 | "reconnection": true,
255 | "reconnectionDelayMax": 20000
256 | }
257 | }
258 | }
259 | },
260 | "log": {
261 | "screenlogic": {
262 | "enabled": false,
263 | "logToConsole": false,
264 | "logToFile": false
265 | },
266 | "packet": {
267 | "logToConsole": false,
268 | "logToFile": false,
269 | "enabled": false,
270 | "filename": "packetLog",
271 | "invalid": true,
272 | "broadcast": {
273 | "enabled": true,
274 | "includeActions": [],
275 | "includeSource": [],
276 | "includeDest": [],
277 | "excludeActions": [],
278 | "excludeSource": [],
279 | "excludeDest": []
280 | },
281 | "pump": {
282 | "enabled": true,
283 | "includeActions": [],
284 | "includeSource": [],
285 | "includeDest": [],
286 | "excludeActions": [],
287 | "excludeSource": [],
288 | "excludeDest": []
289 | },
290 | "chlorinator": {
291 | "enabled": true,
292 | "includeSource": [],
293 | "includeDest": [],
294 | "excludeSource": [],
295 | "excludeDest": []
296 | },
297 | "intellichem": {
298 | "enabled": true,
299 | "includeActions": [],
300 | "excludeActions": [],
301 | "includeSource": [],
302 | "includeDest": [],
303 | "excludeSource": [],
304 | "excludeDest": []
305 | },
306 | "intellivalve": {
307 | "enabled": true,
308 | "includeActions": [],
309 | "excludeActions": [],
310 | "includeSource": [],
311 | "includeDest": [],
312 | "excludeSource": [],
313 | "excludeDest": []
314 | },
315 | "heater": {
316 | "enabled": true,
317 | "includeActions": [],
318 | "excludeActions": [],
319 | "includeSource": [],
320 | "includeDest": [],
321 | "excludeSource": [],
322 | "excludeDest": []
323 | },
324 | "unidentified": {
325 | "enabled": true,
326 | "includeSource": [],
327 | "includeDest": [],
328 | "excludeSource": [],
329 | "excludeDest": []
330 | },
331 | "unknown": {
332 | "enabled": true,
333 | "includeSource": [],
334 | "includeDest": [],
335 | "excludeSource": [],
336 | "excludeDest": []
337 | }
338 | },
339 | "app": {
340 | "enabled": true,
341 | "level": "info",
342 | "captureForReplay": false,
343 | "logToFile": false
344 | }
345 | },
346 | "appVersion": "0.0.1"
347 | }
348 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodejs-poolcontroller",
3 | "version": "8.1.0",
4 | "description": "nodejs-poolController",
5 | "main": "app.js",
6 | "author": {
7 | "name": "Russell Goldin",
8 | "name2": "Robert Strouse"
9 | },
10 | "license": "GNU Affero General Public License v3.0",
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/tagyoureit/nodejs-poolController.git"
14 | },
15 | "engines": {
16 | "npm": ">=7.0.0",
17 | "node": ">=16.0.0"
18 | },
19 | "scripts": {
20 | "start": "npm run build && node dist/app.js",
21 | "start:cached": "node dist/app.js",
22 | "build": "tsc",
23 | "watch": "tsc -w"
24 | },
25 | "dependencies": {
26 | "@influxdata/influxdb-client": "^1.32.0",
27 | "eslint-config-promise": "^2.0.2",
28 | "express": "^4.18.1",
29 | "extend": "^3.0.2",
30 | "jszip": "^3.9.1",
31 | "mqtt": "^4.3.7",
32 | "multer": "^1.4.5-lts.1",
33 | "multicast-dns": "^7.2.4",
34 | "node-screenlogic": "^2.0.0",
35 | "node-ssdp": "^4.0.1",
36 | "serialport": "^11.0.0",
37 | "socket.io": "^4.5.0",
38 | "socket.io-client": "^4.5.0",
39 | "source-map-support": "^0.5.21",
40 | "winston": "^3.7.2"
41 | },
42 | "devDependencies": {
43 | "@types/express": "^4.17.13",
44 | "@types/extend": "^3.0.1",
45 | "@types/multer": "^1.4.7",
46 | "@types/node": "^16.11.48",
47 | "@typescript-eslint/eslint-plugin": "^5.33.0",
48 | "@typescript-eslint/parser": "^5.33.0",
49 | "eslint": "^8.21.0",
50 | "eslint-config-defaults": "^9.0.0",
51 | "eslint-plugin-import": "^2.26.0",
52 | "eslint-plugin-node": "^11.1.0",
53 | "eslint-plugin-promise": "^6.0.0",
54 | "eslint-plugin-standard": "^5.0.0",
55 | "grunt": "^1.5.3",
56 | "grunt-banner": "^0.6.0",
57 | "ts-node": "^10.7.0",
58 | "typescript": "^4.6.4"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/sendSocket.js:
--------------------------------------------------------------------------------
1 | // Import Socket.IO client
2 | const io = require('socket.io-client');
3 |
4 | // Connect to the server
5 | const socket = io('http://localhost:4200');
6 |
7 | // Event handler for successful connection
8 | socket.on('connect', () => {
9 | console.log('Connected to server.');
10 |
11 | // Emit data to the server
12 | socket.emit('message', 'Hello, server!');
13 | socket.emit('echo', `testing 123`);
14 | socket.on('echo', (string)=>{
15 | console.log(string);
16 | })
17 | //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";
18 | const hexData = "10 02 01 80 20 00 00 00 00 00 00 b5 00 03 10 02 10 03";
19 | const formattedHexData = hexData.split(' ').map(hex => parseInt(hex, 16));
20 | //socket.emit('rawbytes', Buffer.from([1, 2, 3]));
21 | socket.emit('rawbytes', formattedHexData);
22 | });
23 |
24 | // Event handler for receiving data from the server
25 | socket.on('message', (data) => {
26 | console.log('Received from server:', data);
27 | });
28 |
29 | // Event handler for disconnection
30 | socket.on('disconnect', () => {
31 | console.log('Disconnected from server.');
32 | });
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | // "paths": { "*": [ "@types/*" ] },
5 | "target":"esnext",
6 | "module":"commonjs",
7 | "noImplicitAny": false,
8 | "removeComments": true,
9 | "preserveConstEnums": true,
10 | "sourceMap": true,
11 | "outDir": "dist",
12 | "moduleResolution": "node",
13 | "allowJs": false,
14 | "allowSyntheticDefaultImports": false,
15 | "esModuleInterop": false,
16 | "jsx": "react"
17 | },
18 | "include": [
19 | "web/**/*",
20 | "logger/**/*",
21 | "controller/**/*",
22 | "config/**/*",
23 | "**/*.ts"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/web/bindings/rulesManager.json:
--------------------------------------------------------------------------------
1 | {
2 | "context": {
3 | "name": "Rules Manager",
4 | "vars": {},
5 | "options": {
6 | "method": "PUT",
7 | "headers": {
8 | "CONTENT-TYPE": "application/json"
9 | }
10 | }
11 | },
12 | "events": [
13 | {
14 | "name": "controller",
15 | "description": "Turn on needsCleaning flag for filter. Evaluate on a controller socket emit. True when the pool is on, pump is not priming, and filter psi > 15",
16 | "enabled": true,
17 | "filter": "@bind=data.id;===1 && @bind=state.filters.getItemById(1).psi;>15 && @bind=data.status.name;!=='priming'",
18 | "options": {
19 | "path": "/config/filter"
20 | },
21 | "body": {"needsCleaning":true}
22 | },
23 | {
24 | "name": "controller",
25 | "description": "Turn on needsCleaning flag for filter. Evaluate on a controller/time socket emit. Eval on the first day of every 3rd month when psi is >15 at any point",
26 | "enabled": true,
27 | "filter": "@bind=(new Date(data.time)).getMonth()%3;===0 && @bind=(new Date(data.time)).getDate();===1 && @bind=state.filters.getItemById(1).psi;>15",
28 | "options": {
29 | "path": "/config/filter"
30 | },
31 | "body": {"needsCleaning":true}
32 | },
33 | {
34 | "name": "chemController",
35 | "description": "Sets chlorinator to 50% if ORP is low",
36 | "enabled": true,
37 | "filter": "@bind=data.orpLevel; < @bind=sys.chemControllers.getItemById(1).orpSetpoint;",
38 | "options": {
39 | "path": "/config/chlorinator"
40 | },
41 | "body": {"id":1, "poolSetpoint":50}
42 | },
43 | {
44 | "name": "chemController",
45 | "description": "Sets chlorinator to 5% if ORP is in range",
46 | "enabled": true,
47 | "filter": "@bind=data.orpLevel; > @bind=sys.chemControllers.getItemById(1).orpSetpoint;",
48 | "options": {
49 | "path": "/config/chlorinator"
50 | },
51 | "body": {"id":1, "poolSetpoint":5}
52 | }
53 |
54 | ]
55 | }
--------------------------------------------------------------------------------
/web/bindings/smartThings-Hubitat.json:
--------------------------------------------------------------------------------
1 | {
2 | "context": {
3 | "name": "SmartThings",
4 | "vars": {},
5 | "options": {
6 | "method": "NOTIFY",
7 | "path": "/notify",
8 | "headers": {
9 | "CONTENT-TYPE": "application/json",
10 | "X-EVENT-TYPE": "@bind=eventName;"
11 | }
12 | }
13 | },
14 | "events": [
15 | {
16 | "name": "justAnExample",
17 | "description": "Shows how the binding can be used to send variable data to SmartThings. This can be any combination of data.",
18 | "body": "{\"air\":\"@bind=data.air;@bind=data.units.name.substring(0, 1);\", \"total\":@bind=data.solar + data.waterSensor1;, \"status\":\"@bind=state.equipment.model;\"}",
19 | "options": {
20 | "headers": {
21 | "X-EVENT2": ""
22 | }
23 | }
24 | },
25 | {
26 | "name": "config",
27 | "description": "Not used for updates",
28 | "enabled": false
29 | },
30 | { "name": "*", "description": "All events that are not trapped by other event names. Sends the entire emitted response.", "body": "@bind=data;" }
31 | ]
32 | }
--------------------------------------------------------------------------------
/web/bindings/valveRelays.json:
--------------------------------------------------------------------------------
1 | {
2 | "context": {
3 | "name": "valveRelay",
4 | "options": {
5 | "method": "GET",
6 | "path": "/@bind=data.pinId;/@bind=data.isDiverted ? 'on' : 'off';",
7 | "headers": {
8 | "CONTENT-TYPE": "application/json"
9 | }
10 | },
11 | "vars": {}
12 | },
13 | "events": [
14 | {
15 | "name": "valve",
16 | "filter": "@bind=data.isVirtual;",
17 | "description": "Send commands to turn on or off the valve relay based upon the valve emit."
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/web/bindings/vera.json:
--------------------------------------------------------------------------------
1 | {
2 | "context": {
3 | "name": "Vera",
4 | "options": {
5 | "method": "POST",
6 | "path": "/data_request?id=action&DeviceNum=@bind=vars.deviceId;&serviceId=urn:rstrouse-com:serviceId:PoolController1&action=SetEventData&targetData=@bind=eventName;",
7 | "headers": {
8 | "CONTENT-TYPE": "application/json"
9 | }
10 | },
11 | "vars": {}
12 | },
13 | "events": [
14 | {
15 | "name": "config",
16 | "description": "Vera doesn't use this payload.",
17 | "enabled": false
18 | },
19 | {
20 | "name": "*",
21 | "description": "This will be the UPnP version when I get the services built in Vera and the only binding required. Sends the entire emitted response.",
22 | "body": "@bind=data;"
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/web/interfaces/baseInterface.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 | import extend = require("extend");
4 | import { logger } from "../../logger/Logger";
5 | import { sys as sysAlias } from "../../controller/Equipment";
6 | import { state as stateAlias} from "../../controller/State";
7 | import { webApp as webAppAlias } from '../Server';
8 | import { config } from "../../config/Config";
9 | export class BindingsFile {
10 | public static async fromBuffer(filename: string, buff: Buffer) {
11 | try {
12 | let bf = new BindingsFile();
13 | bf.filename = filename;
14 | bf.filePath = path.join(process.cwd(), 'web/bindings/custom', bf.filename);
15 | bf.options = await bf.extractBindingOptions(buff);
16 | return typeof bf.options !== 'undefined' ? bf : undefined;
17 | } catch (err) { logger.error(`Error creating buffered backup file: ${filename}`); }
18 | }
19 | public static async fromFile(pathName: string, fileName: string) {
20 | try {
21 | let bf = new BindingsFile();
22 | bf.filePath = path.posix.join(pathName, fileName);
23 | bf.filename = fileName;
24 | bf.options = await bf.extractBindingOptions(bf.filePath);
25 | return typeof bf.options !== 'undefined' ? bf : undefined;
26 | } catch (err) { logger.error(`Error creating bindings file from file ${pathName}${fileName}`); }
27 | }
28 | public filename: string;
29 | public filePath: string;
30 | public options: any;
31 | protected async extractBindingOptions(file: string | Buffer) {
32 | try {
33 | let buff = Buffer.isBuffer(file) ? file.toString() : fs.readFileSync(file, 'utf8');
34 | let bindings = JSON.parse(buff);
35 | let interfaces = config.getSection('web.interfaces');
36 | let ass = [];
37 | for (let ifname in interfaces) {
38 | let iface = interfaces[ifname]
39 | if (typeof iface !== 'undefined' && typeof iface.fileName !== 'undefined')
40 | if (iface.fileName.endsWith(`custom/${this.filename}`)) ass.push(ifname);
41 | }
42 | if (typeof bindings.context !== 'undefined')
43 | return {
44 | filename: this.filename, filepath: this.filePath, name: bindings.context.name || name, type: bindings.context.type || undefined, assoc: ass
45 | };
46 | return this.options;
47 | } catch (err) { logger.error(`Error extracting binding options from ${Buffer.isBuffer(file) ? 'Buffer' : file}: ${err.message}`); }
48 | }
49 | }
50 |
51 | export class BaseInterfaceBindings {
52 | constructor(cfg) {
53 | this.cfg = cfg;
54 | }
55 | public context: InterfaceContext;
56 | public cfg;
57 | public events: InterfaceEvent[];
58 | public bindEvent(evt: string, ...data: any) { };
59 | public bindVarTokens(e: IInterfaceEvent, evt: string, ...data: any) {
60 | let v = {};
61 | let toks = {};
62 | let vars = extend(true, {}, this.context.vars, typeof e !== 'undefined' && e.vars ? e.vars : {}, this.cfg.vars || {});
63 | for (var s in vars) {
64 | let ovalue = vars[s];
65 | if (typeof ovalue === 'string') {
66 | if (ovalue.includes('@bind')) {
67 | this.matchTokens(ovalue, evt, toks, e, data[0], vars);
68 | v[s] = toks;
69 | ovalue = this.evalTokens(ovalue, toks);
70 | }
71 | }
72 | v[s] = ovalue;
73 | }
74 | //console.log(...data);
75 | //console.log(v);
76 | return v;
77 | }
78 | protected matchTokens(input: string, eventName: string, toks: any, e: IInterfaceEvent, data, vars): any {
79 | toks = toks || [];
80 | let s = input;
81 | let regx = /(?<=@bind\=\s*).*?(?=\;)/g;
82 | let match;
83 | let sys = sysAlias;
84 | let state = stateAlias;
85 | let webApp = webAppAlias;
86 | while (match = regx.exec(s)) {
87 | let bind = match[0];
88 | if (typeof toks[bind] !== 'undefined') continue;
89 | let tok: any = {};
90 | toks[bind] = tok;
91 | try {
92 | // we may error out if data can't be found (eg during init)
93 | tok.reg = new RegExp("@bind=" + this.escapeRegex(bind) + ";", "g");
94 | tok.value = eval(bind);
95 | }
96 | catch (err) {
97 | // leave value undefined so it isn't sent to bindings
98 | toks[bind] = null;
99 | }
100 | }
101 | return toks;
102 |
103 | }
104 | protected buildTokens(input: string, eventName: string, toks: any, e: IInterfaceEvent, data): any {
105 | toks = toks || [];
106 | let s = input;
107 | let regx = /(?<=@bind\=\s*).*?(?=\;)/g;
108 | let match;
109 | let vars = this.bindVarTokens(e, eventName, data);
110 | let sys = sysAlias;
111 | let state = stateAlias;
112 | let webApp = webAppAlias;
113 | // Map all the returns to the token list. We are being very basic
114 | // here an the object graph is simply based upon the first object occurrence.
115 | // We simply want to eval against that object reference.
116 |
117 | while (match = regx.exec(s)) {
118 | let bind = match[0];
119 | if (typeof toks[bind] !== 'undefined') continue;
120 | let tok: any = {};
121 | toks[bind] = tok;
122 | try {
123 | // we may error out if data can't be found (eg during init)
124 | tok.reg = new RegExp("@bind=" + this.escapeRegex(bind) + ";", "g");
125 | tok.value = eval(bind);
126 | }
127 | catch (err) {
128 | // leave value undefined so it isn't sent to bindings
129 | toks[bind] = null;
130 | }
131 | }
132 | return toks;
133 | }
134 | protected escapeRegex(reg: string) { return reg.replace(/[-[\]{}()*+?.|,\\^$]/g, '\\$&'); }
135 | protected replaceTokens(input: string, toks: any) {
136 | let s = input;
137 | for (let exp in toks) {
138 | let tok = toks[exp];
139 | if (!tok || typeof tok.reg === 'undefined') continue;
140 | tok.reg.lastIndex = 0; // Start over if we used this before.
141 | if (typeof tok.value === 'string') s = s.replace(tok.reg, tok.value);
142 | else if (typeof tok.value === 'undefined') s = s.replace(tok.reg, 'null');
143 | else s = s.replace(tok.reg, JSON.stringify(tok.value));
144 | }
145 | return s;
146 | }
147 | protected evalTokens(input: string, toks: any) {
148 | let s = input;
149 | for (let exp in toks) {
150 | let tok = toks[exp];
151 | if (!tok || typeof tok.reg === 'undefined') continue;
152 | tok.reg.lastIndex = 0; // Start over if we used this before.
153 | if (typeof tok.value === 'string') s = s.replace(tok.reg, tok.value);
154 | else if (typeof tok.value === 'undefined') s = s.replace(tok.reg, 'null');
155 | else return tok.value;
156 | }
157 | return s;
158 | }
159 | protected tokensReplacer(input: string, eventName: string, toks: any, e: InterfaceEvent, data): any{
160 | this.buildTokens(input, eventName, toks, e, data);
161 | return this.replaceTokens(input, toks);
162 | }
163 | public async stopAsync() { }
164 | }
165 | export interface IInterfaceEvent {
166 | enabled: boolean;
167 | filter?: string;
168 | options?: any;
169 | body?: any;
170 | vars?: any;
171 | processor?: string[]
172 | }
173 | export class InterfaceEvent implements IInterfaceEvent {
174 | public name: string;
175 | public enabled: boolean = true;
176 | public filter: string;
177 | public options: any = {};
178 | public body: any = {};
179 | public vars: any = {};
180 | public processor?: string[]
181 | }
182 | export class InterfaceContext {
183 | public name: string;
184 | public mdnsDiscovery: any;
185 | public upnpDevice: any;
186 | public options: any = {};
187 | public vars: any = {};
188 | }
189 |
--------------------------------------------------------------------------------
/web/interfaces/httpInterface.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { HttpInterfaceServer } from "../../web/Server";
19 | import * as http2 from "http2";
20 | import * as http from "http";
21 | import * as https from "https";
22 | import extend=require("extend");
23 | import { logger } from "../../logger/Logger";
24 | import { PoolSystem, sys } from "../../controller/Equipment";
25 | import { State, state } from "../../controller/State";
26 | import { InterfaceContext, InterfaceEvent, BaseInterfaceBindings } from "./baseInterface";
27 |
28 | export class HttpInterfaceBindings extends BaseInterfaceBindings {
29 | constructor(cfg) {
30 | super(cfg);
31 | }
32 | declare sockets: HttpInterfaceSocketEvent[];
33 | public bindEvent(evt: string, ...data: any) {
34 | // Find the binding by first looking for the specific event name.
35 | // If that doesn't exist then look for the "*" (all events).
36 | if (typeof this.events !== 'undefined') {
37 | let evts = this.events.filter(elem => elem.name === evt);
38 | // If we don't have an explicitly defined event then see if there is a default.
39 | if (evts.length === 0) {
40 | let e = this.events.find(elem => elem.name === '*');
41 | evts = e ? [e] : [];
42 | }
43 |
44 | let baseOpts = extend(true, { headers: {} }, this.cfg.options, this.context.options);
45 | if ((typeof baseOpts.hostname === 'undefined' || !baseOpts.hostname) && (typeof baseOpts.host === 'undefined' || !baseOpts.host || baseOpts.host === '*')) {
46 | logger.warn(`Interface: ${ this.cfg.name } has not resolved to a valid host.`);
47 | return;
48 | }
49 | if (evts.length > 0) {
50 | let toks = {};
51 | for (let i = 0; i < evts.length; i++) {
52 | let e = evts[i];
53 | if (typeof e.enabled !== 'undefined' && !e.enabled) continue;
54 | let opts = extend(true, baseOpts, e.options);
55 | // Figure out whether we need to check the filter.
56 | if (typeof e.filter !== 'undefined') {
57 | this.buildTokens(e.filter, evt, toks, e, data[0]);
58 | if (eval(this.replaceTokens(e.filter, toks)) === false) continue;
59 | }
60 |
61 | // If we are still waiting on mdns then blow this off.
62 | if ((typeof opts.hostname === 'undefined' || !opts.hostname) && (typeof opts.host === 'undefined' || !opts.host || opts.host === '*')) {
63 | logger.warn(`Interface: ${ this.cfg.name } Event: ${ e.name } has not resolved to a valid host.`);
64 | continue;
65 | }
66 |
67 | // Put together the data object.
68 | let sbody = '';
69 | switch (this.cfg.contentType) {
70 | //case 'application/json':
71 | //case 'json':
72 | default:
73 | sbody = typeof e.body !== 'undefined' ? JSON.stringify(e.body) : '';
74 | break;
75 | // We may need an XML output and can add transforms for that
76 | // later. There isn't a native xslt processor in node and most
77 | // of them that I looked at seemed pretty amatuer hour or overbearing
78 | // as they used SAX. => Need down and clean not down and dirty... we aren't building
79 | // a web client at this point.
80 | }
81 | this.buildTokens(sbody, evt, toks, e, data[0]);
82 | sbody = this.replaceTokens(sbody, toks);
83 | for (let prop in opts) {
84 | if (prop === 'headers') {
85 | for (let header in opts.headers) {
86 | this.buildTokens(opts.headers[header], evt, toks, e, data[0]);
87 | opts.headers[header] = this.replaceTokens(opts.headers[header], toks);
88 | }
89 | }
90 | else if (typeof opts[prop] === 'string') {
91 | this.buildTokens(opts[prop], evt, toks, e, data[0]);
92 | opts[prop] = this.replaceTokens(opts[prop] || '', toks);
93 | }
94 | }
95 | if (typeof opts.path !== 'undefined') opts.path = encodeURI(opts.path); // Encode the data just in case we have spaces.
96 | // opts.headers["CONTENT-LENGTH"] = Buffer.byteLength(sbody || '');
97 | logger.debug(`Sending [${evt}] request to ${this.cfg.name}: ${JSON.stringify(opts)}`);
98 | let req: http.ClientRequest;
99 | // We should now have all the tokens. Put together the request.
100 | if (typeof sbody !== 'undefined') {
101 | if (sbody.charAt(0) === '"' && sbody.charAt(sbody.length - 1) === '"') sbody = sbody.substr(1, sbody.length - 2);
102 | opts.headers["CONTENT-LENGTH"] = Buffer.byteLength(sbody || '');
103 | }
104 | if (opts.port === 443 || (opts.protocol || '').startsWith('https')) {
105 | opts.protocol = 'https:';
106 | req = https.request(opts, (response: http.IncomingMessage) => {
107 | //console.log(response);
108 | });
109 | }
110 | else {
111 | opts.protocol = 'http:';
112 | req = http.request(opts, (response: http.IncomingMessage) => {
113 | //console.log(response.statusCode);
114 | });
115 | }
116 | req.on('error', (err, req, res) => {
117 | logger.error(`Error sending request for event ${evt}: ${err.message}`);
118 | });
119 | if (typeof sbody !== 'undefined') {
120 | req.write(sbody);
121 | }
122 | req.end();
123 | }
124 | }
125 | }
126 | }
127 | }
128 | class HttpInterfaceSocketEvent {
129 | event: string;
130 | description: string;
131 | processor: (sock: HttpInterfaceSocketEvent, sys: PoolSystem, state: State, value: any) => void;
132 | constructor(sock: any) {
133 | this.event = sock.event;
134 | if (typeof sock.processor !== 'undefined') {
135 | let fnBody = Array.isArray(sock.processor) ? sock.processor.join('\n') : sock.processor;
136 | try {
137 | this.processor = new Function('sock', 'sys', 'state', 'value', fnBody) as (sock: HttpInterfaceSocketEvent, sys: PoolSystem, state: State, value: any) => void;
138 | } catch (err) { logger.error(`Error compiling socket event processor: ${err} -- ${fnBody}`); }
139 | }
140 | }
141 | }
142 | export interface IHTTPInterfaceSocketEvent {
143 | event: string,
144 | description: string,
145 | processor?: string
146 | }
147 |
148 |
149 |
--------------------------------------------------------------------------------
/web/interfaces/ruleInterface.ts:
--------------------------------------------------------------------------------
1 | /* nodejs-poolController. An application to control pool equipment.
2 | Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3 | Russell Goldin, tagyoureit. russ.goldin@gmail.com
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU Affero General Public License as
7 | published by the Free Software Foundation, either version 3 of the
8 | License, or (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU Affero General Public License for more details.
14 |
15 | You should have received a copy of the GNU Affero General Public License
16 | along with this program. If not, see .
17 | */
18 | import { webApp } from "../../web/Server";
19 | import extend=require("extend");
20 | import { logger } from "../../logger/Logger";
21 | import { PoolSystem, sys } from "../../controller/Equipment";
22 | import { State, state } from "../../controller/State";
23 | import { InterfaceContext, InterfaceEvent, BaseInterfaceBindings } from "./baseInterface";
24 |
25 | export class RuleInterfaceBindings extends BaseInterfaceBindings {
26 | constructor(cfg) { super(cfg);}
27 | declare events: RuleInterfaceEvent[];
28 | public bindProcessor(evt: RuleInterfaceEvent) {
29 | if (evt.processorBound) return;
30 | if (typeof evt.fnProcessor === 'undefined') {
31 | let fnBody = Array.isArray(evt.processor) ? evt.processor.join('\n') : evt.processor;
32 | if (typeof fnBody !== 'undefined' && fnBody !== '') {
33 | //let AsyncFunction = Object.getPrototypeOf(async => () => { }).constructor;
34 | let AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
35 | try {
36 | evt.fnProcessor = new AsyncFunction('rule', 'options', 'vars', 'logger', 'webApp', 'sys', 'state', 'data', fnBody) as (rule: RuleInterfaceEvent, vars: any, sys: PoolSystem, state: State, data: any) => void;
37 | } catch (err) { logger.error(`Error compiling rule event processor: ${err} -- ${fnBody}`); }
38 | }
39 | }
40 | evt.processorBound = true;
41 | }
42 | public executeProcessor(eventName: string, evt: RuleInterfaceEvent, ...data: any) {
43 | this.bindProcessor(evt);
44 | let vars = this.bindVarTokens(evt, eventName, data);
45 | let opts = extend(true, this.cfg.options, this.context.options, evt.options);
46 | if (typeof evt.fnProcessor !== undefined) evt.fnProcessor(evt, opts, vars, logger, webApp, sys, state, data);
47 | }
48 | public bindEvent(evt: string, ...data: any) {
49 | // Find the binding by first looking for the specific event name.
50 | // If that doesn't exist then look for the "*" (all events).
51 | if (typeof this.events !== 'undefined') {
52 | let evts = this.events.filter(elem => elem.name === evt);
53 | // If we don't have an explicitly defined event then see if there is a default.
54 | if (evts.length === 0) {
55 | let e = this.events.find(elem => elem.name === '*');
56 | evts = e ? [e] : [];
57 | }
58 | if (evts.length > 0) {
59 | let toks = {};
60 | for (let i = 0; i < evts.length; i++) {
61 | let e = evts[i];
62 | if (typeof e.enabled !== 'undefined' && !e.enabled) continue;
63 | // Figure out whether we need to check the filter.
64 | if (typeof e.filter !== 'undefined') {
65 | this.buildTokens(e.filter, evt, toks, e, data[0]);
66 | if (eval(this.replaceTokens(e.filter, toks)) === false) continue;
67 | }
68 | // Look for the processor.
69 | this.executeProcessor(evt, e, ...data);
70 | }
71 | }
72 | }
73 | }
74 | }
75 | class RuleInterfaceEvent extends InterfaceEvent {
76 | event: string;
77 | description: string;
78 | fnProcessor: (rule: RuleInterfaceEvent, options:any, vars: any, logger: any, webApp: any, sys: PoolSystem, state: State, data: any) => void;
79 | processorBound: boolean = false;
80 | }
81 | export interface IRuleInterfaceEvent {
82 | event: string,
83 | description: string,
84 | processor?: string
85 | }
86 |
87 |
88 |
--------------------------------------------------------------------------------