From 663c111e1ed661e76099385e17c1855fc382de2a Mon Sep 17 00:00:00 2001 From: jsjames Date: Mon, 8 Jul 2024 14:58:35 -0700 Subject: [PATCH] [pentair] Many enhancements since original commit, including (#13485) * Updated per design review comments * Added unitHint to Dimensionless items Signed-off-by: Jeff James Signed-off-by: Ciprian Pascu --- bundles/org.openhab.binding.pentair/README.md | 298 ++++--- bundles/org.openhab.binding.pentair/pom.xml | 5 - .../internal/PentairBindingConstants.java | 168 ++-- .../pentair/internal/PentairPacket.java | 217 ----- .../internal/PentairPacketHeatSetPoint.java | 72 -- .../internal/PentairPacketIntellichlor.java | 105 --- .../internal/PentairPacketPumpStatus.java | 89 -- .../pentair/internal/PentairPacketStatus.java | 121 --- .../internal/actions/PentairBaseActions.java | 41 + .../actions/PentairControllerActions.java | 289 +++++++ .../actions/PentairIntelliFloActions.java | 167 ++++ .../internal/actions/PentairWriter.java | 175 ++++ .../config/PentairBaseBridgeConfig.java | 30 + .../config/PentairBaseThingConfig.java | 27 + .../config/PentairIPBridgeConfig.java | 19 +- .../config/PentairSerialBridgeConfig.java | 14 +- .../discovery/PentairDiscoveryService.java | 93 ++ .../{ => factory}/PentairHandlerFactory.java | 39 +- .../handler/PentairBaseBridgeHandler.java | 655 +++++++------- .../handler/PentairBaseThingHandler.java | 169 +++- .../handler/PentairControllerHandler.java | 817 ++++++++++++++++++ .../handler/PentairEasyTouchHandler.java | 498 ----------- .../handler/PentairIPBridgeHandler.java | 107 +-- .../handler/PentairIntelliChemHandler.java | 167 ++++ .../handler/PentairIntelliChlorHandler.java | 179 ++-- .../handler/PentairIntelliFloHandler.java | 324 ++++--- .../handler/PentairSerialBridgeHandler.java | 159 ++-- .../helpers/PentairControllerCircuit.java | 228 +++++ .../helpers/PentairControllerLightMode.java | 63 ++ .../helpers/PentairControllerSchedule.java | 303 +++++++ .../helpers/PentairControllerStatus.java | 146 ++++ .../handler/helpers/PentairHeatStatus.java | 93 ++ .../handler/helpers/PentairIntelliChem.java | 372 ++++++++ .../handler/helpers/PentairPumpStatus.java | 102 +++ .../internal/parser/PentairBasePacket.java | 109 +++ .../parser/PentairIntelliChlorPacket.java | 133 +++ .../internal/parser/PentairParser.java | 251 ++++++ .../parser/PentairStandardPacket.java | 126 +++ .../pentair/internal/utils/ExpiringCache.java | 155 ++++ .../resources/OH-INF/i18n/pentair.properties | 276 +++++- .../resources/OH-INF/thing/controller.xml | 350 ++++++++ .../main/resources/OH-INF/thing/easytouch.xml | 136 --- .../resources/OH-INF/thing/intellichem.xml | 258 ++++++ .../resources/OH-INF/thing/intellichlor.xml | 87 +- .../resources/OH-INF/thing/intelliflo.xml | 60 +- .../main/resources/OH-INF/thing/ip_bridge.xml | 15 +- .../resources/OH-INF/thing/serial_bridge.xml | 12 +- .../src/test/data/easytouch8.dat | 740 ++++++++++++++++ .../src/test/data/easytouch8b.dat | 90 ++ .../src/test/data/nodejs-capture.dat | 276 ++++++ .../PentairControllerScheduleTest.java | 122 +++ .../internal/PentairControllerStatusTest.java | 118 +++ .../internal/PentairHeatStatusTest.java | 77 ++ .../internal/PentairIntelliChemTest.java | 237 +++++ .../pentair/internal/PentairParserTest.java | 202 +++++ .../internal/PentairPumpStatusTest.java | 100 +++ .../pentair/internal/TestUtilities.java | 70 ++ .../handler/PentairControllerHandlerTest.java | 211 +++++ .../PentairIntelliChemHandlerTest.java | 163 ++++ .../PentairIntelliChlorHandlerTest.java | 187 ++++ .../handler/PentairIntelliFloHandlerTest.java | 177 ++++ 61 files changed, 8920 insertions(+), 2169 deletions(-) delete mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacket.java delete mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketHeatSetPoint.java delete mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketIntellichlor.java delete mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketPumpStatus.java delete mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketStatus.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairBaseActions.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairControllerActions.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairIntelliFloActions.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairWriter.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairBaseBridgeConfig.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairBaseThingConfig.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/discovery/PentairDiscoveryService.java rename bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/{ => factory}/PentairHandlerFactory.java (60%) create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairControllerHandler.java delete mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairEasyTouchHandler.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChemHandler.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerCircuit.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerLightMode.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerSchedule.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerStatus.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairHeatStatus.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairIntelliChem.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairPumpStatus.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairBasePacket.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairIntelliChlorPacket.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairParser.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairStandardPacket.java create mode 100755 bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/utils/ExpiringCache.java create mode 100644 bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/controller.xml delete mode 100644 bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/easytouch.xml create mode 100644 bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intellichem.xml create mode 100644 bundles/org.openhab.binding.pentair/src/test/data/easytouch8.dat create mode 100644 bundles/org.openhab.binding.pentair/src/test/data/easytouch8b.dat create mode 100644 bundles/org.openhab.binding.pentair/src/test/data/nodejs-capture.dat create mode 100644 bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairControllerScheduleTest.java create mode 100644 bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairControllerStatusTest.java create mode 100644 bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairHeatStatusTest.java create mode 100644 bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairIntelliChemTest.java create mode 100644 bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairParserTest.java create mode 100644 bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairPumpStatusTest.java create mode 100644 bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/TestUtilities.java create mode 100644 bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairControllerHandlerTest.java create mode 100644 bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChemHandlerTest.java create mode 100644 bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChlorHandlerTest.java create mode 100644 bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliFloHandlerTest.java diff --git a/bundles/org.openhab.binding.pentair/README.md b/bundles/org.openhab.binding.pentair/README.md index d9610bbfa02..2f123308996 100644 --- a/bundles/org.openhab.binding.pentair/README.md +++ b/bundles/org.openhab.binding.pentair/README.md @@ -1,8 +1,10 @@ # Pentair Pool This is an openHAB binding for a Pentair Pool System. -It is based on combined efforts of many on the internet in reverse-engineering the proprietary Pentair protocol (see References section). -The binding was developed and tested on a system with a Pentair EasyTouch controller, but should operate with other Pentair systems. +It is based on combined efforts of many on the Internet in reverse-engineering the proprietary Pentair protocol (see References section). +The binding was developed and tested on a system with a Pentair EasyTouch controller, but will also operate with the Pentair IntelliTouch/SunTouch controllers. +Note that with the Pentair IntelliCenter controllers, the functionality will be limited since that utilizes a different protocol which has not yet been implemented. +Further, if there is interest to improve functionality or address issues with any of the Pentair controllers, please reach out to the binding author. ## Hardware Setup @@ -17,24 +19,26 @@ I have cited several of those in the References section below. ### Connecting adapter to your system A USB or serial RS-485 interface or IP based interface can be used to interface to the Pentair system bus. -The binding includes 2 different bridge Things depending on which type of interface you use, serial_bridge or ip_bridge. +The binding includes 2 different Bridges depending on which type of interface you use, serial_bridge or ip_bridge. If your openHAB system is physically located far from your Pentair equipment or indoor control panel, you can use a Raspberry Pi or other computer to redirect USB/serial port traffic over the internet using a program called ser2sock (see Reference section). An example setup would run the following command: "ser2sock -p 10000 -s /dev/ttyUSB1 -b 9600 -d". -Note: This is the setup utlized for the majority of my testing of this binding. -Note: If you are on a Linux system, the framework may not see a symbolically linked device (i.e. /dev/ttyRS485). -To use a symbolically linked device, add the following line to _/etc/default/openhab_ `EXTRA_JAVA_OPTS="-Dgnu.io.rxtx.SerialPorts=/dev/ttyRS485"` +Note: This is the setup utilized for the majority of my testing of this binding. Once you have the interface connected to your system, it is best to test basic connectivity. Note the protocol is a binary protocol (not ASCII text based) and in order to view the communication packets, one must use a program capable of a binary/HEX mode. If connected properly, you will see a periodic traffic with packets staring with FF00FFA5. -This is the preamble for Pentairs communication packet. +This is the preamble for Pentair's communication packet. + After you see this traffic, you can proceed to configuring the Pentair binding in openHAB. +Note: Many adapters use A and B to represent Data+ and Data-. There is no reliable standard for determining which is Data+ and Data-. If you connect the system in reverse, you will still see serial data, however it will be corrupted. Look at your data coming from your device and look for a repeated "FFa5". If you don't see that preamble reliably, you may try switching your data lines." + #### USB/Serial interface -For a USB/Serial interface, you can use most terminal emulators. For Linux, you can use minicom with the following options: `minicom -H -D /dev/ttyUSB1 -b 9600` +For a USB/Serial interface, you can use most terminal emulators. +For Linux, you can use minicom with the following options: `minicom -H -D /dev/ttyUSB1 -b 9600` #### IP interface @@ -42,27 +46,30 @@ For an IP based interface (or utilizing ser2sock) on a Linux system, you can use ### Pentair Controller panel configuration -In order for the Pentair EasyTouch controller to receive commands from this binding, you may need to enable "Spa-side" remote on the controller itself. +In order for the Pentair controller to receive commands from this binding, you may need to enable "Spa-side" remote on the controller itself. ## Supported Things This binding supports the following thing types: -| Thing | Thing Type | Description | +| ThingType UID | Thing Type | Description | | --------------- | :--------: | --------------------------------------- | | ip_bridge | Bridge | A TCP network RS-485 bridge device. | | serial_bridge | Bridge | A USB or serial RS-485 device. | -| EasyTouch | Thing | Pentiar EasyTouch pool controller. | -| Intelliflo Pump | Thing | Pentair Intelliflo variable speed pump. | -| Intellichlor | Thing | Pentair Intellichlor chlorinator. | +| controller | Thing | Pentair EasyTouch, SunTouch, or IntelliTouch pool controller. | +| intelliflo | Thing | Pentair IntelliFlo variable speed pump. | +| intellichlor | Thing | Pentair IntelliChlor chlorinator. | +| intellichem | Thing | Pentair IntelliChem. ## Binding Configuration -There are no overall binding configurations that need to be set up as all configuration is done at the "Thing" level. +There are no overall binding configurations that need to be set up as all configuration is done at the "Bridge/Thing" level. -## Thing Configuration +## Bridge Configuration -The following table shows the available configuration parameters for each thing. +A Bridge item must first be configured to gain access to the Pentair bus. +This can be done via the interactive setup pages in openHAB or manually through a .thing configuration file. +The following table shows the parameters for each Bridge. | Thing | Configuration Parameters | | ------------- | ------------------------------------------------------------ | @@ -72,120 +79,200 @@ The following table shows the available configuration parameters for each thing. | serial_bridge | serialPort - Serial port for the IT-100s bridge - Required. | | | baud - Baud rate of the IT-100 bridge - Not Required - default = 9600. | | | pollPeriod - Period of time in minutes between the poll command being sent to the IT-100 bridge - Not Required - default=1. | -| | id - ID to use when communciating on Pentair control bus - default = 34. | - -Currently automatic discovery is not supported. -Here is an example of a thing configuration file called 'pentair.things': +| | id - ID to use when communicating on Pentair control bus - default = 34. | ```java Bridge pentair:ip_bridge:1 [ address="192.168.1.202", port=10001 ] { - easytouch main [ id=16 ] + controller main [ id=16 ] intelliflo pump1 [ id=96 ] intellichlor ic40 + intellichem chem } ``` For a serial bridge you would use a configuration similar to this, again saved as 'pentair.things': + ```java Bridge pentair:serial_bridge:1 [ serialPort="/dev/ttyUSB0" ] { - easytouch main [ id=16 ] + controller main [ id=16 ] intelliflo pump1 [ id=96 ] intellichlor ic40 + intellichem chem } ``` -## Channels +## Things & Channels -Pentair things support a variety of channels as seen below in the following table: +### Thing: Controller -| Channel | Item Type | Description | -| -------------------- | --------- | ------------------------------------------------------------ | -| EasyTouch Controller | | | -| pooltemp | Number | Current pool temperature (readonly) | -| spatemp | Number | Current spa temperature (readonly) | -| airtemp | Number | Current air temperature (readonly) | -| solartemp | Number | Current solar temperature (readonly) | -| poolheatmode | Number | Current heat mode setting for pool (readonly): 0=Off, 1=Heater, 2=Solar Preferred, 3=Solar | -| poolheatmodestr | String | Current heat mode setting for pool in string form (readonly) | -| spaheatmode | Number | Current heat mode setting for spa (readonly): 0=Off, 1=Heater, 2=Solar Preferred, 3=Solar | -| spaheatmodestr | String | Current heat mode setting for spa in string form (readonly)> | -| poolsetpoint | Number | Current pool temperature set point | -| spasetpoint | Number | Current spa temperature set point | -| heatactive | Number | Heater mode is active | -| pool | Switch | Primary pool mode | -| spa | Switch | Spa mode | -| aux1 | Switch | Aux1 mode | -| aux2 | Switch | Aux2 mode | -| aux3 | Switch | Aux3 mode | -| aux4 | Switch | Aux4 mode | -| aux5 | Switch | Aux5 mode | -| aux6 | Switch | Aux6 mode | -| aux7 | Switch | Aux7 mode | -| feature1 | Switch | Feature1 mode | -| feature2 | Switch | Feature2 mode | -| feature3 | Switch | Feature3 mode | -| feature4 | Switch | Feature4 mode | -| feature5 | Switch | Feature5 mode | -| feature6 | Switch | Feature6 mode | -| feature7 | Switch | Feature7 mode | -| feature8 | Switch | Feature8 mode | -| IntelliChlor | | | -| saltoutput | Number | Current salt output % (readonly) | -| salinity | Number | Salinity (ppm) (readonly) | -| IntelliFlo Pump | | | -| run | Number | Pump running (readonly) | -| drivestate | Number | Pump drivestate (readonly) | -| mode | Number | Pump mode (readonly) | -| rpm | Number | Pump RPM (readonly) | -| power | Number | Pump power in Watts (readonly) | -| error | Number | Pump error (readonly) | -| ppc | Number | Pump PPC? (readonly) | +Represents and interfaces with a Pentair pool controller in the system. This binding should work for both Intellitouch and EasyTouch systems. +Feature availability is dependent on the version of hardware and firmware versions of your specific controller. -## Full Example +#### Synchronize Time -The following is an example of an item file (pentair.items), you can change the °F to °C if you are using metric temperature units: +This configuration setting will instruct the binding to automatically update the controller's clock every 24 hours with the value from the openHAB server. +This is useful to keep the pool system clock set correct and automatically adjust for daylight savings time. + +| Channel Group | Channel | Type | | Description | +| :------------------------------: | :-------: | :----: | :-: | :--------------: | +| pool, spa, aux[1-8], feature[1-8] | switch | Switch | RW | Indicates the particulcar circuit or feature is on or off. | +| " | name | String | R | Name of circuit | +| " | feature | String | R | Feature of ciruit | +| poolheat, spaheat | setpoint | Number:Temperature | RW | Temperature setpoint | +| " | temperature | Number:Temperature | R | Current water temperature. Note, the temperature is only valid while in either pool or spa mode. | +| " | heatmode | String | R | Heat mode configured. Values: NONE, HEATER, SOLARPREFERRED, SOLAR | +| schedule[1-9] | schedule | String | RW | Summary string of schedule. | +| " | type | String | RW | Type of schedule. Note, to actually write the program to the controller, this channel must be written to with the same value 2 times within 5s. Values: NONE, NORMAL, EGGTIMER, ONCE ONLY | +| " | start | Number:Time | RW | Time of day to start schedule expressed in minutes. | +| " | end | Number:Time | RW | Time of day to end schedule expressed in minutes. In the case of EGG TIMER, this shoud be the duration. | +| " | circuit | Number | RW | Circuit/Feature the schedule will control. | +| " | days | String | RW | The days the schedule will run. S=Sunday, M=Monday, T=Tuesday, W=Wednesday, R=Thursday, F=Friday, Y=Saturday | +| status | lightmode | String | RW | Light mode. Values: OFF, ON, COLORSYNC, COLORSWIM, COLORSET, PARTY, ROMANCE, CARIBBEAN, AMERICAN, SUNSET, ROYAL, BLUE, GREEN, RED, WHITE, MAGENTA | +| " | solartemperature | Number:Temperature | R | Solar temperature sensor reading. | +| " | airtemperature | Number:Temperature | R | Air temperature sensor reading. | +| " | servicemode | Switch | R | Indicates whether controller is in service mode. | +| " | solaron | Switch | R | Indicates whether solar heat is on. | +| " | heateron | Switch | R | Indicates whether heater is on. | + +#### Working with schedules + +This binding allows both reading and writing of schedules and supports up to 9 schedules. +Programming of a schedule can be accomplished either by using the discrete channels linked to items (i.e. type, start, end, circuit, days) or you can concatenate those and use the `schedule` channel saved as a comma delimited string. +To prevent erroneous writes to the schedules though, one must write to the `type` channel the same value twice within 5 sec. + +### Thing: IntelliChlor + +Represents an Intellichlor module connected in your system. Currently, the values here are readonly. + +| Channel | Type | | Description | +| :------------------: | :----: | :-: | :---------- | +| saltOutput | Number:Dimensionless | R | Current salt output %. | +| salinity | Number:Dimensionless | R | Salinity (ppm). | +| ok | Switch | R | System is operating normally. | +| lowFlow | Switch | R | Water flow rate is low. | +| lowSalt | Switch | R | Low salt level. | +| veryLowSalt | Switch | R | Very low salt level. | +| highCurrent | Switch | R | High current level. | +| cleanCell | Switch | R | Clean cell. | +| lowVoltage | Switch | R | Low voltage. | +| lowWaterTemp | Switch | R | Water temperature is too low for chlorine generation. | +| commError | Switch | R | Communication error. | + +### Thing: IntelliFlo + +Represents and interfaces to an Intelliflo pump. +When a controller is active in the system all pump values are read only since the pump can only have one master at a time. +If no controller is present or the controller is in service mode, the pump can be controlled directly from OpenHab. + +| Channel | Type | | Description | +| :------------------: | :----: | :-: | :---------- | +| run | Switch | RW | Indicates whether the pump is running. | +| rpm | Number | RW | Pump RPM | +| gpm | Number:VolumetricFlowRate | R | Pump GPM (only valid for VF pumps) | +| power | Number:Power | R | Pump power (Watt) | +| status1 | Number | R | Pump status1. (not reversed engineered) | +| status2 | Number | R | Pump status2. (not reversed engineered) | +| runProgram | Number | RW | Run program (0 to stop, # to run) | + +### Thing: IntelliChem + +Represents and interfaces to an IntelliChem unit. +This is for monitoring of values only and IntelliChem cannot be directly controlled through this binding. +Note: This has limited testing since I don't own an IntelliChem + +| Channel | Type | | Description | +| :------------------: | :----: | :-: | :---------- | +| phReading | Number | R | Current PH reading. | +| orpReading | Number | R | Current Oxidation Reduction Potential (ORP) reading. | +| phSetPoint | Number | R | Current PH set point. | +| orpSetPoint | Number | R | Oxidation Reduction Potential (ORP) set point. | +| tank1Level | Number | R | Tank 1 level (1-7). | +| tank2Level | Number | R | Tank 2 level (1-7). | +| calciumHardness | Number:Dimensionless | R | Calcium hardness PPM (mg/L). | +| cyaReading | Number | R | Cyanuric acid reading. | +| alkalinity | Number | R | Alkalinity reading. | +| phDoserType | String | R | The doser type for PH (None, CO2, Acid). | +| orpDOserType | String | R | The doser type for ORP (None, ORP). | +| phDoserStatus | Switch | R | Whether the chemical is currently dosing. | +| orpDoserStatus | Switch | R | Whether the chemical is currently dosing. | +| phDoseTime | Number:Time | R | The time a particular chemical has been dosing. | +| orpdoseTime | Number:Time | R | The time a particular chemical has been dosing. | +| lsi | Number | R | Langelier Saturation Index. | +| saltLevel | Number:Dimensionless | R | Current salt content reading of the water (PPM). | +| temperature | Number:Temperature | R | Current temperature. | +| alarmWaterFlow | Switch | R | Water flow alarm (on = no water flow). | +| alarmPh | Switch | R | PH alarm reported. | +| alarmOrp | Switch | R | ORP alarm reported. | +| alarmPhTank | Switch | R | PH tank alarm reported. | +| alarmOrpTank | Switch | R | ORP tank alarm reported. | +| alarmProbeFault | Switch | R | Probe fault alarm reported. | +| warningPhLockout | Switch | R | Unit is in PH Lockout. | +| warningPhDailyLimitReached | Switch | R | Daily limit of PH dosing has been reached. | +| warningOrpDailLimitReached | Switch | R | Daily limit of ORP dosing has been reached. | +| warningInvalidSetup | Switch | R | Invalid setup for the unit. | +| warningChlorinatorCommError | Switch | R | Error in communicating with the Chlorinator. | + +## Example setup + +### pentair.items ```java -Group gPool "Pool" +Group gPool (All) -Number Pool_Temp "Pool Temp [%.1f °F]" (gPool) { channel = "pentair:easytouch:1:main:pooltemp" } -Number Spa_Temp "Spa Temp [%.1f °F]" (gPool) { channel = "pentair:easytouch:1:main:spatemp" } -Number Air_Temp "Air Temp [%.1f °F]" (gPool) { channel = "pentair:easytouch:1:main:airtemp" } -Number Solar_Temp "Solar Temp [%.1f °F]" (gPool) { channel = "pentair:easytouch:1:main:solartemp" } +Number:Temperature Pool_Temp "Pool Temperature" (gPool) { channel = "pentair:controller:1:main:poolheat#temperature" } +Number:Temperature Spa_Temp "Spa Temperature " (gPool) { channel = "pentair:controller:1:main:spaheat#temperature" } +Number:Temperature Air_Temp "Air Temperature" (gPool) { channel = "pentair:controller:1:main:status#airtemperature" } +Number:Temperature Solar_Temp "Solar Temperature" (gPool) { channel = "pentair:controller:1:main:status#solartemperature" } -Number PoolHeatMode "Pool Heat Mode [%d]" (gPool) { channel="pentair:easytouch:1:main:poolheatmode" } -String PoolHeatModeStr "Pool Heat Mode [%s]" (gPool) { channel="pentair:easytouch:1:main:poolheatmodestr" } -Number SpaHeatMode "Spa Heat Mode [%d]" (gPool) { channel="pentair:easytouch:1:main:spaheatmode" } -String SpaHeatModeStr "Spa Heat Mode [%s]" (gPool) { channel="pentair:easytouch:1:main:spaheatmodestr" } -Number PoolSetPoint "Pool Set Point [%.1f °F]" (gPool) { channel="pentair:easytouch:1:main:poolsetpoint" } -Number SpaSetPoint "Spa Set Point [%.1f °F]" (gPool) { channel="pentair:easytouch:1:main:spasetpoint" } -Number HeatActive "Heat Active [%d]" (gPool) { channel="pentair:easytouch:1:main:heatactive" } +String PoolHeatMode "Pool Heat Mode [%s]" (gPool) { channel="pentair:controller:1:main:poolheat#heatmode" } +String SpaHeatMode "Spa Heat Mode [%s]" (gPool) { channel="pentair:controller:1:main:spaheat#heatmode" } +Number:Temperature PoolSetPoint "Pool Set Point" (gPool) { channel="pentair:controller:1:main:poolheat#setpoint" } +Number:Temperature SpaSetPoint "Spa Set Point" (gPool) { channel="pentair:controller:1:main:spaheat#setpoint" } -Switch Mode_Spa "Spa Mode" (gPool) { channel = "pentair:easytouch:1:main:spa" } -Switch Mode_Pool "Pool Mode" (gPool) { channel = "pentair:easytouch:1:main:pool" } -Switch Mode_PoolLight "Pool Light" (gPool) { channel = "pentair:easytouch:1:main:aux1" } -Switch Mode_SpaLight "Spa Light" (gPool) { channel = "pentair:easytouch:1:main:aux2" } -Switch Mode_Jets "Jets" (gPool) { channel = "pentair:easytouch:1:main:aux3" } -Switch Mode_Boost "Boost Mode" (gPool) { channel = "pentair:easytouch:1:main:aux4" } -Switch Mode_Aux5 "Aux5 Mode" (gPool) { channel = "pentair:easytouch:1:main:aux5" } -Switch Mode_Aux6 "Aux6 Mode" (gPool) { channel = "pentair:easytouch:1:main:aux6" } -Switch Mode_Aux7 "Aux7 Mode" (gPool) { channel = "pentair:easytouch:1:main:aux7" } -Switch Mode_Spillway "Spillway Mode" (gPool) { channel = "pentair:easytouch:1:main:feature1" } +String PoolLightMode "Light Mode" (gPool) { channel="pentair:controller:1:main:status#lightmode" } -Number SaltOutput "Salt Output [%d%%]" (gPool) { channel = "pentair:intellichlor:1:ic40:saltoutput" } -Number Salinity "Salinity [%d ppm]" (gPool) { channel = "pentair:intellichlor:1:ic40:salinity" } +Number PoolHeatEnable "Pool Heat Enable [%d]" (gPool) { channel="pentair:controller:1:main:poolheatenable" } +Number SpaHeatEnable "Spa Heat Enable [%d]" (gPool) { channel="pentair:controller:1:main:spaheatenable" } -Switch Pump_Run "Pump running [%d]" (gPool) { channel = "pentair:intelliflo:1:pump1:run" } -Number Pump_DriveState "Pump drivestate [%d]" (gPool) { channel = "pentair:intelliflo:1:pump1:drivestate" } -Number Pump_Mode "Pump Mode [%d]" (gPool) { channel = "pentair:intelliflo:1:pump1:mode" } -Number Pump_RPM "Pump RPM [%d]" (gPool) { channel = "pentair:intelliflo:1:pump1:rpm" } -Number Pump_Power "Pump Power [%d W]" (gPool) { channel = "pentair:intelliflo:1:pump1:power" } -Number Pump_Error "Pump Error [%d]" (gPool) { channel = "pentair:intelliflo:1:pump1:error" } -Number Pump_PPC "Pump PPC [%d]" (gPool) { channel = "pentair:intelliflo:1:pump1:ppc" } +Switch Mode_Pool "Pool Mode" (gPool) { channel = "pentair:controller:1:main:pool#switch" } +Switch Mode_Spa "Spa" (gPool) { channel = "pentair:controller:1:main:spa#switch" } +Switch Mode_PoolLight "Pool Light" (gPool) { channel = "pentair:controller:1:main:aux1#switch" } +Switch Mode_SpaLight "Spa Light" (gPool) { channel = "pentair:controller:1:main:aux2#switch" } +Switch Mode_Jets "Jets" (gPool) { channel = "pentair:controller:1:main:aux3#switch" } +Switch Mode_Boost "Boost Mode" (gPool) { channel = "pentair:controller:1:main:aux4#switch" } +Switch Mode_Aux5 "Aux5 Mode" (gPool) { channel = "pentair:controller:1:main:aux5#switch" } +Switch Mode_Aux6 "Aux6 Mode" (gPool) { channel = "pentair:controller:1:main:aux6#switch" } +Switch Mode_Aux7 "Aux7 Mode" (gPool) { channel = "pentair:controller:1:main:aux7#switch" } + +String ModeName_Pool "Pool Name [%s]" (gPool) { channel = "pentair:controller:1:main:pool#name" } +String ModeFunc_Pool "Pool Func [%s]" (gPool) { channel = "pentair:controller:1:main:pool#function" } + +String ModeName_Spa "Spa Name [%s]" (gPool) { channel = "pentair:controller:1:main:spa#name" } +String ModeFunc_Spa "Spa Func [%s]" (gPool) { channel = "pentair:controller:1:main:spa#function" } + +Switch Delay "Heater Delay" (gPool) { channel = "pentair:controller:1:main:status#heaterdelay" } + +Number Salt_Output "Salt Output [%d%%]" (gPool) { channel = "pentair:intellichlor:1:ic40:salt_output" } +Number Salinity "Salinity [%d ppm]" (gPool) { channel = "pentair:intellichlor:1:ic40:salinity" } + +Switch Pump_Run "Pump run" (gPool) { channel = "pentair:intelliflo:1:pump1:run" } +Number Pump_RPM "Pump RPM [%d]" (gPool) { channel = "pentair:intelliflo:1:pump1:rpm" } +Number Pump_GPM "Pump GPM [%d]" (gPool) { channel = "pentair:intelliflo:1:pump1:gpm" } +Number Pump_Power "Pump Power [%d W]" (gPool) { channel = "pentair:intelliflo:1:pump1:power" } +Number Pump_Error "Pump Error [%d]" (gPool) { channel = "pentair:intelliflo:1:pump1:error" } +Number Pump_Status1 "Pump Status 1 [%d]" (gPool) { channel = "pentair:intelliflo:1:pump1:status1" } +Number Pump_Status2 "Pump Status 2 [%d]" (gPool) { channel = "pentair:intelliflo:1:pump1:status2" } + +Number Schedule1_Start "Schedule 1 start" (gPool) { channel = "pentair:controller:1:main:schedule1#start" } +Number Schedule1_End "Schedule 1 end" (gPool) { channel = "pentair:controller:1:main:schedule1#end" } +Number Schedule1_Type "Schedule 1 type" (gPool) { channel = "pentair:controller:1:main:schedule1#type" } +String Schedule1_String "Schedule 1 string" (gPool) { channel = "pentair:controller:1:main:schedule1#schedule" } +Number Schedule1_Circuit "Schedule 1 circuit" (gPool) { channel = "pentair:controller:1:main:schedule1#circuit" } +String Schedule1_Days "Schedule 1 days" (gPool) { channel = "pentair:controller:1:main:schedule1#days" } ``` -Here is an example of a complete sitemap, saved as `pentair.sitemap`. Adjust the temperature values for metric if so desired. +### sitemap ```perl sitemap pool label="Pool stuff" { @@ -194,13 +281,14 @@ sitemap pool label="Pool stuff" { Switch item=Mode_PoolLight Text item=Pool_Temp valuecolor=[>82="red",>77="orange",<=77="blue"] Setpoint item=PoolSetPoint minValue=85 maxValue=103 step=1.0 + Default item=PoolLightMode Group item=gPool label="Advanced" } Frame label="Spa" { Switch item=Mode_Spa Switch item=Mode_SpaLight Switch item=Mode_Jets - Text item=Pool_Temp valuecolor=[>82="red",>77="orange",<=77="blue"] + Text item=Spa_Temp valuecolor=[>82="red",>77="orange",<=77="blue"] Setpoint item=SpaSetPoint minValue=85 maxValue=103 step=1.0 } } @@ -208,12 +296,8 @@ sitemap pool label="Pool stuff" { ## References + Setting up RS485 and basic protocol - ser2sock GitHub - +nodejs-poolController - https://github.com/tagyoureit/nodejs-poolController -## Future Enhancements - -- Add automatic discovery of devices on RS-485 -- Add in IntelliBrite light color selection (need to capture protocol on system that has this) -- Add direct control of pump (non read-only channels) -- Fix heat active - not working on my system. diff --git a/bundles/org.openhab.binding.pentair/pom.xml b/bundles/org.openhab.binding.pentair/pom.xml index 3a0bf6e1d0f..81e12651637 100644 --- a/bundles/org.openhab.binding.pentair/pom.xml +++ b/bundles/org.openhab.binding.pentair/pom.xml @@ -13,9 +13,4 @@ org.openhab.binding.pentair openHAB Add-ons :: Bundles :: Pentair Binding - - - gnu.io;version="[3.12,6)" - - diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairBindingConstants.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairBindingConstants.java index 2e447518eb8..09eadaba159 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairBindingConstants.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairBindingConstants.java @@ -12,11 +12,6 @@ */ package org.openhab.binding.pentair.internal; -import java.util.Collections; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; @@ -36,9 +31,10 @@ public class PentairBindingConstants { public static final String SERIAL_BRIDGE = "serial_bridge"; // List of all Device Types - public static final String EASYTOUCH = "easytouch"; + public static final String CONTROLLER = "controller"; public static final String INTELLIFLO = "intelliflo"; public static final String INTELLICHLOR = "intellichlor"; + public static final String INTELLICHEM = "intellichem"; // List of all Bridge Thing Type UIDs public static final ThingTypeUID IP_BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, IP_BRIDGE); @@ -46,63 +42,137 @@ public class PentairBindingConstants { // List of all Thing Type UIDs public static final ThingTypeUID INTELLIFLO_THING_TYPE = new ThingTypeUID(BINDING_ID, INTELLIFLO); - public static final ThingTypeUID EASYTOUCH_THING_TYPE = new ThingTypeUID(BINDING_ID, EASYTOUCH); + public static final ThingTypeUID CONTROLLER_THING_TYPE = new ThingTypeUID(BINDING_ID, CONTROLLER); public static final ThingTypeUID INTELLICHLOR_THING_TYPE = new ThingTypeUID(BINDING_ID, INTELLICHLOR); + public static final ThingTypeUID INTELLICHEM_THING_TYPE = new ThingTypeUID(BINDING_ID, INTELLICHEM); - // List of all Channel ids - public static final String EASYTOUCH_POOLTEMP = "pooltemp"; - public static final String EASYTOUCH_SPATEMP = "spatemp"; - public static final String EASYTOUCH_AIRTEMP = "airtemp"; - public static final String EASYTOUCH_SOLARTEMP = "solartemp"; + public static final String PARAMETER_ID = "id"; - public static final String EASYTOUCH_SPAHEATMODE = "spaheatmode"; - public static final String EASYTOUCH_SPAHEATMODESTR = "spaheatmodestr"; - public static final String EASYTOUCH_POOLHEATMODE = "poolheatmode"; - public static final String EASYTOUCH_POOLHEATMODESTR = "poolheatmodestr"; - public static final String EASYTOUCH_HEATACTIVE = "heatactive"; + // Controller Items + public static final String PROPERTY_CONTROLLER_FIRMWAREVERSION = "firmwareVersion"; + public static final String PROPERTY_CONTROLLER_ID = "id"; - public static final String EASYTOUCH_POOLSETPOINT = "poolsetpoint"; - public static final String EASYTOUCH_SPASETPOINT = "spasetpoint"; + public static final String CONTROLLER_CONFIGSYNCTIME = "synctime"; - public static final String EASYTOUCH_POOL = "pool"; - public static final String EASYTOUCH_SPA = "spa"; - public static final String EASYTOUCH_AUX1 = "aux1"; - public static final String EASYTOUCH_AUX2 = "aux2"; - public static final String EASYTOUCH_AUX3 = "aux3"; - public static final String EASYTOUCH_AUX4 = "aux4"; - public static final String EASYTOUCH_AUX5 = "aux5"; - public static final String EASYTOUCH_AUX6 = "aux6"; - public static final String EASYTOUCH_AUX7 = "aux7"; + public static final String GROUP_CONTROLLER_STATUS = "status"; - public static final String EASYTOUCH_FEATURE1 = "feature1"; - public static final String EASYTOUCH_FEATURE2 = "feature2"; - public static final String EASYTOUCH_FEATURE3 = "feature3"; - public static final String EASYTOUCH_FEATURE4 = "feature4"; - public static final String EASYTOUCH_FEATURE5 = "feature5"; - public static final String EASYTOUCH_FEATURE6 = "feature6"; - public static final String EASYTOUCH_FEATURE7 = "feature7"; - public static final String EASYTOUCH_FEATURE8 = "feature8"; + public static final String CHANNEL_CONTROLLER_AIRTEMPERATURE = "airtemperature"; + public static final String CHANNEL_CONTROLLER_SOLARTEMPERATURE = "solartemperature"; + public static final String CHANNEL_CONTROLLER_LIGHTMODE = "lightmode"; + public static final String CHANNEL_CONTROLLER_SERVICEMODE = "servicemode"; + public static final String CHANNEL_CONTROLLER_SOLARON = "solaron"; + public static final String CHANNEL_CONTROLLER_HEATERON = "heateron"; + public static final String CHANNEL_CONTROLLER_HEATERDELAY = "heaterdelay"; - public static final String INTELLICHLOR_SALTOUTPUT = "saltoutput"; - public static final String INTELLICHLOR_SALINITY = "salinity"; + public static final String GROUP_CONTROLLER_POOLCIRCUIT = "pool"; + public static final String GROUP_CONTROLLER_SPACIRCUIT = "spa"; + public static final String GROUP_CONTROLLER_AUX1CIRCUIT = "aux1"; + public static final String GROUP_CONTROLLER_AUX2CIRCUIT = "aux2"; + public static final String GROUP_CONTROLLER_AUX3CIRCUIT = "aux3"; + public static final String GROUP_CONTROLLER_AUX4CIRCUIT = "aux4"; + public static final String GROUP_CONTROLLER_AUX5CIRCUIT = "aux5"; + public static final String GROUP_CONTROLLER_AUX6CIRCUIT = "aux6"; + public static final String GROUP_CONTROLLER_AUX7CIRCUIT = "aux7"; + public static final String GROUP_CONTROLLER_AUX8CIRCUIT = "aux8"; - public static final String INTELLIFLO_RUN = "run"; - public static final String INTELLIFLO_MODE = "mode"; - public static final String INTELLIFLO_DRIVESTATE = "drivestate"; - public static final String INTELLIFLO_POWER = "power"; - public static final String INTELLIFLO_RPM = "rpm"; - public static final String INTELLIFLO_PPC = "ppc"; + public static final String CHANNEL_CONTROLLER_CIRCUITSWITCH = "switch"; + public static final String CHANNEL_CONTROLLER_CIRCUITNAME = "name"; + public static final String CHANNEL_CONTROLLER_CIRCUITFUNCTION = "function"; + + public static final String GROUP_CONTROLLER_FEATURE1 = "feature1"; + public static final String GROUP_CONTROLLER_FEATURE2 = "feature2"; + public static final String GROUP_CONTROLLER_FEATURE3 = "feature3"; + public static final String GROUP_CONTROLLER_FEATURE4 = "feature4"; + public static final String GROUP_CONTROLLER_FEATURE5 = "feature5"; + public static final String GROUP_CONTROLLER_FEATURE6 = "feature6"; + public static final String GROUP_CONTROLLER_FEATURE7 = "feature7"; + public static final String GROUP_CONTROLLER_FEATURE8 = "feature8"; + + // List of heat group and items + public static final String GROUP_CONTROLLER_POOLHEAT = "poolheat"; + public static final String GROUP_CONTROLLER_SPAHEAT = "spaheat"; + + public static final String CHANNEL_CONTROLLER_TEMPERATURE = "temperature"; + public static final String CHANNEL_CONTROLLER_SETPOINT = "setpoint"; + public static final String CHANNEL_CONTROLLER_HEATMODE = "heatmode"; + + // List of schedule group and items + public static final String GROUP_CONTROLLER_SCHEDULE = "schedule"; + + public static final String CHANNEL_CONTROLLER_SCHEDULESAVE = "save"; + public static final String CHANNEL_CONTROLLER_SCHEDULESTRING = "schedule"; + public static final String CHANNEL_CONTROLLER_SCHEDULETYPE = "type"; + public static final String CHANNEL_CONTROLLER_SCHEDULECIRCUIT = "circuit"; + public static final String CHANNEL_CONTROLLER_SCHEDULEDAYS = "days"; + public static final String CHANNEL_CONTROLLER_SCHEDULESTART = "start"; + public static final String CHANNEL_CONTROLLER_SCHEDULEEND = "end"; + + // List of Intellichlor channel ids + public static final String CHANNEL_INTELLICHLOR_PROPERTYVERSION = "version"; + public static final String CHANNEL_INTELLICHLOR_PROPERTYMODEL = "model"; + + public static final String CHANNEL_INTELLICHLOR_SALTOUTPUT = "saltOutput"; + public static final String CHANNEL_INTELLICHLOR_SALINITY = "salinity"; + public static final String CHANNEL_INTELLICHLOR_OK = "ok"; + public static final String CHANNEL_INTELLICHLOR_LOWFLOW = "lowFlow"; + public static final String CHANNEL_INTELLICHLOR_LOWSALT = "lowSalt"; + public static final String CHANNEL_INTELLICHLOR_VERYLOWSALT = "veryLowSalt"; + public static final String CHANNEL_INTELLICHLOR_HIGHCURRENT = "highCurrent"; + public static final String CHANNEL_INTELLICHLOR_CLEANCELL = "cleanCell"; + public static final String CHANNEL_INTELLICHLOR_LOWVOLTAGE = "lowVoltage"; + public static final String CHANNEL_INTELLICHLOR_LOWWATERTEMP = "lowWaterTemp"; + public static final String CHANNEL_INTELLICHLOR_COMMERROR = "commError"; + + // IntelliChem Items + + public static final String PROPERTY_INTELLICHEM_FIRMWAREVERSION = "firmwareVersion"; + + public static final String CHANNEL_INTELLICHEM_PHREADING = "phReading"; + public static final String CHANNEL_INTELLICHEM_ORPREADING = "orpReading"; + public static final String CHANNEL_INTELLICHEM_PHSETPOINT = "phSetPoint"; + public static final String CHANNEL_INTELLICHEM_ORPSETPOINT = "orpSetPoint"; + public static final String CHANNEL_INTELLICHEM_TANK1LEVEL = "tank1Level"; + public static final String CHANNEL_INTELLICHEM_TANK2LEVEL = "tank2Level"; + public static final String CHANNEL_INTELLICHEM_CALCIUMHARDNESS = "calciumHardness"; + public static final String CHANNEL_INTELLICHEM_CYAREADING = "cyaReading"; + public static final String CHANNEL_INTELLICHEM_ALKALINITY = "alkalinity"; + public static final String CHANNEL_INTELLICHEM_PHDOSERTYPE = "phDoserType"; + public static final String CHANNEL_INTELLICHEM_ORPDOSERTYPE = "orpDoserType"; + public static final String CHANNEL_INTELLICHEM_PHDOSERSTATUS = "phDoserStatus"; + public static final String CHANNEL_INTELLICHEM_ORPDOSERSTATUS = "orpDoserStatus"; + public static final String CHANNEL_INTELLICHEM_PHDOSETIME = "phDoseTime"; + public static final String CHANNEL_INTELLICHEM_ORPDOSETIME = "orpDoesTIme"; + public static final String CHANNEL_INTELLICHEM_LSI = "lsi"; + public static final String CHANNEL_INTELLICHEM_SALTLEVEL = "saltLevel"; + + public static final String CHANNEL_INTELLICHEM_ALARMWATERFLOW = "alarmWaterFlow"; + public static final String CHANNEL_INTELLICHEM_ALARMPH = "alarmPh"; + public static final String CHANNEL_INTELLICHEM_ALARMORP = "alarmOrp"; + public static final String CHANNEL_INTELLICHEM_ALARMPHTANK = "alarmPhTank"; + public static final String CHANNEL_INTELLICHEM_ALARMORPTANK = "alarmOrpTank"; + public static final String CHANNEL_INTELLICHEM_ALARMPROBEFAULT = "alarmProbeFault"; + + public static final String CHANNEL_INTELLICHEM_WARNINGPHLOCKOUT = "warningPhLockout"; + public static final String CHANNEL_INTELLICHEM_WARNINGPHDAILYLIMITREACHED = "warningPhDailyLimitReached"; + public static final String CHANNEL_INTELLICHEM_WARNINGORPDAILYLIMITREACHED = "warningOrpDailyLimitReached"; + public static final String CHANNEL_INTELLICHEM_WARNINGINVALIDSETUP = "warningInvalidSetup"; + public static final String CHANNEL_INTELLICHEM_WARNINGCHLORINATORCOMMERROR = "warningChlorinatorCommError"; + + // List of all Intelliflo channel ids + public static final String CHANNEL_INTELLIFLO_RUN = "run"; + public static final String CHANNEL_INTELLIFLO_POWER = "power"; + public static final String CHANNEL_INTELLIFLO_RPM = "rpm"; + public static final String INTELLIFLO_GPM = "gpm"; public static final String INTELLIFLO_ERROR = "error"; + public static final String INTELLIFLO_STATUS1 = "status1"; + public static final String INTELLIFLO_STATUS2 = "status2"; public static final String INTELLIFLO_TIMER = "timer"; + public static final String INTELLIFLO_RUNPROGRAM = "runProgram"; public static final String DIAG = "diag"; // Custom Properties public static final String PROPERTY_ADDRESS = "localhost"; public static final Integer PROPERTY_PORT = 10000; - - // Set of all supported Thing Type UIDs - public static final Set SUPPORTED_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(IP_BRIDGE_THING_TYPE, SERIAL_BRIDGE_THING_TYPE, EASYTOUCH_THING_TYPE, - INTELLIFLO_THING_TYPE, INTELLICHLOR_THING_TYPE).collect(Collectors.toSet())); + public static final int DEFAULT_PENTAIR_ID = 34; } diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacket.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacket.java deleted file mode 100644 index 0462ba45e3b..00000000000 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacket.java +++ /dev/null @@ -1,217 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.pentair.internal; - -/** - * Generic class for the standard pentair package protocol. Includes helpers to generate checksum and extract key bytes - * from packet. - * - * @author Jeff James - initial contribution - * - */ -public class PentairPacket { - protected static final char[] HEXARRAY = "0123456789ABCDEF".toCharArray(); - - public static final int OFFSET = 2; - public static final int DEST = 0 + OFFSET; - public static final int SOURCE = 1 + OFFSET; - public static final int ACTION = 2 + OFFSET; - public static final int LENGTH = 3 + OFFSET; - public static final int STARTOFDATA = 4 + OFFSET; - - protected boolean initialized; - - public byte[] buf; - - /** - * Constructor for PentairPacket basic packet. - */ - public PentairPacket() { - buf = new byte[6]; - - buf[0] = (byte) 0xA5; - } - - /** - * Constructor for a PentairPackage with a byte array for the command. Typically used when generating a packet to - * send. Should include all bytes starting with A5. Do not include checksum bytes. - * - * @param buf Array of bytes to be used to populate packet. - */ - public PentairPacket(byte[] buf) { - this.buf = buf; - - initialized = true; - } - - /** - * Constructor to create a copy of PentairPacket p. Note references the same byte array as original. Used when - * coverting from a generic packet to a specialized packet. - * - * @param p PentairPacket to duplicate in new copy. - */ - public PentairPacket(PentairPacket p) { - this.buf = p.buf; - - initialized = true; - } - - /** - * Gets length of packet - * - * @return length of packet - */ - public int getLength() { - return buf[LENGTH]; - } - - /** - * Sets length of packet - * - * @param length length of packet - */ - public void setLength(int length) { - if (length > buf[LENGTH]) { - buf = new byte[length + 6]; - } - buf[LENGTH] = (byte) length; - } - - /** - * Gets action byte of packet - * - * @return action byte of packet - */ - public int getAction() { - return buf[ACTION]; - } - - /** - * Sets action byte of packet - * - * @param action - */ - public void setAction(int action) { - buf[ACTION] = (byte) action; - } - - /** - * Gets source byte or packet - * - * @return source byte of packet - */ - public int getSource() { - return buf[SOURCE]; - } - - /** - * Sets source byte of packet - * - * @param source sets source byte of packet - */ - public void setSource(int source) { - buf[SOURCE] = (byte) source; - } - - /** - * Gets destination byte of packet - * - * @return destination byte of packet - */ - public int getDest() { - return buf[DEST]; - } - - /** - * Sets destination byte of packet - * - * @param dest destination byte of packet - */ - public void setDest(int dest) { - buf[DEST] = (byte) dest; - } - - /** - * Helper function to convert byte to hex representation - * - * @param b byte to re - * @return 2 charater hex string representing the byte - */ - public static String byteToHex(int b) { - char[] hexChars = new char[2]; - - hexChars[0] = HEXARRAY[b >>> 4]; - hexChars[1] = HEXARRAY[b & 0x0F]; - - return new String(hexChars); - } - - /** - * @param bytes array of bytes to convert to a hex string. Entire buf length is converted. - * @return hex string - */ - public static String bytesToHex(byte[] bytes) { - return bytesToHex(bytes, bytes.length); - } - - /** - * @param bytes array of bytes to convert to a hex string. - * @param len Number of bytes to convert - * @return hex string - */ - public static String bytesToHex(byte[] bytes, int len) { - char[] hexChars = new char[len * 3]; - for (int j = 0; j < len; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 3] = HEXARRAY[v >>> 4]; - hexChars[j * 3 + 1] = HEXARRAY[v & 0x0F]; - hexChars[j * 3 + 2] = ' '; - } - return new String(hexChars); - } - - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return bytesToHex(buf, getLength() + 6); - } - - /** - * Used to extract a specific byte from the packet - * - * @param n number of byte (0 based) - * @return byte of packet - */ - public int getByte(int n) { - return buf[n]; - } - - /** - * Calculate checksum of the representative packet. - * - * @return checksum of packet - */ - public int calcChecksum() { - int checksum = 0, i; - - for (i = 0; i < getLength() + 6; i++) { - checksum += buf[i] & 0xFF; - } - - return checksum; - } -} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketHeatSetPoint.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketHeatSetPoint.java deleted file mode 100644 index 63d255b8c06..00000000000 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketHeatSetPoint.java +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.pentair.internal; - -/** - * Pentair heat set point packet specialization of a PentairPacket. Includes public variables for many of the reverse - * engineered - * packet content. - * - * @author Jeff James - initial contribution - * - */ -public class PentairPacketHeatSetPoint extends PentairPacket { - - protected static final int POOLTEMP = 5 + OFFSET; - protected static final int AIRTEMP = 6 + OFFSET; - protected static final int POOLSETPOINT = 7 + OFFSET; - protected static final int SPASETPOINT = 8 + OFFSET; - protected static final int HEATMODE = 9 + OFFSET; - protected static final int SOLARTEMP = 12 + OFFSET; - - protected final String[] heatmodestrs = { "Off", "Heater", "Solar Pref", "Solar" }; - - /** pool temperature set point */ - public int poolsetpoint; - /** pool heat mode - 0=Off, 1=Heater, 2=Solar Pref, 3=Solar */ - public int poolheatmode; - /** pool heat mode as a string */ - public String poolheatmodestr; - /** spa temperature set point */ - public int spasetpoint; - /** spa heat mode - 0=Off, 1=Heater, 2=Solar Pref, 3=Solar */ - public int spaheatmode; - /** spa heat mode as a string */ - public String spaheatmodestr; - - /** - * Constructor to create a specialized packet representing the generic packet. Note, the internal buffer array is - * not - * duplicated. Fills in public class members appropriate with the correct values. - * - * @param p Generic PentairPacket to create specific Status packet - */ - public PentairPacketHeatSetPoint(PentairPacket p) { - super(p); - - poolsetpoint = p.buf[POOLSETPOINT]; - poolheatmode = p.buf[HEATMODE] & 0x03; - poolheatmodestr = heatmodestrs[poolheatmode]; - - spasetpoint = p.buf[SPASETPOINT]; - spaheatmode = (p.buf[HEATMODE] >> 2) & 0x03; - spaheatmodestr = heatmodestrs[spaheatmode]; - } - - /** - * Constructure to create an empty status packet - */ - public PentairPacketHeatSetPoint() { - super(); - } -} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketIntellichlor.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketIntellichlor.java deleted file mode 100644 index bb6086031be..00000000000 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketIntellichlor.java +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.pentair.internal; - -/** - * Pentair Intellichlor specialation of a PentairPacket. Includes public variables for many of the reverse engineered - * packet content. Note, Intellichlor packet is of a different format and all helper functions in the base PentairPacket - * may not apply. - * - * This packet can be a 3 or 4 data byte packet. - * - * 10 02 50 00 00 62 10 03 - * 10 02 00 01 00 00 13 10 03 - * - * @author Jeff James - initial contribution - * - */ -public class PentairPacketIntellichlor extends PentairPacket { // 29 byte packet format - protected static final int CMD = 3; // not sure what this is, needs to be 11 for SALT_OUTPUT or SALINITY to be valid - - // 3 Length command - protected static final int SALTOUTPUT = 4; - - // 4 Length command - protected static final int SALINITY = 4; - - /** length of the packet - 3 or 4 data bytes */ - protected int length; - /** for a saltoutput packet, represents the salt output percent */ - public int saltoutput; - /** for a salinity packet, is value of salinity. Must be multiplied by 50 to get the actual salinity value. */ - public int salinity; - - /** - * Constructor for Intellichlor packet. Does not call super constructure since the Intellichlor packet is structure - * so differently - * - * @param buf - * @param length - */ - public PentairPacketIntellichlor(byte[] buf, int length) { - this.buf = buf; - this.length = length; - - if (length == 3) { - saltoutput = buf[SALTOUTPUT]; - } else if (length == 4) { - salinity = buf[SALINITY] & 0xFF; // make sure it is positive - } - } - - /** - * Constructor for empty Intellichlor packet - */ - public PentairPacketIntellichlor() { - super(); - } - - /* - * (non-Javadoc) - * - * @see org.openhab.binding.pentair.PentairPacket#getLength() - */ - @Override - public int getLength() { - return length; - } - - /* - * (non-Javadoc) - * - * @see org.openhab.binding.pentair.PentairPacket#setLength(int) - */ - @Override - public void setLength(int length) { - if (length != this.length) { - buf = new byte[length + 2]; - } - this.length = length; - } - - /** - * Gets the command byte for this packet - * - * @return command - */ - public int getCmd() { - return buf[CMD]; - } - - @Override - public String toString() { - return bytesToHex(buf, length + 5); - } -} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketPumpStatus.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketPumpStatus.java deleted file mode 100644 index 041c0bf640c..00000000000 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketPumpStatus.java +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.pentair.internal; - -/** - * Pentair pump status packet specialation of a PentairPacket. Includes public variables for many of the reverse - * engineered packet content. - * - * @author Jeff James - initial contribution - * - */ -public class PentairPacketPumpStatus extends PentairPacket { // 15 byte packet format - - protected static final int RUN = 4 + OFFSET; - protected static final int MODE = 5 + OFFSET; // Mode in pump status. Means something else in pump write/response? - protected static final int DRIVESTATE = 6 + OFFSET; // ?? Drivestate in pump status. Means something else in pump - // write/response - protected static final int WATTSH = 7 + OFFSET; - protected static final int WATTSL = 8 + OFFSET; - protected static final int RPMH = 9 + OFFSET; - protected static final int RPML = 10 + OFFSET; - protected static final int PPC = 11 + OFFSET; // ?? - protected static final int ERR = 13 + OFFSET; - protected static final int TIMER = 14 + OFFSET; // ?? Have to explore - protected static final int HOUR = 17 + OFFSET; - protected static final int MIN = 18 + OFFSET; - - /** pump is running */ - public boolean run; - - /** pump mode (1-4) */ - public int mode; - - /** pump drivestate - not sure what this specifically represents. */ - public int drivestate; - /** pump power - in KW */ - public int power; - /** pump rpm */ - public int rpm; - /** pump ppc? */ - public int ppc; - /** byte in packet indicating an error condition */ - public int error; - /** current timer for pump */ - public int timer; - /** hour or packet (based on Intelliflo time setting) */ - public int hour; - /** minute of packet (based on Intelliflo time setting) */ - public int min; - - /** - * Constructor to create a specialized packet representing the generic packet. Note, the internal buffer array is - * not - * duplicated. Fills in public class members appropriate with the correct values. - * - * @param p Generic PentairPacket to create specific Status packet - */ - public PentairPacketPumpStatus(PentairPacket p) { - super(p); - - run = (buf[RUN] == (byte) 0x0A); - mode = buf[MODE]; - drivestate = buf[DRIVESTATE]; - power = (buf[WATTSH] << 8) + buf[WATTSL]; - rpm = (buf[RPMH] << 8) + buf[RPML]; - ppc = buf[PPC]; - error = buf[ERR]; - timer = buf[TIMER]; - hour = buf[HOUR]; - min = buf[MIN]; - } - - /** - * Constructure to create an empty status packet - */ - public PentairPacketPumpStatus() { - super(); - } -} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketStatus.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketStatus.java deleted file mode 100644 index 799e3765f46..00000000000 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairPacketStatus.java +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.pentair.internal; - -/** - * Pentair status packet specialation of a PentairPacket. Includes public variables for many of the reverse engineered - * packet content. - * - * @author Jeff James - initial contribution - * - */ -public class PentairPacketStatus extends PentairPacket { // 29 byte packet format - - protected static final int HOUR = 4 + OFFSET; - protected static final int MIN = 5 + OFFSET; - protected static final int EQUIP1 = 6 + OFFSET; - protected static final int EQUIP2 = 7 + OFFSET; - protected static final int EQUIP3 = 8 + OFFSET; - protected static final int UOM = 13 + OFFSET; // Celsius (0x04) or Farenheit - protected static final int VALVES = 14 + OFFSET; // Not sure what this actually is? Doesn't seem to be valves - protected static final int UNKNOWN = 17 + OFFSET; // Something to do with heat? - protected static final int POOL_TEMP = 18 + OFFSET; - protected static final int SPA_TEMP = 19 + OFFSET; - protected static final int HEATACTIVE = 20 + OFFSET; // Does not seem to toggle for my system - protected static final int AIR_TEMP = 22 + OFFSET; - protected static final int SOLAR_TEMP = 23 + OFFSET; - protected static final int HEATMODE = 26 + OFFSET; - - /** hour byte of packet */ - public int hour; - /** minute byte of packet */ - public int min; - - /** Individual boolean values representing whether a particular ciruit is on or off */ - public boolean pool, spa, aux1, aux2, aux3, aux4, aux5, aux6, aux7; - public boolean feature1, feature2, feature3, feature4, feature5, feature6, feature7, feature8; - - /** Unit of Measure - Celsius = true, Farenheit = false */ - public boolean uom; - - /** pool temperature */ - public int pooltemp; - /** spa temperature */ - public int spatemp; - /** air temperature */ - public int airtemp; - /** solar temperature */ - public int solartemp; - - /** spa heat mode - 0 = Off, 1 = Heater, 2 = Solar Pref, 3 = Solar */ - public int spaheatmode; - /** pool heat mode - 0 = Off, 1 = Heater, 2 = Solar Pref, 3 = Solar */ - public int poolheatmode; - /** Heat is currently active - note this does not work for my system, but has been documented on the internet */ - public int heatactive; - - /** used to store packet value for reverse engineering, not used in normal operation */ - public int diag; - - /** - * Constructor to create a specialized packet representing the generic packet. Note, the internal buffer array is - * not - * duplicated. Fills in public class members appropriate with the correct values. - * - * @param p Generic PentairPacket to create specific Status packet - */ - public PentairPacketStatus(PentairPacket p) { - super(p); - - hour = buf[HOUR]; - min = buf[MIN]; - pool = (buf[EQUIP1] & 0x20) != 0; - spa = (buf[EQUIP1] & 0x01) != 0; - aux1 = (buf[EQUIP1] & 0x02) != 0; - aux2 = (buf[EQUIP1] & 0x04) != 0; - aux3 = (buf[EQUIP1] & 0x08) != 0; - aux4 = (buf[EQUIP1] & 0x10) != 0; - aux5 = (buf[EQUIP1] & 0x40) != 0; - aux6 = (buf[EQUIP1] & 0x80) != 0; - aux7 = (buf[EQUIP2] & 0x01) != 0; - - feature1 = (buf[EQUIP2] & 0x04) != 0; - feature2 = (buf[EQUIP2] & 0x08) != 0; - feature3 = (buf[EQUIP2] & 0x10) != 0; - feature4 = (buf[EQUIP2] & 0x20) != 0; - feature5 = (buf[EQUIP2] & 0x40) != 0; - feature6 = (buf[EQUIP2] & 0x80) != 0; - feature7 = (buf[EQUIP3] & 0x01) != 0; - feature8 = (buf[EQUIP3] & 0x02) != 0; - - uom = (buf[UOM] & 0x04) != 0; - - diag = buf[HEATACTIVE]; - - pooltemp = buf[POOL_TEMP]; - spatemp = buf[SPA_TEMP]; - airtemp = buf[AIR_TEMP]; - solartemp = buf[SOLAR_TEMP]; - - spaheatmode = (buf[HEATMODE] >> 2) & 0x03; - poolheatmode = buf[HEATMODE] & 0x03; - heatactive = buf[HEATACTIVE]; - } - - /** - * Constructure to create an empty status packet - */ - public PentairPacketStatus() { - super(); - } -} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairBaseActions.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairBaseActions.java new file mode 100644 index 00000000000..4664de4cad2 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairBaseActions.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.actions; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * {@link PentairBaseActions } Abstract class for all Pentair actions classes + * + * @author Jeff James - Initial contribution + * + */ +@NonNullByDefault +public class PentairBaseActions { + + @Nullable + private PentairWriter writer; + protected int id; + + public void initialize(PentairWriter writer, int id) { + this.writer = writer; + this.id = id; + } + + public PentairWriter getWriter() { + return Objects.requireNonNull(writer); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairControllerActions.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairControllerActions.java new file mode 100644 index 00000000000..2af59227c7b --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairControllerActions.java @@ -0,0 +1,289 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.actions; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.pentair.internal.handler.helpers.PentairControllerLightMode; +import org.openhab.binding.pentair.internal.handler.helpers.PentairControllerSchedule; +import org.openhab.binding.pentair.internal.handler.helpers.PentairHeatStatus; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairControllerActions } class to be used as base for all action commands to send on Pentair bus + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class PentairControllerActions extends PentairBaseActions { + private final Logger logger = LoggerFactory.getLogger(PentairControllerActions.class); + + public enum ControllerCommand { + GET_STATUS(0x02, 0x02), + GET_CLOCK_SETTINGS(0xC5, 0x05), + SET_CLOCK_SETTINGS(0x85, -1), + SET_CIRCUIT_SWITCH(0x86, 0x01), + GET_LIGHT_GROUPS(0xE7, 0x27), + SET_LIGHT_MODE(0x60, 0x01), + GET_VALVES(0xDD, 0x1D), + GET_CIRCUIT_NAME_FUNCTION(0xCB, 0x0B), + SAVE_SCHEDULE(0x91, 0x01), + GET_SCHEDULE(0xD1, 0x11), + GET_SW_VERSION(0xFD, 0xFC), + DELAY_CANCEL(0x83, 0x01), + GET_HEAT_STATUS(0xC8, 0x08), + SET_HEAT_STATUS(0x88, 0x01); + + public int send, response; + + ControllerCommand(int send, int response) { + this.send = send; + this.response = response; + } + } + + // Byte to use after 0xA5 in communicating to controller. Not sure why this changes, + // but it requires to be in sync and up-to-date + private int preambleByte = -1; + + public void setPreambleByte(int preambleByte) { + this.preambleByte = preambleByte; + } + + /** + * Method to turn on/off a circuit in response to a command from the framework + * + * @param circuit circuit number + * @param state + */ + public boolean setCircuitSwitch(int circuit, boolean state) { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.SET_CIRCUIT_SWITCH.send, (byte) 0x02, (byte) circuit, + (byte) ((state) ? 1 : 0) }; + + if (!getWriter().writePacket(packet, ControllerCommand.SET_CIRCUIT_SWITCH.response, 1)) { + logger.trace("setCircuitSwitch: Timeout"); + return false; + } + return true; + } + + /** + * Method to request clock + */ + public boolean getClockSettings() { // A5 01 10 20 C5 01 00 + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.GET_CLOCK_SETTINGS.send, (byte) 0x01, (byte) 0x00 }; + + if (!getWriter().writePacket(packet, ControllerCommand.GET_CLOCK_SETTINGS.response, 1)) { + logger.trace("getClockSettings: Timeout"); + return false; + } + return true; + } + + /** + * Method to request controller status + * Note the controller regularly sends out status, so this rarely needs to be called + * + */ + public boolean getStatus() { // A5 01 10 20 02 01 00 + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.GET_STATUS.send, (byte) 0x01, (byte) 0x00 }; + + if (!getWriter().writePacket(packet, ControllerCommand.GET_STATUS.response, 1)) { + logger.trace("requestControllerStatus: Timeout"); + return false; + } + return true; + } + + public boolean getLightGroups() { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.GET_LIGHT_GROUPS.send, (byte) 0x01, (byte) 0x00 }; + + if (!getWriter().writePacket(packet, ControllerCommand.GET_LIGHT_GROUPS.response, 1)) { + logger.trace("getLightGroups: Timeout"); + return false; + } + return true; + } + + public boolean setLightMode(int mode) { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, (byte) 0x60, + (byte) 0x02, (byte) mode, (byte) 0x00 }; + + if (!getWriter().writePacket(packet, 0x01, 1)) { + logger.trace("setLightMode: Timeout"); + return false; + } + return true; + } + + public boolean setLightMode(PentairControllerLightMode lightMode) { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.SET_LIGHT_MODE.send, (byte) 0x02, (byte) lightMode.getModeNumber(), + (byte) 0x00 }; + + if (!getWriter().writePacket(packet, ControllerCommand.SET_LIGHT_MODE.response, 1)) { + logger.trace("setLightMode: Timeout"); + return false; + } + return true; + } + + public boolean getValves() { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.GET_VALVES.send, (byte) 0x01, (byte) 0x00 }; + + if (!getWriter().writePacket(packet, ControllerCommand.GET_VALVES.response, 1)) { + logger.trace("getValves: Timeout"); + return false; + } + return true; + } + + public boolean getCircuitNameFunction(int circuit) { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.GET_CIRCUIT_NAME_FUNCTION.send, (byte) 0x01, (byte) circuit }; + + if (!getWriter().writePacket(packet, ControllerCommand.GET_CIRCUIT_NAME_FUNCTION.response, 1)) { + logger.trace("getCircuitNameFunction: Timeout"); + return false; + } + return true; + } + + public boolean getSchedule(int num) { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.GET_SCHEDULE.send, (byte) 0x01, (byte) num }; + + if (!getWriter().writePacket(packet, ControllerCommand.GET_SCHEDULE.response, 1)) { + logger.trace("getSchedule: Timeout"); + return false; + } + return true; + } + + /** + * Method to update the schedule to the controller + * + * @param p + */ + public boolean saveSchedule(PentairControllerSchedule schedule) { + PentairStandardPacket p; + + p = schedule.getWritePacket(id, preambleByte); + if (p == null) { + logger.debug("Schedule {} type is unknown.", id); + return false; + } + + schedule.setDirty(false); + + if (!getWriter().writePacket(p, 0x01, 1)) { + logger.trace("saveSchedule: Timeout"); + return false; + } + return true; + } + + public boolean getSWVersion() { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.GET_SW_VERSION.send, (byte) 0x01, (byte) 0x00 }; + + if (!getWriter().writePacket(packet, ControllerCommand.GET_SW_VERSION.response, 1)) { + logger.trace("requestSWVersion: Timeout"); + return false; + } + return true; + } + + public boolean cancelDelay() { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.DELAY_CANCEL.send, (byte) 0x01, (byte) 0x00 }; + + if (!getWriter().writePacket(packet, ControllerCommand.DELAY_CANCEL.response, 1)) { + logger.trace("cancelDelay: Timeout"); + return false; + } + return true; + } + + /** + * Method to set clock + */ + public boolean setClockSettings(int hour, int min, int dow, int day, int month, int year) { + // A5 01 10 20 85 08 0D 2A 02 1D 04 11 00 00 + + if (hour > 23) { + throw new IllegalArgumentException("hour not in range [0..23]: " + hour); + } + if (min > 59) { + throw new IllegalArgumentException("hour not in range [0..59]: " + min); + } + if (dow > 7 || dow < 1) { + throw new IllegalArgumentException("hour not in range [1..7]: " + dow); + } + if (day > 31 || day < 1) { + throw new IllegalArgumentException("hour not in range [1..31]: " + day); + } + if (month > 12 || month < 1) { + throw new IllegalArgumentException("hour not in range [1..12]: " + month); + } + if (year > 99) { + throw new IllegalArgumentException("hour not in range [0..99]: " + year); + } + + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.SET_CLOCK_SETTINGS.send, (byte) 0x08, (byte) hour, (byte) min, (byte) dow, + (byte) day, (byte) month, (byte) year, (byte) 0x00, (byte) 0x00 }; + + getWriter().writePacket(packet); + return true; + } + + public boolean getHeatStatus() { // A5 01 10 20 C8 01 00 + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.GET_HEAT_STATUS.send, (byte) 0x01, (byte) 0 }; + + if (!getWriter().writePacket(packet, ControllerCommand.GET_HEAT_STATUS.response, 1)) { + logger.trace("getHeatStatus: Timeout"); + return false; + } + return true; + } + + /** + * Method to set heat point for pool (true) of spa (false) + * + * @param Pool pool=true, spa=false + * @param temp + */ + public boolean setHeatStatus(PentairHeatStatus pentairHeatStatus) { + // [16,34,136,4,POOL HEAT Temp,SPA HEAT Temp,Heat Mode,0,2,56] + // [165, preambleByte, 16, 34, 136, 4, currentHeat.poolSetPoint, parseInt(req.params.temp), updateHeatMode, 0] + int heatmode = (pentairHeatStatus.spaHeatMode.getCode() << 2) | pentairHeatStatus.poolHeatMode.getCode(); + + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, + (byte) ControllerCommand.SET_HEAT_STATUS.send, (byte) 0x04, (byte) pentairHeatStatus.poolSetPoint, + (byte) pentairHeatStatus.spaSetPoint, (byte) heatmode, (byte) 0 }; + + if (!getWriter().writePacket(packet, ControllerCommand.SET_HEAT_STATUS.response, 1)) { + logger.trace("setHeatStatus: Timeout"); + return false; + } + return true; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairIntelliFloActions.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairIntelliFloActions.java new file mode 100644 index 00000000000..0c32484b76b --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairIntelliFloActions.java @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.actions; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pentair.internal.handler.PentairIntelliFloHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairIntelliFloActions } class to be used as base for all action commands to send on Pentair bus + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class PentairIntelliFloActions extends PentairBaseActions { + private final Logger logger = LoggerFactory.getLogger(PentairIntelliFloActions.class); + + public enum PumpCommand { + GET_STATUS(0x07, 0x07), + SET_LOCAL_OR_REMOTE_CONTROL(0x04, 0x04), + SET_ON_OR_OFF(0x06, 0x06), + SET_RPM(0x01, 0x01), + SET_RUN_PROGRAM(0x01, 0x06); + + public int send, response; + + PumpCommand(int send, int response) { + this.send = send; + this.response = response; + } + } + + @Nullable + private PentairIntelliFloHandler handler; + + public void setHandler(PentairIntelliFloHandler handler) { + this.handler = handler; + } + + public PentairIntelliFloHandler getHandler() { + return Objects.requireNonNull(handler); + } + + /* Commands to send to IntelliFlo */ + private boolean coreGetStatus() { + byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) id, (byte) 0x00 /* source */, + (byte) PumpCommand.GET_STATUS.send, (byte) 0x00 }; + + if (!getWriter().writePacket(packet, PumpCommand.GET_STATUS.response, 1)) { + logger.debug("sendRequestStatus: Timeout"); + return false; + } + return true; + } + + public boolean getStatus() { + boolean success = setLocalORRemoteControl(false); + success &= coreGetStatus(); + return success; + } + + public boolean setLocalORRemoteControl(boolean bLocal) { + byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) id, (byte) 0x00 /* source */, + (byte) PumpCommand.SET_LOCAL_OR_REMOTE_CONTROL.send, (byte) 0x01, + (bLocal) ? (byte) 0x00 : (byte) 0xFF }; + + if (!getWriter().writePacket(packet, PumpCommand.SET_LOCAL_OR_REMOTE_CONTROL.response, 1)) { + logger.debug("sendLocalOrRemoteControl: Timeout"); + return false; + } + return true; + } + + public boolean coreSetOnOROff(boolean bOn) { + byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) id, (byte) 0x00 /* source */, + (byte) PumpCommand.SET_ON_OR_OFF.send, (byte) 0x01, (bOn) ? (byte) 0x0A : (byte) 0x04 }; + + getHandler().setRunMode(bOn); + if (!getWriter().writePacket(packet, PumpCommand.SET_ON_OR_OFF.response, 1)) { + logger.trace("sendPumpOnOROff: Timeout"); + return false; + } + return true; + } + + public boolean setOnOrOff(boolean bOn) { + boolean success = setLocalORRemoteControl(false); + success &= coreSetOnOROff(bOn); + success &= coreGetStatus(); + success &= setLocalORRemoteControl(true); + return success; + } + + // sendPumpRPM - low-level call to send to pump the RPM command + private boolean coreSetRPM(int rpm) { + int rpmH, rpmL; + + rpmH = rpm / 256; + rpmL = rpm % 256; + + byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) id, (byte) 0x00 /* source */, + (byte) PumpCommand.SET_RPM.send, (byte) 0x04, (byte) 0x02, (byte) 0xC4, (byte) rpmH, (byte) rpmL }; + + if (rpm < 400 || rpm > 3450) { + throw new IllegalArgumentException("rpm not in range [400..3450]: " + rpm); + } + + getHandler().setRunMode(true); + if (!getWriter().writePacket(packet, PumpCommand.SET_RPM.response, 1)) { + logger.debug("sendPumpRPM: timeout"); + return false; + } + return true; + } + + // setPumpRPM - high-level call that includes wrapper commands and delay functions + public boolean setRPM(int rpm) { + boolean success = setLocalORRemoteControl(false); + success &= coreSetRPM(rpm); + success &= coreSetOnOROff(true); + success &= coreGetStatus(); + success &= setLocalORRemoteControl(true); + return success; + } + + // sendRunProgram - low-level call to send the command to pump + private boolean coreSetRunProgram(int program) { + if (program < 1 || program > 4) { + return false; + } + + byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) id, (byte) 0x00 /* source */, + (byte) PumpCommand.SET_RUN_PROGRAM.send, (byte) 0x04, (byte) 0x03, (byte) 0x21, (byte) 0x00, + (byte) (program << 3) }; + + getHandler().setRunMode(true); + if (!getWriter().writePacket(packet, PumpCommand.SET_RUN_PROGRAM.response, 1)) { + logger.debug("sendRunProgram: Timeout"); + return false; + } + return true; + } + + // setRunProgram - high-level call to run program - including wrapper calls + public boolean setRunProgram(int program) { + boolean success = setLocalORRemoteControl(false); + success &= coreSetRunProgram(program); + success &= coreSetOnOROff(true); + success &= coreGetStatus(); + success &= setLocalORRemoteControl(true); + return success; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairWriter.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairWriter.java new file mode 100644 index 00000000000..f78d2fd4f0a --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/actions/PentairWriter.java @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.actions; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pentair.internal.parser.PentairBasePacket; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairWriter } class to be used as base for all action commands to send on Pentair bus. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class PentairWriter { + private final Logger logger = LoggerFactory.getLogger(PentairWriter.class); + + private @Nullable OutputStream outputStream; + private int sourceId; + + // lock used to prevent multiple commands to be sent at same time. Condition waitAck will be cleared when the + // appropriate response has been received thus making writePacket blocking. + private final ReentrantLock lock = new ReentrantLock(); + private Condition waitAck = lock.newCondition(); + private int ackResponse = -1; + private CallbackWriter owner; + + public interface CallbackWriter { + void writerFailureCallback(); + } + + public PentairWriter(CallbackWriter owner) { + this.owner = owner; + } + + public void initialize(OutputStream outputStream, int sourceId) { + this.outputStream = outputStream; + this.sourceId = sourceId; + } + + public int getSourceId() { + return sourceId; + } + + public OutputStream getOutputStream() { + return Objects.requireNonNull(outputStream, "outputStream is null"); + } + + /** + * Method to write a byte array to the bus. This method is blocking until a valid response is received, or a + * failure. + * + * @param packet is the byte array to write to the bus + */ + public boolean writePacket(byte[] packet) { + return writePacket(packet, -1, 0); + } + + /** + * Method to write a byte array to the bus. This method is blocking until a valid response is received, or a + * failure. + * + * @param packet is the byte array to write to the bus + * @param response is the response to wait for + * @param retries is the number of retries + */ + public boolean writePacket(byte[] packet, int response, int retries) { + PentairStandardPacket p = new PentairStandardPacket(packet); + + return writePacket(p, response, retries); + } + + /** + * Method to write a PentairStandardPackage to the bus. This method is blocking until a valid response is received, + * or a failure. + * + * @param p {@link PentairStandardPacket} to write + */ + public boolean writePacket(PentairStandardPacket p) { + return writePacket(p, -1, 0); + } + + /** + * Method to write a package on the Pentair bus. Will add source ID and checksum to bytes written. This method is + * blocking until a valid response is received, or a failure. + * + * @param p {@link PentairStandardPacket} to write + * @param response is the expected response type to wait for from this package send. The Lock will + * clear when a response of this type is received and ackReponse is called. + * @param retries is number of retries before a time-out + */ + public boolean writePacket(PentairStandardPacket p, int response, int retries) { + boolean success = true; + OutputStream outputStream; + + outputStream = getOutputStream(); + + try { + byte[] buf; + int nRetries = retries; + + p.setByte(PentairStandardPacket.SOURCE, (byte) sourceId); + + buf = p.wrapPacketToSend(); + + lock.lock(); + this.ackResponse = response; + + do { + logger.trace("[{}] Writing packet: {}", p.getDest(), PentairBasePacket.toHexString(buf)); + + outputStream.write(buf, 0, buf.length); + outputStream.flush(); + + if (response != -1) { + logger.trace("[{}] writePacket: wait for ack (response: {}, retries: {})", p.getDest(), response, + nRetries); + success = waitAck.await(1000, TimeUnit.MILLISECONDS); // success will be false if timeout + nRetries--; + } + } while (!success && (nRetries >= 0)); + } catch (IOException e) { + owner.writerFailureCallback(); + return false; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + owner.writerFailureCallback(); + return false; + } finally { + lock.unlock(); + } + + if (!success) { + logger.trace("[{}] writePacket: timeout", p.getDest()); + } + + return success; + } + + /** + * Method to acknowledge an ack or response packet has been sent + * + * @param cmdresponse is the command that was seen as a return. This is validate against that this was the response + * before signally a return. + */ + public void ackResponse(int response) { + if (response != ackResponse) { + return; + } + + lock.lock(); + waitAck.signalAll(); + lock.unlock(); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairBaseBridgeConfig.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairBaseBridgeConfig.java new file mode 100644 index 00000000000..46d067903b5 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairBaseBridgeConfig.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.config; + +import static org.openhab.binding.pentair.internal.PentairBindingConstants.DEFAULT_PENTAIR_ID; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PentairBaseBridgeConfig } class contains the base parameters in all bridges + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class PentairBaseBridgeConfig { + /** ID to use when sending commands on the Pentair RS485 bus. */ + public int id = DEFAULT_PENTAIR_ID; + /** enable automatic discovery */ + public boolean discovery = true; +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairBaseThingConfig.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairBaseThingConfig.java new file mode 100644 index 00000000000..849044173a0 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairBaseThingConfig.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PentairBaseThingConfig } class contains configuration parameters for all Pentair child things. + * + * @author Jeff James - initial contribution + * + */ +@NonNullByDefault +public class PentairBaseThingConfig { + /** ID of thing on the Pentair RS485 bus. */ + public int id; +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairIPBridgeConfig.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairIPBridgeConfig.java index 864542af689..8aa469de35d 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairIPBridgeConfig.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairIPBridgeConfig.java @@ -12,23 +12,16 @@ */ package org.openhab.binding.pentair.internal.config; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** - * Configuration parameters for IP Bridge + * The {@link PentairIPBridgeConfig } class contains the parameters for IP Bridge * * @author Jeff James - initial contribution * */ +@NonNullByDefault public class PentairIPBridgeConfig { - /** IP address of destination */ - public String address; - /** Port of destination */ - public Integer port; - - /** ID to use when sending commands on the Pentair RS485 bus. */ - public Integer id; - - @Override - public String toString() { - return getClass().getSimpleName() + "{ address=" + address + ", port=" + port + ", id=" + id + "}"; - } + public String address = ""; + public int port = 10000; } diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairSerialBridgeConfig.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairSerialBridgeConfig.java index 5637bf1c80a..9ff634af550 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairSerialBridgeConfig.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/config/PentairSerialBridgeConfig.java @@ -12,20 +12,16 @@ */ package org.openhab.binding.pentair.internal.config; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** - * Configuration parameters for Serial Bridge + * The {@link PentairSerialBridgeConfig } class contains the configuration parameters for Serial Bridge * * @author Jeff James - initial contribution * */ +@NonNullByDefault public class PentairSerialBridgeConfig { /** path or name of serial port, usually /dev/ttyUSB0 format for linux/mac, COM1 for windows */ - public String serialPort; - /** ID to use when sending commands on the Pentair RS485 bus. */ - public Integer id; - - @Override - public String toString() { - return getClass().getSimpleName() + "{ serialPort=" + serialPort + ", id=" + id + "}"; - } + public String serialPort = ""; } diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/discovery/PentairDiscoveryService.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/discovery/PentairDiscoveryService.java new file mode 100644 index 00000000000..4f088db566f --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/discovery/PentairDiscoveryService.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.discovery; + +import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; + +import java.util.Objects; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pentair.internal.handler.PentairBaseBridgeHandler; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairDiscoveryService} handles discovery of devices as they are identified by the bridge handler. + * Requests from the framework to startScan() are ignored, since no active scanning is possible. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class PentairDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService { + + public static final Set DISCOVERABLE_THING_TYPES = Set.of(CONTROLLER_THING_TYPE, + INTELLIFLO_THING_TYPE, INTELLICHLOR_THING_TYPE, INTELLICHEM_THING_TYPE); + + private final Logger logger = LoggerFactory.getLogger(PentairDiscoveryService.class); + private @Nullable PentairBaseBridgeHandler bridgeHandler; + + public PentairDiscoveryService() throws IllegalArgumentException { + super(DISCOVERABLE_THING_TYPES, 0, false); + } + + @Override + public void activate() { + super.activate(null); + } + + @Override + public void deactivate() { + super.deactivate(); + } + + @Override + protected void startScan() { + // Ignore start scan requests + } + + public void notifyDiscoveredThing(ThingTypeUID thingTypeUID, int id, String label) { + PentairBaseBridgeHandler bridgeHandler = Objects.requireNonNull(this.bridgeHandler, + "Discovery with null bridge handler."); + ThingUID bridgeUID = bridgeHandler.getThing().getUID(); + ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, label); + + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID) + .withProperty(PARAMETER_ID, id).withRepresentationProperty(PARAMETER_ID).withLabel(label).build(); + thingDiscovered(result); + logger.debug("Discovered Thing {}", thingUID); + } + + @Override + public void setThingHandler(ThingHandler handler) { + if (handler instanceof PentairBaseBridgeHandler baseBridgeHandler) { + this.bridgeHandler = baseBridgeHandler; + baseBridgeHandler.setDiscoveryService(this); + } else { + this.bridgeHandler = null; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return this.bridgeHandler; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairHandlerFactory.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/factory/PentairHandlerFactory.java similarity index 60% rename from bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairHandlerFactory.java rename to bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/factory/PentairHandlerFactory.java index 4e4ea8c42e6..f4007e4fb58 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairHandlerFactory.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/factory/PentairHandlerFactory.java @@ -10,50 +10,75 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.pentair.internal; +package org.openhab.binding.pentair.internal.factory; import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; -import org.openhab.binding.pentair.internal.handler.PentairEasyTouchHandler; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pentair.internal.handler.PentairControllerHandler; import org.openhab.binding.pentair.internal.handler.PentairIPBridgeHandler; +import org.openhab.binding.pentair.internal.handler.PentairIntelliChemHandler; import org.openhab.binding.pentair.internal.handler.PentairIntelliChlorHandler; import org.openhab.binding.pentair.internal.handler.PentairIntelliFloHandler; import org.openhab.binding.pentair.internal.handler.PentairSerialBridgeHandler; +import org.openhab.core.io.transport.serial.SerialPortManager; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; /** - * The {@link PentairHandlerFactory} is responsible for creating things and thing + * The {@link PentairHandlerFactory} is responsible for creating thing * handlers. * * @author Jeff James - Initial contribution */ + +@NonNullByDefault @Component(service = ThingHandlerFactory.class, configurationPid = "binding.pentair") public class PentairHandlerFactory extends BaseThingHandlerFactory { + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(IP_BRIDGE_THING_TYPE, + SERIAL_BRIDGE_THING_TYPE, CONTROLLER_THING_TYPE, INTELLIFLO_THING_TYPE, INTELLICHLOR_THING_TYPE, + INTELLICHEM_THING_TYPE); + + private final SerialPortManager serialPortManager; + + @Activate + public PentairHandlerFactory(final @Reference SerialPortManager serialPortManager) { + // Obtain the serial port manager service using an OSGi reference + this.serialPortManager = serialPortManager; + } + @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); } @Override - protected ThingHandler createHandler(Thing thing) { + protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (thingTypeUID.equals(IP_BRIDGE_THING_TYPE)) { return new PentairIPBridgeHandler((Bridge) thing); } else if (thingTypeUID.equals(SERIAL_BRIDGE_THING_TYPE)) { - return new PentairSerialBridgeHandler((Bridge) thing); - } else if (thingTypeUID.equals(EASYTOUCH_THING_TYPE)) { - return new PentairEasyTouchHandler(thing); + return new PentairSerialBridgeHandler((Bridge) thing, serialPortManager); + } else if (thingTypeUID.equals(CONTROLLER_THING_TYPE)) { + return new PentairControllerHandler(thing); } else if (thingTypeUID.equals(INTELLIFLO_THING_TYPE)) { return new PentairIntelliFloHandler(thing); } else if (thingTypeUID.equals(INTELLICHLOR_THING_TYPE)) { return new PentairIntelliChlorHandler(thing); + } else if (thingTypeUID.equals(INTELLICHEM_THING_TYPE)) { + return new PentairIntelliChemHandler(thing); } return null; diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairBaseBridgeHandler.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairBaseBridgeHandler.java index 82810dbefb2..65c0107a37a 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairBaseBridgeHandler.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairBaseBridgeHandler.java @@ -12,57 +12,94 @@ */ package org.openhab.binding.pentair.internal.handler; -import static org.openhab.binding.pentair.internal.PentairBindingConstants.INTELLIFLO_THING_TYPE; +import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; -import java.util.ArrayList; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.openhab.binding.pentair.internal.PentairPacket; -import org.openhab.binding.pentair.internal.PentairPacketIntellichlor; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pentair.internal.actions.PentairWriter; +import org.openhab.binding.pentair.internal.actions.PentairWriter.CallbackWriter; +import org.openhab.binding.pentair.internal.config.PentairBaseBridgeConfig; +import org.openhab.binding.pentair.internal.discovery.PentairDiscoveryService; +import org.openhab.binding.pentair.internal.parser.PentairIntelliChlorPacket; +import org.openhab.binding.pentair.internal.parser.PentairParser; +import org.openhab.binding.pentair.internal.parser.PentairParser.CallbackPentairParser; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; -import org.openhab.core.types.RefreshType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Abstract class for all common functions for different bridge implementations. Use as superclass for IPBridge and - * SerialBridge implementations. + * The {@link PentairBaseBridgeHandler } abstract class for all common functions for different bridge implementations. + * Use as superclass for IPBridge and SerialBridge implementations. * * - Implements parsing of packets on Pentair bus and dispositions to appropriate Thing * - Periodically sends query to any {@link PentairIntelliFloHandler} things * - Provides function to write packets * * @author Jeff James - Initial contribution - * */ -public abstract class PentairBaseBridgeHandler extends BaseBridgeHandler { +@NonNullByDefault +public abstract class PentairBaseBridgeHandler extends BaseBridgeHandler + implements CallbackPentairParser, CallbackWriter { private final Logger logger = LoggerFactory.getLogger(PentairBaseBridgeHandler.class); - /** input stream - subclass needs to assign in connect function */ - protected BufferedInputStream reader; - /** output stream - subclass needs to assing in connect function */ - protected BufferedOutputStream writer; - /** thread for parser - subclass needs to create/assign connect */ - protected Thread thread; - /** parser object - subclass needs to create/assign during connect */ - protected Parser parser; - /** polling job for pump status */ - protected ScheduledFuture pollingjob; - /** ID to use when sending commands on Pentair bus - subclass needs to assign based on configuration parameter */ - protected int id; - /** array to keep track of IDs seen on the Pentair bus that do not correlate to a configured Thing object */ - protected ArrayList unregistered = new ArrayList<>(); + private final PentairParser parser = new PentairParser(); + private final PentairWriter actions = new PentairWriter(this); + + // input/output stream must be assigned by sub-class in connect method + @Nullable + private BufferedInputStream inputStream; + @Nullable + private BufferedOutputStream outputStream; + + private @Nullable PentairDiscoveryService discoveryService; + private @Nullable Thread parserThread; + + private @Nullable ScheduledFuture monitorIOJob; + + private PentairBaseBridgeConfig config = new PentairBaseBridgeConfig(); + + /** array to keep track of IDs seen on the Pentair bus that are not configured yet */ + private final Set unregistered = new HashSet(); + + final Map equipment = new HashMap<>(); + + // keep accessible a static reference to the bridge. This binding will only work with a single bridge + @Nullable + private static Bridge bridge; + + @Nullable + public static Bridge getSingleBridge() { + return bridge; + } + + public void setDiscoveryService(PentairDiscoveryService discoveryService) { + this.discoveryService = discoveryService; + } /** * Gets pentair bus id @@ -70,64 +107,209 @@ public abstract class PentairBaseBridgeHandler extends BaseBridgeHandler { * @return id */ public int getId() { - return id; + return config.id; } - private enum ParserState { - WAIT_SOC, - CMD_PENTAIR, - CMD_INTELLICHLOR + public PentairWriter getBaseActions() { + return actions; } - /** - * Constructor - * - * @param bridge - */ PentairBaseBridgeHandler(Bridge bridge) { super(bridge); + parser.setCallback(this); + } + + @Override + public Collection> getServices() { + return Collections.singleton(PentairDiscoveryService.class); } @Override public void handleCommand(ChannelUID channelUID, Command command) { - if (command instanceof RefreshType) { - logger.debug("Bridge received refresh command"); - } } @Override public void initialize() { - logger.debug("initializing Pentair Bridge handler."); + this.config = getConfigAs(PentairBaseBridgeConfig.class); - connect(); + if (PentairBaseBridgeHandler.bridge != null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.bridge-duplicate"); + return; + } - pollingjob = scheduler.scheduleWithFixedDelay(new PumpStatus(), 10, 120, TimeUnit.SECONDS); + PentairBaseBridgeHandler.bridge = this.getThing(); + + updateStatus(ThingStatus.UNKNOWN); + + this.monitorIOJob = scheduler.scheduleWithFixedDelay(this::monitorIO, 60, 30, TimeUnit.SECONDS); + + baseConnect(); } @Override public void dispose() { - logger.debug("Handler disposed."); - pollingjob.cancel(true); - disconnect(); + PentairBaseBridgeHandler.bridge = null; + + ScheduledFuture monitorIOJob = this.monitorIOJob; + if (monitorIOJob != null) { + monitorIOJob.cancel(true); + } + + baseDisconnect(); + } + + /* + * Custom function to call during initialization to notify the bridge. childHandlerInitialized is not called + * until the child thing actually goes to the ONLINE status. + */ + public void childHandlerInitializing(ThingHandler childHandler, Thing childThing) { + if (childHandler instanceof PentairBaseThingHandler baseThingHandler) { + equipment.put(baseThingHandler.getPentairID(), baseThingHandler); + unregistered.remove(baseThingHandler.getPentairID()); + } + } + + @Override + public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) { + if (childHandler instanceof PentairBaseThingHandler baseThingHandler) { + equipment.remove(baseThingHandler.getPentairID()); + } } /** * Abstract method for creating connection. Must be implemented in subclass. + * Return 0 if all goes well. Must call setInputStream and setOutputStream before exciting. */ - protected abstract void connect(); + protected abstract boolean connect(); - /** - * Abstract method for disconnect. Must be implemented in subclass - */ protected abstract void disconnect(); + private void baseConnect() { + if (getThing().getStatus() == ThingStatus.ONLINE) { + return; + } + + // montiorIOJob will only start after a successful connection + if (monitorIOJob == null) { + monitorIOJob = scheduler.scheduleWithFixedDelay(this::monitorIO, 60, 30, TimeUnit.SECONDS); + } + + if (!connect()) { + // if connect() sets to offline, preserve the StatusDetail\ + if (getThing().getStatus() != ThingStatus.OFFLINE) { + updateStatus(ThingStatus.OFFLINE); + } + return; + } + + Thread parserThread = new Thread(parser, "OH-pentair-" + this.getThing().getUID() + "-parser"); + this.parserThread = parserThread; + + parserThread.setDaemon(true); + parserThread.start(); + + if (inputStream == null || outputStream == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication-error.iostream-error "); + return; + } + + updateStatus(ThingStatus.ONLINE); + } + + private void baseDisconnect() { + // Preserve OFFLINE status detail if already OFFLINE + if (getThing().getStatus() != ThingStatus.OFFLINE) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } + + Thread parserThread = this.parserThread; + if (parserThread != null) { + try { + parserThread.interrupt(); + parserThread.join(3000); // wait for thread to complete + } catch (InterruptedException e) { + // do nothing + } + parserThread = null; + } + + BufferedInputStream reader = this.inputStream; + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + logger.debug("setInputStream: Exception error while closing: {}", e.getMessage()); + } + } + + BufferedOutputStream writer = this.outputStream; + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + logger.debug("setOutputStream: Exception error while closing: {}", e.getMessage()); + } + } + + disconnect(); + } + + public void setInputStream(InputStream inputStream) { + BufferedInputStream reader = this.inputStream; + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + logger.trace("setInputStream: Exception error while closing: {}", e.getMessage()); + } + } + + this.inputStream = new BufferedInputStream(inputStream); + parser.setInputStream(inputStream); + } + + public void setOutputStream(OutputStream outputStream) { + BufferedOutputStream writer = this.outputStream; + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + logger.trace("setOutputStream: Exception error while closing: {}", e.getMessage()); + } + } + + writer = new BufferedOutputStream(outputStream); + this.outputStream = writer; + + actions.initialize(writer, getId()); + } + + // method to poll to try and reconnect upon being disconnected. Note this should only be started on an initial + private void monitorIO() { + ThingStatus thingStatus = getThing().getStatus(); + + if (thingStatus == ThingStatus.ONLINE) { + // Check if parser thread has terminated and if it has reconnect. This will take down the interface and + // restart the interface. + Thread parserThread = Objects.requireNonNull(this.parserThread); + if (!parserThread.isAlive()) { + baseDisconnect(); + baseConnect(); + } + } else if (thingStatus == ThingStatus.OFFLINE) { + baseConnect(); + } + } + /** * Helper function to find a Thing assigned to this bridge with a specific pentair bus id. * - * @param id Pentiar bus id + * @param id Pentair bus id * @return Thing object. null if id is not found. */ - public Thing findThing(int id) { + public @Nullable Thing findThing(int id) { List things = getThing().getThings(); for (Thing t : things) { @@ -141,313 +323,116 @@ public abstract class PentairBaseBridgeHandler extends BaseBridgeHandler { return null; } - /** - * Class for throwing an End of Buffer exception, used in getByte when read returns a -1. This is used to signal an - * exit from the parser. - * - * @author Jeff James - initial contribution - * - */ - public class EOBException extends Exception { - private static final long serialVersionUID = 1L; - } + public @Nullable PentairControllerHandler findController() { + List things = getThing().getThings(); - /** - * Gets a single byte from reader input stream - * - * @param s used during debug to identify proper state transitioning - * @return next byte from reader - * @throws EOBException - * @throws IOException - */ - private int getByte(ParserState s) throws EOBException, IOException { - int c = 0; + for (Thing t : things) { + PentairBaseThingHandler handler = (PentairBaseThingHandler) t.getHandler(); - c = reader.read(); - if (c == -1) { - // EOBException is thrown if no more bytes in buffer. This exception is used to exit the parser when full - // packet is not in buffer - throw new EOBException(); - } - - return c; - } - - /** - * Gets a specific number of bytes from reader input stream - * - * @param buf byte buffer to store bytes - * @param start starting index to store bytes - * @param n number of bytes to read - * @return number of bytes read - * @throws EOBException - * @throws IOException - */ - private int getBytes(byte[] buf, int start, int n) throws EOBException, IOException { - int i; - int c; - - for (i = 0; i < n; i++) { - c = reader.read(); - if (c == -1) { - // EOBException is thrown if no more bytes in buffer. This exception is used to exit the parser when - // full packet is not in buffer - throw new EOBException(); - } - - buf[start + i] = (byte) c; - } - - return i; - } - - /** - * Job to send pump query status packages to all Intelliflo Pump things in order to see the status. - * Note: From the internet is seems some FW versions of EasyTouch controllers send this automatically and this the - * pump status packets can just be snooped, however my controller version does not do this. No harm in sending. - * - * @author Jeff James - * - */ - class PumpStatus implements Runnable { - @Override - public void run() { - List things = getThing().getThings(); - - // FF 00 FF A5 00 60 10 07 00 01 1C - byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) 0x00, (byte) id, (byte) 0x07, (byte) 0x00 }; - - PentairPacket p = new PentairPacket(packet); - - for (Thing t : things) { - if (!t.getThingTypeUID().equals(INTELLIFLO_THING_TYPE)) { - continue; - } - - p.setDest(((PentairIntelliFloHandler) t.getHandler()).id); - writePacket(p); - try { - Thread.sleep(300); // make sure each pump has time to respond - } catch (InterruptedException e) { - break; - } + if (handler instanceof PentairControllerHandler controllerHandler) { + return controllerHandler; } } + + return null; } - /** - * Implements the thread to read and parse the input stream. Once a packet can be indentified, it locates the - * representive sending Thing and dispositions the packet so it can be further processed. - * - * @author Jeff James - initial implementation - * - */ - class Parser implements Runnable { - @Override - public void run() { - logger.debug("parser thread started"); - byte[] buf = new byte[40]; - int c; - int chksum, i, length; - Thing thing; - PentairBaseThingHandler thinghandler; + public @Nullable PentairIntelliChlorHandler findIntellichlor() { + List things = getThing().getThings(); - ParserState parserstate = ParserState.WAIT_SOC; + for (Thing t : things) { + PentairBaseThingHandler handler = (PentairBaseThingHandler) t.getHandler(); - try { - while (!Thread.currentThread().isInterrupted()) { - c = getByte(parserstate); + if (handler instanceof PentairIntelliChlorHandler intelliChlorHandler) { + return intelliChlorHandler; + } + } - switch (parserstate) { - case WAIT_SOC: - if (c == 0xFF) { // for CMD_PENTAIR, we need at lease one 0xFF - do { - c = getByte(parserstate); - } while (c == 0xFF); // consume all 0xFF + return null; + } - if (c == 0x00) { - parserstate = ParserState.CMD_PENTAIR; - } + @Override + public void onPentairPacket(PentairStandardPacket p) { + PentairBaseThingHandler thinghandler; + + int source = p.getSource(); + thinghandler = equipment.get(source); + + if (thinghandler == null) { + int sourceType = (source >> 4); + + if (sourceType == 0x02) { // control panels are 0x2*, don't treat as an + // unregistered device + logger.debug("[{}] Command from control panel device: {}", source, p); + } else if (!unregistered.contains(source)) { // if not yet seen discover + PentairDiscoveryService discoveryService = this.discoveryService; + if (discoveryService != null) { + if (sourceType == 0x01) { // controller + PentairControllerHandler handler = this.findController(); + if (handler == null) { // only register one controller + if (config.discovery) { + discoveryService.notifyDiscoveredThing(CONTROLLER_THING_TYPE, source, CONTROLLER); } - - if (c == 0x10) { - parserstate = ParserState.CMD_INTELLICHLOR; - } - break; - case CMD_PENTAIR: - parserstate = ParserState.WAIT_SOC; // any break will go back to WAIT_SOC - - if (c != 0xFF) { - logger.debug("FF00 !FF"); - break; - } - - if (getBytes(buf, 0, 6) != 6) { // read enough to get the length - logger.debug("Unable to read 6 bytes"); - - break; - } - if (buf[0] != (byte) 0xA5) { - logger.debug("FF00FF !A5"); - break; - } - - length = buf[5]; - if (length == 0) { - logger.debug("Command length of 0"); - } - if (length > 34) { - logger.debug("Received packet longer than 34 bytes: {}", length); - break; - } - if (getBytes(buf, 6, length) != length) { // read remaining packet - break; - } - - chksum = 0; - for (i = 0; i < length + 6; i++) { - chksum += buf[i] & 0xFF; - } - - c = getByte(parserstate) << 8; - c += getByte(parserstate); - - if (c != chksum) { - logger.debug("Checksum error: {}", PentairPacket.bytesToHex(buf, length + 6)); - break; - } - - PentairPacket p = new PentairPacket(buf); - - thing = findThing(p.getSource()); - if (thing == null) { - if ((p.getSource() >> 8) == 0x02) { // control panels are 0x3*, don't treat as an - // unregistered device - logger.trace("Command from control panel device ({}): {}", p.getSource(), p); - } else if (!unregistered.contains(p.getSource())) { // if not yet seen, print out log - // message once - logger.info("Command from unregistered device ({}): {}", p.getSource(), p); - unregistered.add(p.getSource()); - } else { - logger.trace("Command from unregistered device ({}): {}", p.getSource(), p); - } - break; - } - - thinghandler = (PentairBaseThingHandler) thing.getHandler(); - if (thinghandler == null) { - logger.debug("Thing handler = null"); - break; - } - - logger.trace("Received pentair command: {}", p); - - thinghandler.processPacketFrom(p); - - break; - case CMD_INTELLICHLOR: - parserstate = ParserState.WAIT_SOC; - - buf[0] = 0x10; // 0x10 is included in checksum - if (c != (byte) 0x02) { - break; - } - - buf[1] = 0x2; - length = 3; - // assume 3 byte command, plus 1 checksum, plus 0x10, 0x03 - if (getBytes(buf, 2, 6) != 6) { - break; - } - - // Check to see if this is a 3 or 4 byte command - if ((buf[6] != (byte) 0x10 || buf[7] != (byte) 0x03)) { - length = 4; - - buf[8] = (byte) getByte(parserstate); - if ((buf[7] != (byte) 0x10) && (buf[8] != (byte) 0x03)) { - logger.debug("Invalid Intellichlor command: {}", - PentairPacket.bytesToHex(buf, length + 6)); - break; // invalid command - } - } - - chksum = 0; - for (i = 0; i < length + 2; i++) { - chksum += buf[i] & 0xFF; - } - - c = buf[length + 2] & 0xFF; - if (c != (chksum & 0xFF)) { // make sure it matches chksum - logger.debug("Invalid Intellichlor checksum: {}", - PentairPacket.bytesToHex(buf, length + 6)); - break; - } - - PentairPacketIntellichlor pic = new PentairPacketIntellichlor(buf, length); - - thing = findThing(0); - - if (thing == null) { - if (!unregistered.contains(0)) { // if not yet seen, print out log message - logger.info("Command from unregistered Intelliflow: {}", pic); - unregistered.add(0); - } else { - logger.trace("Command from unregistered Intelliflow: {}", pic); - } - - break; - } - - thinghandler = (PentairBaseThingHandler) thing.getHandler(); - if (thinghandler == null) { - logger.debug("Thing handler = null"); - break; - } - - thinghandler.processPacketFrom(pic); - - break; + } + } else if (sourceType == 0x06) { + if (config.discovery) { + int pumpid = (source & 0x04) + 1; + discoveryService.notifyDiscoveredThing(INTELLIFLO_THING_TYPE, source, "pump" + pumpid); + } + } else if (sourceType == 0x09) { + if (config.discovery) { + discoveryService.notifyDiscoveredThing(INTELLICHEM_THING_TYPE, source, INTELLICHEM); + } } - } - } catch (IOException e) { - logger.trace("I/O error while reading from stream: {}", e.getMessage()); - disconnect(); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); - } catch (EOBException e) { - // EOB Exception is used to exit the parser loop if full message is not in buffer. - } - logger.debug("msg reader thread exited"); + logger.debug("[{}] First command from unregistered device: {}", source, p); + unregistered.add(source); + } + } else { + logger.debug("[{}] Subsequent command from unregistered device: {}", source, p); + } + } else { + logger.trace("[{}] Received pentair command: {}", source, p); + + thinghandler.processPacketFrom(p); + actions.ackResponse(p.getAction()); } } - /** - * Method to write a package on the Pentair bus. Will add preamble and checksum to bytes written - * - * @param p {@link PentairPacket} to write - */ - public void writePacket(PentairPacket p) { - try { // FF 00 FF A5 00 60 10 07 00 01 1C - byte[] preamble = { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xFF }; - byte[] buf = new byte[5 + p.getLength() + 8]; // 5 is preamble, 8 is 6 bytes for header and 2 for checksum + @Override + public void onIntelliChlorPacket(PentairIntelliChlorPacket p) { + PentairBaseThingHandler thinghandler; - p.setSource(id); + thinghandler = equipment.get(0); - System.arraycopy(preamble, 0, buf, 0, 5); - System.arraycopy(p.buf, 0, buf, 5, p.getLength() + 6); - int checksum = p.calcChecksum(); + if (thinghandler == null) { + // Only register if the packet is sent from chlorinator (i.e. action=0x12) + int dest = p.getByte(PentairIntelliChlorPacket.DEST); + if (!unregistered.contains(0) && p.getByte(PentairIntelliChlorPacket.ACTION) == 0x12) { + PentairDiscoveryService discoveryService = this.discoveryService; - buf[p.getLength() + 11] = (byte) ((checksum >> 8) & 0xFF); - buf[p.getLength() + 12] = (byte) (checksum & 0xFF); + if (config.discovery && discoveryService != null) { + discoveryService.notifyDiscoveredThing(INTELLICHLOR_THING_TYPE, 0, INTELLICHLOR); - logger.debug("Writing packet: {}", PentairPacket.bytesToHex(buf)); - - writer.write(buf, 0, 5 + p.getLength() + 8); - writer.flush(); - } catch (IOException e) { - logger.trace("I/O error while writing stream", e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + logger.debug("[{}] First command from unregistered Intellichlor: {}", dest, p); + unregistered.add(0); + } + } else { + logger.debug("[{}] Subsequent command from unregistered Intellichlor: {}", dest, p); + } + return; } + + thinghandler.processPacketFrom(p); + } + + @Override + public void writerFailureCallback() { + baseDisconnect(); + } + + @Override + public void parserFailureCallback() { + baseDisconnect(); } } diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairBaseThingHandler.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairBaseThingHandler.java index 6652d27fcaf..ddfff132f8e 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairBaseThingHandler.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairBaseThingHandler.java @@ -12,37 +12,182 @@ */ package org.openhab.binding.pentair.internal.handler; -import org.openhab.binding.pentair.internal.PentairPacket; +import java.util.Collection; +import java.util.List; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pentair.internal.config.PentairBaseThingConfig; +import org.openhab.binding.pentair.internal.parser.PentairBasePacket; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * Abstract class for all Pentair Things. + * {@link PentairBaseThingHandler } Abstract class for all Pentair thing handlers. * * @author Jeff James - Initial contribution * */ +@NonNullByDefault public abstract class PentairBaseThingHandler extends BaseThingHandler { - /** ID of Thing on Pentair bus */ - protected int id; + @SuppressWarnings("unused") + private final Logger logger = LoggerFactory.getLogger(PentairBaseThingHandler.class); + + private PentairBaseThingConfig config = new PentairBaseThingConfig(); + + // waitStatusForOnline indicates whether the device is waiting to go fully online until after a first packet is + // received + protected boolean waitStatusForOnline = false; public PentairBaseThingHandler(Thing thing) { super(thing); } - /** - * Gets Pentair bus ID of Thing - * - * @return - */ + @Override + public void initialize() { + this.config = getConfigAs(PentairBaseThingConfig.class); + + PentairBaseBridgeHandler bh = getBridgeHandler(); + + if (bh == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.bridge-missing"); + return; + } + + if (bh.equipment.get(config.id) != null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.duplicate-id"); + return; + } + + bh.childHandlerInitializing(this, this.getThing()); + + goOnline(); + + updateStatus(ThingStatus.UNKNOWN); + } + + public void goOnline() { + waitStatusForOnline = true; + } + + public void finishOnline() { + waitStatusForOnline = false; + updateStatus(ThingStatus.ONLINE); + } + + public void goOffline(ThingStatusDetail detail) { + updateStatus(ThingStatus.OFFLINE, detail); + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) { + goOffline(ThingStatusDetail.BRIDGE_OFFLINE); + } else if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) { + waitStatusForOnline = false; + goOnline(); + } + } + public int getPentairID() { - return id; + return config.id; + } + + @Nullable + public PentairBaseBridgeHandler getBridgeHandler() { + // make sure bridge exists and is online + Bridge bridge = this.getBridge(); + if (bridge == null) { + return null; + } + PentairBaseBridgeHandler bh = (PentairBaseBridgeHandler) bridge.getHandler(); + if (bh == null) { + return null; + } + + return bh; } /** - * Abstract function to be implemented by Thing to dispose/parse a received packet + * Helper function to update channel. + */ + public void updateChannel(ChannelUID channel, boolean value) { + updateState(channel, OnOffType.from(value)); + } + + public void updateChannel(ChannelUID channel, int value) { + updateState(channel, new DecimalType(value)); + } + + public void updateChannel(ChannelUID channel, double value) { + updateState(channel, new DecimalType(value)); + } + + public void updateChannel(ChannelUID channel, String value) { + updateState(channel, new StringType(value)); + } + + public void updateChannel(ChannelUID channel, Number value, Unit unit) { + updateState(channel, new QuantityType<>(value, unit)); + } + + public void refreshAllChannels() { + List channels = getThing().getChannels(); + + refreshChannels(channels); + } + + public void refreshGroupChannels(String group) { + List channels = getThing().getChannelsOfGroup(group); + + refreshChannels(channels); + } + + public void refreshChannels(Collection channels) { + ThingHandler handler = getThing().getHandler(); + if (handler == null) { + return; + } + + for (Channel channel : channels) { + ChannelUID uid = channel.getUID(); + handler.handleCommand(uid, RefreshType.REFRESH); + } + } + + public void refreshChannelsFromUIDs(Collection channelUIDs) { + ThingHandler handler = getThing().getHandler(); + if (handler == null) { + return; + } + + for (ChannelUID channelUID : channelUIDs) { + handler.handleCommand(channelUID, RefreshType.REFRESH); + } + } + + /** + * Abstract function to be implemented by Thing to parse a received packet * * @param p */ - public abstract void processPacketFrom(PentairPacket p); + public abstract void processPacketFrom(PentairBasePacket p); } diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairControllerHandler.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairControllerHandler.java new file mode 100644 index 00000000000..0b87e64ebf5 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairControllerHandler.java @@ -0,0 +1,817 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler; + +import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.measure.Unit; +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pentair.internal.actions.PentairControllerActions; +import org.openhab.binding.pentair.internal.handler.helpers.PentairControllerCircuit; +import org.openhab.binding.pentair.internal.handler.helpers.PentairControllerLightMode; +import org.openhab.binding.pentair.internal.handler.helpers.PentairControllerSchedule; +import org.openhab.binding.pentair.internal.handler.helpers.PentairControllerStatus; +import org.openhab.binding.pentair.internal.handler.helpers.PentairHeatStatus; +import org.openhab.binding.pentair.internal.parser.PentairBasePacket; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.openhab.binding.pentair.internal.utils.ExpiringCache; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelGroupUID; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairControllerHandler} is responsible for implementation of the EasyTouch Controller. It will handle + * commands sent to a thing and implements the different channels. It also parses of the packets seen on the + * bus from the controller. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class PentairControllerHandler extends PentairBaseThingHandler { + private static final int NUM_CIRCUITS = PentairControllerStatus.NUMCIRCUITS; + private static final int NUM_SCHEDULES = 9; + private static final int CACHE_EXPIRY = (int) TimeUnit.SECONDS.toMillis(60); + private static final int CACHE_EXPIRY_LONG = (int) TimeUnit.MINUTES.toMillis(30); + + private static final List CIRCUIT_GROUPS = List.of(GROUP_CONTROLLER_SPACIRCUIT, + GROUP_CONTROLLER_AUX1CIRCUIT, GROUP_CONTROLLER_AUX2CIRCUIT, GROUP_CONTROLLER_AUX3CIRCUIT, + GROUP_CONTROLLER_AUX4CIRCUIT, GROUP_CONTROLLER_POOLCIRCUIT, GROUP_CONTROLLER_AUX5CIRCUIT, + GROUP_CONTROLLER_AUX6CIRCUIT, GROUP_CONTROLLER_AUX7CIRCUIT, GROUP_CONTROLLER_AUX8CIRCUIT, + GROUP_CONTROLLER_FEATURE1, GROUP_CONTROLLER_FEATURE2, GROUP_CONTROLLER_FEATURE3, GROUP_CONTROLLER_FEATURE4, + GROUP_CONTROLLER_FEATURE5, GROUP_CONTROLLER_FEATURE6, GROUP_CONTROLLER_FEATURE7, GROUP_CONTROLLER_FEATURE8); + + private List circuitSwitchUIDs = new ArrayList(); + + private boolean serviceMode = false; + private Unit uom = SIUnits.CELSIUS; + + private final Logger logger = LoggerFactory.getLogger(PentairControllerHandler.class); + + private @Nullable ScheduledFuture syncTimeJob; + + private long lastScheduleTypeWrite; + + private final ExpiringCache controllerStatusCache = new ExpiringCache<>(CACHE_EXPIRY); + private final ExpiringCache heatStatusCache = new ExpiringCache<>(CACHE_EXPIRY); + + private int majorrev, minorrev; + + private PentairControllerActions actions = new PentairControllerActions(); + + @SuppressWarnings("unchecked") + private final ExpiringCache[] circuitsCache = new ExpiringCache[NUM_CIRCUITS]; + @SuppressWarnings("unchecked") + private final ExpiringCache[] schedulesCache = new ExpiringCache[NUM_SCHEDULES]; + + private @Nullable PentairControllerLightMode lightMode; + + public PentairControllerHandler(Thing thing) { + super(thing); + + for (int i = 0; i < NUM_SCHEDULES; i++) { + schedulesCache[i] = new ExpiringCache(CACHE_EXPIRY_LONG); + } + + for (int i = 0; i < NUM_CIRCUITS; i++) { + circuitsCache[i] = new ExpiringCache(CACHE_EXPIRY_LONG); + } + } + + @Override + public void initialize() { + for (String group : CIRCUIT_GROUPS) { + circuitSwitchUIDs.add(new ChannelUID(new ChannelGroupUID(this.getThing().getUID(), group), + CHANNEL_CONTROLLER_CIRCUITSWITCH)); + } + + super.initialize(); + } + + @Override + public void goOnline() { + // Only a single controller is supported on the Pentair bus so prevent multiple controller + // things being created. + PentairBaseBridgeHandler bridgeHandler = getBridgeHandler(); + + if (bridgeHandler == null) { // will not be null here since this is validated in initialize of the super + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.bridge-missing"); + return; + } + + PentairControllerHandler handler = bridgeHandler.findController(); + + if (handler != null && !handler.equals(this)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.duplicate-controller"); + } else { + super.goOnline(); + } + } + + @Override + public void finishOnline() { + super.finishOnline(); + actions.initialize(Objects.requireNonNull(getBridgeHandler()).getBaseActions(), getPentairID()); + + // setup syncTimeJob to run once a day. The initial syncTime is called as part of the initControllerSettings as + // part of the controller coming online + syncTimeJob = scheduler.scheduleWithFixedDelay(this::syncTime, 1, 1, TimeUnit.DAYS); + + scheduler.execute(() -> initControllerSettings()); + } + + public void syncTime() { + boolean synctime = ((boolean) getConfig().get(CONTROLLER_CONFIGSYNCTIME)); + if (synctime) { + logger.debug("Synchronizing System Time with Pentair controller"); + Calendar now = Calendar.getInstance(); + + actions.setClockSettings(now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), + now.get(Calendar.DAY_OF_WEEK), now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.MONTH) + 1, + now.get(Calendar.YEAR) - 2000); + + } + } + + public void initControllerSettings() { + int i; + + actions.getSWVersion(); + actions.getHeatStatus(); + actions.getClockSettings(); + + for (i = 1; i <= NUM_CIRCUITS; i++) { + actions.getCircuitNameFunction(i); + } + + for (i = 1; i <= NUM_SCHEDULES; i++) { + actions.getSchedule(i); + } + + actions.getLightGroups(); + actions.getValves(); + syncTime(); + } + + @Override + public void goOffline(ThingStatusDetail detail) { + super.goOffline(detail); + + ScheduledFuture syncTimeJob = this.syncTimeJob; + if (syncTimeJob != null) { + syncTimeJob.cancel(true); + } + } + + public @Nullable PentairControllerCircuit getCircuitByGroupID(String group) { + int index = CIRCUIT_GROUPS.indexOf(group); + + if (index == -1) { + return null; + } + + return circuitsCache[index].getLastKnownValue(); + } + + public int getScheduleNumber(String name) { + int scheduleNum; + + scheduleNum = Integer.parseInt(name.substring(GROUP_CONTROLLER_SCHEDULE.length())); + + if (scheduleNum < 1 || scheduleNum > NUM_SCHEDULES) { + return 0; + } + + return scheduleNum; + } + + public @Nullable PentairControllerSchedule getScheduleByGroupID(String groupid) { + int scheduleNumber = getScheduleNumber(groupid); + if (scheduleNumber == 0) { + return null; + } + + PentairControllerSchedule schedule = schedulesCache[scheduleNumber - 1].getLastKnownValue(); + + return schedule; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + String group = channelUID.getGroupId(); + if (group == null) { + return; + } + + if (command instanceof RefreshType) { + logger.debug("handleCommand (refresh): {}", channelUID.getId()); + + switch (group) { + case GROUP_CONTROLLER_POOLHEAT: + case GROUP_CONTROLLER_SPAHEAT: + handleRefreshHeatStatusChannel(channelUID); + return; + case GROUP_CONTROLLER_STATUS: + handleRefreshStatusChannel(channelUID); + return; + + case GROUP_CONTROLLER_POOLCIRCUIT: + case GROUP_CONTROLLER_SPACIRCUIT: + case GROUP_CONTROLLER_AUX1CIRCUIT: + case GROUP_CONTROLLER_AUX2CIRCUIT: + case GROUP_CONTROLLER_AUX3CIRCUIT: + case GROUP_CONTROLLER_AUX4CIRCUIT: + case GROUP_CONTROLLER_AUX5CIRCUIT: + case GROUP_CONTROLLER_AUX6CIRCUIT: + case GROUP_CONTROLLER_AUX7CIRCUIT: + case GROUP_CONTROLLER_AUX8CIRCUIT: + case GROUP_CONTROLLER_FEATURE1: + case GROUP_CONTROLLER_FEATURE2: + case GROUP_CONTROLLER_FEATURE3: + case GROUP_CONTROLLER_FEATURE4: + case GROUP_CONTROLLER_FEATURE5: + case GROUP_CONTROLLER_FEATURE6: + case GROUP_CONTROLLER_FEATURE7: + case GROUP_CONTROLLER_FEATURE8: + handleRefreshCircuitChannel(channelUID); + return; + } + + if (group.substring(0, GROUP_CONTROLLER_SCHEDULE.length()).equals(GROUP_CONTROLLER_SCHEDULE)) { + handleRefreshScheduleChannel(channelUID); + return; + } + + return; + } + + logger.debug("handleCommand: {}", channelUID.getId()); + + switch (channelUID.getIdWithoutGroup()) { + case CHANNEL_CONTROLLER_CIRCUITSWITCH: { + if (!(command instanceof OnOffType onOffCommand)) { + logger.trace("Command is not OnOffType"); + break; + } + + int index = CIRCUIT_GROUPS.indexOf(group); + if (index == -1) { + break; + } + + boolean state = onOffCommand == OnOffType.ON; + + actions.setCircuitSwitch(index + 1, state); + + break; + } + case CHANNEL_CONTROLLER_LIGHTMODE: { + if (!(command instanceof StringType)) { + break; + } + String str = command.toString(); + PentairControllerLightMode lightMode; + + try { + lightMode = PentairControllerLightMode.valueOf(str); + actions.setLightMode(lightMode); + } catch (IllegalArgumentException e) { + logger.debug("Invalid light mode: {}", str); + } + break; + } + case CHANNEL_CONTROLLER_SCHEDULESTRING: { + if (!(command instanceof StringType)) { + break; + } + PentairControllerSchedule schedule = getScheduleByGroupID(group); + + if (schedule == null) { + break; + } + String str = command.toString(); + + if (!schedule.fromString(str)) { + logger.debug("schedule invalid format: {}", str); + } + break; + } + case CHANNEL_CONTROLLER_SCHEDULETYPE: { + if (!(command instanceof StringType)) { + break; + } + PentairControllerSchedule schedule = getScheduleByGroupID(group); + + if (schedule == null) { + break; + } + String str = command.toString(); + // In order to prevent accidental programming of schedules by an inadvertent update, make sure the same + // value is written twice to this field within 5s. Only then will the schedule update command be + // sent to the controller. + boolean bUpdate = (str.equals(schedule.getScheduleTypeStr()) + && ((System.currentTimeMillis() - lastScheduleTypeWrite) < 5000) && schedule.isDirty()); + if (!schedule.setScheduleType(str)) { + return; + } + lastScheduleTypeWrite = System.currentTimeMillis(); + if (bUpdate) { + actions.saveSchedule(schedule); + + lastScheduleTypeWrite = 0; + refreshGroupChannels(group); + } + break; + } + case CHANNEL_CONTROLLER_SCHEDULESTART: { + if (!(command instanceof Number numberCommand)) { + break; + } + + PentairControllerSchedule schedule = getScheduleByGroupID(group); + + if (schedule == null) { + break; + } + int start = numberCommand.intValue(); + schedule.setScheduleStart(start); + break; + } + case CHANNEL_CONTROLLER_SCHEDULEEND: { + if (!(command instanceof Number numberCommand)) { + break; + } + PentairControllerSchedule schedule = getScheduleByGroupID(group); + if (schedule == null) { + break; + } + int end = numberCommand.intValue(); + schedule.setScheduleEnd(end); + break; + } + case CHANNEL_CONTROLLER_SCHEDULECIRCUIT: { + if (!(command instanceof Number numberCommand)) { + break; + } + PentairControllerSchedule schedule = getScheduleByGroupID(group); + if (schedule == null) { + break; + } + int circuit = numberCommand.intValue(); + schedule.setScheduleCircuit(circuit); + break; + } + case CHANNEL_CONTROLLER_SCHEDULEDAYS: { + if (!(command instanceof StringType)) { + break; + } + PentairControllerSchedule schedule = getScheduleByGroupID(group); + if (schedule == null) { + break; + } + String days = command.toString(); + schedule.setDays(days); + break; + } + case CHANNEL_CONTROLLER_SETPOINT: { + if (!(command instanceof QuantityType)) { + break; + } + + PentairHeatStatus heatStatus = heatStatusCache.getLastKnownValue(); + if (heatStatus == null) { + return; + } + + @SuppressWarnings("unchecked") + QuantityType newTempQT = (QuantityType) command; + newTempQT = newTempQT.toUnit(uom); // convert to units for the controller + if (newTempQT == null) { + return; + } + int newTemp = newTempQT.intValue(); + + switch (group) { + case GROUP_CONTROLLER_SPAHEAT: + heatStatus.spaSetPoint = newTemp; + break; + case GROUP_CONTROLLER_POOLHEAT: + heatStatus.poolSetPoint = newTemp; + break; + } + + actions.setHeatStatus(heatStatus); + + break; + } + case CHANNEL_CONTROLLER_HEATERDELAY: { + if (!(command instanceof OnOffType onOffCommand)) { + break; + } + if (onOffCommand != OnOffType.OFF) { // Delay can only be cancelled + break; + } + + actions.cancelDelay(); + } + } + } + + @Override + public void processPacketFrom(PentairBasePacket packet) { + PentairStandardPacket p = (PentairStandardPacket) packet; + + switch (p.getByte(PentairStandardPacket.ACTION)) { + case 0x01: // Ack + logger.trace("[{}] Ack command from device: {}", p.getSource(), p); + break; + case 0x02: // Controller Status + if (p.getPacketLengthHeader() != 29) { + logger.debug("Expected length of 29: {}", p); + return; + } + + logger.trace("[{}] Controller Status: {}", p.getSource(), p); + + int preambleByte = p.getByte(PentairStandardPacket.PREAMBLE); // Adjust what byte is used for preamble + actions.setPreambleByte(preambleByte); + + if (waitStatusForOnline) { + finishOnline(); + } + + PentairControllerStatus currentControllerStatus = controllerStatusCache.getLastKnownValue(); + PentairControllerStatus newControllerStatus = new PentairControllerStatus(); + newControllerStatus.parsePacket(p); + + // always update the cached value to reset the expire timer + controllerStatusCache.putValue(newControllerStatus); + + // Refresh initially when currentControllerStatus is not set - or when status has changed + if (currentControllerStatus == null || !newControllerStatus.equals(currentControllerStatus)) { + logger.debug("[{}] New controller status: {} - {}", p.getSource(), newControllerStatus, p); + + this.uom = newControllerStatus.uom; + this.serviceMode = newControllerStatus.serviceMode; + + refreshChannelsFromUIDs(circuitSwitchUIDs); + refreshGroupChannels(GROUP_CONTROLLER_STATUS); + handleRefreshHeatStatusChannel(new ChannelUID(this.getThing().getUID(), GROUP_CONTROLLER_POOLHEAT, + CHANNEL_CONTROLLER_TEMPERATURE)); + handleRefreshHeatStatusChannel(new ChannelUID(this.getThing().getUID(), GROUP_CONTROLLER_SPAHEAT, + CHANNEL_CONTROLLER_TEMPERATURE)); + } + + break; + case 0x04: // Pump control panel on/off - handled in intelliflo controller + // Controller sends packet often to keep control of the motor + int data = p.getPacketLengthHeader() > 5 ? p.getByte(PentairStandardPacket.STARTOFDATA) & 0xFF : -1; + logger.debug("[{}] Pump control panel on/off: {}|{}|{} - {}", p.getSource(), + p.getByte(PentairStandardPacket.ACTION), // + p.getByte(PentairStandardPacket.LENGTH), data, p); + break; + case 0x05: // Current Clock - A5 01 0F 10 05 08 0E 09 02 1D 04 11 00 00 - H M DOW D M YY YY ?? + int hour = p.getByte(0 + PentairStandardPacket.STARTOFDATA); + int minute = p.getByte(1 + PentairStandardPacket.STARTOFDATA); + int dow = p.getByte(2 + PentairStandardPacket.STARTOFDATA); + int day = p.getByte(3 + PentairStandardPacket.STARTOFDATA); + int month = p.getByte(4 + PentairStandardPacket.STARTOFDATA); + int year = p.getByte(5 + PentairStandardPacket.STARTOFDATA); + + logger.debug("[{}] System Clock: {}.{}.{} {}:{}, DOW={}", p.getSource(), day, month, year, hour, minute, + dow); + break; + case 0x06: // Set run mode + // No action - have not verified these commands, here for documentation purposes and future enhancement + if (p.getPacketLengthHeader() != 1) { + logger.debug("[{}] Expected run mode length of 1: {}", p.getSource(), p); + return; + } + int run = p.getByte(PentairStandardPacket.STARTOFDATA) & 0xFF; + String s; + switch (run) { + case 0x04: // off + s = "OFF"; + break; + case 0x0A: // on + s = "ON"; + break; + default: + s = "n/a (" + run + ")"; + } + logger.debug("[{}] Set run mode for device {}: {} ", p.getSource(), p.getDest(), s); + break; + case 0x07: // Pump Status - handled in IntelliFlo handler + logger.trace("[{}] Pump request status (unseen): {}", p.getSource(), p); + break; + case 0x08: // Heat Status - A5 01 0F 10 08 0D 4B 4B 4D 55 5E 07 00 00 58 00 00 00  + if (p.getPacketLengthHeader() != 0x0D) { + logger.debug("Expected length of 13: {}", p); + return; + } + + PentairHeatStatus heatStatus = new PentairHeatStatus(p); + heatStatusCache.putValue(heatStatus); + + logger.debug("[{}] Heat status: {} - {}", p.getSource(), heatStatus, p); + + refreshGroupChannels(GROUP_CONTROLLER_POOLHEAT); + refreshGroupChannels(GROUP_CONTROLLER_SPAHEAT); + break; + case 0x0A: // Custom Names + logger.trace("[{}] Get Custom Names (unseen): {}", p.getSource(), p); + break; + case 0x0B: // Circuit Names + int index; + + index = p.getByte(0 + PentairStandardPacket.STARTOFDATA); + index--; // zero index + if (index < 0 || index >= NUM_CIRCUITS) { + break; + } + PentairControllerCircuit circuit = new PentairControllerCircuit(index + 1); + circuit.setName(p.getByte(2 + PentairStandardPacket.STARTOFDATA)); + circuit.setFunction(p.getByte(1 + PentairStandardPacket.STARTOFDATA)); + + circuitsCache[index].putValue(circuit); + + refreshGroupChannels(CIRCUIT_GROUPS.get(index)); + logger.debug("[{}] Circuit Names - Circuit: {}, Function: {}, Name: {}", p.getSource(), circuit.id, + circuit.circuitFunction.getFriendlyName(), circuit.circuitName.getFriendlyName()); + break; + case 0x11: // schedule - A5 1E 0F 10 11 07 01 06 0B 00 0F 00 7F + PentairControllerSchedule schedule = new PentairControllerSchedule(p); + + if (schedule.id < 1 || schedule.id > NUM_SCHEDULES) { + break; + } + String groupID = schedule.getGroupID(); + schedulesCache[schedule.id - 1].putValue(schedule); + + refreshGroupChannels(groupID); + + logger.debug( + "[{}] Controller Schedule - ID: {}, Name: {}, Type: {}, Circuit: {}, Start Time: {}:{}, End Time: {}:{}, Days: {}", + p.getSource(), schedule.id, schedule.type.getName(), schedule.type, schedule.circuit, + schedule.start / 60, schedule.start % 60, schedule.end / 60, schedule.end % 60, schedule.days); + break; + case 0x12: // IntelliChem + logger.debug("[{}] IntelliChem status: {}", p.getSource(), p); + break; + case 0x19: // Intellichlor status + logger.trace("[{}] Intellichlor status: {}", p.getSource(), p); + break; + case 0x1B: // Pump config (Extended) + logger.debug("[{}] Pump Config: {}", p.getSource(), p); + break; + case 0x1D: // Valves + logger.debug("[{}] Values: {}", p.getSource(), p); + break; + case 0x1E: // High speed circuits + logger.debug("[{}] High speed circuits: {}", p.getSource(), p); + break; + case 0x20: // spa-side is4/is10 remote + case 0x21: // spa-side quicktouch remotes + logger.debug("[{}] Spa-side remotes: {}", p.getSource(), p); + break; + case 0x22: // Solar/Heat Pump status + logger.trace("[{}] Solar/Heat Pump status: {}", p.getSource(), p); + break; + case 0x23: // Delay status + logger.debug("[{}] Delay status: {}", p.getSource(), p); + break; + case 0x27: // Light Groups/Positions + logger.trace("[{}] Light Groups/Positions; {}", p.getSource(), p); + break; + case 0x28: // Settings? heat mode + logger.trace("[{}] Settings?: {}", p.getSource(), p); + break; + case 0x60: // set intellibrite colors + logger.trace("[{}] Set intellibrite colors: {}", p.getSource(), p); + break; + case 0x86: // Set Curcuit On/Off + logger.trace("[{}] Set Circuit Function On/Off (unseen): {}", p.getSource(), p); + break; + case 0xD2: // Get Intellichem status + logger.trace("[{}] Get IntelliChem status: {}", p.getSource(), p); + break; + case 0xFC: // Status - A5 1E 0F 10 FC 11 00 02 0A 00 00 01 0A 00 00 00 00 00 00 00 00 00 00 + majorrev = p.getByte(1 + PentairStandardPacket.STARTOFDATA); + minorrev = p.getByte(2 + PentairStandardPacket.STARTOFDATA); + + String version = String.format("%d.%d", majorrev, minorrev); + updateProperty(PROPERTY_CONTROLLER_FIRMWAREVERSION, version); + logger.debug("[{}] SW Version - {}", p.getSource(), version); + break; + default: + logger.debug("[{}] Not Implemented {}: {}", p.getSource(), p.getByte(PentairStandardPacket.ACTION), p); + + break; + } + } + + /* + * Helper routines to handle Refresh commands + */ + + private void handleRefreshScheduleChannel(ChannelUID channelUID) { + String group = channelUID.getGroupId(); + String channel = channelUID.getIdWithoutGroup(); + + if (group == null) { + return; + } + + PentairControllerSchedule schedule = getScheduleByGroupID(group); + if (schedule == null) { + return; + } + + switch (channel) { + case CHANNEL_CONTROLLER_SCHEDULESTRING: + updateChannel(channelUID, schedule.toString()); + return; + case CHANNEL_CONTROLLER_SCHEDULETYPE: + String type = schedule.getScheduleTypeStr(); + updateChannel(channelUID, type); + return; + case CHANNEL_CONTROLLER_SCHEDULECIRCUIT: + updateChannel(channelUID, schedule.circuit); + return; + case CHANNEL_CONTROLLER_SCHEDULESTART: + updateChannel(channelUID, schedule.start, Units.MINUTE); + return; + case CHANNEL_CONTROLLER_SCHEDULEEND: + updateChannel(channelUID, schedule.end, Units.MINUTE); + return; + case CHANNEL_CONTROLLER_SCHEDULEDAYS: + updateChannel(channelUID, schedule.getDays()); + return; + } + } + + private void handleRefreshStatusChannel(ChannelUID channelUID) { + String channel = channelUID.getIdWithoutGroup(); + + PentairControllerStatus status = controllerStatusCache.getValue(() -> { + + actions.getStatus(); + + }); + + if (status == null) { + return; + } + + switch (channel) { + case CHANNEL_CONTROLLER_AIRTEMPERATURE: + updateChannel(channelUID, status.airTemp, uom); + return; + case CHANNEL_CONTROLLER_SOLARTEMPERATURE: + updateChannel(channelUID, status.solarTemp, uom); + return; + case CHANNEL_CONTROLLER_SERVICEMODE: + updateChannel(channelUID, status.serviceMode); + return; + case CHANNEL_CONTROLLER_SOLARON: + updateChannel(channelUID, status.solarOn); + return; + case CHANNEL_CONTROLLER_HEATERON: + updateChannel(channelUID, status.heaterOn); + return; + case CHANNEL_CONTROLLER_HEATERDELAY: + updateChannel(channelUID, status.heaterDelay); + return; + case CHANNEL_CONTROLLER_LIGHTMODE: + PentairControllerLightMode lightMode = this.lightMode; + if (lightMode != null) { + updateChannel(channelUID, lightMode.name()); + } + return; + } + } + + private void handleRefreshHeatStatusChannel(ChannelUID channelUID) { + String group = channelUID.getGroupId(); + String channel = channelUID.getIdWithoutGroup(); + + if (group == null) { + return; + } + + boolean poolChannel = group.equals(GROUP_CONTROLLER_POOLHEAT); + PentairHeatStatus heatStatus = heatStatusCache.getLastKnownValue(); + + if (heatStatus == null) { + return; + } + + switch (channel) { + case CHANNEL_CONTROLLER_SETPOINT: + updateChannel(channelUID, (poolChannel) ? heatStatus.poolSetPoint : heatStatus.spaSetPoint, uom); + return; + case CHANNEL_CONTROLLER_HEATMODE: + updateChannel(channelUID, + (poolChannel) ? heatStatus.poolHeatMode.name() : heatStatus.spaHeatMode.name()); + return; + case CHANNEL_CONTROLLER_TEMPERATURE: { + PentairControllerStatus status = this.controllerStatusCache.getLastKnownValue(); + if (status == null) { + return; + } + + if (poolChannel) { + if (status.pool) { + updateChannel(channelUID, status.poolTemp, uom); + } else { + updateState(channelUID, UnDefType.UNDEF); + } + } else { + if (status.spa) { + updateChannel(channelUID, status.poolTemp, uom); + } else { + updateState(channelUID, UnDefType.UNDEF); + } + } + } + } + } + + private void handleRefreshCircuitChannel(ChannelUID channelUID) { + String group = channelUID.getGroupId(); + String channel = channelUID.getIdWithoutGroup(); + + if (group == null) { + return; + } + + switch (channel) { + case CHANNEL_CONTROLLER_CIRCUITNAME: + case CHANNEL_CONTROLLER_CIRCUITFUNCTION: { + PentairControllerCircuit circuit = getCircuitByGroupID(group); + if (circuit == null) { + return; + } + + String circuitString = channel.equals(CHANNEL_CONTROLLER_CIRCUITNAME) + ? circuit.circuitName.getFriendlyName() + : circuit.circuitFunction.getFriendlyName(); + + updateChannel(channelUID, circuitString); + return; + } + case CHANNEL_CONTROLLER_CIRCUITSWITCH: { + PentairControllerStatus status = controllerStatusCache.getValue(() -> { + actions.getStatus(); + + }); + + int index = CIRCUIT_GROUPS.indexOf(group); + + if (index == -1 || status == null) { + return; + } + boolean on = status.circuits[index]; + updateChannel(channelUID, on); + return; + } + } + } + + public boolean getServiceMode() { + return serviceMode; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairEasyTouchHandler.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairEasyTouchHandler.java deleted file mode 100644 index d629438af89..00000000000 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairEasyTouchHandler.java +++ /dev/null @@ -1,498 +0,0 @@ -/** - * Copyright (c) 2010-2024 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.pentair.internal.handler; - -import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; - -import java.math.BigDecimal; -import java.util.List; -import java.util.Objects; - -import org.openhab.binding.pentair.internal.PentairBindingConstants; -import org.openhab.binding.pentair.internal.PentairPacket; -import org.openhab.binding.pentair.internal.PentairPacketHeatSetPoint; -import org.openhab.binding.pentair.internal.PentairPacketStatus; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.OnOffType; -import org.openhab.core.library.types.StringType; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.types.Command; -import org.openhab.core.types.RefreshType; -import org.openhab.core.types.UnDefType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link PentairEasyTouchHandler} is responsible for implementation of the EasyTouch Controller. It will handle - * commands sent to a thing and implements the different channels. It also parses/disposes of the packets seen on the - * bus from the controller. - * - * @author Jeff James - Initial contribution - */ -public class PentairEasyTouchHandler extends PentairBaseThingHandler { - - private final Logger logger = LoggerFactory.getLogger(PentairEasyTouchHandler.class); - - /** - * current/last status packet recieved, used to compare new packet values to determine if status needs to be updated - */ - protected PentairPacketStatus p29cur = new PentairPacketStatus(); - /** current/last heat set point packet, used to determine if status in framework should be updated */ - protected PentairPacketHeatSetPoint phspcur = new PentairPacketHeatSetPoint(); - - public PentairEasyTouchHandler(Thing thing) { - super(thing); - } - - @Override - public void initialize() { - logger.debug("Initializing EasyTouch - Thing ID: {}.", this.getThing().getUID()); - - id = ((BigDecimal) getConfig().get("id")).intValue(); - - // make sure there are no exisitng EasyTouch controllers - PentairBaseBridgeHandler bh = (PentairBaseBridgeHandler) this.getBridge().getHandler(); - List things = bh.getThing().getThings(); - - for (Thing t : things) { - if (t.getUID().equals(this.getThing().getUID())) { - continue; - } - if (t.getThingTypeUID().equals(EASYTOUCH_THING_TYPE)) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Another EasyTouch controller is already configured."); - return; - } - } - - updateStatus(ThingStatus.ONLINE); - } - - @Override - public void dispose() { - logger.debug("Thing {} disposed.", getThing().getUID()); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - // When channel gets a refresh request, sending a null as the PentairPacket to updateChannel will force an - // updateState, regardless of previous packet value - if (command instanceof RefreshType) { - logger.debug("EasyTouch received refresh command"); - - updateChannel(channelUID.getId(), null); - - return; - } - - if (command instanceof OnOffType onOffCommand) { - boolean state = onOffCommand == OnOffType.ON; - - switch (channelUID.getId()) { - case EASYTOUCH_POOL: - circuitSwitch(6, state); - break; - case EASYTOUCH_SPA: - circuitSwitch(1, state); - break; - case EASYTOUCH_AUX1: - circuitSwitch(2, state); - break; - case EASYTOUCH_AUX2: - circuitSwitch(3, state); - break; - case EASYTOUCH_AUX3: - circuitSwitch(4, state); - break; - case EASYTOUCH_AUX4: - circuitSwitch(5, state); - break; - case EASYTOUCH_AUX5: - circuitSwitch(7, state); - break; - case EASYTOUCH_AUX6: - circuitSwitch(8, state); - break; - case EASYTOUCH_AUX7: // A5 01 10 20 86 02 09 01 - circuitSwitch(9, state); - break; - case EASYTOUCH_FEATURE1: - circuitSwitch(11, state); - break; - case EASYTOUCH_FEATURE2: - circuitSwitch(12, state); - break; - case EASYTOUCH_FEATURE3: - circuitSwitch(13, state); - break; - case EASYTOUCH_FEATURE4: - circuitSwitch(14, state); - break; - case EASYTOUCH_FEATURE5: - circuitSwitch(15, state); - break; - case EASYTOUCH_FEATURE6: - circuitSwitch(16, state); - break; - case EASYTOUCH_FEATURE7: - circuitSwitch(17, state); - break; - case EASYTOUCH_FEATURE8: - circuitSwitch(18, state); - break; - } - } else if (command instanceof DecimalType decimalCommand) { - int sp = decimalCommand.intValue(); - - switch (channelUID.getId()) { - case EASYTOUCH_SPASETPOINT: - setPoint(false, sp); - break; - case EASYTOUCH_POOLSETPOINT: - setPoint(true, sp); - break; - } - } - } - - /** - * Method to turn on/off a circuit in response to a command from the framework - * - * @param circuit circuit number - * @param state - */ - public void circuitSwitch(int circuit, boolean state) { - byte[] packet = { (byte) 0xA5, (byte) 0x01, (byte) id, (byte) 0x00 /* source */, (byte) 0x86, (byte) 0x02, - (byte) circuit, (byte) ((state) ? 1 : 0) }; - - PentairPacket p = new PentairPacket(packet); - - PentairBaseBridgeHandler bbh = (PentairBaseBridgeHandler) this.getBridge().getHandler(); - bbh.writePacket(p); - } - - /** - * Method to set heat point for pool (true) of spa (false) - * - * @param pool pool=true, spa=false - * @param temp - */ - public void setPoint(boolean pool, int temp) { - // [16,34,136,4,POOL HEAT Temp,SPA HEAT Temp,Heat Mode,0,2,56] - // [165, preambleByte, 16, 34, 136, 4, currentHeat.poolSetPoint, parseInt(req.params.temp), updateHeatMode, 0] - int spaset = (!pool) ? temp : phspcur.spasetpoint; - int poolset = (pool) ? temp : phspcur.poolsetpoint; - int heatmode = (phspcur.spaheatmode << 2) | phspcur.poolheatmode; - - byte[] packet = { (byte) 0xA5, (byte) 0x01, (byte) id, (byte) 0x00 /* source */, (byte) 0x88, (byte) 0x04, - (byte) poolset, (byte) spaset, (byte) heatmode, (byte) 0 }; - - logger.info("Set {} temperature: {}", (pool) ? "Pool" : "Spa", temp); - - PentairPacket p = new PentairPacket(packet); - - PentairBaseBridgeHandler bbh = (PentairBaseBridgeHandler) this.getBridge().getHandler(); - bbh.writePacket(p); - } - - @Override - public void processPacketFrom(PentairPacket p) { - switch (p.getAction()) { - case 1: // Write command to pump - logger.trace("Write command to pump (unimplemented): {}", p); - break; - case 2: - if (p.getLength() != 29) { - logger.debug("Expected length of 29: {}", p); - return; - } - - /* - * Save the previous state of the packet (p29cur) into a temp variable (p29old) - * Update the current state to the new packet we just received. - * Then call updateChannel which will compare the previous state (now p29old) to the new state (p29cur) - * to determine if updateState needs to be called - */ - PentairPacketStatus p29Old = p29cur; - p29cur = new PentairPacketStatus(p); - - updateChannel(EASYTOUCH_POOL, p29Old); - updateChannel(EASYTOUCH_POOLTEMP, p29Old); - updateChannel(EASYTOUCH_SPATEMP, p29Old); - updateChannel(EASYTOUCH_AIRTEMP, p29Old); - updateChannel(EASYTOUCH_SOLARTEMP, p29Old); - updateChannel(EASYTOUCH_HEATACTIVE, p29Old); - updateChannel(EASYTOUCH_POOL, p29Old); - updateChannel(EASYTOUCH_SPA, p29Old); - updateChannel(EASYTOUCH_AUX1, p29Old); - updateChannel(EASYTOUCH_AUX2, p29Old); - updateChannel(EASYTOUCH_AUX3, p29Old); - updateChannel(EASYTOUCH_AUX4, p29Old); - updateChannel(EASYTOUCH_AUX5, p29Old); - updateChannel(EASYTOUCH_AUX6, p29Old); - updateChannel(EASYTOUCH_AUX7, p29Old); - updateChannel(EASYTOUCH_FEATURE1, p29Old); - updateChannel(EASYTOUCH_FEATURE2, p29Old); - updateChannel(EASYTOUCH_FEATURE3, p29Old); - updateChannel(EASYTOUCH_FEATURE4, p29Old); - updateChannel(EASYTOUCH_FEATURE5, p29Old); - updateChannel(EASYTOUCH_FEATURE6, p29Old); - updateChannel(EASYTOUCH_FEATURE7, p29Old); - updateChannel(EASYTOUCH_FEATURE8, p29Old); - updateChannel(DIAG, p29Old); - - break; - case 4: // Pump control panel on/off - // No action - have not verified these commands, here for documentation purposes and future enhancement - logger.trace("Pump control panel on/of {}: {}", p.getDest(), p.getByte(PentairPacket.STARTOFDATA)); - - break; - case 5: // Set pump mode - // No action - have not verified these commands, here for documentation purposes and future enhancement - logger.trace("Set pump mode {}: {}", p.getDest(), p.getByte(PentairPacket.STARTOFDATA)); - - break; - case 6: // Set run mode - // No action - have not verified these commands, here for documentation purposes and future enhancement - logger.trace("Set run mode {}: {}", p.getDest(), p.getByte(PentairPacket.STARTOFDATA)); - - break; - case 7: - // No action - have not verified these commands, here for documentation purposes and future enhancement - logger.trace("Pump request status (unseen): {}", p); - break; - case 8: // A5 01 0F 10 08 0D 4B 4B 4D 55 5E 07 00 00 58 00 00 00  - if (p.getLength() != 0x0D) { - logger.debug("Expected length of 13: {}", p); - return; - } - - /* - * Save the previous state of the packet (phspcur) into a temp variable (phspOld) - * Update the current state to the new packet we just received. - * Then call updateChannel which will compare the previous state (now phspold) to the new state - * (phspcur) to determine if updateState needs to be called - */ - PentairPacketHeatSetPoint phspOld = phspcur; - phspcur = new PentairPacketHeatSetPoint(p); - - updateChannel(EASYTOUCH_POOLSETPOINT, phspOld); - updateChannel(EASYTOUCH_SPASETPOINT, phspOld); - updateChannel(EASYTOUCH_SPAHEATMODE, phspOld); - updateChannel(EASYTOUCH_SPAHEATMODESTR, phspOld); - updateChannel(EASYTOUCH_POOLHEATMODE, phspOld); - updateChannel(EASYTOUCH_POOLHEATMODESTR, phspOld); - - logger.debug("Heat set point: {}, {}, {}", p, phspcur.poolsetpoint, phspcur.spasetpoint); - break; - case 10: - logger.debug("Get Custom Names (unseen): {}", p); - break; - case 11: - logger.debug("Get Ciruit Names (unseen): {}", p); - break; - case 17: - logger.debug("Get Schedules (unseen): {}", p); - break; - case 134: - logger.debug("Set Circuit Function On/Off (unseen): {}", p); - break; - default: - logger.debug("Not Implemented: {}", p); - break; - } - } - - /** - * Helper function to compare and update channel if needed. The class variables p29_cur and phsp_cur are used to - * determine the appropriate state of the channel. - * - * @param channel name of channel to be updated, corresponds to channel name in {@link PentairBindingConstants} - * @param p Packet representing the former state. If null, no compare is done and state is updated. - */ - public void updateChannel(String channel, PentairPacket p) { - PentairPacketStatus p29 = null; - PentairPacketHeatSetPoint phsp = null; - - if (p != null) { - if (p.getLength() == 29) { - p29 = (PentairPacketStatus) p; - } else if (p.getLength() == 13) { - phsp = (PentairPacketHeatSetPoint) p; - } - } - - switch (channel) { - case EASYTOUCH_POOL: - if (p29 == null || (p29.pool != p29cur.pool)) { - updateState(channel, OnOffType.from((p29cur.pool))); - } - break; - case EASYTOUCH_SPA: - if (p29 == null || (p29.spa != p29cur.spa)) { - updateState(channel, OnOffType.from((p29cur.spa))); - } - break; - case EASYTOUCH_AUX1: - if (p29 == null || (p29.aux1 != p29cur.aux1)) { - updateState(channel, OnOffType.from((p29cur.aux1))); - } - break; - case EASYTOUCH_AUX2: - if (p29 == null || (p29.aux2 != p29cur.aux2)) { - updateState(channel, OnOffType.from((p29cur.aux2))); - } - break; - case EASYTOUCH_AUX3: - if (p29 == null || (p29.aux3 != p29cur.aux3)) { - updateState(channel, OnOffType.from((p29cur.aux3))); - } - break; - case EASYTOUCH_AUX4: - if (p29 == null || (p29.aux4 != p29cur.aux4)) { - updateState(channel, OnOffType.from((p29cur.aux4))); - } - break; - case EASYTOUCH_AUX5: - if (p29 == null || (p29.aux5 != p29cur.aux5)) { - updateState(channel, OnOffType.from((p29cur.aux5))); - } - break; - case EASYTOUCH_AUX6: - if (p29 == null || (p29.aux6 != p29cur.aux6)) { - updateState(channel, OnOffType.from((p29cur.aux6))); - } - break; - case EASYTOUCH_AUX7: - if (p29 == null || (p29.aux7 != p29cur.aux7)) { - updateState(channel, OnOffType.from((p29cur.aux7))); - } - break; - case EASYTOUCH_FEATURE1: - if (p29 == null || (p29.feature1 != p29cur.feature1)) { - updateState(channel, OnOffType.from((p29cur.feature1))); - } - break; - case EASYTOUCH_FEATURE2: - if (p29 == null || (p29.feature2 != p29cur.feature2)) { - updateState(channel, OnOffType.from((p29cur.feature2))); - } - break; - case EASYTOUCH_FEATURE3: - if (p29 == null || (p29.feature3 != p29cur.feature3)) { - updateState(channel, OnOffType.from((p29cur.feature3))); - } - break; - case EASYTOUCH_FEATURE4: - if (p29 == null || (p29.feature4 != p29cur.feature4)) { - updateState(channel, OnOffType.from((p29cur.feature4))); - } - break; - case EASYTOUCH_FEATURE5: - if (p29 == null || (p29.feature5 != p29cur.feature5)) { - updateState(channel, OnOffType.from((p29cur.feature5))); - } - break; - case EASYTOUCH_FEATURE6: - if (p29 == null || (p29.feature6 != p29cur.feature6)) { - updateState(channel, OnOffType.from((p29cur.feature6))); - } - break; - case EASYTOUCH_FEATURE7: - if (p29 == null || (p29.feature7 != p29cur.feature7)) { - updateState(channel, OnOffType.from((p29cur.feature7))); - } - break; - case EASYTOUCH_FEATURE8: - if (p29 == null || (p29.feature8 != p29cur.feature8)) { - updateState(channel, OnOffType.from((p29cur.feature8))); - } - break; - case EASYTOUCH_POOLTEMP: - if (p29 == null || (p29.pooltemp != p29cur.pooltemp)) { - if (p29cur.pool) { - updateState(channel, new DecimalType(p29cur.pooltemp)); - } else { - updateState(channel, UnDefType.UNDEF); - } - } - break; - case EASYTOUCH_SPATEMP: - if (p29 == null || (p29.spatemp != p29cur.spatemp)) { - if (p29cur.spa) { - updateState(channel, new DecimalType(p29cur.spatemp)); - } else { - updateState(channel, UnDefType.UNDEF); - } - } - break; - case EASYTOUCH_AIRTEMP: - if (p29 == null || (p29.airtemp != p29cur.airtemp)) { - updateState(channel, new DecimalType(p29cur.airtemp)); - } - break; - case EASYTOUCH_SOLARTEMP: - if (p29 == null || (p29.solartemp != p29cur.solartemp)) { - updateState(channel, new DecimalType(p29cur.solartemp)); - } - break; - case EASYTOUCH_SPAHEATMODE: - if (phsp == null || (phsp.spaheatmode != phspcur.spaheatmode)) { - updateState(channel, new DecimalType(phspcur.spaheatmode)); - } - break; - case EASYTOUCH_SPAHEATMODESTR: - if (phsp == null || (!Objects.equals(phsp.spaheatmodestr, phspcur.spaheatmodestr))) { - if (phspcur.spaheatmodestr != null) { - updateState(channel, new StringType(phspcur.spaheatmodestr)); - } - } - break; - case EASYTOUCH_POOLHEATMODE: - if (phsp == null || (phsp.poolheatmode != phspcur.poolheatmode)) { - updateState(channel, new DecimalType(phspcur.poolheatmode)); - } - break; - case EASYTOUCH_POOLHEATMODESTR: - if (phsp == null || (!Objects.equals(phsp.poolheatmodestr, phspcur.poolheatmodestr))) { - if (phspcur.poolheatmodestr != null) { - updateState(channel, new StringType(phspcur.poolheatmodestr)); - } - } - break; - case EASYTOUCH_HEATACTIVE: - if (p29 == null || (p29.heatactive != p29cur.heatactive)) { - updateState(channel, new DecimalType(p29cur.heatactive)); - } - break; - case EASYTOUCH_POOLSETPOINT: - if (phsp == null || (phsp.poolsetpoint != phspcur.poolsetpoint)) { - updateState(channel, new DecimalType(phspcur.poolsetpoint)); - } - break; - case EASYTOUCH_SPASETPOINT: - if (phsp == null || (phsp.spasetpoint != phspcur.spasetpoint)) { - updateState(channel, new DecimalType(phspcur.spasetpoint)); - } - break; - case DIAG: - if (p29 == null || (p29.diag != p29cur.diag)) { - updateState(channel, new DecimalType(p29cur.diag)); - } - break; - } - } -} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIPBridgeHandler.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIPBridgeHandler.java index a97fb59aa78..9fbe3943fa1 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIPBridgeHandler.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIPBridgeHandler.java @@ -12,12 +12,14 @@ */ package org.openhab.binding.pentair.internal.handler; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.Socket; import java.net.UnknownHostException; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.pentair.internal.config.PentairIPBridgeConfig; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ThingStatus; @@ -26,91 +28,76 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Handler for the IPBridge. Implements the connect and disconnect abstract methods of {@link PentairBaseBridgeHandler} + * The {@link PentairIPBridgeHandler } class implements the the IPBridge. + * Implements the connect and disconnect abstract methods of {@link PentairBaseBridgeHandler} * * @author Jeff James - Initial contribution - * */ + +@NonNullByDefault public class PentairIPBridgeHandler extends PentairBaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(PentairIPBridgeHandler.class); - /** Socket object for connection */ - protected Socket socket; + public PentairIPBridgeConfig config = new PentairIPBridgeConfig(); + + private @Nullable Socket socket; public PentairIPBridgeHandler(Bridge bridge) { super(bridge); } @Override - protected synchronized void connect() { - PentairIPBridgeConfig configuration = getConfigAs(PentairIPBridgeConfig.class); - - id = configuration.id; + protected synchronized boolean connect() { + config = getConfigAs(PentairIPBridgeConfig.class); try { - socket = new Socket(configuration.address, configuration.port); - reader = new BufferedInputStream(socket.getInputStream()); - writer = new BufferedOutputStream(socket.getOutputStream()); - logger.info("Pentair IPBridge connected to {}:{}", configuration.address, configuration.port); + this.socket = new Socket(config.address, config.port); + Socket socket = this.socket; + + if (socket == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication-error.ip-stream-error"); + return false; + } + + InputStream inputStream = socket.getInputStream(); + OutputStream outputStream = socket.getOutputStream(); + if (inputStream == null || outputStream == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication-error.ip-stream-error"); + return false; + } + + setInputStream(socket.getInputStream()); + setOutputStream(socket.getOutputStream()); + + logger.debug("Pentair IPBridge connected to {}:{}", config.address, config.port); } catch (UnknownHostException e) { - String msg = String.format("unknown host name: %s", configuration.address); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; + if (getThing().getStatus() != ThingStatus.OFFLINE) { + String msg = String.format("unknown host name: %s, %s", config.address, e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); + } + return false; } catch (IOException e) { - String msg = String.format("cannot open connection to %s", configuration.address); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; + if (getThing().getStatus() != ThingStatus.OFFLINE) { + String msg = String.format("cannot open connection to %s, %s", config.address, e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); + } + return false; } - parser = new Parser(); - thread = new Thread(parser); - thread.start(); - - if (socket != null && reader != null && writer != null) { - updateStatus(ThingStatus.ONLINE); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unable to connect"); - } + return true; } @Override protected synchronized void disconnect() { - updateStatus(ThingStatus.OFFLINE); - - if (thread != null) { - try { - thread.interrupt(); - thread.join(); // wait for thread to complete - } catch (InterruptedException e) { - // do nothing - } - thread = null; - parser = null; - } - - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error in closing reader"); - } - reader = null; - } - - if (writer != null) { - try { - writer.close(); - } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error in closing writer"); - } - writer = null; - } + Socket socket = this.socket; if (socket != null) { try { socket.close(); } catch (IOException e) { - logger.error("error when closing socket ", e); + logger.debug("error when closing socket ", e); } socket = null; } diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChemHandler.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChemHandler.java new file mode 100644 index 00000000000..137da259fb3 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChemHandler.java @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler; + +import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.pentair.internal.handler.helpers.PentairIntelliChem; +import org.openhab.binding.pentair.internal.parser.PentairBasePacket; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairIntelliChemHandler} is responsible for implementation of the IntelliChem. This will + * parse of status packets to set the stat for various channels. All channels are read only. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class PentairIntelliChemHandler extends PentairBaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(PentairIntelliChemHandler.class); + + private PentairIntelliChem pic = new PentairIntelliChem(); + + private String firmwareVersion = ""; + + public PentairIntelliChemHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + // The IntelliChem routinely updates the state, so just refresh to last state + switch (channelUID.getId()) { + case CHANNEL_INTELLICHEM_PHREADING: + updateChannel(channelUID, pic.phReading); + break; + case CHANNEL_INTELLICHEM_ORPREADING: + updateChannel(channelUID, pic.orpReading); + break; + case CHANNEL_INTELLICHEM_PHSETPOINT: + updateChannel(channelUID, pic.phSetPoint); + break; + case CHANNEL_INTELLICHEM_ORPSETPOINT: + updateChannel(channelUID, pic.orpSetPoint); + break; + case CHANNEL_INTELLICHEM_TANK1LEVEL: + updateChannel(channelUID, pic.tank1Level); + break; + case CHANNEL_INTELLICHEM_TANK2LEVEL: + updateChannel(channelUID, pic.tank2Level); + break; + case CHANNEL_INTELLICHEM_CALCIUMHARDNESS: + updateChannel(channelUID, pic.calciumHardness, Units.PARTS_PER_MILLION); + break; + case CHANNEL_INTELLICHEM_CYAREADING: + updateChannel(channelUID, pic.cyaReading); + break; + case CHANNEL_INTELLICHEM_ALKALINITY: + updateChannel(channelUID, pic.alkalinity); + break; + case CHANNEL_INTELLICHEM_LSI: + updateChannel(channelUID, pic.lsi); + break; + case CHANNEL_INTELLICHEM_PHDOSERTYPE: + updateChannel(channelUID, pic.phDoserType.name()); + break; + case CHANNEL_INTELLICHEM_ORPDOSERTYPE: + updateChannel(channelUID, pic.orpDoserType.name()); + break; + case CHANNEL_INTELLICHEM_PHDOSERSTATUS: + updateChannel(channelUID, pic.phDoserStatus.name()); + break; + case CHANNEL_INTELLICHEM_ORPDOSERSTATUS: + updateChannel(channelUID, pic.orpDoserStatus.name()); + break; + case CHANNEL_INTELLICHEM_PHDOSETIME: + updateChannel(channelUID, pic.phDoseTime, Units.SECOND); + break; + case CHANNEL_INTELLICHEM_ORPDOSETIME: + updateChannel(channelUID, pic.orpDoseTime, Units.SECOND); + break; + case CHANNEL_INTELLICHEM_SALTLEVEL: + updateChannel(channelUID, pic.saltLevel); + break; + case CHANNEL_INTELLICHEM_ALARMWATERFLOW: + updateChannel(channelUID, pic.alarmWaterFlow); + break; + case CHANNEL_INTELLICHEM_ALARMPH: + updateChannel(channelUID, pic.alarmPh); + break; + case CHANNEL_INTELLICHEM_ALARMORP: + updateChannel(channelUID, pic.alarmOrp); + break; + case CHANNEL_INTELLICHEM_ALARMPHTANK: + updateChannel(channelUID, pic.alarmPhTank); + break; + case CHANNEL_INTELLICHEM_ALARMORPTANK: + updateChannel(channelUID, pic.alarmOrpTank); + break; + case CHANNEL_INTELLICHEM_ALARMPROBEFAULT: + updateChannel(channelUID, pic.alarmProbeFault); + break; + case CHANNEL_INTELLICHEM_WARNINGPHLOCKOUT: + updateChannel(channelUID, pic.warningPhLockout); + break; + case CHANNEL_INTELLICHEM_WARNINGPHDAILYLIMITREACHED: + updateChannel(channelUID, pic.warningPhDailyLimitReached); + break; + case CHANNEL_INTELLICHEM_WARNINGORPDAILYLIMITREACHED: + updateChannel(channelUID, pic.warningOrpDailyLimitReached); + break; + case CHANNEL_INTELLICHEM_WARNINGINVALIDSETUP: + updateChannel(channelUID, pic.warningInvalidSetup); + break; + case CHANNEL_INTELLICHEM_WARNINGCHLORINATORCOMMERROR: + updateChannel(channelUID, pic.warningChlorinatorCommError); + break; + } + } + } + + @Override + public void processPacketFrom(PentairBasePacket packet) { + if (waitStatusForOnline) { + finishOnline(); + } + + PentairStandardPacket p = (PentairStandardPacket) packet; + + switch (p.getByte(PentairStandardPacket.ACTION)) { + case 0x12: // Status packet + pic.parsePacket(p); + logger.debug("Intellichem status: {}: ", pic.toString()); + + this.refreshAllChannels(); + + if (!this.firmwareVersion.equals(pic.firmwareVersion)) { + firmwareVersion = pic.firmwareVersion; + updateProperty(PROPERTY_INTELLICHEM_FIRMWAREVERSION, pic.firmwareVersion); + } + break; + + default: + logger.debug("Unhandled Intellichem packet: {}", p.toString()); + break; + } + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChlorHandler.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChlorHandler.java index aaaec4b3ad9..de1523d6f8a 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChlorHandler.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChlorHandler.java @@ -14,13 +14,17 @@ package org.openhab.binding.pentair.internal.handler; import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; -import org.openhab.binding.pentair.internal.PentairBindingConstants; -import org.openhab.binding.pentair.internal.PentairPacket; -import org.openhab.binding.pentair.internal.PentairPacketIntellichlor; -import org.openhab.core.library.types.DecimalType; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.pentair.internal.parser.PentairBasePacket; +import org.openhab.binding.pentair.internal.parser.PentairIntelliChlorPacket; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.slf4j.Logger; @@ -28,96 +32,149 @@ import org.slf4j.LoggerFactory; /** * The {@link PentairIntelliChlorHandler} is responsible for implementation of the Intellichlor Salt generator. It will - * process - * Intellichlor commands and set the appropriate channel states. There are currently no commands implemented for this - * Thing to receive from the framework. + * process Intellichlor commands and set the appropriate channel states. There are currently no commands implemented for + * this Thing to receive from the framework. * * @author Jeff James - Initial contribution */ +@NonNullByDefault public class PentairIntelliChlorHandler extends PentairBaseThingHandler { private final Logger logger = LoggerFactory.getLogger(PentairIntelliChlorHandler.class); - protected PentairPacketIntellichlor pic3cur = new PentairPacketIntellichlor(); - protected PentairPacketIntellichlor pic4cur = new PentairPacketIntellichlor(); + public int version; + public String name = ""; + + /** for a saltoutput packet, represents the salt output percent */ + private int saltOutput; + /** for a salinity packet, is value of salinity. Must be multiplied by 50 to get the actual salinity value. */ + private int salinity; + + private boolean ok; + private boolean lowFlow; + private boolean lowSalt; + private boolean veryLowSalt; + private boolean highCurrent; + private boolean cleanCell; + private boolean lowVoltage; + private boolean lowWaterTemp; + private boolean commError; public PentairIntelliChlorHandler(Thing thing) { super(thing); } @Override - public void initialize() { - logger.debug("Initializing IntelliChlor - Thing ID: {}.", this.getThing().getUID()); + public void goOnline() { + PentairIntelliChlorHandler handler = Objects.requireNonNull(getBridgeHandler()).findIntellichlor(); - id = 0; // Intellichlor doesn't have ID - - updateStatus(ThingStatus.ONLINE); + if (handler != null && !handler.equals(this)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.duplicate-intllichlor"); + return; + } else { + super.goOnline(); + } } @Override - public void dispose() { - logger.debug("Thing {} disposed.", getThing().getUID()); + public void goOffline(ThingStatusDetail detail) { + super.goOffline(detail); } @Override public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { - logger.debug("IntelliChlor received refresh command"); - updateChannel(channelUID.getId(), null); + logger.trace("IntelliChlor received refresh command"); + + switch (channelUID.getId()) { + case CHANNEL_INTELLICHLOR_SALTOUTPUT: + updateChannel(channelUID, saltOutput, Units.PERCENT); + break; + case CHANNEL_INTELLICHLOR_SALINITY: + updateChannel(channelUID, salinity, Units.PARTS_PER_MILLION); + break; + case CHANNEL_INTELLICHLOR_OK: + updateChannel(channelUID, ok); + break; + case CHANNEL_INTELLICHLOR_LOWFLOW: + updateChannel(channelUID, lowFlow); + break; + case CHANNEL_INTELLICHLOR_LOWSALT: + updateChannel(channelUID, lowSalt); + break; + case CHANNEL_INTELLICHLOR_VERYLOWSALT: + updateChannel(channelUID, veryLowSalt); + break; + case CHANNEL_INTELLICHLOR_HIGHCURRENT: + updateChannel(channelUID, highCurrent); + break; + case CHANNEL_INTELLICHLOR_CLEANCELL: + updateChannel(channelUID, cleanCell); + break; + case CHANNEL_INTELLICHLOR_LOWVOLTAGE: + updateChannel(channelUID, lowVoltage); + break; + case CHANNEL_INTELLICHLOR_LOWWATERTEMP: + updateChannel(channelUID, lowWaterTemp); + break; + case CHANNEL_INTELLICHLOR_COMMERROR: + updateChannel(channelUID, commError); + break; + } } } @Override - public void processPacketFrom(PentairPacket p) { - PentairPacketIntellichlor pic = (PentairPacketIntellichlor) p; + public void processPacketFrom(PentairBasePacket packet) { + PentairIntelliChlorPacket p = (PentairIntelliChlorPacket) packet; - switch (pic.getLength()) { - case 3: - if (pic.getCmd() != 0x11) { // only packets with 0x11 have valid saltoutput numbers. - break; + switch (p.getByte(PentairIntelliChlorPacket.ACTION)) { + case 0x03: + version = p.getVersion(); + name = p.getName(); + + Map editProperties = editProperties(); + editProperties.put(CHANNEL_INTELLICHLOR_PROPERTYVERSION, Integer.toString(version)); + editProperties.put(CHANNEL_INTELLICHLOR_PROPERTYMODEL, name); + updateProperties(editProperties); + + logger.debug("Intellichlor version: {}, {}", version, name); + break; + + case 0x11: // set salt output % command + saltOutput = p.getSaltOutput(); + updateChannel(new ChannelUID(getThing().getUID(), CHANNEL_INTELLICHLOR_SALTOUTPUT), saltOutput, + Units.PERCENT); + logger.debug("Intellichlor set output % {}", saltOutput); + break; + case 0x12: // response to set salt output + if (waitStatusForOnline) { // Only go online after first response from the Intellichlor + finishOnline(); } - PentairPacketIntellichlor pic3Old = pic3cur; - pic3cur = pic; + salinity = p.getSalinity(); - updateChannel(INTELLICHLOR_SALTOUTPUT, pic3Old); + ok = p.getOk(); + lowFlow = p.getLowFlow(); + lowSalt = p.getLowSalt(); + veryLowSalt = p.getVeryLowSalt(); + highCurrent = p.getHighCurrent(); + cleanCell = p.getCleanCell(); + lowVoltage = p.getLowVoltage(); + lowWaterTemp = p.getLowWaterTemp(); - break; - case 4: - if (pic.getCmd() != 0x12) { - break; - } + this.refreshAllChannels(); - PentairPacketIntellichlor pic4Old = pic4cur; - pic4cur = pic; - - updateChannel(INTELLICHLOR_SALINITY, pic4Old); - - break; - } - - logger.debug("Intellichlor command: {}", pic); - } - - /** - * Helper function to compare and update channel if needed. The class variables p29_cur and phsp_cur are used to - * determine the appropriate state of the channel. - * - * @param channel name of channel to be updated, corresponds to channel name in {@link PentairBindingConstants} - * @param p Packet representing the former state. If null, no compare is done and state is updated. - */ - public void updateChannel(String channel, PentairPacket p) { - PentairPacketIntellichlor pic = (PentairPacketIntellichlor) p; - - switch (channel) { - case INTELLICHLOR_SALINITY: - if (pic == null || (pic.salinity != pic4cur.salinity)) { - updateState(channel, new DecimalType(pic4cur.salinity)); + if (logger.isDebugEnabled()) { + String status = String.format( + "saltoutput = %d, salinity = %d, ok = %b, lowflow = %b, lowsalt = %b, verylowsalt = %b, highcurrent = %b, cleancell = %b, lowvoltage = %b, lowwatertemp = %b", + saltOutput, salinity, ok, lowFlow, lowSalt, veryLowSalt, highCurrent, cleanCell, lowVoltage, + lowWaterTemp); + logger.debug("IntelliChlor salinity/status: {}, {}", salinity, status); } break; - case INTELLICHLOR_SALTOUTPUT: - if (pic == null || (pic.saltoutput != pic3cur.saltoutput)) { - updateState(channel, new DecimalType(pic3cur.saltoutput)); - } + case 0x14: + logger.debug("IntelliChlor GetModel request (0x14): {}", p.toString()); break; } } diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliFloHandler.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliFloHandler.java index e67aa19c029..1f1987d2038 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliFloHandler.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairIntelliFloHandler.java @@ -14,16 +14,26 @@ package org.openhab.binding.pentair.internal.handler; import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; -import java.math.BigDecimal; +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; -import org.openhab.binding.pentair.internal.PentairBindingConstants; -import org.openhab.binding.pentair.internal.PentairPacket; -import org.openhab.binding.pentair.internal.PentairPacketPumpStatus; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pentair.internal.actions.PentairIntelliFloActions; +import org.openhab.binding.pentair.internal.handler.helpers.PentairPumpStatus; +import org.openhab.binding.pentair.internal.parser.PentairBasePacket; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.unit.ImperialUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.slf4j.Logger; @@ -31,158 +41,226 @@ import org.slf4j.LoggerFactory; /** * The {@link PentairIntelliFloHandler} is responsible for implementation of the Intelliflo Pump. This will - * parse/dispose of - * status packets to set the stat for various channels. + * parse status packets to set the stat for various channels. * * @author Jeff James - Initial contribution */ +@NonNullByDefault public class PentairIntelliFloHandler extends PentairBaseThingHandler { private final Logger logger = LoggerFactory.getLogger(PentairIntelliFloHandler.class); - protected PentairPacketPumpStatus ppscur = new PentairPacketPumpStatus(); + private PentairPumpStatus pumpStatus = new PentairPumpStatus(); + + // runmode is used to send watchdog to pump when running + private boolean runMode = false; + + private static @Nullable ScheduledFuture pollingJob; + + private PentairIntelliFloActions actions = new PentairIntelliFloActions(); public PentairIntelliFloHandler(Thing thing) { super(thing); } @Override - public void initialize() { - logger.debug("Initializing Intelliflo - Thing ID: {}.", this.getThing().getUID()); + public void finishOnline() { + super.finishOnline(); + actions.initialize(Objects.requireNonNull(getBridgeHandler()).getBaseActions(), getPentairID()); - id = ((BigDecimal) getConfig().get("id")).intValue(); - - updateStatus(ThingStatus.ONLINE); + startPollingJob(); } @Override - public void dispose() { - logger.debug("Thing {} disposed.", getThing().getUID()); + public void goOffline(ThingStatusDetail detail) { + super.goOffline(detail); + + // PentairIntelliFloHandler.pollingJob will be cancelled when called and there are no pumps associated + // with the bridge + } + + public PentairIntelliFloActions getActions() { + return actions; + } + + public void setRunMode(boolean runMode) { + this.runMode = runMode; + } + + private void startPollingJob() { + if (pollingJob == null) { + PentairIntelliFloHandler.pollingJob = scheduler + .scheduleWithFixedDelay(PentairIntelliFloHandler::pumpWatchDog, 10, 30, TimeUnit.SECONDS); + } + } + + private static void stopPollingJob() { + ScheduledFuture pollingJob = PentairIntelliFloHandler.pollingJob; + if (pollingJob != null) { + pollingJob.cancel(true); + } + PentairIntelliFloHandler.pollingJob = null; + } + + /** + * Job to send pump query status packages to all Intelliflo Pump things in order to see the status. + * Note: From the internet is seems some FW versions of EasyTouch controllers send this automatically and this the + * pump status packets can just be snooped, however my controller version does not do this. No harm in sending. + * + */ + private static void pumpWatchDog() { + boolean pumpsStillOnline = false; + Bridge bridge = PentairBaseBridgeHandler.getSingleBridge(); + if (bridge == null) { + PentairIntelliFloHandler.stopPollingJob(); + return; + } + + Collection things = bridge.getThings(); + + for (Thing t : things) { + if (!t.getThingTypeUID().equals(INTELLIFLO_THING_TYPE)) { + continue; + } + + if (t.getStatus() != ThingStatus.ONLINE) { + continue; + } + + pumpsStillOnline = true; + + PentairIntelliFloHandler handler = (PentairIntelliFloHandler) t.getHandler(); + if (handler == null) { + continue; + } + + if (handler.runMode) { + handler.getActions().coreSetOnOROff(true); + } else { + handler.getActions().getStatus(); + } + } + + if (!pumpsStillOnline) { + PentairIntelliFloHandler.stopPollingJob(); + } + } + + // checkOtherMaster - check to make sure the system does not have a controller OR that the controller is in + // servicemode + private boolean checkOtherMaster() { + PentairBaseBridgeHandler bridgeHandler = getBridgeHandler(); + + if (bridgeHandler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.bridge-missing"); + return true; + } + + PentairControllerHandler handler = bridgeHandler.findController(); + + return (handler != null && !handler.getServiceMode()); } @Override public void handleCommand(ChannelUID channelUID, Command command) { - if (command instanceof RefreshType) { - logger.debug("IntelliFlo received refresh command"); - updateChannel(channelUID.getId(), null); + if (command instanceof OnOffType onOffCommand) { + boolean state = onOffCommand == OnOffType.ON; + + switch (channelUID.getId()) { + case CHANNEL_INTELLIFLO_RUN: + case CHANNEL_INTELLIFLO_RPM: + if (!state) { + updateState(INTELLIFLO_RUNPROGRAM, OnOffType.OFF); + } + + actions.setOnOrOff(state); + + break; + case INTELLIFLO_RUNPROGRAM: + if (checkOtherMaster()) { + logger.debug("Unable to send command to pump as there is another master in the system"); + return; + } + + if (command instanceof DecimalType programNumber) { + if (programNumber.intValue() == 0) { + actions.setOnOrOff(false); + } else { + actions.setRunProgram(programNumber.intValue()); + } + } + } + } else if (command instanceof DecimalType decimalCommand) { + int num = decimalCommand.intValue(); + + switch (channelUID.getId()) { + case CHANNEL_INTELLIFLO_RPM: + updateState(INTELLIFLO_RUNPROGRAM, OnOffType.OFF); + actions.setRPM(num); + break; + } + } else if (command instanceof RefreshType) { + switch (channelUID.getId()) { + case CHANNEL_INTELLIFLO_RUN: + updateChannel(channelUID, pumpStatus.run); + break; + case CHANNEL_INTELLIFLO_POWER: + updateChannel(channelUID, pumpStatus.power, Units.WATT); + break; + case CHANNEL_INTELLIFLO_RPM: + updateChannel(channelUID, pumpStatus.rpm); + break; + case INTELLIFLO_GPM: + updateChannel(channelUID, pumpStatus.gpm, ImperialUnits.GALLON_PER_MINUTE); + break; + case INTELLIFLO_STATUS1: + updateChannel(channelUID, pumpStatus.status1); + break; + case INTELLIFLO_STATUS2: + updateChannel(channelUID, pumpStatus.status2); + break; + } } } @Override - public void processPacketFrom(PentairPacket p) { - switch (p.getAction()) { - case 1: // Pump command - A5 00 10 60 01 02 00 20 - logger.trace("Pump command (ack): {}: ", p); + public void processPacketFrom(PentairBasePacket packet) { + if (waitStatusForOnline) { + finishOnline(); + } + + PentairStandardPacket p = (PentairStandardPacket) packet; + + switch (p.getByte(PentairStandardPacket.ACTION)) { + case 0x01: // Pump command - A5 00 10 60 01 02 00 20 + logger.debug("[{}] Pump command (ack)", p.getSource()); break; - case 4: // Pump control panel on/off - logger.trace("Turn pump control panel (ack) {}: {} - {}", p.getSource(), - p.getByte(PentairPacket.STARTOFDATA), p); + case 0x04: // Pump control panel on/off + boolean remotemode; + + remotemode = p.getByte(0 + PentairStandardPacket.STARTOFDATA) == (byte) 0xFF; + logger.debug("[{}] Pump control panel (ack): {}", p.getSource(), remotemode); break; - case 5: // Set pump mode - logger.trace("Set pump mode (ack) {}: {} - {}", p.getSource(), p.getByte(PentairPacket.STARTOFDATA), p); + case 0x05: // Set pump mode ack + logger.debug("[{}] Set pump mode (ack): {}", p.getSource(), + p.getByte(0 + PentairStandardPacket.STARTOFDATA)); break; - case 6: // Set run mode - logger.trace("Set run mode (ack) {}: {} - {}", p.getSource(), p.getByte(PentairPacket.STARTOFDATA), p); + case 0x06: // Set run mode ack + logger.debug("[{}] Set run mode (ack): {}", p.getSource(), + p.getByte(0 + PentairStandardPacket.STARTOFDATA)); break; - case 7: // Pump status (after a request) - if (p.getLength() != 15) { - logger.debug("Expected length of 15: {}", p); + case 0x07: // Pump status (after a request) + if (p.getPacketLengthHeader() != 15) { + logger.debug("[{}]: Expected length of 15 onm pump status: {}", p.getSource(), p); return; } - /* - * P: A500 d=10 s=60 c=07 l=0f 0A0602024A08AC120000000A000F22 <028A> - * RUN 0a Started - * MOD 06 Feature 1 - * PMP 02 ? drive state - * PWR 024a 586 WATT - * RPM 08ac 2220 RPM - * GPM 12 18 GPM - * PPC 00 0 % - * b09 00 ? - * ERR 00 ok - * b11 0a ? - * TMR 00 0 MIN - * CLK 0f22 15:34 - */ - - logger.debug("Pump status: {}", p); - - /* - * Save the previous state of the packet (p29cur) into a temp variable (p29old) - * Update the current state to the new packet we just received. - * Then call updateChannel which will compare the previous state (now p29old) to the new state (p29cur) - * to determine if updateState needs to be called - */ - PentairPacketPumpStatus ppsOld = ppscur; - ppscur = new PentairPacketPumpStatus(p); - - updateChannel(INTELLIFLO_RUN, ppsOld); - updateChannel(INTELLIFLO_MODE, ppsOld); - updateChannel(INTELLIFLO_DRIVESTATE, ppsOld); - updateChannel(INTELLIFLO_POWER, ppsOld); - updateChannel(INTELLIFLO_RPM, ppsOld); - updateChannel(INTELLIFLO_PPC, ppsOld); - updateChannel(INTELLIFLO_ERROR, ppsOld); - updateChannel(INTELLIFLO_TIMER, ppsOld); - + pumpStatus.parsePacket(p); + logger.debug("[{}] Pump status: {}", p.getSource(), pumpStatus); + this.refreshAllChannels(); break; default: - logger.debug("Unhandled Intelliflo command: {}", p.toString()); - break; - } - } - - /** - * Helper function to compare and update channel if needed. The class variables p29_cur and phsp_cur are used to - * determine the appropriate state of the channel. - * - * @param channel name of channel to be updated, corresponds to channel name in {@link PentairBindingConstants} - * @param p Packet representing the former state. If null, no compare is done and state is updated. - */ - public void updateChannel(String channel, PentairPacket p) { - // Only called from this class's processPacketFrom, so we are confident this will be a PentairPacketPumpStatus - PentairPacketPumpStatus pps = (PentairPacketPumpStatus) p; - - switch (channel) { - case INTELLIFLO_RUN: - if (pps == null || (pps.run != ppscur.run)) { - updateState(channel, OnOffType.from((ppscur.run))); - } - break; - case INTELLIFLO_MODE: - if (pps == null || (pps.mode != ppscur.mode)) { - updateState(channel, new DecimalType(ppscur.mode)); - } - break; - case INTELLIFLO_DRIVESTATE: - if (pps == null || (pps.drivestate != ppscur.drivestate)) { - updateState(channel, new DecimalType(ppscur.drivestate)); - } - break; - case INTELLIFLO_POWER: - if (pps == null || (pps.power != ppscur.power)) { - updateState(channel, new DecimalType(ppscur.power)); - } - break; - case INTELLIFLO_RPM: - if (pps == null || (pps.rpm != ppscur.rpm)) { - updateState(channel, new DecimalType(ppscur.rpm)); - } - break; - case INTELLIFLO_PPC: - if (pps == null || (pps.ppc != ppscur.ppc)) { - updateState(channel, new DecimalType(ppscur.ppc)); - } - break; - case INTELLIFLO_ERROR: - if (pps == null || (pps.error != ppscur.error)) { - updateState(channel, new DecimalType(ppscur.error)); - } - break; - case INTELLIFLO_TIMER: - if (pps == null || (pps.timer != ppscur.timer)) { - updateState(channel, new DecimalType(ppscur.timer)); - } + logger.debug("[{}] Unhandled Intelliflo command {}: {}", p.getSource(), p.getAction(), p.toString()); break; } } diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairSerialBridgeHandler.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairSerialBridgeHandler.java index 4589bb00832..a9b0600d7d5 100644 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairSerialBridgeHandler.java +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairSerialBridgeHandler.java @@ -12,130 +12,115 @@ */ package org.openhab.binding.pentair.internal.handler; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.pentair.internal.config.PentairSerialBridgeConfig; +import org.openhab.core.io.transport.serial.PortInUseException; +import org.openhab.core.io.transport.serial.SerialPort; +import org.openhab.core.io.transport.serial.SerialPortIdentifier; +import org.openhab.core.io.transport.serial.SerialPortManager; +import org.openhab.core.io.transport.serial.UnsupportedCommOperationException; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import gnu.io.CommPort; -import gnu.io.CommPortIdentifier; -import gnu.io.NoSuchPortException; -import gnu.io.PortInUseException; -import gnu.io.SerialPort; -import gnu.io.UnsupportedCommOperationException; - /** - * Handler for the IPBridge. Implements the connect and disconnect abstract methods of {@link PentairBaseBridgeHandler} + * The {@link PentairSerialBridgeHandler } implments the class for the serial bridge. Implements the connect and + * disconnect abstract methods of {@link PentairBaseBridgeHandler} * * @author Jeff James - initial contribution * */ +@NonNullByDefault public class PentairSerialBridgeHandler extends PentairBaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(PentairSerialBridgeHandler.class); - /** SerialPort object representing the port where the RS485 adapter is connected */ - SerialPort port; + public PentairSerialBridgeConfig config = new PentairSerialBridgeConfig(); - public PentairSerialBridgeHandler(Bridge bridge) { + private final SerialPortManager serialPortManager; + @Nullable + private SerialPort port; + @Nullable + private SerialPortIdentifier portIdentifier; + + public PentairSerialBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) { super(bridge); + this.serialPortManager = serialPortManager; } @Override - protected synchronized void connect() { - PentairSerialBridgeConfig configuration = getConfigAs(PentairSerialBridgeConfig.class); + protected synchronized boolean connect() { + config = getConfigAs(PentairSerialBridgeConfig.class); + + if (config.serialPort.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.serial-port-empty"); + return false; + } + + this.portIdentifier = serialPortManager.getIdentifier(config.serialPort); + SerialPortIdentifier portIdentifier = this.portIdentifier; + if (portIdentifier == null) { + if (getThing().getStatus() != ThingStatus.OFFLINE) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.communication-error.serial-port-not-found" + config.serialPort); + } + return false; + } try { - CommPortIdentifier ci = CommPortIdentifier.getPortIdentifier(configuration.serialPort); - CommPort cp = ci.open("openhabpentairbridge", 10000); - if (cp == null) { - throw new IllegalStateException("cannot open serial port!"); + logger.trace("connect port: {}", config.serialPort); + + if (portIdentifier.isCurrentlyOwned()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication-error.serial-port-busy" + config.serialPort); + return false; } - if (cp instanceof SerialPort serialPort) { - port = serialPort; - } else { - throw new IllegalStateException("unknown port type"); + this.port = portIdentifier.open("org.openhab.binding.pentair", 10000); + SerialPort port = this.port; + + if (port == null) { + return false; } + port.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); - port.disableReceiveFraming(); - port.disableReceiveThreshold(); + port.setFlowControlMode(SerialPort.FLOWCONTROL_NONE); - reader = new BufferedInputStream(port.getInputStream()); - writer = new BufferedOutputStream(port.getOutputStream()); - logger.info("Pentair Bridge connected to serial port: {}", configuration.serialPort); + InputStream is = port.getInputStream(); + OutputStream os = port.getOutputStream(); + + if (is != null) { + setInputStream(is); + } + + if (os != null) { + setOutputStream(os); + } } catch (PortInUseException e) { - String msg = String.format("cannot open serial port: %s", configuration.serialPort); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; - } catch (UnsupportedCommOperationException e) { - String msg = String.format("got unsupported operation %s on port %s", e.getMessage(), - configuration.serialPort); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; - } catch (NoSuchPortException e) { - String msg = String.format("got no such port for %s", configuration.serialPort); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; - } catch (IllegalStateException e) { - String msg = String.format("receive IllegalStateException for port %s", configuration.serialPort); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; - } catch (IOException e) { - String msg = String.format("IOException on port %s", configuration.serialPort); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication-error.serial-port-busy" + config.serialPort); + return false; + } catch (UnsupportedCommOperationException | IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication-error.serial-port-error" + config.serialPort + ", " + e.getMessage()); + return false; } - parser = new Parser(); - thread = new Thread(parser); - thread.start(); + logger.debug("Pentair Bridge connected to serial port: {}", config.serialPort); - if (port != null && reader != null && writer != null) { - updateStatus(ThingStatus.ONLINE); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unable to connect"); - } + return true; } @Override protected synchronized void disconnect() { - updateStatus(ThingStatus.OFFLINE); - - if (thread != null) { - try { - thread.interrupt(); - thread.join(); // wait for thread to complete - } catch (InterruptedException e) { - // do nothing - } - thread = null; - parser = null; - } - - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - logger.trace("IOException when closing serial reader", e); - } - reader = null; - } - - if (writer != null) { - try { - writer.close(); - } catch (IOException e) { - logger.trace("IOException when closing serial writer", e); - } - writer = null; - } - + SerialPort port = this.port; if (port != null) { port.close(); port = null; diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerCircuit.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerCircuit.java new file mode 100644 index 00000000000..f7088bf846a --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerCircuit.java @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler.helpers; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PentairControllerCircuit } class is used to define circuit/features of the controller + * + * @author Jeff James - initial contribution + * + */ +@NonNullByDefault +public class PentairControllerCircuit { + public enum CircuitName { + EMPTY(-1, ""), + NOTUSED(0, "NOT USED"), + AERATOR(1, "AERATOR"), + AIRBLOWER(2, "AIR BLOWER"), + AUX1(3, "AUX 1"), + AUX2(4, "AUX 2"), + AUX3(5, "AUX 3"), + AUX4(6, "AUX 4"), + AUX5(7, "AUX 5"), + AUX6(8, "AUX 6"), + AUX7(9, "AUX 7"), + AUX8(10, "AUX 8"), + AUX9(11, "AUX 9"), + AUX10(12, "AUX 10"), + BACKWASH(13, "BACKWASH"), + BACKLIGHT(14, "BACK LIGHT"), + BBQLIGHT(15, "BBQ LIGHT"), + BEACHLIGHT(16, "BEACH LIGHT"), + BOOSTERPUMP(17, "BOOSTER PUMP"), + BUGLIGHT(18, "BUG LIGHT"), + CABANALTS(19, "CABANA LTS"), + CHEMFEEDER(20, "CHEM. 2FEEDER"), + CHLORINATOR(21, "CHLORINATOR"), + CLEANER(22, "CLEANER"), + COLORWHEEL(23, "COLOR WHEEL"), + DECKLIGHT(24, "DECK LIGHT"), + DRAINLINE(25, "DRAIN LINE"), + DRIVELIGHT(26, "DRIVE LIGHT"), + EDGEPUMP(27, "EDGE PUMP"), + ENTRYLIGHT(28, "ENTRY LIGHT"), + FAN(29, "FAN"), + FIBEROPTIC(30, "FIBER OPTIC"), + FIBERWORKS(31, "FIBER WORKS"), + FILLLINE(32, "FILL LINE"), + FLOORCLNR(33, "FLOOR CLNR"), + FOGGER(34, "FOGGER"), + FOUNTAIN(35, "FOUNTAIN"), + FOUNTAIN1(36, "FOUNTAIN 1"), + FOUNTAIN2(37, "FOUNTAIN 2"), + FOUNTAIN3(38, "FOUNTAIN 3"), + FOUNTAINS(39, "FOUNTAINS"), + FRONTLIGHT(40, "FRONT LIGHT"), + GARDENLTS(41, "GARDEN LTS"), + GAZEBOLTS(42, "GAZEBO LTS"), + HIGHSPEED(43, "HIGH SPEED"), + HITEMP(44, "HI-TEMP"), + HOUSELIGHT(45, "HOUSE LIGHT"), + JETS(46, "JETS"), + LIGHTS(47, "LIGHTS"), + LOWSPEED(48, "LOW SPEED"), + LOTEMP(49, "LO-TEMP"), + MALIBULTS(50, "MALIBU LTS"), + MIST(51, "MIST"), + MUSIC(52, "MUSIC"), + NOTUSED2(53, "NOT USED"), + OZONATOR(54, "OZONATOR"), + PATHLIGHTS(55, "PATH LIGHTS"), + PATIOLTS(56, "PATIO LTS"), + PERIMETERL(57, "PERIMETER L"), + PG2000(58, "PG2000"), + PONDLIGHT(59, "POND LIGHT"), + POOLPUMP(60, "POOL PUMP"), + POOL(61, "POOL"), + POOLHIGH(62, "POOL HIGH"), + POOLLIGHT(63, "POOL LIGHT"), + POOLLOW(64, "POOL LOW"), + SAM(65, "SAM"), + POOLSAM1(66, "POOL SAM 1"), + POOLSAM2(67, "POOL SAM 2"), + POOLSAM3(68, "POOL SAM 3"), + SECURITYLT(69, "SECURITY LT"), + SLIDE(70, "SLIDE"), + SOLAR(71, "SOLAR"), + SPA(72, "SPA"), + SPAHIGH(73, "SPA HIGH"), + SPALIGHT(74, "SPA LIGHT"), + SPALOW(75, "SPA LOW"), + SPASAL(76, "SPA SAL"), + SPASAM(77, "SPA SAM"), + SPAWTRFLL(78, "SPA WTRFLL"), + SPILLWAY(79, "SPILLWAY"), + SPRINKLERS(80, "SPRINKLERS"), + STREAM(81, "STREAM"), + STAUTELT(82, "STATUE LT"), + SWIMJETS(83, "SWIM JETS"), + WTRFEATURE(84, "WTR FEATURE"), + WTRFEATLT(85, "WTR FEAT LT"), + WATERFALL(86, "WATERFALL"), + WATERFALL1(87, "WATERFALL 1"), + WATERFALL2(88, "WATERFALL 2"), + WATERFALL3(89, "WATERFALL 3"), + WHIRLPOOL(90, "WHIRLPOOL"), + WTRFLLGHT(91, "WTRFL LGHT"), + YARDLIGHT(92, "YARD LIGHT"), + AUXEXTRA(93, "AUX EXTRA"), + FEATURE1(94, "FEATURE 1"), + FEATURE2(95, "FEATURE 2"), + FEATURE3(96, "FEATURE 3"), + FEATURE4(97, "FEATURE 4"), + FEATURE5(98, "FEATURE 5"), + FEATURE6(99, "FEATURE 6"), + FEATURE7(100, "FEATURE 7"), + FEATURE8(101, "FEATURE 8"), + USERNAME01(200, "USERNAME-01"), + USERNAME02(201, "USERNAME-02"), + USERNAME03(202, "USERNAME-03"), + USERNAME04(203, "USERNAME-04"), + USERNAME05(204, "USERNAME-05"), + USERNAME06(205, "USERNAME-06"), + USERNAME07(206, "USERNAME-07"), + USERNAME08(207, "USERNAME-08"), + USERNAME09(208, "USERNAME-09"), + USERNAME10(209, "USERNAME-10"); + + private final int number; + private final String friendlyName; + + CircuitName(int n, String friendlyName) { + this.number = n; + this.friendlyName = friendlyName; + } + + public int getCode() { + return number; + } + + public String getFriendlyName() { + return friendlyName; + } + + public static CircuitName valueOfModeNumber(int number) { + return Arrays.stream(values()).filter(value -> (value.getCode() == number)).findFirst() + .orElse(CircuitName.EMPTY); + } + } + + public enum CircuitFunction { + EMPTY(-1, ""), + GENERIC(0, "GENERIC"), + SPA(1, "SPA"), + POOL(2, "POOL"), + MASTERCLEANER(5, "MASTER CLEANER"), + LIGHT(7, "LIGHT"), + SAMLIGHT(9, "SAM LIGHT"), + SALLIGHT(10, "SAL LIGHT"), + PHOTONGEN(11, "PHOTON GEN"), + COLORWHEEL(12, "COLOR WHEEL"), + VALVES(13, "VALVES"), + SPILLWAY(14, "SPILLWAY"), + FLOORCLEANER(15, "FLOOR CLEANER"), + INTELLIBRITE(16, "INTELLIBRITE"), + MAGICSTREAM(17, "MAGICSTREAM"), + NOTUSED(19, "NOT USED"), + FREEZEPROTECT(64, "FREEZE PROTECTION ON"); + + private final int code; + private final String friendlyName; + + private CircuitFunction(int code, String friendlyName) { + this.code = code; + this.friendlyName = friendlyName; + } + + public int getCode() { + return code; + } + + public String getFriendlyName() { + return friendlyName; + } + + public static CircuitFunction valueOfModeNumber(int number) { + return Arrays.stream(values()).filter(value -> (value.getCode() == number)).findFirst() + .orElse(CircuitFunction.EMPTY); + } + } + + public final int id; + public CircuitName circuitName = CircuitName.EMPTY; + public CircuitFunction circuitFunction = CircuitFunction.EMPTY; + + public PentairControllerCircuit(int id) { + this.id = id; + } + + public void setName(int n) { + circuitName = CircuitName.valueOfModeNumber(n); + } + + public void setName(CircuitName circuitName) { + this.circuitName = circuitName; + } + + public void setFunction(int f) { + circuitFunction = CircuitFunction.valueOfModeNumber(f); + } + + public void setFunction(CircuitFunction circuitFunction) { + this.circuitFunction = circuitFunction; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerLightMode.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerLightMode.java new file mode 100644 index 00000000000..f10923dc587 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerLightMode.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler.helpers; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PentairControllerLightMode } enum constants used to define the different light modes of the controller. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public enum PentairControllerLightMode { + EMPTY(-1, ""), + OFF(0, "Off"), + ON(1, "On"), + COLORSYNC(128, "Color Sync"), + COLORSWIM(144, "Color Swim"), + COLORSET(160, "COLORSET"), + PARTY(177, "PARTY"), + ROMANCE(178, "ROMANCE"), + CARIBBENA(179, "CARIBBEAN"), + AMERICAN(180, "AMERICAN"), + SUNSET(181, "SUNSET"), + ROYAL(182, "ROYAL"), + BLUE(193, "BLUE"), + GREEN(194, "GREEN"), + RED(195, "RED"), + WHITE(96, "WHITE"), + MAGENTA(197, "MAGENTA"); + + private final int number; + private final String name; + + private PentairControllerLightMode(int n, String name) { + this.number = n; + this.name = name; + } + + public int getModeNumber() { + return number; + } + + public String getName() { + return name; + } + + public static PentairControllerLightMode valueOfModeNumber(int modeNumber) { + return Arrays.stream(values()).filter(value -> (value.getModeNumber() == modeNumber)).findFirst().orElse(EMPTY); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerSchedule.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerSchedule.java new file mode 100644 index 00000000000..2b21bd4e3b6 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerSchedule.java @@ -0,0 +1,303 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler.helpers; + +import static org.openhab.binding.pentair.internal.PentairBindingConstants.GROUP_CONTROLLER_SCHEDULE; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pentair.internal.actions.PentairControllerActions; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; + +/** + * The {@link PentairControllerSchdule } class stores the schedule details for a given controller schedule. + * + * @author Jeff James - initial contribution + * + */ +@NonNullByDefault +public class PentairControllerSchedule { + public static final int ID = 0 + +PentairStandardPacket.STARTOFDATA; + private static final int CIRCUIT = 1 + PentairStandardPacket.STARTOFDATA; + private static final int STARTH = 2 + PentairStandardPacket.STARTOFDATA; + private static final int STARTM = 3 + PentairStandardPacket.STARTOFDATA; + private static final int ENDH = 4 + PentairStandardPacket.STARTOFDATA; + private static final int ENDM = 5 + PentairStandardPacket.STARTOFDATA; + private static final int DAYS = 6 + PentairStandardPacket.STARTOFDATA; + + private static final String REGEX_SCHEDULE = "^(NONE|NORMAL|EGGTIMER|ONCEONLY),(\\\\d+),(\\\\d+):(\\\\d+),(\\\\d+):(\\\\d+),([SMTWRFY]+)"; + private static final Pattern PATTERN_SCHEDULE = Pattern.compile(REGEX_SCHEDULE); + + private boolean dirty; + + public enum ScheduleType { + NONE("None"), + NORMAL("Normal"), + EGGTIMER("Egg Timer"), + ONCEONLY("Once Only"), + UNKNOWN("Unknown"); + + private final String name; + + private ScheduleType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public int id; + public int circuit; + public ScheduleType type = ScheduleType.UNKNOWN; + public int start; + public int end; + public int days; + + public PentairControllerSchedule() { + super(); + } + + public PentairControllerSchedule(PentairStandardPacket p) { + super(); + parsePacket(p); + } + + public boolean isDirty() { + return dirty; + } + + public void setDirty(boolean d) { + this.dirty = d; + } + + public void parsePacket(PentairStandardPacket p) { + this.id = p.getByte(ID); + this.circuit = p.getByte(CIRCUIT); + this.days = p.getByte(DAYS); + + if (p.getByte(STARTH) == 25) { + this.type = ScheduleType.EGGTIMER; + this.start = 0; + this.end = p.getByte(ENDH) * 60 + p.getByte(ENDM); + } else if (p.getByte(ENDH) == 26) { + this.type = ScheduleType.ONCEONLY; + this.start = p.getByte(STARTH) * 60 + p.getByte(STARTM); + this.end = 0; + } else if (circuit == 0) { + this.type = ScheduleType.NONE; + this.start = 0; + this.end = 0; + } else { + this.type = ScheduleType.NORMAL; + this.start = p.getByte(STARTH) * 60 + p.getByte(STARTM); + this.end = p.getByte(ENDH) * 60 + p.getByte(ENDM); + } + } + + public String getScheduleTypeStr() { + return type.name(); + } + + public boolean setScheduleCircuit(int c) { + if (circuit == c) { + return true; + } + + if (c > 18 || c <= 0) { + return false; + } + + this.circuit = c; + this.dirty = true; + + return true; + } + + public boolean setScheduleStart(int min) { + if (min == start) { + return true; + } + + if (min > 1440 || min < 0) { + return false; + } + + this.start = min; + this.dirty = true; + + return true; + } + + public boolean setScheduleEnd(int min) { + if (min == end) { + return true; + } + + if (min > 1440 || min < 0) { + return false; + } + + this.end = min; + this.dirty = true; + + return true; + } + + public boolean setScheduleType(ScheduleType type) { + if (this.type == type) { + return true; + } + + this.type = type; + this.dirty = true; + + return true; + } + + public boolean setScheduleType(String typestring) { + ScheduleType scheduleType; + + try { + scheduleType = ScheduleType.valueOf(typestring); + } catch (IllegalArgumentException e) { + return false; + } + + return setScheduleType(scheduleType); + } + + public boolean setDays(String d) { + final String dow = "SMTWRFY"; + + days = 0; + for (int i = 0; i <= 6; i++) { + if (d.indexOf(dow.charAt(i)) >= 0) { + days |= 1 << i; + } + } + + dirty = true; + + return true; + } + + public @Nullable PentairStandardPacket getWritePacket(int controllerid, int preamble) { + byte[] packet = { (byte) 0xA5, (byte) preamble, (byte) controllerid, (byte) 0x00 /* source */, + (byte) PentairControllerActions.ControllerCommand.SAVE_SCHEDULE.send, (byte) 7, (byte) id, + (byte) circuit, (byte) (start / 60), (byte) (start % 60), (byte) (end / 60), (byte) (end % 60), + (byte) days }; + PentairStandardPacket p = new PentairStandardPacket(packet); + + switch (type) { + case NONE: + p.setByte(STARTH, (byte) 0); + p.setByte(STARTM, (byte) 0); + p.setByte(ENDH, (byte) 0); + p.setByte(ENDM, (byte) 0); + p.setByte(CIRCUIT, (byte) 0); + p.setByte(DAYS, (byte) 0); + break; + + case NORMAL: + break; + + case ONCEONLY: + p.setByte(ENDH, (byte) 26); + p.setByte(ENDM, (byte) 0); + break; + case EGGTIMER: + p.setByte(STARTH, (byte) 25); + p.setByte(STARTM, (byte) 0); + p.setByte(DAYS, (byte) 0); + break; + case UNKNOWN: + return null; + } + + return p; + } + + public String getDays() { + final String dow = "SMTWRFY"; + String str = ""; + + for (int i = 0; i <= 6; i++) { + if ((((days >> i) & 0x01)) == 0x01) { + str += dow.charAt(i); + } + } + + return str; + } + + @Override + public String toString() { + String str = String.format("%s,%d,%02d:%02d,%02d:%02d,%s", getScheduleTypeStr(), circuit, start / 60, + start % 60, end / 60, end % 60, getDays()); + + return str; + } + + public boolean fromString(String str) { + String schedulestr = str.toUpperCase(); + Matcher m = PATTERN_SCHEDULE.matcher(schedulestr); + + if (!m.find()) { + return false; + } + + if (!setScheduleCircuit(Integer.parseUnsignedInt(m.group(2)))) { + return false; + } + + int min = Integer.parseUnsignedInt(m.group(3)) * 60 + Integer.parseUnsignedInt(m.group(4)); + if (!setScheduleStart(min)) { + return false; + } + + min = Integer.parseUnsignedInt(m.group(5)) * 60 + Integer.parseUnsignedInt(m.group(6)); + if (!setScheduleEnd(min)) { + return false; + } + + if (!setDays(m.group(7))) { + return false; + } + + ScheduleType t; + try { + t = ScheduleType.valueOf(m.group(1)); + } catch (IllegalArgumentException e) { + return false; + } + + if (!setScheduleType(t)) { + return false; + } + + dirty = true; + + return true; + } + + public String getGroupID() { + String groupID = GROUP_CONTROLLER_SCHEDULE + Integer.toString(id); + + return groupID; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerStatus.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerStatus.java new file mode 100644 index 00000000000..3dddcab6349 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairControllerStatus.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler.helpers; + +import java.util.Arrays; +import java.util.Objects; + +import javax.measure.Unit; +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.openhab.core.library.unit.ImperialUnits; +import org.openhab.core.library.unit.SIUnits; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairControllerStatus } class contain all status values from the controller. + * + * @author Jeff James - initial contribution + * + */ +@NonNullByDefault +public class PentairControllerStatus { // 29 byte packet format + private final Logger logger = LoggerFactory.getLogger(PentairControllerStatus.class); + + public static final int NUMCIRCUITS = 18; + + private static final int HOUR = 0 + PentairStandardPacket.STARTOFDATA; + private static final int MIN = 1 + PentairStandardPacket.STARTOFDATA; + private static final int EQUIP1 = 2 + PentairStandardPacket.STARTOFDATA; + private static final int EQUIP2 = 3 + PentairStandardPacket.STARTOFDATA; + private static final int EQUIP3 = 4 + PentairStandardPacket.STARTOFDATA; + private static final int STATUS = 9 + PentairStandardPacket.STARTOFDATA; // Celsius (0x04) or Farenheit, Service + // Mode (0x01) + private static final int HEAT_ACTIVE = 10 + PentairStandardPacket.STARTOFDATA; + private static final int HEATER_DELAY = 12 + PentairStandardPacket.STARTOFDATA; // Something to do with heat? + private static final int POOL_TEMP = 14 + PentairStandardPacket.STARTOFDATA; + private static final int SPA_TEMP = 15 + PentairStandardPacket.STARTOFDATA; + private static final int AIR_TEMP = 18 + PentairStandardPacket.STARTOFDATA; + private static final int SOLAR_TEMP = 19 + PentairStandardPacket.STARTOFDATA; + + public int hour; + public int min; + + /** Individual boolean values representing whether a particular ciruit is on or off */ + public int equip; + public boolean pool, spa; + public boolean[] circuits = new boolean[NUMCIRCUITS]; + + public Unit uom = SIUnits.CELSIUS; + public boolean serviceMode; + public boolean heaterOn; + public boolean solarOn; + public boolean heaterDelay; + public int poolTemp; + /** spa temperature */ + public int spaTemp; + /** air temperature */ + public int airTemp; + /** solar temperature */ + public int solarTemp; + + /** spa heat mode - 0 = Off, 1 = Heater, 2 = Solar Pref, 3 = Solar */ + public int spaHeatMode; + /** pool heat mode - 0 = Off, 1 = Heater, 2 = Solar Pref, 3 = Solar */ + public int poolHeatMode; + + /** used to store packet value for reverse engineering, not used in normal operation */ + public int diag; + + public void parsePacket(PentairStandardPacket p) { + if (p.getPacketLengthHeader() != 29) { + logger.debug("Controller status packet not 29 bytes long"); + return; + } + + hour = p.getByte(HOUR); + min = p.getByte(MIN); + + pool = (p.getByte(EQUIP1) & 0x20) != 0; + spa = (p.getByte(EQUIP1) & 0x01) != 0; + + equip = p.getByte(EQUIP3) << 16 | p.getByte(EQUIP2) << 8 | p.getByte(EQUIP1); + + for (int i = 0; i < NUMCIRCUITS; i++) { + circuits[i] = ((equip >> i) & 0x0001) == 1; + } + + uom = ((p.getByte(STATUS) & 0x04) == 0) ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS; + serviceMode = (p.getByte(STATUS) & 0x01) != 0; + + heaterDelay = (p.getByte(HEATER_DELAY) & 0x02) != 0; + + diag = p.getByte(HEAT_ACTIVE); + + poolTemp = p.getByte(POOL_TEMP); + spaTemp = p.getByte(SPA_TEMP); + airTemp = p.getByte(AIR_TEMP); + solarTemp = p.getByte(SOLAR_TEMP); + + solarOn = (p.getByte(HEAT_ACTIVE) & 0x30) != 0; + heaterOn = (p.getByte(HEAT_ACTIVE) & 0x0C) != 0; + } + + @Override + public String toString() { + String str = String.format( + "%02d:%02d equip:%s pooltemp:%d spatemp:%d airtemp:%d solarttemp:%d uom:%s, service:%b, heaterDelay:%b", + hour, min, String.format("%18s", Integer.toBinaryString(equip)).replace(' ', '0'), poolTemp, spaTemp, + airTemp, solarTemp, uom.toString(), serviceMode, heaterDelay); + + return str; + } + + @Override + public boolean equals(@Nullable Object object) { + if (!(object instanceof PentairControllerStatus controllerStatus)) { + return false; + } + + PentairControllerStatus p = controllerStatus; + + return Arrays.equals(circuits, p.circuits) && poolTemp == p.poolTemp && spaTemp == p.spaTemp + && airTemp == p.airTemp && solarTemp == p.solarTemp && uom.equals(p.uom) && serviceMode == p.serviceMode + && solarOn == p.solarOn && heaterOn == p.heaterOn && heaterDelay == p.heaterDelay; + } + + @Override + public int hashCode() { + return Objects.hash(circuits, poolTemp, spaTemp, airTemp, solarTemp, uom, serviceMode, solarOn, heaterOn, + heaterDelay); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairHeatStatus.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairHeatStatus.java new file mode 100644 index 00000000000..44b48b35ef0 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairHeatStatus.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler.helpers; + +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; + +/** + * The {@link PentairHeatStatus } class contain heat set point info. Includes public variables. + * + * @author Jeff James - initial contribution + * + */ +@NonNullByDefault +public class PentairHeatStatus { + public enum HeatMode { + EMPTY(-1, ""), + NONE(0, "None"), + HEATER(1, "Heater"), + SOLARPREFERRED(2, "Solar Preferred"), + SOLAR(3, "Solar"); + + private final int code; + private final String friendlyName; + + private HeatMode(int code, String friendlyName) { + this.code = code; + this.friendlyName = friendlyName; + } + + public int getCode() { + return code; + } + + public String getFriendlyName() { + return friendlyName; + } + + public static HeatMode valueOfCode(int code) { + return Arrays.stream(values()).filter(value -> (value.getCode() == code)).findFirst().orElse(EMPTY); + } + } + + @SuppressWarnings("unused") + private static final int POOLTEMP = 1 + PentairStandardPacket.STARTOFDATA; + @SuppressWarnings("unused") + private static final int AIRTEMP = 2 + PentairStandardPacket.STARTOFDATA; + private static final int POOLSETPOINT = 3 + PentairStandardPacket.STARTOFDATA; + private static final int SPASETPOINT = 4 + PentairStandardPacket.STARTOFDATA; + private static final int HEATMODE = 5 + PentairStandardPacket.STARTOFDATA; + @SuppressWarnings("unused") + private static final int SOLARTEMP = 8 + PentairStandardPacket.STARTOFDATA; + + public int poolSetPoint; + public HeatMode poolHeatMode = HeatMode.EMPTY; + public int spaSetPoint; + public HeatMode spaHeatMode = HeatMode.EMPTY; + + public PentairHeatStatus() { + } + + public PentairHeatStatus(PentairStandardPacket p) { + parsePacket(p); + } + + public void parsePacket(PentairStandardPacket p) { + poolSetPoint = p.getByte(POOLSETPOINT); + poolHeatMode = HeatMode.valueOfCode(p.getByte(HEATMODE) & 0x03); + + spaSetPoint = p.getByte(SPASETPOINT); + spaHeatMode = HeatMode.valueOfCode((p.getByte(HEATMODE) >> 2) & 0x03); + } + + @Override + public String toString() { + String str = String.format("poolSetPoint: %d, poolHeatMode: %s, spaSetPoint: %d, spaHeatMode: %s", poolSetPoint, + poolHeatMode.name(), spaSetPoint, spaHeatMode.name()); + + return str; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairIntelliChem.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairIntelliChem.java new file mode 100644 index 00000000000..8d11e85f4f5 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairIntelliChem.java @@ -0,0 +1,372 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler.helpers; + +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pentair.internal.parser.PentairBasePacket; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairIntelliChem } class contains key values from the Pentair IntelliChem. + * + * @author Jeff James - initial contribution. + * + */ +@NonNullByDefault +public class PentairIntelliChem { + private final Logger logger = LoggerFactory.getLogger(PentairIntelliChem.class); + + private static final int PHREADINGHI = 0 + PentairStandardPacket.STARTOFDATA; + private static final int PHREADINGLO = 1 + PentairStandardPacket.STARTOFDATA; + private static final int ORPREADINGHI = 2 + PentairStandardPacket.STARTOFDATA; + private static final int ORPREADINGLO = 3 + PentairStandardPacket.STARTOFDATA; + private static final int PHSETPOINTHI = 4 + PentairStandardPacket.STARTOFDATA; + private static final int PHSETPOINTLO = 5 + PentairStandardPacket.STARTOFDATA; + private static final int ORPSETPOINTHI = 6 + PentairStandardPacket.STARTOFDATA; + private static final int ORPSETPOINTLO = 7 + PentairStandardPacket.STARTOFDATA; + private static final int PHDOSETIMEHI = 10 + PentairStandardPacket.STARTOFDATA; + private static final int PHDOSETIMELO = 11 + PentairStandardPacket.STARTOFDATA; + private static final int ORPDOSETIMEHI = 14 + PentairStandardPacket.STARTOFDATA; + private static final int ORPDOSETIMELO = 15 + PentairStandardPacket.STARTOFDATA; + @SuppressWarnings("unused") + private static final int PHVOLUMEDOSEDHI = 16 + PentairStandardPacket.STARTOFDATA; + @SuppressWarnings("unused") + private static final int PHVOLUMEDOSEDLO = 17 + PentairStandardPacket.STARTOFDATA; + @SuppressWarnings("unused") + private static final int ORPVOLUMEDOSEDHI = 18 + PentairStandardPacket.STARTOFDATA; + @SuppressWarnings("unused") + private static final int ORPVOLUMEDOSEDLO = 19 + PentairStandardPacket.STARTOFDATA; + private static final int TANK1LEVEL = 20 + PentairStandardPacket.STARTOFDATA; + private static final int TANK2LEVEL = 21 + PentairStandardPacket.STARTOFDATA; + private static final int LSI = 22 + PentairStandardPacket.STARTOFDATA; + private static final int CALCIUMHARDNESSHI = 23 + PentairStandardPacket.STARTOFDATA; + private static final int CALCIUMHARDNESSLO = 24 + PentairStandardPacket.STARTOFDATA; + private static final int CYAREADING = 26 + PentairStandardPacket.STARTOFDATA; + private static final int ALKALINITYHI = 27 + PentairStandardPacket.STARTOFDATA; + private static final int ALKALINITYLO = 28 + PentairStandardPacket.STARTOFDATA; + private static final int SALTLEVEL = 29 + PentairStandardPacket.STARTOFDATA; + @SuppressWarnings("unused") + private static final int TEMPERATURE = 31 + PentairStandardPacket.STARTOFDATA; + private static final int ALARMS = 32 + PentairStandardPacket.STARTOFDATA; + private static final int WARNINGS = 33 + PentairStandardPacket.STARTOFDATA; + private static final int DOSER_TYPE_STATUS = 34 + PentairStandardPacket.STARTOFDATA; + @SuppressWarnings("unused") + private static final int DELAYS = 35 + PentairStandardPacket.STARTOFDATA; + private static final int FIRMWAREMINOR = 36 + PentairStandardPacket.STARTOFDATA; + private static final int FIRMWAREMAJOR = 37 + PentairStandardPacket.STARTOFDATA; + + public enum PhDoserType { + NONE, + CO2, + ACID; + + private static PhDoserType getType(int num) { + switch (num) { + case 0: + return NONE; + case 1: + return ACID; + case 2: + return CO2; + case 3: + return ACID; + + } + return NONE; + } + } + + public enum OrpDoserType { + NONE, + ORP; + + private static OrpDoserType getType(int num) { + if (num == 0) { + return NONE; + } + + return ORP; + } + } + + public enum DosingStatus { + NONE, + DOSING, + MIXING, + MONITORING; + + private static DosingStatus getType(int num, boolean enabled) { + if (!enabled) { + return NONE; + } + + switch (num) { + case 0: + return DOSING; + case 1: + return MIXING; + case 2: + return MONITORING; + } + + return NONE; + } + } + + public double phReading; + public int orpReading; + public double phSetPoint; + public int orpSetPoint; // Oxidation Reduction Potential + public int tank1Level; + public int tank2Level; + public int calciumHardness; + public int cyaReading; // Cyanuric Acid + public int alkalinity; + + public boolean alarmWaterFlow; + public boolean alarmPh; + public boolean alarmOrp; + public boolean alarmPhTank; + public boolean alarmOrpTank; + public boolean alarmProbeFault; + + public boolean warningPhLockout; + public boolean warningPhDailyLimitReached; + public boolean warningOrpDailyLimitReached; + public boolean warningInvalidSetup; + public boolean warningChlorinatorCommError; + + public double lsi; + public PhDoserType phDoserType = PhDoserType.NONE; + public OrpDoserType orpDoserType = OrpDoserType.NONE; + public DosingStatus phDoserStatus = DosingStatus.NONE; + public DosingStatus orpDoserStatus = DosingStatus.NONE; + public int phDoseTime; + public int orpDoseTime; + public int saltLevel; + + public String firmwareVersion = ""; + + public double calcCalciumHardnessFactor() { + double calciumHardnessFactor = 0; + + if (calciumHardness <= 25) { + calciumHardnessFactor = 1.0; + } else if (calciumHardness <= 50) { + calciumHardnessFactor = 1.3; + } else if (calciumHardness <= 75) { + calciumHardnessFactor = 1.5; + } else if (calciumHardness <= 100) { + calciumHardnessFactor = 1.6; + } else if (calciumHardness <= 125) { + calciumHardnessFactor = 1.7; + } else if (calciumHardness <= 150) { + calciumHardnessFactor = 1.8; + } else if (calciumHardness <= 200) { + calciumHardnessFactor = 1.9; + } else if (calciumHardness <= 250) { + calciumHardnessFactor = 2.0; + } else if (calciumHardness <= 300) { + calciumHardnessFactor = 2.1; + } else if (calciumHardness <= 400) { + calciumHardnessFactor = 2.2; + } else if (calciumHardness <= 800) { + calciumHardnessFactor = 2.5; + } + + return calciumHardnessFactor; + } + + public double calcTemperatureFactor(QuantityType t) { + double temperatureFactor = 0; + int temperature = t.intValue(); + + if (t.getUnit().equals(SIUnits.CELSIUS)) { + if (temperature <= 0) { + temperatureFactor = 0.0; + } else if (temperature <= 2.8) { + temperatureFactor = 0.1; + } else if (temperature <= 7.8) { + temperatureFactor = 0.2; + } else if (temperature <= 11.7) { + temperatureFactor = 0.3; + } else if (temperature <= 15.6) { + temperatureFactor = 0.4; + } else if (temperature <= 18.9) { + temperatureFactor = 0.5; + } else if (temperature <= 24.4) { + temperatureFactor = 0.6; + } else if (temperature <= 28.9) { + temperatureFactor = 0.7; + } else if (temperature <= 34.4) { + temperatureFactor = 0.8; + } else if (temperature <= 40.6) { + temperatureFactor = 0.9; + } + } else { // Fahrenheit + if (temperature <= 32) { + temperatureFactor = 0.0; + } else if (temperature <= 37) { + temperatureFactor = 0.1; + } else if (temperature <= 46) { + temperatureFactor = 0.2; + } else if (temperature <= 53) { + temperatureFactor = 0.3; + } else if (temperature <= 60) { + temperatureFactor = 0.4; + } else if (temperature <= 66) { + temperatureFactor = 0.5; + } else if (temperature <= 76) { + temperatureFactor = 0.6; + } else if (temperature <= 84) { + temperatureFactor = 0.7; + } else if (temperature <= 94) { + temperatureFactor = 0.8; + } else if (temperature <= 105) { + temperatureFactor = 0.9; + } + } + + return temperatureFactor; + } + + public double calcCorrectedAlkalinity() { + return alkalinity - cyaReading / 3; + } + + public double calcAlkalinityFactor() { + double ppm = calcCorrectedAlkalinity(); + double alkalinityFactor = 0; + + if (ppm <= 25) { + alkalinityFactor = 1.4; + } else if (ppm <= 50) { + alkalinityFactor = 1.7; + } else if (ppm <= 75) { + alkalinityFactor = 1.9; + } else if (ppm <= 100) { + alkalinityFactor = 2.0; + } else if (ppm <= 125) { + alkalinityFactor = 2.1; + } else if (ppm <= 150) { + alkalinityFactor = 2.2; + } else if (ppm <= 200) { + alkalinityFactor = 2.3; + } else if (ppm <= 250) { + alkalinityFactor = 2.4; + } else if (ppm <= 300) { + alkalinityFactor = 2.5; + } else if (ppm <= 400) { + alkalinityFactor = 2.6; + } else if (ppm <= 800) { + alkalinityFactor = 2.9; + } + + return alkalinityFactor; + } + + public double calcTotalDisovledSolidsFactor(boolean saltPool) { + // 12.1 for non-salt; 12.2 for salt + + if (saltPool) { + return 12.2; + } + + return 12.1; + } + + public double calcSaturationIndex(@Nullable QuantityType waterTemp, boolean saltPool) { + double alkalinityFactor; + double temperatureFactor = .4; // if no temperature is available, use default value of .4 + double saturationIndex; + + if (waterTemp != null) { + temperatureFactor = calcTemperatureFactor(waterTemp); + } + + alkalinityFactor = calcAlkalinityFactor(); + + saturationIndex = this.phReading + calcCalciumHardnessFactor() + alkalinityFactor + temperatureFactor + - calcTotalDisovledSolidsFactor(saltPool); + + return saturationIndex; + } + + /** + * parsePacket - This function will parse a IntelliChem status packet. Note, this is based on the efforts of the + * nodejs-poolController utility since this is not equipment that I have and only minimally tested by the community. + * + * @param p - PentairPacket to parse + */ + public void parsePacket(PentairBasePacket packet) { + PentairStandardPacket p = (PentairStandardPacket) packet; + + if (p.getPacketLengthHeader() != 41) { + logger.debug("Intellichem packet not 41 bytes long"); + return; + } + + phReading = ((p.getByte(PHREADINGHI) << 8) + p.getByte(PHREADINGLO)) / 100.0; + orpReading = (p.getByte(ORPREADINGHI) << 8) + p.getByte(ORPREADINGLO); + phSetPoint = ((p.getByte(PHSETPOINTHI) << 8) + p.getByte(PHSETPOINTLO)) / 100.0; + orpSetPoint = (p.getByte(ORPSETPOINTHI) << 8) + p.getByte(ORPSETPOINTLO); + tank1Level = p.getByte(TANK1LEVEL); // should be value between 1-7 + tank2Level = p.getByte(TANK2LEVEL); + calciumHardness = (p.getByte(CALCIUMHARDNESSHI) << 8) + p.getByte(CALCIUMHARDNESSLO); + cyaReading = p.getByte(CYAREADING); + alkalinity = (p.getByte(ALKALINITYHI) << 8) + p.getByte(ALKALINITYLO); + phDoserType = PhDoserType.getType(p.getByte(DOSER_TYPE_STATUS) & 0x03); + orpDoserType = OrpDoserType.getType((p.getByte(DOSER_TYPE_STATUS) & 0x0C) >> 2); + phDoserStatus = DosingStatus.getType((p.getByte(DOSER_TYPE_STATUS) & 0x30) >> 4, + phDoserType != PhDoserType.NONE); + orpDoserStatus = DosingStatus.getType((p.getByte(DOSER_TYPE_STATUS) & 0xC0) >> 6, + orpDoserType != OrpDoserType.NONE); + lsi = ((p.getByte(LSI) & 0x80) != 0) ? (256 - p.getByte(LSI)) / -100.0 : p.getByte(LSI) / 100.0; + phDoseTime = (p.getByte(PHDOSETIMEHI) << 8) + p.getByte(PHDOSETIMELO); + orpDoseTime = (p.getByte(ORPDOSETIMEHI) << 8) + p.getByte(ORPDOSETIMELO); + saltLevel = p.getByte(SALTLEVEL) * 50; + + alarmWaterFlow = (p.getByte(ALARMS) & 0x01) != 0; + alarmPh = (p.getByte(ALARMS) & 0x06) != 0; + alarmOrp = (p.getByte(ALARMS) & 0x08) != 0; + alarmPhTank = (p.getByte(ALARMS) & 0x20) != 0; + alarmOrpTank = (p.getByte(ALARMS) & 0x40) != 0; + alarmProbeFault = (p.getByte(ALARMS) & 0x80) != 0; + + warningPhLockout = (p.getByte(WARNINGS) & 0x01) != 0; + warningPhDailyLimitReached = (p.getByte(WARNINGS) & 0x02) != 0; + warningOrpDailyLimitReached = (p.getByte(WARNINGS) & 0x04) != 0; + warningInvalidSetup = (p.getByte(WARNINGS) & 0x08) != 0; + warningChlorinatorCommError = (p.getByte(WARNINGS) & 0x10) != 0; + + firmwareVersion = String.format("%d.%03d", p.getByte(FIRMWAREMAJOR), p.getByte(FIRMWAREMINOR)); + } + + @Override + public String toString() { + String str = String.format( + "PH: %.2f, OPR: %d, PH set point: %.2f, ORP set point: %d, tank1: %d, tank2: %d, calcium hardness: %d, cyareading: %d, alkalinity: %d, phDoserType: %s, orpDoserType: %s, phDoserStatus: %b, orpDoserStatus: %b, phDoseTime: %d, orpDoseTime: %d, saturationindex: %f.1", + phReading, orpReading, phSetPoint, orpSetPoint, tank1Level, tank2Level, calciumHardness, cyaReading, + alkalinity, phDoserType.toString(), orpDoserType.toString(), phDoserStatus, orpDoserStatus, phDoseTime, + orpDoseTime, lsi); + + return str; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairPumpStatus.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairPumpStatus.java new file mode 100644 index 00000000000..59ef76d6073 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/helpers/PentairPumpStatus.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler.helpers; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairPumpStatus } class contains status fields from the pump status packet specialation of a + * PentairPacket. + * + * @author Jeff James - initial contribution + * + */ +@NonNullByDefault +public class PentairPumpStatus { // 15 byte packet format + private final Logger logger = LoggerFactory.getLogger(PentairPumpStatus.class); + + private static final int RUN = 0 + PentairStandardPacket.STARTOFDATA; + private static final int MODE = 1 + PentairStandardPacket.STARTOFDATA; // Mode in pump status. Means something + // else in pump + // write/response? + private static final int DRIVESTATE = 2 + PentairStandardPacket.STARTOFDATA; // ?? Drivestate in pump status. + // Means something else in + // pump write/response + private static final int WATTSH = 3 + PentairStandardPacket.STARTOFDATA; + private static final int WATTSL = 4 + PentairStandardPacket.STARTOFDATA; + private static final int RPMH = 5 + PentairStandardPacket.STARTOFDATA; + private static final int RPML = 6 + PentairStandardPacket.STARTOFDATA; + private static final int GPM = 7 + PentairStandardPacket.STARTOFDATA; + @SuppressWarnings("unused") + private static final int PPC = 8 + PentairStandardPacket.STARTOFDATA; // not sure what this is? always 0 + private static final int STATUS1 = 11 + PentairStandardPacket.STARTOFDATA; + private static final int STATUS2 = 12 + PentairStandardPacket.STARTOFDATA; + private static final int HOUR = 13 + PentairStandardPacket.STARTOFDATA; + private static final int MIN = 14 + PentairStandardPacket.STARTOFDATA; + + /** pump is running */ + public boolean run; + + /** pump mode (1-4) */ + public int mode; + + /** pump drivestate - not sure what this specifically represents. */ + public int drivestate; + /** pump power - in KW */ + public int power; + /** pump rpm */ + public int rpm; + /** pump gpm */ + public int gpm; + /** byte in packet indicating an error condition */ + public int error; + /** byte in packet indicated status */ + public int status1; + public int status2; + /** current timer for pump */ + public int timer; + /** hour or packet (based on Intelliflo time setting) */ + public int hour; + /** minute of packet (based on Intelliflo time setting) */ + public int min; + + public void parsePacket(PentairStandardPacket p) { + if (p.getPacketLengthHeader() != 15) { + logger.debug("Pump status packet not 15 bytes long"); + return; + } + + run = (p.getByte(RUN) == (byte) 0x0A); + mode = p.getByte(MODE); + drivestate = p.getByte(DRIVESTATE); + power = ((p.getByte(WATTSH) & 0xFF) * 256) + (p.getByte(WATTSL) & 0xFF); + rpm = ((p.getByte(RPMH) & 0xFF) * 256) + (p.getByte(RPML) & 0xFF); + gpm = p.getByte(GPM) & 0xFF; + + status1 = p.getByte(STATUS1); + status2 = p.getByte(STATUS2); + hour = p.getByte(HOUR); + min = p.getByte(MIN); + } + + @Override + public String toString() { + String str = String.format("%02d:%02d run:%b mode:%d power:%d rpm:%d gpm:%d status11:0x%h status12:0x%h", hour, + min, run, mode, power, rpm, gpm, status1, status2); + + return str; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairBasePacket.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairBasePacket.java new file mode 100644 index 00000000000..494fe845d1e --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairBasePacket.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.parser; + +import java.nio.ByteBuffer; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PentairBasePacket } base class is meant to be extended for either a "standard" pentair packet or the + * non-standard intellchlor packet + * + * @author Jeff James - initial contribution + * + */ +@NonNullByDefault +public class PentairBasePacket { + private static final char[] HEXARRAY = "0123456789ABCDEF".toCharArray(); + + public byte[] buf; + + public PentairBasePacket(int l) { + buf = new byte[l]; + } + + public PentairBasePacket(byte[] buf) { + this(buf, buf.length); + } + + public PentairBasePacket(byte[] buf, int l) { + this.buf = new byte[l]; + System.arraycopy(buf, 0, this.buf, 0, l); + } + + public int getLength() { + return buf.length; + } + + public int getByte(int n) { + return (buf[n]) & 0xff; + } + + public void setByte(int n, byte b) { + buf[n] = b; + } + + /** + * Helper function to convert byte to hex representation + * + * @param b byte to re + * @return 2 character hex string representing the byte + */ + public static String byteToHex(int b) { + char[] hexChars = new char[2]; + + hexChars[0] = HEXARRAY[b >>> 4]; + hexChars[1] = HEXARRAY[b & 0x0F]; + + return new String(hexChars); + } + + /** + * @param bytes array of bytes to convert to a hex string. Entire buf length is converted. + * @return hex string + */ + public static String toHexString(byte[] bytes) { + return toHexString(bytes, bytes.length); + } + + /** + * @param bytes array of bytes to convert to a hex string. + * @param len Number of bytes to convert + * @return hex string + */ + public static String toHexString(byte[] bytes, int len) { + char[] hexChars = new char[len * 3]; + for (int j = 0; j < len; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 3] = HEXARRAY[v >>> 4]; + hexChars[j * 3 + 1] = HEXARRAY[v & 0x0F]; + hexChars[j * 3 + 2] = ' '; + } + return new String(hexChars); + } + + public static String toHexString(ByteBuffer buf) { + return toHexString(buf.array(), buf.limit()); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return toHexString(buf, getLength()); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairIntelliChlorPacket.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairIntelliChlorPacket.java new file mode 100644 index 00000000000..87d4670c0a6 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairIntelliChlorPacket.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.parser; + +import java.nio.charset.StandardCharsets; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PentairIntelliChlorPacket } class extends the PentairPacket class to add specific items for the + * IntelliChlor packet format since it does not follow the standard packet format. + * + * @author Jeff James - initial contribution + * + */ +@NonNullByDefault +public class PentairIntelliChlorPacket extends PentairBasePacket { + public static final int DEST = 2; + public static final int ACTION = 3; + + // Set Generate % + public static final int SALTOUTPUT = 4; + + // Response to set Generate % + public static final int SALINITY = 4; + public static final int STATUS = 5; + + // Response to get version + public static final int VERSION = 4; + public static final int NAME = 5; + + public static int getPacketDataLength(int command) { + int length = -1; + + switch (command) { + case 0x03: // Response to version + length = 17; + break; + case 0x00: // Get status of Chlorinator + case 0x11: // Set salt output level (from controller->chlorinator) + case 0x14: + length = 1; + break; + case 0x01: // Response to Get Status + case 0x12: // status update with salinity and status + length = 2; + break; + } + + return length; + } + + public PentairIntelliChlorPacket(byte[] buf, int length) { + super(buf, length); + } + + public int getVersion() { + if (this.getByte(ACTION) != 0x03) { + return -1; + } + + return buf[VERSION] & 0xFF; + } + + public int getDest() { + return buf[DEST]; + } + + public String getName() { + if (this.getByte(ACTION) != 0x03) { + return ""; + } + + String name = new String(buf, NAME, 16, StandardCharsets.UTF_8); + + return name; + } + + /* + * Salt Output is available only in packets where the action is 0x11. This is packet sent from the + * controller to the chlorinator to set the salt output to a specific level. + */ + public int getSaltOutput() { + return (this.getByte(ACTION) == 0x11) ? (buf[SALTOUTPUT] & 0xFF) : -1; + } + + // Salinity and LED status are sent on a packet with action is 0x12. This is sent from the chlorinator. + public int getSalinity() { + return (this.getByte(ACTION) == 0x12) ? (buf[SALINITY] & 0xFF) * 50 : -1; + } + + public boolean getOk() { + return (this.getByte(ACTION) == 0x12) ? ((buf[STATUS] & 0xFF) == 0) || ((buf[STATUS] & 0xFF) == 0x80) : false; + } + + public boolean getLowFlow() { + return (this.getByte(ACTION) == 0x12) ? (buf[STATUS] & 0x01) != 0 : false; + } + + public boolean getLowSalt() { + return (this.getByte(ACTION) == 0x12) ? (buf[STATUS] & 0x02) != 0 : false; + } + + public boolean getVeryLowSalt() { + return (this.getByte(ACTION) == 0x12) ? (buf[STATUS] & 0x04) != 0 : false; + } + + public boolean getHighCurrent() { + return (this.getByte(ACTION) == 0x12) ? (buf[STATUS] & 0x08) != 0 : false; + } + + public boolean getCleanCell() { + return (this.getByte(ACTION) == 0x12) ? (buf[STATUS] & 0x10) != 0 : false; + } + + public boolean getLowVoltage() { + return (this.getByte(ACTION) == 0x12) ? (buf[STATUS] & 0x20) != 0 : false; + } + + public boolean getLowWaterTemp() { + return (this.getByte(ACTION) == 0x12) ? (buf[STATUS] & 0x40) != 0 : false; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairParser.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairParser.java new file mode 100644 index 00000000000..82cd2370a17 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairParser.java @@ -0,0 +1,251 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.parser; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairParser } class implements the thread to read and parse the input stream. Once a packet can be + * identified, it locates the + * representative sending Thing and dispositions the packet so it can be further processed. + * + * @author Jeff James - Initial contribution + * + */ +@NonNullByDefault +public class PentairParser implements Runnable { + public static final int MAX_PACKET_SIZE = 50; + + private final Logger logger = LoggerFactory.getLogger(PentairParser.class); + + private enum ParserState { + WAIT_STARTOFPACKET, + CMD_PENTAIR, + CMD_INTELLICHLOR + }; + + private @Nullable InputStream reader; + + public void setInputStream(InputStream reader) { + this.reader = reader; + } + + // Callback interface when a packet is received + public interface CallbackPentairParser { + public void onPentairPacket(PentairStandardPacket p); + + public void onIntelliChlorPacket(PentairIntelliChlorPacket p); + + public void parserFailureCallback(); + }; + + @Nullable + private CallbackPentairParser callback; + + public void setCallback(CallbackPentairParser cb) { + callback = cb; + } + + private int getByte() throws IOException { + InputStream reader = Objects.requireNonNull(this.reader, "Reader has not been initialized."); + + return reader.read(); + } + + private int getBytes(ByteBuffer buf, int n) throws IOException { + for (int i = 0; i < n; i++) { + buf.put((byte) getByte()); + } + + return n; + } + + private int calcChecksum(ByteBuffer buf) { + int chksum = 0, i; + + for (i = 0; i < buf.limit(); i++) { + chksum += (buf.get() & 0xFF); + } + + return chksum; + } + + @Override + public void run() { + ByteBuffer buf = ByteBuffer.allocate(MAX_PACKET_SIZE + 10); + int c, c2; + int checksumInPacket, checksumCalc; + int length; + + ParserState parserstate = ParserState.WAIT_STARTOFPACKET; + + Objects.requireNonNull(this.reader, "Reader stream has not been set."); + + while (!Thread.interrupted()) { + try { + c = getByte(); + + switch (parserstate) { + case WAIT_STARTOFPACKET: // will parse FF FF FF ... 00 + if (c == 0xFF) { // for CMD_PENTAIR, we need at lease one 0xFF + do { + c = getByte(); + } while (c == 0xFF); // consume all 0xFF + + if (c == 0x00) { + parserstate = ParserState.CMD_PENTAIR; + } + } + + if (c == 0x10) { + parserstate = ParserState.CMD_INTELLICHLOR; + } + break; + case CMD_PENTAIR: { + parserstate = ParserState.WAIT_STARTOFPACKET; // any break caused by invalid packet will go + // back to waiting for a new start of packet + + if (c != 0xFF) { + logger.trace("parser: FF00 !FF"); + break; + } + + buf.clear(); + + if (getBytes(buf, 6) != 6) { // read enough to get the length + logger.trace("Unable to read 6 bytes"); + + break; + } + + if (buf.get(0) != (byte) 0xA5) { + logger.trace("parser: FF00FF !A5"); + break; + } + + length = (buf.get(5) & 0xFF); + if (length > MAX_PACKET_SIZE) { + logger.trace("Received packet longer than {} bytes: {}", MAX_PACKET_SIZE, length); + break; + } + + // buf should contain A5 00 0F 10 02 1D (A5 00 D S A L) + if (getBytes(buf, length) != length) { // read remaining packet + break; + } + + checksumInPacket = (getByte() << 8) & 0xFF00; + checksumInPacket += (getByte() & 0xFF); + + buf.flip(); + + checksumCalc = calcChecksum(buf.duplicate()); + + if (checksumInPacket != checksumCalc) { + logger.trace("Checksum error: {}!={}-{}", checksumInPacket, checksumCalc, + PentairBasePacket.toHexString(buf)); + break; + } + + PentairStandardPacket p = new PentairStandardPacket(buf.array(), buf.limit()); + + logger.trace("[{}] PentairPacket: {}", p.getSource(), p.toString()); + CallbackPentairParser callback = this.callback; + if (callback != null) { + callback.onPentairPacket(p); + } + + break; + } + case CMD_INTELLICHLOR: { // 10 02 00 12 89 90 xx 10 03 + parserstate = ParserState.WAIT_STARTOFPACKET; // any break caused by invalid packet will go back + // to waiting on a new packet frame + + buf.clear(); + buf.put((byte) 0x10); // need to add back in the initial start of packet since that is included + // in checksum + + if ((byte) c != (byte) 0x02) { + break; + } + buf.put((byte) c); + buf.put((byte) getByte()); // Destination + + c = (byte) getByte(); + buf.put((byte) c); // Command + + length = PentairIntelliChlorPacket.getPacketDataLength(c); + int dest = buf.get(2); + if (length == -1) { + logger.debug("[{}] IntelliChlor Packet unseen: command - {}", dest, c & 0xFF); + break; + } + + // data bytes + 1 checksum + 0x10, 0x03 + if (getBytes(buf, length) != length) { + break; + } + + checksumInPacket = getByte(); + + c = getByte(); // 0x10 + c2 = getByte(); // 0x03 + // Check to see if closing command is 0x10 and and 0x03 + if ((byte) c != (byte) 0x10 || (byte) c2 != (byte) 0x03) { + logger.trace("[{}]Invalid Intellichlor command: {}", dest, + PentairBasePacket.toHexString(buf)); + break; // invalid command + } + + buf.flip(); + checksumCalc = calcChecksum(buf.duplicate()); + if ((byte) checksumCalc != (byte) checksumInPacket) { + logger.trace("[{}] Invalid Intellichlor checksum: {}", dest, + PentairBasePacket.toHexString(buf)); + break; + } + + PentairIntelliChlorPacket pic = new PentairIntelliChlorPacket(buf.array(), buf.limit()); + + logger.trace("[{}] IntelliChlor Packet: {}", dest, pic.toString()); + CallbackPentairParser callback = this.callback; + if (callback != null) { + callback.onIntelliChlorPacket(pic); + } + + break; + } + } + } catch (IOException e) { + logger.debug("I/O error while reading from stream: {}", e.getMessage()); + Thread.currentThread().interrupt(); + CallbackPentairParser callback = this.callback; + if (callback != null) { + callback.parserFailureCallback(); + } + break; // exit while loop + // PentairBaseBridgeHandler will monitor this thread and restart if it exits unexpectedly + } + } + + logger.trace("msg reader thread exited"); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairStandardPacket.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairStandardPacket.java new file mode 100644 index 00000000000..ea86bb8978a --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/parser/PentairStandardPacket.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.parser; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * + * { @link PentairStandardPacket } class implements the pentair standard packet format. Most commands sent over the bus + * utilize this format (with the exception of the Intellichlor packets). + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class PentairStandardPacket extends PentairBasePacket { + public static final int A5 = 0; + public static final int PREAMBLE = 1; + public static final int DEST = 2; + public static final int SOURCE = 3; + public static final int ACTION = 4; + public static final int LENGTH = 5; + public static final int STARTOFDATA = 6; + + /** + * Constructor for an empty packet. Typically used when generating a packet to + * send. Should include all bytes starting with A5, but not including the checksum + */ + public PentairStandardPacket() { + super(6); + + buf[0] = (byte) 0xA5; + } + + public PentairStandardPacket(byte[] array, int limit) { + super(array, limit); + } + + /* + * Constructor to create packet from this p + */ + public PentairStandardPacket(byte[] packet) { + super(packet); + } + + /** + * Gets length of packet + * + * @return length of packet + */ + public int getPacketLengthHeader() { + return (buf[LENGTH] & 0xFF); + } + + /** + * Sets length of packet + * + * @param length length of packet + */ + public void setPacketLengthHeader(int length) { + if (length > (buf[LENGTH] & 0xFF)) { + buf = new byte[length + 6]; + } + buf[LENGTH] = (byte) length; + } + + public int getSource() { + return buf[SOURCE]; + } + + public int getDest() { + return buf[DEST]; + } + + public int getAction() { + return buf[ACTION]; + } + + /** + * Calculate checksum of the representative packet. + * + * @return checksum of packet + */ + public int calcChecksum() { + int checksum = 0, i; + + for (i = 0; i < getPacketLengthHeader() + 6; i++) { + checksum += buf[i] & 0xFF; + } + + return checksum; + } + + /** + * Helper function to prepare the packet (including pre-amble and checksum) before being sent + * + * @return + */ + public byte[] wrapPacketToSend() { + int checksum; + + byte[] preamble = { (byte) 0xFF, (byte) 0x00, (byte) 0xFF }; + byte[] writebuf; + + writebuf = new byte[preamble.length + buf.length + 2]; + + System.arraycopy(preamble, 0, writebuf, 0, preamble.length); + System.arraycopy(this.buf, 0, writebuf, preamble.length, buf.length); + + checksum = calcChecksum(); + + writebuf[writebuf.length - 2] = (byte) ((checksum >> 8) & 0xFF); + writebuf[writebuf.length - 1] = (byte) (checksum & 0xFF); + + return writebuf; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/utils/ExpiringCache.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/utils/ExpiringCache.java new file mode 100755 index 00000000000..5e375b45219 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/utils/ExpiringCache.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.utils; + +import java.lang.ref.SoftReference; +import java.time.Duration; +import java.util.function.Supplier; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This is a modified version of the ExpiryCache which adds functions such as getLastKnownValue. It also allows an + * interface via Supplier which will return the value, or through a function which calls putValue. + * + * There must be provided an action in order to retrieve/calculate the value. This action will be called only if the + * answer from the last calculation is not valid anymore, i.e. if it is expired. + * + * @author Christoph Weitkamp - Initial contribution + * @author Martin van Wingerden - Add Duration constructor + * @author Jeff James - Added added getLastKnownValue + * + * @param the type of the value + */ +@NonNullByDefault +public class ExpiringCache { + private final long expiry; + + private SoftReference<@Nullable V> value = new SoftReference<>(null); + private long expiresAt; + + public interface RefreshAction { + void refresh(); + } + + public ExpiringCache() { + this.expiry = 0; + } + + /** + * Create a new instance. + * + * @param expiry the duration for how long the value stays valid + * @param action the action to retrieve/calculate the value + * @throws IllegalArgumentException For an expire value <=0. + */ + public ExpiringCache(Duration expiry) { + if (expiry.isNegative() || expiry.isZero()) { + throw new IllegalArgumentException("Cache expire time must be greater than 0"); + } + this.expiry = expiry.toNanos(); + } + + /** + * Create a new instance. + * + * @param expiry the duration in milliseconds for how long the value stays valid + * @param action the action to retrieve/calculate the value + */ + public ExpiringCache(long expiry) { + this(Duration.ofMillis(expiry)); + } + + /** + * Returns the value - possibly from the cache, if it is still valid. + */ + public synchronized @Nullable V getValue(Supplier<@Nullable V> action) { + @Nullable + V cachedValue = value.get(); + if (cachedValue == null || isExpired()) { + return refreshValue(action); + } + return cachedValue; + } + + /** + * Returns the value - either from the cache or will call the action function which is responsible for calling + * putValue. + */ + public synchronized @Nullable V getValue(RefreshAction action) { + @Nullable + V cachedValue = value.get(); + if (cachedValue == null || isExpired()) { + action.refresh(); + cachedValue = value.get(); + } + + return cachedValue; + } + + /** + * Returns the last known value + */ + public synchronized @Nullable V getLastKnownValue() { + return value.get(); + } + + /** + * Puts a new value into the cache. + * + * @param value the new value + */ + public final synchronized void putValue(@Nullable V value) { + this.value = new SoftReference<>(value); + expiresAt = calcExpiresAt(); + } + + /** + * Invalidates the value in the cache. + */ + public final synchronized void invalidateValue() { + value = new SoftReference<>(null); + expiresAt = 0; + } + + /** + * Refreshes and returns the value in the cache. + * If null returned from action.get, the get action should have sued putValue to update the item + * + * @return the new value + */ + public synchronized @Nullable V refreshValue(Supplier<@Nullable V> action) { + @Nullable + V freshValue = action.get(); + if (freshValue == null) { + return null; + } + value = new SoftReference<>(freshValue); + expiresAt = calcExpiresAt(); + return freshValue; + } + + /** + * Checks if the value is expired. + * + * @return true if the value is expired + */ + public boolean isExpired() { + return expiresAt < System.nanoTime(); + } + + private long calcExpiresAt() { + return System.nanoTime() + expiry; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/i18n/pentair.properties b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/i18n/pentair.properties index d1b605b1d9c..c214910600e 100644 --- a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/i18n/pentair.properties +++ b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/i18n/pentair.properties @@ -5,14 +5,251 @@ addon.pentair.description = This is the binding for Pentair pool systems. # thing types -thing-type.pentair.easytouch.label = EasyTouch Controller -thing-type.pentair.easytouch.description = Pentair EasyTouch Controller +thing-type.pentair.controller.label = Controller +thing-type.pentair.controller.description = A Pentair Controller such as EasyTouch or IntelliTouch. +thing-type.pentair.controller.group.aux1.label = Aux 1 Circuit +thing-type.pentair.controller.group.aux2.label = Aux 2 Circuit +thing-type.pentair.controller.group.aux3.label = Aux 3 Circuit +thing-type.pentair.controller.group.aux4.label = Aux 4 Circuit +thing-type.pentair.controller.group.aux5.label = Aux 5 Circuit +thing-type.pentair.controller.group.aux6.label = Aux 6 Circuit +thing-type.pentair.controller.group.aux7.label = Aux 7 Circuit +thing-type.pentair.controller.group.aux8.label = Aux 8 Circuit +thing-type.pentair.controller.group.feature1.label = Feature 1 +thing-type.pentair.controller.group.feature2.label = Feature 2 +thing-type.pentair.controller.group.feature3.label = Feature 3 +thing-type.pentair.controller.group.feature4.label = Feature 4 +thing-type.pentair.controller.group.feature5.label = Feature 5 +thing-type.pentair.controller.group.feature6.label = Feature 6 +thing-type.pentair.controller.group.feature7.label = Feature 7 +thing-type.pentair.controller.group.feature8.label = Feature 8 +thing-type.pentair.controller.group.pool.label = Pool Circuit +thing-type.pentair.controller.group.poolheat.label = Pool Temperature +thing-type.pentair.controller.group.schedule1.label = Schedule 1 +thing-type.pentair.controller.group.schedule2.label = Schedule 2 +thing-type.pentair.controller.group.schedule3.label = Schedule 3 +thing-type.pentair.controller.group.schedule4.label = Schedule 4 +thing-type.pentair.controller.group.schedule5.label = Schedule 5 +thing-type.pentair.controller.group.schedule6.label = Schedule 6 +thing-type.pentair.controller.group.schedule7.label = Schedule 7 +thing-type.pentair.controller.group.schedule8.label = Schedule 8 +thing-type.pentair.controller.group.schedule9.label = Schedule 9 +thing-type.pentair.controller.group.spa.label = Spa Circuit +thing-type.pentair.controller.group.spaheat.label = Spa Temperature +thing-type.pentair.intellichem.label = Intellichem +thing-type.pentair.intellichem.description = A Pentair Intellichem controller. thing-type.pentair.intellichlor.label = Intellichlor IC40 thing-type.pentair.intellichlor.description = Pentair Intellichlor IC40 -thing-type.pentair.intelliflo.label = Intelliflo Pump -thing-type.pentair.intelliflo.description = Pentair Intelliflo Pump +thing-type.pentair.intelliflo.label = Pentair Intelliflo +thing-type.pentair.intelliflo.description = A Pentair Intelliflo pump thing-type.pentair.ip_bridge.label = IP Bridge -thing-type.pentair.ip_bridge.description = This bridge is for use over a network interface. +thing-type.pentair.ip_bridge.description = This bridge is for used over a network interface. +thing-type.pentair.serial_bridge.label = Pentair-RS485 Serial Bridge +thing-type.pentair.serial_bridge.description = This bridge should be configured when using a USB->RS485 interface. + +# thing types config + +thing-type.config.pentair.controller.id.label = ID +thing-type.config.pentair.controller.id.description = The ID of the device (in decimal, not hex) +thing-type.config.pentair.controller.synctime.label = Synchronize Time +thing-type.config.pentair.controller.synctime.description = Enables automatic synchronization of the pool controller clock with the system clock +thing-type.config.pentair.intellichem.id.label = ID +thing-type.config.pentair.intellichem.id.description = The ID of the device (in decimal, not hex) +thing-type.config.pentair.intelliflo.id.label = ID +thing-type.config.pentair.intelliflo.id.description = The ID of the device (in decimal, not hex) +thing-type.config.pentair.ip_bridge.address.label = IP Address +thing-type.config.pentair.ip_bridge.address.description = The IP address of the network interface. +thing-type.config.pentair.ip_bridge.discovery.label = Enable Discovery +thing-type.config.pentair.ip_bridge.discovery.description = Enable automatic discovery of devices +thing-type.config.pentair.ip_bridge.id.label = Pentair ID +thing-type.config.pentair.ip_bridge.id.description = The ID to use when sending commands on the Pentair bus (default: 34) +thing-type.config.pentair.ip_bridge.port.label = Port +thing-type.config.pentair.ip_bridge.port.description = The port used to connect to the network interface. +thing-type.config.pentair.serial_bridge.discovery.label = Enable Discovery +thing-type.config.pentair.serial_bridge.discovery.description = Enable automatic discovery of devices +thing-type.config.pentair.serial_bridge.id.label = Pentair ID +thing-type.config.pentair.serial_bridge.id.description = The ID to use to send commands on the Pentair bus (default: 34) +thing-type.config.pentair.serial_bridge.serialPort.label = Serial Port +thing-type.config.pentair.serial_bridge.serialPort.description = The serial port name. Valid values are e.g. COM1 for Windows and /dev/ttyS0 or /dev/ttyUSB0 for Linux. + +# channel group types + +channel-group-type.pentair.circuit.label = Circuit +channel-group-type.pentair.circuit.description = Circuit +channel-group-type.pentair.feature.label = Feature +channel-group-type.pentair.feature.description = Features +channel-group-type.pentair.heat.label = Heat +channel-group-type.pentair.heat.description = Heat +channel-group-type.pentair.schedule.label = Schedule +channel-group-type.pentair.schedule.description = schedule +channel-group-type.pentair.status.label = Status +channel-group-type.pentair.status.description = General status channels for controller + +# channel types + +channel-type.pentair.airTemp.label = Air Temperature +channel-type.pentair.airTemp.description = The temperature of the air. +channel-type.pentair.alarmOrp.label = ORP Alarm +channel-type.pentair.alarmOrp.description = ORP alarm reported. +channel-type.pentair.alarmOrpTank.label = ORP Tank Alarm +channel-type.pentair.alarmOrpTank.description = ORP tank alarm reported. +channel-type.pentair.alarmPh.label = PH Alarm +channel-type.pentair.alarmPh.description = PH alarm reported. +channel-type.pentair.alarmPhTank.label = PH Tank Alarm +channel-type.pentair.alarmPhTank.description = PH tank alarm reported. +channel-type.pentair.alarmProbeFault.label = Probe Fault Alarm +channel-type.pentair.alarmProbeFault.description = Probe fault alarm reported. +channel-type.pentair.alarmWaterFlow.label = Water Flow Alarm +channel-type.pentair.alarmWaterFlow.description = Water flow alarm (on = no water flow). +channel-type.pentair.alkalinity.label = Total Alkalinity +channel-type.pentair.alkalinity.description = Total Alkalinity reading (ppm). +channel-type.pentair.auxSwitch.label = Auxillary Switch +channel-type.pentair.auxSwitch.description = The on/off control for this circuit. +channel-type.pentair.calciumHardness.label = Calcium Hardess +channel-type.pentair.calciumHardness.description = Calcium hardness (ppm). +channel-type.pentair.circuitFunction.label = Circuit Function +channel-type.pentair.circuitFunction.description = The function this circuit controls. +channel-type.pentair.circuitName.label = Circuit Name +channel-type.pentair.circuitName.description = The name of this circuit. +channel-type.pentair.cleanCell.label = Clean Cell +channel-type.pentair.cleanCell.description = Clean chlorinator cell. +channel-type.pentair.cyaReading.label = CYA Reading +channel-type.pentair.cyaReading.description = Cyanuric acid reading (ppm). +channel-type.pentair.doseTime.label = Dose Time +channel-type.pentair.doseTime.description = The time a particular chemical has been dosing. +channel-type.pentair.doserStatus.label = Doser Status +channel-type.pentair.doserStatus.description = Whether the chemical is currently dosing. +channel-type.pentair.gpm.label = GPM +channel-type.pentair.gpm.description = Pump GPM (only valid for VF pumps) +channel-type.pentair.heatMode.label = Heat Mode +channel-type.pentair.heatMode.description = The current head mode (None, Heater, Solar Preferred, Solar). +channel-type.pentair.heatMode.state.option.NONE = None +channel-type.pentair.heatMode.state.option.HEATER = Heater +channel-type.pentair.heatMode.state.option.SOLARPREFERRED = Solar Preferred +channel-type.pentair.heatMode.state.option.SOLAR = Solar +channel-type.pentair.heatSetPoint.label = Temperature Set Point +channel-type.pentair.heatSetPoint.description = The set point temperature for this mode. +channel-type.pentair.heatTemperature.label = Water Temperature +channel-type.pentair.heatTemperature.description = The temperature of the water. Only valid when pool pump is running. +channel-type.pentair.heaterDelay.label = Heater Delay +channel-type.pentair.heaterDelay.description = Pump is continuing to run to allow the heater to cool. +channel-type.pentair.heaterState.label = Heater State +channel-type.pentair.heaterState.description = The state of the heater (on, off) +channel-type.pentair.highCurrent.label = High Current +channel-type.pentair.highCurrent.description = Chlorinator drawing high current. +channel-type.pentair.lightMode.label = Light Mode +channel-type.pentair.lightMode.description = The current light mode. +channel-type.pentair.lightMode.state.option.OFF = Off +channel-type.pentair.lightMode.state.option.ON = On +channel-type.pentair.lightMode.state.option.COLORSYNC = Color Sync +channel-type.pentair.lightMode.state.option.COLORSWIM = Color Swim +channel-type.pentair.lightMode.state.option.COLORSET = Color Set +channel-type.pentair.lightMode.state.option.PARTY = Party +channel-type.pentair.lightMode.state.option.ROMANCE = Romance +channel-type.pentair.lightMode.state.option.CARIBBEAN = Caribbean +channel-type.pentair.lightMode.state.option.AMERICAN = American +channel-type.pentair.lightMode.state.option.SUNSET = Sunset +channel-type.pentair.lightMode.state.option.ROYAL = Royal +channel-type.pentair.lightMode.state.option.BLUE = Blue +channel-type.pentair.lightMode.state.option.GREEN = Green +channel-type.pentair.lightMode.state.option.RED = Red +channel-type.pentair.lightMode.state.option.WHITE = White +channel-type.pentair.lightMode.state.option.MAGENTA = Magenta +channel-type.pentair.lowFlow.label = Low Flow +channel-type.pentair.lowFlow.description = Water flow rate is low. +channel-type.pentair.lowSalt.label = Low Salt +channel-type.pentair.lowSalt.description = Low salt level. +channel-type.pentair.lowVoltage.label = Low Voltage +channel-type.pentair.lowVoltage.description = Chlorinator cell is at a low voltage. +channel-type.pentair.lowWaterTemp.label = Low Water Temperature +channel-type.pentair.lowWaterTemp.description = Water temperature is too low for chlorine generation. +channel-type.pentair.lsi.label = LSI +channel-type.pentair.lsi.description = Langelier Saturation Index. +channel-type.pentair.ok.label = Chlorinator OK +channel-type.pentair.ok.description = Chlorinator is operating correctly. +channel-type.pentair.orpDoserType.label = ORP Doser Type +channel-type.pentair.orpDoserType.description = The doser type for ORP (None, ORP). +channel-type.pentair.orpDoserType.state.option.NONE = None +channel-type.pentair.orpDoserType.state.option.ORP = ORP +channel-type.pentair.orpReading.label = ORP Reading +channel-type.pentair.orpReading.description = Current Oxidation Reduction Potential (ORP) reading. +channel-type.pentair.orpSetPoint.label = ORP Set Point +channel-type.pentair.orpSetPoint.description = Oxidation Reduction Potential (ORP) set point. +channel-type.pentair.phDoserType.label = PH Doser Type +channel-type.pentair.phDoserType.description = The doser type for PH (None, CO2, Acid). +channel-type.pentair.phDoserType.state.option.NONE = None +channel-type.pentair.phDoserType.state.option.CO2 = CO2 +channel-type.pentair.phDoserType.state.option.ACID = Acid +channel-type.pentair.phReading.label = PH Reading +channel-type.pentair.phReading.description = Current PH reading. +channel-type.pentair.phSetPoint.label = PH Set Point +channel-type.pentair.phSetPoint.description = Current PH set point. +channel-type.pentair.power.label = Power +channel-type.pentair.power.description = Pump power +channel-type.pentair.pumpStatus1.label = Pump Status 1 +channel-type.pentair.pumpStatus1.description = Pump Status 1 +channel-type.pentair.pumpStatus2.label = Pump Status 2 +channel-type.pentair.pumpStatus2.description = Pump Status 2 +channel-type.pentair.rpm.label = RPM +channel-type.pentair.rpm.description = Pump RPM +channel-type.pentair.run.label = Pump Running +channel-type.pentair.run.description = Indicator on whether the pump is running or not. +channel-type.pentair.runProgram.label = Run Program +channel-type.pentair.runProgram.description = Run program (0 to stop, # to run) +channel-type.pentair.salinity.label = Salinity (PPM) +channel-type.pentair.salinity.description = Current salt content reading of the water (PPM). +channel-type.pentair.saltLevel.label = Salt Level (PPM) +channel-type.pentair.saltLevel.description = Current salt content reading of the water (PPM). +channel-type.pentair.saltOutput.label = Salt Output (%) +channel-type.pentair.saltOutput.description = Current salt output setting for the chlorinator (%). +channel-type.pentair.scheduleCircuit.label = Circuit +channel-type.pentair.scheduleCircuit.description = The circuit number for the schedule. +channel-type.pentair.scheduleDays.label = Days +channel-type.pentair.scheduleDays.description = A string containing the days the schedule will run (SMTWRFY). +channel-type.pentair.scheduleEnd.label = End Time +channel-type.pentair.scheduleEnd.description = The end time (or duration for Egg Timer) of this schedule. +channel-type.pentair.scheduleStart.label = Start Time +channel-type.pentair.scheduleStart.description = The start time for this schedule. +channel-type.pentair.scheduleString.label = Schedule +channel-type.pentair.scheduleString.description = String format of schedule. +channel-type.pentair.scheduleType.label = Schedule Type +channel-type.pentair.scheduleType.description = Type of schedule (None, Normal, EggTimer, OnceOnly). +channel-type.pentair.scheduleType.state.option.NONE = None +channel-type.pentair.scheduleType.state.option.NORMAL = Normal +channel-type.pentair.scheduleType.state.option.EGGTIMER = Egg Timer +channel-type.pentair.scheduleType.state.option.ONCEONLY = Once Only +channel-type.pentair.serviceMode.label = Service Mode +channel-type.pentair.serviceMode.description = Controller is in service mode +channel-type.pentair.solarState.label = Solar Heater State +channel-type.pentair.solarState.description = The state of the solar heater (on, off) +channel-type.pentair.solarTemp.label = Solar Temperature +channel-type.pentair.solarTemp.description = The temperature of the solar sensor. +channel-type.pentair.tankLevel.label = Tank Level +channel-type.pentair.tankLevel.description = Tank level (1-7). +channel-type.pentair.temperature.label = Temperature +channel-type.pentair.temperature.description = Current temperature. +channel-type.pentair.veryLowSalt.label = Very Low Salt +channel-type.pentair.veryLowSalt.description = Very low salt level. +channel-type.pentair.warningChlorinatorCommError.label = Chlorinator Comm Error +channel-type.pentair.warningChlorinatorCommError.description = Error in communicating with the Chlorinator. +channel-type.pentair.warningInvalidSetup.label = Invalid Setup +channel-type.pentair.warningInvalidSetup.description = Invalid setup for the unit. +channel-type.pentair.warningOrpDailyLimitReached.label = ORP Daily Limit Reached +channel-type.pentair.warningOrpDailyLimitReached.description = Daily limit of ORP dosing has been reached. +channel-type.pentair.warningPhDailyLimitReached.label = PH Daily Limit Reached +channel-type.pentair.warningPhDailyLimitReached.description = Daily limit of PH dosing has been reached. +channel-type.pentair.warningPhLockout.label = PH Lockout Warning +channel-type.pentair.warningPhLockout.description = Unit is in PH Lockout. + +# binding + +binding.pentair.name = Pentair Binding +binding.pentair.description = This is the binding for Pentair pool systems. + +# thing types + +thing-type.pentair.easytouch.label = EasyTouch Controller +thing-type.pentair.easytouch.description = Pentair EasyTouch Controller thing-type.pentair.pentair_serial_bridge.label = Serial Bridge thing-type.pentair.pentair_serial_bridge.description = This bridge is used when using a USB->RS485 interface. @@ -22,14 +259,6 @@ thing-type.config.pentair.easytouch.id.label = ID thing-type.config.pentair.easytouch.id.description = The ID of the device (in decimal, not hex) thing-type.config.pentair.intellichlor.id.label = ID thing-type.config.pentair.intellichlor.id.description = The ID of the device (in decimal, not hex) -thing-type.config.pentair.intelliflo.id.label = ID -thing-type.config.pentair.intelliflo.id.description = The ID of the device (in decimal, not hex) -thing-type.config.pentair.ip_bridge.address.label = IP Address -thing-type.config.pentair.ip_bridge.address.description = The IP address to connect to. -thing-type.config.pentair.ip_bridge.id.label = Pentair ID -thing-type.config.pentair.ip_bridge.id.description = The ID to use to send commands on the Pentair bus (default: 34) -thing-type.config.pentair.ip_bridge.port.label = Port -thing-type.config.pentair.ip_bridge.port.description = The port to connect to. thing-type.config.pentair.pentair_serial_bridge.id.label = Pentair ID thing-type.config.pentair.pentair_serial_bridge.id.description = The ID to use to send commands on the Pentair bus (default: 34) thing-type.config.pentair.pentair_serial_bridge.serialPort.label = Serial Port @@ -57,20 +286,14 @@ channel-type.pentair.poolsetpoint.label = Pool Temperature Set Point channel-type.pentair.poolsetpoint.description = Pool temperature set point channel-type.pentair.pooltemp.label = Pool Water Temperature channel-type.pentair.pooltemp.description = Pool water temperature. Only valid when pool pump is running and in pool mode. -channel-type.pentair.power.label = Power -channel-type.pentair.power.description = Pump power channel-type.pentair.ppc.label = PPC channel-type.pentair.ppc.description = Pump PPC channel-type.pentair.pumperror.label = Pump Error channel-type.pentair.pumperror.description = Pump Error channel-type.pentair.pumpmode.label = Pump Mode channel-type.pentair.pumpmode.description = Pump mode -channel-type.pentair.rpm.label = RPM -channel-type.pentair.rpm.description = Pump RPM channel-type.pentair.runswitch.label = Pump Running channel-type.pentair.runswitch.description = Indicator on whether the pump is running or not. -channel-type.pentair.salinity.label = Salinity (PPM) -channel-type.pentair.salinity.description = Current salt content reading of the water (PPM). channel-type.pentair.saltoutput.label = Salt Output (%) channel-type.pentair.saltoutput.description = Current salt output setting for the chlorinator (%). channel-type.pentair.solartemp.label = Solar Temperature @@ -79,3 +302,18 @@ channel-type.pentair.spasetpoint.label = Spa Temperature Set Point channel-type.pentair.spasetpoint.description = Spa temperature set point channel-type.pentair.spatemp.label = Spa Water Temperature channel-type.pentair.spatemp.description = Spa water temperature. Only valide when in spa mode. + +# offline configuration errors + +offline.configuration-error.bridge-missing = The Pentair bridge is missing. +offline.configuration-error.bridge-duplicate = A Pentair bridge is already setup. This binding will only support a single bridge. +offline.configuration-error.duplicate-id = A Pentair thing has already been created with this id. +offline.configuration-error.duplicate-controller = A Pentair controller has already been created. Only a single controller is supported. +offline.configuration-error.duplicate-intllichlor = A Pentair IntelliChlor device has already been created. Only a single IntelliChlor is supported. +offline.communication-error.iostream-error = A write or read iostream was unable to be created. +offline.communication-error.ip-stream-error = An error occurred in accessing the ip port. +offline.configuration-error.serial-port-empty = There is no configured serial port. +offline.communication-error.serial-port-busy = The serial port is being used by another application: +offline.communication-error.serial-port-not-found = Serial port does not exist: +offline.communication-error.serial-port-open-failed = Serial port cannot be opened: +offline.communication-error.serial-port-error = An exception occurred in accessing the serial port: diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/controller.xml b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/controller.xml new file mode 100644 index 00000000000..6e7440c78e9 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/controller.xml @@ -0,0 +1,350 @@ + + + + + + + + + + + A Pentair Controller such as EasyTouch or IntelliTouch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Firmware Version + + + id + + + + + The ID of the device (in decimal, not hex) + 16 + + + + + Enables automatic synchronization of the pool controller clock with the system clock + true + + + + + + + Circuit + + + + + + + + + + Features + + + + + + + + + + Heat + + + + + + + + + + schedule + + + + + + + + + + + + + General status channels for controller + + + + + + + + + + + + + + Number:Temperature + + The temperature of the water. Only valid when pool pump is running. + + + + + Number:Temperature + + The temperature of the solar sensor. + + + + + Number:Temperature + + The temperature of the air. + + + + + Switch + + The on/off control for this circuit. + + + + String + + The name of this circuit. + + + + + String + + The function this circuit controls. + + + + + String + + The current head mode (None, Heater, Solar Preferred, Solar). + + + + + + + + + + + + Number:Temperature + + The set point temperature for this mode. + + + + + String + + The current light mode. + + + + + + + + + + + + + + + + + + + + + recommend + + + + String + + Type of schedule (None, Normal, EggTimer, OnceOnly). + + + + + + + + + + + + String + + String format of schedule. + + + + Number:Time + + The start time for this schedule. + + + + + Number:Time + + The end time (or duration for Egg Timer) of this schedule. + + + + + Number + + The circuit number for the schedule. + + + + String + + A string containing the days the schedule will run (SMTWRFY). + + + + Switch + + Controller is in service mode + + + + + Switch + + Pump is continuing to run to allow the heater to cool. + + + + Switch + + The state of the solar heater (on, off) + + + + + Switch + + The state of the heater (on, off) + + + + diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/easytouch.xml b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/easytouch.xml deleted file mode 100644 index 37db93dac74..00000000000 --- a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/easytouch.xml +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - - - - - Pentair EasyTouch Controller - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The ID of the device (in decimal, not hex) - 16 - - - - - - Number - - Pool water temperature. Only valid when pool pump is running and in pool mode. - - - - Number - - Spa water temperature. Only valide when in spa mode. - - - - Number - - Air temperature. - - - - Number - - Solar temperature. - - - - Switch - - Auxillary Switch - - - - Switch - - Feature Switch - - - - Number - - Heat mode - - - - - - - - - - - - Number - - Heat active state - - - - - Number - - Pool temperature set point - - - - Number - - Spa temperature set point - - - - String - - Heat mode string - - - - diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intellichem.xml b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intellichem.xml new file mode 100644 index 00000000000..db22104883c --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intellichem.xml @@ -0,0 +1,258 @@ + + + + + + + + + + + A Pentair Intellichem controller. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Firmware Version + + + id + + + + + The ID of the device (in decimal, not hex) + 144 + + + + + + Number + + Current PH reading. + + + + + Number + + Current Oxidation Reduction Potential (ORP) reading. + + + + + Number + + Current PH set point. + + + + + Number + + Oxidation Reduction Potential (ORP) set point. + + + + + Number + + Tank level (1-7). + + + + + Number:Dimensionless + + Calcium hardness (ppm). + + + + + Number:Dimensionless + + Cyanuric acid reading (ppm). + + + + + Number:Dimensionless + + Total Alkalinity reading (ppm). + + + + + String + + The doser type for PH (None, CO2, Acid). + + + + + + + + + + + String + + The doser type for ORP (None, ORP). + + + + + + + + + + Switch + + Whether the chemical is currently dosing. + + + + + Number:Time + + The time a particular chemical has been dosing. + + + + Number + + Langelier Saturation Index. + + + + + Number:Dimensionless + + Current salt content reading of the water (PPM). + + + + + Number:Temperature + + Current temperature. + + + + + Switch + + Water flow alarm (on = no water flow). + + + + + Switch + + PH alarm reported. + + + + + Switch + + ORP alarm reported. + + + + + Switch + + PH tank alarm reported. + + + + + Switch + + ORP tank alarm reported. + + + + + Switch + + Probe fault alarm reported. + + + + + Switch + + Unit is in PH Lockout. + + + + + Switch + + Daily limit of PH dosing has been reached. + + + + + Switch + + Daily limit of ORP dosing has been reached. + + + + + Switch + + Invalid setup for the unit. + + + + + Switch + + Error in communicating with the Chlorinator. + + + + diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intellichlor.xml b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intellichlor.xml index a5f133af04b..b6023e9a87c 100644 --- a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intellichlor.xml +++ b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intellichlor.xml @@ -7,38 +7,101 @@ - + Pentair Intellichlor IC40 - + + + + + + + + + - - - - The ID of the device (in decimal, not hex) - 96 - - + + Version + Model + + + id - - Number + + Number:Dimensionless Current salt output setting for the chlorinator (%). - Number + Number:Dimensionless Current salt content reading of the water (PPM). + + Switch + + Chlorinator is operating correctly. + + + + + Switch + + Water flow rate is low. + + + + + Switch + + Low salt level. + + + + + Switch + + Very low salt level. + + + + + Switch + + Chlorinator drawing high current. + + + + + Switch + + Clean chlorinator cell. + + + + + Switch + + Chlorinator cell is at a low voltage. + + + + + Switch + + Water temperature is too low for chlorine generation. + + + diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intelliflo.xml b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intelliflo.xml index f8968beeb6e..59dc2364573 100644 --- a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intelliflo.xml +++ b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/intelliflo.xml @@ -7,23 +7,26 @@ - + - - Pentair Intelliflo Pump + + A Pentair Intelliflo pump - - + + - - + + + + id + - + The ID of the device (in decimal, not hex) 96 @@ -31,14 +34,7 @@ - - Number - - Pump mode - - - - + Switch Indicator on whether the pump is running or not. @@ -49,28 +45,42 @@ Number Pump RPM - + + + + + Number:VolumetricFlowRate + + Pump GPM (only valid for VF pumps) + - Number + Number:Power Pump power + + + + + Number + + Pump Status 1 - + Number - - Pump PPC + + Pump Status 2 - + Number - - Pump Error - + + Run program (0 to stop, # to run) + diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/ip_bridge.xml b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/ip_bridge.xml index 5881cc471a3..4faf9b1e6f5 100644 --- a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/ip_bridge.xml +++ b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/ip_bridge.xml @@ -6,25 +6,32 @@ - This bridge is for use over a network interface. + This bridge is for used over a network interface. - The IP address to connect to. + The IP address of the network interface. network-address + 127.0.0.1 - The port to connect to. + The port used to connect to the network interface. 10000 - The ID to use to send commands on the Pentair bus (default: 34) + The ID to use when sending commands on the Pentair bus (default: 34) 34 + + + + Enable automatic discovery of devices + true + diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/serial_bridge.xml b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/serial_bridge.xml index 40b04c7f553..6d89d27ed4a 100644 --- a/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/serial_bridge.xml +++ b/bundles/org.openhab.binding.pentair/src/main/resources/OH-INF/thing/serial_bridge.xml @@ -4,9 +4,9 @@ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"> - - - This bridge is used when using a USB->RS485 interface. + + + This bridge should be configured when using a USB->RS485 interface. @@ -18,6 +18,12 @@ The ID to use to send commands on the Pentair bus (default: 34) 34 + + + + Enable automatic discovery of devices + true + diff --git a/bundles/org.openhab.binding.pentair/src/test/data/easytouch8.dat b/bundles/org.openhab.binding.pentair/src/test/data/easytouch8.dat new file mode 100644 index 00000000000..c0084d92fb8 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/data/easytouch8.dat @@ -0,0 +1,740 @@ +ffffffffffffffff00ffa5240f10021d083a000100000000002000000004 +4a4a0000440000000400007ce6000d03b9ff00ffa50060100401ff0219ff +00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa5001060 +06010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202 +ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202005c02ee +000000000001151f02baffffffffffffffff00ffa5240f10021d083a0001 +000000000020000000044a4a0000440000000400007ce6000d03b9ffffff +ffffffffff00ffa5240f10021d083a0001000000000020000000044a4a00 +00440000000400007ce6000d03b9ff00ffa50060100401ff0219ff00ffa5 +0010600401ff0219ff00ffa500601006010a0126ff00ffa500106006010a +0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208 +ffffffffffffffff00ffa5240f10080d4a4a444e5e040000000000000002 +85ff00ffa50060100700011cff00ffa5001060070f0a0202005a02ee0000 +00000001151f02b810025014007610031002000300496e74656c6c696368 +6c6f722d2d3430bc1003ffffffffffffffff00ffa5240f10021d083b0001 +000000000020000000044a4a0000440000000400007ce6000d03baff00ff +a50060100401ff0219ff00ffa50010600401ff0219ff00ffa50060100401 +ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ff +a500106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa50010 +60010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202 +005c02ee000000000001151f02baffffffffffffffff00ffa5240f10021d +083b0001000000000020000000044a4a0000440000000400007ce6000d03 +baffffffffffffffff00ffa5240f10021d083b0001000000000020000000 +044a4a0000440000000400007ce6000d03baff00ffa50060100401ff0219 +ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa50010 +6006010a0126ff00ffa5006010010402c402ee02d0ff00ffa50010600102 +02ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202005a02 +ee000000000001151f02b8ffffffffffffffff00ffa5240f10021d083b00 +01000000000020000000044a4a0000440000000400007ce6000d03baffff +ffffffffffff00ffa5240f10021d083b0001000000000020000000044a4a +0000440000000400007ce6000d03baff00ffa50060100401ff0219ff00ff +a50010600401ff0219ff00ffa500601006010a0126ff00ffa50010600601 +0a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee02 +08ff00ffa50060100700011cff00ffa5001060070f0a0202005b02ee0000 +00000001151f02b9ffffffffffffffff00ffa5240f10021d083b00010000 +00000020000000044a4a0000440000000400007ce6000d03baffffffffff +ffffff00ffa5240f10021d083b0001000000000020000000044a4a000044 +0000000400007ce6000d03baff00ffa50060100401ff0219ff00ffa50010 +600401ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126 +ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208ff00 +ffa50060100700011cff00ffa5001060070f0a0202005b02ee0000000000 +01151f02b9ffffffffffffffff00ffa5240f10021d083b00010000000000 +20000000044a4a0000440000000400007ce6000d03baffffffffffffffff +00ffa5240f10021d083b0001000000000020000000044a4a000044000000 +0400007ce6000d03baff00ffa50060100401ff0219ff00ffa50010600401 +ff0219ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ff +a500601006010a0126ff00ffa500106006010a0126ff00ffa50060100104 +02c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700011c +ff00ffa5001060070f0a0202005c02ee000000000001151f02baffffffff +ffffffff00ffa5240f10021d083b0001000000000020000000044a4a0000 +440000000400007ce6000d03baffffffffffffffff00ffa5240f10021d08 +3b0001000000000020000000044a4a0000440000000400007ce6000d03ba +ffffffffffffffff00ffa5240f10021d083b000100000000002000000004 +4a4a0000440000000400007ce6000d03baffffffffffffffff00ffa5240f +10021d083b0001000000000020000000044a4a0000440000000400007ce6 +000d03baffffffffffffffff00ffa5240f10021d083b0001000000000020 +000000044a4a0000440000000400007ce6000d03baffffffffffffffff00 +ffa5240f10021d083b0001000000000020000000044a4a00004400000004 +00007ce6000d03baffffffffffffffff00ffa5240f10021d083b00010000 +00000020000000044a4a0000440000000400007ce6000d03ba1002501100 +731003100200124c81f11003ffffffffffffff +ff00ffa5240f10021d083b0001000000000020000000044a4a0000440000000400007ce6000d03baff +00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa5006010 +06010a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee +02d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa5 +001060070f0a0202005b02ee000000000001151f02b9ffffffffffffffff +00ffa5240f10021d083b0001000000000020000000044a4a000044000000 +0400007ce6000d03baffffffffffffffff00ffa5240f10021d083b000100 +0000000020000000044a4a0000440000000400007ce6000d03baff00ffa5 +0060100401ff0219ff00ffa50010600401ff0219ff00ffa50060100401ff +0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa5 +00106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060 +010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a020200 +5c02ee000000000001151f02baffffffffffffffff00ffa5240f10021d08 +3b0001000000000020000000044a4a0000440000000400007ce6000d03ba +ffffffffffffffff00ffa5240f10021d083b000100000000002000000004 +4a4a0000440000000400007ce6000d03baff00ffa50060100401ff0219ff +00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa5001060 +06010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202 +ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202005b02ee +000000000001152002baffffffffffffffff00ffa5240f10021d083b0001 +000000000020000000044a4a0000440000000400007ce6000d03baffffff +ffffffffff00ffa5240f10021d083b0001000000000020000000044a4a00 +00440000000400007ce6000d03baff00ffa50060100401ff0219ff00ffa5 +0010600401ff0219ff00ffa500601006010a0126ff00ffa500106006010a +0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208 +ff00ffa50060100700011cff00ffa5001060070f0a0202005b02ee000000 +000001152002baffffffffffffffff00ffa5240f10021d083b0001000000 +000020000000044a4a0000440000000400007ce6000d03baffffffffffff +ffff00ffa5240f10021d083b0001000000000020000000044a4a00004400 +00000400007ce6000d03baff00ffa50060100401ff0219ff00ffa5001060 +0401ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126ff +00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208ff00ff +a50060100700011cff00ffa5001060070f0a0202005c02ee000000000001 +152002bb1002501100731003100200124c81f11003ffffffffffffffff00 +ffa5240f10021d083b0001000000000020000000044a4a00004400000004 +00007ce6000d03baff00ffa50060100401ff0219ff00ffa50010600401ff +0219ff00ffa500601006010a0126ff00ffa500106006010a0126ff00ffa5 +006010010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060 +100700011cff00ffa5001060070f0a0202005b02ee000000000001152002 +baffffffffffffffff00ffa5240f10021d083b0001000000000020000000 +044a4a0000440000000400007ce6000d03baffffffffffffffff00ffa524 +0f10021d083b0001000000000020000000044a4a0000440000000400007c +e6000d03baff00ffa50060100401ff0219ff00ffa50010600401ff0219ff +00ffa500601006010a0126ff00ffa500106006010a0126ff00ffa5006010 +010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700 +011cff00ffa5001060070f0a0202005c02ee000000000001152002bbffff +ffffffffffff00ffa5240f10021d083b0001000000000020000000044a4a +0000440000000400007ce6000d03baffffffffffffffff00ffa5240f1002 +1d083b0001000000000020000000044a4a0000440000000400007ce6000d +03baff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa5 +0060100401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a +0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff +00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa5001060 +070f0a0202005902ee000000000001152002b8ffffffffffffffff00ffa5 +240f10021d083b0001000000000020000000044a4a000044000000040000 +7ce6000d03baffffffffffffffff00ffa5240f10021d083b000100000000 +0020000000044a4a0000440000000400007ce6000d03baff00ffa5006010 +0401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff +00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa5 +001060010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a +0202005b02ee000000000001152002baffffffffffffffff00ffa5240f10 +021d083b0001000000000020000000044a4a0000440000000400007ce600 +0d03baffffffffffffffff00ffa5240f1005080900041e06140000013aff +ffffffffffffff00ffa5240f10021d09000001000000000020000000044a +4a0000440000000400007ce6000d0380ff00ffa50060100401ff0219ff00 +ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa500106006 +010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee +0208ff00ffa50060100700011cff00ffa5001060070f0a0202005a02ee00 +0000000001152002b91002500000621003100200010000131003ffffffff +ffffffff00ffa5240f10021d09000001000000000020000000044a4a0000 +440000000400007ce6000d0380ff00ffa50060100401ff0219ff00ffa500 +10600401ff0219ffff00ffa50060100401ff0219ff00ffa50010600401ff +0219ff00ffa500601006010a0126ff00ffa500106006010a0126ff00ffa5 +006010010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060 +100700011cff00ffa5001060070f0a0202005c02ee000000000001152002 +bbffffffffffffffff00ffa5240f10021d09000001000000000020000000 +044a4a0000440000000400007ce6000d0380ffffffffffffffff00ffa524 +0f10021d09000001000000000020000000044a4a0000440000000400007c +e6000d0380ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff +00ffa500601006010a0126ff00ffa500106006010a0126ff00ffa5006010 +010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700 +011cff00ffa5001060070f0a0202005c02ee000000000001152002bbffff +ffffffffffff00ffa5240f10021d09000001000000000020000000044a4a +0000440000000400007ce6000d0380ffffffffffffffff00ffa5240f1002 +1d09000001000000000020000000044a4a0000440000000400007ce6000d +0380ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa5 +00601006010a0126ff00ffa500106006010a0126ff00ffa5006010010402 +c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff +00ffa5001060070f0a0202005b02ee000000000001152002baffffffffff +ffffff00ffa5240f10021d09000001000000000020000000044a4a000044 +0000000400007ce6000d0380ffffffffffffffff00ffa5240f10021d0900 +0001000000000020000000044a4a0000440000000400007ce6000d0380ff +00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa5006010 +06010a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee +02d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa5 +001060070f0a0202005b02ee000000000001152002baffffffffffffffff +00ffa5240f10021d09000001000000000020000000044a4a000044000000 +0400007ce6000d0380ffffffffffffffff00ffa5240f10021d0900000100 +0000000020000000044a4a0000440000000400007ce6000d0380ff00ffa5 +0060100401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a +0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff +00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa5001060 +070f0a0202005c02ee000000000001152002bbffffffffffffffff00ffa5 +240f10021d09000001000000000020000000044a4a000044000000040000 +7ce6000d0380ffffffffffffffff00ffa5240f10021d0900000100000000 +0020000000044a4a0000440000000400007ce6000d0380ffffffffffffff +ff00ffa5240f10021d09000001000000000020000000044a4a0000440000 +000400007ce6000d0380ffffffffffffffff00ffa5240f10021d09000001 +000000000020000000044a4a0000440000000400007ce6000d0380ffffff +ffffffffff00ffa5240f10021d09000001000000000020000000044a4a00 +00440000000400007ce6000d0380ffffffffffffffff00ffa5240f10021d +09000001000000000020000000044a4a0000440000000400007ce6000d03 +80ffffffffffffffff00ffa5240f10021d09000001000000000020000000 +044a4a0000440000000400007ce6000d0380100250110073100310020012 +4c81f11003ffffffffffffffff00ffa5240f10021d090000010000000000 +20000000044a4a0000440000000400007ce6000d0380ff00ffa500601004 +01ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00 +ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa500 +1060010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a02 +02005a02ee000000000001152002b9ffffffffffffffff00ffa5240f1002 +1d09000001000000000020000000044a4a0000440000000400007ce6000d +0380ffffffffffffffff00ffa5240f10021d090000010000000000200000 +00044a4a0000440000000400007ce6000d0380ff00ffa50060100401ff02 +19ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa500 +106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa500106001 +0202ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202005a +02ee000000000001152102baffffffffffffffff00ffa5240f10021d0900 +0001000000000020000000044a4a0000440000000400007ce6000d0380ff +ffffffffffffff00ffa5240f10021d09000001000000000020000000044a +4a0000440000000400007ce6000d0380ff00ffa50060100401ff0219ff00 +ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa500106006 +010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee +0208ff00ffa50060100700011cff00ffa5001060070f0a0202005b02ee00 +0000000001152102bbffffffffffffffff00ffa5240f10021d0900000100 +0000000020000000044a4a0000440000000400007ce6000d0380ffffffff +ffffffff00ffa5240f10fc110002500000010a0000000000000000000002 +52ffffffffffffffff00ffa5240f10080d4a4a444e5e0400000000000000 +0285ffffffffffffffff00ffa5240f1005080900041e06140000013affff +ffffffffffff00ffa5240f100b0501014800000142ffffffffffffffff00 +ffa5240f100b050200110000010bffffffffffffffff00ffa5240f100b05 +03005600000151ffffffffffffffff00ffa5240f100b0504104a00000156 +ffffffffffffffff00ffa5240f100b0505103f0000014cffffffffffffff +ff00ffa5240f100b0506023d0000013dffffffffffffffff00ffa5240f10 +0b0507000700000106ffffffffffffffff00ffa5240f100b050800080000 +0108ffffffffffffffff00ffa5240f100b0509003000000131ffffffffff +ffffff00ffa5240f100b050a000000000102ffffffffffffffff00ffa524 +0f100b050b05160000011effffffffffffffff00ffa5240f100b050c005f +00000163ffffffffffffffff00ffa5240f100b050d006000000165ffffff +ffffffffff00ffa5240f100b050e006100000167ffffffffffffffff00ff +a5240f100b050f006200000169ffffffffffffffff00ffa5240f100b0510 +00630000016bffffffffffffffff00ffa5240f100b051100640000016dff +ffffffffffffff00ffa5240f100b05120fc8000001e1ffffffffffffffff +00ffa5240f1011070106010006007f018dffffffffffffffff00ffa5240f +101107020907010e007f01a0ffffffffffffffff00ffa5240f1011070309 +110113007f01b0ffffffffffffffff00ffa5240f1011070409160100007f +01a3ffffffffffffffff00ffa5240f10110705030c000c287f01c7ffffff +ffffffffff00ffa5240f101107060000000000000106ffffffffffffffff +00ffa5240f101107070b0f0011007f01b1ffffffffffffffff00ffa5240f +101107080000000000000108ffffffffffffffff00ffa5240f1011070909 +080011007f01aaffffffffffffffff00ffa5240f10272005040200041402 +0000000a0000000a0000000a0000000a0000000a0000000a000190ffffff +ffffffffff00ffa5240f101d18030000001200ffffff0101020304050607 +08090a010203040471ffffffffffffffff00ffa5240f10fc110002500000 +010a000000000000000000000252ffffffffffffff +ff00ffa5240f10021d09000001000000000020000000044a4a0000440000000400007ce6000d0380 +ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa500 +601006010a0126ff00ffa500106006010a0126ff00ffa5006010010402c4 +02ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff00 +ffa5001060070f0a0202005b02ee000000000001152102bbffffffffffff +ffff00ffa5240f10021d09000001000000000020000000044a4a00004400 +00000400007ce6000d0380ffffffffffffffff00ffa5240f10021d090000 +01000000000020000000044a4a0000440000000400007ce6000d0380ff00 +ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa500601006 +010a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02 +d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa500 +1060070f0a0202005b02ee000000000001152102bb100250110073100310 +0200124c81f11003ff00ffa50021600401ff022aff00ffa5002160070f0a +0202005d02ee000000000001152102ceffffffffffffffff00ffa5240f10 +021d09000001000000000020000000044a4a0000440000000400007ce600 +0d0380ff00ffa50060100401ff0219ff00ffa50010600401ff0219ffff00 +ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa500601006 +010a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02 +d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa500 +1060070f0a0202005c02ee000000000001152102bcffffffffffffffff00 +ffa5240f10021d09000001000000000020000000044a4a00004400000004 +00007ce6000d0380ffffffffffffffff00ffa5240f10021d090000010000 +00000020000000044a4a0000440000000400007ce6000d0380ff00ffa500 +60100401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a01 +26ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff00 +ffa5001060010202ee0208ff00ffa50060100700011cff00ffa500106007 +0f0a0202005b02ee000000000001152102bbffffffffffffffff00ffa524 +0f10021d09000001000000000020000000044a4a0000440000000400007c +e6000d0380ffffffffffffffff00ffa5240f10021d090000010000000000 +20000000044a4a0000440000000400007ce6000d0380ff00ffa500601004 +01ff0219ff00ffa50010600401ff0219ffff00ffa50060100401ff0219ff +00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa5001060 +06010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202 +ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202005b02ee +000000000001152102bbffffffffffffffff00ffa5240f10021d09000001 +000000000020000000044a4a0000440000000400007ce6000d0380ffffff +ffffffffff00ffa5240f10021d09000001000000000020000000044a4a00 +00440000000400007ce6000d0380ff00ffa50060100401ff0219ff00ffa5 +0010600401ff0219ff00ffa500601006010a0126ff00ffa500106006010a +0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208 +ff00ffa50060100700011cff00ffa5001060070f0a0202005c02ee000000 +000001152102bcffffffffffffffff00ffa5240f10021d09000001000000 +000020000000044a4a0000440000000400007ce6000d0380ffffffffffff +ffff00ffa5240f10080d4a4a444e5e04000000000000000285ffffffffff +ffffff00ffa5240f10021d09010001000000000020000000044a4a000044 +0000000400007ce6000d0381ff00ffa50060100401ff0219ff00ffa50010 +600401ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126 +ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208ff00 +ffa50060100700011cff00ffa5001060070f0a0202005b02ee0000000000 +01152102bb10025014007610031002000300496e74656c6c6963686c6f72 +2d2d3430bc1003ffffffffffffffff00ffa5240f10021d09010001000000 +000020000000044a4a0000440000000400007ce6000d0381ff00ffa50060 +100401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126 +ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff00ff +a5001060010202ee0208ff00ffa50060100700011cff00ffa5001060070f +0a0202005a02ee000000000001152102baffffffffffffffff00ffa5240f +10021d09010001000000000020000000044a4a0000440000000400007ce6 +000d0381ffffffffffffffff00ffa5240f10021d09010001000000000020 +000000044a4a0000440000000400007ce6000d0381ff00ffa50060100401 +ff0219ff00ffa50010600401ff0219ffff00ffa50060100401ff0219ff00 +ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa500106006 +010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee +0208ff00ffa50060100700011cff00ffa5001060070f0a0202005c02ee00 +0000000001152102bcffffffffffffffff00ffa5240f10021d0901000100 +0000000020000000044a4a0000440000000400007ce6000d0381ffffffff +ffffffff00ffa5240f10021d09010001000000000020000000044a4a0000 +440000000400007ce6000d0381ff00ffa50021600401ff022aff00ffa500 +2160070f0a0202005b02ee000000000001152102ccff00ffa50060100401 +ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ff +a500106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa50010 +60010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202 +005b02ee000000000001152102bbffffffffffffffff00ffa5240f10021d +09010001000000000020000000044a4a0000440000000400007ce6000d03 +81ffffffffffffffff00ffa5240f10021d09010001000000000020000000 +044a4a0000440000000400007ce6000d0381ff00ffa50060100401ff0219 +ff00ffa50010600401ff0219ffff00ffa50060100401ff0219ff00ffa500 +10600401ff0219ff00ffa500601006010a0126ff00ffa500106006010a01 +26ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208ff +00ffa50060100700011cff00ffa5001060070f0a0202005b02ee00000000 +0001152102bbffffffffffffffff00ffa5240f10021d0901000100000000 +0020000000044a4a0000440000000400007ce6000d0381ffffffffffffff +ff00ffa5240f10021d09010001000000000020000000044a4a0000440000 +000400007ce6000d0381ff00ffa50060100401ff0219ff00ffa500106004 +01ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126ff00 +ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa5 +0060100700011cff00ffa5001060070f0a0202005b02ee00000000000115 +2102bbffffffffffffffff00ffa5240f10021d0901000100000000002000 +0000044a4a0000440000000400007ce6000d0381ffffffffffffffff00ff +a5240f10021d09010001000000000020000000044a4a0000440000000400 +007ce6000d0381ffffffffffffffff00ffa5240f10021d09010001000000 +000020000000044a4a0000440000000400007ce6000d0381ffffffffffff +ffff00ffa5240f10021d09010001000000000020000000044a4a00004400 +00000400007ce6000d0381ffffffffffffffff00ffa5240f10021d090100 +01000000000020000000044a4a0000440000000400007ce6000d0381ffff +ffffffffffff00ffa5240f10021d09010001000000000020000000044a4a +0000440000000400007ce6000d0381ffffffffffffffff00ffa5240f1002 +1d09010001000000000020000000044a4a0000440000000400007ce6000d +03811002501100731003100200124c81f11003ffffffffffffffff00ffa5 +240f10021d09010001000000000020000000044a4a000044000000040000 +7ce6000d0381ff00ffa50060100401ff0219ff00ffa50010600401ff0219 +ff00ffa500601006010a0126ff00ffa500106006010a0126ff00ffa50060 +10010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa500601007 +00011cff00ffa5001060070f0a0202005b02ee000000000001152102bbff +ffffffffffffff00ffa5240f10021d09010001000000000020000000044a +4a0000440000000400007ce6000d0381ffffffffffffffff00ffa5240f10 +021d09010001000000000020000000044a4a0000440000000400007ce600 +0d0381ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ff +a500601006010a0126ff00ffa500106006010a0126ff00ffa50060100104 +02c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700011c +ff00ffa5001060070f0a0202005a02ee000000000001152202bbffffffff +ffffffff00ffa5240f10021d09010001000000000020000000044a4a0000 +440000000400007ce6000d0381ffffffffffffffff00ffa5240f10021d09 +010001000000000020000000044a4a0000440000000400007ce6000d0381 +ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa50060 +1006010a0126ff00ffa500106006010a0126ff00ffa5006010010402c402 +ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ff +a5001060070f0a0202005b02ee000000000001152202bcffffffffffffff +ff00ffa5240f10021d09010001000000000020000000044a4a0000440000 +000400007ce6000d0381ffffffffffffffff00ffa5240f10021d09010001 +000000000020000000044a4a0000440000000400007ce6000d0381ff00ff +a50060100401ff0219ff00ffa50010600401ff0219ff00ffa50060100601 +0a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0 +ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa50010 +60070f0a0202005c02ee000000000001152202bdffffffffffffffff00ff +a5240f10021d09010001000000000020000000044a4a0000440000000400 +007ce6000d0381ffffffffffffffff00ffa5240f10021d09010001000000 +000020000000044a4a0000440000000400007ce6000d0381ff00ffa50060 +100401ff0219ff00ffa50010600401ff0219ff00ffa50060100401ff0219 +ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa50010 +6006010a0126ff00ffa5006010010402c402ee02d0ff00ffa50010600102 +02ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202005c02 +ee000000000001152202bdff00ffa50021600401ff022aff00ffa5002160 +070f0a0202005c02ee000000000001152202ce1002501100731003100200 +124c81f11003ffffffffffffffff00ffa5240f10021d0901000100000000 +0020000000044a4a0000440000000400007ce6000d0381ff00ffa5006010 +0401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff +00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa5 +001060010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a +0202005b02ee000000000001152202bcffffffffffffffff00ffa5240f10 +021d09010001000000000020000000044a4a0000440000000400007ce600 +0d0381ffffffffffffffff00ffa5240f10021d0901000100000000002000 +0000044a4a0000440000000400007ce6000d0381ff00ffa50060100401ff +0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa5 +00106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060 +010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a020200 +5a02ee000000000001152202bbffffffffffffffff00ffa5240f10021d09 +010001000000000020000000044a4a0000440000000400007ce6000d0381 +ffffffffffffffff00ffa5240f10021d0901000100000000002000000004 +4a4a0000440000000400007ce6000d0381ff00ffa50060100401ff0219ff +00ffa50010600401ff0219ffff00ffa50060100401ff0219ff00ffa50010 +600401ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126 +ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208ff00 +ffa50060100700011cff00ffa5001060070f0a0202005b02ee0000000000 +01152202bcffffffffffffffff00ffa5240f10021d090100010000000000 +20000000044a4a0000440000000400007ce6000d0381ffffffffffffffff +00ffa5240f10021d09010001000000000020000000044a4a000044000000 +0400007ce6000d0381ff00ffa50060100401ff0219ff00ffa50010600401 +ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126ff00ff +a5006010010402c402ee02d0ff00ffa5001060010202ee0208ffffffffff +ffffff00ffa5240f1005080902041e06140000013cff00ffa50060100700 +011cff00ffa5001060070f0a0202005b02ee000000000001152202bcffff +ffffffffffff00ffa5240f10021d09020001000000000020000000044a4a +0000440000000400007ce6000d0382ffffffffffffffff00ffa5240f1002 +1d09020001000000000020000000044a4a0000440000000400007ce6000d +0382ff00ffa50060100401ff0219ff00ffa50010600401ff0219ffff00ff +a50060100401ff0219ff00ffa50010600401ff0219ff00ffa50060100601 +0a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0 +ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa50010 +60070f0a0202005c02ee000000000001152202bd10025000006210031002 +00010000131003ffffffffffffffff00ffa5240f10021d09020001000000 +000020000000044a4a0000440000000400007ce6000d0382ff00ffa50060 +100401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126 +ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff00ff +a5001060010202ee0208ff00ffa50060100700011cff00ffa5001060070f +0a0202005b02ee000000000001152202bcffffffffffffffff00ffa5240f +10021d09020001000000000020000000044a4a0000440000000400007ce6 +000d0382ffffffffffffffff00ffa5240f10021d09020001000000000020 +000000044a4a0000440000000400007ce6000d0382ff00ffa50060100401 +ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ff +a500106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa50010 +60010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202 +005a02ee000000000001152202bbffffffffffffffff00ffa5240f10021d +09020001000000000020000000044a4a0000440000000400007ce6000d03 +82ff00ffa50021600401ff022aff00ffa5002160070f0a0202005b02ee00 +0000000001152202cdffffffffffffffff00ffa5240f10021d0902000100 +0000000020000000044a4a0000440000000400007ce6000d0382ff00ffa5 +0060100401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a +0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff +00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa5001060 +070f0a0202005c02ee000000000001152202bdffffffffffffffff00ffa5 +240f10021d09020001000000000020000000044a4a000044000000040000 +7ce6000d0382ffffffffffffffff00ffa5240f10021d0902000100000000 +0020000000044a4a0000440000000400007ce6000d0382ff00ffa5006010 +0401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff +00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa5 +001060010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a +0202005902ee000000000001152202baffffffffffffffff00ffa5240f10 +021d09020001000000000020000000044a4a0000440000000400007ce600 +0d0382ffffffffffffffff00ffa5240f10021d0902000100000000002000 +0000044a4a0000440000000400007ce6000d0382ff00ffa50060100401ff +0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa5 +00106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060 +010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a020200 +5d02ee000000000001152202beffffffffffffffff00ffa5240f10021d09 +020001000000000020000000044a4a0000440000000400007ce6000d0382 +ffffffffffffffff00ffa5240f10021d0902000100000000002000000004 +4a4a0000440000000400007ce6000d0382ffffffffffffffff00ffa5240f +10021d09020001000000000020000000044a4a0000440000000400007ce6 +000d0382ffffffffffffffff00ffa5240f10021d09020001000000000020 +000000044a4a0000440000000400007ce6000d0382ffffffffffffffff00 +ffa5240f10021d09020001000000000020000000044a4a00004400000004 +00007ce6000d0382ffffffffffffffff00ffa5240f10021d090200010000 +00000020000000044a4a0000440000000400007ce6000d0382ffffffffff +ffffff00ffa5240f10021d09020001000000000020000000044a4a000044 +0000000400007ce6000d03821002501100731003100200124c81f11003ff +ffffffffffffff00ffa5240f10021d09020001000000000020000000044a +4a0000440000000400007ce6000d0382ff00ffa50060100401ff0219ff00 +ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa500106006 +010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee +0208ff00ffa50060100700011cff00ffa5001060070f0a0202005b02ee00 +0000000001152202bcffffffffffffffff00ffa5240f10021d0902000100 +0000000020000000044a4a0000440000000400007ce6000d0382ffffffff +ffffffff00ffa5240f10021d09020001000000000020000000044a4a0000 +440000000400007ce6000d0382ff00ffa50060100401ff0219ff00ffa500 +10600401ff0219ff00ffa50060100401ff0219ff00ffa50010600401ff02 +19ff00ffa500601006010a0126ff00ffa500106006010a0126ff00ffa500 +6010010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa5006010 +0700011cff00ffa5001060070f0a0202005b02ee000000000001152302bd +ffffffffffffffff00ffa5240f10021d0902000100000000002000000004 +4a4a0000440000000400007ce6000d0382ffffffffffffffff00ffa5240f +10021d09020001000000000020000000044a4a0000440000000400007ce6 +000d0382ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00 +ffa500601006010a0126ff00ffa500106006010a0126ff00ffa500601001 +0402c402ee02d0ff00ffa5001060010202ee0208ff00ffa5006010070001 +1cff00ffa5001060070f0a0202005c02ee000000000001152302beffffff +ffffffffff00ffa5240f10021d09020001000000000020000000044a4a00 +00440000000400007ce6000d0382ffffffffffffffff00ffa5240f10021d +09020001000000000020000000044a4a0000440000000400007ce6000d03 +82ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa500 +601006010a0126ff00ffa500106006010a0126ff00ffa5006010010402c4 +02ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff00 +ffa5001060070f0a0202005a02ee000000000001152302bcffffffffffff +ffff00ffa5240f10021d09020001000000000020000000044a4a00004400 +00000400007ce6000d0382ffffffffffffffff00ffa5240f10021d090200 +01000000000020000000044a4a0000440000000400007ce6000d0382ff00 +ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa500601006 +010a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02 +d0ff00ffa5001060010202ee0208ff00ffa50021600401ff022aff00ffa5 +002160070f0a0202005b02ee000000000001152302ceff00ffa500601007 +00011cff00ffa5001060070f0a0202005c02ee000000000001152302be10 +02501100731003100200124c81f11003ffffffffffffffff00ffa5240f10 +021d09020001000000000020000000044a4a0000440000000400007ce600 +0d0382ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ff +a500601006010a0126ff00ffa500106006010a0126ff00ffa50060100601 +0a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0 +ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa50010 +60070f0a0202005b02ee000000000001152302bdffffffffffffffff00ff +a5240f10021d09020001000000000020000000044a4a0000440000000400 +007ce6000d0382ffffffffffffffff00ffa5240f10021d09020001000000 +000020000000044a4a0000440000000400007ce6000d0382ff00ffa50060 +100401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126 +ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff00ff +a5001060010202ee0208ff00ffa50060100700011cff00ffa5001060070f +0a0202005a02ee000000000001152302bcffffffffffffffff00ffa5240f +10021d09020001000000000020000000044a4a0000440000000400007ce6 +000d0382ffffffffffffffff00ffa5240f10021d09020001000000000020 +000000044a4a0000440000000400007ce6000d0382ff00ffa50060100401 +ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ff +a500106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa50010 +60010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202 +005b02ee000000000001152302bdffffffffffffffff00ffa5240f10021d +09020001000000000020000000044a4a0000440000000400007ce6000d03 +82ffffffffffffffff00ffa5240f10021d09020001000000000020000000 +044a4a0000440000000400007ce6000d0382ff00ffa50060100401ff0219 +ff00ffa50010600401ff0219ffff00ffa500601006010a0126ff00ffa500 +106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa500106001 +0202ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202005b +02ee000000000001152302bdffffffffffffffff00ffa5240f10021d0903 +0001000000000020000000044a4a0000440000000400007ce6000d0383ff +ffffffffffffff00ffa5240f10021d09030001000000000020000000044a +4a0000440000000400007ce6000d0383ff00ffa50060100401ff0219ff00 +ffa50010600401ff0219ff00ffa50060100401ff0219ff00ffa500106004 +01ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126ff00 +ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa5 +0060100700011cff00ffa5001060070f0a0202005d02ee00000000000115 +2302bf10025014007610031002000300496e74656c6c6963686c6f722d2d +3430bc1003ffffffffffffffff00ffa5240f10021d090300010000000000 +20000000044a4a0000440000000400007ce6000d0383ff00ffa500601004 +01ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00 +ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa500 +1060010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a02 +02005c02ee000000000001152302beffffffffffffffff00ffa5240f1002 +1d09030001000000000020000000044a4a0000440000000400007ce6000d +0383ffffffffffffffff00ffa5240f10021d090300010000000000200000 +00044a4a0000440000000400007ce6000d0383ff00ffa50060100401ff02 +19ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa500 +106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa500106001 +0202ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202005c +02ee000000000001152302beffffffffffffffff00ffa5240f10021d0903 +0001000000000020000000044a4a0000440000000400007ce6000d0383ff +00ffa50021600401ff022aff00ffa5002160070f0a0202005b02ee000000 +000001152302ceffffffffffffffff00ffa5240f10021d09030001000000 +000020000000044a4a0000440000000400007ce6000d0383ff00ffa50060 +100401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126 +ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff00ff +a5001060010202ee0208ff00ffa50060100700011cff00ffa5001060070f +0a0202005c02ee000000000001152302beffffffffffffffff00ffa5240f +10021d09030001000000000020000000044a4a0000440000000400007ce6 +000d0383ffffffffffffffff00ffa5240f10021d09030001000000000020 +000000044a4a0000440000000400007ce6000d0383ff00ffa50060100401 +ff0219ff00ffa50010600401ff0219ff00ffa50060100401ff0219ff00ff +a50010600401ff0219ff00ffa500601006010a0126ff00ffa50010600601 +0a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee02 +08ff00ffa50060100700011cff00ffa5001060070f0a0202005b02ee0000 +00000001152302bdffffffffffffffff00ffa5240f10021d090300010000 +00000020000000044a4a0000440000000400007ce6000d0383ffffffffff +ffffff00ffa5240f10021d09030001000000000020000000044a4a000044 +0000000400007ce6000d0383ff00ffa50060100401ff0219ff00ffa50010 +600401ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126 +ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208ff00 +ffa50060100700011cff00ffa5001060070f0a0202005c02ee0000000000 +01152302beffffffffffffffff00ffa5240f10021d090300010000000000 +20000000044a4a0000440000000400007ce6000d0383ffffffffffffffff +00ffa5240f10021d09030001000000000020000000044a4a000044000000 +0400007ce6000d0383ffffffffffffffff00ffa5240f10021d0903000100 +0000000020000000044a4a0000440000000400007ce6000d0383ffffffff +ffffffff00ffa5240f10021d09030001000000000020000000044a4a0000 +440000000400007ce6000d0383ffffffffffffffff00ffa5240f10021d09 +030001000000000020000000044a4a0000440000000400007ce6000d0383 +ffffffffffffffff00ffa5240f10021d0903000100000000002000000004 +4a4a0000440000000400007ce6000d0383ffffffffffffffff00ffa5240f +10021d09030001000000000020000000044a4a0000440000000400007ce6 +000d03831002501100731003100200124c81f11003ffffffffffffffff00 +ffa5240f10021d09030001000000000020000000044a4a00004400000004 +00007ce6000d0383ff00ffa50060100401ff0219ff00ffa50010600401ff +0219ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa5 +00601006010a0126ff00ffa500106006010a0126ff00ffa5006010010402 +c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff +00ffa5001060070f0a0202005c02ee000000000001152302beffffffffff +ffffff00ffa5240f10021d09030001000000000020000000044a4a000044 +0000000400007ce6000d0383ffffffffffffffff00ffa5240f10021d0903 +0001000000000020000000044a4a0000440000000400007ce6000d0383ff +00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa5006010 +06010a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee +02d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa5 +001060070f0a0202005b02ee000000000001152402beffffffffffffffff +00ffa5240f10021d09030001000000000020000000044a4a000044000000 +0400007ce6000d0383ffffffffffffffff00ffa52421100101850181ffff +ffffffffffff00ffa5240f10021d09030001000000000020000000044a4a +0000450000000400007ce6000d0384ff00ffa50060100401ff0219ff00ff +a50010600401ff0219ff00ffa50060100401ff0219ff00ffa50010600401 +ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126ff00ff +a5006010010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa500 +60100700011cff00ffa5001060070f0a0202005d02ee0000000000011524 +02c0ffffffffffffffff00ffa5240f10021d090300010000000000200000 +00044a4a0000450000000400007ce6000d0384ffffffffffffffff00ffa5 +240f10021d09030001000000000020000000044a4a000045000000040000 +7ce6000d0384ff00ffa50060100401ff0219ff00ffa50010600401ff0219 +ff00ffa500601006010a0126ff00ffa500106006010a0126ff00ffa50060 +10010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa500601007 +00011cff00ffa5001060070f0a0202005b02ee000000000001152402beff +ffffffffffffff00ffa5240f10021d09030001000000000020000000044a +4a0000450000000400007ce6000d0384ffffffffffffff +ff00ffa5240f10021d09030001000000000020000000044a4a0000450000000400007ce6000dfffc +ff00ffa50060100401ff021900ffa50010600401ff0219ff00ffa5 +0060100401ff0219ff00ffa50010600401ff0219ff00ffa50060100401ff +0219ff00ffa50010600401ff0219ff00ffa50060100700011cff00ffa500 +1060070f0a0202005c02ee000000000001152402bfff00ffa50060100700 +011cff00ffa5001060070f0a0202005c02ee000000000001152402bf1002 +501100731003100200124c81f11003ffffffffffffffff00ffa5240f1002 +1d09030001000000000020000000044a4a0000440000000400007ce6000d +0383ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa5 +0060100401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a +0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff +00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa5001060 +070f0a0202005b02ee000000000001152402beffffffffffffffff00ffa5 +240f10021d09030001000000000020000000044a4a000044000000040000 +7ce6000d0383ffffffffffffffff00ffa5240f10021d0903000100000000 +0020000000044a4a0000440000000400007ce6000d0383ff00ffa5006010 +0401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff +00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa5 +001060010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a +0202005b02ee000000000001152402beffffffffffffffff00ffa5240f10 +021d09030001000000000020000000044a4a0000440000000400007ce600 +0d0383ffffffffffffffff00ffa5240f10021d0903000100000000002000 +0000044a4a0000440000000400007ce6000d0383ff00ffa50060100401ff +0219ff00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa5 +00106006010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060 +010202ee0208ff00ffa50060100700011cff00ffa5001060070f0a020200 +5b02ee000000000001152402beffffffffffffffff00ffa5240f10021d09 +030001000000000020000000044a4a0000440000000400007ce6000d0383 +ffffffffffffffff00ffa5240f10021d0903000100000000002000000004 +4a4a0000440000000400007ce6000d0383ff00ffa50060100401ff0219ff +00ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa5001060 +06010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202 +ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202005b02ee +000000000001152402beffffffffffffffff00ffa5240f10021d09030001 +000000000020000000044a4a0000440000000400007ce6000d0383ffffff +ffffffffff00ffa5240f10021d09030001000000000020000000044a4a00 +00440000000400007ce6000d0383ff00ffa50060100401ff0219ff00ffa5 +0010600401ff0219ff00ffa500601006010a0126ff00ffa500106006010a +0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208 +ff00ffa50060100700011cff00ffa5001060070f0a0202005d02ee000000 +000001152402c01002501100731003100200124c81f11003ffffffffffff +ffff00ffa5240f10021d09030001000000000020000000044a4a00004500 +00000400007ce6000d0384ff00ffa50060100401ff0219ff00ffa5001060 +0401ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126ff +00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208ff00ff +a50060100700011cff00ffa5001060070f0a0202005b02ee000000000001 +152402beffffffffffffffff00ffa5240f10021d09030001000000000020 +000000044a4a0000450000000400007ce6000d0384ffffffffffffffff00 +ffa5240f10021d09030001000000000020000000044a4a00004500000004 +00007ce6000d0384ff00ffa50060100401ff0219ff00ffa50010600401ff +0219ffff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ff +a500601006010a0126ff00ffa500106006010a0126ff00ffa50060100104 +02c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700011c +ff00ffa5001060070f0a0202005b02ee000000000001152402beffffffff +ffffffff00ffa5240f10021d09030001000000000020000000044a4a0000 +450000000400007ce6000d0384ff00ffa50021600401ff022aff00ffa500 +2160070f0a0202005c02ee000000000001152402d0ffffffffffffffff00 +ffa5240f10021d09030001000000000020000000044a4a00004500000004 +00007ce6000d0384ff00ffa50060100401ff0219ff00ffa50010600401ff +0219ff00ffa500601006010a0126ff00ffa500106006010a0126ff00ffa5 +006010010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060 +100700011cff00ffa5001060070f0a0202005b02ee000000000001152402 +beffffffffffffffff00ffa5240f10021d09030001000000000020000000 +044a4a0000450000000400007ce6000d0384ffffffffffffffff00ffa524 +0f10021d09030001000000000020000000044a4a0000450000000400007c +e6000d0384ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff +00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa5006010 +06010a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee +02d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa5 +001060070f0a0202005c02ee000000000001152402bfffffffffffffffff +00ffa5240f10021d09030001000000000020000000044a4a000045000000 +0400007ce6000d0384ffffffffffffffff00ffa5240f10021d0903000100 +0000000020000000044a4a0000450000000400007ce6000d0384ff00ffa5 +0060100401ff0219ff00ffa50010600401ff0219ff00ffa500601006010a +0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0ff +00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa5001060 +070f0a0202005b02ee000000000001152402beffffffffffffffff00ffa5 +240f10021d09030001000000000020000000044a4a000045000000040000 +7ce6000d0384ffffffffffffffff00ffa5240f10021d0903000100000000 +0020000000044a4a0000450000000400007ce6000d0384ffffffffffffff +ff00ffa5240f10021d09030001000000000020000000044a4a0000450000 +000400007ce6000d0384ffffffffffffffff00ffa5240f10021d09030001 +000000000020000000044a4a0000450000000400007ce6000d0384ffffff +ffffffffff00ffa5240f10021d09030001000000000020000000044a4a00 +00450000000400007ce6000d0384ffffffffffffffff00ffa5240f10021d +09030001000000000020000000044a4a0000450000000400007ce6000d03 +84ffffffffffffffff00ffa5240f10021d09030001000000000020000000 +044a4a0000450000000400007ce6000d0384100250110073100310020012 +4c81f11003ffffffffffffffff00ffa5240f10021d090300010000000000 +20000000044a4a0000450000000400007ce6000d0384ff00ffa500601004 +01ff0219ff00ffa50010600401ff0219ff00ffa50060100401ff0219ff00 +ffa50010600401ff0219ff00ffa500601006010a0126ff00ffa500106006 +010a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee +0208ff00ffa50060100700011cff00ffa5001060070f0a0202005c02ee00 +0000000001152502c0ffffffffffffffff00ffa5240f10021d0903000100 +0000000020000000044a4a0000450000000400007ce6000d0384ffffffff +ffffffff00ffa5240f10021d09030001000000000020000000044a4a0000 +450000000400007ce6000d0384ff00ffa50060100401ff0219ff00ffa500 +10600401ff0219ff00ffa500601006010a0126ff00ffa500106006010a01 +26ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208ff +00ffa50060100700011cff00ffa5001060070f0a0202005b02ee00000000 +0001152502bfffffffffffffffff00ffa5240f10021d0903000100000000 +0020000000044a4a0000450000000400007ce6000d0384ffffffffffffff +ff00ffa5240f1005080904041e06140000013effffffffffffffff00ffa5 +240f10021d09040001000000000020000000044a4a000045000000040000 +7ce6000d0385ff00ffa50060100401ff0219ff00ffa50010600401ff0219 +ff00ffa500601006010a0126ff00ffa500106006010a0126ff00ffa50060 +10010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa500601007 +00011cff00ffa5001060070f0a0202005b02ee000000000001152502bfff +ffffffffffffff00ffa5240f10021d09040001000000000020000000044a +4a0000450000000400007ce6000d0385ffffffffffffffff00ffa5240f10 +021d09040001000000000020000000044a4a0000450000000400007ce600 +0d0385ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ff +a500601006010a0126ff00ffa500106006010a0126ff00ffa50060100104 +02c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700011c +ff00ffa5001060070f0a0202005a02ee000000000001152502beffffffff +ffffffff00ffa5240f10021d09040001000000000020000000044a4a0000 +450000000400007ce6000d0385ffffffffffffffff00ffa5240f10021d09 +040001000000000020000000044a4a0000450000000400007ce6000d0385 +ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa5ffff +1fffffeafffdb596500a0126ff00ffa500601006010a0126ff00ffa50010 +6006010a0126ff00ffa5006010010402c402ee02d0ff00ffa50010600102 +02ee0208ff00ffa50060100700011cff00ffa5001060070f0a0202005b02 +ee000000000001152502bf1002500000621003100200010000131003ffff +ffffffffffff00ffa5240f10021d09040001000000000020000000044a4a +0000450000000400007ce6000d0385ff00ffa50060100401ff0219ff00ff +a50010600401ff0219ff00ffa50060100401ff0219ff00ffa50010600401 +ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126ff00ff +a5006010010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa500 +60100700011cff00ffa5001060070f0a0202005d02ee0000000000011525 +02c1ffffffffffffffff00ffa5240f10021d090400010000000000200000 +00044a4a0000450000000400007ce6000d0385ffffffffffffffff00ffa5 +240f10021d09040001000000000020000000044a4a000045000000040000 +7ce6000d0385ff00ffa50060100401ff0219ff00ffa50010600401ff0219 +ff00ffa500601006010a0126ff00ffa500106006010a0126ff00ffa50060 +10010402c402ee02d0ff00ffa5001060010202ee0208ff00ffa500601007 +00011cff00ffa5001060070f0a0202005b02ee000000000001152502bfff +ffffffffffffff00ffa5240f10021d09040001000000000020000000044a +4a0000450000000400007ce6000d0385ffffffffffffffff00ffa5240f10 +021d09040001000000000020000000044a4a0000450000000400007ce600 +0d0385ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ff +a50060100401ff0219ff00ffa50010600401ff0219ff00ffa50060100601 +0a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0 +ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa50010 +60070f0a0202005a02ee000000000001152502beffffffffffffffff00ff +a5240f10021d09040001000000000020000000044a4a0000450000000400 +007ce6000d0385ffffffffffffffff00ffa5240f10021d09040001000000 +000020000000044a4a0000450000000400007ce6000d0385ff00ffa50060 +100401ff0219ff00ffa50010600401ff0219ff00 +ffa500601006010a0126 +ff00ffa500106006010a0126ff00ffa5006010010402c402ee02d0 +ff00ffa5001060010202ee0208 \ No newline at end of file diff --git a/bundles/org.openhab.binding.pentair/src/test/data/easytouch8b.dat b/bundles/org.openhab.binding.pentair/src/test/data/easytouch8b.dat new file mode 100644 index 00000000000..21dc3bac3c9 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/data/easytouch8b.dat @@ -0,0 +1,90 @@ +ffffffffffffff +ff00ffa5240f10021d083a0001000000000020000000044a4a0000440000000400007ce6000d03b9 +ff00ffa50060010401ff0219 +ff00ffa50010600401ff0219 +ff00ffa500601006010a0126 +ff00ffa500106006010a0126 +ff00ffa5006010010402c402ee02d0 +ff00ffa5001060010202ee0208 +ff00ffa50060100700011c +ff00ffa5001060070f0a0202005c02ee000000000001151f02ba +ffffffffffffff +ff00ffa5240f10021d083a0001000000000020000000044a4a0000440000000400007ce6000d03b9 +ffffffffffffff +ff00ffa5240f10021d083a0001000000000020000000044a4a0000440000000400007ce6000d03b9 +ff00ffa50060100401ff0219 +ff00ffa50010600401ff0219 +ff00ffa500601006010a0126 +ff00ffa500106006010a0126 +ff00ffa5006010010402c402ee02d0 +ff00ffa5001060010202ee0208 +ffffffffffffff +ff00ffa5240f10080d4a4a444e5e04000000000000000285 +ff00ffa50060100700011c +ff00ffa5001060070f0a0202005a02ee000000000001151f02b8 +1002501400761003 +10 02 00 03 00 49 6e 74 65 6c 6c 69 63 68 6c 6f 72 2d 2d 34 30 bc 10 03 +ffffffffffffff +ff00ffa5240f10021d083b0001000000000020000000044a4a0000440000000400007ce6000d03ba +ff00ffa50060100401ff0219 +ff00ffa50010600401ff0219 +ff00ffa50060100401ff0219 +ff00ffa50010600401ff0219 +ff00ffa500601006010a0126 +ff00ffa500106006010a0126 +ff00ffa5006010010402c402ee02d0 +ff00ffa5001060010202ee0208 +ff00ffa50060100700011c +ff00ffa5001060070f0a0202005c02ee000000000001151f02ba +ffffffffffffff +ff00ffa5240f10021d083b0001000000000020000000044a4a0000440000000400007ce6000d03ba +ffffffffffffff +ff00ffa5240f10021d083b0001000000000020000000044a4a0000440000000400007ce6000d03ba +ff00ffa50060100401ff0219 +ff00ffa50010600401ff0219 +ff00ffa500601006010a0126 +ff00ffa500106006010a0126 +ff00ffa5006010010402c402ee02d0 +ff00ffa5001060010202ee0208 +ff00ffa50060100700011c +ff00ffa5001060070f0a0202005a02ee000000000001151f02b8 +ffffffffffffff +ff00ffa5240f10021d083b0001000000000020000000044a4a0000440000000400007ce6000d03ba +ffffffffffffff +ff00ffa5240f10021d083b0001000000000020000000044a4a +0000440000000400007ce6000d03baff00ffa50060100401ff0219ff00ff +a50010600401ff0219ff00ffa500601006010a0126ff00ffa50010600601 +0a0126ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee02 +08ff00ffa50060100700011cff00ffa5001060070f0a0202005b02ee0000 +00000001151f02b9ffffffffffffffff00ffa5240f10021d083b00010000 +00000020000000044a4a0000440000000400007ce6000d03baffffffffff +ffffff00ffa5240f10021d083b0001000000000020000000044a4a000044 +0000000400007ce6000d03baff00ffa50060100401ff0219ff00ffa50010 +600401ff0219ff00ffa500601006010a0126ff00ffa500106006010a0126 +ff00ffa5006010010402c402ee02d0ff00ffa5001060010202ee0208ff00 +ffa50060100700011cff00ffa5001060070f0a0202005b02ee0000000000 +01151f02b9ffffffffffffffff00ffa5240f10021d083b00010000000000 +20000000044a4a0000440000000400007ce6000d03baffffffffffffffff +00ffa5240f10021d083b0001000000000020000000044a4a000044000000 +0400007ce6000d03baff00ffa50060100401ff0219ff00ffa50010600401 +ff0219ff00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ff +a500601006010a0126ff00ffa500106006010a0126ff00ffa50060100104 +02c402ee02d0ff00ffa5001060010202ee0208ff00ffa50060100700011c +ff00ffa5001060070f0a0202005c02ee000000000001151f02baffffffff +ffffffff00ffa5240f10021d083b0001000000000020000000044a4a0000 +440000000400007ce6000d03baffffffffffffffff00ffa5240f10021d08 +3b0001000000000020000000044a4a0000440000000400007ce6000d03ba +ffffffffffffffff00ffa5240f10021d083b000100000000002000000004 +4a4a0000440000000400007ce6000d03baffffffffffffffff00ffa5240f +10021d083b0001000000000020000000044a4a0000440000000400007ce6 +000d03baffffffffffffffff00ffa5240f10021d083b0001000000000020 +000000044a4a0000440000000400007ce6000d03baffffffffffffffff00 +ffa5240f10021d083b0001000000000020000000044a4a00004400000004 +00007ce6000d03baffffffffffffffff00ffa5240f10021d083b00010000 +00000020000000044a4a0000440000000400007ce6000d03ba1002501100 +731003100200124c81f11003ffffffffffffffff00ffa5240f10021d083b +0001000000000020000000044a4a0000440000000400007ce6000d03baff +00ffa50060100401ff0219ff00ffa50010600401ff0219ff00ffa5006010 +06010a0126ff00ffa500106006010a0126ff00ffa5006010010402c402ee +02d0ff00ffa5001060010202ee0208ff00ffa50060100700011cff00ffa5 +001060070f0a0202005b02ee000000000001151f02b9ffffffffffffffff diff --git a/bundles/org.openhab.binding.pentair/src/test/data/nodejs-capture.dat b/bundles/org.openhab.binding.pentair/src/test/data/nodejs-capture.dat new file mode 100644 index 00000000000..051815745f9 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/data/nodejs-capture.dat @@ -0,0 +1,276 @@ +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 02 1D 14 1E 00 00 00 00 00 00 00 00 03 00 40 04 39 39 +20 00 3A 38 00 00 04 00 00 88 BE 00 0D 03 C7 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 02 1D 14 1E 00 00 00 00 00 00 00 00 03 00 40 04 39 39 +20 00 3A 38 00 00 04 00 00 88 BE 00 0D 03 C7 +FF 00 FF A5 10 10 22 86 02 0B 01 01 7B +FF FF FF FF FF FF FF FF 00 FF A5 10 22 10 01 01 86 01 6F +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 02 1D 14 1E 20 04 00 00 00 00 00 00 03 00 40 04 39 39 +20 00 3A 38 00 00 04 00 00 88 BE 00 0D 03 EB +FF 00 FF A5 00 60 10 07 00 01 1C +FF 00 FF A5 00 10 60 07 0F 04 00 00 00 00 00 00 00 00 00 00 00 00 14 1E 01 61 FF 00 FF A5 00 61 +10 07 00 01 1D +FF 00 FF A5 00 10 61 07 0F 04 00 00 00 00 00 00 00 00 00 00 00 00 14 1E 01 62 +FF 00 FF A5 10 10 22 86 02 06 00 01 75 +FF FF FF FF FF FF FF FF 00 FF A5 10 22 10 01 01 86 01 6F +10 02 50 11 00 73 10 03 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 02 1D 14 1E 20 04 00 00 00 00 00 00 03 00 40 04 39 39 +20 00 3A 38 00 00 04 00 00 88 BE 00 0D 03 EB +FF 00 FF A5 00 60 10 04 01 FF 02 19 +FF 00 FF A5 00 10 60 04 01 FF 02 19 +FF 00 FF A5 00 60 10 06 01 0A 01 26 +FF 00 FF A5 00 10 60 06 01 0A 01 26 +FF 00 FF A5 00 60 10 01 04 02 C4 07 6C 02 53 +FF 00 FF A5 00 10 60 01 02 07 6C 01 8B +FF 00 FF A5 00 61 10 04 01 FF 02 1A FF 00 FF A5 00 10 61 04 01 FF 02 1A +FF 00 FF A5 00 61 10 06 01 04 01 21 +FF 00 FF A5 00 10 61 06 01 04 01 21 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 02 1D 14 1E 20 04 00 00 00 00 00 00 03 00 40 04 39 39 +20 00 3A 38 00 00 04 00 00 88 BE 00 0D 03 EB +FF 00 FF A5 10 10 22 88 04 52 64 07 00 02 30 +FF FF FF FF FF FF FF FF 00 FF A5 10 22 10 01 01 88 01 71 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 02 1D 14 1E 20 04 00 00 00 00 00 00 03 00 40 04 39 39 +20 00 3A 38 00 00 07 00 00 7D C1 00 0D 03 E6 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 C5 01 00 +01 AB FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 05 08 14 1E 04 07 02 11 00 01 01 32 FF FF FF FF +FF FF FF FF 00 FF A5 10 10 20 C8 01 00 01 AE +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 08 0D 39 39 3A 52 64 07 00 00 38 00 00 00 00 02 8A +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 00 01 B0 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 00 57 74 72 46 61 6C 6C 20 31 00 FB 04 F2 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 01 01 B1 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 01 57 74 72 46 61 6C 6C 20 31 2E 35 04 5B FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 02 01 B2 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 02 57 74 72 46 61 6C 6C 20 32 00 FB 04 F5 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 03 01 B3 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 03 57 74 72 46 61 6C 6C 20 33 00 FB 04 F7 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 04 01 B4 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 04 50 6F 6F 6C 20 4C 6F 77 32 00 FB 05 07 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 05 01 B5 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 05 55 53 45 52 4E 41 4D 45 2D 30 36 03 E2 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 06 01 B6 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 06 55 53 45 52 4E 41 4D 45 2D 30 37 03 E4 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 07 01 B7 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 07 55 53 45 52 4E 41 4D 45 2D 30 38 03 E6 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 08 01 B8 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 08 55 53 45 52 4E 41 4D 45 2D 30 39 03 E8 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 09 01 B9 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 09 55 53 45 52 4E 41 4D 45 2D 31 30 03 E1 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 01 01 B2 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 01 01 48 00 00 01 2E FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 02 01 B3 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 02 00 2E 00 00 01 14 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 03 01 B4 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 03 00 02 00 00 00 E9 FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 04 01 B5 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 04 05 16 00 00 01 +03 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 05 01 B6 FF FF FF FF FF FF FF FF 00 FF A5 10 +0F 10 0B 05 05 40 C9 00 00 01 F2 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 06 01 B7 FF FF +FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 06 42 3D 00 00 01 69 FF FF FF FF FF FF FF FF 00 FF A5 +10 10 20 CB 01 07 01 B8 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 07 07 4A 00 00 01 3C FF +FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 08 01 B9 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 +0B 05 08 07 3F 00 00 01 32 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 09 01 BA FF FF FF FF +FF FF FF FF 00 FF A5 10 0F 10 0B 05 09 07 37 00 00 01 2B FF FF FF FF FF FF FF FF 00 FF A5 10 10 +20 CB 01 0A 01 BB FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 0A 00 00 00 00 00 EE FF FF FF +FF FF FF FF FF 00 FF A5 10 10 20 CB 01 0B 01 BC FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 +0B 0E 4F 00 00 01 4C FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 0C 01 BD FF FF FF FF FF FF +FF FF 00 FF A5 10 0F 10 0B 05 0C 00 C8 00 00 01 B8 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB +01 0D 01 BE +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 0D 00 CA 00 00 01 BB FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 0E 01 BF +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 0E 00 CB 00 00 01 BD FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 0F 01 C0 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 0F 00 CC 00 00 01 BF FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 10 01 C1 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 10 0E 35 00 00 01 37 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 11 01 C2 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 11 0E 35 00 00 01 38 FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 12 01 C3 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 12 0E 35 00 00 01 39 FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 D1 01 01 01 B8 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 01 06 09 19 0F 37 FF 02 5A +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D1 01 02 01 B9 FF FF FF FF FF FF FF FF 00 FF A5 10 0F +10 11 07 02 0D 0E 39 0F 08 D5 02 2E +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D1 01 03 01 BA +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 03 04 0A 0F 0B 00 FF 02 16 FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 04 01 BB +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 04 06 19 00 07 0F 00 01 25 FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 05 01 BC +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 05 04 19 00 04 00 00 01 12 FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 06 01 BD +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 06 0F 15 0A 17 37 FF 02 6D +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D1 01 07 01 BE +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 07 0F 00 05 09 14 FF 02 23 FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 08 01 BF FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 08 07 19 00 +02 00 00 01 16 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D1 01 09 01 C0 FF FF FF FF FF FF FF FF +00 FF A5 10 0F 10 11 07 09 02 19 00 03 2D 00 01 40 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D1 +01 0A 01 C1 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 0A 09 19 00 04 0F 00 01 2B FF FF FF +FF FF FF FF FF 00 FF A5 10 10 20 D1 01 0B 01 C2 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 +0B 0B 0D 00 0D 0B FF 02 26 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D1 01 0C 01 C3 FF FF FF FF +FF FF FF FF 00 FF A5 10 0F 10 11 07 0C 05 0D 14 0D 28 95 01 E8 FF FF FF FF FF FF FF FF 00 FF A5 +10 10 20 E2 01 00 01 C8 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 22 03 07 80 44 01 C4 FF FF FF +FF FF FF FF FF 00 FF A5 10 10 20 E3 01 00 01 C9 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 23 02 +10 00 01 09 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 E8 01 00 01 CE +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 28 0A 00 00 00 FE 01 00 00 00 00 00 02 05 FF FF FF FF +FF FF FF FF 00 FF A5 10 10 20 DE 01 00 01 C4 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 1E 10 00 00 00 00 01 48 00 00 00 2E 00 00 00 02 00 00 +01 7B FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 E1 01 00 01 C7 FF FF FF FF FF FF FF FF 00 FF A5 +10 0F 10 21 04 01 02 03 04 01 03 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 E0 01 00 01 C6 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 20 0B 00 07 02 01 08 05 06 07 08 09 0A 01 3E FF FF FF +FF FF FF FF FF 00 FF A5 10 10 20 DD 01 00 01 C3 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 1D 18 02 00 00 00 80 01 FF FF FF 00 07 02 01 08 05 06 +07 08 09 0A 01 02 03 04 04 D2 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D9 01 00 01 BF +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 19 16 0B 08 80 1C 85 00 49 6E 74 65 6C 6C 69 63 68 6C +6F 72 2D 2D 34 30 07 DE FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D6 01 00 01 BC FF FF FF FF FF +FF FF FF 00 FF A5 10 0F 10 16 10 00 02 07 6C 00 01 32 0A 01 90 0D 7A 0F 82 00 00 03 55 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 E7 01 00 01 CD +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 27 20 08 00 00 00 09 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 2C FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 +E0 01 01 01 C7 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 20 0B 01 01 02 03 04 05 06 07 08 09 0A +01 37 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D8 01 01 01 BF +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 18 1F 01 80 00 02 00 01 06 02 0C 04 09 0B 07 06 05 80 +08 84 03 0F 03 03 D6 80 2E 6C 14 AC E8 20 E8 07 91 +FF 00 FF A5 10 10 22 88 04 53 64 07 00 02 31 +FF FF FF FF FF FF FF FF 00 FF A5 10 22 10 01 01 88 01 71 +FF +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D8 01 02 01 C0 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 18 1F 02 80 03 02 00 0C 03 05 05 0D 07 0E 0B 00 03 00 +03 00 03 00 03 0C E8 DC D0 B8 E8 E8 E8 E8 1C 08 F8 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 02 1D 14 1E 20 04 00 00 00 00 00 00 03 00 40 04 39 39 +20 00 3A 38 00 00 07 00 00 81 C2 00 0D 03 EB +FF 00 FF A5 10 10 22 88 04 53 64 04 00 02 2E +FF FF FF FF FF FF FF FF 00 FF A5 10 22 10 01 01 88 01 71 +FF 00 FF A5 00 60 10 04 01 FF 02 19 +FF 00 FF A5 00 10 60 04 01 FF 02 19 +FF 00 FF A5 00 60 10 06 01 0A 01 26 +FF 00 FF A5 00 10 60 06 01 0A 01 26 FF 00 FF A5 00 60 10 01 04 02 C4 07 6C 02 53 FF 00 FF A5 00 +10 60 01 02 07 6C 01 8B FF 00 FF A5 00 61 10 04 01 FF 02 1A FF 00 FF A5 00 10 61 04 01 FF 02 1A +FF 00 FF A5 00 61 10 06 01 04 01 21 FF 00 FF A5 00 10 61 06 01 04 01 21 +FF 00 FF A5 10 10 22 86 02 0B 00 01 7A +FF FF FF FF FF FF FF FF 00 FF A5 10 22 10 01 01 86 01 6F +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 02 1D 14 1E 00 00 00 00 00 00 00 00 03 00 40 04 39 39 +20 00 3A 38 00 00 04 00 00 82 BF 00 0D 03 C2 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 C5 01 00 +01 AB +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 05 08 14 1E 04 07 02 11 00 01 01 32 FF FF FF FF FF FF +FF FF 00 FF A5 10 10 20 C8 01 00 01 AE FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 08 0D 39 39 3A +53 64 04 00 00 38 00 00 00 00 02 88 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 00 01 B0 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 00 57 74 72 46 61 6C 6C 20 31 00 FB 04 F2 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 01 01 B1 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 01 57 74 72 46 61 6C 6C 20 31 2E 35 04 5B FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 02 01 B2 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 02 57 74 72 46 61 6C 6C 20 32 00 FB 04 F5 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 03 01 B3 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 03 57 74 72 46 61 6C 6C 20 33 00 FB 04 F7 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 04 01 B4 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 04 50 6F 6F 6C 20 4C 6F 77 32 00 FB 05 07 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 05 01 B5 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 05 55 53 45 52 4E 41 4D 45 2D 30 36 03 E2 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 06 01 B6 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 06 55 53 45 52 4E 41 4D 45 2D 30 37 03 E4 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 07 01 B7 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 07 55 53 45 52 4E 41 4D 45 2D 30 38 03 E6 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 08 01 B8 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A 0C 08 55 53 45 52 4E 41 4D 45 2D 30 39 03 E8 FF FF +FF FF FF FF FF FF 00 FF A5 10 10 20 CA 01 09 01 B9 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0A +0C 09 55 53 45 52 4E 41 4D 45 2D 31 30 03 E1 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 01 +01 B2 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 01 01 48 00 00 01 2E FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 CB 01 02 01 B3 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 02 00 2E 00 +00 01 14 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 03 01 B4 FF FF FF FF FF FF FF FF 00 FF +A5 10 0F 10 0B 05 03 00 02 00 00 00 E9 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 04 01 B5 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 04 05 16 00 00 01 03 FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 05 01 B6 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 05 40 C9 00 00 01 +F2 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 06 01 B7 FF FF FF FF FF FF FF FF 00 FF A5 10 +0F 10 0B 05 06 42 3D 00 00 01 69 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 07 01 B8 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 07 07 4A 00 00 01 3C FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 08 01 B9 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 08 07 3F 00 00 01 32 FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 09 01 BA +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 09 07 37 00 00 01 2B FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 0A 01 BB +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 0A 00 00 00 00 00 EE FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 0B 01 BC +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 0B 0E 4F 00 00 01 4C FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 0C 01 BD +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 0C 00 C8 00 00 01 B8 FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 0D 01 BE +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 0D 00 CA 00 00 01 BB FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 0E 01 BF +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 0E 00 CB 00 00 01 BD +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 0F 01 C0 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 0F 00 CC 00 00 01 BF FF FF FF FF FF FF FF FF 00 +FF A5 10 10 20 CB 01 10 01 C1 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 10 0E 35 00 00 01 37 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 11 01 C2 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 0B 05 11 0E 35 00 00 01 38 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 CB 01 12 01 C3 FF FF FF FF FF FF FF FF 00 FF A5 10 0F +10 0B 05 12 0E 35 00 00 01 39 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D1 01 01 01 B8 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 01 06 09 19 0F 37 FF 02 5A FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 02 01 B9 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 02 0D 0E 39 0F 08 D5 02 2E FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 03 01 BA +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 03 04 0A 0F 0B 00 FF 02 16 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D1 01 04 01 BB +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 04 06 19 00 07 0F 00 01 25 FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 05 01 BC +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 05 04 19 00 04 00 00 01 12 FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 06 01 BD +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 06 0F 15 0A 17 37 FF 02 6D FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 07 01 BE +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 07 0F 00 05 09 14 FF 02 23 FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 08 01 BF +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 08 07 19 00 02 00 00 01 16 FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 09 01 C0 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 09 02 19 00 03 2D 00 01 40 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D1 01 0A 01 C1 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 0A 09 19 00 04 0F 00 01 2B +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D1 01 0B 01 C2 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 0B 0B 0D 00 0D 0B FF 02 26 FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 D1 01 0C 01 C3 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 11 07 0C 05 0D 14 0D 28 95 01 E8 FF FF FF FF FF FF FF +FF 00 FF A5 10 10 20 E2 01 00 01 C8 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 22 03 07 80 44 01 C4 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 E3 01 00 01 C9 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 23 02 10 00 01 09 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 E8 01 00 01 CE +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 28 0A 00 00 00 FE 01 00 00 00 00 00 02 05 FF FF FF FF +FF FF FF FF 00 FF A5 10 10 20 DE 01 00 01 C4 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 1E 10 00 00 00 00 01 48 00 00 00 2E 00 00 00 02 00 00 +01 7B FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 E1 01 00 01 C7 FF FF FF FF FF FF FF FF 00 FF A5 +10 0F 10 21 04 01 02 03 04 01 03 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 E0 01 00 01 C6 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 20 0B 00 07 02 01 08 05 06 07 08 09 0A 01 3E +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 DD 01 00 01 C3 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 1D 18 02 00 00 00 80 01 FF FF FF 00 07 02 01 08 05 06 +07 08 09 0A 01 02 03 04 04 D2 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D9 01 00 01 BF FF FF FF +FF FF FF FF FF 00 FF A5 10 0F 10 19 16 0B 08 80 1C 85 00 49 6E 74 65 6C 6C 69 63 68 6C 6F 72 2D +2D 34 30 07 DE FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D6 01 00 01 BC FF FF FF FF FF FF FF FF +00 FF A5 10 0F 10 16 10 00 02 00 00 00 01 32 0A 01 90 0D 7A 0F 82 00 00 02 E2 +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 E7 01 00 01 CD +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 27 20 08 00 00 00 09 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 2C FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 +E0 01 01 01 C7 FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 20 0B 01 01 02 03 04 05 06 07 08 09 0A +01 37 FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D8 01 01 01 BF +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 18 1F 01 80 00 02 00 01 06 02 0C 04 09 0B 07 06 05 80 +08 84 03 0F 03 03 D6 80 2E 6C 14 AC E8 20 E8 07 91 +FF 00 FF A5 10 10 22 86 02 06 01 01 76 +FF FF FF FF FF FF FF FF 00 FF A5 10 22 10 01 01 86 01 6F +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 02 1D 14 1E 20 00 00 00 00 00 00 00 03 00 40 04 39 39 +20 00 3A 38 00 00 04 00 00 82 BF 00 0D 03 E2 +FF +FF FF FF FF FF FF FF FF 00 FF A5 10 10 20 D8 01 02 01 C0 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 18 1F 02 80 03 02 00 0C 03 05 05 0D 07 0E 0B 00 03 00 +03 00 03 00 03 0C E8 DC D0 B8 E8 E8 E8 E8 1C 08 F8 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 02 1D 14 1E 20 00 00 00 00 00 00 00 03 00 40 04 39 39 +20 00 3A 38 00 00 04 00 00 82 BF 00 0D 03 E2 +FF 00 FF A5 00 60 10 04 01 FF 02 19 +FF 00 FF A5 00 10 60 04 01 FF 02 19 +FF 00 FF A5 00 60 10 06 01 0A 01 26 +FF 00 FF A5 00 10 60 06 01 0A 01 26 +FF 00 FF A5 00 60 10 01 04 02 C4 05 14 01 F9 +FF 00 FF A5 00 10 60 01 02 05 14 01 31 +FF 00 FF A5 00 61 10 04 01 FF 02 1A +FF 00 FF A5 00 10 61 04 01 FF 02 1A +FF 00 FF A5 00 61 10 06 01 04 01 21 +FF 00 FF A5 00 10 61 06 01 04 01 21 +FF 00 FF A5 10 10 22 D7 01 01 01 C0 +FF FF FF FF FF FF FF FF 00 FF A5 10 0F 10 17 10 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 80 +01 7C +FF 00 FF A5 10 10 22 D7 01 02 01 C1 diff --git a/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairControllerScheduleTest.java b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairControllerScheduleTest.java new file mode 100644 index 00000000000..d0d882e1475 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairControllerScheduleTest.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.openhab.binding.pentair.internal.TestUtilities.parsehex; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.pentair.internal.handler.helpers.PentairControllerSchedule; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; + +/** + * PentairControllerSchduleTest + * + * @author Jeff James - Initial contribution + * + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class PentairControllerScheduleTest { + + //@formatter:off + public static byte[][] packets = { + parsehex("A5 1E 0F 10 11 07 01 06 0A 00 10 00 7F"), + parsehex("A5 1E 0F 10 11 07 02 05 0A 00 0B 00 7F"), + parsehex("A5 1E 0F 10 11 07 03 07 08 00 1A 00 08"), + parsehex("A5 1E 0F 10 11 07 04 09 19 00 02 15 0F") + }; + //@formatter:on + + @BeforeAll + public static void setUpBeforeClass() throws Exception { + } + + @AfterAll + public static void tearDownAfterClass() throws Exception { + } + + @BeforeEach + public void setUp() throws Exception { + } + + @AfterEach + public void tearDown() throws Exception { + } + + @Test + public void parseTest() { + PentairControllerSchedule pcs = new PentairControllerSchedule(); + + PentairStandardPacket p = new PentairStandardPacket(packets[0]); + + pcs.parsePacket(p); + assertThat(pcs.circuit, equalTo(6)); + assertThat(pcs.start, equalTo(10 * 60)); + assertThat(pcs.end, equalTo(16 * 60)); + assertThat(pcs.days, equalTo(0x7F)); + assertThat(pcs.type, equalTo(PentairControllerSchedule.ScheduleType.NORMAL)); + assertThat(pcs.id, equalTo(1)); + + PentairStandardPacket p2 = new PentairStandardPacket(packets[1]); + pcs.parsePacket(p2); + assertThat(pcs.circuit, equalTo(5)); + assertThat(pcs.start, equalTo(10 * 60)); + assertThat(pcs.end, equalTo(11 * 60)); + assertThat(pcs.days, equalTo(0x7F)); + assertThat(pcs.type, equalTo(PentairControllerSchedule.ScheduleType.NORMAL)); + assertThat(pcs.id, equalTo(2)); + + PentairStandardPacket p3 = new PentairStandardPacket(packets[2]); + pcs.parsePacket(p3); + assertThat(pcs.circuit, equalTo(7)); + assertThat(pcs.start, equalTo(8 * 60)); + assertThat(pcs.days, equalTo(0x08)); + assertThat(pcs.type, equalTo(PentairControllerSchedule.ScheduleType.ONCEONLY)); + assertThat(pcs.id, equalTo(3)); + + PentairStandardPacket p4 = new PentairStandardPacket(packets[3]); + pcs.parsePacket(p4); + assertThat(pcs.circuit, equalTo(9)); + assertThat(pcs.end, equalTo(0x02 * 60 + 0x15)); + assertThat(pcs.days, equalTo(0x0F)); + assertThat(pcs.type, equalTo(PentairControllerSchedule.ScheduleType.EGGTIMER)); + assertThat(pcs.id, equalTo(4)); + } + + @Test + public void setTest() { + PentairControllerSchedule pcs = new PentairControllerSchedule(); + + pcs.id = 1; + pcs.circuit = 4; + pcs.start = 5 * 60 + 15; // 5:15 + pcs.end = 10 * 60 + 30; // 10:30 + pcs.type = PentairControllerSchedule.ScheduleType.NORMAL; + pcs.days = 0x07; + + PentairStandardPacket p = Objects.requireNonNull(pcs.getWritePacket(0x10, 0x00)); + + assertThat(p.buf, is(parsehex("A5 00 10 00 91 07 01 04 05 0F 0A 1E 07"))); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairControllerStatusTest.java b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairControllerStatusTest.java new file mode 100644 index 00000000000..aa25820d853 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairControllerStatusTest.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openhab.binding.pentair.internal.TestUtilities.parsehex; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.pentair.internal.handler.helpers.PentairControllerStatus; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairControllerStatusTest} + * + * @author Jeff James - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class PentairControllerStatusTest { + private final Logger logger = LoggerFactory.getLogger(PentairControllerStatusTest.class); + + //@formatter:off + public static byte[][] packets = { + parsehex("A5 1E 0F 10 02 1D 09 20 21 00 00 00 00 00 00 20 0F 00 00 04 3F 3F 00 00 41 3C 00 00 07 00 00 6A B6 00 0D"), + parsehex("A5 24 0f 10 02 1d 08 3b 00 01 00 00 00 00 00 20 00 00 00 04 4a 4a 00 00 44 00 00 00 04 00 00 7c e6 00 0d 03 ba"), + parsehex("a5 24 0f 10 02 1d 09 04 00 31 00 00 00 00 00 20 00 00 00 04 4a 4a 00 00 45 00 00 00 04 00 07 ce 60 00 0d 03 85"), + parsehex("A5 1E 0F 10 02 1D 0A 0B 00 00 00 00 00 00 00 21 33 00 00 04 45 45 00 00 3F 3F 00 00 07 00 00 D9 89 00 0D") + }; + //@formatter:on + + @BeforeAll + public static void setUpBeforeClass() throws Exception { + } + + @AfterAll + public static void tearDownAfterClass() throws Exception { + } + + @BeforeEach + public void setUp() throws Exception { + } + + @AfterEach + public void tearDown() throws Exception { + } + + @Test + public void test() { + PentairControllerStatus pcs = new PentairControllerStatus(); + + PentairStandardPacket p = new PentairStandardPacket(packets[0], packets[0].length); + pcs.parsePacket(p); + logger.debug(pcs.toString()); + + assertThat(pcs.circuits[0], equalTo(true)); + assertThat(pcs.circuits[5], equalTo(true)); + assertThat(pcs.pool, equalTo(true)); + assertThat(pcs.poolTemp, equalTo(63)); + assertThat(pcs.spaTemp, equalTo(63)); + assertThat(pcs.airTemp, equalTo(65)); + assertThat(pcs.solarTemp, equalTo(60)); + + p = new PentairStandardPacket(packets[1], packets[1].length); + pcs.parsePacket(p); + logger.debug(pcs.toString()); + + assertThat(pcs.circuits[8], equalTo(true)); + assertThat(pcs.pool, equalTo(false)); + assertThat(pcs.poolTemp, equalTo(74)); + assertThat(pcs.spaTemp, equalTo(74)); + assertThat(pcs.airTemp, equalTo(68)); + assertThat(pcs.solarTemp, equalTo(0)); + + p = new PentairStandardPacket(packets[2], packets[2].length); + pcs.parsePacket(p); + logger.debug(pcs.toString()); + + assertThat(pcs.circuits[8], equalTo(true)); + assertThat(pcs.circuits[12], equalTo(true)); + assertThat(pcs.circuits[13], equalTo(true)); + assertThat(pcs.pool, equalTo(false)); + assertThat(pcs.poolTemp, equalTo(74)); + assertThat(pcs.spaTemp, equalTo(74)); + assertThat(pcs.airTemp, equalTo(69)); + assertThat(pcs.solarTemp, equalTo(0)); + + p = new PentairStandardPacket(packets[3], packets[3].length); + pcs.parsePacket(p); + logger.debug(pcs.toString()); + assertThat(pcs.equip, equalTo(0)); + assertThat(pcs.pool, equalTo(false)); + assertThat(pcs.poolTemp, equalTo(69)); + assertThat(pcs.spaTemp, equalTo(69)); + assertThat(pcs.airTemp, equalTo(63)); + assertThat(pcs.solarTemp, equalTo(63)); + assertThat(pcs.serviceMode, equalTo(true)); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairHeatStatusTest.java b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairHeatStatusTest.java new file mode 100644 index 00000000000..bd9babb109b --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairHeatStatusTest.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openhab.binding.pentair.internal.TestUtilities.parsehex; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.pentair.internal.handler.helpers.PentairHeatStatus; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairHeatStatusTest} + * + * @author Jeff James - Initial contribution + */ + +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +class PentairHeatStatusTest { + private final Logger logger = LoggerFactory.getLogger(PentairHeatStatusTest.class); + + //@formatter:off + public static byte[][] packets = { + parsehex("A5 01 0F 10 08 0D 4B 4B 4D 55 5E 07 00 00 58 00 00 00") + }; + //@formatter:on + + @BeforeAll + static void setUpBeforeClass() throws Exception { + } + + @AfterAll + static void tearDownAfterClass() throws Exception { + } + + @BeforeEach + void setUp() throws Exception { + } + + @AfterEach + void tearDown() throws Exception { + } + + @Test + void test() { + PentairHeatStatus hs = new PentairHeatStatus(); + + PentairStandardPacket p = new PentairStandardPacket(packets[0], packets[0].length); + hs.parsePacket(p); + + assertThat(hs.poolSetPoint, equalTo(85)); + assertThat(hs.poolHeatMode, equalTo(PentairHeatStatus.HeatMode.SOLAR)); + assertThat(hs.spaSetPoint, equalTo(94)); + assertThat(hs.spaHeatMode, equalTo(PentairHeatStatus.HeatMode.HEATER)); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairIntelliChemTest.java b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairIntelliChemTest.java new file mode 100644 index 00000000000..e722d71f45a --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairIntelliChemTest.java @@ -0,0 +1,237 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openhab.binding.pentair.internal.TestUtilities.parsehex; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.pentair.internal.handler.helpers.PentairIntelliChem; +import org.openhab.binding.pentair.internal.handler.helpers.PentairIntelliChem.DosingStatus; +import org.openhab.binding.pentair.internal.handler.helpers.PentairIntelliChem.OrpDoserType; +import org.openhab.binding.pentair.internal.handler.helpers.PentairIntelliChem.PhDoserType; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; + +/** + * PentairIntelliChemTest + * + * @author Jeff James - Initial contribution + * + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class PentairIntelliChemTest { + //@formatter:off + public static byte[][] packets = { + parsehex("A50010901229030202A302D002C60000000000000000000000000006070000C8003F005A3C00580006A5201E010000"), + parsehex("A5100F10122902E302AF02EE02BC000000020000002A0004005C060518019000000096140051000065203C01000000"), + parsehex("A5001090122902E4030202E402BC00000010000000000023000006060300FA002C00A0140051080095005001000000"), + parsehex("A5001090122902F3030502F802EE0000007900000000000000000000FD01C2005000465200550800A2205001000000"), + parsehex("A5001090122902EA02DC02F8028A0000000800000000000000000000F4015E0000004652004D0000A2205001000000") + }; + //@formatter:on + + @Test + public void test() { + PentairIntelliChem pic = new PentairIntelliChem(); + PentairStandardPacket p = new PentairStandardPacket(packets[0], packets[0].length); + + pic.parsePacket(p); + + assertThat(pic.phReading, equalTo(7.70)); + assertThat(pic.orpReading, equalTo(675)); + assertThat(pic.phSetPoint, equalTo(7.20)); + assertThat(pic.orpSetPoint, equalTo(710)); + assertThat(pic.tank1Level, equalTo(0)); + assertThat(pic.tank2Level, equalTo(6)); + assertThat(pic.calciumHardness, equalTo(0)); + assertThat(pic.cyaReading, equalTo(0)); + assertThat(pic.alkalinity, equalTo(16128)); + // assertThat(pic.alarmWaterFlow, equalTo(fal)); + assertThat(pic.lsi, equalTo(0.07)); + assertThat(pic.phDoserType, equalTo(PhDoserType.CO2)); + assertThat(pic.orpDoserType, equalTo(OrpDoserType.ORP)); + assertThat(pic.phDoserStatus, equalTo(DosingStatus.DOSING)); + assertThat(pic.orpDoserStatus, equalTo(DosingStatus.DOSING)); + assertThat(pic.phDoseTime, equalTo(0)); + assertThat(pic.orpDoseTime, equalTo(0)); + assertThat(pic.saltLevel, equalTo(4500)); + assertThat(pic.calcCalciumHardnessFactor(), equalTo(1.0)); + + assertThat(pic.alarmWaterFlow, equalTo(false)); + assertThat(pic.alarmPh, equalTo(false)); + assertThat(pic.alarmOrp, equalTo(true)); + assertThat(pic.alarmPhTank, equalTo(false)); + assertThat(pic.alarmOrpTank, equalTo(true)); + assertThat(pic.alarmProbeFault, equalTo(false)); + + assertThat(pic.warningPhLockout, equalTo(false)); + assertThat(pic.warningPhDailyLimitReached, equalTo(false)); + assertThat(pic.warningOrpDailyLimitReached, equalTo(false)); + assertThat(pic.warningInvalidSetup, equalTo(false)); + assertThat(pic.warningChlorinatorCommError, equalTo(false)); + assertThat(pic.firmwareVersion, equalTo("30.032")); + + p = new PentairStandardPacket(packets[1], packets[1].length); + pic.parsePacket(p); + + assertThat(pic.phReading, equalTo(7.39)); + assertThat(pic.orpReading, equalTo(687)); + assertThat(pic.phSetPoint, equalTo(7.50)); + assertThat(pic.orpSetPoint, equalTo(700)); + assertThat(pic.tank1Level, equalTo(6)); + assertThat(pic.tank2Level, equalTo(5)); + assertThat(pic.calciumHardness, equalTo(400)); + assertThat(pic.cyaReading, equalTo(0)); + assertThat(pic.alkalinity, equalTo(150)); + // assertThat(pic.alarmWaterFlow, equalTo(false)); + assertThat(pic.lsi, equalTo(0.24)); + assertThat(pic.phDoserType, equalTo(PhDoserType.ACID)); + assertThat(pic.orpDoserType, equalTo(OrpDoserType.ORP)); + assertThat(pic.phDoserStatus, equalTo(DosingStatus.MONITORING)); + assertThat(pic.orpDoserStatus, equalTo(DosingStatus.MIXING)); + assertThat(pic.phDoseTime, equalTo(2)); + assertThat(pic.orpDoseTime, equalTo(42)); + assertThat(pic.saltLevel, equalTo(1000)); + assertThat(pic.calcCalciumHardnessFactor(), equalTo(2.2)); + + assertThat(pic.alarmWaterFlow, equalTo(false)); + assertThat(pic.alarmPh, equalTo(false)); + assertThat(pic.alarmOrp, equalTo(false)); + assertThat(pic.alarmPhTank, equalTo(false)); + assertThat(pic.alarmOrpTank, equalTo(false)); + assertThat(pic.alarmProbeFault, equalTo(false)); + + assertThat(pic.warningPhLockout, equalTo(false)); + assertThat(pic.warningPhDailyLimitReached, equalTo(false)); + assertThat(pic.warningOrpDailyLimitReached, equalTo(false)); + assertThat(pic.warningInvalidSetup, equalTo(false)); + assertThat(pic.warningChlorinatorCommError, equalTo(false)); + assertThat(pic.firmwareVersion, equalTo("1.060")); + + p = new PentairStandardPacket(packets[2], packets[2].length); + pic.parsePacket(p); + + assertThat(pic.phReading, equalTo(7.4)); + assertThat(pic.orpReading, equalTo(770)); + assertThat(pic.phSetPoint, equalTo(7.4)); + assertThat(pic.orpSetPoint, equalTo(700)); + assertThat(pic.tank1Level, equalTo(6)); + assertThat(pic.tank2Level, equalTo(6)); + assertThat(pic.calciumHardness, equalTo(250)); + assertThat(pic.cyaReading, equalTo(44)); + assertThat(pic.alkalinity, equalTo(160)); + assertThat(pic.lsi, equalTo(0.03)); + // assertThat(pic.alarmWaterFlow, equalTo(false)); + assertThat(pic.phDoserType, equalTo(PhDoserType.ACID)); + assertThat(pic.orpDoserType, equalTo(OrpDoserType.ORP)); + assertThat(pic.phDoserStatus, equalTo(DosingStatus.MIXING)); + assertThat(pic.orpDoserStatus, equalTo(DosingStatus.MONITORING)); + assertThat(pic.phDoseTime, equalTo(16)); + assertThat(pic.orpDoseTime, equalTo(0)); + assertThat(pic.saltLevel, equalTo(1000)); + assertThat(pic.calcCalciumHardnessFactor(), equalTo(2.0)); + + assertThat(pic.alarmWaterFlow, equalTo(false)); + assertThat(pic.alarmPh, equalTo(false)); + assertThat(pic.alarmOrp, equalTo(true)); + assertThat(pic.alarmPhTank, equalTo(false)); + assertThat(pic.alarmOrpTank, equalTo(false)); + assertThat(pic.alarmProbeFault, equalTo(false)); + + assertThat(pic.warningPhLockout, equalTo(false)); + assertThat(pic.warningPhDailyLimitReached, equalTo(false)); + assertThat(pic.warningOrpDailyLimitReached, equalTo(false)); + assertThat(pic.warningInvalidSetup, equalTo(false)); + assertThat(pic.warningChlorinatorCommError, equalTo(false)); + assertThat(pic.firmwareVersion, equalTo("1.080")); + + p = new PentairStandardPacket(packets[3], packets[3].length); + pic.parsePacket(p); + + assertThat(pic.phReading, equalTo(7.55)); + assertThat(pic.orpReading, equalTo(773)); + assertThat(pic.phSetPoint, equalTo(7.6)); + assertThat(pic.orpSetPoint, equalTo(750)); + assertThat(pic.tank1Level, equalTo(0)); + assertThat(pic.tank2Level, equalTo(0)); + assertThat(pic.calciumHardness, equalTo(450)); + assertThat(pic.cyaReading, equalTo(80)); + assertThat(pic.alkalinity, equalTo(70)); + assertThat(pic.lsi, equalTo(-0.03)); + // assertThat(pic.alarmWaterFlow, equalTo(false)); + assertThat(pic.phDoserType, equalTo(PhDoserType.CO2)); + assertThat(pic.orpDoserType, equalTo(OrpDoserType.NONE)); + assertThat(pic.phDoserStatus, equalTo(DosingStatus.MONITORING)); + assertThat(pic.orpDoserStatus, equalTo(DosingStatus.NONE)); + assertThat(pic.phDoseTime, equalTo(121)); + assertThat(pic.orpDoseTime, equalTo(0)); + assertThat(pic.saltLevel, equalTo(4100)); + assertThat(pic.calcCalciumHardnessFactor(), equalTo(2.5)); + + assertThat(pic.alarmWaterFlow, equalTo(false)); + assertThat(pic.alarmPh, equalTo(false)); + assertThat(pic.alarmOrp, equalTo(true)); + assertThat(pic.alarmPhTank, equalTo(false)); + assertThat(pic.alarmOrpTank, equalTo(false)); + assertThat(pic.alarmProbeFault, equalTo(false)); + + assertThat(pic.warningPhLockout, equalTo(false)); + assertThat(pic.warningPhDailyLimitReached, equalTo(false)); + assertThat(pic.warningOrpDailyLimitReached, equalTo(false)); + assertThat(pic.warningInvalidSetup, equalTo(false)); + assertThat(pic.warningChlorinatorCommError, equalTo(false)); + assertThat(pic.firmwareVersion, equalTo("1.080")); + + p = new PentairStandardPacket(packets[4], packets[4].length); + pic.parsePacket(p); + + assertThat(pic.phReading, equalTo(7.46)); + assertThat(pic.orpReading, equalTo(732)); + assertThat(pic.phSetPoint, equalTo(7.6)); + assertThat(pic.orpSetPoint, equalTo(650)); + assertThat(pic.tank1Level, equalTo(0)); + assertThat(pic.tank2Level, equalTo(0)); + assertThat(pic.calciumHardness, equalTo(350)); + assertThat(pic.cyaReading, equalTo(0)); + assertThat(pic.alkalinity, equalTo(70)); + assertThat(pic.lsi, equalTo(-0.12)); + // assertThat(pic.alarmWaterFlow, equalTo(false)); + assertThat(pic.phDoserType, equalTo(PhDoserType.CO2)); + assertThat(pic.orpDoserType, equalTo(OrpDoserType.NONE)); + assertThat(pic.phDoserStatus, equalTo(DosingStatus.MONITORING)); + assertThat(pic.orpDoserStatus, equalTo(DosingStatus.NONE)); + assertThat(pic.phDoseTime, equalTo(8)); + assertThat(pic.orpDoseTime, equalTo(0)); + assertThat(pic.saltLevel, equalTo(4100)); + assertThat(pic.calcCalciumHardnessFactor(), equalTo(2.2)); + + assertThat(pic.alarmWaterFlow, equalTo(false)); + assertThat(pic.alarmPh, equalTo(false)); + assertThat(pic.alarmOrp, equalTo(false)); + assertThat(pic.alarmPhTank, equalTo(false)); + assertThat(pic.alarmOrpTank, equalTo(false)); + assertThat(pic.alarmProbeFault, equalTo(false)); + + assertThat(pic.warningPhLockout, equalTo(false)); + assertThat(pic.warningPhDailyLimitReached, equalTo(false)); + assertThat(pic.warningOrpDailyLimitReached, equalTo(false)); + assertThat(pic.warningInvalidSetup, equalTo(false)); + assertThat(pic.warningChlorinatorCommError, equalTo(false)); + assertThat(pic.firmwareVersion, equalTo("1.080")); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairParserTest.java b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairParserTest.java new file mode 100644 index 00000000000..87c000f28be --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairParserTest.java @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.*; +import static org.openhab.binding.pentair.internal.TestUtilities.parsehex; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.pentair.internal.parser.PentairIntelliChlorPacket; +import org.openhab.binding.pentair.internal.parser.PentairParser; +import org.openhab.binding.pentair.internal.parser.PentairParser.CallbackPentairParser; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * PentairParserTest + * + * @author Jeff James - Initial contribution + * + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +class PentairParserTest { + private final Logger logger = LoggerFactory.getLogger(PentairParserTest.class); + + //@formatter:off + public static byte[] stream = parsehex( + "FF 00 FF A5 1E 0F 10 02 1D 09 1F 00 00 00 00 00 00 00 20 03 00 00 04 3F 3F 00 00 41 3C 00 00 07 00 00 6A B6 00 0D 03 7F" + + "FF 00 FF A5 10 0F 10 12 29 02 E3 02 AF 02 EE 02 BC 00 00 00 02 00 00 00 2A 00 04 00 5C 06 05 18 01 90 00 00 00 96 14 00 51 00 00 65 20 3C 01 00 00 00 07 50 " + + "FF 00 FF A5 01 0F 10 02 1D 0D 1D 20 00 00 00 00 00 00 00 33 00 00 04 4D 4D 00 00 51 6D 00 00 07 00 00 5E D5 00 0D 04 04"); + //@formatter:on + + PentairParser parser = new PentairParser(); + + @Mock + @NonNullByDefault({}) + CallbackPentairParser callback; + + @Captor + @NonNullByDefault({}) + ArgumentCaptor packetsStandard; + + @Captor + @NonNullByDefault({}) + ArgumentCaptor packetsIntellichlor; + + @NonNullByDefault({}) + Thread thread; + + @BeforeEach + public void setUp() throws Exception { + parser.setCallback(callback); + } + + @AfterEach + public void tearDown() throws Exception { + if (thread != null) { + thread.interrupt(); + thread.join(); + } + thread = null; + } + + @Test + public void test() throws InterruptedException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(stream, 0, stream.length); + + parser.setInputStream(inputStream); + + thread = new Thread(parser); + thread.start(); + + Thread.sleep(2000); + + thread.interrupt(); + + thread.join(); + thread = null; + + verify(callback, times(3)).onPentairPacket(packetsStandard.capture()); + + List allPackets = new ArrayList(); + allPackets = packetsStandard.getAllValues(); + + assertThat(allPackets.size(), equalTo(3)); + + logger.info("1: {}", allPackets.get(0).getByte(PentairStandardPacket.ACTION)); + logger.info("2: {}", allPackets.get(1).getByte(PentairStandardPacket.ACTION)); + logger.info("3: {}", allPackets.get(2).getByte(PentairStandardPacket.ACTION)); + + assertThat(allPackets.get(0).getByte(PentairStandardPacket.ACTION), equalTo(0x02)); + + assertThat(allPackets.get(1).getByte(PentairStandardPacket.ACTION), equalTo(0x12)); + + assertThat(allPackets.get(2).getByte(PentairStandardPacket.ACTION), equalTo(0x02)); + } + + @Test + public void testNodeJSCapture() throws InterruptedException, IOException { + byte[] array = parsehex(Files.readAllBytes(Paths.get("src/test/data/nodejs-capture.dat"))); + + logger.info("testNodeJSCapture"); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(array, 0, array.length); + + parser.setInputStream(inputStream); + + thread = new Thread(parser); + thread.start(); + + Thread.sleep(2000); + + thread.interrupt(); + + thread.join(); + thread = null; + + verify(callback, atLeast(1)).onPentairPacket(packetsStandard.capture()); + verify(callback, atLeast(1)).onIntelliChlorPacket(packetsIntellichlor.capture()); + + List allPackets = new ArrayList(); + allPackets = packetsStandard.getAllValues(); + + logger.info("Number of Pentair packets: {}", allPackets.size()); + + assertThat(allPackets.size(), equalTo(281)); + + List allPacketsIntellichlor = new ArrayList(); + allPacketsIntellichlor = packetsIntellichlor.getAllValues(); + + logger.info("Number of Intellichlor packets: {}", allPacketsIntellichlor.size()); + + assertThat(allPacketsIntellichlor.size(), equalTo(1)); + } + + @Test + public void parseEasyTouch8() throws IOException, InterruptedException { + byte[] array = parsehex(Files.readAllBytes(Paths.get("src/test/data/easytouch8.dat"))); + + logger.info("parseEasyTouch8"); + + // logger.debug("{}", javax.xml.bind.DatatypeConverter.printHexBinary(array)); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(array, 0, array.length); + parser.setInputStream(inputStream); + + thread = new Thread(parser); + thread.start(); + + Thread.sleep(2000); + + thread.interrupt(); + + thread.join(); + thread = null; + + verify(callback, atLeast(1)).onPentairPacket(packetsStandard.capture()); + verify(callback, atLeast(1)).onIntelliChlorPacket(packetsIntellichlor.capture()); + + List allPackets = new ArrayList(); + allPackets = packetsStandard.getAllValues(); + + logger.info("Number of Pentair packets: {}", allPackets.size()); + + assertThat(allPackets.size(), equalTo(1032)); + + List allPacketsIntellichlor = new ArrayList(); + allPacketsIntellichlor = packetsIntellichlor.getAllValues(); + + logger.info("Number of Intellichlor packets: {}", allPacketsIntellichlor.size()); + + assertThat(allPacketsIntellichlor.size(), equalTo(36)); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairPumpStatusTest.java b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairPumpStatusTest.java new file mode 100644 index 00000000000..cd274187442 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/PentairPumpStatusTest.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.openhab.binding.pentair.internal.TestUtilities.parsehex; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.pentair.internal.handler.helpers.PentairPumpStatus; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairPumpStatusTest} + * + * @author Jeff James - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class PentairPumpStatusTest { + private final Logger logger = LoggerFactory.getLogger(PentairPumpStatus.class); + + //@formatter:off + public static byte[][] packets = { + parsehex("A5 00 22 60 07 0F 0A 02 02 00 E7 06 D6 00 00 00 00 00 01 02 03"), + parsehex("A5 00 22 60 07 0F 0A 00 00 01 F9 07 D5 00 00 00 00 09 21 0A 3A"), // SVRS alarm + parsehex("a5 00 10 60 07 0f 0a 02 02 00 5a 02 ee 00 00 00 00 00 01 15 1f"), + parsehex("A5 00 10 60 07 0F 04 00 00 00 00 00 00 00 00 00 00 00 00 14 1E") + }; + //@formatter:on + + @BeforeAll + public static void setUpBeforeClass() throws Exception { + } + + @AfterAll + public static void tearDownAfterClass() throws Exception { + } + + @BeforeEach + public void setUp() throws Exception { + } + + @AfterEach + public void tearDown() throws Exception { + } + + @Test + public void test() { + PentairPumpStatus ps = new PentairPumpStatus(); + + PentairStandardPacket p = new PentairStandardPacket(packets[0], packets[0].length); + ps.parsePacket(p); + logger.debug(ps.toString()); + + assertThat(ps.run, equalTo(true)); + assertThat(ps.mode, equalTo(2)); + assertThat(ps.power, equalTo(231)); + assertThat(ps.rpm, equalTo(1750)); + + p = new PentairStandardPacket(packets[1], packets[1].length); + ps.parsePacket(p); + logger.debug(ps.toString()); + assertThat(ps.run, equalTo(true)); + assertThat(ps.mode, equalTo(0)); + assertThat(ps.power, equalTo(505)); + assertThat(ps.rpm, equalTo(2005)); + + p = new PentairStandardPacket(packets[2], packets[2].length); + ps.parsePacket(p); + logger.debug(ps.toString()); + + p = new PentairStandardPacket(packets[3], packets[3].length); + ps.parsePacket(p); + logger.debug(ps.toString()); + assertThat(ps.run, equalTo(false)); + assertThat(ps.mode, equalTo(0)); + assertThat(ps.power, equalTo(0)); + assertThat(ps.rpm, equalTo(0)); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/TestUtilities.java b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/TestUtilities.java new file mode 100644 index 00000000000..3c701e19481 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/TestUtilities.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * TestUtilities + * + * @author Jeff James - Initial contribution + * + */ +@NonNullByDefault +public class TestUtilities { + + public static byte[] parsehex(String in) { + String out = in.replaceAll("\\s", ""); + + return javax.xml.bind.DatatypeConverter.parseHexBinary(out); + } + + private static int hexToBin(byte in) { + if ('0' <= in && in <= '9') { + return in - '0'; + } + if ('A' <= in && in <= 'F') { + return in - 'A' + 10; + } + if ('a' <= in && in <= 'f') { + return in - 'a' + 10; + } + return -1; + } + + public static byte[] parsehex(byte[] in) { + byte[] out = new byte[in.length / 2 + 1]; + + int i = 0; + int length = 0; + while (i < in.length) { + int h = hexToBin(in[i]); + i++; + if (h == -1) { + continue; + } + + if (i >= in.length) { + break; + } + int l = hexToBin(in[i]); + i++; + if (l == -1) { + continue; + } + + out[length++] = (byte) (h * 16 + l); + } + return out; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairControllerHandlerTest.java b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairControllerHandlerTest.java new file mode 100644 index 00000000000..06e0c2582de --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairControllerHandlerTest.java @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; +import static org.openhab.binding.pentair.internal.TestUtilities.parsehex; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.ImperialUnits; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelGroupUID; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentairControllerHandlerTest } + * + * @author Jeff James - Initial contribution + */ + +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class PentairControllerHandlerTest { + @SuppressWarnings("unused") + private final Logger logger = LoggerFactory.getLogger(PentairControllerHandlerTest.class); + + //@formatter:off + public static byte[][] packets = { + parsehex("A5 1E 0F 10 02 1D 09 20 21 00 00 00 00 00 00 20 0F 00 00 04 3F 3F 00 00 41 3C 00 00 07 00 00 6A B6 00 0D"), + parsehex("A5 24 0f 10 02 1d 08 3b 00 01 00 00 00 00 00 20 00 00 00 04 4a 4a 00 00 44 00 00 00 04 00 00 7c e6 00 0d"), + parsehex("a5 24 0f 10 02 1d 09 04 00 31 00 00 00 00 00 20 00 00 00 04 4a 4a 00 00 45 00 00 00 04 00 07 ce 60 00 0d"), + parsehex("A5 1E 0F 10 02 1D 0A 0B 00 00 00 00 00 00 00 21 33 00 00 04 45 45 00 00 3F 3F 00 00 07 00 00 D9 89 00 0D") + }; + //@formatter:on + + public class NullOutputStream extends OutputStream { + @Override + public void write(int b) throws IOException { + } + } + + @Mock + private @NonNullByDefault({}) Bridge bridgeMock; + + @Mock + private @NonNullByDefault({}) ThingHandlerCallback callback; + + @Mock + private @NonNullByDefault({}) Thing thing; + + @Mock + private @NonNullByDefault({}) PentairIPBridgeHandler pibh; + + private @NonNullByDefault({}) PentairControllerHandler handler; + private String uid = "1:2:3"; + private ThingUID thingUID = new ThingUID(uid); + + private List statusChannels = new ArrayList(); + private List spaCircuitChannels = new ArrayList(); + private List poolCircuitChannels = new ArrayList(); + + @BeforeAll + public static void setUpBeforeClass() throws Exception { + } + + @AfterAll + public static void tearDownAfterClass() throws Exception { + } + + @BeforeEach + public void setUp() throws Exception { + pibh = new PentairIPBridgeHandler(bridgeMock); + + OutputStream outputStream = new NullOutputStream(); + pibh.setOutputStream(outputStream); + + handler = new PentairControllerHandler(thing) { + @Override + public @NonNull PentairBaseBridgeHandler getBridgeHandler() { + return pibh; + } + }; + + ChannelGroupUID groupID = new ChannelGroupUID(thingUID, GROUP_CONTROLLER_STATUS); + + statusChannels.add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_AIRTEMPERATURE)).build()); + statusChannels.add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_SOLARTEMPERATURE)).build()); + statusChannels.add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_LIGHTMODE)).build()); + statusChannels.add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_SERVICEMODE)).build()); + statusChannels.add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_SOLARON)).build()); + statusChannels.add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_HEATERON)).build()); + statusChannels.add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_HEATERDELAY)).build()); + + groupID = new ChannelGroupUID(thingUID, GROUP_CONTROLLER_SPACIRCUIT); + + spaCircuitChannels + .add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_CIRCUITSWITCH)).build()); + spaCircuitChannels.add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_CIRCUITNAME)).build()); + spaCircuitChannels + .add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_CIRCUITFUNCTION)).build()); + + groupID = new ChannelGroupUID(thingUID, GROUP_CONTROLLER_POOLCIRCUIT); + + poolCircuitChannels + .add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_CIRCUITSWITCH)).build()); + poolCircuitChannels.add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_CIRCUITNAME)).build()); + poolCircuitChannels + .add(ChannelBuilder.create(new ChannelUID(groupID, CHANNEL_CONTROLLER_CIRCUITFUNCTION)).build()); + + // channels.add(ChannelBuilder.create(new ChannelUID(groupID, )).build()); + + when(thing.getConfiguration()).thenReturn(new Configuration(Collections.singletonMap("id", 0x10))); + when(thing.getHandler()).thenReturn(handler); + when(thing.getUID()).thenReturn(thingUID); + when(thing.getChannelsOfGroup(eq(GROUP_CONTROLLER_STATUS))).thenReturn(statusChannels); + when(thing.getChannelsOfGroup(eq(GROUP_CONTROLLER_SPACIRCUIT))).thenReturn(spaCircuitChannels); + when(thing.getChannelsOfGroup(eq(GROUP_CONTROLLER_POOLCIRCUIT))).thenReturn(poolCircuitChannels); + + handler.setCallback(callback); + } + + public List getChannelsOfGroup(String uid) { + return statusChannels; + } + + @AfterEach + public void tearDown() throws Exception { + handler.dispose(); + } + + @Test + @MockitoSettings(strictness = Strictness.LENIENT) + public void testPacketProcessing() { + handler.initialize(); + + verify(callback, times(1)).statusUpdated(eq(thing), + argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + + PentairStandardPacket p = new PentairStandardPacket(packets[0], packets[0].length); + handler.processPacketFrom(p); + verify(callback, times(1)).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE))); + ChannelUID cuid = new ChannelUID(thingUID, GROUP_CONTROLLER_SPACIRCUIT + "#switch"); + verify(callback, times(1)).stateUpdated(cuid, OnOffType.ON); + cuid = new ChannelUID(thingUID, GROUP_CONTROLLER_POOLCIRCUIT + "#switch"); + verify(callback, times(1)).stateUpdated(cuid, OnOffType.ON); + cuid = new ChannelUID(thingUID, GROUP_CONTROLLER_STATUS + "#" + CHANNEL_CONTROLLER_AIRTEMPERATURE); + verify(callback, times(1)).stateUpdated(cuid, new QuantityType<>(65, ImperialUnits.FAHRENHEIT)); + + Mockito.reset(callback); + + p = new PentairStandardPacket(packets[1], packets[1].length); + handler.processPacketFrom(p); + + Mockito.reset(callback); + + p = new PentairStandardPacket(packets[2], packets[2].length); + handler.processPacketFrom(p); + cuid = new ChannelUID(thingUID, GROUP_CONTROLLER_POOLCIRCUIT + "#switch"); + verify(callback, times(1)).stateUpdated(cuid, OnOffType.OFF); + cuid = new ChannelUID(thingUID, GROUP_CONTROLLER_FEATURE3 + "#switch"); + // verify(callback, times(1)).stateUpdated(cuid, OnOffType.ON); + cuid = new ChannelUID(thingUID, GROUP_CONTROLLER_FEATURE4 + "#switch"); + // verify(callback, times(1)).stateUpdated(cuid, OnOffType.ON); + cuid = new ChannelUID(thingUID, GROUP_CONTROLLER_STATUS + "#" + CHANNEL_CONTROLLER_AIRTEMPERATURE); + verify(callback, times(1)).stateUpdated(cuid, new QuantityType<>(69, ImperialUnits.FAHRENHEIT)); + + p = new PentairStandardPacket(packets[3], packets[3].length); + handler.processPacketFrom(p); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChemHandlerTest.java b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChemHandlerTest.java new file mode 100644 index 00000000000..6f62921a8a8 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChemHandlerTest.java @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; +import static org.openhab.binding.pentair.internal.TestUtilities.parsehex; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ChannelBuilder; + +/** + * PentairIntelliChemHandlerTest + * + * @author Jeff James - Initial contribution + * + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class PentairIntelliChemHandlerTest { + + //@formatter:off + public static byte[][] packets = { + parsehex("A50010901229030202A302D002C60000000000000000000000000006070000C8003F005A3C00580006A5201E01000000"), + parsehex("A5100F10122902E302AF02EE02BC000000020000002A0004005C060518019000000096140051000065203C0100000000") + }; + //@formatter:on + + private @NonNullByDefault({}) PentairIntelliChemHandler pich; + private String uid = "1:2:3"; + private ThingUID thingUID = new ThingUID(uid); + + private List channels = new ArrayList(); + + @Mock + private @NonNullByDefault({}) Bridge bridge; + + @Mock + private @NonNullByDefault({}) ThingHandlerCallback callback; + + @Mock + private @NonNullByDefault({}) Thing thing; + + @Mock + private @NonNullByDefault({}) PentairIPBridgeHandler pibh; + + @BeforeAll + public static void setUpBeforeClass() throws Exception { + } + + @AfterAll + public static void tearDownAfterClass() throws Exception { + } + + @BeforeEach + public void setUp() throws Exception { + pibh = new PentairIPBridgeHandler(bridge); + + pich = new PentairIntelliChemHandler(thing) { + @Override + public @NonNull PentairBaseBridgeHandler getBridgeHandler() { + return pibh; + } + }; + + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_PHREADING)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ORPREADING)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_PHSETPOINT)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ORPSETPOINT)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_TANK1LEVEL)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_TANK2LEVEL)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_CALCIUMHARDNESS)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_CYAREADING)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ALKALINITY)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_PHDOSERTYPE)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ORPDOSERTYPE)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_PHDOSERSTATUS)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ORPDOSERSTATUS)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_PHDOSETIME)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ORPDOSETIME)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_LSI)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_SALTLEVEL)).build()); + + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ALARMWATERFLOW)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ALARMPH)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ALARMORP)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ALARMPHTANK)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ALARMORPTANK)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ALARMPROBEFAULT)).build()); + + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_WARNINGPHLOCKOUT)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_WARNINGPHDAILYLIMITREACHED)) + .build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_WARNINGORPDAILYLIMITREACHED)) + .build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_WARNINGINVALIDSETUP)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHEM_WARNINGCHLORINATORCOMMERROR)) + .build()); + + when(thing.getConfiguration()).thenReturn(new Configuration(Collections.singletonMap("id", 144))); + when(thing.getHandler()).thenReturn(pich); + when(thing.getChannels()).thenReturn(channels); + pich.setCallback(callback); + } + + @AfterEach + public void tearDown() throws Exception { + pich.dispose(); + } + + @Test + public void test() { + pich.initialize(); + + PentairStandardPacket p = new PentairStandardPacket(packets[0], packets[0].length); + + verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + + pich.processPacketFrom(p); + + verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE))); + + ChannelUID cuid = new ChannelUID(thingUID, CHANNEL_INTELLICHEM_PHREADING); + verify(callback, times(1)).stateUpdated(cuid, new DecimalType(7.7)); + + cuid = new ChannelUID(thingUID, CHANNEL_INTELLICHEM_ORPREADING); + verify(callback, times(1)).stateUpdated(cuid, new DecimalType(675)); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChlorHandlerTest.java b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChlorHandlerTest.java new file mode 100644 index 00000000000..0ef7fde4449 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliChlorHandlerTest.java @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; +import static org.openhab.binding.pentair.internal.TestUtilities.parsehex; + +import java.util.ArrayList; +import java.util.List; + +import javax.measure.quantity.Dimensionless; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.pentair.internal.parser.PentairIntelliChlorPacket; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ChannelBuilder; + +/** + * PentairIntelliChloreHandlerTest + * + * @author Jeff James - Initial contribution + * + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class PentairIntelliChlorHandlerTest { + + //@formatter:off + public static byte[][] packets = { + parsehex("10 02 50 11 50"), + parsehex("10 02 00 12 67 80"), + parsehex("10 02 50 14 00"), + parsehex("10 02 50 11 00"), + parsehex("10 02 00 12 4C 81"), + parsehex("10 02 00 03 00 49 6E 74 65 6C 6C 69 63 68 6C 6F 72 2D 2D 34 30"), + parsehex("10 02 00 12 4C 81") + }; + //@formatter:on + + private @NonNullByDefault({}) PentairIntelliChlorHandler handler; + private String uid = "1:2:3"; + private ThingUID thingUID = new ThingUID(uid); + + @Mock + private @NonNullByDefault({}) Bridge bridge; + + @Mock + private @NonNullByDefault({}) ThingHandlerCallback callback; + + @Mock + private @NonNullByDefault({}) Thing thing; + + @Mock + private @NonNullByDefault({}) PentairIPBridgeHandler pibh; + + @BeforeAll + public static void setUpBeforeClass() throws Exception { + } + + @AfterAll + public static void tearDownAfterClass() throws Exception { + } + + @BeforeEach + public void setUp() throws Exception { + pibh = new PentairIPBridgeHandler(bridge); + + handler = new PentairIntelliChlorHandler(thing) { + @Override + public @NonNull PentairBaseBridgeHandler getBridgeHandler() { + return pibh; + } + }; + + List channels = new ArrayList(); + + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_PROPERTYVERSION)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_PROPERTYMODEL)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_SALTOUTPUT)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_SALINITY)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_OK)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_LOWFLOW)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_LOWSALT)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_VERYLOWSALT)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_HIGHCURRENT)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_CLEANCELL)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_LOWVOLTAGE)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_LOWWATERTEMP)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLICHLOR_COMMERROR)).build()); + + when(thing.getConfiguration()).thenReturn(new Configuration()); + when(thing.getUID()).thenReturn(thingUID); + when(thing.getHandler()).thenReturn(handler); + when(thing.getChannels()).thenReturn(channels); + + handler.setCallback(callback); + } + + @AfterEach + public void tearDown() throws Exception { + handler.dispose(); + } + + @Test + public void test() { + handler.initialize(); + + verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + assertThat(handler.getPentairID(), equalTo(0)); + + PentairIntelliChlorPacket p = new PentairIntelliChlorPacket(packets[0], packets[0].length); + handler.processPacketFrom(p); + ChannelUID cuid = new ChannelUID(new ThingUID("1:2:3"), CHANNEL_INTELLICHLOR_SALTOUTPUT); + verify(callback, times(1)).stateUpdated(cuid, new QuantityType(80, Units.PERCENT)); + + p = new PentairIntelliChlorPacket(packets[1], packets[1].length); + handler.processPacketFrom(p); + + // Won't actually go ONLINE until a packet FROM the intellichlor + verify(callback, times(1)).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE))); + cuid = new ChannelUID(new ThingUID("1:2:3"), CHANNEL_INTELLICHLOR_SALINITY); + verify(callback, times(1)).stateUpdated(cuid, new QuantityType(5150, Units.PARTS_PER_MILLION)); + cuid = new ChannelUID(new ThingUID("1:2:3"), CHANNEL_INTELLICHLOR_OK); + verify(callback, times(1)).stateUpdated(cuid, OnOffType.ON); + + p = new PentairIntelliChlorPacket(packets[2], packets[2].length); + handler.processPacketFrom(p); + + p = new PentairIntelliChlorPacket(packets[3], packets[3].length); + handler.processPacketFrom(p); + cuid = new ChannelUID(new ThingUID("1:2:3"), CHANNEL_INTELLICHLOR_SALTOUTPUT); + verify(callback, times(1)).stateUpdated(cuid, new QuantityType(0, Units.PERCENT)); + + p = new PentairIntelliChlorPacket(packets[4], packets[4].length); + handler.processPacketFrom(p); + cuid = new ChannelUID(new ThingUID("1:2:3"), CHANNEL_INTELLICHLOR_SALINITY); + verify(callback, times(1)).stateUpdated(cuid, new QuantityType(3800, Units.PARTS_PER_MILLION)); + cuid = new ChannelUID(new ThingUID("1:2:3"), CHANNEL_INTELLICHLOR_OK); + verify(callback, times(1)).stateUpdated(cuid, OnOffType.OFF); + + cuid = new ChannelUID(new ThingUID("1:2:3"), CHANNEL_INTELLICHLOR_LOWFLOW); + verify(callback, times(1)).stateUpdated(cuid, OnOffType.ON); + + p = new PentairIntelliChlorPacket(packets[5], packets[5].length); + handler.processPacketFrom(p); + assertThat(handler.version, equalTo(0)); + assertThat(handler.name, equalTo("Intellichlor--40")); + + p = new PentairIntelliChlorPacket(packets[6], packets[6].length); + handler.processPacketFrom(p); + assertThat(handler.version, equalTo(0)); + assertThat(handler.name, equalTo("Intellichlor--40")); + } +} diff --git a/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliFloHandlerTest.java b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliFloHandlerTest.java new file mode 100644 index 00000000000..fb57b87ec34 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/test/java/org/openhab/binding/pentair/internal/handler/PentairIntelliFloHandlerTest.java @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.pentair.internal.handler; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; +import static org.openhab.binding.pentair.internal.TestUtilities.parsehex; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.pentair.internal.parser.PentairStandardPacket; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PentailIntelliFlowHandlerTest } + * + * @author Jeff James - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +public class PentairIntelliFloHandlerTest { + @SuppressWarnings("unused") + private final Logger logger = LoggerFactory.getLogger(PentairIntelliFloHandlerTest.class); + + //@formatter:off + public static byte[][] packets = { + parsehex("A5 00 22 60 07 0F 0A 02 02 00 E7 06 D6 00 00 00 00 00 01 02 03"), + parsehex("A5 00 22 60 07 0F 0A 00 00 01 F9 07 D5 00 00 00 00 09 21 0A 3A"), // SVRS alarm + parsehex("a5 00 10 60 07 0f 0a 02 02 00 5a 02 ee 00 00 00 00 00 01 15 1f"), + parsehex("A5 00 10 60 07 0F 04 00 00 00 00 00 00 00 00 00 00 00 00 14 1E") + }; + //@formatter:on + + private @NonNullByDefault({}) PentairIntelliFloHandler handler; + private String uid = "1:2:3"; + private ThingUID thingUID = new ThingUID(uid); + + private List channels = new ArrayList(); + + @Mock + private @NonNullByDefault({}) Bridge bridge; + + @Mock + private @NonNullByDefault({}) ThingHandlerCallback callback; + + @Mock + private @NonNullByDefault({}) Thing thing; + + @Mock + private @NonNullByDefault({}) PentairIPBridgeHandler pibh; + + @BeforeAll + public static void setUpBeforeClass() throws Exception { + } + + @AfterAll + public static void tearDownAfterClass() throws Exception { + } + + @BeforeEach + public void setUp() throws Exception { + pibh = new PentairIPBridgeHandler(bridge); + + handler = new PentairIntelliFloHandler(thing) { + @Override + public @NonNull PentairBaseBridgeHandler getBridgeHandler() { + return pibh; + } + }; + + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLIFLO_RUN)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLIFLO_POWER)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, CHANNEL_INTELLIFLO_RPM)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, INTELLIFLO_GPM)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, INTELLIFLO_ERROR)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, INTELLIFLO_STATUS1)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, INTELLIFLO_STATUS2)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, INTELLIFLO_TIMER)).build()); + channels.add(ChannelBuilder.create(new ChannelUID(thingUID, INTELLIFLO_RUNPROGRAM)).build()); + + when(thing.getConfiguration()).thenReturn(new Configuration(Collections.singletonMap("id", 0x10))); + when(thing.getHandler()).thenReturn(handler); + when(thing.getChannels()).thenReturn(channels); + handler.setCallback(callback); + } + + @AfterEach + public void tearDown() throws Exception { + handler.dispose(); + } + + @Test + public void testPacketProcessing() { + ChannelUID cuid; + PentairStandardPacket p; + + handler.initialize(); + + verify(callback, times(1)).statusUpdated(eq(thing), + argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + + p = new PentairStandardPacket(packets[0], packets[0].length); + handler.processPacketFrom(p); + verify(callback, times(1)).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE))); + cuid = new ChannelUID(thingUID, CHANNEL_INTELLIFLO_RUN); + verify(callback, times(1)).stateUpdated(cuid, OnOffType.ON); + cuid = new ChannelUID(thingUID, CHANNEL_INTELLIFLO_POWER); + verify(callback, times(1)).stateUpdated(cuid, new QuantityType<>(231, Units.WATT)); + cuid = new ChannelUID(thingUID, CHANNEL_INTELLIFLO_RPM); + verify(callback, times(1)).stateUpdated(cuid, new DecimalType(1750)); + + Mockito.reset(callback); + + p = new PentairStandardPacket(packets[1], packets[1].length); + handler.processPacketFrom(p); + cuid = new ChannelUID(thingUID, CHANNEL_INTELLIFLO_RUN); + verify(callback, times(1)).stateUpdated(cuid, OnOffType.ON); + cuid = new ChannelUID(thingUID, CHANNEL_INTELLIFLO_POWER); + verify(callback, times(1)).stateUpdated(cuid, new QuantityType<>(505, Units.WATT)); + cuid = new ChannelUID(thingUID, CHANNEL_INTELLIFLO_RPM); + verify(callback, times(1)).stateUpdated(cuid, new DecimalType(2005)); + + Mockito.reset(callback); + + p = new PentairStandardPacket(packets[2], packets[2].length); + handler.processPacketFrom(p); + + Mockito.reset(callback); + + p = new PentairStandardPacket(packets[3], packets[3].length); + handler.processPacketFrom(p); + cuid = new ChannelUID(thingUID, CHANNEL_INTELLIFLO_RUN); + verify(callback, times(1)).stateUpdated(cuid, OnOffType.OFF); + cuid = new ChannelUID(thingUID, CHANNEL_INTELLIFLO_POWER); + verify(callback, times(1)).stateUpdated(cuid, new QuantityType<>(0, Units.WATT)); + cuid = new ChannelUID(thingUID, CHANNEL_INTELLIFLO_RPM); + verify(callback, times(1)).stateUpdated(cuid, new DecimalType(0)); + } +}