diff --git a/bundles/org.openhab.binding.pentair/README.md b/bundles/org.openhab.binding.pentair/README.md index 0ac03c18afd54..8e64f7e557c14 100644 --- a/bundles/org.openhab.binding.pentair/README.md +++ b/bundles/org.openhab.binding.pentair/README.md @@ -1,4 +1,4 @@ -# Pentair Pool +# 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). @@ -21,15 +21,12 @@ The binding includes 2 different bridge Things depending on which type of interf 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/openhab2, `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. #### USB/Serial interface @@ -42,7 +39,7 @@ 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 @@ -52,8 +49,8 @@ This binding supports the following thing types: | --------------- | :--------: | --------------------------------------- | | 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. | +| Controller | Thing | Pentair EasyTouch pool controller. | +| Intelliflo | Thing | Pentair Intelliflo variable speed pump. | | Intellichlor | Thing | Pentair Intellichlor chlorinator. | @@ -77,129 +74,175 @@ The following table shows the available configuration parameters for each thing. | | id - ID to use when communciating on Pentair control bus - default = 34. | Currently automatic discovery is not supported and the binding requires configuration via the Paper UI or a file in the conf/things folder. -Here is an example of a thing configuration file called 'pentair.thing': + +Here is an example of a thing configuration file called 'pentair.things' for using the ip_bridge: ``` 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 +} +``` + +For a serial bridge you would use a configuration similar to this, again saved as 'pentair.things': + +``` +Bridge pentair:serial_bridge:1 [ serialPort="/dev/ttyUSB0" ] { + controller main [ id=16 ] intelliflo pump1 [ id=96 ] intellichlor ic40 } ``` -## Channels - -Pentair things support a variety of channels as seen below in the following table: - -| 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) | - -## Full Example - -The following is an example of an item file (pentair.items): +## Things & Channels + +### Thing: Controller + +Represents and interfaces with a Pentair pool controller in the system. This binding should work for both Intellitouch and EasyTouch systems, however only the EasyTouch controllers have been tested. Feature availability is dependent on the version of hardware and firmware versions of your specific controller. + +#### Synchronize Time + +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 will automatically reprogram the controller clock when entering or leaving 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. | +| " | minsrun | Number | RW | Number of minutes circuit or feature has been on since binding start. Does not persist across restarts. | +| " | 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 | RW | Time of day to start schedule expressed in minutes. | +| " | end | Number | 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. | +| " | heatactive | Number | R | | +| " | uom | String | R | Unit of measure. Values: CELCIUS, FARENHEIT. | +| " | 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 | R | Current salt output %. | +| salinity | Number | R | Salinity (ppm). | + +### 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 | R | Pump GPM | +| power | Number:Power | R | Pump power (Watt) | +| error | Number | R | Pump error. +| program1 | Switch | RW | Run pump program 1 settings. | +| program2 | Switch | RW | Run pump program 2 settings. | +| program3 | Switch | RW | Run pump program 3 settings. | +| program4 | Switch | RW | Run pump program 4 settings. | + + +## Example setup + +### pentair.items ``` -Group gPool "Pool" - -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 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" } -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" } - -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" } - -Number SaltOutput "Salt Output [%d%%]" (gPool) { channel = "pentair:intellichlor:1:ic40:saltoutput" } -Number Salinity "Salinity [%d ppm]" (gPool) { channel = "pentair:intellichlor:1:ic40:salinity" } - -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" } + +Group gPool (All) + +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" } + +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 PoolSetPoint "Pool Set Point" (gPool) { channel="pentair:controller:1:main:poolheat#setpoint" } +Number SpaSetPoint "Spa Set Point" (gPool) { channel="pentair:controller:1:main:spaheat#setpoint" } + +String PoolLightMode "Light Mode [%s]" (gPool) { channel="pentair:controller:1:main:status#lightmode" } + +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 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" } + +Switch Valve1 "Valve 1" (gPool) { channel = "pentair:controller:1:main:valve1" } +Switch Valve2 "Valve 2" (gPool) { channel = "pentair:controller:1:main:valve2" } + +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_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" } ``` -Here is an example sitemap: +### sitemap ``` -Frame label="Pool" { - Switch item=Mode_Spa - Switch item=Mode_PoolLight - Switch item=Mode_SpaLight - Switch item=Mode_Jets - Text item=Pool_Temp valuecolor=[>82="red",>77="orange",<=77="blue"] - Text item=Spa_Temp valuecolor=[>97="red",>93="orange",<=93="blue"] - Setpoint item=SpaSetPoint minValue=85 maxValue=103 step=1.0 - Group item=gPool label="Advanced" +sitemap pool label="Pool stuff" { + Frame label="Pool" { + Switch item=Mode_Pool + 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=Spa_Temp valuecolor=[>82="red",>77="orange",<=77="blue"] + Setpoint item=SpaSetPoint minValue=85 maxValue=103 step=1.0 + } } ``` ## References Setting up RS485 and basic protocol - https://www.sdyoung.com/home/decoding-the-pentair-easytouch-rs-485-protocol/ -ser2sock GitHub - https://github.com/nutechsoftware/ser2sock +ser2sock GitHub - https://github.com/nutechsoftware/ser2sock +nodejs-poolController - https://github.com/tagyoureit/nodejs-poolController + +## Updates in 2.5.6 +Added automotic discovery of devices +EasyTouch thing has been renamed to a more generic Controller +Controller makes liberal use of channel groups to better organize channels +Added support for reading and writing Controller schedules +Added support for synchronizing the controller time +Added support for direct control of Intelliflo pumps +Added support for IntelliBrite color selection +Added support for UOM for temperature and pump power. +Improved robustness of communication on RS485 bus +Move serial implementation to openhab-transport-serial from gnu.io ## 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 efe3c32439ee1..0c6487126edaf 100644 --- a/bundles/org.openhab.binding.pentair/pom.xml +++ b/bundles/org.openhab.binding.pentair/pom.xml @@ -14,8 +14,4 @@ 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/MapUtils.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/MapUtils.java new file mode 100644 index 0000000000000..cc52b660178b6 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/MapUtils.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2020 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 java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PentairBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public final class MapUtils { + public static Map mapOf(Object... keyValues) { + Map map = new HashMap(keyValues.length / 2); + + for (int index = 0; index < keyValues.length / 2; index++) { + map.put((K) keyValues[index * 2], (V) keyValues[index * 2 + 1]); + } + + return map; + } + + public static Map invertMap(Map map) { + Map reverseMap = new HashMap<>(); + for (Map.Entry entry : map.entrySet()) { + reverseMap.put(entry.getValue(), entry.getKey()); + } + return reverseMap; + } +} 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 a1a5da62ebdcb..671151597d101 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 @@ -36,7 +36,7 @@ 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"; @@ -46,54 +46,89 @@ 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); - // 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 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"; - - public static final String EASYTOUCH_POOLSETPOINT = "poolsetpoint"; - public static final String EASYTOUCH_SPASETPOINT = "spasetpoint"; - - 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 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 PARAMETER_ID = "id"; + + // Controller Groups and Items + + public static final String CONTROLLER_PROPERTYFWVERSION = "fwversion"; + + public static final String CONTROLLER_STATUS = "status"; + + public static final String CONTROLLER_AIRTEMPERATURE = "airtemperature"; + public static final String CONTROLLER_SOLARTEMPERATURE = "solartemperature"; + public static final String CONTROLLER_LIGHTMODE = "lightmode"; + public static final String CONTROLLER_HEATACTIVE = "heatactive"; + public static final String CONTROLLER_UOM = "uom"; + public static final String CONTROLLER_SERVICEMODE = "servicemode"; + public static final String CONTROLLER_SOLARON = "solaron"; + public static final String CONTROLLER_HEATERON = "heateron"; + + public static final String CONTROLLER_POOLCIRCUIT = "pool"; + public static final String CONTROLLER_SPACIRCUIT = "spa"; + public static final String CONTROLLER_AUX1CIRCUIT = "aux1"; + public static final String CONTROLLER_AUX2CIRCUIT = "aux2"; + public static final String CONTROLLER_AUX3CIRCUIT = "aux3"; + public static final String CONTROLLER_AUX4CIRCUIT = "aux4"; + public static final String CONTROLLER_AUX5CIRCUIT = "aux5"; + public static final String CONTROLLER_AUX6CIRCUIT = "aux6"; + public static final String CONTROLLER_AUX7CIRCUIT = "aux7"; + public static final String CONTROLLER_AUX8CIRCUIT = "aux8"; + + public static final String CONTROLLER_CIRCUITSWITCH = "switch"; + public static final String CONTROLLER_CIRCUITMINSRUN = "minsrun"; + public static final String CONTROLLER_CIRCUITNAME = "name"; + public static final String CONTROLLER_CIRCUITFUNCTION = "function"; + + public static final String CONTROLLER_FEATURE1 = "feature1"; + public static final String CONTROLLER_FEATURE2 = "feature2"; + public static final String CONTROLLER_FEATURE3 = "feature3"; + public static final String CONTROLLER_FEATURE4 = "feature4"; + public static final String CONTROLLER_FEATURE5 = "feature5"; + public static final String CONTROLLER_FEATURE6 = "feature6"; + public static final String CONTROLLER_FEATURE7 = "feature7"; + public static final String CONTROLLER_FEATURE8 = "feature8"; + + public static final String CONTROLLER_FEATURESWITCH = "switch"; + + // List of heat group and items + public static final String CONTROLLER_POOLHEAT = "poolheat"; + public static final String CONTROLLER_SPAHEAT = "spaheat"; + + public static final String CONTROLLER_TEMPERATURE = "temperature"; + public static final String CONTROLLER_SETPOINT = "setpoint"; + public static final String CONTROLLER_HEATMODE = "heatmode"; + + // List of schedule group and items + public static final String CONTROLLER_SCHEDULE = "schedule%d"; + + public static final String CONTROLLER_SCHEDULESAVE = "save"; + public static final String CONTROLLER_SCHEDULESTRING = "schedule"; + public static final String CONTROLLER_SCHEDULETYPE = "type"; + public static final String CONTROLLER_SCHEDULECIRCUIT = "circuit"; + public static final String CONTROLLER_SCHEDULEDAYS = "days"; + public static final String CONTROLLER_SCHEDULESTART = "start"; + public static final String CONTROLLER_SCHEDULEEND = "end"; + + // List of Intellichlor channel ids public static final String INTELLICHLOR_SALTOUTPUT = "saltoutput"; public static final String INTELLICHLOR_SALINITY = "salinity"; + // List of all Intelliflo channel ids 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 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_PROGRAM1 = "program1"; + public static final String INTELLIFLO_PROGRAM2 = "program2"; + public static final String INTELLIFLO_PROGRAM3 = "program3"; + public static final String INTELLIFLO_PROGRAM4 = "program4"; public static final String DIAG = "diag"; @@ -103,6 +138,9 @@ public class PentairBindingConstants { // 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, + .unmodifiableSet(Stream.of(IP_BRIDGE_THING_TYPE, SERIAL_BRIDGE_THING_TYPE, CONTROLLER_THING_TYPE, INTELLIFLO_THING_TYPE, INTELLICHLOR_THING_TYPE).collect(Collectors.toSet())); + + public static final Set DISCOVERABLE_DEVICE_TYPE_UIDS = Collections.unmodifiableSet(Stream + .of(CONTROLLER_THING_TYPE, INTELLIFLO_THING_TYPE, INTELLICHLOR_THING_TYPE).collect(Collectors.toSet())); } diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairControllerCircuit.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairControllerCircuit.java new file mode 100644 index 0000000000000..aa45043f0050c --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairControllerCircuit.java @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2010-2020 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.openhab.binding.pentair.internal.PentairBindingConstants.*; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class to manage Controller Circuits/Features + * + * @author Jeff James - initial contribution + * + */ +@NonNullByDefault +public class PentairControllerCircuit { + //@formatter:off + public static final Map CIRCUITNAME = MapUtils.mapOf( + 0, "NOT USED", + 1, "AERATOR", + 2, "AIR BLOWER", + 3, "AUX 1", + 4, "AUX 2", + 5, "AUX 3", + 6, "AUX 4", + 7, "AUX 5", + 8, "AUX 6", + 9, "AUX 7", + 10, "AUX 8", + 11, "AUX 9", + 12, "AUX 10", + 13, "BACKWASH", + 14, "BACK LIGHT", + 15, "BBQ LIGHT", + 16, "BEACH LIGHT", + 17, "BOOSTER PUMP", + 18, "BUG LIGHT", + 19, "CABANA LTS", + 20, "CHEM. FEEDER", + 21, "CHLORINATOR", + 22, "CLEANER", + 23, "COLOR WHEEL", + 24, "DECK LIGHT", + 25, "DRAIN LINE", + 26, "DRIVE LIGHT", + 27, "EDGE PUMP", + 28, "ENTRY LIGHT", + 29, "FAN", + 30, "FIBER OPTIC", + 31, "FIBER WORKS", + 32, "FILL LINE", + 33, "FLOOR CLNR", + 34, "FOGGER", + 35, "FOUNTAIN", + 36, "FOUNTAIN 1", + 37, "FOUNTAIN 2", + 38, "FOUNTAIN 3", + 39, "FOUNTAINS", + 40, "FRONT LIGHT", + 41, "GARDEN LTS", + 42, "GAZEBO LTS", + 43, "HIGH SPEED", + 44, "HI-TEMP", + 45, "HOUSE LIGHT", + 46, "JETS", + 47, "LIGHTS", + 48, "LOW SPEED", + 49, "LO-TEMP", + 50, "MALIBU LTS", + 51, "MIST", + 52, "MUSIC", + 53, "NOT USED", + 54, "OZONATOR", + 55, "PATH LIGHTS", + 56, "PATIO LTS", + 57, "PERIMETER L", + 58, "PG2000", + 59, "POND LIGHT", + 60, "POOL PUMP", + 61, "POOL", + 62, "POOL HIGH", + 63, "POOL LIGHT", + 64, "POOL LOW", + 65, "SAM", + 66, "POOL SAM 1", + 67, "POOL SAM 2", + 68, "POOL SAM 3", + 69, "SECURITY LT", + 70, "SLIDE", + 71, "SOLAR", + 72, "SPA", + 73, "SPA HIGH", + 74, "SPA LIGHT", + 75, "SPA LOW", + 76, "SPA SAL", + 77, "SPA SAM", + 78, "SPA WTRFLL", + 79, "SPILLWAY", + 80, "SPRINKLERS", + 81, "STREAM", + 82, "STATUE LT", + 83, "SWIM JETS", + 84, "WTR FEATURE", + 85, "WTR FEAT LT", + 86, "WATERFALL", + 87, "WATERFALL 1", + 88, "WATERFALL 2", + 89, "WATERFALL 3", + 90, "WHIRLPOOL", + 91, "WTRFL LGHT", + 92, "YARD LIGHT", + 93, "AUX EXTRA", + 94, "FEATURE 1", + 95, "FEATURE 2", + 96, "FEATURE 3", + 97, "FEATURE 4", + 98, "FEATURE 5", + 99, "FEATURE 6", + 100, "FEATURE 7", + 101, "FEATURE 8", + 200, "USERNAME-01", + 201, "USERNAME-02", + 202, "USERNAME-03", + 203, "USERNAME-04", + 204, "USERNAME-05", + 205, "USERNAME-06", + 206, "USERNAME-07", + 207, "USERNAME-08", + 208, "USERNAME-09", + 209, "USERNAME-10"); + + public static final Map CIRCUITFUNCTION = MapUtils.mapOf( + 0, "GENERIC", + 1, "SPA", + 2, "POOL", + 5, "MASTER CLEANER", + 7, "LIGHT", + 9, "SAM LIGHT", + 10, "SAL LIGHT", + 11, "PHOTON GEN", + 12, "COLOR WHEEL", + 13, "VALVES", + 14, "SPILLWAY", + 15, "FLOOR CLEANER", + 16, "INTELLIBRITE", + 17, "MAGICSTREAM", + 19, "NOT USED", + 64, "FREEZE PROTECTION ON"); + + public static final Map GROUPNAME = MapUtils.mapOf( + 1, CONTROLLER_SPACIRCUIT, + 2, CONTROLLER_AUX1CIRCUIT, + 3, CONTROLLER_AUX2CIRCUIT, + 4, CONTROLLER_AUX3CIRCUIT, + 5, CONTROLLER_AUX4CIRCUIT, + 6, CONTROLLER_POOLCIRCUIT, + 7, CONTROLLER_AUX5CIRCUIT, + 8, CONTROLLER_AUX6CIRCUIT, + 9, CONTROLLER_AUX7CIRCUIT, + 10, CONTROLLER_AUX8CIRCUIT, + 11, CONTROLLER_FEATURE1, + 12, CONTROLLER_FEATURE2, + 13, CONTROLLER_FEATURE3, + 14, CONTROLLER_FEATURE4, + 15, CONTROLLER_FEATURE5, + 16, CONTROLLER_FEATURE6, + 17, CONTROLLER_FEATURE7, + 18, CONTROLLER_FEATURE8 + ); + public static final Map GROUPNAME_INV = MapUtils.invertMap(GROUPNAME); + + //@formatter:on + + public int id; + public int name; + public int function; + + public boolean on; + public int minsrun; + + public PentairControllerCircuit(int i) { + id = i; + } + + public void setName(int n) { + name = n; + } + + public void setFunction(int f) { + function = f; + } + + public String getNameStr() { + return CIRCUITNAME.get(name); + } + + public String getFunctionStr() { + return CIRCUITFUNCTION.get(function); + } + + public String getGroup() { + return GROUPNAME.get(id); + } + + public int getMinsRun() { + return minsrun; + } + + public void setOnOROff(boolean b) { + on = b; + } +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairControllerConstants.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairControllerConstants.java new file mode 100644 index 0000000000000..481325fd33e19 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairControllerConstants.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2020 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 java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Constants used for the Controller class + * + * @author Jeff James - Initial contribution + */ +@NonNullByDefault +public class PentairControllerConstants { + + //@formatter:off + + public static final Map LIGHTMODES = MapUtils.mapOf( + 0, "OFF", + 1, "ON", + 128, "COLORSYNC", + 144, "COLORSWIM", + 160, "COLORSET", + 177, "PARTY", + 178, "ROMANCE", + 179, "CARIBBEAN", + 180, "AMERICAN", + 181, "SUNSET", + 182, "ROYAL", + 193, "BLUE", + 194, "GREEN", + 195, "RED", + 196, "WHITE", + 197, "MAGENTA"); + public static final Map LIGHTMODES_INV = MapUtils.invertMap(LIGHTMODES); + + public static final Map HEATMODE = MapUtils.mapOf( + 0, "NONE", + 1, "HEATER", + 2, "SOLARPREFERRED", + 3, "SOLAR" + ); + public static final Map HEATMODE_INV = MapUtils.invertMap(HEATMODE); + + //@formatter:on +} diff --git a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairControllerSchedule.java b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairControllerSchedule.java new file mode 100644 index 0000000000000..b1b30ffff9628 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/PentairControllerSchedule.java @@ -0,0 +1,284 @@ +/** + * Copyright (c) 2010-2020 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 java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Class for the pentair controller schedules. + * + * @author Jeff James - initial contribution + * + */ +@NonNullByDefault +public class PentairControllerSchedule { + public static final int ID = 0; + public static final int CIRCUIT = 1; + public static final int STARTH = 2; + public static final int STARTM = 3; + public static final int ENDH = 4; + public static final int ENDM = 5; + public static final int DAYS = 6; + + public static final int SCHEDULETYPE_NONE = 0; + public static final int SCHEDULETYPE_NORMAL = 1; + public static final int SCHEDULETYPE_EGGTIMER = 2; + public static final int SCHEDULETYPE_ONCEONLY = 3; + + private boolean dirty; + + //@formatter:off + public static final Map SCHEDULETYPE = MapUtils.mapOf( + SCHEDULETYPE_NONE, "NONE", + SCHEDULETYPE_NORMAL, "NORMAL", + SCHEDULETYPE_EGGTIMER, "EGGTIMER", + SCHEDULETYPE_ONCEONLY, "ONCEONLY"); + public static final Map<@Nullable String, Integer> SCHEDULETYPE_INV = MapUtils.invertMap(SCHEDULETYPE); + //@formatter:on + + public int id; + public int circuit; + public int type; + + public int start; + public int end; + + public int days; + + public boolean isDirty() { + return dirty; + } + + public void setDirty(boolean d) { + dirty = d; + } + + public void parsePacket(PentairPacket p) { + id = p.getByte(ID); + circuit = p.getByte(CIRCUIT); + days = p.getByte(DAYS); + + if (p.getByte(STARTH) == 25) { + type = SCHEDULETYPE_EGGTIMER; + start = 0; + end = p.getByte(ENDH) * 60 + p.getByte(ENDM); + } else if (p.getByte(ENDH) == 26) { + type = SCHEDULETYPE_ONCEONLY; + start = p.getByte(STARTH) * 60 + p.getByte(STARTM); + end = 0; + } else if (circuit == 0) { + type = SCHEDULETYPE_NONE; + start = 0; + end = 0; + } else { + type = SCHEDULETYPE_NORMAL; + start = p.getByte(STARTH) * 60 + p.getByte(STARTM); + end = p.getByte(ENDH) * 60 + p.getByte(ENDM); + } + } + + @Nullable + public String getScheduleTypeStr() { + String str = SCHEDULETYPE.get(type); + + return str; + } + + public boolean setScheduleCircuit(int c) { + if (circuit == c) { + return true; + } + + if (c > 18 | c <= 0) { + return false; + } + + circuit = c; + dirty = true; + + return true; + } + + public boolean setScheduleStart(int min) { + if (min == start) { + return true; + } + + if (min > 1440 | min < 0) { + return false; + } + + start = min; + dirty = true; + + return true; + } + + public boolean setScheduleEnd(int min) { + if (min == end) { + return true; + } + + if (min > 1440 | min < 0) { + return false; + } + + end = min; + dirty = true; + + return true; + } + + public boolean setScheduleType(int t) { + if (t == type) { + return true; + } + + if (t < 0 | t > 3) { + return false; + } + + type = t; + dirty = true; + + return true; + } + + public boolean setScheduleType(String typestring) { + @Nullable + Integer type; + + type = SCHEDULETYPE_INV.get(typestring); + if (type == null) { + return false; + } + return setScheduleType(type); + } + + public boolean setDays(String d) { + 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 PentairPacket getWritePacket(int controllerid, int preamble) { + byte[] packet = { (byte) 0xA5, (byte) preamble, (byte) controllerid, (byte) 0x00 /* source */, (byte) 0x91, + (byte) 7, (byte) id, (byte) circuit, (byte) (start / 60), (byte) (start % 60), (byte) (end / 60), + (byte) (end % 60), (byte) days }; + PentairPacket p = new PentairPacket(packet); + + switch (type) { + case SCHEDULETYPE_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 SCHEDULETYPE_NORMAL: + break; + + case SCHEDULETYPE_ONCEONLY: + p.setByte(ENDH, (byte) 26); + p.setByte(ENDM, (byte) 0); + break; + case SCHEDULETYPE_EGGTIMER: + p.setByte(STARTH, (byte) 25); + p.setByte(STARTM, (byte) 0); + p.setByte(DAYS, (byte) 0); + break; + } + + return p; + } + + public String getDays() { + 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(); + + Pattern ptn = Pattern + .compile("^(NONE|NORMAL|EGGTIMER|ONCEONLY),(\\d+),(\\d+):(\\d+),(\\d+):(\\d+),([SMTWRFY]+)"); + Matcher m = ptn.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; + } + + Integer t = SCHEDULETYPE_INV.get(m.group(1)); + if (t == null) { + return false; + } + if (!setScheduleType(t)) { + return false; + } + + dirty = true; + + return true; + } + +} 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/PentairHandlerFactory.java index da1e22200a7a0..df261845dc9f1 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/PentairHandlerFactory.java @@ -14,18 +14,32 @@ import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.discovery.DiscoveryService; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; -import org.openhab.binding.pentair.internal.handler.PentairEasyTouchHandler; +import org.eclipse.smarthome.io.transport.serial.SerialPortManager; +import org.openhab.binding.pentair.internal.handler.PentairBaseBridgeHandler; +import org.openhab.binding.pentair.internal.handler.PentairControllerHandler; import org.openhab.binding.pentair.internal.handler.PentairIPBridgeHandler; 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.osgi.framework.ServiceRegistration; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link PentairHandlerFactory} is responsible for creating things and thing @@ -33,23 +47,45 @@ * * @author Jeff James - Initial contribution */ + +@NonNullByDefault @Component(service = ThingHandlerFactory.class, configurationPid = "binding.pentair") public class PentairHandlerFactory extends BaseThingHandlerFactory { + + private final Logger logger = LoggerFactory.getLogger(PentairHandlerFactory.class); + 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); } + private final Map> discoveryServiceRegMap = new HashMap<>(); + // Marked as Nullable only to fix incorrect redundant null check complaints from null annotations + @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); + PentairIPBridgeHandler bridgeHandler = new PentairIPBridgeHandler((Bridge) thing); + + registerDiscoveryService(bridgeHandler); + return bridgeHandler; } else if (thingTypeUID.equals(SERIAL_BRIDGE_THING_TYPE)) { - return new PentairSerialBridgeHandler((Bridge) thing); - } else if (thingTypeUID.equals(EASYTOUCH_THING_TYPE)) { - return new PentairEasyTouchHandler(thing); + PentairSerialBridgeHandler bridgeHandler = new PentairSerialBridgeHandler((Bridge) thing, + serialPortManager); + + registerDiscoveryService(bridgeHandler); + return bridgeHandler; + } 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)) { @@ -58,4 +94,28 @@ protected ThingHandler createHandler(Thing thing) { return null; } + + @Override + protected synchronized void removeHandler(ThingHandler thingHandler) { + if (thingHandler instanceof PentairBaseBridgeHandler) { + ServiceRegistration serviceReg = discoveryServiceRegMap.remove(thingHandler.getThing().getUID()); + if (serviceReg != null) { + logger.debug("Unregistering discovery service."); + serviceReg.unregister(); + } + } + } + + /** + * Register a discovery service for a bridge handler. + * + * @param bridgeHandler bridge handler for which to register the discovery service + */ + private synchronized void registerDiscoveryService(PentairBaseBridgeHandler bridgeHandler) { + logger.debug("Registering discovery service."); + PentairDiscoveryService discoveryService = new PentairDiscoveryService(bridgeHandler); + bridgeHandler.setDiscoveryService(discoveryService); + discoveryServiceRegMap.put(bridgeHandler.getThing().getUID(), + bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, null)); + } } 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 index 59b37e7f4ef5f..ed80d45a56d5d 100644 --- 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 @@ -12,6 +12,8 @@ */ package org.openhab.binding.pentair.internal; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Generic class for the standard pentair package protocol. Includes helpers to generate checksum and extract key bytes * from packet. @@ -19,6 +21,7 @@ * @author Jeff James - initial contribution * */ +@NonNullByDefault public class PentairPacket { protected static final char[] HEXARRAY = "0123456789ABCDEF".toCharArray(); @@ -93,7 +96,7 @@ public void setLength(int length) { * @return action byte of packet */ public int getAction() { - return buf[ACTION]; + return buf[ACTION] & 0xFF; // need to convert to unsigned value } /** @@ -141,6 +144,39 @@ public void setDest(int dest) { buf[DEST] = (byte) dest; } + /** + * Gets the preamble byte of packet + */ + public int getPreambleByte() { + return buf[1] & 0xFF; + } + + /** + * Gets a particular byte of packet + * + * @param number of byte in packet + */ + public int getByte(int num) { + int num2; + + num2 = STARTOFDATA + num; + if (num2 > buf.length) { + return -1; + } + return buf[num2] & 0xFF; + } + + public void setByte(int num, byte b) { + int num2; + + num2 = STARTOFDATA + num; + if (num2 > buf.length) { + return; + } + + buf[num2] = b; + } + /** * Helper function to convert byte to hex representation * @@ -190,16 +226,6 @@ 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. * 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 index ec677046e2f58..b113fd264b0d4 100644 --- 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 @@ -12,6 +12,8 @@ */ package org.openhab.binding.pentair.internal; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Pentair heat set point packet specialization of a PentairPacket. Includes public variables for many of the reverse * engineered @@ -20,6 +22,7 @@ * @author Jeff James - initial contribution * */ +@NonNullByDefault public class PentairPacketHeatSetPoint extends PentairPacket { protected static final int POOLTEMP = 5 + OFFSET; @@ -29,20 +32,14 @@ public class PentairPacketHeatSetPoint extends PentairPacket { 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 @@ -56,11 +53,9 @@ public PentairPacketHeatSetPoint(PentairPacket 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]; } /** 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 index 35b33f89d71b5..bd29e56ab2650 100644 --- 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 @@ -12,6 +12,8 @@ */ package org.openhab.binding.pentair.internal; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * 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 @@ -25,6 +27,7 @@ * @author Jeff James - initial contribution * */ +@NonNullByDefault 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 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 index e8816c673bcc4..e04fb42d4f9e9 100644 --- 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 @@ -12,6 +12,8 @@ */ package org.openhab.binding.pentair.internal; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Pentair pump status packet specialation of a PentairPacket. Includes public variables for many of the reverse * engineered packet content. @@ -19,21 +21,26 @@ * @author Jeff James - initial contribution * */ +@NonNullByDefault 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; + protected static final int RUN = STARTOFDATA; + protected static final int MODE = STARTOFDATA + 1; // Mode in pump status. Means something else in pump + // write/response? + protected static final int DRIVESTATE = STARTOFDATA + 2; // ?? Drivestate in pump status. Means something else in + // pump write/respoonse + protected static final int WATTSH = STARTOFDATA + 3; + protected static final int WATTSL = STARTOFDATA + 4; + protected static final int RPMH = STARTOFDATA + 5; + protected static final int RPML = STARTOFDATA + 6; + protected static final int GPM = STARTOFDATA + 7; + protected static final int PPC = STARTOFDATA + 8; // not sure what this is? always 0 + protected static final int B09 = STARTOFDATA + 9; + protected static final int ERR = STARTOFDATA + 10; + protected static final int STATUS11 = STARTOFDATA + 11; + protected static final int STATUS12 = STARTOFDATA + 12; + protected static final int HOUR = STARTOFDATA + 13; + protected static final int MIN = STARTOFDATA + 14; /** pump is running */ public boolean run; @@ -47,10 +54,13 @@ public class PentairPacketPumpStatus extends PentairPacket { // 15 byte packet f public int power; /** pump rpm */ public int rpm; - /** pump ppc? */ - public int ppc; + /** pump gpm */ + public int gpm; /** byte in packet indicating an error condition */ public int error; + /** byte in packet indicated status */ + public int status11; + public int status12; /** current timer for pump */ public int timer; /** hour or packet (based on Intelliflo time setting) */ @@ -71,11 +81,13 @@ public PentairPacketPumpStatus(PentairPacket 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]; + power = ((buf[WATTSH] & 0xFF) * 256) + (buf[WATTSL] & 0xFF); + rpm = ((buf[RPMH] & 0xFF) * 256) + (buf[RPML] & 0xFF); + gpm = buf[GPM]; + error = buf[ERR]; - timer = buf[TIMER]; + status11 = buf[STATUS11]; + status12 = buf[STATUS12]; hour = buf[HOUR]; min = buf[MIN]; } 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 index 2ba1240f620c9..53f3cf14b804e 100644 --- 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 @@ -12,6 +12,9 @@ */ package org.openhab.binding.pentair.internal; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * Pentair status packet specialation of a PentairPacket. Includes public variables for many of the reverse engineered * packet content. @@ -19,22 +22,27 @@ * @author Jeff James - initial contribution * */ +@NonNullByDefault 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; + protected static final int HOUR = STARTOFDATA; + protected static final int MIN = STARTOFDATA + 1; + protected static final int EQUIP1 = STARTOFDATA + 2; + protected static final int EQUIP2 = STARTOFDATA + 3; + protected static final int EQUIP3 = STARTOFDATA + 4; + protected static final int STATUS = STARTOFDATA + 9; // Celsius (0x04) or Farenheit, Service Mode (0x01) + protected static final int HEATACTIVE = STARTOFDATA + 10; // Heater (0x0C), Solar (0x30), Unknown (0x03) + protected static final int UNKNOWN = STARTOFDATA + 13; // Something to do with heat? + protected static final int POOL_TEMP = STARTOFDATA + 14; + protected static final int SPA_TEMP = STARTOFDATA + 15; + protected static final int AIR_TEMP = STARTOFDATA + 18; + protected static final int SOLAR_TEMP = STARTOFDATA + 19; + + // Heat mode defines + protected static final int HEATMODE_OFF = 0; + protected static final int HEATMODE_HEATER = 1; + protected static final int HEATMODE_SOLARPREF = 2; + protected static final int HEATMODE_SOLARONLY = 3; /** hour byte of packet */ public int hour; @@ -42,11 +50,16 @@ public class PentairPacketStatus extends PentairPacket { // 29 byte packet forma 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; + // public boolean pool, spa, aux1, aux2, aux3, aux4, aux5, aux6, aux7, aux8; + // public boolean feature1, feature2, feature3, feature4, feature5, feature6, feature7, feature8; + public boolean pool, spa; + public boolean[] circuits = new boolean[18]; /** Unit of Measure - Celsius = true, Farenheit = false */ public boolean uom; + public boolean servicemode; + public boolean heateron; + public boolean solaron; /** pool temperature */ public int pooltemp; @@ -61,8 +74,6 @@ public class PentairPacketStatus extends PentairPacket { // 29 byte packet forma 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; @@ -79,26 +90,18 @@ public PentairPacketStatus(PentairPacket 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; + + int equip = buf[EQUIP3] << 16 | buf[EQUIP2] << 8 | buf[EQUIP1]; + + for (int i = 0; i < 8; i++) { + circuits[i] = ((equip >> i) & 0x0001) == 1; + } + + uom = (buf[STATUS] & 0x04) != 0; + servicemode = (buf[STATUS] & 0x01) != 0; diag = buf[HEATACTIVE]; @@ -107,9 +110,8 @@ public PentairPacketStatus(PentairPacket p) { airtemp = buf[AIR_TEMP]; solartemp = buf[SOLAR_TEMP]; - spaheatmode = (buf[HEATMODE] >> 2) & 0x03; - poolheatmode = buf[HEATMODE] & 0x03; - heatactive = buf[HEATACTIVE]; + solaron = (buf[HEATACTIVE] & 0x30) != 0; + heateron = (buf[HEATACTIVE] & 0x0C) != 0; } /** @@ -118,4 +120,29 @@ public PentairPacketStatus(PentairPacket p) { public PentairPacketStatus() { super(); } + + @Override + public boolean equals(@Nullable Object object) { + if (!(object instanceof PentairPacketStatus)) { + return false; + } + + PentairPacketStatus p = (PentairPacketStatus) object; + + for (int i = 0; i < 18; i++) { + if (circuits[i] != p.circuits[i]) { + return false; + } + } + + if (pooltemp != p.pooltemp || spatemp != p.spatemp || airtemp != p.airtemp || solartemp != p.solartemp) { + return false; + } + + if (uom != p.uom || servicemode != p.servicemode || solaron != p.solaron || heateron != p.heateron) { + return false; + } + + return true; + } } 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 c2fce9790c075..f7f9ffdbbf16a 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,7 +12,7 @@ */ package org.openhab.binding.pentair.internal.config; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.eclipse.jdt.annotation.NonNullByDefault; /** * Configuration parameters for IP Bridge @@ -20,17 +20,14 @@ * @author Jeff James - initial contribution * */ +@NonNullByDefault public class PentairIPBridgeConfig { /** IP address of destination */ - public String address; + public String address = ""; /** Port of destination */ - public Integer port; - + public Integer port = 10000; /** ID to use when sending commands on the Pentair RS485 bus. */ - public Integer id; - - @Override - public String toString() { - return new ToStringBuilder(this).append("address", address).append("port", port).append("id", id).toString(); - } + public Integer id = 34; + /** enable automatic discovery */ + public boolean discovery = false; } 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 e4731a10fb886..059d93a57c366 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,7 +12,7 @@ */ package org.openhab.binding.pentair.internal.config; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.eclipse.jdt.annotation.NonNullByDefault; /** * Configuration parameters for Serial Bridge @@ -20,14 +20,12 @@ * @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; + public String serialPort = ""; /** ID to use when sending commands on the Pentair RS485 bus. */ - public Integer id; - - @Override - public String toString() { - return new ToStringBuilder(this).append("serialPort", serialPort).append("id", id).toString(); - } + public Integer id = 34; + /** Enable automotic discovery */ + public boolean discovery = false; } 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 dddd0722e558e..d80804b4211c1 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,8 +12,6 @@ */ package org.openhab.binding.pentair.internal.handler; -import static org.openhab.binding.pentair.internal.PentairBindingConstants.INTELLIFLO_THING_TYPE; - import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; @@ -21,7 +19,11 @@ import java.util.List; import java.util.concurrent.ScheduledFuture; 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.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; @@ -30,6 +32,7 @@ import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; +import org.openhab.binding.pentair.internal.PentairDiscoveryService; import org.openhab.binding.pentair.internal.PentairPacket; import org.openhab.binding.pentair.internal.PentairPacketIntellichlor; import org.slf4j.Logger; @@ -46,24 +49,40 @@ * @author Jeff James - Initial contribution * */ +@NonNullByDefault public abstract class PentairBaseBridgeHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(PentairBaseBridgeHandler.class); /** input stream - subclass needs to assign in connect function */ - protected BufferedInputStream reader; + protected @Nullable BufferedInputStream reader; /** output stream - subclass needs to assing in connect function */ - protected BufferedOutputStream writer; + + protected @Nullable BufferedOutputStream writer; /** thread for parser - subclass needs to create/assign connect */ - protected Thread thread; + protected @Nullable Thread thread; /** parser object - subclass needs to create/assign during connect */ - protected Parser parser; - /** polling job for pump status */ - protected ScheduledFuture pollingjob; + protected @Nullable Parser parser; + /** polling job for reconnecting */ + protected @Nullable 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<>(); + protected ConnectState connectstate = ConnectState.INIT; + + private ReentrantLock lock = new ReentrantLock(); + private Condition waitAck = lock.newCondition(); + private int ackResponse = -1; + + protected boolean discovery = false; + + protected @Nullable PentairDiscoveryService discoveryService; + + public void setDiscoveryService(PentairDiscoveryService discoveryService) { + this.discoveryService = discoveryService; + } + /** * Gets pentair bus id * @@ -73,11 +92,19 @@ public int getId() { return id; } + protected enum ConnectState { + CONNECTING, + DISCONNECTED, + CONNECTED, + INIT, + CONFIGERROR + }; + private enum ParserState { WAIT_SOC, CMD_PENTAIR, CMD_INTELLICHLOR - } + }; /** * Constructor @@ -86,6 +113,7 @@ private enum ParserState { */ PentairBaseBridgeHandler(Bridge bridge) { super(bridge); + connectstate = ConnectState.INIT; } @Override @@ -99,35 +127,81 @@ public void handleCommand(ChannelUID channelUID, Command command) { public void initialize() { logger.debug("initializing Pentair Bridge handler."); - connect(); + _connect(); - pollingjob = scheduler.scheduleWithFixedDelay(new PumpStatus(), 10, 120, TimeUnit.SECONDS); + return; } @Override public void dispose() { logger.debug("Handler disposed."); - pollingjob.cancel(true); - disconnect(); + + _disconnect(); + + if (pollingjob != null) { + pollingjob.cancel(true); + } } /** * Abstract method for creating connection. Must be implemented in subclass. + * Return 0 if all goes well. + * + * @throws Exception */ - protected abstract void connect(); + protected abstract int connect(); + + private int _connect() { + if (connectstate != ConnectState.DISCONNECTED && connectstate != ConnectState.INIT) { + logger.debug("_connect() without ConnectState == DISCONNECTED or INIT: {}", connectstate); + } + + connectstate = ConnectState.CONNECTING; + + if (connect() != 0) { + connectstate = ConnectState.CONFIGERROR; + + return -1; + } + + connectstate = ConnectState.CONNECTED; + + if (pollingjob == null) { + pollingjob = scheduler.scheduleWithFixedDelay(new ReconnectIO(), 60, 30, TimeUnit.SECONDS); + } + + return 0; + } /** * Abstract method for disconnect. Must be implemented in subclass */ protected abstract void disconnect(); + private void _disconnect() { + disconnect(); + + connectstate = ConnectState.DISCONNECTED; + } + + // Job to pull to try and reconnect upon being disconnected. Note this should only be started on an initial + class ReconnectIO implements Runnable { + @Override + public void run() { + logger.debug("ReconnectIO:run"); + if (connectstate == ConnectState.DISCONNECTED) { + _connect(); + } + } + } + /** * 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) { @@ -201,40 +275,6 @@ private int getBytes(byte[] buf, int start, int n) throws EOBException, IOExcept 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; - } - } - } - } - /** * 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. @@ -321,15 +361,30 @@ public void run() { 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); + byte source = (byte) p.getSource(); + if ((source >> 4) == 0x02) { // control panels are 0x2*, don't treat as an + // unregistered device + logger.debug("Command from control panel device ({}): {}", p.getSource(), p); } else if (!unregistered.contains(p.getSource())) { // if not yet seen, print out log // message once + if ((source >> 4) == 0x01) { // controller + if (PentairControllerHandler.onlineController == null) { // only register one + // controller + if (discovery) { + discoveryService.notifyDiscoveredController(source); + } + } + } else if ((source >> 4) == 0x06) { + if (discovery) { + discoveryService.notifyDiscoverdIntelliflo(source); + } + } + logger.info("Command from unregistered device ({}): {}", p.getSource(), p); unregistered.add(p.getSource()); + } else { - logger.trace("Command from unregistered device ({}): {}", p.getSource(), p); + logger.debug("Command from unregistered device ({}): {}", p.getSource(), p); } break; } @@ -340,9 +395,10 @@ public void run() { break; } - logger.trace("Received pentair command: {}", p); + logger.debug("Received pentair command: {}", p); thinghandler.processPacketFrom(p); + ackResponse(p.getAction()); break; case CMD_INTELLICHLOR: @@ -390,6 +446,9 @@ public void run() { if (thing == null) { if (!unregistered.contains(0)) { // if not yet seen, print out log message + if (discovery) { + discoveryService.notifyDiscoveredIntellichlor(0); + } logger.info("Command from unregistered Intelliflow: {}", pic); unregistered.add(0); } else { @@ -411,11 +470,14 @@ public void run() { } } } catch (IOException e) { - logger.trace("I/O error while reading from stream: {}", e.getMessage()); - disconnect(); + logger.debug("I/O error while reading from stream: {}", e.getMessage()); + _disconnect(); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); } catch (EOBException e) { + logger.debug("EOB Exception: {}", e.getMessage()); // EOB Exception is used to exit the parser loop if full message is not in buffer. + } catch (Exception e) { + logger.debug("Exception: {}", e.getMessage()); } logger.debug("msg reader thread exited"); @@ -428,10 +490,29 @@ public void run() { * @param p {@link PentairPacket} to write */ public void writePacket(PentairPacket p) { + writePacket(p, -1, 0); + } + + /** + * Method to acknowledge an ack or response packet has been sent + * + * @param response is the command that was seen as a return. This is validate against that this was the response + * before signally a return. + * @param retries is the number of retries if an ack is not seen before timeout + * @return true if writePacket was successful and if required saw a response + */ + public boolean writePacket(PentairPacket p, int response, int retries) { + boolean bReturn = true; + 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 + int nRetries = retries; + if (writer == null) { + logger.debug("writePacket: writer = null"); + return false; + } p.setSource(id); System.arraycopy(preamble, 0, buf, 0, 5); @@ -441,13 +522,56 @@ public void writePacket(PentairPacket p) { buf[p.getLength() + 11] = (byte) ((checksum >> 8) & 0xFF); buf[p.getLength() + 12] = (byte) (checksum & 0xFF); - logger.debug("Writing packet: {}", PentairPacket.bytesToHex(buf)); + lock.lock(); + ackResponse = response; - writer.write(buf, 0, 5 + p.getLength() + 8); - writer.flush(); + do { + logger.debug("Writing packet: {}", PentairPacket.bytesToHex(buf)); + + writer.write(buf, 0, 5 + p.getLength() + 8); + writer.flush(); + + if (response != -1) { + logger.trace("writePacket: wait for ack (response: {}, retries: {})", response, nRetries); + bReturn = waitAck.await(1000, TimeUnit.MILLISECONDS); // bReturn will be false if timeout + nRetries--; + } + } while ((bReturn != true) && (nRetries >= 0)); } catch (IOException e) { - logger.trace("I/O error while writing stream", e); + logger.debug("I/O error while writing stream: {}", e.getMessage()); + _disconnect(); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (InterruptedException e) { + logger.debug("writePacket: InterruptedException: {}", e.getMessage()); + } catch (Exception e) { + logger.debug("writePacket: Exception: {}", e.getMessage()); + } finally { + lock.unlock(); + } + + if (!bReturn) { + logger.debug("writePacket: timeout"); + } + return bReturn; + } + + /** + * 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; + } + + try { + lock.lock(); + waitAck.signalAll(); + lock.unlock(); + } catch (Exception e) { + logger.debug("Exception: {}", e.getMessage()); } } } 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 39456eee47c40..553d021140376 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,6 +12,8 @@ */ package org.openhab.binding.pentair.internal.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; import org.openhab.binding.pentair.internal.PentairPacket; @@ -22,7 +24,9 @@ * @author Jeff James - Initial contribution * */ +@NonNullByDefault public abstract class PentairBaseThingHandler extends BaseThingHandler { + /** ID of Thing on Pentair bus */ protected int id; @@ -39,8 +43,39 @@ public int getPentairID() { return id; } + public void writePacket(byte[] packet) { + writePacket(packet, -1, 0); + } + + public boolean writePacket(byte[] packet, int response, int retries) { + PentairPacket p = new PentairPacket(packet); + + return writePacket(p, response, retries); + } + + public boolean writePacket(PentairPacket p, int response, int retries) { + Bridge bridge = this.getBridge(); + if (bridge == null) { + return false; + } + + PentairBaseBridgeHandler bbh = (PentairBaseBridgeHandler) bridge.getHandler(); + if (bbh == null) { + return false; + } + + return bbh.writePacket(p, response, retries); + } + + public void delay300() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + } + /** - * Abstract function to be implemented by Thing to dispose/parse a received packet + * Abstract function to be implemented by Thing to parse a received packet * * @param 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 0000000000000..f1d17d7fad05b --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairControllerHandler.java @@ -0,0 +1,1014 @@ +/** + * Copyright (c) 2010-2020 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.Calendar; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.unit.ImperialUnits; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.ThingStatusInfo; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.pentair.internal.PentairControllerCircuit; +import org.openhab.binding.pentair.internal.PentairControllerConstants; +import org.openhab.binding.pentair.internal.PentairControllerSchedule; +import org.openhab.binding.pentair.internal.PentairPacket; +import org.openhab.binding.pentair.internal.PentairPacketHeatSetPoint; +import org.openhab.binding.pentair.internal.PentairPacketStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import tec.uom.se.unit.Units; + +/** + * 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 { + + protected static final int NUMCIRCUITS = 18; + protected static final int NUMSCHEDULES = 9; + + // only one controller can be online at a time, used to validate only one is online & to access status + @Nullable + public static PentairControllerHandler onlineController; + public boolean servicemode = false; + + private final Logger logger = LoggerFactory.getLogger(PentairControllerHandler.class); + @Nullable + protected ScheduledFuture syncTimeJob; + @Nullable + protected ScheduledFuture updateMinsRunJob; + private int preambleByte = -1; // 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 boolean waitStatusForOnline = false; // To manage online status, only go online when we received first + // status command + private long lastScheduleTypeWrite; + + /** + * current/last status packet recieved, used to compare new packet values to determine if status needs to be updated + */ + @Nullable + protected PentairPacketStatus p29cur = new PentairPacketStatus(); + @Nullable + protected PentairPacketStatus p29old; + /** current/last heat set point packet, used to determine if status in framework should be updated */ + protected PentairPacketHeatSetPoint phspcur = new PentairPacketHeatSetPoint(); + + private int majorrev, minorrev; + + protected PentairControllerCircuit[] circuits = new PentairControllerCircuit[NUMCIRCUITS]; + protected PentairControllerSchedule[] schedules = new PentairControllerSchedule[NUMSCHEDULES]; + + protected int lightmode; + + public PentairControllerHandler(Thing thing) { + super(thing); + + for (int i = 0; i < NUMSCHEDULES; i++) { + schedules[i] = new PentairControllerSchedule(); + } + + for (int i = 0; i < NUMCIRCUITS; i++) { + circuits[i] = new PentairControllerCircuit(i + 1); + } + } + + @Override + public void initialize() { + logger.debug("Initializing Controller - Thing ID: {}.", this.getThing().getUID()); + + goOnline(); + } + + @Override + public void dispose() { + + logger.debug("Thing {} disposed.", getThing().getUID()); + try { + throw (new Exception("dispose")); + } catch (Exception e) { + logger.debug("dispose {}", e.getStackTrace()); + } + + goOffline(ThingStatusDetail.NONE); + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + logger.debug("PentairControllerHandler: bridgeStatusChanged: {}", bridgeStatusInfo); + + if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) { + goOffline(ThingStatusDetail.BRIDGE_OFFLINE); + } else if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) { + goOnline(); + } + } + + public void goOnline() { + logger.debug("Thing {} goOnline.", getThing().getUID()); + + this.waitStatusForOnline = false; + + if (onlineController != null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Another Controller controller is already configured."); + } + + id = ((BigDecimal) getConfig().get("id")).intValue(); + + // make sure bridge exists and is online + Bridge bridge = this.getBridge(); + if (bridge == null) { + return; + } + PentairBaseBridgeHandler bh = (PentairBaseBridgeHandler) bridge.getHandler(); + if (bh == null) { + logger.debug("Bridge does not exist"); + return; + } + + ThingStatus ts = bh.getThing().getStatus(); + if (!ts.equals(ThingStatus.ONLINE)) { + logger.debug("Bridge is not online"); + return; + } + + waitStatusForOnline = true; // Wait for first status response to go online + } + + public void finishOnline() { + onlineController = this; + + // setup timer to sync time + Runnable runnable = new Runnable() { + + @Override + public void run() { + boolean synctime = ((boolean) getConfig().get("synctime")); + if (synctime) { + logger.info("Synchronizing System Time"); + Calendar now = Calendar.getInstance(); + 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); + } + } + }; + + // setup syncTimeJob to run once a day, initial time to sync is 3 minutes after controller goes online. This is + // to prevent collision with main thread queries on initial startup + syncTimeJob = scheduler.scheduleWithFixedDelay(runnable, 3, 24 * 60 * 60, TimeUnit.MINUTES); + + Runnable countMinsRun = new Runnable() { + @Override + public void run() { + logger.debug("Incrementing minutes run"); + for (int i = 0; i < NUMCIRCUITS; i++) { + if (circuits[i].on) { + circuits[i].minsrun++; + updateState(circuits[i].getGroup() + "#" + CONTROLLER_CIRCUITMINSRUN, + new QuantityType<>(circuits[i].minsrun, Units.MINUTE)); + } + } + } + }; + + updateMinsRunJob = scheduler.scheduleAtFixedRate(countMinsRun, 1, 1, TimeUnit.MINUTES); + + Runnable queryinfo = new Runnable() { + @Override + public void run() { + int i; + + getSWVersion(); + + getHeat(); + + getClockSettings(); + + for (i = 1; i <= NUMCIRCUITS; i++) { + getCircuitNameFunction(i); + } + + for (i = 1; i <= NUMSCHEDULES; i++) { + getSchedule(i); + } + + getLightGroups(); + + getValves(); + + getSWVersion(); + } + }; + Thread thread = new Thread(queryinfo); + thread.start(); + + updateStatus(ThingStatus.ONLINE); + } + + public void goOffline(ThingStatusDetail detail) { + logger.debug("Thing {} goOffline.", getThing().getUID()); + + if (syncTimeJob != null) { + syncTimeJob.cancel(true); + } + + if (updateMinsRunJob != null) { + updateMinsRunJob.cancel(true); + } + + onlineController = null; + updateStatus(ThingStatus.OFFLINE, detail); + } + + public int getCircuitNumber(String name) { + return PentairControllerCircuit.GROUPNAME_INV.get(name); + } + + public int getScheduleNumber(String name) { + int i; + + for (i = 1; i <= NUMSCHEDULES; i++) { + String str = String.format(CONTROLLER_SCHEDULE, i); + if (str.equals(name)) { + return i; + } + } + + return 0; + } + + public @Nullable PentairControllerSchedule getPPCS(String groupid) { + int schedule = getScheduleNumber(groupid); + if (schedule == 0) { + return null; + } + + PentairControllerSchedule ppcs = schedules[schedule - 1]; + + return ppcs; + } + + public @Nullable PentairControllerCircuit getPCC(String groupid) { + int circuit = getCircuitNumber(groupid); + if (circuit == 0) { + return null; + } + + PentairControllerCircuit pcc = circuits[circuit - 1]; + + return pcc; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + String groupId = channelUID.getGroupId(); + + if (groupId == null) { + return; + } + + if (command instanceof RefreshType) { + logger.debug("handleCommand (refresh): {}", channelUID.getId()); + + switch (channelUID.getIdWithoutGroup()) { + case CONTROLLER_LIGHTMODE: + updateState(channelUID, new StringType(PentairControllerConstants.LIGHTMODES.get(lightmode))); + break; + + case CONTROLLER_SETPOINT: + if (groupId == CONTROLLER_POOLHEAT) { + updateChannelTemp(groupId, channelUID.getIdWithoutGroup(), phspcur.poolsetpoint); + } else { // groupId == CONTROLLER_SPAHEAT + updateChannelTemp(groupId, channelUID.getIdWithoutGroup(), phspcur.spasetpoint); + } + + break; + case CONTROLLER_SCHEDULESTRING: { + PentairControllerSchedule pcs = getPPCS(groupId); + + if (pcs != null) { + updateState(channelUID, new StringType(pcs.toString())); + } + break; + } + case CONTROLLER_SCHEDULETYPE: { + PentairControllerSchedule ppcs = getPPCS(groupId); + + if (ppcs != null) { + updateState(channelUID, new StringType(ppcs.getScheduleTypeStr())); + } + break; + } + case CONTROLLER_SCHEDULESTART: { + PentairControllerSchedule ppcs = getPPCS(groupId); + + if (ppcs != null) { + updateState(channelUID, new DecimalType(ppcs.start)); + } + break; + } + case CONTROLLER_SCHEDULEEND: { + PentairControllerSchedule ppcs = getPPCS(groupId); + + if (ppcs != null) { + updateState(channelUID, new DecimalType(ppcs.end)); + } + break; + } + case CONTROLLER_SCHEDULECIRCUIT: { + PentairControllerSchedule ppcs = getPPCS(groupId); + + if (ppcs != null) { + updateState(channelUID, new DecimalType(ppcs.circuit)); + } + break; + } + case CONTROLLER_SCHEDULEDAYS: { + PentairControllerSchedule ppcs = getPPCS(groupId); + + if (ppcs != null) { + updateState(channelUID, new StringType(ppcs.getDays())); + } + break; + } + case CONTROLLER_CIRCUITNAME: { + PentairControllerCircuit pcc = getPCC(groupId); + if (pcc != null) { + updateState(channelUID, new StringType(pcc.getNameStr())); + } + break; + } + case CONTROLLER_CIRCUITFUNCTION: { + PentairControllerCircuit pcc = getPCC(groupId); + if (pcc != null) { + updateState(channelUID, new StringType(pcc.getFunctionStr())); + } + break; + } + case CONTROLLER_CIRCUITMINSRUN: { + PentairControllerCircuit pcc = getPCC(groupId); + if (pcc != null) { + updateState(channelUID, new QuantityType<>(pcc.getMinsRun(), Units.MINUTE)); + } + break; + } + } + + return; + } + + logger.debug("handleCommand: {}", channelUID.getId()); + + switch (channelUID.getIdWithoutGroup()) { + case CONTROLLER_CIRCUITSWITCH: { + int circuit = getCircuitNumber(groupId); + + boolean state = ((OnOffType) command) == OnOffType.ON; + circuitSwitch(circuit, state); + + break; + } + case CONTROLLER_CIRCUITMINSRUN: { + int circuit = getCircuitNumber(groupId); + + BigDecimal mins = ((QuantityType) command).toBigDecimal(); + circuits[circuit - 1].minsrun = mins.intValue(); + + break; + } + case CONTROLLER_LIGHTMODE: { + + String str = ((StringType) command).toString(); + + int mode = PentairControllerConstants.LIGHTMODES_INV.get(str); + lightmode = mode; + setLightMode(mode); + + // not sure why this doesn't autoupdate + updateState(channelUID, (State) command); + + break; + } + case CONTROLLER_SCHEDULESTRING: { + PentairControllerSchedule ppcs = getPPCS(groupId); + + if (ppcs == null) { + break; + } + + String str = ((StringType) command).toString(); + + if (!ppcs.fromString(str)) { + logger.debug("schedule invalid format: {}", str); + } + + break; + } + case CONTROLLER_SCHEDULETYPE: { + PentairControllerSchedule ppcs = getPPCS(groupId); + + if (ppcs == null) { + break; + } + + String str = ((StringType) command).toString(); + // save schedule only if a double update to the same value occurs within 5s of the last update + boolean bUpdate = (str.equals(ppcs.getScheduleTypeStr()) + && ((System.currentTimeMillis() - lastScheduleTypeWrite) < 5000) && ppcs.isDirty()); + if (!ppcs.setScheduleType(str)) { + return; + } + lastScheduleTypeWrite = System.currentTimeMillis(); + + if (bUpdate) { + saveSchedule(ppcs); + lastScheduleTypeWrite = 0; + + updateScheduleChannels(groupId, ppcs); + } + + break; + } + case CONTROLLER_SCHEDULESTART: { + PentairControllerSchedule ppcs = getPPCS(groupId); + + if (ppcs == null) { + break; + } + + int start = ((Number) command).intValue(); + + ppcs.setScheduleStart(start); + + break; + } + case CONTROLLER_SCHEDULEEND: { + PentairControllerSchedule ppcs = getPPCS(groupId); + + if (ppcs == null) { + break; + } + + int end = ((Number) command).intValue(); + + ppcs.setScheduleEnd(end); + + break; + } + case CONTROLLER_SCHEDULECIRCUIT: { + PentairControllerSchedule ppcs = getPPCS(groupId); + + if (ppcs == null) { + break; + } + + int circuit = ((Number) command).intValue(); + + ppcs.setScheduleCircuit(circuit); + + break; + } + case CONTROLLER_SCHEDULEDAYS: { + PentairControllerSchedule ppcs = getPPCS(groupId); + + if (ppcs == null) { + break; + } + + String days = ((StringType) command).toString(); + + ppcs.setDays(days); + + break; + } + + case CONTROLLER_SETPOINT: { + if (!(command instanceof QuantityType)) { + break; + } + + int sp = ((QuantityType) command).toBigDecimal().intValue(); + + if (sp == 0) { + return; + } + + switch (groupId) { + case CONTROLLER_SPAHEAT: + setPoint(false, sp); + break; + case CONTROLLER_POOLHEAT: + setPoint(true, sp); + break; + } + + break; + } + } + + } + + /* Commands to send to Controller */ + + /** + * Method to turn on/off a circuit in response to a command from the framework + * + * @param circuit circuit number + * @param state + */ + public boolean circuitSwitch(int circuit, boolean state) { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, (byte) 0x86, + (byte) 0x02, (byte) circuit, (byte) ((state) ? 1 : 0) }; + + logger.info("circuit Switch: {}, {}", circuit, state); + + if (!writePacket(packet, 0x01, 1)) { + logger.debug("circuitSwitch: 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) 0xC5, + (byte) 0x01, (byte) 0x00 }; + + logger.info("Request clock settings"); + if (!writePacket(packet, 0x05, 1)) { + logger.debug("getClockSetting: Timeout"); + + return false; + } + + return true; + } + + public void getControllerStatus() { // A5 01 10 20 02 01 00 + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, (byte) 0x02, + (byte) 0x01, (byte) 0x00 }; + + logger.info("Request controller status"); + + if (!writePacket(packet, 0x02, 1)) { + logger.debug("getControllerStatus: Timeout"); + } + } + + public void getLightGroups() { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, (byte) 0xE7, + (byte) 0x01, (byte) 0x00 }; + + logger.info("Get Light Groups"); + + if (!writePacket(packet, 0x27, 1)) { + logger.debug("getLightGroups: Timeout"); + } + } + + public void setLightMode(int mode) { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, (byte) 0x60, + (byte) 0x02, (byte) mode, (byte) 0x00 }; + + logger.info("setLightMode: {}", mode); + + if (!writePacket(packet, 0x01, 1)) { + logger.debug("setLightMode: Timeout"); + } + } + + public void getValves() { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, (byte) 0xDD, + (byte) 0x01, (byte) 0x00 }; + + logger.info("getValves"); + + if (!writePacket(packet, 29, 1)) { + logger.debug("getValves: Timeout"); + } + } + + public boolean getCircuitNameFunction(int circuit) { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, (byte) 0xCB, + (byte) 0x01, (byte) circuit }; + + logger.info("getCircuitNameFunction: {}", circuit); + + if (!writePacket(packet, 0x0B, 1)) { + logger.debug("getCircuitNameFunction: Timeout"); + + return false; + } + return true; + } + + public boolean getSchedule(int num) { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, (byte) 0xD1, + (byte) 0x01, (byte) num }; + + logger.info("getSchedule: {}", num); + + if (!writePacket(packet, 0x11, 1)) { + logger.debug("getSchedule: Timeout"); + + return false; + } + + return true; + } + + /** + * Method to update the schedule to the controller + * + * @param p + */ + public boolean saveSchedule(PentairControllerSchedule schedule) { + PentairPacket p; + + p = schedule.getWritePacket(id, preambleByte); + + logger.debug("saveSchedule: {}", p.toString()); + schedule.setDirty(false); + + if (!writePacket(p, 0x01, 1)) { + logger.debug("saveSchedule: Timeout"); + + return false; + } + + return true; + } + + public boolean getSWVersion() { + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, (byte) 0xFD, + (byte) 0x01, (byte) 0x00 }; + + logger.info("getSWVersion"); + + if (!writePacket(packet, 0xFC, 1)) { + return false; + } + + /* + * String version = String.format("%d.%d", majorrev, minorrev); + * + * this causes thingUpdated to be updated - default implementation is to dispose and re-initialize which is + * extreme! I don't want to override in case I break some expected behavior in the framework. So removing this + * property + * update since it is for informational purposes only. + * Map editProperties = editProperties(); + * editProperties.put(CONTROLLER_PROPERTYFWVERSION, version); + * updateProperties(editProperties); + */ + + return true; + } + + /** + * Method to set clock + * + */ + public void 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 + + logger.info("Set Clock Settings {}:{} {} {}/{}/{}", hour, min, dow, day, month, year); + + 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) 0x85, + (byte) 0x08, (byte) hour, (byte) min, (byte) dow, (byte) day, (byte) month, (byte) year, (byte) 0x00, + (byte) 0x00 }; + + writePacket(packet); + } + + public void getHeat() { // A5 01 10 20 C8 01 00 + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (byte) id, (byte) 0x00 /* source */, (byte) 0xC8, + (byte) 0x01, (byte) 0 }; + + logger.info("Get heat settings"); + + if (!writePacket(packet, 0x08, 1)) { + logger.debug("getHeat: Timeout"); + } + } + + /** + * 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; + + if (temp < 50 || temp > 105) { + return; + } + + byte[] packet = { (byte) 0xA5, (byte) preambleByte, (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); + + writePacket(packet); + } + + @SuppressWarnings("null") + @Override + public void processPacketFrom(PentairPacket p) { + + switch (p.getAction()) { + case 1: // Ack + logger.debug("Ack command from device: {} - {}", p.getByte(0), p); + break; + case 2: // Controller Status + if (p.getLength() != 29) { + logger.debug("Expected length of 29: {}", p); + return; + } + + logger.trace("Controller Status: {}", p); + + preambleByte = p.getPreambleByte(); // Adjust what byte is used for preamble + if (waitStatusForOnline) { + waitStatusForOnline = false; + finishOnline(); + } + + p29cur = new PentairPacketStatus(p); + + // only update packet of value has changed + if (p29cur.equals(p29old)) { + return; + } + p29old = p29cur; + + for (int i = 0; i < NUMCIRCUITS; i++) { + PentairControllerCircuit circuit = circuits[i]; + + circuit.setOnOROff(p29cur.circuits[i]); + + updateChannel(circuits[i].getGroup(), CONTROLLER_CIRCUITSWITCH, circuits[i].on); + } + + updateChannelTemp(CONTROLLER_POOLHEAT, CONTROLLER_TEMPERATURE, (p29cur.pool) ? p29cur.pooltemp : 999); + updateChannelTemp(CONTROLLER_SPAHEAT, CONTROLLER_TEMPERATURE, (p29cur.spa) ? p29cur.spatemp : 999); + + updateChannelTemp(CONTROLLER_STATUS, CONTROLLER_AIRTEMPERATURE, p29cur.airtemp); + updateChannelTemp(CONTROLLER_STATUS, CONTROLLER_SOLARTEMPERATURE, p29cur.solartemp); + updateChannel(CONTROLLER_STATUS, CONTROLLER_UOM, (p29cur.uom) ? "CELCIUS" : "FAHRENHEIT"); + updateChannel(CONTROLLER_STATUS, CONTROLLER_SERVICEMODE, p29cur.servicemode); + servicemode = p29cur.servicemode; + + updateChannel(CONTROLLER_STATUS, CONTROLLER_SOLARON, p29cur.solaron); + updateChannel(CONTROLLER_STATUS, CONTROLLER_HEATERON, p29cur.heateron); + + break; + case 4: // Pump control panel on/off - handled in intelliflo controller + // Controller sends packet often to keep control of the motor + logger.debug("Pump control panel on/of {}: {}", p.getDest(), p.getByte(0)); + + break; + case 5: // 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); + int minute = p.getByte(1); + int dow = p.getByte(2); + int day = p.getByte(3); + int month = p.getByte(4); + int year = p.getByte(5); + + logger.debug("System Clock: {}:{} {} {}/{}/{}", hour, minute, dow, day, month, year); + + break; + case 6: // Set run mode + // No action - have not verified these commands, here for documentation purposes and future enhancement + logger.debug("Set run mode {}: {}", p.getDest(), p.getByte(0)); + + break; + case 7: // Pump Status - handled in IntelliFlo handler + // No action - have not verified these commands, here for documentation purposes and future enhancement + logger.debug("Pump request status (unseen): {}", p); + break; + case 8: // Heat Status - 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; + } + + phspcur = new PentairPacketHeatSetPoint(p); + + updateChannelTemp(CONTROLLER_POOLHEAT, CONTROLLER_SETPOINT, phspcur.poolsetpoint); + updateChannelTemp(CONTROLLER_SPAHEAT, CONTROLLER_SETPOINT, phspcur.spasetpoint); + + updateChannel(CONTROLLER_POOLHEAT, CONTROLLER_HEATMODE, + PentairControllerConstants.HEATMODE.get(phspcur.poolheatmode)); + updateChannel(CONTROLLER_SPAHEAT, CONTROLLER_HEATMODE, + PentairControllerConstants.HEATMODE.get(phspcur.spaheatmode)); + + logger.debug("Heat set point: {}, {}, {}", p, phspcur.poolsetpoint, phspcur.spasetpoint); + break; + case 10: // Custom Names + logger.debug("Get Custom Names (unseen): {}", p); + break; + case 11: // Circuit Names + int index; + + index = p.getByte(0); + if (index < 1 || index > NUMCIRCUITS) { + break; + } + + PentairControllerCircuit pcc = circuits[index - 1]; + + pcc.setName(p.getByte(2)); + pcc.setFunction(p.getByte(1)); + + updateCircuitChannels(pcc.getGroup(), pcc); + + logger.debug("Circuit Names - Circuit: {}, Function: {}, Name: {}", pcc.id, pcc.getFunctionStr(), + pcc.getNameStr()); + break; + case 17: // schedule - A5 1E 0F 10 11 07 01 06 0B 00 0F 00 7F + PentairControllerSchedule ppcs; + int id; + + id = p.getByte(PentairControllerSchedule.ID); + if (id < 1 || id > NUMSCHEDULES) { + break; + } + + ppcs = schedules[id - 1]; + ppcs.parsePacket(p); + + String group = String.format(CONTROLLER_SCHEDULE, ppcs.id); + + logger.debug("Controller schedule group: {}", group); + + updateScheduleChannels(group, ppcs); + + logger.debug( + "Controller Schedule - ID: {}, Type: {}, Circuit: {}, Start Time: {}:{}, End Time: {}:{}, Days: {}", + ppcs.id, ppcs.type, ppcs.circuit, ppcs.start / 60, ppcs.start % 60, ppcs.end / 60, + ppcs.end % 60, ppcs.days); + break; + case 18: // IntelliChem + logger.debug("IntelliChem status: {}", p); + break; + case 25: // Intellichlor status + logger.debug("Intellichlor status: {}", p); + break; + case 27: // Pump config (Extended) + logger.debug("Pump Config: {}", p); + break; + case 29: // Valves + logger.debug("Values: {}", p); + break; + case 30: // High speed circuits + logger.debug("High speed circuits: {}", p); + break; + case 32: // spa-side is4/is10 remote + case 33: // spa-side quicktouch remotes + logger.debug("Spa-side remotes: {}", p); + break; + case 34: // Solar/Heat Pump status + logger.debug("Solar/Heat Pump status: {}", p); + break; + case 35: // Delay status + logger.debug("Delay status: {}", p); + break; + case 39: // Light Groups/Positions + logger.debug("Light Groups/Positions; {}", p); + break; + case 40: // Settings? heat mode + logger.debug("Settings?: {}", p); + break; + case 96: // set intellebrite colors + logger.debug("Set intellebrite colors: {}", p); + break; + case 134: // Set Curcuit On/Off + logger.debug("Set Circuit Function On/Off (unseen): {}", p); + break; + case 252: // 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); + minorrev = p.getByte(2); + logger.debug("SW Version - {}:{}", majorrev, minorrev); + break; + default: + logger.debug("Not Implemented {}: {}", p.getAction(), p); + break; + } + } + + /** + * Helper function to update channel. + */ + public void updateChannel(String group, String channel, boolean value) { + updateState(group + "#" + channel, (value) ? OnOffType.ON : OnOffType.OFF); + } + + @SuppressWarnings("null") + public void updateChannelTemp(String group, String channel, int value) { + if (value != 999) { + updateState(group + "#" + channel, + new QuantityType<>(value, (p29cur.uom) ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT)); + } else { + updateState(group + "#" + channel, UnDefType.UNDEF); + } + } + + public void updateChannel(String group, String channel, int value) { + updateState(group + "#" + channel, new DecimalType(value)); + } + + public void updateChannel(String group, String channel, @Nullable String value) { + if (value == null) { + updateState(group + "#" + channel, UnDefType.NULL); + } else { + updateState(group + "#" + channel, new StringType(value)); + } + } + + public void updateScheduleChannels(String group, PentairControllerSchedule pcs) { + updateChannel(group, CONTROLLER_SCHEDULESTRING, pcs.toString()); + + updateChannel(group, CONTROLLER_SCHEDULETYPE, pcs.getScheduleTypeStr()); + updateChannel(group, CONTROLLER_SCHEDULECIRCUIT, pcs.circuit); + updateChannel(group, CONTROLLER_SCHEDULEDAYS, pcs.days); + updateChannel(group, CONTROLLER_SCHEDULESTART, pcs.start); + updateChannel(group, CONTROLLER_SCHEDULEEND, pcs.end); + updateChannel(group, CONTROLLER_SCHEDULEDAYS, pcs.getDays()); + + logger.debug( + "Controller Schedule - ID: {}, Type: {}, Circuit: {}, Start Time: {}:{}, End Time: {}:{}, Days: {}", + pcs.id, pcs.type, pcs.circuit, pcs.start / 60, pcs.start % 60, pcs.end / 60, pcs.end % 60, + pcs.getDays()); + } + + public void updateCircuitChannels(String group, PentairControllerCircuit pcc) { + updateChannel(group, CONTROLLER_CIRCUITSWITCH, pcc.on); + updateChannel(group, CONTROLLER_CIRCUITMINSRUN, pcc.minsrun); + updateChannel(group, CONTROLLER_CIRCUITNAME, pcc.getNameStr()); + updateChannel(group, CONTROLLER_CIRCUITFUNCTION, pcc.getFunctionStr()); + } +} 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 239db976071a4..0000000000000 --- a/bundles/org.openhab.binding.pentair/src/main/java/org/openhab/binding/pentair/internal/handler/PentairEasyTouchHandler.java +++ /dev/null @@ -1,497 +0,0 @@ -/** - * Copyright (c) 2010-2020 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 org.eclipse.smarthome.core.library.types.DecimalType; -import org.eclipse.smarthome.core.library.types.OnOffType; -import org.eclipse.smarthome.core.library.types.StringType; -import org.eclipse.smarthome.core.thing.ChannelUID; -import org.eclipse.smarthome.core.thing.Thing; -import org.eclipse.smarthome.core.thing.ThingStatus; -import org.eclipse.smarthome.core.thing.ThingStatusDetail; -import org.eclipse.smarthome.core.types.Command; -import org.eclipse.smarthome.core.types.RefreshType; -import org.eclipse.smarthome.core.types.UnDefType; -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.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) { - boolean state = ((OnOffType) command) == 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) { - int sp = ((DecimalType) command).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, (p29cur.pool) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_SPA: - if (p29 == null || (p29.spa != p29cur.spa)) { - updateState(channel, (p29cur.spa) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_AUX1: - if (p29 == null || (p29.aux1 != p29cur.aux1)) { - updateState(channel, (p29cur.aux1) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_AUX2: - if (p29 == null || (p29.aux2 != p29cur.aux2)) { - updateState(channel, (p29cur.aux2) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_AUX3: - if (p29 == null || (p29.aux3 != p29cur.aux3)) { - updateState(channel, (p29cur.aux3) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_AUX4: - if (p29 == null || (p29.aux4 != p29cur.aux4)) { - updateState(channel, (p29cur.aux4) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_AUX5: - if (p29 == null || (p29.aux5 != p29cur.aux5)) { - updateState(channel, (p29cur.aux5) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_AUX6: - if (p29 == null || (p29.aux6 != p29cur.aux6)) { - updateState(channel, (p29cur.aux6) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_AUX7: - if (p29 == null || (p29.aux7 != p29cur.aux7)) { - updateState(channel, (p29cur.aux7) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_FEATURE1: - if (p29 == null || (p29.feature1 != p29cur.feature1)) { - updateState(channel, (p29cur.feature1) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_FEATURE2: - if (p29 == null || (p29.feature2 != p29cur.feature2)) { - updateState(channel, (p29cur.feature2) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_FEATURE3: - if (p29 == null || (p29.feature3 != p29cur.feature3)) { - updateState(channel, (p29cur.feature3) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_FEATURE4: - if (p29 == null || (p29.feature4 != p29cur.feature4)) { - updateState(channel, (p29cur.feature4) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_FEATURE5: - if (p29 == null || (p29.feature5 != p29cur.feature5)) { - updateState(channel, (p29cur.feature5) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_FEATURE6: - if (p29 == null || (p29.feature6 != p29cur.feature6)) { - updateState(channel, (p29cur.feature6) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_FEATURE7: - if (p29 == null || (p29.feature7 != p29cur.feature7)) { - updateState(channel, (p29cur.feature7) ? OnOffType.ON : OnOffType.OFF); - } - break; - case EASYTOUCH_FEATURE8: - if (p29 == null || (p29.feature8 != p29cur.feature8)) { - updateState(channel, (p29cur.feature8) ? OnOffType.ON : OnOffType.OFF); - } - 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 || (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 || (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 afa5b3126a6ca..0f23b873210e0 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 @@ -18,6 +18,8 @@ import java.net.Socket; import java.net.UnknownHostException; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ThingStatus; import org.eclipse.smarthome.core.thing.ThingStatusDetail; @@ -31,10 +33,14 @@ * @author Jeff James - Initial contribution * */ +@NonNullByDefault public class PentairIPBridgeHandler extends PentairBaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(PentairIPBridgeHandler.class); + public PentairIPBridgeConfig config = new PentairIPBridgeConfig(); + /** Socket object for connection */ + @Nullable protected Socket socket; public PentairIPBridgeHandler(Bridge bridge) { @@ -42,39 +48,53 @@ public PentairIPBridgeHandler(Bridge bridge) { } @Override - protected synchronized void connect() { - PentairIPBridgeConfig configuration = getConfigAs(PentairIPBridgeConfig.class); + protected synchronized int connect() { + logger.debug("PenatiarIPBridgeHander: connect"); + + config = getConfigAs(PentairIPBridgeConfig.class); - id = configuration.id; + this.id = config.id; + this.discovery = config.discovery; try { - socket = new Socket(configuration.address, configuration.port); + Socket socket = new Socket(config.address, config.port); + this.socket = socket; + reader = new BufferedInputStream(socket.getInputStream()); writer = new BufferedOutputStream(socket.getOutputStream()); - logger.info("Pentair IPBridge connected to {}:{}", configuration.address, configuration.port); + + logger.info("Pentair IPBridge connected to {}:{}", config.address, config.port); } catch (UnknownHostException e) { - String msg = String.format("unknown host name: %s", configuration.address); + String msg = String.format("unknown host name: %s", config.address); + logger.debug("{}", msg); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; + return -1; } catch (IOException e) { - String msg = String.format("cannot open connection to %s", configuration.address); + String msg = String.format("cannot open connection to %s", config.address); + logger.debug("{}", msg); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; + return -2; } parser = new Parser(); - thread = new Thread(parser); + Thread thread = new Thread(parser); + this.thread = thread; thread.start(); if (socket != null && reader != null && writer != null) { updateStatus(ThingStatus.ONLINE); } else { + logger.debug("connect: socket, reader or writer is null"); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unable to connect"); } + + return 0; } + @SuppressWarnings("null") @Override protected synchronized void disconnect() { + logger.debug("PentairIPBridgeHandler: disconnect"); updateStatus(ThingStatus.OFFLINE); if (thread != null) { @@ -92,6 +112,7 @@ protected synchronized void disconnect() { try { reader.close(); } catch (IOException e) { + logger.debug("disconnect: IOException"); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error in closing reader"); } reader = null; @@ -101,6 +122,7 @@ protected synchronized void disconnect() { try { writer.close(); } catch (IOException e) { + logger.debug("disconnect: IOException"); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error in closing writer"); } writer = null; 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 f1b4222870f41..358aec2764dd1 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,10 +14,14 @@ import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.library.types.DecimalType; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.ThingStatusInfo; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; import org.openhab.binding.pentair.internal.PentairBindingConstants; @@ -34,8 +38,10 @@ * * @author Jeff James - Initial contribution */ +@NonNullByDefault public class PentairIntelliChlorHandler extends PentairBaseThingHandler { private final Logger logger = LoggerFactory.getLogger(PentairIntelliChlorHandler.class); + private boolean waitStatusForOnline = false; protected PentairPacketIntellichlor pic3cur = new PentairPacketIntellichlor(); protected PentairPacketIntellichlor pic4cur = new PentairPacketIntellichlor(); @@ -50,18 +56,43 @@ public void initialize() { id = 0; // Intellichlor doesn't have ID - updateStatus(ThingStatus.ONLINE); + goOnline(); } @Override public void dispose() { logger.debug("Thing {} disposed.", getThing().getUID()); + + goOffline(ThingStatusDetail.NONE); + } + + public void goOnline() { + logger.debug("Thing {} goOnline.", getThing().getUID()); + + waitStatusForOnline = true; + } + + public void goOffline(ThingStatusDetail detail) { + logger.debug("Thing {} goOffline.", getThing().getUID()); + + updateStatus(ThingStatus.OFFLINE, detail); + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + logger.trace("PentairIntelliChlorHandler: bridgeStatusChanged"); + + if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) { + goOffline(ThingStatusDetail.BRIDGE_OFFLINE); + } else if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) { + goOnline(); + } } @Override public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { - logger.debug("IntelliChlor received refresh command"); + logger.trace("IntelliChlor received refresh command"); updateChannel(channelUID.getId(), null); } } @@ -70,6 +101,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { public void processPacketFrom(PentairPacket p) { PentairPacketIntellichlor pic = (PentairPacketIntellichlor) p; + if (waitStatusForOnline) { + updateStatus(ThingStatus.ONLINE); + waitStatusForOnline = false; + } + switch (pic.getLength()) { case 3: if (pic.getCmd() != 0x11) { // only packets with 0x11 have valid saltoutput numbers. @@ -105,7 +141,7 @@ public void processPacketFrom(PentairPacket p) { * @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) { + public void updateChannel(String channel, @Nullable PentairPacket p) { PentairPacketIntellichlor pic = (PentairPacketIntellichlor) p; switch (channel) { 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 d5f92212eea20..7515ddaa64eb9 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 @@ -15,15 +15,25 @@ import static org.openhab.binding.pentair.internal.PentairBindingConstants.*; import java.math.BigDecimal; +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.config.core.Configuration; import org.eclipse.smarthome.core.library.types.DecimalType; import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.ThingStatusInfo; import org.eclipse.smarthome.core.types.Command; -import org.eclipse.smarthome.core.types.RefreshType; -import org.openhab.binding.pentair.internal.PentairBindingConstants; import org.openhab.binding.pentair.internal.PentairPacket; import org.openhab.binding.pentair.internal.PentairPacketPumpStatus; import org.slf4j.Logger; @@ -31,16 +41,24 @@ /** * 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 of 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 boolean waitStatusForOnline = false; + // runmode is used to send watchdog to pump when running + private boolean runmode = false; + + /** polling job for pump status */ + @Nullable + static protected ScheduledFuture pollingjob; + public PentairIntelliFloHandler(Thing thing) { super(thing); } @@ -49,39 +67,370 @@ public PentairIntelliFloHandler(Thing thing) { public void initialize() { logger.debug("Initializing Intelliflo - Thing ID: {}.", this.getThing().getUID()); - id = ((BigDecimal) getConfig().get("id")).intValue(); + Configuration config = getConfig(); - updateStatus(ThingStatus.ONLINE); + id = ((BigDecimal) config.get("id")).intValue(); + + goOnline(); } @Override public void dispose() { logger.debug("Thing {} disposed.", getThing().getUID()); + goOffline(ThingStatusDetail.NONE); + } + + public void goOnline() { + logger.debug("Thing {} goOnline.", getThing().getUID()); + + this.waitStatusForOnline = false; + + // make sure bridge exists and is online + Bridge bridge = this.getBridge(); + if (bridge == null) { + logger.debug("PentaiIntelliFloHandler: goOnline - bridge == null"); + return; + } + PentairBaseBridgeHandler bh = (PentairBaseBridgeHandler) bridge.getHandler(); + if (bh == null) { + logger.debug("Bridge does not exist"); + return; + } + + ThingStatus ts = bh.getThing().getStatus(); + if (!ts.equals(ThingStatus.ONLINE)) { + logger.debug("Bridge is not online"); + return; + } + + waitStatusForOnline = true; + // requestPumpStatus(); // pump doesn't go online until it sees first response on the RS485 + } + + public void finishOnline() { + if (pollingjob == null) { + pollingjob = scheduler.scheduleWithFixedDelay(new PumpStatus(), 10, 30, TimeUnit.SECONDS); + } + + updateStatus(ThingStatus.ONLINE); + } + + public void goOffline(ThingStatusDetail detail) { + logger.debug("Thing {} goOffline.", getThing().getUID()); + + if (pollingjob != null) { + pollingjob.cancel(true); + } + pollingjob = null; + + updateStatus(ThingStatus.OFFLINE, detail); + } + + /** + * 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() { + Bridge bridge = getBridge(); + if (bridge == null) { + return; + } + + List things = bridge.getThings(); + + for (Thing t : things) { + if (!t.getThingTypeUID().equals(INTELLIFLO_THING_TYPE)) { + continue; + } + + PentairIntelliFloHandler handler = (PentairIntelliFloHandler) t.getHandler(); + if (handler == null) { + return; + } + + logger.debug("pump runmode = {}", runmode); + + if (handler.runmode == true) { + logger.debug("Sending watchdog to pump"); + handler.sendPumpOnOROff(true); + } else { + handler.requestPumpStatus(); + } + } + } + }; + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + logger.trace("PentairIntelliFloHandler: bridgeStatusChanged"); + + if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) { + goOffline(ThingStatusDetail.BRIDGE_OFFLINE); + } else if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) { + goOnline(); + } + } + + // checkOtherMaster - check to make sure the system does not have a controller OR that the controller is in + // servicemode + protected boolean checkOtherMaster() { + PentairControllerHandler pch = PentairControllerHandler.onlineController; + + if (pch != null && pch.servicemode == false) { + return true; + } + + return false; + } + + /* Commands to send to IntelliFlo */ + + public void sendRequestPumpStatus() { + logger.debug("sendRequestPumpStatus"); + byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) id, (byte) 0x00 /* source */, (byte) 0x07, (byte) 0x00 }; + + if (!writePacket(packet, 0x07, 1)) { + logger.debug("sendRequestStatus: Timeout"); + } + } + + public void requestPumpStatus() { + logger.debug("requestPumpStatus"); + + sendLocalORRemoteControl(false); + sendRequestPumpStatus(); + } + + public void sendLocalORRemoteControl(boolean bLocal) { + byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) id, (byte) 0x00 /* source */, (byte) 0x04, (byte) 0x01, + (bLocal) ? (byte) 0x00 : (byte) 0xFF }; + + logger.debug("sendLocalORRemoteControl: {}", bLocal); + + if (!writePacket(packet, 0x04, 1)) { + logger.debug("sendLocalOrRemoteControl: Timeout"); + } + } + + public void sendPumpOnOROff(boolean bOn) { + byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) id, (byte) 0x00 /* source */, (byte) 0x06, (byte) 0x01, + (bOn) ? (byte) 0x0A : (byte) 0x04 }; + + logger.debug("sendPumpOnOROff: {}", bOn); + if (checkOtherMaster()) { + logger.info("Unable to send command to pump as there is another master in the system"); + return; + } + + if (!writePacket(packet, 0x06, 1)) { + logger.debug("sendPumpOnOROff: Timeout"); + } + } + + public void setPumpOnOROff(boolean bOn) { + logger.debug("setPumpOnOROff: {}", bOn); + + if (!bOn) { + helperClearPrograms(0); + } + + runmode = bOn; + + sendLocalORRemoteControl(false); + sendPumpOnOROff(bOn); + sendRequestPumpStatus(); + sendLocalORRemoteControl(true); + } + + // sendPumpRPM - low-level call to send to pump the RPM command + public void sendPumpRPM(int rpm) { + int rpmH, rpmL; + + logger.debug("sendPumpRPM: {}", rpm); + if (checkOtherMaster()) { + logger.info("Unable to send command to pump as there is another master in the system"); + return; + } + + rpmH = rpm / 256; + rpmL = rpm % 256; + + byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) id, (byte) 0x00 /* source */, (byte) 0x01, (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); + } + + if (!writePacket(packet, 0x01, 1)) { + logger.debug("sendPumpRPM: timeout"); + } + } + + // setPumpRPM - high-level call that includes wrapper commands and delay functions + public void setPumpRPM(int rpm) { + logger.debug("setPumpRPM: {}", rpm); + + helperClearPrograms(0); + + runmode = true; + + sendLocalORRemoteControl(false); + sendPumpRPM(rpm); + sendPumpOnOROff(true); + sendRequestPumpStatus(); + sendLocalORRemoteControl(true); + } + + // sendRunProgram - low-level call to send the command to pump + public void sendRunProgram(int program) { + logger.debug("sendRunProgram: {}", program); + + if (checkOtherMaster()) { + logger.info("Unable to send command to pump as there is another master in the system"); + return; + } + + if (program < 1 || program > 4) { + return; + } + + byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) id, (byte) 0x00 /* source */, (byte) 0x01, (byte) 0x04, + (byte) 0x03, (byte) 0x21, (byte) 0x00, (byte) (program << 3) }; + + if (!writePacket(packet, 0x06, 1)) { + logger.debug("sendRunProgram: Timeout"); + } + } + + // setRunProgram - high-level call to run program - including wrapper calls + public void setRunProgram(int program) { + logger.debug("setRunProgram: {}", program); + + helperClearPrograms(program); + + runmode = true; + + sendLocalORRemoteControl(false); + sendRunProgram(program); + sendPumpOnOROff(true); + sendRequestPumpStatus(); + sendLocalORRemoteControl(true); + } + + // helperClearPrograms - turns off any other channels/items that were used to start the pump + public void helperClearPrograms(int program) { + logger.debug("helperClearProgram = {}", program); + + if (program != 1) { + logger.debug("Turn off program1"); + updateState(INTELLIFLO_PROGRAM1, OnOffType.OFF); + } else { + updateState(INTELLIFLO_PROGRAM1, OnOffType.ON); + } + + if (program != 2) { + logger.debug("Turn off program2"); + updateState(INTELLIFLO_PROGRAM2, OnOffType.OFF); + } else { + updateState(INTELLIFLO_PROGRAM2, OnOffType.ON); + } + + if (program != 3) { + logger.debug("Turn off program3"); + updateState(INTELLIFLO_PROGRAM3, OnOffType.OFF); + } else { + updateState(INTELLIFLO_PROGRAM3, OnOffType.ON); + } + + if (program != 4) { + logger.debug("Turn off program4"); + updateState(INTELLIFLO_PROGRAM4, OnOffType.OFF); + } else { + updateState(INTELLIFLO_PROGRAM4, OnOffType.ON); + } } @Override public void handleCommand(ChannelUID channelUID, Command command) { - if (command instanceof RefreshType) { - logger.debug("Intellflo received refresh command"); - updateChannel(channelUID.getId(), null); + logger.debug("handleCommand, {}, {}", channelUID, command); + if (command instanceof OnOffType) { + boolean state = ((OnOffType) command) == OnOffType.ON; + + switch (channelUID.getId()) { + case INTELLIFLO_RUN: + case INTELLIFLO_RPM: + setPumpOnOROff(state); + break; + case INTELLIFLO_PROGRAM1: + if (state) { + setRunProgram(1); + } else { + setPumpOnOROff(false); + } + break; + case INTELLIFLO_PROGRAM2: + if (state) { + setRunProgram(2); + } else { + setPumpOnOROff(false); + } + break; + case INTELLIFLO_PROGRAM3: + if (state) { + setRunProgram(3); + } else { + setPumpOnOROff(false); + } + break; + case INTELLIFLO_PROGRAM4: + if (state) { + setRunProgram(4); + } else { + setPumpOnOROff(false); + } + break; + } + } else if (command instanceof DecimalType) { + int num = ((DecimalType) command).intValue(); + + switch (channelUID.getId()) { + case INTELLIFLO_RPM: + setPumpRPM(num); + break; + } } } @Override public void processPacketFrom(PentairPacket p) { + if (waitStatusForOnline) { + finishOnline(); + waitStatusForOnline = false; + } + switch (p.getAction()) { case 1: // Pump command - A5 00 10 60 01 02 00 20 - logger.trace("Pump command (ack): {}: ", p); + logger.debug("Pump command (ack): {}: ", p.toString()); break; case 4: // Pump control panel on/off - logger.trace("Turn pump control panel (ack) {}: {} - {}", p.getSource(), - p.getByte(PentairPacket.STARTOFDATA), p); + boolean remotemode; + + remotemode = p.getByte(0) == (byte) 0xFF; + logger.debug("Pump control panel (ack) {}: {} - {}", p.getSource(), remotemode, p); + break; - case 5: // Set pump mode - logger.trace("Set pump mode (ack) {}: {} - {}", p.getSource(), p.getByte(PentairPacket.STARTOFDATA), p); + case 5: // Set pump mode ack + logger.debug("Set pump mode (ack) {}: {} - {}", p.getSource(), p.getByte(0), p); break; - case 6: // Set run mode - logger.trace("Set run mode (ack) {}: {} - {}", p.getSource(), p.getByte(PentairPacket.STARTOFDATA), p); + case 6: // Set run mode ack + logger.debug("Set run mode (ack) {}: {} - {}", p.getSource(), p.getByte(0), p); break; case 7: // Pump status (after a request) if (p.getLength() != 15) { @@ -89,6 +438,8 @@ public void processPacketFrom(PentairPacket p) { return; } + PentairPacketPumpStatus pps = new PentairPacketPumpStatus(p); + /* * P: A500 d=10 s=60 c=07 l=0f 0A0602024A08AC120000000A000F22 <028A> * RUN 0a Started @@ -107,23 +458,13 @@ public void processPacketFrom(PentairPacket p) { 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); + updateChannel(INTELLIFLO_RUN, pps.run); + updateChannelPower(INTELLIFLO_POWER, pps.power); + updateChannel(INTELLIFLO_RPM, pps.rpm); + updateChannel(INTELLIFLO_GPM, pps.gpm); + updateChannel(INTELLIFLO_ERROR, pps.error); + updateChannel(INTELLIFLO_STATUS1, pps.status11); + updateChannel(INTELLIFLO_STATUS2, pps.status12); break; default: @@ -133,57 +474,21 @@ public void processPacketFrom(PentairPacket p) { } /** - * 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. + * Helper function to update channel. */ - 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, (ppscur.run) ? OnOffType.ON : OnOffType.OFF); - } - 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)); - } - break; - } + public void updateChannel(String channel, boolean value) { + updateState(channel, (value) ? OnOffType.ON : OnOffType.OFF); + } + + public void updateChannel(String channel, int value) { + updateState(channel, new DecimalType(value)); + } + + public void updateChannel(String channel, String value) { + updateState(channel, new StringType(value)); + } + + public void updateChannelPower(String channel, int value) { + updateState(channel, new QuantityType<>(value, SmartHomeUnits.WATT)); } } 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 f4ac00a9e74b2..1381099f07282 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 @@ -16,101 +16,113 @@ import java.io.BufferedOutputStream; import java.io.IOException; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ThingStatus; import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.io.transport.serial.PortInUseException; +import org.eclipse.smarthome.io.transport.serial.SerialPort; +import org.eclipse.smarthome.io.transport.serial.SerialPortIdentifier; +import org.eclipse.smarthome.io.transport.serial.SerialPortManager; +import org.eclipse.smarthome.io.transport.serial.UnsupportedCommOperationException; import org.openhab.binding.pentair.internal.config.PentairSerialBridgeConfig; 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} * * @author Jeff James - initial contribution * */ +@NonNullByDefault public class PentairSerialBridgeHandler extends PentairBaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(PentairSerialBridgeHandler.class); + public PentairSerialBridgeConfig config = new PentairSerialBridgeConfig(); /** SerialPort object representing the port where the RS485 adapter is connected */ - SerialPort port; + private final SerialPortManager serialPortManager; + private @Nullable SerialPort serialPort; + private @NonNullByDefault({}) SerialPortIdentifier portIdentifier; - public PentairSerialBridgeHandler(Bridge bridge) { + public PentairSerialBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) { super(bridge); + this.serialPortManager = serialPortManager; } @Override - protected synchronized void connect() { - PentairSerialBridgeConfig configuration = getConfigAs(PentairSerialBridgeConfig.class); + protected synchronized int connect() { + logger.debug("PentairSerialBridgeHander: connect"); + config = getConfigAs(PentairSerialBridgeConfig.class); + + if (config.serialPort.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "no serial port configured"); + return -1; + } + + this.id = config.id; + logger.debug("Serial port id: {}", id); + this.discovery = config.discovery; + + portIdentifier = serialPortManager.getIdentifier(config.serialPort); + if (portIdentifier == null) { + logger.debug("Serial Error: Port {} does not exist.", config.serialPort); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Configured serial port does not exist"); + return -1; + } try { - CommPortIdentifier ci = CommPortIdentifier.getPortIdentifier(configuration.serialPort); - CommPort cp = ci.open("openhabpentairbridge", 10000); - if (cp == null) { - throw new IllegalStateException("cannot open serial port!"); - } + logger.debug("connect port: {}", config.serialPort); - if (cp instanceof SerialPort) { - port = (SerialPort) cp; - } else { - throw new IllegalStateException("unknown port type"); - } - port.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); - port.disableReceiveFraming(); - port.disableReceiveThreshold(); + SerialPort serialPort = portIdentifier.open("org.openhab.binding.pentair", 10000); + serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); + serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE); + + // Note: The V1 code called disableReceiveFraming() and disableReceiveThreshold() here + // port.disableReceiveFraming(); + // port.disableReceiveThreshold(); - reader = new BufferedInputStream(port.getInputStream()); - writer = new BufferedOutputStream(port.getOutputStream()); - logger.info("Pentair Bridge connected to serial port: {}", configuration.serialPort); + reader = new BufferedInputStream(serialPort.getInputStream()); + writer = new BufferedOutputStream(serialPort.getOutputStream()); } catch (PortInUseException e) { - String msg = String.format("cannot open serial port: %s", configuration.serialPort); + String msg = String.format("cannot open serial port: %s", config.serialPort); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; + return -1; } 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); + String msg = String.format("got unsupported operation %s on port %s", e.getMessage(), config.serialPort); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; + return -2; } catch (IOException e) { - String msg = String.format("IOException on port %s", configuration.serialPort); + String msg = String.format("got IOException %s on port %s", e.getMessage(), config.serialPort); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg); - return; + return -2; } + // if you have gotten this far, you should be connected to the serial port + logger.info("Pentair Bridge connected to serial port: {}", config.serialPort); + parser = new Parser(); thread = new Thread(parser); thread.start(); - if (port != null && reader != null && writer != null) { - updateStatus(ThingStatus.ONLINE); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unable to connect"); - } + updateStatus(ThingStatus.ONLINE); + + return 0; } @Override protected synchronized void disconnect() { + logger.debug("PentairSerialBridgeHandler: disconnect"); updateStatus(ThingStatus.OFFLINE); if (thread != null) { try { thread.interrupt(); - thread.join(); // wait for thread to complete + if (thread != null) { + thread.join(); // wait for thread to complete + } } catch (InterruptedException e) { // do nothing } @@ -122,7 +134,7 @@ protected synchronized void disconnect() { try { reader.close(); } catch (IOException e) { - logger.trace("IOException when closing serial reader", e); + logger.trace("IOException when closing serial reader: {}", e.toString()); } reader = null; } @@ -131,14 +143,14 @@ protected synchronized void disconnect() { try { writer.close(); } catch (IOException e) { - logger.trace("IOException when closing serial writer", e); + logger.trace("IOException when closing serial writer: {}", e.toString()); } writer = null; } - if (port != null) { - port.close(); - port = null; + if (serialPort != null) { + serialPort.close(); + serialPort = null; } } } diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/controller.xml b/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/controller.xml new file mode 100644 index 0000000000000..5582d7cd98268 --- /dev/null +++ b/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/controller.xml @@ -0,0 +1,384 @@ + + + + + + + + + + Pentair Controller + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FWVersion + + + + + + The ID of the device (in decimal, not hex) + 16 + + + + + Enables automatic synchoronization of the pool controller clock with the system clock + true + + + + + + + + + + Circuit + + + + + + + + + + + Features + + + + + + + + + + + Heat + + + + + + + + + + schedule + + + + + + + + + + + + + General status channels for controller + + + + + + + + + + + + + + + Number:Temperature + + Water temperature. Only valid when pool pump is running. + + + + + Number:Temperature + + Solar temperature. + + + + + Number:Temperature + + Air temperature. + + + + + Switch + + Auxillary Switch + + + + Number:Time + + Cumulative minutes run + + + + + String + + Name of circuit + + + + + String + + Function of circuit + + + + + String + + Heat mode + + + + + + + + + + + + + Number + + Heat active state + + + + + Number:Temperature + + Temperature set point + + + + + String + + Light mode + + + + + + + + + + + + + + + + + + + + + + + + String + + >Type of schedule (None, Normal, EggTimer, OnceOnly) + + + + + + + + + + + + String + + String format of schedule + + + + Number + + Start Time in minutes + + + + + Number:time + + End time (or duration for Egg Timer) of schedule in minutes + + + + + Number + + >Circuit + + + + String + + >Days + + + + String + + Unit of measure + + + + + + + + + + Switch + + Controller is in service mode + + + + + Switch + + Solar heater is on + + + + + Switch + + Heater is on + + + + + diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/easytouch.xml b/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/easytouch.xml deleted file mode 100644 index 37db93dac7412..0000000000000 --- a/bundles/org.openhab.binding.pentair/src/main/resources/ESH-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/ESH-INF/thing/intellichlor.xml b/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/intellichlor.xml index a5f133af04b96..243056823c984 100644 --- a/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/intellichlor.xml +++ b/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/intellichlor.xml @@ -6,8 +6,8 @@ - - + + diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/intelliflo.xml b/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/intelliflo.xml index f8968beeb6eba..0e71a5df69148 100644 --- a/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/intelliflo.xml +++ b/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/intelliflo.xml @@ -6,20 +6,25 @@ - - + + - + Pentair Intelliflo Pump - - - - - - + + + + + + + + + + + @@ -31,46 +36,59 @@ - - Number - - Pump mode - - - Switch Indicator on whether the pump is running or not. - + - + Number Pump RPM - + - + Number - - Pump power - + + Pump GPM + - - Number - - Pump PPC - + + Number:Power + + Pump power + Number Pump Error - + + + + + Number + + Pump Status 1 + + + + + Number + + Pump Status 2 + + + + + Switch + + Pump program switch diff --git a/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/ip_bridge.xml b/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/ip_bridge.xml index 5881cc471a311..d5e58924f8c07 100644 --- a/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/ip_bridge.xml +++ b/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/ip_bridge.xml @@ -12,6 +12,7 @@ The IP address to connect to. network-address + 127.0.0.1 @@ -25,6 +26,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/main/resources/ESH-INF/thing/serial_bridge.xml b/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/serial_bridge.xml index 40b04c7f553f7..dfae1413d3261 100644 --- a/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/serial_bridge.xml +++ b/bundles/org.openhab.binding.pentair/src/main/resources/ESH-INF/thing/serial_bridge.xml @@ -4,12 +4,12 @@ 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. - + The serial port name. Valid values are e.g. COM1 for Windows and /dev/ttyS0 or /dev/ttyUSB0 for Linux. @@ -18,6 +18,12 @@ The ID to use to send commands on the Pentair bus (default: 34) 34 + + + + Enable automatic discovery of devices + true +