]> git.basschouten.com Git - openhab-addons.git/commitdiff
[omnilink] Initial contribution (#8922)
authorEthan Dye <mrtops03@gmail.com>
Tue, 19 Jan 2021 23:31:10 +0000 (16:31 -0700)
committerGitHub <noreply@github.com>
Tue, 19 Jan 2021 23:31:10 +0000 (00:31 +0100)
Signed-off-by: Ethan Dye <mrtops03@gmail.com>
Co-authored-by: Dan Cunningham <dan@digitaldan.com>
Co-authored-by: Craig Hamilton <craigh@quailholdings.com>
Co-authored-by: Brian O'Connell <boc@us.ibm.com>
Co-authored-by: Dan Cunningham <dan@digitaldan.com>
Co-authored-by: Craig Hamilton <craigh@quailholdings.com>
Co-authored-by: Brian O'Connell <boc@us.ibm.com>
53 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.omnilink/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/README.md [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/AreaAlarm.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/AudioPlayer.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/OmnilinkBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/OmnilinkHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/SystemType.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/config/OmnilinkBridgeConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/ObjectPropertyRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/ObjectPropertyRequests.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/OmnilinkDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractAreaHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkStatusHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioSourceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioZoneHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/BridgeOfflineException.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ButtonHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ConsoleHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/HumiditySensorHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LockHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LuminaAreaHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniAreaHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniLinkCmd.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmnilinkBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TempSensorHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TemperatureFormat.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ThermostatHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/UnitHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ZoneHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/DimmableUnitHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/FlagHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/OutputHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/UpbRoomHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/dimmable/UpbUnitHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/area.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/audio-source.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/audio-zone.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/bridge.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/button.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/console.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/humidity-sensor.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/lock.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/temp-sensor.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/thermostat.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/unit.xml [new file with mode: 0644]
bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/zone.xml [new file with mode: 0644]
bundles/pom.xml

index 868649530d67760c2b6823430a24a4d223157754..9cb676a5c0f5d366767dc5267f7816bb6aeebb9c 100644 (file)
 /bundles/org.openhab.binding.oceanic/ @kgoderis
 /bundles/org.openhab.binding.ojelectronics/ @EvilPingu
 /bundles/org.openhab.binding.omnikinverter/ @hansbogert
+/bundles/org.openhab.binding.omnilink/ @ecdye
 /bundles/org.openhab.binding.onebusaway/ @sdwilsh
 /bundles/org.openhab.binding.onewire/ @J-N-K
 /bundles/org.openhab.binding.onewiregpio/ @aogorek
index 3c733884952ee41388bf011e3d764726120a9700..cdc57aa16edd3d4271788d7684b17520d33b13d7 100644 (file)
       <artifactId>org.openhab.binding.omnikinverter</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.omnilink</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.onebusaway</artifactId>
diff --git a/bundles/org.openhab.binding.omnilink/NOTICE b/bundles/org.openhab.binding.omnilink/NOTICE
new file mode 100644 (file)
index 0000000..88908f5
--- /dev/null
@@ -0,0 +1,20 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
+
+== Third-party Content
+
+jomnilink
+* License: EPL-2.0
+* Project: https://github.com/digitaldan/jomnilink
+* Source:  https://github.com/digitaldan/jomnilink
diff --git a/bundles/org.openhab.binding.omnilink/README.md b/bundles/org.openhab.binding.omnilink/README.md
new file mode 100644 (file)
index 0000000..8a2d454
--- /dev/null
@@ -0,0 +1,321 @@
+# HAI/Leviton OmniLink Binding
+
+This binding integrates the [OmniPro and Lumina](http://www.leviton.com/en/products/security-automation/automation-av-controllers/omni-security-systems) line of home automation systems.
+At its core the OmniPro is a hardware board that provides security and access features.
+It connects to many other devices through serial ports or wired contacts and exposes them through a single TCP based API.
+
+## Supported Things
+
+The OmniPro/Lumina controller acts as a "bridge" for accessing other connected devices.
+
+
+| Omni type                  | Hardware Type                                    | Things                            |
+|:---------------------------|:-------------------------------------------------|:----------------------------------|
+| Controller                 | Omni (Pro II, IIe, LTe), Lumina                  | `controller` (omni, lumina)       |
+| Lights                     | Built-in, UPB, HLC                               | `unit`, `dimmable`, `upb`, `room` |
+| Thermostats                | Omnistat, Omnistat2                              | `thermostat`                      |
+| Temperature Sensors        | 31A00-1/31A00-7                                  | `temp_sensor`                     |
+| Humidity Sensors           | 31A00-2                                          | `humidity_sensor`                 |
+| Zones                      | Built-in/Hardwire, GE Wireless                   | `zone`                            |
+| Audio Zones/Sources        | HAI Hi-Fi, Russound, NuVo, Xantech, Speakercraft | `audio_zone`, `audio_source`      |
+| Consoles                   | HAI Omni Console, HAI Lumina Console             | `console`                         |
+| Areas                      | Built-in                                         | `area`, `lumina_area`             |
+| Buttons                    | Built-in                                         | `button`                          |
+| Flags                      | Built-in                                         | `flag`                            |
+| Output                     | Built-in/Hardwire                                | `output`                          |
+| Access Control Reader Lock | Leviton Access Control Reader                    | `lock`                            |
+
+
+
+## Discovery
+
+### Controller
+
+Omni and Lumina controllers must be manually added using the IP and port of the controller as well as the 2 encryption keys required for network access.
+
+### Devices
+
+Once a connection can be established to a controller, all connected devices will be automatically discovered and added to the inbox.
+
+## Thing Configuration
+
+An Omni or Lumina controller requires the IP address (`ipAddress`), optional port (`port` defaults to 4369), and 2 encryption keys (`key1`, `key2`).
+The hexadecimal pairs in the encryption keys are typically delimited using a colon`:`, but dashes `-`, spaces ` ` or no delimiter may be used.
+
+In the thing file, this looks like:
+
+```
+Bridge omnilink:controller:home [ ipAddress="127.0.0.1", port=4369, key1="XXXXXXXXXXXXXXXX", key2="XXXXXXXXXXXXXXXX" ] {
+    // Add your things here
+}
+```
+
+The devices are identified by the device number that the OmniLink bridge assigns to them, see the [Full Example](#full-example) section below for a manual configuration example.
+
+## Channels
+
+The devices support some of the following channels:
+
+| Channel Type ID             | Item Type            | Description                                                                          | Thing types supporting this channel                 |
+|-----------------------------|----------------------|--------------------------------------------------------------------------------------|-----------------------------------------------------|
+| `activate_keypad_emergency` | Number               | Activate a burglary, fire, or auxiliary keypad emergency alarm on Omni based models. | `area`                                              |
+| `alarm_burglary`            | Switch               | Indicates if a burglary alarm is active.                                             | `area`                                              |
+| `alarm_fire`                | Switch               | Indicates if a fire alarm is active.                                                 | `area`                                              |
+| `alarm_gas`                 | Switch               | Indicates if a gas alarm is active.                                                  | `area`                                              |
+| `alarm_auxiliary`           | Switch               | Indicates if a auxiliary alarm is active.                                            | `area`                                              |
+| `alarm_freeze`              | Switch               | Indicates if a freeze alarm is active.                                               | `area`                                              |
+| `alarm_water`               | Switch               | Indicates if a water alarm is active.                                                | `area`                                              |
+| `alarm_duress`              | Switch               | Indicates if a duress alarm is active.                                               | `area`                                              |
+| `alarm_temperature`         | Switch               | Indicates if a temperature alarm is active.                                          | `area`                                              |
+| `mode`                      | Number               | Represents the area security mode.                                                   | `area`, `lumina_area`                               |
+| `disarm`                    | String               | Send a 4 digit user code to disarm the system.                                       | `area`                                              |
+| `day`                       | String               | Send a 4 digit user code to arm the system to day.                                   | `area`                                              |
+| `night`                     | String               | Send a 4 digit user code to arm the system to night.                                 | `area`                                              |
+| `away`                      | String               | Send a 4 digit user code to arm the system to away.                                  | `area`                                              |
+| `vacation`                  | String               | Send a 4 digit user code to arm the system to vacation.                              | `area`                                              |
+| `day_instant`               | String               | Send a 4 digit user code to arm the system to day instant.                           | `area`                                              |
+| `night_delayed`             | String               | Send a 4 digit user code to arm the system to night delayed.                         | `area`                                              |
+| `home`                      | String               | Send a 4 digit user code to set the system to home.                                  | `lumina_area`                                       |
+| `sleep`                     | String               | Send a 4 digit user code to set the system to sleep.                                 | `lumina_area`                                       |
+| `away`                      | String               | Send a 4 digit user code to set the system to away.                                  | `lumina_area`                                       |
+| `vacation`                  | String               | Send a 4 digit user code to set the system to vacation.                              | `lumina_area`                                       |
+| `party`                     | String               | Send a 4 digit user code to set the system to party.                                 | `lumina_area`                                       |
+| `special`                   | String               | Send a 4 digit user code to set the system to special.                               | `lumina_area`                                       |
+| `source_text_{1,2,3,4,5,6}` | String               | A line of metadata from this audio source.                                           | `audio_source`                                      |
+| `polling`                   | Switch               | Enable or disable polling of this audio source.                                      | `audio_source`                                      |
+| `zone_power`                | Switch               | Power status of this audio zone.                                                     | `audio_zone`                                        |
+| `zone_mute`                 | Switch               | Mute status of this audio zone.                                                      | `audio_zone`                                        |
+| `zone_volume`               | Dimmer               | Volume level of this audio zone.                                                     | `audio_zone`                                        |
+| `zone_source`               | Number               | Source for this audio zone.                                                          | `audio_zone`                                        |
+| `zone_control`              | Player               | Control the audio zone, e.g. start/stop/next/previous.                               | `audio_zone`                                        |
+| `sysdate`                   | DateTime             | Set controller date/time.                                                            | `controller`                                        |
+| `last_log`                  | String               | Last log message on the controller, represented in JSON.                             | `controller`                                        |
+| `enable_disable_beeper`     | Switch               | Enable/Disable the beeper for this/all console(s).                                   | `controller`, `console`                             |
+| `beep`                      | Switch               | Send a beep command to this/all console(s).                                          | `controller`, `console`                             |
+| `press`                     | Switch               | Sends a button event to the controller.                                              | `button`                                            |
+| `low_setpoint`              | Number               | The current low setpoint for this humidity/temperature sensor.                       | `temp_sensor`, `humidity_sensor`                    |
+| `high_setpoint`             | Number               | The current high setpoint for this humidity/temperature sensor.                      | `temp_sensor`, `humidity_sensor`                    |
+| `temperature`               | Number:Temperature   | The current temperature at this thermostat/temperature sensor.                       | `thermostat`, `temp_sensor`                         |
+| `humidity`                  | Number:Dimensionless | The current relative humidity at this thermostat/humidity sensor.                    | `thermostat`, `humidity_sensor`                     |
+| `freeze_alarm`              | Contact              | Closed when freeze alarm is triggered by this thermostat.                            | `thermostat`                                        |
+| `comm_failure`              | Contact              | Closed during a communications failure with this thermostat.                         | `thermostat`                                        |
+| `outdoor_temperature`       | Number:Temperature   | The current outdoor temperature detected by this thermostat.                         | `thermostat`                                        |
+| `heat_setpoint`             | Number:Temperature   | The current low/heating setpoint of this thermostat.                                 | `thermostat`                                        |
+| `cool_setpoint`             | Number:Temperature   | The current high/cooling setpoint of this thermostat.                                | `thermostat`                                        |
+| `humidify_setpoint`         | Number:Dimensionless | The current low/humidify setpoint for this thermostat.                               | `thermostat`                                        |
+| `dehumidify_setpoint`       | Number:Dimensionless | The current high/dehumidify setpoint for this thermostat.                            | `thermostat`                                        |
+| `system_mode`               | Number               | The current system mode of this thermostat.                                          | `thermostat`                                        |
+| `fan_mode`                  | Number               | The current fan mode of this thermostat.                                             | `thermostat`                                        |
+| `hold_status`               | Number               | The current hold status of this thermostat.                                          | `thermostat`                                        |
+| `status`                    | Number               | The current numeric status of this thermostat.                                       | `thermostat`                                        |
+| `level`                     | Dimmer               | Increase/Decrease the level of this unit/dimmable unit/UPB unit.                     | `unit`, `dimmable`, `upb`                           |
+| `switch`                    | Switch               | Turn this unit/dimmable unit/flag/output/room on/off.                                | `unit`, `dimmable`, `upb`, `flag`, `output`, `room` |
+| `on_for_seconds`            | Number               | Turn on this unit for a specified number of seconds.                                 | `unit`, `dimmable`, `upb`, `flag`, `output`         |
+| `off_for_seconds`           | Number               | Turn off this unit for a specified number of seconds.                                | `unit`, `dimmable`, `upb`, `flag`, `output`         |
+| `on_for_minutes`            | Number               | Turn on this unit for a specified number of minutes.                                 | `unit`, `dimmable`, `upb`, `flag`, `output`         |
+| `off_for_minutes`           | Number               | Turn off this unit for a specified number of minutes.                                | `unit`, `dimmable`, `upb`, `flag`, `output`         |
+| `on_for_hours`              | Number               | Turn on this unit for a specified number of hours.                                   | `unit`, `dimmable`, `upb`, `flag`, `output`         |
+| `off_for_hours`             | Number               | Turn off this unit for a specified number of hours.                                  | `unit`, `dimmable`, `upb`, `flag`, `output`         |
+| `upb_status`                | String               | Send a UPB status request message for this UPB unit to the controller.               | `upb`                                               |
+| `value`                     | Number               | Numeric value of this flag.                                                          | `flag`                                              |
+| `scene_{a,b,c,d}`           | Switch               | Turn this scene on/off.                                                              | `room`                                              |
+| `state`                     | Number               | The current state of this room.                                                      | `room`                                              |
+| `contact`                   | Contact              | Contact state information of this zone.                                              | `zone`                                              |
+| `current_condition`         | Number               | Current condition of this zone.                                                      | `zone`                                              |
+| `latched_alarm_status`      | Number               | Latched alarm status of this zone.                                                   | `zone`                                              |
+| `arming_status`             | Number               | Arming status of this zone.                                                          | `zone`                                              |
+| `bypass`                    | String               | Send a 4 digit user code to bypass this zone.                                        | `zone`                                              |
+| `restore`                   | String               | Send a 4 digit user code to restore this zone.                                       | `zone`                                              |
+
+
+### Trigger Channels
+
+The devices support some of the following trigger channels:
+
+| Channel Type ID               | Description                                                                          | Thing types supporting this channel |
+|-------------------------------|--------------------------------------------------------------------------------------|-------------------------------------|
+| `all_on_off_event`            | Event sent when an all on/off event occurs.                                          | `area`, `lumina_area`               |
+| `phone_line_event`            | Event sent when the phone line changes state.                                        | `controller`                        |
+| `ac_power_event`              | Event sent when AC trouble conditions are detected.                                  | `controller`                        |
+| `battery_event`               | Event sent when battery trouble conditions are detected.                             | `controller`                        |
+| `dcm_event`                   | Event sent when digital communicator trouble conditions are detected.                | `controller`                        |
+| `energy_cost_event`           | Event sent when the cost of energy changes.                                          | `controller`                        |
+| `camera_trigger_event`        | Event sent when a camera trigger is detected.                                        | `controller`                        |
+| `upb_link_activated_event`    | Event sent when a UPB link is activated.                                             | `controller`                        |
+| `upb_link_deactivated_event`  | Event sent when a UPB link is deactivated.                                           | `controller`                        |
+| `activated_event`             | Event sent when a button is activated.                                               | `button`                            |
+| `switch_press_event`          | Event sent when an ALC, UPB, Radio RA, or Starlite switch is pressed.                | `dimmable`, `upb`                   |
+
+
+## Full Example
+
+### Example `omnilink.things`
+
+```
+Bridge omnilink:controller:home [ ipAddress="127.0.0.1", port=4369, key1="XXXXXXXXXXXXXXXX", key2="XXXXXXXXXXXXXXXX" ] {
+    Thing area         MainArea         "Main Area"              @   "Home"                    [ number=1 ]
+    Thing upb          UpKitTable       "Table Lights"           @   "Upstairs Kitchen"        [ number=4 ]
+    Thing upb          UpOfcDesk        "Desk Lights"            @   "Upstairs Office"         [ number=10 ]
+    Thing thermostat   UpstrsThermo     "Upstairs Temperature"   @   "Upstairs Entry"          [ number=1 ]
+    Thing zone         FrontDoor        "Front Door"             @   "Upstairs Entry"          [ number=2 ]
+    Thing zone         GarageDoor       "Garage Door"            @   "Laundry Room"            [ number=3 ]
+    Thing zone         BackDoor         "Back Door"              @   "Upstairs Kitchen"        [ number=4 ]
+    Thing zone         OneCarGarageDo   "One Car Garage"         @   "Garage"                  [ number=5 ]
+    Thing zone         TwoCarGarageDo   "Two Car Garage"         @   "Garage"                  [ number=6 ]
+    Thing zone         BsmtBackDoor     "Back Door"              @   "Basement Workout Room"   [ number=8 ]
+    Thing zone         MBRDeckDoor      "Deck Door"              @   "Master Bedroom"          [ number=9 ]
+    Thing zone         MBRMotion        "Motion"                 @   "Master Bedroom"          [ number=10 ]
+    Thing zone         PorchDoor        "Porch Door"             @   "Upstairs Office"         [ number=11 ]
+    Thing zone         UpOffMotion      "Motion"                 @   "Upstairs Office"         [ number=12 ]
+    Thing zone         UpLivMotion      "Motion"                 @   "Upstairs Living Room"    [ number=13 ]
+    Thing zone         BsmtWORMotion    "Motion"                 @   "Basement Workout Room"   [ number=14 ]
+    Thing zone         GarageMotion     "Motion"                 @   "Garage"                  [ number=15 ]
+    Thing console      UpstrsConsole    "Console"                @   "Laundry Room"            [ number=1 ]
+    Thing button       MainButton       "Button"                 @   "Home"                    [ number=1 ]
+}
+```
+
+### Example `omnilink.items`
+
+```
+/*
+ * Alarms / Areas
+ */
+Group:Switch:OR(ON, OFF) Alarms "All Alarms [%s]"
+String    AlarmMode          "Alarm [%s]"              <alarm>               {channel="omnilink:area:home:MainArea:mode" [profile="transform:MAP", function="area-modes.map", sourceFormat="%s"]}
+Switch    AlarmBurglary      "Burglary Alarm [%s]"               (Alarms)    {channel="omnilink:area:home:MainArea:alarm_burglary"}
+Switch    AlarmFire          "Fire Alarm [%s]"                   (Alarms)    {channel="omnilink:area:home:MainArea:alarm_fire"}
+Switch    alarm_gas          "Gas Alarm [%s]"                    (Alarms)    {channel="omnilink:area:home:MainArea:alarm_gas"}
+Switch    AlarmAuxiliary     "Auxiliary Alarm [%s]"              (Alarms)    {channel="omnilink:area:home:MainArea:alarm_auxiliary"}
+Switch    AlarmFreeze        "Freeze Alarm [%s]"                 (Alarms)    {channel="omnilink:area:home:MainArea:alarm_freeze"}
+Switch    AlarmWater         "Water Alarm [%s]"                  (Alarms)    {channel="omnilink:area:home:MainArea:alarm_water"}
+Switch    AlarmDuress        "Duress Alarm [%s]"                 (Alarms)    {channel="omnilink:area:home:MainArea:alarm_duress"}
+Switch    AlarmTemperature   "Temperature Alarm [%s]"            (Alarms)    {channel="omnilink:area:home:MainArea:alarm_temperature"}
+Number    AlarmModeDisarm                                                    {channel="omnilink:area:home:MainArea:disarm"}
+Number    AlarmModeDay                                                       {channel="omnilink:area:home:MainArea:day"}
+Number    AlarmModeNight                                                     {channel="omnilink:area:home:MainArea:night"}
+Number    AlarmModeAway                                                      {channel="omnilink:area:home:MainArea:away"}
+Number    AlarmModeVacation                                                  {channel="omnilink:area:home:MainArea:vacation"}
+Number    AlarmModeDayInstant                                                {channel="omnilink:area:home:MainArea:day_instant"}
+Number    AlarmModeNightDelayed                                              {channel="omnilink:area:home:MainArea:night_delayed"}
+
+/*
+ * Lights
+ */
+Switch   UpKitTable     "Table Lights [%s]"   <switch>   {channel="omnilink:upb:home:UpKitTable:level"}
+Dimmer   UpOfcDesk      "Desk Lights [%d]"    <slider>   {channel="omnilink:upb:home:UpOfcDesk:level"}
+
+/*
+ * Thermostat
+ */
+Group                UpstrsThermo             "Upstairs Thermostat"
+Number:Temperature   UpstrsThermo_Temp        "Temperature [%.1f %unit%]"                  <temperature>        (UpstrsThermo)   {channel="omnilink:thermostat:home:UpstrsThermo:temperature"}
+Number               UpstrsThermo_Status      "Status [MAP(therm-status.map):%s]"          <heating>            (UpstrsThermo)   {channel="omnilink:thermostat:home:UpstrsThermo:status"}
+Number               UpstrsThermo_System      "System Mode [MAP(therm-tempmode.map):%s]"   <temperature>        (UpstrsThermo)   {channel="omnilink:thermostat:home:UpstrsThermo:system_mode"}
+Number               UpstrsThermo_Fan         "Fan Mode [MAP(therm-fanmode.map):%s]"       <fan>                (UpstrsThermo)   {channel="omnilink:thermostat:home:UpstrsThermo:fan_mode"}
+Number               UpstrsThermo_Hold        "Hold Mode [MAP(therm-holdmode.map):%s]"     <fan>                (UpstrsThermo)   {channel="omnilink:thermostat:home:UpstrsThermo:hold_mode"}
+Number               UpstrsThermo_HeatPoint   "System HeatPoint [%d]"                      <temperature_hot>    (UpstrsThermo)   {channel="omnilink:thermostat:home:UpstrsThermo:heat_setpoint"}
+Number               UpstrsThermo_CoolPoint   "System CoolPoint [%d]"                      <temperature_cool>   (UpstrsThermo)   {channel="omnilink:thermostat:home:UpstrsThermo:cool_setpoint"}
+
+/*
+ * Motion and Doors
+ */
+Group:Contact:OR(OPEN, CLOSED)   Doors         "All Doors [%s]"
+Contact   FrontDoor        "Front Door"            <door>         (Doors)            {channel="omnilink:zone:home:FrontDoor:contact"}
+Contact   GarageDoor       "Garage Door"           <door>         (Doors)            {channel="omnilink:zone:home:GarageDoor:contact"}
+Contact   BackDoor         "Back Door"             <door>         (Doors)            {channel="omnilink:zone:home:BackDoor:contact"}
+Contact   BsmtBackDoor     "Back Door"             <door>         (Doors)            {channel="omnilink:zone:home:BsmtBackDoor:contact"}
+Contact   MBRDeckDoor      "Deck Door"             <door>         (Doors)            {channel="omnilink:zone:home:MBRDeckDoor:contact"}
+Contact   PorchDoor        "Porch Door"            <door>         (Doors)            {channel="omnilink:zone:home:PorchDoor:contact"}
+
+Group:Contact:OR(OPEN, CLOSED)   GarageDoors   "All Garage Doors [%s]"
+Contact   TwoCarGarageDo   "Two Car Garage Door"   <garagedoor>   (GarageDoors)      {channel="omnilink:zone:home:TwoCarGarageDo:contact"}
+Contact   OneCarGarageDo   "One Car Garage Door"   <garagedoor>   (GarageDoors)      {channel="omnilink:zone:home:OneCarGarageDo:contact"}
+
+Group:Contact:OR(OPEN, CLOSED)   Motion        "All Motion Sensors [%s]"
+Contact   MBRMotion        "Motion"                <presence>     (Motion)           {homekit="MotionSensor", channel="omnilink:zone:home:MBRMotion:contact"}
+Contact   UpOffMotion      "Motion"                <presence>     (Motion)           {homekit="MotionSensor", channel="omnilink:zone:home:UpOffMotion:contact"}
+Contact   UpLivMotion      "Motion"                <presence>     (Motion)           {homekit="MotionSensor", channel="omnilink:zone:home:UpLivMotion:contact"}
+Contact   BsmtWORMotion    "Motion"                <presence>     (Motion)           {homekit="MotionSensor", channel="omnilink:zone:home:BsmtWORMotion:contact"}
+Contact   GarageMotion     "Motion"                <presence>     (Motion)           {homekit="MotionSensor", channel="omnilink:zone:home:GarageMotion:contact"}
+
+/*
+ * Console
+ */
+String     UpstrsConsole_Beeper   "Enable/Disable Beeper [%s]"            {channel="omnilink:console:home:UpstrsConsole:enable_disable_beeper"}
+Number     UpstrsConsole_Beep     "Beep Console"                          {channel="omnilink:console:home:UpstrsConsole:beep"}
+
+/*
+ * Button
+ */
+Switch   MainButton   "Toggle button [%s]"   <switch>   {channel="omnilink:button:home:MainButton:press"}
+
+/*
+ * Other OmniPro items
+ */
+DateTime   OmniProTime   "Last Time Update [%1$ta %1$tR]"   <time>   {channel="omnilink:controller:home:sysdate"}
+```
+
+### Example `therm-status.map`
+
+```
+0=Idle
+1=Heating
+2=Cooling
+```
+
+### Example `therm-tempmode.map`
+
+```
+0=Off
+1=Heat
+2=Cool
+3=Auto
+5=Emergency heat
+```
+
+### Example `therm-fanmode.map`
+
+```
+0=Auto
+1=On
+2=Cycle
+```
+
+### Example `therm-holdmode.map`
+
+```
+0=Off
+1=Hold
+2=Vacation hold
+```
+
+### Example `area-modes.map`
+
+```
+0=Off
+1=Day
+2=Night
+3=Away
+4=Vacation
+5=Day instant
+6=Night delayed
+9=Arming day
+10=Arming night
+11=Arming away
+12=Arming vacation
+13=Arming day instant
+14=Arming night delay
+=Unknown
+```
+
+### Example `omnilink.rules`
+
+```
+rule "Update OmniPro Time"
+when
+  Time cron "0 0 0/1 1/1 * ? *"
+then
+  OmniProTime.sendCommand( new DateTimeType() )
+end
+```
diff --git a/bundles/org.openhab.binding.omnilink/pom.xml b/bundles/org.openhab.binding.omnilink/pom.xml
new file mode 100644 (file)
index 0000000..ee0e276
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.omnilink</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: OmniLink Binding</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.github.digitaldan</groupId>
+      <artifactId>jomnilink</artifactId>
+      <version>1.4.0</version>
+      <scope>compile</scope>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/feature/feature.xml b/bundles/org.openhab.binding.omnilink/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..250b3b3
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+       Copyright (c) 2010-2021 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
+
+-->
+<features name="org.openhab.binding.omnilink-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-omnilink" description="OmniLink Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.omnilink/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/AreaAlarm.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/AreaAlarm.java
new file mode 100644 (file)
index 0000000..6b8bcd2
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.math.BigInteger;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link AreaAlarm} class defines the different types of alarms supported
+ * by the OmniLink Protocol.
+ *
+ * @author Craig Hamilton - Initial contribution
+ */
+@NonNullByDefault
+public enum AreaAlarm {
+    BURGLARY(CHANNEL_AREA_ALARM_BURGLARY, 0),
+    FIRE(CHANNEL_AREA_ALARM_FIRE, 1),
+    GAS(CHANNEL_AREA_ALARM_GAS, 2),
+    AUXILIARY(CHANNEL_AREA_ALARM_AUXILIARY, 3),
+    FREEZE(CHANNEL_AREA_ALARM_FREEZE, 4),
+    WATER(CHANNEL_AREA_ALARM_WATER, 5),
+    DURESS(CHANNEL_AREA_ALARM_DURESS, 6),
+    TEMPERATURE(CHANNEL_AREA_ALARM_TEMPERATURE, 7);
+
+    private final String channelUID;
+    private final int bit;
+
+    AreaAlarm(String channelUID, int bit) {
+        this.channelUID = channelUID;
+        this.bit = bit;
+    }
+
+    public boolean isSet(BigInteger alarmBits) {
+        return alarmBits.testBit(bit);
+    }
+
+    public boolean isSet(int alarmBits) {
+        return isSet(BigInteger.valueOf(alarmBits));
+    }
+
+    public String getChannelUID() {
+        return channelUID;
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/AudioPlayer.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/AudioPlayer.java
new file mode 100644 (file)
index 0000000..e53467c
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link AudioPlayer} defines some methods that are used to
+ * interface with an OmniLink Audio Player.
+ *
+ * @author Brian O'Connell - Initial contribution
+ */
+@NonNullByDefault
+public enum AudioPlayer {
+    NUVO(1, 6, 8, 7, 9, 10),
+    NUVO_GRAND_ESSENTIA_SIMPLESE(2, 6, 8, 7, 9, 10),
+    NUVO_GRAND_GRAND_CONCERTO(3, 6, 6, 6, 9, 10),
+    RUSSOUND(4, 6, 8, 7, 11, 12),
+    XANTECH(6, 13, 15, 14, 16, 17),
+    SPEAKERCRAFT(7, 45, 44, 46, 42, 43),
+    PROFICIENT(8, 45, 44, 46, 42, 43);
+
+    private final int featureCode;
+    private final int playCommand;
+    private final int pauseCommand;
+    private final int stopCommand;
+    private final int previousCommand;
+    private final int nextCommand;
+
+    AudioPlayer(int featureCode, int playCommand, int pauseCommand, int stopCommand, int previousCommand,
+            int nextCommand) {
+        this.featureCode = featureCode;
+        this.playCommand = playCommand;
+        this.pauseCommand = pauseCommand;
+        this.stopCommand = stopCommand;
+        this.previousCommand = previousCommand;
+        this.nextCommand = nextCommand;
+    }
+
+    public int getPlayCommand() {
+        return playCommand;
+    }
+
+    public int getPauseCommand() {
+        return pauseCommand;
+    }
+
+    public int getStopCommand() {
+        return stopCommand;
+    }
+
+    public int getPreviousCommand() {
+        return previousCommand;
+    }
+
+    public int getNextCommand() {
+        return nextCommand;
+    }
+
+    public static Optional<AudioPlayer> getAudioPlayerForFeatureCode(int featureCode) {
+        return Arrays.stream(values()).filter(v -> v.featureCode == featureCode).findAny();
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/OmnilinkBindingConstants.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/OmnilinkBindingConstants.java
new file mode 100644 (file)
index 0000000..0584302
--- /dev/null
@@ -0,0 +1,188 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link OmnilinkBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class OmnilinkBindingConstants {
+
+    public static final String BINDING_ID = "omnilink";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "controller");
+    public static final ThingTypeUID THING_TYPE_OMNI_AREA = new ThingTypeUID(BINDING_ID, "area");
+    public static final ThingTypeUID THING_TYPE_LUMINA_AREA = new ThingTypeUID(BINDING_ID, "lumina_area");
+    public static final ThingTypeUID THING_TYPE_ZONE = new ThingTypeUID(BINDING_ID, "zone");
+    public static final ThingTypeUID THING_TYPE_LOCK = new ThingTypeUID(BINDING_ID, "lock");
+    public static final ThingTypeUID THING_TYPE_UNIT_UPB = new ThingTypeUID(BINDING_ID, "upb");
+    public static final ThingTypeUID THING_TYPE_UNIT = new ThingTypeUID(BINDING_ID, "unit");
+    public static final ThingTypeUID THING_TYPE_DIMMABLE = new ThingTypeUID(BINDING_ID, "dimmable");
+    public static final ThingTypeUID THING_TYPE_FLAG = new ThingTypeUID(BINDING_ID, "flag");
+    public static final ThingTypeUID THING_TYPE_OUTPUT = new ThingTypeUID(BINDING_ID, "output");
+    public static final ThingTypeUID THING_TYPE_ROOM = new ThingTypeUID(BINDING_ID, "room");
+    public static final ThingTypeUID THING_TYPE_BUTTON = new ThingTypeUID(BINDING_ID, "button");
+    public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
+    public static final ThingTypeUID THING_TYPE_AUDIO_ZONE = new ThingTypeUID(BINDING_ID, "audio_zone");
+    public static final ThingTypeUID THING_TYPE_AUDIO_SOURCE = new ThingTypeUID(BINDING_ID, "audio_source");
+    public static final ThingTypeUID THING_TYPE_CONSOLE = new ThingTypeUID(BINDING_ID, "console");
+    public static final ThingTypeUID THING_TYPE_TEMP_SENSOR = new ThingTypeUID(BINDING_ID, "temp_sensor");
+    public static final ThingTypeUID THING_TYPE_HUMIDITY_SENSOR = new ThingTypeUID(BINDING_ID, "humidity_sensor");
+
+    // List of all Channel ids
+
+    // zones
+    public static final String CHANNEL_ZONE_CONTACT = "contact";
+    public static final String CHANNEL_ZONE_CURRENT_CONDITION = "current_condition";
+    public static final String CHANNEL_ZONE_LATCHED_ALARM_STATUS = "latched_alarm_status";
+    public static final String CHANNEL_ZONE_ARMING_STATUS = "arming_status";
+    public static final String CHANNEL_ZONE_BYPASS = "bypass";
+    public static final String CHANNEL_ZONE_RESTORE = "restore";
+
+    // areas
+    public static final String CHANNEL_AREA_MODE = "mode";
+    public static final String CHANNEL_AREA_ACTIVATE_KEYPAD_EMERGENCY = "activate_keypad_emergency";
+    public static final String CHANNEL_AREA_ALARM_BURGLARY = "alarm_burglary";
+    public static final String CHANNEL_AREA_ALARM_FIRE = "alarm_fire";
+    public static final String CHANNEL_AREA_ALARM_GAS = "alarm_gas";
+    public static final String CHANNEL_AREA_ALARM_AUXILIARY = "alarm_auxiliary";
+    public static final String CHANNEL_AREA_ALARM_FREEZE = "alarm_freeze";
+    public static final String CHANNEL_AREA_ALARM_WATER = "alarm_water";
+    public static final String CHANNEL_AREA_ALARM_DURESS = "alarm_duress";
+    public static final String CHANNEL_AREA_ALARM_TEMPERATURE = "alarm_temperature";
+
+    public static final String CHANNEL_AREA_SECURITY_MODE_DISARM = "disarm";
+    public static final String CHANNEL_AREA_SECURITY_MODE_DAY = "day";
+    public static final String CHANNEL_AREA_SECURITY_MODE_NIGHT = "night";
+    public static final String CHANNEL_AREA_SECURITY_MODE_AWAY = "away";
+    public static final String CHANNEL_AREA_SECURITY_MODE_VACATION = "vacation";
+    public static final String CHANNEL_AREA_SECURITY_MODE_DAY_INSTANT = "day_instant";
+    public static final String CHANNEL_AREA_SECURITY_MODE_NIGHT_DELAYED = "night_delayed";
+
+    public static final String CHANNEL_AREA_SECURITY_MODE_HOME = "home";
+    public static final String CHANNEL_AREA_SECURITY_MODE_SLEEP = "sleep";
+    public static final String CHANNEL_AREA_SECURITY_MODE_PARTY = "party";
+    public static final String CHANNEL_AREA_SECURITY_MODE_SPECIAL = "special";
+
+    // units
+    public static final String CHANNEL_UNIT_LEVEL = "level";
+    public static final String CHANNEL_UNIT_SWITCH = "switch";
+    public static final String CHANNEL_UNIT_ON_FOR_SECONDS = "on_for_seconds";
+    public static final String CHANNEL_UNIT_ON_FOR_MINUTES = "on_for_minutes";
+    public static final String CHANNEL_UNIT_ON_FOR_HOURS = "on_for_hours";
+    public static final String CHANNEL_UNIT_OFF_FOR_SECONDS = "off_for_seconds";
+    public static final String CHANNEL_UNIT_OFF_FOR_MINUTES = "off_for_minutes";
+    public static final String CHANNEL_UNIT_OFF_FOR_HOURS = "off_for_hours";
+    public static final String CHANNEL_FLAG_VALUE = "value";
+    public static final String CHANNEL_FLAG_SWITCH = "switch";
+    public static final String CHANNEL_UPB_STATUS = "upb_status";
+
+    public static final String CHANNEL_ROOM_SWITCH = "switch";
+    public static final String CHANNEL_ROOM_SCENE_A = "scene_a";
+    public static final String CHANNEL_ROOM_SCENE_B = "scene_b";
+    public static final String CHANNEL_ROOM_SCENE_C = "scene_c";
+    public static final String CHANNEL_ROOM_SCENE_D = "scene_d";
+    public static final String CHANNEL_ROOM_STATE = "state";
+
+    public static final String CHANNEL_SYSTEMDATE = "sysdate";
+    public static final String CHANNEL_EVENT_LOG = "last_log";
+
+    // buttons
+    public static final String CHANNEL_BUTTON_PRESS = "press";
+
+    // locks
+    public static final String CHANNEL_LOCK_SWITCH = "switch";
+
+    // thermostats
+    public static final String CHANNEL_THERMO_FREEZE_ALARM = "freeze_alarm";
+    public static final String CHANNEL_THERMO_COMM_FAILURE = "comm_failure";
+    public static final String CHANNEL_THERMO_STATUS = "status";
+    public static final String CHANNEL_THERMO_CURRENT_TEMP = "temperature";
+    public static final String CHANNEL_THERMO_OUTDOOR_TEMP = "outdoor_temperature";
+    public static final String CHANNEL_THERMO_HUMIDITY = "humidity";
+    public static final String CHANNEL_THERMO_HUMIDIFY_SETPOINT = "humidify_setpoint";
+    public static final String CHANNEL_THERMO_DEHUMIDIFY_SETPOINT = "dehumidify_setpoint";
+    public static final String CHANNEL_THERMO_SYSTEM_MODE = "system_mode";
+    public static final String CHANNEL_THERMO_FAN_MODE = "fan_mode";
+    public static final String CHANNEL_THERMO_HOLD_STATUS = "hold_status";
+    public static final String CHANNEL_THERMO_COOL_SETPOINT = "cool_setpoint";
+    public static final String CHANNEL_THERMO_HEAT_SETPOINT = "heat_setpoint";
+
+    // temp / humidity sensors
+    public static final String CHANNEL_AUX_TEMP = "temperature";
+    public static final String CHANNEL_AUX_HUMIDITY = "humidity";
+    public static final String CHANNEL_AUX_LOW_SETPOINT = "low_setpoint";
+    public static final String CHANNEL_AUX_HIGH_SETPOINT = "high_setpoint";
+
+    // consoles
+    public static final String CHANNEL_CONSOLE_BEEP = "beep";
+    public static final String CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER = "enable_disable_beeper";
+
+    // audio zones
+    public static final String CHANNEL_AUDIO_ZONE_POWER = "zone_power";
+    public static final String CHANNEL_AUDIO_ZONE_MUTE = "zone_mute";
+    public static final String CHANNEL_AUDIO_ZONE_VOLUME = "zone_volume";
+    public static final String CHANNEL_AUDIO_ZONE_SOURCE = "zone_source";
+    public static final String CHANNEL_AUDIO_ZONE_CONTROL = "zone_control";
+
+    // audio sources
+    public static final String CHANNEL_AUDIO_SOURCE_TEXT1 = "source_text_1";
+    public static final String CHANNEL_AUDIO_SOURCE_TEXT2 = "source_text_2";
+    public static final String CHANNEL_AUDIO_SOURCE_TEXT3 = "source_text_3";
+    public static final String CHANNEL_AUDIO_SOURCE_TEXT4 = "source_text_4";
+    public static final String CHANNEL_AUDIO_SOURCE_TEXT5 = "source_text_5";
+    public static final String CHANNEL_AUDIO_SOURCE_TEXT6 = "source_text_6";
+    public static final String CHANNEL_AUDIO_SOURCE_POLLING = "polling";
+
+    // trigger channels
+    public static final String TRIGGER_CHANNEL_BUTTON_ACTIVATED_EVENT = "activated_event";
+    public static final String TRIGGER_CHANNEL_PHONE_LINE_EVENT = "phone_line_event";
+    public static final String TRIGGER_CHANNEL_AC_POWER_EVENT = "ac_power_event";
+    public static final String TRIGGER_CHANNEL_BATTERY_EVENT = "battery_event";
+    public static final String TRIGGER_CHANNEL_DCM_EVENT = "dcm_event";
+    public static final String TRIGGER_CHANNEL_ENERGY_COST_EVENT = "energy_cost_event";
+    public static final String TRIGGER_CHANNEL_CAMERA_TRIGGER_EVENT = "camera_trigger_event";
+    public static final String TRIGGER_CHANNEL_ACCESS_CONTROL_READER_EVENT = "access_control_reader_event";
+    public static final String TRIGGER_CHANNEL_AREA_ALL_ON_OFF_EVENT = "all_on_off_Event";
+    public static final String TRIGGER_CHANNEL_ZONE_STATE_EVENT = "zone_state_Event";
+    public static final String TRIGGER_CHANNEL_SWITCH_PRESS_EVENT = "switch_press_event";
+    public static final String TRIGGER_CHANNEL_UPB_LINK_ACTIVATED_EVENT = "upb_link_activated_event";
+    public static final String TRIGGER_CHANNEL_UPB_LINK_DEACTIVATED_EVENT = "upb_link_deactivated_event";
+
+    // thing configuration and properties keys
+    public static final String THING_PROPERTIES_NAME = "name";
+    public static final String THING_PROPERTIES_NUMBER = "number";
+    public static final String THING_PROPERTIES_AREA = "area";
+    public static final String THING_PROPERTIES_AUTO_START = "autostart";
+    public static final String THING_PROPERTIES_MODEL_NUMBER = "modelNumber";
+    public static final String THING_PROPERTIES_MAJOR_VERSION = "majorVersion";
+    public static final String THING_PROPERTIES_MINOR_VERSION = "minorVersion";
+    public static final String THING_PROPERTIES_REVISION = "revision";
+    public static final String THING_PROPERTIES_PHONE_NUMBER = "phoneNumber";
+
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_OMNI_AREA,
+            THING_TYPE_LUMINA_AREA, THING_TYPE_ZONE, THING_TYPE_BRIDGE, THING_TYPE_FLAG, THING_TYPE_ROOM,
+            THING_TYPE_BUTTON, THING_TYPE_UNIT_UPB, THING_TYPE_THERMOSTAT, THING_TYPE_CONSOLE, THING_TYPE_AUDIO_ZONE,
+            THING_TYPE_AUDIO_SOURCE, THING_TYPE_TEMP_SENSOR, THING_TYPE_HUMIDITY_SENSOR, THING_TYPE_LOCK,
+            THING_TYPE_OUTPUT, THING_TYPE_UNIT, THING_TYPE_DIMMABLE);
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/OmnilinkHandlerFactory.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/OmnilinkHandlerFactory.java
new file mode 100644 (file)
index 0000000..0c88a08
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.handler.AudioSourceHandler;
+import org.openhab.binding.omnilink.internal.handler.AudioZoneHandler;
+import org.openhab.binding.omnilink.internal.handler.ButtonHandler;
+import org.openhab.binding.omnilink.internal.handler.ConsoleHandler;
+import org.openhab.binding.omnilink.internal.handler.HumiditySensorHandler;
+import org.openhab.binding.omnilink.internal.handler.LockHandler;
+import org.openhab.binding.omnilink.internal.handler.LuminaAreaHandler;
+import org.openhab.binding.omnilink.internal.handler.OmniAreaHandler;
+import org.openhab.binding.omnilink.internal.handler.OmnilinkBridgeHandler;
+import org.openhab.binding.omnilink.internal.handler.TempSensorHandler;
+import org.openhab.binding.omnilink.internal.handler.ThermostatHandler;
+import org.openhab.binding.omnilink.internal.handler.UnitHandler;
+import org.openhab.binding.omnilink.internal.handler.ZoneHandler;
+import org.openhab.binding.omnilink.internal.handler.units.DimmableUnitHandler;
+import org.openhab.binding.omnilink.internal.handler.units.FlagHandler;
+import org.openhab.binding.omnilink.internal.handler.units.OutputHandler;
+import org.openhab.binding.omnilink.internal.handler.units.UpbRoomHandler;
+import org.openhab.binding.omnilink.internal.handler.units.dimmable.UpbUnitHandler;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link OmnilinkHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+@Component(service = ThingHandlerFactory.class, configurationPid = "binding.omnilink")
+public class OmnilinkHandlerFactory extends BaseThingHandlerFactory {
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (thingTypeUID.equals(THING_TYPE_AUDIO_SOURCE)) {
+            return new AudioSourceHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_AUDIO_ZONE)) {
+            return new AudioZoneHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_BRIDGE)) {
+            return new OmnilinkBridgeHandler((Bridge) thing);
+        } else if (thingTypeUID.equals(THING_TYPE_BUTTON)) {
+            return new ButtonHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_CONSOLE)) {
+            return new ConsoleHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_DIMMABLE)) {
+            return new DimmableUnitHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_FLAG)) {
+            return new FlagHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_HUMIDITY_SENSOR)) {
+            return new HumiditySensorHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_LOCK)) {
+            return new LockHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_LUMINA_AREA)) {
+            return new LuminaAreaHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_OMNI_AREA)) {
+            return new OmniAreaHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_OUTPUT)) {
+            return new OutputHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_ROOM)) {
+            return new UpbRoomHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_TEMP_SENSOR)) {
+            return new TempSensorHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_THERMOSTAT)) {
+            return new ThermostatHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_UNIT)) {
+            return new UnitHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_UNIT_UPB)) {
+            return new UpbUnitHandler(thing);
+        } else if (thingTypeUID.equals(THING_TYPE_ZONE)) {
+            return new ZoneHandler(thing);
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/SystemType.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/SystemType.java
new file mode 100644 (file)
index 0000000..5ac9cf6
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal;
+
+import java.util.Arrays;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link SystemType} enum defines the two supported system types which can
+ * interface with the binding
+ *
+ * @author Craig Hamilton - Initial contribution
+ */
+@NonNullByDefault
+public enum SystemType {
+    OMNI(16, 30, 38),
+    LUMINA(36, 37);
+
+    private final Set<Integer> modelNumbers;
+
+    SystemType(Integer... modelNumbers) {
+        this.modelNumbers = Set.of(modelNumbers);
+    }
+
+    public static SystemType getType(int modelNumber) {
+        return Arrays.stream(values()).filter(s -> s.modelNumbers.contains(modelNumber)).findFirst()
+                .orElseThrow(IllegalArgumentException::new);
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/config/OmnilinkBridgeConfig.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/config/OmnilinkBridgeConfig.java
new file mode 100644 (file)
index 0000000..8eaeec9
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link OmnilinkBridgeConfig} sets the authentication settings of the
+ * OmniLink Controller that will allow for proper communication.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class OmnilinkBridgeConfig {
+
+    private @Nullable String key1;
+    private @Nullable String key2;
+    private @Nullable String ipAddress;
+    private int port;
+    private int logPollingInterval;
+
+    public int getLogPollingInterval() {
+        return logPollingInterval;
+    }
+
+    public void setLogPollingInterval(int logPollingInterval) {
+        this.logPollingInterval = logPollingInterval;
+    }
+
+    public @Nullable String getKey1() {
+        return key1;
+    }
+
+    public void setKey1(String key1) {
+        this.key1 = key1;
+    }
+
+    public @Nullable String getKey2() {
+        return key2;
+    }
+
+    public void setKey2(String key2) {
+        this.key2 = key2;
+    }
+
+    public @Nullable String getIpAddress() {
+        return ipAddress;
+    }
+
+    public void setIpAddress(String ipAddress) {
+        this.ipAddress = ipAddress;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/ObjectPropertyRequest.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/ObjectPropertyRequest.java
new file mode 100644 (file)
index 0000000..7cf1dd8
--- /dev/null
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.discovery;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.omnilink.internal.handler.BridgeOfflineException;
+import org.openhab.binding.omnilink.internal.handler.OmnilinkBridgeHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.ObjectProperties;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * @author Craig Hamilton - Initial contribution
+ *
+ * @param <T>
+ */
+@NonNullByDefault
+public class ObjectPropertyRequest<T extends ObjectProperties> implements Iterable<T> {
+    private final Logger logger = LoggerFactory.getLogger(ObjectPropertyRequest.class);
+
+    public static <T extends ObjectProperties, U extends ObjectPropertyRequests<T>> Builder<T> builder(
+            OmnilinkBridgeHandler bridgeHandler, U request, int objectNumber, int offset) {
+        return new Builder<>(bridgeHandler, request, objectNumber, offset);
+    }
+
+    private final OmnilinkBridgeHandler bridgeHandler;
+    private final ObjectPropertyRequests<T> request;
+    private final int objectNumber;
+    private final int filter1;
+    private final int filter2;
+    private final int filter3;
+    private final int offset;
+
+    private ObjectPropertyRequest(OmnilinkBridgeHandler bridgeHandler, ObjectPropertyRequests<T> request,
+            int objectNumber, int filter1, int filter2, int filter3, int offset) {
+        this.bridgeHandler = bridgeHandler;
+        this.request = request;
+        this.objectNumber = objectNumber;
+        this.filter1 = filter1;
+        this.filter2 = filter2;
+        this.filter3 = filter3;
+        this.offset = offset;
+    }
+
+    @Override
+    public Iterator<T> iterator() {
+        List<T> messages = new ArrayList<T>();
+        int currentObjectNumber = objectNumber;
+
+        while (true) {
+            try {
+                Message message = bridgeHandler.reqObjectProperties(request.getPropertyRequest(), currentObjectNumber,
+                        offset, filter1, filter2, filter3);
+                if (message.getMessageType() == Message.MESG_TYPE_OBJ_PROP) {
+                    ObjectProperties objectProperties = (ObjectProperties) message;
+                    messages.add(request.getResponseType().cast(objectProperties));
+                    if (offset == 0) {
+                        break;
+                    } else if (offset == 1) {
+                        currentObjectNumber++;
+                    } else if (offset == -1) {
+                        currentObjectNumber--;
+                    }
+                } else {
+                    break;
+                }
+            } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+                logger.warn("Error retrieving object properties: {}", e.getMessage());
+            }
+        }
+        return messages.iterator();
+    }
+
+    public static class Builder<T extends ObjectProperties> {
+        private final OmnilinkBridgeHandler bridgeHandler;
+        private final ObjectPropertyRequests<T> request;
+        private final int objectNumber;
+        private final int offset;
+        private int filter1 = ObjectProperties.FILTER_1_NONE;
+        private int filter2 = ObjectProperties.FILTER_2_NONE;
+        private int filter3 = ObjectProperties.FILTER_3_NONE;
+
+        private Builder(OmnilinkBridgeHandler bridgeHandler, ObjectPropertyRequests<T> request, int objectNumber,
+                int offset) {
+            this.bridgeHandler = bridgeHandler;
+            this.request = request;
+            this.objectNumber = objectNumber;
+            this.offset = offset;
+        }
+
+        public Builder<T> selectNamed() {
+            this.filter1 = ObjectProperties.FILTER_1_NAMED;
+            return this;
+        }
+
+        public Builder<T> areaFilter(int area) {
+            this.filter2 = area;
+            return this;
+        }
+
+        public Builder<T> selectAnyLoad() {
+            this.filter3 = ObjectProperties.FILTER_3_ANY_LOAD;
+            return this;
+        }
+
+        public ObjectPropertyRequest<T> build() {
+            return new ObjectPropertyRequest<T>(bridgeHandler, request, objectNumber, filter1, filter2, filter3,
+                    offset);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/ObjectPropertyRequests.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/ObjectPropertyRequests.java
new file mode 100644 (file)
index 0000000..38aa3fc
--- /dev/null
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.discovery;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.ObjectProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AccessControlReaderProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AudioSourceProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AudioZoneProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AuxSensorProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.ButtonProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.ThermostatProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.UnitProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.ZoneProperties;
+
+/**
+ * @author Craig Hamilton - Initial contribution
+ *
+ * @param <T>
+ */
+@NonNullByDefault
+public class ObjectPropertyRequests<T extends ObjectProperties> {
+
+    public static final ObjectPropertyRequests<ThermostatProperties> THERMOSTAT = new ObjectPropertyRequests<>(
+            Message.OBJ_TYPE_THERMO, ThermostatProperties.class);
+
+    public static final ObjectPropertyRequests<ButtonProperties> BUTTONS = new ObjectPropertyRequests<>(
+            Message.OBJ_TYPE_BUTTON, ButtonProperties.class);
+
+    public static final ObjectPropertyRequests<AreaProperties> AREA = new ObjectPropertyRequests<>(
+            Message.OBJ_TYPE_AREA, AreaProperties.class);
+
+    public static final ObjectPropertyRequests<ZoneProperties> ZONE = new ObjectPropertyRequests<>(
+            Message.OBJ_TYPE_ZONE, ZoneProperties.class);
+
+    public static final ObjectPropertyRequests<UnitProperties> UNIT = new ObjectPropertyRequests<>(
+            Message.OBJ_TYPE_UNIT, UnitProperties.class);
+
+    public static final ObjectPropertyRequests<AudioZoneProperties> AUDIO_ZONE = new ObjectPropertyRequests<>(
+            Message.OBJ_TYPE_AUDIO_ZONE, AudioZoneProperties.class);
+
+    public static final ObjectPropertyRequests<AudioSourceProperties> AUDIO_SOURCE = new ObjectPropertyRequests<>(
+            Message.OBJ_TYPE_AUDIO_SOURCE, AudioSourceProperties.class);
+
+    public static final ObjectPropertyRequests<AuxSensorProperties> AUX_SENSORS = new ObjectPropertyRequests<>(
+            Message.OBJ_TYPE_AUX_SENSOR, AuxSensorProperties.class);
+
+    public static final ObjectPropertyRequests<AccessControlReaderProperties> LOCK = new ObjectPropertyRequests<>(
+            Message.OBJ_TYPE_CONTROL_READER, AccessControlReaderProperties.class);
+
+    private final int propertyRequest;
+    private final Class<T> responseType;
+
+    private ObjectPropertyRequests(int propertyRequest, Class<T> type) {
+        this.propertyRequest = propertyRequest;
+        this.responseType = type;
+    }
+
+    public int getPropertyRequest() {
+        return propertyRequest;
+    }
+
+    public Class<T> getResponseType() {
+        return responseType;
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/OmnilinkDiscoveryService.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/discovery/OmnilinkDiscoveryService.java
new file mode 100644 (file)
index 0000000..86ceadf
--- /dev/null
@@ -0,0 +1,542 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.discovery;
+
+import static com.digitaldan.jomnilinkII.MessageTypes.properties.AuxSensorProperties.*;
+import static com.digitaldan.jomnilinkII.MessageTypes.properties.UnitProperties.*;
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.SystemType;
+import org.openhab.binding.omnilink.internal.handler.BridgeOfflineException;
+import org.openhab.binding.omnilink.internal.handler.OmnilinkBridgeHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.MessageTypes.SystemInformation;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AccessControlReaderProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AudioSourceProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AudioZoneProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AuxSensorProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.ButtonProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.ThermostatProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.UnitProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.ZoneProperties;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * The {@link OmnilinkDiscoveryService} creates things based on the configured bridge.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class OmnilinkDiscoveryService extends AbstractDiscoveryService
+        implements DiscoveryService, ThingHandlerService {
+    private final Logger logger = LoggerFactory.getLogger(OmnilinkDiscoveryService.class);
+    private static final int DISCOVER_TIMEOUT_SECONDS = 30;
+    private @Nullable OmnilinkBridgeHandler bridgeHandler;
+    private @Nullable SystemType systemType;
+    private @Nullable List<AreaProperties> areas;
+
+    /**
+     * Creates an OmnilinkDiscoveryService.
+     */
+    public OmnilinkDiscoveryService() {
+        super(SUPPORTED_THING_TYPES_UIDS, DISCOVER_TIMEOUT_SECONDS, false);
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof OmnilinkBridgeHandler) {
+            bridgeHandler = (OmnilinkBridgeHandler) handler;
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return bridgeHandler;
+    }
+
+    @Override
+    public void activate() {
+    }
+
+    @Override
+    public void deactivate() {
+    }
+
+    @Override
+    protected synchronized void startScan() {
+        final OmnilinkBridgeHandler handler = bridgeHandler;
+        if (handler != null) {
+            logger.debug("Starting scan");
+            try {
+                SystemInformation systemInformation = handler.reqSystemInformation();
+                this.systemType = SystemType.getType(systemInformation.getModel());
+                this.areas = discoverAreas();
+                discoverUnits();
+                discoverZones();
+                discoverButtons();
+                discoverThermostats();
+                discoverAudioZones();
+                discoverAudioSources();
+                discoverTempSensors();
+                discoverHumiditySensors();
+                discoverLocks();
+            } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+                logger.debug("Received error during discovery: {}", e.getMessage());
+            }
+        }
+    }
+
+    @Override
+    protected synchronized void stopScan() {
+        super.stopScan();
+        removeOlderResults(getTimestampOfLastScan());
+    }
+
+    /**
+     * Calculate the area filter the a supplied area
+     *
+     * @param area Area to calculate filter for.
+     * @return Calculated Bit Filter for the supplied area. Bit 0 is area 1, bit 2 is area 2 and so on.
+     */
+    private static int bitFilterForArea(AreaProperties areaProperties) {
+        return BigInteger.ZERO.setBit(areaProperties.getNumber() - 1).intValue();
+    }
+
+    /**
+     * Discovers OmniLink buttons
+     */
+    private void discoverButtons() {
+        final OmnilinkBridgeHandler handler = bridgeHandler;
+        if (handler != null) {
+            final ThingUID bridgeUID = handler.getThing().getUID();
+            final List<AreaProperties> areas = this.areas;
+
+            if (areas != null) {
+                for (AreaProperties areaProperties : areas) {
+                    int areaFilter = bitFilterForArea(areaProperties);
+
+                    ObjectPropertyRequest<ButtonProperties> objectPropertyRequest = ObjectPropertyRequest
+                            .builder(handler, ObjectPropertyRequests.BUTTONS, 0, 1).selectNamed().areaFilter(areaFilter)
+                            .build();
+
+                    for (ButtonProperties buttonProperties : objectPropertyRequest) {
+                        String thingName = buttonProperties.getName();
+                        String thingID = Integer.toString(buttonProperties.getNumber());
+
+                        Map<String, Object> properties = new HashMap<>();
+                        properties.put(THING_PROPERTIES_NAME, thingName);
+                        properties.put(THING_PROPERTIES_AREA, areaProperties.getNumber());
+
+                        ThingUID thingUID = new ThingUID(THING_TYPE_BUTTON, bridgeUID, thingID);
+
+                        DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
+                                .withProperties(properties).withProperty(THING_PROPERTIES_NUMBER, thingID)
+                                .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID)
+                                .withLabel(thingName).build();
+                        thingDiscovered(discoveryResult);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Discovers OmniLink locks
+     */
+    private void discoverLocks() {
+        final OmnilinkBridgeHandler handler = bridgeHandler;
+        if (handler != null) {
+            final ThingUID bridgeUID = handler.getThing().getUID();
+
+            ObjectPropertyRequest<AccessControlReaderProperties> objectPropertyRequest = ObjectPropertyRequest
+                    .builder(handler, ObjectPropertyRequests.LOCK, 0, 1).selectNamed().build();
+
+            for (AccessControlReaderProperties lockProperties : objectPropertyRequest) {
+                String thingName = lockProperties.getName();
+                String thingID = Integer.toString(lockProperties.getNumber());
+
+                Map<String, Object> properties = Map.of(THING_PROPERTIES_NAME, thingName);
+
+                ThingUID thingUID = new ThingUID(THING_TYPE_LOCK, bridgeUID, thingID);
+
+                DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+                        .withProperty(THING_PROPERTIES_NUMBER, thingID)
+                        .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID).withLabel(thingName)
+                        .build();
+                thingDiscovered(discoveryResult);
+            }
+        }
+    }
+
+    /**
+     * Discovers OmniLink audio zones
+     */
+    private void discoverAudioZones() {
+        final OmnilinkBridgeHandler handler = bridgeHandler;
+        if (handler != null) {
+            final ThingUID bridgeUID = handler.getThing().getUID();
+
+            ObjectPropertyRequest<AudioZoneProperties> objectPropertyRequest = ObjectPropertyRequest
+                    .builder(handler, ObjectPropertyRequests.AUDIO_ZONE, 0, 1).selectNamed().build();
+
+            for (AudioZoneProperties audioZoneProperties : objectPropertyRequest) {
+                String thingName = audioZoneProperties.getName();
+                String thingID = Integer.toString(audioZoneProperties.getNumber());
+
+                Map<String, Object> properties = Map.of(THING_PROPERTIES_NAME, thingName);
+
+                ThingUID thingUID = new ThingUID(THING_TYPE_AUDIO_ZONE, bridgeUID, thingID);
+
+                DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+                        .withProperty(THING_PROPERTIES_NUMBER, thingID)
+                        .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID).withLabel(thingName)
+                        .build();
+                thingDiscovered(discoveryResult);
+            }
+        }
+    }
+
+    /**
+     * Discovers OmniLink audio sources
+     */
+    private void discoverAudioSources() {
+        final OmnilinkBridgeHandler handler = bridgeHandler;
+        if (handler != null) {
+            final ThingUID bridgeUID = handler.getThing().getUID();
+
+            ObjectPropertyRequest<AudioSourceProperties> objectPropertyRequest = ObjectPropertyRequest
+                    .builder(handler, ObjectPropertyRequests.AUDIO_SOURCE, 0, 1).selectNamed().build();
+
+            for (AudioSourceProperties audioSourceProperties : objectPropertyRequest) {
+                String thingName = audioSourceProperties.getName();
+                String thingID = Integer.toString(audioSourceProperties.getNumber());
+
+                Map<String, Object> properties = Map.of(THING_PROPERTIES_NAME, thingName);
+
+                ThingUID thingUID = new ThingUID(THING_TYPE_AUDIO_SOURCE, bridgeUID, thingID);
+
+                DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+                        .withProperty(THING_PROPERTIES_NUMBER, thingID)
+                        .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID).withLabel(thingName)
+                        .build();
+                thingDiscovered(discoveryResult);
+            }
+        }
+    }
+
+    /**
+     * Discovers OmniLink temperature sensors
+     */
+    private void discoverTempSensors() {
+        final OmnilinkBridgeHandler handler = bridgeHandler;
+        if (handler != null) {
+            final ThingUID bridgeUID = handler.getThing().getUID();
+            final List<AreaProperties> areas = this.areas;
+
+            if (areas != null) {
+                for (AreaProperties areaProperties : areas) {
+                    int areaFilter = bitFilterForArea(areaProperties);
+
+                    ObjectPropertyRequest<AuxSensorProperties> objectPropertyRequest = ObjectPropertyRequest
+                            .builder(handler, ObjectPropertyRequests.AUX_SENSORS, 0, 1).selectNamed()
+                            .areaFilter(areaFilter).build();
+
+                    for (AuxSensorProperties auxSensorProperties : objectPropertyRequest) {
+                        if (auxSensorProperties.getSensorType() != SENSOR_TYPE_PROGRAMMABLE_ENERGY_SAVER_MODULE
+                                && auxSensorProperties.getSensorType() != SENSOR_TYPE_HUMIDITY) {
+                            String thingName = auxSensorProperties.getName();
+                            String thingID = Integer.toString(auxSensorProperties.getNumber());
+
+                            Map<String, Object> properties = new HashMap<>();
+                            properties.put(THING_PROPERTIES_NAME, thingName);
+                            properties.put(THING_PROPERTIES_AREA, areaProperties.getNumber());
+
+                            ThingUID thingUID = new ThingUID(THING_TYPE_TEMP_SENSOR, bridgeUID, thingID);
+
+                            DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
+                                    .withProperties(properties).withProperty(THING_PROPERTIES_NUMBER, thingID)
+                                    .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID)
+                                    .withLabel(thingName).build();
+                            thingDiscovered(discoveryResult);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Discovers OmniLink humidity sensors
+     */
+    private void discoverHumiditySensors() {
+        final OmnilinkBridgeHandler handler = bridgeHandler;
+        if (handler != null) {
+            final ThingUID bridgeUID = handler.getThing().getUID();
+            final List<AreaProperties> areas = this.areas;
+
+            if (areas != null) {
+                for (AreaProperties areaProperties : areas) {
+                    int areaFilter = bitFilterForArea(areaProperties);
+
+                    ObjectPropertyRequest<AuxSensorProperties> objectPropertyRequest = ObjectPropertyRequest
+                            .builder(handler, ObjectPropertyRequests.AUX_SENSORS, 0, 1).selectNamed()
+                            .areaFilter(areaFilter).build();
+
+                    for (AuxSensorProperties auxSensorProperties : objectPropertyRequest) {
+                        if (auxSensorProperties.getSensorType() == SENSOR_TYPE_HUMIDITY) {
+                            String thingName = auxSensorProperties.getName();
+                            String thingID = Integer.toString(auxSensorProperties.getNumber());
+
+                            Map<String, Object> properties = new HashMap<>();
+                            properties.put(THING_PROPERTIES_NAME, thingName);
+                            properties.put(THING_PROPERTIES_AREA, areaProperties.getNumber());
+
+                            ThingUID thingUID = new ThingUID(THING_TYPE_HUMIDITY_SENSOR, bridgeUID, thingID);
+
+                            DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
+                                    .withProperties(properties).withProperty(THING_PROPERTIES_NUMBER, thingID)
+                                    .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID)
+                                    .withLabel(thingName).build();
+                            thingDiscovered(discoveryResult);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Discovers OmniLink thermostats
+     */
+    private void discoverThermostats() {
+        final OmnilinkBridgeHandler handler = bridgeHandler;
+        if (handler != null) {
+            final ThingUID bridgeUID = handler.getThing().getUID();
+            final List<AreaProperties> areas = this.areas;
+
+            if (areas != null) {
+                for (AreaProperties areaProperties : areas) {
+                    int areaFilter = bitFilterForArea(areaProperties);
+
+                    ObjectPropertyRequest<ThermostatProperties> objectPropertyRequest = ObjectPropertyRequest
+                            .builder(handler, ObjectPropertyRequests.THERMOSTAT, 0, 1).selectNamed()
+                            .areaFilter(areaFilter).build();
+
+                    for (ThermostatProperties thermostatProperties : objectPropertyRequest) {
+                        String thingName = thermostatProperties.getName();
+                        String thingID = Integer.toString(thermostatProperties.getNumber());
+
+                        ThingUID thingUID = new ThingUID(THING_TYPE_THERMOSTAT, bridgeUID, thingID);
+
+                        Map<String, Object> properties = new HashMap<>();
+                        properties.put(THING_PROPERTIES_NAME, thingName);
+                        properties.put(THING_PROPERTIES_AREA, areaProperties.getNumber());
+
+                        DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
+                                .withProperties(properties).withProperty(THING_PROPERTIES_NUMBER, thingID)
+                                .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID)
+                                .withLabel(thingName).build();
+                        thingDiscovered(discoveryResult);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Discovers OmniLink areas
+     */
+    private @Nullable List<AreaProperties> discoverAreas() {
+        final OmnilinkBridgeHandler handler = bridgeHandler;
+        if (handler != null) {
+            final ThingUID bridgeUID = handler.getThing().getUID();
+            List<AreaProperties> areas = new LinkedList<>();
+
+            ObjectPropertyRequest<AreaProperties> objectPropertyRequest = ObjectPropertyRequest
+                    .builder(handler, ObjectPropertyRequests.AREA, 0, 1).build();
+
+            for (AreaProperties areaProperties : objectPropertyRequest) {
+                int thingNumber = areaProperties.getNumber();
+                String thingName = areaProperties.getName();
+                String thingID = Integer.toString(thingNumber);
+                ThingUID thingUID = null;
+
+                /*
+                 * It seems that for simple OmniLink Controller configurations there
+                 * is only 1 area, without a name. So if there is no name for the
+                 * first area, we will call that Main Area. If other area's name is
+                 * blank, we will not create a thing.
+                 */
+                if (thingNumber == 1 && "".equals(thingName)) {
+                    thingName = "Main Area";
+                } else if ("".equals(thingName)) {
+                    break;
+                }
+
+                Map<String, Object> properties = Map.of(THING_PROPERTIES_NAME, thingName);
+
+                final SystemType systemType = this.systemType;
+                if (systemType != null) {
+                    switch (systemType) {
+                        case LUMINA:
+                            thingUID = new ThingUID(THING_TYPE_LUMINA_AREA, bridgeUID, thingID);
+                            break;
+                        case OMNI:
+                            thingUID = new ThingUID(THING_TYPE_OMNI_AREA, bridgeUID, thingID);
+                            break;
+                        default:
+                            throw new IllegalStateException("Unknown System Type");
+                    }
+                }
+
+                if (thingUID != null) {
+                    DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+                            .withProperty(THING_PROPERTIES_NUMBER, thingID)
+                            .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID)
+                            .withLabel(thingName).build();
+                    thingDiscovered(discoveryResult);
+                }
+
+                areas.add(areaProperties);
+            }
+            return areas;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Discovers OmniLink supported units
+     */
+    private void discoverUnits() {
+        final OmnilinkBridgeHandler handler = bridgeHandler;
+        if (handler != null) {
+            final ThingUID bridgeUID = handler.getThing().getUID();
+            final List<AreaProperties> areas = this.areas;
+
+            if (areas != null) {
+                for (AreaProperties areaProperties : areas) {
+                    int areaFilter = bitFilterForArea(areaProperties);
+
+                    ObjectPropertyRequest<UnitProperties> objectPropertyRequest = ObjectPropertyRequest
+                            .builder(handler, ObjectPropertyRequests.UNIT, 0, 1).selectNamed().areaFilter(areaFilter)
+                            .selectAnyLoad().build();
+
+                    for (UnitProperties unitProperties : objectPropertyRequest) {
+                        int thingType = unitProperties.getUnitType();
+                        String thingName = unitProperties.getName();
+                        String thingID = Integer.toString(unitProperties.getNumber());
+                        ThingUID thingUID = null;
+
+                        Map<String, Object> properties = new HashMap<>();
+                        properties.put(THING_PROPERTIES_NAME, thingName);
+                        properties.put(THING_PROPERTIES_AREA, areaProperties.getNumber());
+
+                        switch (thingType) {
+                            case UNIT_TYPE_HLC_ROOM:
+                            case UNIT_TYPE_VIZIARF_ROOM:
+                                thingUID = new ThingUID(THING_TYPE_ROOM, bridgeUID, thingID);
+                                break;
+                            case UNIT_TYPE_FLAG:
+                                thingUID = new ThingUID(THING_TYPE_FLAG, bridgeUID, thingID);
+                                break;
+                            case UNIT_TYPE_OUTPUT:
+                                thingUID = new ThingUID(THING_TYPE_OUTPUT, bridgeUID, thingID);
+                                break;
+                            case UNIT_TYPE_UPB:
+                            case UNIT_TYPE_HLC_LOAD:
+                                thingUID = new ThingUID(THING_TYPE_UNIT_UPB, bridgeUID, thingID);
+                                break;
+                            case UNIT_TYPE_CENTRALITE:
+                            case UNIT_TYPE_RADIORA:
+                            case UNIT_TYPE_VIZIARF_LOAD:
+                            case UNIT_TYPE_COMPOSE:
+                                thingUID = new ThingUID(THING_TYPE_DIMMABLE, bridgeUID, thingID);
+                                break;
+                            default:
+                                thingUID = new ThingUID(THING_TYPE_UNIT, bridgeUID, thingID);
+                                logger.debug("Generic unit type: {}", thingType);
+                                break;
+                        }
+
+                        DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
+                                .withProperties(properties).withProperty(THING_PROPERTIES_NUMBER, thingID)
+                                .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID)
+                                .withLabel(thingName).build();
+                        thingDiscovered(discoveryResult);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Generates zone items
+     */
+    private void discoverZones() {
+        final OmnilinkBridgeHandler handler = bridgeHandler;
+        if (handler != null) {
+            final ThingUID bridgeUID = handler.getThing().getUID();
+            final List<AreaProperties> areas = this.areas;
+
+            if (areas != null) {
+                for (AreaProperties areaProperties : areas) {
+                    int areaFilter = bitFilterForArea(areaProperties);
+
+                    ObjectPropertyRequest<ZoneProperties> objectPropertyRequest = ObjectPropertyRequest
+                            .builder(handler, ObjectPropertyRequests.ZONE, 0, 1).selectNamed().areaFilter(areaFilter)
+                            .build();
+
+                    for (ZoneProperties zoneProperties : objectPropertyRequest) {
+                        if (zoneProperties.getZoneType() <= SENSOR_TYPE_PROGRAMMABLE_ENERGY_SAVER_MODULE) {
+                            String thingName = zoneProperties.getName();
+                            String thingID = Integer.toString(zoneProperties.getNumber());
+
+                            Map<String, Object> properties = new HashMap<>();
+                            properties.put(THING_PROPERTIES_NAME, thingName);
+                            properties.put(THING_PROPERTIES_AREA, areaProperties.getNumber());
+
+                            ThingUID thingUID = new ThingUID(THING_TYPE_ZONE, bridgeUID, thingID);
+
+                            DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
+                                    .withProperties(properties).withProperty(THING_PROPERTIES_NUMBER, thingID)
+                                    .withRepresentationProperty(THING_PROPERTIES_NUMBER).withBridge(bridgeUID)
+                                    .withLabel(thingName).build();
+                            thingDiscovered(discoveryResult);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractAreaHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractAreaHandler.java
new file mode 100644 (file)
index 0000000..d0e6db0
--- /dev/null
@@ -0,0 +1,240 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.math.BigInteger;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.omnilink.internal.AreaAlarm;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.SecurityCodeValidation;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAreaStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.systemevents.AllOnOffEvent;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * The {@link AbstractAreaHandler} defines some methods that can be used across
+ * the many different areas defined in an OmniLink Controller.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public abstract class AbstractAreaHandler extends AbstractOmnilinkStatusHandler<ExtendedAreaStatus> {
+    private final Logger logger = LoggerFactory.getLogger(AbstractAreaHandler.class);
+    private final int thingID = getThingNumber();
+
+    public AbstractAreaHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+
+        super.initialize();
+        if (bridgeHandler != null) {
+            updateAreaProperties(bridgeHandler);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    "Received null bridge while initializing Area!");
+        }
+    }
+
+    private void updateAreaProperties(OmnilinkBridgeHandler bridgeHandler) {
+        final List<AreaProperties> areas = getAreaProperties();
+        if (areas != null) {
+            for (AreaProperties areaProperties : areas) {
+                String thingName = areaProperties.getName();
+                if (areaProperties.getNumber() == 1 && "".equals(thingName)) {
+                    thingName = "Main Area";
+                }
+                Map<String, String> properties = editProperties();
+                properties.put(THING_PROPERTIES_NAME, thingName);
+                updateProperties(properties);
+            }
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand: {}, command: {}", channelUID, command);
+
+        if (command instanceof RefreshType) {
+            retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE,
+                    ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!"));
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_AREA_ACTIVATE_KEYPAD_EMERGENCY:
+                handleKeypadEmergency(channelUID, command);
+                break;
+            default:
+                handleSecurityMode(channelUID, command);
+                break;
+        }
+    }
+
+    private void handleSecurityMode(ChannelUID channelUID, Command command) {
+        int mode = getMode(channelUID);
+
+        if (!(command instanceof StringType)) {
+            logger.debug("Invalid command: {}, must be StringType", command);
+            return;
+        }
+
+        logger.debug("Received mode: {}, on area: {}", mode, thingID);
+
+        char[] code = command.toFullString().toCharArray();
+        if (code.length != 4) {
+            logger.warn("Invalid code length, code must be 4 digits");
+        } else {
+            // mode, codeNum, areaNum
+            try {
+                final OmnilinkBridgeHandler bridge = getOmnilinkBridgeHandler();
+                if (bridge != null) {
+                    SecurityCodeValidation codeValidation = bridge.reqSecurityCodeValidation(thingID,
+                            Character.getNumericValue(code[0]), Character.getNumericValue(code[1]),
+                            Character.getNumericValue(code[2]), Character.getNumericValue(code[3]));
+                    /*
+                     * 0 Invalid code
+                     * 1 Master
+                     * 2 Manager
+                     * 3 User
+                     */
+                    logger.debug("User code number: {}, level: {}", codeValidation.getCodeNumber(),
+                            codeValidation.getAuthorityLevel());
+
+                    /*
+                     * Valid user code number are 1-99, 251 is duress code, 0 means code does not exist
+                     */
+                    if ((codeValidation.getCodeNumber() > 0 && codeValidation.getCodeNumber() <= 99)
+                            && codeValidation.getAuthorityLevel() > 0) {
+                        sendOmnilinkCommand(mode, codeValidation.getCodeNumber(), thingID);
+                    } else {
+                        logger.warn("System reported an invalid code");
+                    }
+                } else {
+                    logger.debug("Received null bridge while sending area command!");
+                }
+            } catch (OmniInvalidResponseException e) {
+                logger.debug("Could not arm area: {}, are all zones closed?", e.getMessage());
+            } catch (OmniUnknownMessageTypeException | BridgeOfflineException e) {
+                logger.debug("Could not send area command: {}", e.getMessage());
+            }
+        }
+        // This is a send only channel, so don't store the user code
+        updateState(channelUID, UnDefType.UNDEF);
+    }
+
+    /**
+     * Get the specific mode for the OmniLink type
+     *
+     * @param channelUID Channel that maps to a mode
+     * @return OmniLink representation of mode.
+     */
+    protected abstract int getMode(ChannelUID channelUID);
+
+    /**
+     * Get the set of alarms supported by this area handler.
+     *
+     * @return Set of alarms for this handler.
+     */
+    protected abstract EnumSet<AreaAlarm> getAlarms();
+
+    private void handleKeypadEmergency(ChannelUID channelUID, Command command) {
+        if (command instanceof DecimalType) {
+            try {
+                final OmnilinkBridgeHandler bridge = getOmnilinkBridgeHandler();
+                if (bridge != null) {
+                    bridge.activateKeypadEmergency(thingID, ((DecimalType) command).intValue());
+                } else {
+                    logger.debug("Received null bridge while sending Keypad Emergency command!");
+                }
+            } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+                logger.debug("Received exception while sending command to OmniLink Controller: {}", e.getMessage());
+            }
+        } else {
+            logger.debug("Invalid command: {}, must be DecimalType", command);
+        }
+    }
+
+    @Override
+    public void updateChannels(ExtendedAreaStatus status) {
+        logger.debug("Handle area event: mode: {}, alarms: {}, entryTimer: {}, exitTimer: {}", status.getMode(),
+                status.getAlarms(), status.getEntryTimer(), status.getExitTimer());
+
+        /*
+         * According to the specification, if the 3rd bit is set on a area mode, then that mode is in a delayed state.
+         * Unfortunately, this is not the case, but we can fix that by looking to see if the exit timer
+         * is set and do this manually.
+         */
+        int mode = status.getExitTimer() > 0 ? status.getMode() | 1 << 3 : status.getMode();
+        updateState(new ChannelUID(thing.getUID(), CHANNEL_AREA_MODE), new DecimalType(mode));
+
+        /*
+         * Alarm status is actually 8 status, packed into each bit, so we loop through to see if a bit is set, note that
+         * this means you can have multiple alarms set at once
+         */
+        BigInteger alarmBits = BigInteger.valueOf(status.getAlarms());
+        for (AreaAlarm alarm : getAlarms()) {
+            OnOffType alarmState = OnOffType.from(alarm.isSet(alarmBits));
+            updateState(new ChannelUID(thing.getUID(), alarm.getChannelUID()), alarmState);
+        }
+    }
+
+    public void handleAllOnOffEvent(AllOnOffEvent event) {
+        ChannelUID activateChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_AREA_ALL_ON_OFF_EVENT);
+        triggerChannel(activateChannel, event.isOn() ? "ON" : "OFF");
+    }
+
+    @Override
+    protected Optional<ExtendedAreaStatus> retrieveStatus() {
+        try {
+            final OmnilinkBridgeHandler bridge = getOmnilinkBridgeHandler();
+            if (bridge != null) {
+                ObjectStatus objStatus = bridge.requestObjectStatus(Message.OBJ_TYPE_AREA, thingID, thingID, true);
+                return Optional.of((ExtendedAreaStatus) objStatus.getStatuses()[0]);
+            } else {
+                logger.debug("Received null bridge while updating Area status!");
+                return Optional.empty();
+            }
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Received exception while refreshing Area status: {}", e.getMessage());
+            return Optional.empty();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkHandler.java
new file mode 100644 (file)
index 0000000..f17f716
--- /dev/null
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.math.BigInteger;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * The {@link AbstractOmnilinkHandler} defines some methods that can be used across
+ * the many different things exposed by the OmniLink protocol
+ *
+ * @author Brian O'Connell - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public abstract class AbstractOmnilinkHandler extends BaseThingHandler {
+    private final Logger logger = LoggerFactory.getLogger(AbstractOmnilinkHandler.class);
+
+    public AbstractOmnilinkHandler(Thing thing) {
+        super(thing);
+    }
+
+    public @Nullable OmnilinkBridgeHandler getOmnilinkBridgeHandler() {
+        Bridge bridge = getBridge();
+        if (bridge != null) {
+            return (OmnilinkBridgeHandler) bridge.getHandler();
+        } else {
+            return null;
+        }
+    }
+
+    protected void sendOmnilinkCommand(int message, int param1, int param2) {
+        try {
+            final OmnilinkBridgeHandler bridge = getOmnilinkBridgeHandler();
+            if (bridge != null) {
+                bridge.sendOmnilinkCommand(message, param1, param2);
+            } else {
+                logger.debug("Received null bridge while sending OmniLink command!");
+            }
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Could not send command to OmniLink Controller: {}", e.getMessage());
+        }
+    }
+
+    /**
+     * Calculate the area filter the a supplied area
+     *
+     * @param area Area to calculate filter for.
+     * @return Calculated Bit Filter for the supplied area. Bit 0 is area 1, bit 2 is area 2 and so on.
+     */
+    protected static int bitFilterForArea(AreaProperties areaProperties) {
+        return BigInteger.ZERO.setBit(areaProperties.getNumber() - 1).intValue();
+    }
+
+    protected @Nullable List<AreaProperties> getAreaProperties() {
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        List<AreaProperties> areas = new LinkedList<>();
+
+        if (bridgeHandler != null) {
+            ObjectPropertyRequest<AreaProperties> objectPropertyRequest = ObjectPropertyRequest
+                    .builder(bridgeHandler, ObjectPropertyRequests.AREA, 0, 1).build();
+
+            for (AreaProperties areaProperties : objectPropertyRequest) {
+                String thingName = areaProperties.getName();
+                if (areaProperties.getNumber() == 1 && "".equals(thingName)) {
+                    areas.add(areaProperties);
+                    break;
+                } else if ("".equals(thingName)) {
+                    break;
+                } else {
+                    areas.add(areaProperties);
+                }
+            }
+        }
+        return areas;
+    }
+
+    /**
+     * Gets the configured number for a thing.
+     *
+     * @return Configured number for a thing.
+     */
+    protected int getThingNumber() {
+        return ((Number) getThing().getConfiguration().get(THING_PROPERTIES_NUMBER)).intValue();
+    }
+
+    /**
+     * Gets the configured area number for a thing.
+     *
+     * @return Configured area number for a thing.
+     */
+    protected int getAreaNumber() {
+        return ((Number) getThing().getConfiguration().get(THING_PROPERTIES_AREA)).intValue();
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkStatusHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AbstractOmnilinkStatusHandler.java
new file mode 100644 (file)
index 0000000..01e5290
--- /dev/null
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.Status;
+
+/**
+ * The {@link AbstractOmnilinkStatusHandler} defines some methods that can be used across
+ * the many different units exposed by the OmniLink protocol to retrive updated status information.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public abstract class AbstractOmnilinkStatusHandler<T extends Status> extends AbstractOmnilinkHandler {
+    public AbstractOmnilinkStatusHandler(Thing thing) {
+        super(thing);
+    }
+
+    private volatile Optional<T> status = Optional.empty();
+
+    @Override
+    public void initialize() {
+        updateHandlerStatus();
+    }
+
+    /**
+     * Attempt to retrieve an updated status for this handler type.
+     *
+     * @return Optional with updated status if possible, empty optional otherwise.
+     */
+    protected abstract Optional<T> retrieveStatus();
+
+    /**
+     * Update channels associated with handler
+     *
+     * @param t Status object to update channels with
+     */
+    protected abstract void updateChannels(T t);
+
+    /**
+     * Process a status update for this handler. This will dispatch updateChannels where appropriate.
+     *
+     * @param t Status to process.
+     */
+    public void handleStatus(T t) {
+        this.status = Optional.of(t);
+        updateChannels(t);
+    }
+
+    @Override
+    public void channelLinked(ChannelUID channelUID) {
+        status.ifPresent(this::updateChannels);
+    }
+
+    private void updateHandlerStatus() {
+        Bridge bridge = getBridge();
+        if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
+            updateStatus(ThingStatus.ONLINE);
+            retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE,
+                    ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!"));
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioSourceHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioSourceHandler.java
new file mode 100644 (file)
index 0000000..9846edf
--- /dev/null
@@ -0,0 +1,182 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.AudioSourceStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AudioSourceProperties;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * The {@link AudioSourceHandler} defines some methods that are used to
+ * interface with an OmniLink Audio Source. This by extension also defines the
+ * Audio Source thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Brian O'Connell - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class AudioSourceHandler extends AbstractOmnilinkHandler {
+    private final Logger logger = LoggerFactory.getLogger(AudioSourceHandler.class);
+    private final int POLL_DELAY_SECONDS = 5;
+    private final int thingID = getThingNumber();
+    private @Nullable ScheduledFuture<?> scheduledPolling = null;
+    public @Nullable String number;
+
+    public AudioSourceHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        if (bridgeHandler != null) {
+            updateStatus(ThingStatus.ONLINE);
+            if (((Boolean) getThing().getConfiguration().get(THING_PROPERTIES_AUTO_START)).booleanValue()) {
+                logger.debug("Autostart enabled, scheduling polling for Audio Source: {}", thingID);
+                schedulePolling();
+            } else {
+                logger.debug("Autostart disabled, not scheduling polling for Audio Source: {}", thingID);
+                cancelPolling();
+            }
+            updateAudioSourceProperties(bridgeHandler);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    "Received null bridge while initializing Audio Source!");
+        }
+    }
+
+    private void updateAudioSourceProperties(OmnilinkBridgeHandler bridgeHandler) {
+        ObjectPropertyRequest<AudioSourceProperties> objectPropertyRequest = ObjectPropertyRequest
+                .builder(bridgeHandler, ObjectPropertyRequests.AUDIO_SOURCE, thingID, 0).selectNamed().build();
+
+        for (AudioSourceProperties audioSourceProperties : objectPropertyRequest) {
+            Map<String, String> properties = editProperties();
+            properties.put(THING_PROPERTIES_NAME, audioSourceProperties.getName());
+            updateProperties(properties);
+        }
+    }
+
+    @Override
+    public synchronized void dispose() {
+        cancelPolling();
+        super.dispose();
+    }
+
+    private synchronized void cancelPolling() {
+        final ScheduledFuture<?> scheduledPolling = this.scheduledPolling;
+        if (scheduledPolling != null) {
+            logger.debug("Cancelling polling for Audio Source: {}", thingID);
+            scheduledPolling.cancel(false);
+        }
+    }
+
+    private synchronized void schedulePolling() {
+        cancelPolling();
+        logger.debug("Scheduling polling for Audio Source: {}", thingID);
+        scheduledPolling = super.scheduler.scheduleWithFixedDelay(this::pollAudioSource, 0, POLL_DELAY_SECONDS,
+                TimeUnit.SECONDS);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+        final ScheduledFuture<?> scheduledPolling = this.scheduledPolling;
+
+        switch (channelUID.getId()) {
+            case CHANNEL_AUDIO_SOURCE_POLLING:
+                if (command instanceof RefreshType) {
+                    updateState(CHANNEL_AUDIO_SOURCE_POLLING,
+                            OnOffType.from((scheduledPolling != null && !scheduledPolling.isDone())));
+                } else if (command instanceof OnOffType) {
+                    handlePolling(channelUID, (OnOffType) command);
+                } else {
+                    logger.debug("Invalid command: {}, must be RefreshType or OnOffType", command);
+                }
+                break;
+            default:
+                logger.warn("Unknown channel for Audio Source thing: {}", channelUID);
+        }
+    }
+
+    private void handlePolling(ChannelUID channelUID, OnOffType command) {
+        logger.debug("handlePolling called for channel: {}, command: {}", channelUID, command);
+        if (OnOffType.ON.equals(command)) {
+            schedulePolling();
+        } else {
+            cancelPolling();
+        }
+    }
+
+    public void pollAudioSource() {
+        try {
+            final OmnilinkBridgeHandler bridge = getOmnilinkBridgeHandler();
+            if (bridge != null) {
+                Message message;
+                int position = 0;
+                while ((message = bridge.requestAudioSourceStatus(thingID, position))
+                        .getMessageType() == Message.MESG_TYPE_AUDIO_SOURCE_STATUS) {
+                    logger.trace("Polling for Audio Source statuses on thing: {}", thingID);
+                    AudioSourceStatus audioSourceStatus = (AudioSourceStatus) message;
+                    position = audioSourceStatus.getPosition();
+                    switch (position) {
+                        case 1:
+                            updateState(CHANNEL_AUDIO_SOURCE_TEXT1, new StringType(audioSourceStatus.getSourceData()));
+                            break;
+                        case 2:
+                            updateState(CHANNEL_AUDIO_SOURCE_TEXT2, new StringType(audioSourceStatus.getSourceData()));
+                            break;
+                        case 3:
+                            updateState(CHANNEL_AUDIO_SOURCE_TEXT3, new StringType(audioSourceStatus.getSourceData()));
+                            break;
+                        case 4:
+                            updateState(CHANNEL_AUDIO_SOURCE_TEXT4, new StringType(audioSourceStatus.getSourceData()));
+                            break;
+                        case 5:
+                            updateState(CHANNEL_AUDIO_SOURCE_TEXT5, new StringType(audioSourceStatus.getSourceData()));
+                            break;
+                        case 6:
+                            updateState(CHANNEL_AUDIO_SOURCE_TEXT6, new StringType(audioSourceStatus.getSourceData()));
+                            break;
+                    }
+                }
+            } else {
+                logger.debug("Received null bridge while polling Audio Source statuses!");
+            }
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Exception recieved while polling for Audio Source statuses: {}", e.getMessage());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioZoneHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/AudioZoneHandler.java
new file mode 100644 (file)
index 0000000..d6b7be7
--- /dev/null
@@ -0,0 +1,208 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.AudioPlayer;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.NextPreviousType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AudioZoneProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAudioZoneStatus;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * The {@link AudioZoneHandler} defines some methods that are used to
+ * interface with an OmniLink Audio Zone. This by extension also defines the
+ * Audio Zone thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class AudioZoneHandler extends AbstractOmnilinkStatusHandler<ExtendedAudioZoneStatus> {
+    private final Logger logger = LoggerFactory.getLogger(AudioZoneHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    public AudioZoneHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        if (bridgeHandler != null) {
+            updateAudioZoneProperties(bridgeHandler);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    "Received null bridge while initializing Audio Zone!");
+        }
+    }
+
+    private void updateAudioZoneProperties(OmnilinkBridgeHandler bridgeHandler) {
+        ObjectPropertyRequest<AudioZoneProperties> objectPropertyRequest = ObjectPropertyRequest
+                .builder(bridgeHandler, ObjectPropertyRequests.AUDIO_ZONE, thingID, 0).selectNamed().build();
+
+        for (AudioZoneProperties audioZoneProperties : objectPropertyRequest) {
+            Map<String, String> properties = editProperties();
+            properties.put(THING_PROPERTIES_NAME, audioZoneProperties.getName());
+            updateProperties(properties);
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+
+        if (command instanceof RefreshType) {
+            retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE,
+                    ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!"));
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_AUDIO_ZONE_POWER:
+                if (command instanceof OnOffType) {
+                    sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_ON_MUTE.getNumber(),
+                            OnOffType.OFF.equals(command) ? 0 : 1, thingID);
+                } else {
+                    logger.debug("Invalid command: {}, must be OnOffType", command);
+                }
+                break;
+            case CHANNEL_AUDIO_ZONE_MUTE:
+                if (command instanceof OnOffType) {
+                    sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_ON_MUTE.getNumber(),
+                            OnOffType.OFF.equals(command) ? 2 : 3, thingID);
+                } else {
+                    logger.debug("Invalid command: {}, must be OnOffType", command);
+                }
+                break;
+            case CHANNEL_AUDIO_ZONE_VOLUME:
+                if (command instanceof PercentType) {
+                    sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_VOLUME.getNumber(),
+                            ((PercentType) command).intValue(), thingID);
+                } else {
+                    logger.debug("Invalid command: {}, must be PercentType", command);
+                }
+                break;
+            case CHANNEL_AUDIO_ZONE_SOURCE:
+                if (command instanceof DecimalType) {
+                    sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_SOURCE.getNumber(),
+                            ((DecimalType) command).intValue(), thingID);
+                } else {
+                    logger.debug("Invalid command: {}, must be DecimalType", command);
+                }
+                break;
+            case CHANNEL_AUDIO_ZONE_CONTROL:
+                if (command instanceof PlayPauseType) {
+                    handlePlayPauseCommand(channelUID, (PlayPauseType) command);
+                } else if (command instanceof NextPreviousType) {
+                    handleNextPreviousCommand(channelUID, (NextPreviousType) command);
+                } else {
+                    logger.debug("Invalid command: {}, must be PlayPauseType or NextPreviousType", command);
+                }
+                break;
+            default:
+                logger.warn("Unknown channel for Audio Zone thing: {}", channelUID);
+        }
+    }
+
+    private void handlePlayPauseCommand(ChannelUID channelUID, PlayPauseType command) {
+        logger.debug("handlePlayPauseCommand called for channel: {}, command: {}", channelUID, command);
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+
+        if (bridgeHandler != null) {
+            Optional<AudioPlayer> audioPlayer = bridgeHandler.getAudioPlayer();
+            if (audioPlayer.isPresent()) {
+                AudioPlayer player = audioPlayer.get();
+                sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_SOURCE.getNumber(),
+                        PlayPauseType.PLAY.equals(command) ? player.getPlayCommand() : player.getPauseCommand(),
+                        thingID);
+            } else {
+                logger.warn("No Audio Player was detected!");
+            }
+        } else {
+            logger.debug("Received null bridge while sending Audio Zone command!");
+        }
+    }
+
+    private void handleNextPreviousCommand(ChannelUID channelUID, NextPreviousType command) {
+        logger.debug("handleNextPreviousCommand called for channel: {}, command: {}", channelUID, command);
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+
+        if (bridgeHandler != null) {
+            Optional<AudioPlayer> audioPlayer = bridgeHandler.getAudioPlayer();
+            if (audioPlayer.isPresent()) {
+                AudioPlayer player = audioPlayer.get();
+                sendOmnilinkCommand(OmniLinkCmd.CMD_AUDIO_ZONE_SET_SOURCE.getNumber(),
+                        NextPreviousType.NEXT.equals(command) ? player.getNextCommand() : player.getPreviousCommand(),
+                        thingID);
+            } else {
+                logger.warn("Audio Player could not be found!");
+            }
+        } else {
+            logger.debug("Received null bridge while sending Audio Zone command!");
+        }
+    }
+
+    @Override
+    public void updateChannels(ExtendedAudioZoneStatus status) {
+        logger.debug("updateChannels called for Audio Zone status: {}", status);
+        updateState(CHANNEL_AUDIO_ZONE_POWER, OnOffType.from(status.isPower()));
+        updateState(CHANNEL_AUDIO_ZONE_MUTE, OnOffType.from(status.isMute()));
+        updateState(CHANNEL_AUDIO_ZONE_VOLUME, new PercentType(status.getVolume()));
+        updateState(CHANNEL_AUDIO_ZONE_SOURCE, new DecimalType(status.getSource()));
+    }
+
+    @Override
+    protected Optional<ExtendedAudioZoneStatus> retrieveStatus() {
+        try {
+            final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+            if (bridgeHandler != null) {
+                ObjectStatus objStatus = bridgeHandler.requestObjectStatus(Message.OBJ_TYPE_AUDIO_ZONE, thingID,
+                        thingID, true);
+                return Optional.of((ExtendedAudioZoneStatus) objStatus.getStatuses()[0]);
+            } else {
+                logger.debug("Received null bridge while updating Audio Zone status!");
+                return Optional.empty();
+            }
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Received exception while refreshing Audio Zone status: {}", e.getMessage());
+            return Optional.empty();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/BridgeOfflineException.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/BridgeOfflineException.java
new file mode 100644 (file)
index 0000000..891d4c4
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link BridgeOfflineException} defines an exception for when the OmniLink
+ * Bridge is offline or unavailable.
+ *
+ * @author Craig Hamilton - Initial contribution
+ */
+@NonNullByDefault
+public class BridgeOfflineException extends Exception {
+    private static final long serialVersionUID = -9081729691518514097L;
+
+    public BridgeOfflineException(Exception e) {
+        super(e);
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ButtonHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ButtonHandler.java
new file mode 100644 (file)
index 0000000..a288fa0
--- /dev/null
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.ButtonProperties;
+
+/**
+ * The {@link ButtonHandler} defines some methods that are used to
+ * interface with an OmniLink Button. This by extension also defines the
+ * Button thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class ButtonHandler extends AbstractOmnilinkHandler {
+    private final Logger logger = LoggerFactory.getLogger(ButtonHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    public ButtonHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        if (bridgeHandler != null) {
+            updateStatus(ThingStatus.ONLINE);
+            updateChannels();
+            updateButtonProperties(bridgeHandler);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    "Received null bridge while initializing Button!");
+        }
+    }
+
+    private void updateButtonProperties(OmnilinkBridgeHandler bridgeHandler) {
+        final List<AreaProperties> areas = getAreaProperties();
+        if (areas != null) {
+            for (AreaProperties areaProperties : areas) {
+                int areaFilter = bitFilterForArea(areaProperties);
+
+                ObjectPropertyRequest<ButtonProperties> objectPropertyRequest = ObjectPropertyRequest
+                        .builder(bridgeHandler, ObjectPropertyRequests.BUTTONS, thingID, 0).selectNamed()
+                        .areaFilter(areaFilter).build();
+
+                for (ButtonProperties buttonProperties : objectPropertyRequest) {
+                    Map<String, String> properties = editProperties();
+                    properties.put(THING_PROPERTIES_NAME, buttonProperties.getName());
+                    properties.put(THING_PROPERTIES_AREA, Integer.toString(areaProperties.getNumber()));
+                    updateProperties(properties);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+
+        if (command instanceof RefreshType) {
+            updateChannels();
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_BUTTON_PRESS:
+                if (command instanceof OnOffType) {
+                    sendOmnilinkCommand(OmniLinkCmd.CMD_BUTTON.getNumber(), 0, thingID);
+                    updateChannels();
+                } else {
+                    logger.debug("Invalid command: {}, must be OnOffType", command);
+                }
+                break;
+            default:
+                logger.warn("Unknown channel for Button thing: {}", channelUID);
+        }
+    }
+
+    public void buttonActivated() {
+        ChannelUID activateChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_BUTTON_ACTIVATED_EVENT);
+        triggerChannel(activateChannel);
+    }
+
+    public void updateChannels() {
+        updateState(CHANNEL_BUTTON_PRESS, OnOffType.OFF);
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ConsoleHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ConsoleHandler.java
new file mode 100644 (file)
index 0000000..70340ed
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link ConsoleHandler} defines some methods that are used to
+ * interface with an OmniLink Console. This by extension also defines the
+ * Console thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class ConsoleHandler extends AbstractOmnilinkHandler {
+    private final Logger logger = LoggerFactory.getLogger(ConsoleHandler.class);
+    private final int thingID = getThingNumber();
+
+    public ConsoleHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        if (getOmnilinkBridgeHandler() != null) {
+            updateStatus(ThingStatus.ONLINE);
+            updateChannels();
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    "Received null bridge while initializing Console!");
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+
+        if (command instanceof RefreshType) {
+            updateChannels();
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER:
+                if (command instanceof StringType) {
+                    sendOmnilinkCommand(OmniLinkCmd.CMD_CONSOLE_ENABLE_DISABLE_BEEPER.getNumber(),
+                            ((StringType) command).equals(StringType.valueOf("OFF")) ? 0 : 1, thingID);
+                } else {
+                    logger.debug("Invalid command: {}, must be StringType", command);
+                }
+                break;
+            case CHANNEL_CONSOLE_BEEP:
+                if (command instanceof DecimalType) {
+                    sendOmnilinkCommand(OmniLinkCmd.CMD_CONSOLE_BEEP.getNumber(), ((DecimalType) command).intValue(),
+                            thingID);
+                } else {
+                    logger.debug("Invalid command: {}, must be DecimalType", command);
+                }
+                break;
+            default:
+                logger.warn("Unknown channel for Console thing: {}", channelUID);
+        }
+        updateChannels();
+    }
+
+    public void updateChannels() {
+        updateState(CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER, UnDefType.UNDEF);
+        updateState(CHANNEL_CONSOLE_BEEP, UnDefType.UNDEF);
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/HumiditySensorHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/HumiditySensorHandler.java
new file mode 100644 (file)
index 0000000..6536175
--- /dev/null
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.measure.quantity.Dimensionless;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AuxSensorProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAuxSensorStatus;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * The {@link HumiditySensorHandler} defines some methods that are used to
+ * interface with an OmniLink Humidity Sensor. This by extension also defines
+ * the Humidity Sensor thing that openHAB will be able to pick up and interface
+ * with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class HumiditySensorHandler extends AbstractOmnilinkStatusHandler<ExtendedAuxSensorStatus> {
+    private final Logger logger = LoggerFactory.getLogger(HumiditySensorHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    public HumiditySensorHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        if (bridgeHandler != null) {
+            updateHumiditySensorProperties(bridgeHandler);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    "Received null bridge while initializing Humidity Sensor!");
+        }
+    }
+
+    private void updateHumiditySensorProperties(OmnilinkBridgeHandler bridgeHandler) {
+        final List<AreaProperties> areas = getAreaProperties();
+        if (areas != null) {
+            for (AreaProperties areaProperties : areas) {
+                int areaFilter = bitFilterForArea(areaProperties);
+
+                ObjectPropertyRequest<AuxSensorProperties> objectPropertyRequest = ObjectPropertyRequest
+                        .builder(bridgeHandler, ObjectPropertyRequests.AUX_SENSORS, thingID, 0).selectNamed()
+                        .areaFilter(areaFilter).build();
+
+                for (AuxSensorProperties auxSensorProperties : objectPropertyRequest) {
+                    Map<String, String> properties = editProperties();
+                    properties.put(THING_PROPERTIES_NAME, auxSensorProperties.getName());
+                    properties.put(THING_PROPERTIES_AREA, Integer.toString(areaProperties.getNumber()));
+                    updateProperties(properties);
+                }
+            }
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+
+        if (command instanceof RefreshType) {
+            retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE,
+                    ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!"));
+            return;
+        }
+
+        if (!(command instanceof QuantityType)) {
+            logger.debug("Invalid command: {}, must be QuantityType", command);
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_AUX_LOW_SETPOINT:
+                sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_HEAT_LOW_POINT.getNumber(),
+                        TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType<Dimensionless>) command).intValue()),
+                        thingID);
+                break;
+            case CHANNEL_AUX_HIGH_SETPOINT:
+                sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_COOL_HIGH_POINT.getNumber(),
+                        TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType<Dimensionless>) command).intValue()),
+                        thingID);
+                break;
+            default:
+                logger.warn("Unknown channel for Humdity Sensor thing: {}", channelUID);
+        }
+    }
+
+    @Override
+    public void updateChannels(ExtendedAuxSensorStatus status) {
+        logger.debug("updateChannels called for Humidity Sensor status: {}", status);
+        updateState(CHANNEL_AUX_HUMIDITY,
+                new QuantityType<>(TemperatureFormat.FAHRENHEIT.omniToFormat(status.getTemperature()), Units.PERCENT));
+        updateState(CHANNEL_AUX_LOW_SETPOINT,
+                new QuantityType<>(TemperatureFormat.FAHRENHEIT.omniToFormat(status.getHeatSetpoint()), Units.PERCENT));
+        updateState(CHANNEL_AUX_HIGH_SETPOINT,
+                new QuantityType<>(TemperatureFormat.FAHRENHEIT.omniToFormat(status.getCoolSetpoint()), Units.PERCENT));
+    }
+
+    @Override
+    protected Optional<ExtendedAuxSensorStatus> retrieveStatus() {
+        try {
+            final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+            if (bridgeHandler != null) {
+                ObjectStatus objStatus = bridgeHandler.requestObjectStatus(Message.OBJ_TYPE_AUX_SENSOR, thingID,
+                        thingID, true);
+                return Optional.of((ExtendedAuxSensorStatus) objStatus.getStatuses()[0]);
+            } else {
+                logger.debug("Received null bridge while updating Humidity Sensor status!");
+                return Optional.empty();
+            }
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Received exception while refreshing Humidity Sensor status: {}", e.getMessage());
+            return Optional.empty();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LockHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LockHandler.java
new file mode 100644 (file)
index 0000000..92a20ec
--- /dev/null
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AccessControlReaderProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAccessControlReaderLockStatus;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * The {@link LockHandler} defines some methods that are used to
+ * interface with an OmniLink Lock. This by extension also defines the
+ * Lock thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Brian O'Connell - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class LockHandler extends AbstractOmnilinkStatusHandler<ExtendedAccessControlReaderLockStatus> {
+    private final Logger logger = LoggerFactory.getLogger(LockHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    public LockHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        if (bridgeHandler != null) {
+            updateLockProperties(bridgeHandler);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    "Received null bridge while initializing Lock!");
+        }
+    }
+
+    private void updateLockProperties(OmnilinkBridgeHandler bridgeHandler) {
+        ObjectPropertyRequest<AccessControlReaderProperties> objectPropertyRequest = ObjectPropertyRequest
+                .builder(bridgeHandler, ObjectPropertyRequests.LOCK, thingID, 0).selectNamed().build();
+
+        for (AccessControlReaderProperties lockProperties : objectPropertyRequest) {
+            Map<String, String> properties = editProperties();
+            properties.put(THING_PROPERTIES_NAME, lockProperties.getName());
+            updateProperties(properties);
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+
+        if (command instanceof RefreshType) {
+            retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE,
+                    ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!"));
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_LOCK_SWITCH:
+                if (command instanceof OnOffType) {
+                    sendOmnilinkCommand(OnOffType.OFF.equals(command) ? OmniLinkCmd.CMD_UNLOCK_DOOR.getNumber()
+                            : OmniLinkCmd.CMD_LOCK_DOOR.getNumber(), 0, thingID);
+                } else {
+                    logger.debug("Invalid command {}, must be OnOffType", command);
+                }
+                break;
+            default:
+                logger.warn("Unknown channel for Lock thing: {}", channelUID);
+        }
+    }
+
+    @Override
+    public void updateChannels(ExtendedAccessControlReaderLockStatus status) {
+        logger.debug("updateChannels called for Lock status: {}", status);
+        updateState(CHANNEL_LOCK_SWITCH, OnOffType.from(status.isLocked()));
+    }
+
+    @Override
+    protected Optional<ExtendedAccessControlReaderLockStatus> retrieveStatus() {
+        try {
+            final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+            if (bridgeHandler != null) {
+                ObjectStatus objStatus = bridgeHandler.requestObjectStatus(Message.OBJ_TYPE_CONTROL_LOCK, thingID,
+                        thingID, true);
+                return Optional.of((ExtendedAccessControlReaderLockStatus) objStatus.getStatuses()[0]);
+            } else {
+                logger.debug("Received null bridge while updating Lock status!");
+                return Optional.empty();
+            }
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Received exception while refreshing Lock status: {}", e.getMessage());
+            return Optional.empty();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LuminaAreaHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/LuminaAreaHandler.java
new file mode 100644 (file)
index 0000000..e560e09
--- /dev/null
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.AreaAlarm.*;
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.util.EnumSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.AreaAlarm;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+
+/**
+ * The {@link LuminaAreaHandler} defines some methods that are used to
+ * interface with an OmniLink Lumina Area. This by extension also defines the
+ * Lumina Area thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class LuminaAreaHandler extends AbstractAreaHandler {
+    private static final EnumSet<AreaAlarm> LUMINA_ALARMS = EnumSet.of(FREEZE, WATER, TEMPERATURE);
+    public @Nullable String number;
+
+    public LuminaAreaHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected int getMode(ChannelUID channelUID) {
+        switch (channelUID.getId()) {
+            case CHANNEL_AREA_SECURITY_MODE_HOME:
+                return OmniLinkCmd.CMD_SECURITY_LUMINA_HOME_MODE.getNumber();
+            case CHANNEL_AREA_SECURITY_MODE_SLEEP:
+                return OmniLinkCmd.CMD_SECURITY_LUMINA_SLEEP_MODE.getNumber();
+            case CHANNEL_AREA_SECURITY_MODE_AWAY:
+                return OmniLinkCmd.CMD_SECURITY_LUMINA_AWAY_MODE.getNumber();
+            case CHANNEL_AREA_SECURITY_MODE_VACATION:
+                return OmniLinkCmd.CMD_SECURITY_LUMINA_VACATION_MODE.getNumber();
+            case CHANNEL_AREA_SECURITY_MODE_PARTY:
+                return OmniLinkCmd.CMD_SECURITY_LUMINA_PARTY_MODE.getNumber();
+            case CHANNEL_AREA_SECURITY_MODE_SPECIAL:
+                return OmniLinkCmd.CMD_SECURITY_LUMINA_SPECIAL_MODE.getNumber();
+            default:
+                throw new IllegalStateException("Unknown channel for area thing " + channelUID);
+        }
+    }
+
+    @Override
+    protected EnumSet<AreaAlarm> getAlarms() {
+        return LUMINA_ALARMS;
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniAreaHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniAreaHandler.java
new file mode 100644 (file)
index 0000000..7424d8d
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.AreaAlarm.*;
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.util.EnumSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.AreaAlarm;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+
+/**
+ * The {@link OmniAreaHandler} defines some methods that are used to
+ * interface with an OmniLink OmniPro Area. This by extension also defines the
+ * OmniPro Area thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class OmniAreaHandler extends AbstractAreaHandler {
+    private static final EnumSet<AreaAlarm> OMNI_ALARMS = EnumSet.of(BURGLARY, FIRE, GAS, AUXILIARY, FREEZE, WATER,
+            DURESS, TEMPERATURE);
+    public @Nullable String number;
+
+    public OmniAreaHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected int getMode(ChannelUID channelUID) {
+        switch (channelUID.getId()) {
+            case CHANNEL_AREA_SECURITY_MODE_DISARM:
+                return OmniLinkCmd.CMD_SECURITY_OMNI_DISARM.getNumber();
+            case CHANNEL_AREA_SECURITY_MODE_DAY:
+                return OmniLinkCmd.CMD_SECURITY_OMNI_DAY_MODE.getNumber();
+            case CHANNEL_AREA_SECURITY_MODE_NIGHT:
+                return OmniLinkCmd.CMD_SECURITY_OMNI_NIGHT_MODE.getNumber();
+            case CHANNEL_AREA_SECURITY_MODE_AWAY:
+                return OmniLinkCmd.CMD_SECURITY_OMNI_AWAY_MODE.getNumber();
+            case CHANNEL_AREA_SECURITY_MODE_VACATION:
+                return OmniLinkCmd.CMD_SECURITY_OMNI_VACATION_MODE.getNumber();
+            case CHANNEL_AREA_SECURITY_MODE_DAY_INSTANT:
+                return OmniLinkCmd.CMD_SECURITY_OMNI_DAY_INSTANCE_MODE.getNumber();
+            case CHANNEL_AREA_SECURITY_MODE_NIGHT_DELAYED:
+                return OmniLinkCmd.CMD_SECURITY_OMNI_NIGHT_DELAYED_MODE.getNumber();
+            default:
+                throw new IllegalStateException("Unknown channel for area thing " + channelUID);
+        }
+    }
+
+    @Override
+    protected EnumSet<AreaAlarm> getAlarms() {
+        return OMNI_ALARMS;
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniLinkCmd.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmniLinkCmd.java
new file mode 100644 (file)
index 0000000..20e3f7c
--- /dev/null
@@ -0,0 +1,199 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * OmniLink commands
+ *
+ * @author Dan Cunningham - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ * @since 1.5.0
+ */
+@NonNullByDefault
+public enum OmniLinkCmd {
+    CMD_UNIT_OFF(0),
+    CMD_UNIT_ON(1),
+    CMD_UNIT_AREA_ALL_OFF(2),
+    CMD_UNIT_AREA_ALL_ON(3),
+    CMD_UNIT_PERCENT(9),
+    CMD_UNIT_LO9_LEVEL_HIGH7(101),
+    CMD_UNIT_DECREMENT_COUNTER(10),
+    CMD_UNIT_INCREMENT_COUNTER(11),
+    CMD_UNIT_SET_COUNTER(12),
+    CMD_UNIT_LO9_RAMP_HIGH7(13),
+    CMD_UNIT_LIGHTOLIER(14),
+    CMD_UNIT_UPB_REQ_STATUS(15),
+    CMD_UNIT_UNIT_DIM_STEP_1(17),
+    CMD_UNIT_UNIT_DIM_STEP_2(18),
+    CMD_UNIT_UNIT_DIM_STEP_3(19),
+    CMD_UNIT_UNIT_DIM_STEP_4(20),
+    CMD_UNIT_UNIT_DIM_STEP_5(21),
+    CMD_UNIT_UNIT_DIM_STEP_6(22),
+    CMD_UNIT_UNIT_DIM_STEP_7(23),
+    CMD_UNIT_UNIT_DIM_STEP_8(24),
+    CMD_UNIT_UNIT_DIM_STEP_9(25),
+    CMD_UNIT_UNIT_BRIGHTEN_STEP_1(33),
+    CMD_UNIT_UNIT_BRIGHTEN_STEP_2(34),
+    CMD_UNIT_UNIT_BRIGHTEN_STEP_3(35),
+    CMD_UNIT_UNIT_BRIGHTEN_STEP_4(36),
+    CMD_UNIT_UNIT_BRIGHTEN_STEP_5(37),
+    CMD_UNIT_UNIT_BRIGHTEN_STEP_6(38),
+    CMD_UNIT_UNIT_BRIGHTEN_STEP_7(39),
+    CMD_UNIT_UNIT_BRIGHTEN_STEP_8(40),
+    CMD_UNIT_UNIT_BRIGHTEN_STEP_9(41),
+    CMD_UNIT_UPB_LO9_BLINK_HIGH7(26),
+    CMD_UNIT_UPB_STOP_BLINK(27),
+    CMD_UNIT_UPB_LINK_OFF(28),
+    CMD_UNIT_UPB_LINK_ON(29),
+    CMD_UNIT_UPB_LINK_SET(30),
+    CMD_UNIT_CENTRALITE_SCENE_OFF(42),
+    CMD_UNIT_CENTRALITE_SCENE_ON(43),
+    CMD_UNIT_UPB_LED_OFF(44),
+    CMD_UNIT_UPB_LED_ON(45),
+    CMD_UNIT_RADIORA_PHANTOM_BUTTON_OFF(46),
+    CMD_UNIT_RADIORA_PHANTM_BUTTON_ON(46),
+    CMD_UNIT_LEVITON_SCENE_OFF(60),
+    CMD_UNIT_LEVITON_SCENE_ON(61),
+    CMD_UNIT_LEVITON_SCENE_SET(62),
+
+    CMD_SECURITY_OMNI_DISARM(48),
+    CMD_SECURITY_OMNI_DAY_MODE(49),
+    CMD_SECURITY_OMNI_NIGHT_MODE(50),
+    CMD_SECURITY_OMNI_AWAY_MODE(51),
+    CMD_SECURITY_OMNI_VACATION_MODE(52),
+    CMD_SECURITY_OMNI_DAY_INSTANCE_MODE(53),
+    CMD_SECURITY_OMNI_NIGHT_DELAYED_MODE(54),
+    CMD_SECURITY_BYPASS_ZONE(4),
+    CMD_SECURITY_RESTORE_ZONE(5),
+    CMD_SECURITY_RESTORE_ALL_ZONES(6),
+    CMD_SECURITY_LUMINA_HOME_MODE(49),
+    CMD_SECURITY_LUMINA_SLEEP_MODE(50),
+    CMD_SECURITY_LUMINA_AWAY_MODE(51),
+    CMD_SECURITY_LUMINA_VACATION_MODE(52),
+    CMD_SECURITY_LUMINA_PARTY_MODE(53),
+    CMD_SECURITY_LUMINA_SPECIAL_MODE(54),
+
+    CMD_BUTTON(7),
+
+    CMD_ENERGY_SAVER_OFF(64),
+    CMD_ENERGY_SAVER_ON(65),
+
+    CMD_THERMO_SET_HEAT_LOW_POINT(66),
+    CMD_THERMO_SET_COOL_HIGH_POINT(67),
+    CMD_THERMO_SET_SYSTEM_MODE(68),
+    CMD_THERMO_SET_FAN_MODE(69),
+    CMD_THERMO_SET_HOLD_MODE(70),
+    CMD_THERMO_RAISE_LOWER_HEAT(71),
+    CMD_THERMO_RAISE_LOWER_COOL(72),
+    CMD_THERMO_SET_HUMDIFY_POINT(73),
+    CMD_THERMO_SET_DEHUMIDIFY_POINT(74),
+
+    CMD_MESSAGE_SHOW_MESSAGE_WITH_BEEP_AND_LED(80),
+    CMD_MESSAGE_SHOW_MESSAGE_WITH_BEEP_OR_LED(86),
+    CMD_MESSAGE_LOG_MESSAGE(81),
+    CMD_MESSAGE_CLEAR_MESSAGE(82),
+    CMD_MESSAGE_SAY_MESSAGE(83),
+    CMD_MESSAGE_PHONE_AND_SAY_MESSAGE(84),
+    CMD_MESSAGE_SEND_MESSAGE_TO_SERIAL_PORT(85),
+
+    CMD_CONSOLE_ENABLE_DISABLE_BEEPER(102),
+    CMD_CONSOLE_BEEP(103),
+
+    CMD_LOCK_DOOR(105),
+    CMD_UNLOCK_DOOR(106),
+
+    CMD_AUDIO_ZONE_SET_ON_MUTE(112),
+    CMD_AUDIO_ZONE_SET_VOLUME(113),
+    CMD_AUDIO_ZONE_SET_SOURCE(114),
+    CMD_AUDIO_ZONE_SELECT_KEY(115),
+
+    SENSOR_UNIT_POWER(1001),
+    SENSOR_UNIT_LEVEL(1002),
+    SENSOR_UNIT_DISPLAY(1003),
+    SENSOR_THERMO_HEAT_POINTC(2001),
+    SENSOR_THERMO_HEAT_POINTF(2002),
+    SENSOR_THERMO_COOL_POINTC(2003),
+    SENSOR_THERMO_COOL_POINTF(2004),
+    SENSOR_THERMO_SYSTEM_MODE(2005),
+    SENSOR_THERMO_FAN_MODE(2006),
+    SENSOR_THERMO_HOLD_MODE(2007),
+    SENSOR_THERMO_TEMPC(2006),
+    SENSOR_THERMO_TEMPF(2007),
+    SENSOR_ZONE_STATUS_CURRENT(3001),
+    SENSOR_ZONE_STATUS_LATCHED(3002),
+    SENSOR_ZONE_STATUS_ARMING(3003),
+    SENSOR_AREA_STATUS_MODE(4001),
+    SENSOR_AREA_STATUS_ALARM(4002),
+    SENSOR_AREA_STATUS_EXIT_DELAY(4003),
+    SENSOR_AREA_STATUS_ENTRY_DELAY(4003),
+    SENSOR_AREA_EXIT_TIMER(4004),
+    SENSOR_AREA_ENTRY_TIMER(4005),
+    SENSOR_AUX_STATUS(5001),
+    SENSOR_AUX_CURRENTC(5002),
+    SENSOR_AUX_CURRENTF(5003),
+    SENSOR_AUX_LOWC(5004),
+    SENSOR_AUX_LOWF(5005),
+    SENSOR_AUX_HIGHC(5006),
+    SENSOR_AUX_HIGHF(5007),
+    SENSOR_AUDIOZONE_POWER(6001),
+    SENSOR_AUDIOZONE_SOURCE(6002),
+    SENSOR_AUDIOZONE_VOLUME(6003),
+    SENSOR_AUDIOZONE_MUTE(6004),
+    SENSOR_AUDIOZONE_TEXT(6005),
+    SENSOR_AUDIOZONE_TEXT_FIELD1(6006),
+    SENSOR_AUDIOZONE_TEXT_FIELD2(6007),
+    SENSOR_AUDIOZONE_TEXT_FIELD3(6008),
+    SENSOR_AUDIOSOURCE_TEXT(7001),
+    SENSOR_AUDIOSOURCE_TEXT_FIELD1(7002),
+    SENSOR_AUDIOSOURCE_TEXT_FIELD2(7003),
+    SENSOR_AUDIOSOURCE_TEXT_FIELD3(7004);
+
+    private int number;
+
+    OmniLinkCmd(int number) {
+        this.number = number;
+    }
+
+    public int getNumber() {
+        return number;
+    }
+
+    public static @Nullable OmniLinkCmd getCommand(String name) {
+        for (OmniLinkCmd command : OmniLinkCmd.values()) {
+            if (name.equals(command.toString())) {
+                return command;
+            }
+        }
+        return null;
+    }
+
+    public static @Nullable OmniLinkCmd getCommand(int ordinal) {
+        OmniLinkCmd[] values = OmniLinkCmd.values();
+        if (ordinal < values.length) {
+            return values[ordinal];
+        } else {
+            return null;
+        }
+    }
+
+    public static String toList() {
+        StringBuilder sb = new StringBuilder();
+        for (OmniLinkCmd command : OmniLinkCmd.values()) {
+            sb.append(command.toString()).append(",");
+        }
+        return sb.toString();
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmnilinkBridgeHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/OmnilinkBridgeHandler.java
new file mode 100644 (file)
index 0000000..7cc1056
--- /dev/null
@@ -0,0 +1,696 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.time.ZonedDateTime;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.AudioPlayer;
+import org.openhab.binding.omnilink.internal.SystemType;
+import org.openhab.binding.omnilink.internal.config.OmnilinkBridgeConfig;
+import org.openhab.binding.omnilink.internal.discovery.OmnilinkDiscoveryService;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.Connection;
+import com.digitaldan.jomnilinkII.DisconnectListener;
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.EventLogData;
+import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.SecurityCodeValidation;
+import com.digitaldan.jomnilinkII.MessageTypes.SystemFeatures;
+import com.digitaldan.jomnilinkII.MessageTypes.SystemFormats;
+import com.digitaldan.jomnilinkII.MessageTypes.SystemInformation;
+import com.digitaldan.jomnilinkII.MessageTypes.SystemStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAccessControlReaderLockStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAreaStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAudioZoneStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAuxSensorStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedThermostatStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedUnitStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedZoneStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.Status;
+import com.digitaldan.jomnilinkII.MessageTypes.systemevents.AllOnOffEvent;
+import com.digitaldan.jomnilinkII.MessageTypes.systemevents.ButtonEvent;
+import com.digitaldan.jomnilinkII.MessageTypes.systemevents.SwitchPressEvent;
+import com.digitaldan.jomnilinkII.MessageTypes.systemevents.SystemEvent;
+import com.digitaldan.jomnilinkII.MessageTypes.systemevents.UPBLinkEvent;
+import com.digitaldan.jomnilinkII.NotificationListener;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniNotConnectedException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+import com.google.gson.Gson;
+
+/**
+ * The {@link OmnilinkBridgeHandler} defines some methods that are used to
+ * interface with an OmniLink Controller. This by extension also defines the
+ * OmniLink bridge that openHAB will be able to pick up and interface with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class OmnilinkBridgeHandler extends BaseBridgeHandler implements NotificationListener, DisconnectListener {
+    private final Logger logger = LoggerFactory.getLogger(OmnilinkBridgeHandler.class);
+    private @Nullable Connection omniConnection = null;
+    private @Nullable ScheduledFuture<?> connectJob;
+    private @Nullable ScheduledFuture<?> eventPollingJob;
+    private final int autoReconnectPeriod = 60;
+    private Optional<AudioPlayer> audioPlayer = Optional.empty();
+    private @Nullable SystemType systemType = null;
+    private final Gson gson = new Gson();
+    private int eventLogNumber = 0;
+
+    public OmnilinkBridgeHandler(Bridge bridge) {
+        super(bridge);
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(OmnilinkDiscoveryService.class);
+    }
+
+    public void sendOmnilinkCommand(final int message, final int param1, final int param2)
+            throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
+        try {
+            getOmniConnection().controllerCommand(message, param1, param2);
+        } catch (IOException | OmniNotConnectedException e) {
+            setOfflineAndReconnect(e.getMessage());
+            throw new BridgeOfflineException(e);
+        }
+    }
+
+    public SecurityCodeValidation reqSecurityCodeValidation(int area, int digit1, int digit2, int digit3, int digit4)
+            throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
+        try {
+            return getOmniConnection().reqSecurityCodeValidation(area, digit1, digit2, digit3, digit4);
+        } catch (IOException | OmniNotConnectedException e) {
+            setOfflineAndReconnect(e.getMessage());
+            throw new BridgeOfflineException(e);
+        }
+    }
+
+    public void activateKeypadEmergency(int area, int emergencyType)
+            throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
+        try {
+            getOmniConnection().activateKeypadEmergency(area, emergencyType);
+        } catch (IOException | OmniNotConnectedException e) {
+            setOfflineAndReconnect(e.getMessage());
+            throw new BridgeOfflineException(e);
+        }
+    }
+
+    public SystemInformation reqSystemInformation()
+            throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
+        try {
+            return getOmniConnection().reqSystemInformation();
+        } catch (IOException | OmniNotConnectedException e) {
+            setOfflineAndReconnect(e.getMessage());
+            throw new BridgeOfflineException(e);
+        }
+    }
+
+    public SystemFormats reqSystemFormats()
+            throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
+        try {
+            return getOmniConnection().reqSystemFormats();
+        } catch (IOException | OmniNotConnectedException e) {
+            setOfflineAndReconnect(e.getMessage());
+            throw new BridgeOfflineException(e);
+        }
+    }
+
+    private SystemFeatures reqSystemFeatures()
+            throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
+        try {
+            return getOmniConnection().reqSystemFeatures();
+        } catch (IOException | OmniNotConnectedException e) {
+            setOfflineAndReconnect(e.getMessage());
+            throw new BridgeOfflineException(e);
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+
+        if (command instanceof RefreshType) {
+            updateChannels();
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_SYSTEMDATE:
+                if (command instanceof DateTimeType) {
+                    ZonedDateTime zdt = ((DateTimeType) command).getZonedDateTime();
+                    boolean inDaylightSavings = zdt.getZone().getRules().isDaylightSavings(zdt.toInstant());
+                    try {
+                        getOmniConnection().setTimeCommand(zdt.getYear() - 2000, zdt.getMonthValue(),
+                                zdt.getDayOfMonth(), zdt.getDayOfWeek().getValue(), zdt.getHour(), zdt.getMinute(),
+                                inDaylightSavings);
+                    } catch (IOException | OmniNotConnectedException | OmniInvalidResponseException
+                            | OmniUnknownMessageTypeException e) {
+                        logger.debug("Could not send Set Time command to OmniLink Controller: {}", e.getMessage());
+                    }
+                } else {
+                    logger.debug("Invalid command: {}, must be DateTimeType", command);
+                }
+                break;
+            case CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER:
+                if (command instanceof StringType) {
+                    try {
+                        sendOmnilinkCommand(OmniLinkCmd.CMD_CONSOLE_ENABLE_DISABLE_BEEPER.getNumber(),
+                                ((StringType) command).equals(StringType.valueOf("OFF")) ? 0 : 1, 0);
+                        updateState(CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER, UnDefType.UNDEF);
+                    } catch (NumberFormatException | OmniInvalidResponseException | OmniUnknownMessageTypeException
+                            | BridgeOfflineException e) {
+                        logger.debug("Could not send Console command to OmniLink Controller: {}", e.getMessage());
+                    }
+                } else {
+                    logger.debug("Invalid command: {}, must be StringType", command);
+                }
+                break;
+            case CHANNEL_CONSOLE_BEEP:
+                if (command instanceof DecimalType) {
+                    try {
+                        sendOmnilinkCommand(OmniLinkCmd.CMD_CONSOLE_BEEP.getNumber(),
+                                ((DecimalType) command).intValue(), 0);
+                        updateState(CHANNEL_CONSOLE_BEEP, UnDefType.UNDEF);
+                    } catch (NumberFormatException | OmniInvalidResponseException | OmniUnknownMessageTypeException
+                            | BridgeOfflineException e) {
+                        logger.debug("Could not send Console command to OmniLink Controller: {}", e.getMessage());
+                    }
+                } else {
+                    logger.debug("Invalid command: {}, must be DecimalType", command);
+                }
+                break;
+            default:
+                logger.warn("Unknown channel for Bridge thing: {}", channelUID);
+        }
+    }
+
+    private void makeOmnilinkConnection() {
+        final Connection connection = omniConnection;
+        if (connection != null && connection.connected()) {
+            return;
+        }
+
+        logger.debug("Attempting to connect to controller!");
+        try {
+            OmnilinkBridgeConfig config = getConfigAs(OmnilinkBridgeConfig.class);
+
+            this.omniConnection = new Connection(config.getIpAddress(), config.getPort(),
+                    config.getKey1() + ":" + config.getKey2());
+
+            /*
+             * HAI only supports one audio player - cycle through features until we find a feature that is an audio
+             * player.
+             */
+            audioPlayer = reqSystemFeatures().getFeatures().stream()
+                    .map(featureCode -> AudioPlayer.getAudioPlayerForFeatureCode(featureCode))
+                    .filter(Optional::isPresent).findFirst().orElse(Optional.empty());
+
+            systemType = SystemType.getType(reqSystemInformation().getModel());
+
+            if (config.getLogPollingInterval() > 0) {
+                startEventPolling(config.getLogPollingInterval());
+            }
+
+            final Connection connectionNew = omniConnection;
+            if (connectionNew != null) {
+                connectionNew.enableNotifications();
+                connectionNew.addNotificationListener(OmnilinkBridgeHandler.this);
+                connectionNew.addDisconnectListener(this);
+            }
+
+            updateStatus(ThingStatus.ONLINE);
+            cancelReconnectJob(false);
+            updateChannels();
+            updateBridgeProperties();
+        } catch (UnknownHostException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+        } catch (IOException e) {
+            final Throwable cause = e.getCause();
+            if (cause != null) {
+                final String causeMessage = cause.getMessage();
+
+                if (causeMessage != null && causeMessage.contains("Connection timed out")) {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                            "IP Address probably incorrect, timed out creating connection!");
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, causeMessage);
+                }
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+            }
+        } catch (Exception e) {
+            setOfflineAndReconnect(e.getMessage());
+            logger.debug("Error connecting to OmniLink Controller: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public void objectStatusNotification(@Nullable ObjectStatus objectStatus) {
+        if (objectStatus != null) {
+            Status[] statuses = objectStatus.getStatuses();
+            for (Status status : statuses) {
+                if (status instanceof ExtendedUnitStatus) {
+                    ExtendedUnitStatus unitStatus = (ExtendedUnitStatus) status;
+                    int unitNumber = unitStatus.getNumber();
+
+                    logger.debug("Received status update for Unit: {}, status: {}", unitNumber, unitStatus);
+                    Optional<Thing> theThing = getUnitThing(unitNumber);
+                    theThing.map(Thing::getHandler)
+                            .ifPresent(theHandler -> ((UnitHandler) theHandler).handleStatus(unitStatus));
+                } else if (status instanceof ExtendedZoneStatus) {
+                    ExtendedZoneStatus zoneStatus = (ExtendedZoneStatus) status;
+                    int zoneNumber = zoneStatus.getNumber();
+
+                    logger.debug("Received status update for Zone: {}, status: {}", zoneNumber, zoneStatus);
+                    Optional<Thing> theThing = getChildThing(THING_TYPE_ZONE, zoneNumber);
+                    theThing.map(Thing::getHandler)
+                            .ifPresent(theHandler -> ((ZoneHandler) theHandler).handleStatus(zoneStatus));
+                } else if (status instanceof ExtendedAreaStatus) {
+                    final SystemType systemType = this.systemType;
+                    ExtendedAreaStatus areaStatus = (ExtendedAreaStatus) status;
+                    int areaNumber = areaStatus.getNumber();
+
+                    if (systemType != null) {
+                        logger.debug("Received status update for Area: {}, status: {}", areaNumber, areaStatus);
+                        Optional<Thing> theThing;
+                        switch (systemType) {
+                            case OMNI:
+                                theThing = getChildThing(THING_TYPE_OMNI_AREA, areaNumber);
+                                break;
+                            case LUMINA:
+                                theThing = getChildThing(THING_TYPE_LUMINA_AREA, areaNumber);
+                                break;
+                            default:
+                                theThing = Optional.empty();
+                        }
+                        theThing.map(Thing::getHandler)
+                                .ifPresent(theHandler -> ((AbstractAreaHandler) theHandler).handleStatus(areaStatus));
+                    } else {
+                        logger.debug("Received null System Type!");
+                    }
+                } else if (status instanceof ExtendedAccessControlReaderLockStatus) {
+                    ExtendedAccessControlReaderLockStatus lockStatus = (ExtendedAccessControlReaderLockStatus) status;
+                    int lockNumber = lockStatus.getNumber();
+
+                    logger.debug("Received status update for Lock: {}, status: {}", lockNumber, lockStatus);
+                    Optional<Thing> theThing = getChildThing(THING_TYPE_LOCK, lockNumber);
+                    theThing.map(Thing::getHandler)
+                            .ifPresent(theHandler -> ((LockHandler) theHandler).handleStatus(lockStatus));
+                } else if (status instanceof ExtendedThermostatStatus) {
+                    ExtendedThermostatStatus thermostatStatus = (ExtendedThermostatStatus) status;
+                    int thermostatNumber = thermostatStatus.getNumber();
+
+                    logger.debug("Received status update for Thermostat: {}, status: {}", thermostatNumber,
+                            thermostatStatus);
+                    Optional<Thing> theThing = getChildThing(THING_TYPE_THERMOSTAT, thermostatNumber);
+                    theThing.map(Thing::getHandler)
+                            .ifPresent(theHandler -> ((ThermostatHandler) theHandler).handleStatus(thermostatStatus));
+                } else if (status instanceof ExtendedAudioZoneStatus) {
+                    ExtendedAudioZoneStatus audioZoneStatus = (ExtendedAudioZoneStatus) status;
+                    int audioZoneNumber = audioZoneStatus.getNumber();
+
+                    logger.debug("Received status update for Audio Zone: {}, status: {}", audioZoneNumber,
+                            audioZoneStatus);
+                    Optional<Thing> theThing = getChildThing(THING_TYPE_AUDIO_ZONE, audioZoneNumber);
+                    theThing.map(Thing::getHandler)
+                            .ifPresent(theHandler -> ((AudioZoneHandler) theHandler).handleStatus(audioZoneStatus));
+                } else if (status instanceof ExtendedAuxSensorStatus) {
+                    ExtendedAuxSensorStatus auxSensorStatus = (ExtendedAuxSensorStatus) status;
+                    int auxSensorNumber = auxSensorStatus.getNumber();
+
+                    // Aux Sensors can be either temperature or humidity, need to check both.
+                    Optional<Thing> tempThing = getChildThing(THING_TYPE_TEMP_SENSOR, auxSensorNumber);
+                    Optional<Thing> humidityThing = getChildThing(THING_TYPE_HUMIDITY_SENSOR, auxSensorNumber);
+                    if (tempThing.isPresent()) {
+                        logger.debug("Received status update for Temperature Sensor: {}, status: {}", auxSensorNumber,
+                                auxSensorStatus);
+                        tempThing.map(Thing::getHandler).ifPresent(
+                                theHandler -> ((TempSensorHandler) theHandler).handleStatus(auxSensorStatus));
+                    }
+                    if (humidityThing.isPresent()) {
+                        logger.debug("Received status update for Humidity Sensor: {}, status: {}", auxSensorNumber,
+                                auxSensorStatus);
+                        humidityThing.map(Thing::getHandler).ifPresent(
+                                theHandler -> ((HumiditySensorHandler) theHandler).handleStatus(auxSensorStatus));
+                    }
+                } else {
+                    logger.debug("Received Object Status Notification that was not processed: {}", objectStatus);
+                }
+            }
+        } else {
+            logger.debug("Received null Object Status Notification!");
+        }
+    }
+
+    @Override
+    public void systemEventNotification(@Nullable SystemEvent event) {
+        if (event != null) {
+            logger.debug("Received System Event Notification of type: {}", event.getType());
+            switch (event.getType()) {
+                case PHONE_LINE_DEAD:
+                case PHONE_LINE_OFF_HOOK:
+                case PHONE_LINE_ON_HOOK:
+                case PHONE_LINE_RING:
+                    ChannelUID channel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_PHONE_LINE_EVENT);
+                    triggerChannel(channel, event.getType().toString().replaceAll("^PHONE_LINE_", ""));
+                    break;
+                case AC_POWER_OFF:
+                case AC_POWER_RESTORED:
+                    ChannelUID acChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_AC_POWER_EVENT);
+                    triggerChannel(acChannel, event.getType().toString().replaceAll("^AC_POWER_", ""));
+                    break;
+                case BATTERY_LOW:
+                case BATTERY_OK:
+                    ChannelUID batteryChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_BATTERY_EVENT);
+                    triggerChannel(batteryChannel, event.getType().toString().replaceAll("^BATTERY_", ""));
+                    break;
+                case DCM_OK:
+                case DCM_TROUBLE:
+                    ChannelUID dcmChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_DCM_EVENT);
+                    triggerChannel(dcmChannel, event.getType().toString().replaceAll("^DCM_", ""));
+                    break;
+                case ENERGY_COST_CRITICAL:
+                case ENERGY_COST_HIGH:
+                case ENERGY_COST_LOW:
+                case ENERGY_COST_MID:
+                    ChannelUID energyChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_ENERGY_COST_EVENT);
+                    triggerChannel(energyChannel, event.getType().toString().replaceAll("^ENERGY_COST_", ""));
+                    break;
+                case CAMERA_1_TRIGGER:
+                case CAMERA_2_TRIGGER:
+                case CAMERA_3_TRIGGER:
+                case CAMERA_4_TRIGGER:
+                case CAMERA_5_TRIGGER:
+                case CAMERA_6_TRIGGER:
+                    ChannelUID cameraChannel = new ChannelUID(getThing().getUID(),
+                            TRIGGER_CHANNEL_CAMERA_TRIGGER_EVENT);
+                    triggerChannel(cameraChannel, String.valueOf(event.getType().toString().charAt(8)));
+                    break;
+                case BUTTON:
+                    Optional<Thing> buttonThing = getChildThing(THING_TYPE_BUTTON,
+                            ((ButtonEvent) event).getButtonNumber());
+                    buttonThing.map(Thing::getHandler)
+                            .ifPresent(theHandler -> ((ButtonHandler) theHandler).buttonActivated());
+                    break;
+                case ALL_ON_OFF:
+                    Optional<Thing> areaThing = getChildThing(THING_TYPE_OMNI_AREA, ((AllOnOffEvent) event).getArea());
+                    if (areaThing.isPresent()) {
+                        logger.debug("Thing for allOnOff event: {}", areaThing.get().getUID());
+                        areaThing.map(Thing::getHandler).ifPresent(theHandler -> ((AbstractAreaHandler) theHandler)
+                                .handleAllOnOffEvent((AllOnOffEvent) event));
+                    }
+                    break;
+                case UPB_LINK:
+                    UPBLinkEvent linkEvent = (UPBLinkEvent) event;
+                    UPBLinkEvent.Command command = linkEvent.getLinkCommand();
+                    int link = linkEvent.getLinkNumber();
+                    handleUPBLink(link, command);
+                    break;
+                case ALC_UPB_RADIORA_STARLITE_SWITCH_PRESS:
+                    SwitchPressEvent switchPressEvent = (SwitchPressEvent) event;
+                    int unitNumber = switchPressEvent.getUnitNumber();
+
+                    Optional<Thing> unitThing = getUnitThing(unitNumber);
+                    unitThing.map(Thing::getHandler).ifPresent(
+                            theHandler -> ((UnitHandler) theHandler).handleSwitchPressEvent(switchPressEvent));
+                    break;
+                default:
+                    logger.warn("Ignoring System Event Notification of type: {}", event.getType());
+            }
+        } else {
+            logger.debug("Received null System Event Notification!");
+        }
+    }
+
+    private void handleUPBLink(int link, UPBLinkEvent.Command command) {
+        final ChannelUID activateChannel;
+
+        if (command == UPBLinkEvent.Command.ACTIVATED) {
+            activateChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_UPB_LINK_ACTIVATED_EVENT);
+        } else if (command == UPBLinkEvent.Command.DEACTIVATED) {
+            activateChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_UPB_LINK_DEACTIVATED_EVENT);
+        } else {
+            logger.debug("Received unsupported UPB link event: {}", command);
+            return;
+        }
+        triggerChannel(activateChannel, Integer.toString(link));
+    }
+
+    @Override
+    public void notConnectedEvent(@Nullable Exception e) {
+        if (e != null) {
+            logger.debug("Received an OmniLink Controller not connected event: {}", e.getMessage());
+            setOfflineAndReconnect(e.getMessage());
+        }
+    }
+
+    private void getSystemStatus() throws IOException, OmniNotConnectedException, OmniInvalidResponseException,
+            OmniUnknownMessageTypeException {
+        SystemStatus status = getOmniConnection().reqSystemStatus();
+        logger.debug("Received system status: {}", status);
+        // Let's update system time
+        String dateString = new StringBuilder().append(2000 + status.getYear()).append("-")
+                .append(String.format("%02d", status.getMonth())).append("-")
+                .append(String.format("%02d", status.getDay())).append("T")
+                .append(String.format("%02d", status.getHour())).append(":")
+                .append(String.format("%02d", status.getMinute())).append(":")
+                .append(String.format("%02d", status.getSecond())).toString();
+        updateState(CHANNEL_SYSTEMDATE, new DateTimeType(dateString));
+    }
+
+    public Message reqObjectProperties(int objectType, int objectNum, int direction, int filter1, int filter2,
+            int filter3) throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
+        try {
+            return getOmniConnection().reqObjectProperties(objectType, objectNum, direction, filter1, filter2, filter3);
+        } catch (OmniNotConnectedException | IOException e) {
+            setOfflineAndReconnect(e.getMessage());
+            throw new BridgeOfflineException(e);
+        }
+    }
+
+    public Message requestAudioSourceStatus(final int source, final int position)
+            throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
+        try {
+            return getOmniConnection().reqAudioSourceStatus(source, position);
+        } catch (OmniNotConnectedException | IOException e) {
+            setOfflineAndReconnect(e.getMessage());
+            throw new BridgeOfflineException(e);
+        }
+    }
+
+    public ObjectStatus requestObjectStatus(final int objType, final int startObject, final int endObject,
+            boolean extended)
+            throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
+        try {
+            return getOmniConnection().reqObjectStatus(objType, startObject, endObject, extended);
+        } catch (OmniNotConnectedException | IOException e) {
+            setOfflineAndReconnect(e.getMessage());
+            throw new BridgeOfflineException(e);
+        }
+    }
+
+    public Optional<TemperatureFormat> getTemperatureFormat() {
+        try {
+            return Optional.of(TemperatureFormat.valueOf(reqSystemFormats().getTempFormat()));
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Could not request temperature format from controller: {}", e.getMessage());
+            return Optional.empty();
+        }
+    }
+
+    public void updateChannels() {
+        try {
+            getSystemStatus();
+            updateState(CHANNEL_CONSOLE_ENABLE_DISABLE_BEEPER, UnDefType.UNDEF);
+            updateState(CHANNEL_CONSOLE_BEEP, UnDefType.UNDEF);
+        } catch (IOException | OmniNotConnectedException | OmniInvalidResponseException
+                | OmniUnknownMessageTypeException e) {
+            logger.warn("Unable to update bridge channels: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public void dispose() {
+        cancelReconnectJob(true);
+        cancelEventPolling();
+        final Connection connection = omniConnection;
+        if (connection != null) {
+            connection.removeDisconnectListener(this);
+            connection.disconnect();
+        }
+    }
+
+    private Optional<Thing> getChildThing(ThingTypeUID type, int number) {
+        Bridge bridge = getThing();
+        return bridge.getThings().stream().filter(t -> t.getThingTypeUID().equals(type))
+                .filter(t -> ((Number) t.getConfiguration().get(THING_PROPERTIES_NUMBER)).intValue() == number)
+                .findFirst();
+    }
+
+    private Optional<Thing> getUnitThing(int unitId) {
+        Optional<Thing> theThing = getChildThing(THING_TYPE_UNIT_UPB, unitId);
+        if (!(theThing.isPresent())) {
+            theThing = getChildThing(THING_TYPE_ROOM, unitId);
+        }
+        if (!(theThing.isPresent())) {
+            theThing = getChildThing(THING_TYPE_FLAG, unitId);
+        }
+        if (!(theThing.isPresent())) {
+            theThing = getChildThing(THING_TYPE_OUTPUT, unitId);
+        }
+        if (!(theThing.isPresent())) {
+            theThing = getChildThing(THING_TYPE_DIMMABLE, unitId);
+        }
+        if (!(theThing.isPresent())) {
+            theThing = getChildThing(THING_TYPE_UNIT, unitId);
+        }
+
+        return theThing;
+    }
+
+    public Optional<AudioPlayer> getAudioPlayer() {
+        return audioPlayer;
+    }
+
+    public Message readEventRecord(int eventNumber, int direction)
+            throws OmniInvalidResponseException, OmniUnknownMessageTypeException, BridgeOfflineException {
+        try {
+            return getOmniConnection().readEventRecord(eventNumber, direction);
+        } catch (OmniNotConnectedException | IOException e) {
+            setOfflineAndReconnect(e.getMessage());
+            throw new BridgeOfflineException(e);
+        }
+    }
+
+    private void updateBridgeProperties() {
+        try {
+            SystemInformation systemInformation = reqSystemInformation();
+            Map<String, String> properties = editProperties();
+            properties.put(THING_PROPERTIES_MODEL_NUMBER, Integer.toString(systemInformation.getModel()));
+            properties.put(THING_PROPERTIES_MAJOR_VERSION, Integer.toString(systemInformation.getMajor()));
+            properties.put(THING_PROPERTIES_MINOR_VERSION, Integer.toString(systemInformation.getMinor()));
+            properties.put(THING_PROPERTIES_REVISION, Integer.toString(systemInformation.getRevision()));
+            properties.put(THING_PROPERTIES_PHONE_NUMBER, systemInformation.getPhone());
+            updateProperties(properties);
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Could not request system information from OmniLink Controller: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public void initialize() {
+        scheduleReconnectJob();
+    }
+
+    private void scheduleReconnectJob() {
+        ScheduledFuture<?> currentReconnectJob = connectJob;
+        if (currentReconnectJob == null || currentReconnectJob.isDone()) {
+            connectJob = super.scheduler.scheduleWithFixedDelay(this::makeOmnilinkConnection, 0, autoReconnectPeriod,
+                    TimeUnit.SECONDS);
+        }
+    }
+
+    private void cancelReconnectJob(boolean kill) {
+        ScheduledFuture<?> currentReconnectJob = connectJob;
+        if (currentReconnectJob != null) {
+            currentReconnectJob.cancel(kill);
+        }
+    }
+
+    private void setOfflineAndReconnect(@Nullable String message) {
+        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
+        cancelEventPolling();
+        final Connection connection = omniConnection;
+        if (connection != null) {
+            connection.removeDisconnectListener(this);
+        }
+        scheduleReconnectJob();
+    }
+
+    private void startEventPolling(int interval) {
+        ScheduledFuture<?> eventPollingJobFuture = eventPollingJob;
+        if (eventPollingJobFuture == null || eventPollingJobFuture.isDone()) {
+            eventLogNumber = 0;
+            eventPollingJob = super.scheduler.scheduleWithFixedDelay(this::pollEvents, 0, interval, TimeUnit.SECONDS);
+        }
+    }
+
+    private void cancelEventPolling() {
+        ScheduledFuture<?> eventPollingJobFuture = eventPollingJob;
+        if (eventPollingJobFuture != null) {
+            eventPollingJobFuture.cancel(true);
+        }
+    }
+
+    private void pollEvents() {
+        // On first run, direction is -1 (most recent event), after its 1 for the next log message
+        try {
+            Message message;
+            do {
+                logger.trace("Polling for event log messages.");
+                int direction = eventLogNumber == 0 ? -1 : 1;
+                message = readEventRecord(eventLogNumber, direction);
+                if (message.getMessageType() == Message.MESG_TYPE_EVENT_LOG_DATA) {
+                    EventLogData logData = (EventLogData) message;
+                    logger.debug("Processing event log message number: {}", logData.getEventNumber());
+                    eventLogNumber = logData.getEventNumber();
+                    String json = gson.toJson(logData);
+                    logger.debug("Receieved event log message: {}", json);
+                    updateState(CHANNEL_EVENT_LOG, new StringType(json));
+                }
+            } while (message.getMessageType() != Message.MESG_TYPE_END_OF_DATA);
+
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Exception recieved while polling for event log messages: {}", e.getMessage());
+        }
+    }
+
+    private Connection getOmniConnection() throws OmniNotConnectedException {
+        final Connection connection = omniConnection;
+        if (connection != null) {
+            return connection;
+        } else {
+            throw new OmniNotConnectedException("Connection not yet established!");
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TempSensorHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TempSensorHandler.java
new file mode 100644 (file)
index 0000000..ddc4e12
--- /dev/null
@@ -0,0 +1,182 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AuxSensorProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedAuxSensorStatus;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * The {@link TempSensorHandler} defines some methods that are used to interface
+ * with an OmniLink Temperature Sensor. This by extension also defines the
+ * Temperature Sensor thing that openHAB will be able to pick up and interface
+ * with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class TempSensorHandler extends AbstractOmnilinkStatusHandler<ExtendedAuxSensorStatus> {
+    private final Logger logger = LoggerFactory.getLogger(TempSensorHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    public TempSensorHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        if (bridgeHandler != null) {
+            updateTempSensorProperties(bridgeHandler);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    "Received null bridge while initializing Temperature Sensor!");
+        }
+    }
+
+    private void updateTempSensorProperties(OmnilinkBridgeHandler bridgeHandler) {
+        final List<AreaProperties> areas = getAreaProperties();
+        if (areas != null) {
+            for (AreaProperties areaProperties : areas) {
+                int areaFilter = bitFilterForArea(areaProperties);
+
+                ObjectPropertyRequest<AuxSensorProperties> objectPropertyRequest = ObjectPropertyRequest
+                        .builder(bridgeHandler, ObjectPropertyRequests.AUX_SENSORS, thingID, 0).selectNamed()
+                        .areaFilter(areaFilter).build();
+
+                for (AuxSensorProperties auxSensorProperties : objectPropertyRequest) {
+                    Map<String, String> properties = editProperties();
+                    properties.put(THING_PROPERTIES_NAME, auxSensorProperties.getName());
+                    properties.put(THING_PROPERTIES_AREA, Integer.toString(areaProperties.getNumber()));
+                    updateProperties(properties);
+                }
+            }
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        Optional<TemperatureFormat> temperatureFormat = Optional.empty();
+
+        if (command instanceof RefreshType) {
+            retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE,
+                    ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!"));
+            return;
+        }
+
+        if (!(command instanceof QuantityType)) {
+            logger.debug("Invalid command: {}, must be QuantityType", command);
+            return;
+        }
+        if (bridgeHandler != null) {
+            temperatureFormat = bridgeHandler.getTemperatureFormat();
+            if (!temperatureFormat.isPresent()) {
+                logger.warn("Receieved null temperature format!");
+                return;
+            }
+        } else {
+            logger.warn("Could not connect to Bridge, failed to get temperature format!");
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_AUX_LOW_SETPOINT:
+                sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_HEAT_LOW_POINT.getNumber(),
+                        temperatureFormat.get().formatToOmni(((QuantityType<Temperature>) command).intValue()),
+                        thingID);
+                break;
+            case CHANNEL_AUX_HIGH_SETPOINT:
+                sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_COOL_HIGH_POINT.getNumber(),
+                        temperatureFormat.get().formatToOmni(((QuantityType<Temperature>) command).intValue()),
+                        thingID);
+                break;
+            default:
+                logger.warn("Unknown channel for Temperature Sensor thing: {}", channelUID);
+        }
+    }
+
+    @Override
+    public void updateChannels(ExtendedAuxSensorStatus status) {
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        if (bridgeHandler != null) {
+            Optional<TemperatureFormat> temperatureFormat = bridgeHandler.getTemperatureFormat();
+            if (temperatureFormat.isPresent()) {
+                updateState(CHANNEL_AUX_TEMP, new QuantityType<>(
+                        temperatureFormat.get().omniToFormat(status.getTemperature()),
+                        temperatureFormat.get().getFormatNumber() == 1 ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS));
+                updateState(CHANNEL_AUX_LOW_SETPOINT, new QuantityType<>(
+                        temperatureFormat.get().omniToFormat(status.getCoolSetpoint()),
+                        temperatureFormat.get().getFormatNumber() == 1 ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS));
+                updateState(CHANNEL_AUX_HIGH_SETPOINT, new QuantityType<>(
+                        temperatureFormat.get().omniToFormat(status.getHeatSetpoint()),
+                        temperatureFormat.get().getFormatNumber() == 1 ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS));
+            } else {
+                logger.warn("Receieved null temperature format, could not update Temperature Sensor channels!");
+            }
+        } else {
+            logger.debug("Received null bridge while updating Temperature Sensor channels!");
+        }
+    }
+
+    @Override
+    protected Optional<ExtendedAuxSensorStatus> retrieveStatus() {
+        try {
+            final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+            if (bridgeHandler != null) {
+                ObjectStatus objStatus = bridgeHandler.requestObjectStatus(Message.OBJ_TYPE_AUX_SENSOR, thingID,
+                        thingID, true);
+                return Optional.of((ExtendedAuxSensorStatus) objStatus.getStatuses()[0]);
+            } else {
+                logger.debug("Received null bridge while updating Temperature Sensor status!");
+                return Optional.empty();
+            }
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Received exception while refreshing Temperature Sensor status: {}", e.getMessage());
+            return Optional.empty();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TemperatureFormat.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/TemperatureFormat.java
new file mode 100644 (file)
index 0000000..8379e0d
--- /dev/null
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.digitaldan.jomnilinkII.MessageUtils;
+
+/**
+ * The {@link TemperatureFormat} defines some methods that are used to
+ * convert OmniLink temperature values into Fahrenheit or Celsius.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public enum TemperatureFormat {
+    // Don't convert zero - it appears that is what omni returns when there is no value.
+    CELSIUS(2) {
+        @Override
+        public float omniToFormat(int omniNumber) {
+            return MessageUtils.omniToC(omniNumber);
+        }
+
+        @Override
+        public int formatToOmni(int celsius) {
+            return MessageUtils.CToOmni(celsius);
+        }
+    },
+    FAHRENHEIT(1) {
+        @Override
+        public float omniToFormat(int omniNumber) {
+            return MessageUtils.omniToF(omniNumber);
+        }
+
+        @Override
+        public int formatToOmni(int fahrenheit) {
+            return MessageUtils.FtoOmni(fahrenheit);
+        }
+    };
+
+    private final int formatNumber;
+
+    private TemperatureFormat(int formatNumber) {
+        this.formatNumber = formatNumber;
+    }
+
+    /**
+     * Convert a number represented by the omni to the format.
+     *
+     * @param omniNumber Number to convert
+     * @return Number converted to appropriate format.
+     */
+    public abstract float omniToFormat(int omniNumber);
+
+    /**
+     * Convert a number from this format into an omni number.
+     *
+     * @param format Number in the current format.
+     * @return Omni formatted number.
+     */
+    public abstract int formatToOmni(int format);
+
+    /**
+     * Get the number which identifies this format as defined by the omniprotocol.
+     *
+     * @return Number which identifies this temperature format.
+     */
+    public int getFormatNumber() {
+        return formatNumber;
+    }
+
+    public static TemperatureFormat valueOf(int tempFormat) {
+        if (tempFormat == CELSIUS.formatNumber) {
+            return CELSIUS;
+        } else if (tempFormat == FAHRENHEIT.formatNumber) {
+            return FAHRENHEIT;
+        } else {
+            throw new IllegalArgumentException("Invalid temperature format!");
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ThermostatHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ThermostatHandler.java
new file mode 100644 (file)
index 0000000..ee61ee2
--- /dev/null
@@ -0,0 +1,265 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.measure.quantity.Dimensionless;
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.ThermostatProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedThermostatStatus;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * The {@link ThermostatHandler} defines some methods that are used to
+ * interface with an OmniLink Thermostat. This by extension also defines the
+ * Thermostat thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class ThermostatHandler extends AbstractOmnilinkStatusHandler<ExtendedThermostatStatus> {
+    private final Logger logger = LoggerFactory.getLogger(ThermostatHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    private enum ThermostatStatus {
+        HEATING(0, 1),
+        COOLING(1, 2),
+        HUMIDIFYING(2, 3),
+        DEHUMIDIFYING(3, 4);
+
+        private final int bit;
+        private final int modeValue;
+
+        private ThermostatStatus(int bit, int modeValue) {
+            this.bit = bit;
+            this.modeValue = modeValue;
+        }
+    }
+
+    public ThermostatHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        if (bridgeHandler != null) {
+            updateThermostatProperties(bridgeHandler);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    "Received null bridge while initializing Thermostat!");
+        }
+    }
+
+    private void updateThermostatProperties(OmnilinkBridgeHandler bridgeHandler) {
+        final List<AreaProperties> areas = getAreaProperties();
+        if (areas != null) {
+            for (AreaProperties areaProperties : areas) {
+                int areaFilter = bitFilterForArea(areaProperties);
+
+                ObjectPropertyRequest<ThermostatProperties> objectPropertyRequest = ObjectPropertyRequest
+                        .builder(bridgeHandler, ObjectPropertyRequests.THERMOSTAT, thingID, 0).selectNamed()
+                        .areaFilter(areaFilter).build();
+
+                for (ThermostatProperties thermostatProperties : objectPropertyRequest) {
+                    Map<String, String> properties = editProperties();
+                    properties.put(THING_PROPERTIES_NAME, thermostatProperties.getName());
+                    properties.put(THING_PROPERTIES_AREA, Integer.toString(areaProperties.getNumber()));
+                    updateProperties(properties);
+                }
+            }
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        Optional<TemperatureFormat> temperatureFormat = Optional.empty();
+
+        if (command instanceof RefreshType) {
+            retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE,
+                    ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!"));
+            return;
+        }
+
+        if (!(command instanceof DecimalType) && !(command instanceof QuantityType)) {
+            logger.debug("Invalid command: {}, must be DecimalType or QuantityType", command);
+            return;
+        }
+        if (bridgeHandler != null) {
+            temperatureFormat = bridgeHandler.getTemperatureFormat();
+            if (!temperatureFormat.isPresent()) {
+                logger.warn("Receieved null temperature format!");
+                return;
+            }
+        } else {
+            logger.warn("Could not connect to Bridge, failed to get temperature format!");
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_THERMO_SYSTEM_MODE:
+                sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_SYSTEM_MODE.getNumber(),
+                        ((DecimalType) command).intValue(), thingID);
+                break;
+            case CHANNEL_THERMO_FAN_MODE:
+                sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_FAN_MODE.getNumber(), ((DecimalType) command).intValue(),
+                        thingID);
+                break;
+            case CHANNEL_THERMO_HOLD_STATUS:
+                sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_HOLD_MODE.getNumber(),
+                        ((DecimalType) command).intValue(), thingID);
+                break;
+            case CHANNEL_THERMO_HEAT_SETPOINT:
+                sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_HEAT_LOW_POINT.getNumber(),
+                        temperatureFormat.get().formatToOmni(((QuantityType<Temperature>) command).intValue()),
+                        thingID);
+                break;
+            case CHANNEL_THERMO_COOL_SETPOINT:
+                sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_COOL_HIGH_POINT.getNumber(),
+                        temperatureFormat.get().formatToOmni(((QuantityType<Temperature>) command).intValue()),
+                        thingID);
+                break;
+            case CHANNEL_THERMO_HUMIDIFY_SETPOINT:
+                sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_HUMDIFY_POINT.getNumber(),
+                        TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType<Dimensionless>) command).intValue()),
+                        thingID);
+                break;
+            case CHANNEL_THERMO_DEHUMIDIFY_SETPOINT:
+                sendOmnilinkCommand(OmniLinkCmd.CMD_THERMO_SET_DEHUMIDIFY_POINT.getNumber(),
+                        TemperatureFormat.FAHRENHEIT.formatToOmni(((QuantityType<Dimensionless>) command).intValue()),
+                        thingID);
+                break;
+            default:
+                logger.warn("Unknown channel for Thermostat thing: {}", channelUID);
+        }
+    }
+
+    @Override
+    protected void updateChannels(ExtendedThermostatStatus status) {
+        logger.debug("updateChannels called for Thermostat status: {}", status);
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+
+        // Thermostat communication status
+        BigInteger thermostatAlarms = BigInteger.valueOf(status.getStatus());
+        updateState(CHANNEL_THERMO_COMM_FAILURE,
+                thermostatAlarms.testBit(0) ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
+        updateState(CHANNEL_THERMO_FREEZE_ALARM,
+                thermostatAlarms.testBit(1) ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
+
+        // Thermostat operation status
+        BigInteger thermostatStatus = BigInteger.valueOf(status.getExtendedStatus());
+        if (thermostatStatus.testBit(ThermostatStatus.HEATING.bit)) {
+            updateState(CHANNEL_THERMO_STATUS, new DecimalType(ThermostatStatus.HEATING.modeValue));
+        } else if (thermostatStatus.testBit(ThermostatStatus.COOLING.bit)) {
+            updateState(CHANNEL_THERMO_STATUS, new DecimalType(ThermostatStatus.COOLING.modeValue));
+        } else if (thermostatStatus.testBit(ThermostatStatus.HUMIDIFYING.bit)) {
+            updateState(CHANNEL_THERMO_STATUS, new DecimalType(ThermostatStatus.HUMIDIFYING.modeValue));
+        } else if (thermostatStatus.testBit(ThermostatStatus.DEHUMIDIFYING.bit)) {
+            updateState(CHANNEL_THERMO_STATUS, new DecimalType(ThermostatStatus.DEHUMIDIFYING.modeValue));
+        } else {
+            updateState(CHANNEL_THERMO_STATUS, new DecimalType(0));
+        }
+
+        // Thermostat temperature status
+        if (bridgeHandler != null) {
+            Optional<TemperatureFormat> temperatureFormat = bridgeHandler.getTemperatureFormat();
+            if (temperatureFormat.isPresent()) {
+                updateState(CHANNEL_THERMO_CURRENT_TEMP, new QuantityType<>(
+                        temperatureFormat.get().omniToFormat(status.getCurrentTemperature()),
+                        temperatureFormat.get().getFormatNumber() == 1 ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS));
+                updateState(CHANNEL_THERMO_OUTDOOR_TEMP, new QuantityType<>(
+                        temperatureFormat.get().omniToFormat(status.getOutdoorTemperature()),
+                        temperatureFormat.get().getFormatNumber() == 1 ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS));
+                updateState(CHANNEL_THERMO_COOL_SETPOINT, new QuantityType<>(
+                        temperatureFormat.get().omniToFormat(status.getCoolSetpoint()),
+                        temperatureFormat.get().getFormatNumber() == 1 ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS));
+                updateState(CHANNEL_THERMO_HEAT_SETPOINT, new QuantityType<>(
+                        temperatureFormat.get().omniToFormat(status.getHeatSetpoint()),
+                        temperatureFormat.get().getFormatNumber() == 1 ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS));
+            } else {
+                logger.warn("Receieved null temperature format, could not update Thermostat channels!");
+            }
+        } else {
+            logger.warn("Could not connect to Bridge, failed to get temperature format!");
+            return;
+        }
+
+        // Thermostat humidity status
+        updateState(CHANNEL_THERMO_HUMIDITY, new QuantityType<>(
+                TemperatureFormat.FAHRENHEIT.omniToFormat(status.getCurrentHumidity()), Units.PERCENT));
+        updateState(CHANNEL_THERMO_HUMIDIFY_SETPOINT, new QuantityType<>(
+                TemperatureFormat.FAHRENHEIT.omniToFormat(status.getHumidifySetpoint()), Units.PERCENT));
+        updateState(CHANNEL_THERMO_DEHUMIDIFY_SETPOINT, new QuantityType<>(
+                TemperatureFormat.FAHRENHEIT.omniToFormat(status.getDehumidifySetpoint()), Units.PERCENT));
+
+        // Thermostat mode, fan, and hold status
+        updateState(CHANNEL_THERMO_SYSTEM_MODE, new DecimalType(status.getSystemMode()));
+        updateState(CHANNEL_THERMO_FAN_MODE, new DecimalType(status.getFanMode()));
+        updateState(CHANNEL_THERMO_HOLD_STATUS,
+                new DecimalType(status.getHoldStatus() > 2 ? 1 : status.getHoldStatus()));
+    }
+
+    @Override
+    protected Optional<ExtendedThermostatStatus> retrieveStatus() {
+        try {
+            final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+            if (bridgeHandler != null) {
+                ObjectStatus objStatus = bridgeHandler.requestObjectStatus(Message.OBJ_TYPE_THERMO, thingID, thingID,
+                        true);
+                return Optional.of((ExtendedThermostatStatus) objStatus.getStatuses()[0]);
+            } else {
+                logger.debug("Received null bridge while updating Thermostat status!");
+                return Optional.empty();
+            }
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Received exception while refreshing Thermostat status: {}", e.getMessage());
+            return Optional.empty();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/UnitHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/UnitHandler.java
new file mode 100644 (file)
index 0000000..5370dae
--- /dev/null
@@ -0,0 +1,214 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.UnitProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedUnitStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.systemevents.SwitchPressEvent;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * The {@link AbstractOmnilinkHandler} defines some methods that can be used across
+ * the many different Units exposed by the OmniLink protocol
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class UnitHandler extends AbstractOmnilinkStatusHandler<ExtendedUnitStatus> {
+    private final Logger logger = LoggerFactory.getLogger(UnitHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    public UnitHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        if (bridgeHandler != null) {
+            updateUnitProperties(bridgeHandler);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    "Received null bridge while initializing Unit!");
+        }
+    }
+
+    private void updateUnitProperties(OmnilinkBridgeHandler bridgeHandler) {
+        final List<AreaProperties> areas = getAreaProperties();
+        if (areas != null) {
+            for (AreaProperties areaProperties : areas) {
+                int areaFilter = bitFilterForArea(areaProperties);
+
+                ObjectPropertyRequest<UnitProperties> objectPropertyRequest = ObjectPropertyRequest
+                        .builder(bridgeHandler, ObjectPropertyRequests.UNIT, thingID, 0).selectNamed()
+                        .areaFilter(areaFilter).selectAnyLoad().build();
+
+                for (UnitProperties unitProperties : objectPropertyRequest) {
+                    Map<String, String> properties = editProperties();
+                    properties.put(THING_PROPERTIES_NAME, unitProperties.getName());
+                    properties.put(THING_PROPERTIES_AREA, Integer.toString(areaProperties.getNumber()));
+                    updateProperties(properties);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+
+        if (command instanceof RefreshType) {
+            retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE,
+                    ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!"));
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_UNIT_LEVEL:
+            case CHANNEL_UNIT_SWITCH:
+                if (command instanceof OnOffType) {
+                    handleOnOff(channelUID, (OnOffType) command);
+                } else {
+                    logger.debug("Invalid command: {}, must be OnOffType", command);
+                }
+                break;
+            case CHANNEL_UNIT_ON_FOR_SECONDS:
+            case CHANNEL_UNIT_OFF_FOR_SECONDS:
+            case CHANNEL_UNIT_ON_FOR_MINUTES:
+            case CHANNEL_UNIT_OFF_FOR_MINUTES:
+            case CHANNEL_UNIT_ON_FOR_HOURS:
+            case CHANNEL_UNIT_OFF_FOR_HOURS:
+                if (command instanceof DecimalType) {
+                    handleUnitDuration(channelUID, (DecimalType) command);
+                } else {
+                    logger.debug("Invalid command: {}, must be DecimalType", command);
+                }
+                break;
+            default:
+                logger.warn("Unknown channel for Unit thing: {}", channelUID);
+        }
+    }
+
+    private void handleUnitDuration(ChannelUID channelUID, DecimalType command) {
+        logger.debug("handleUnitDuration called for channel: {}, command: {}", channelUID, command);
+        final String channelID = channelUID.getId();
+
+        int duration;
+        if (channelID.endsWith("seconds")) {
+            duration = command.intValue();
+        } else if (channelID.endsWith("minutes")) {
+            duration = command.intValue() + 100;
+        } else if (channelID.endsWith("hours")) {
+            duration = command.intValue() + 200;
+        } else {
+            logger.warn("Unknown channel for Unit duration: {}", channelUID);
+            return;
+        }
+
+        sendOmnilinkCommand(
+                channelID.startsWith("on") ? OmniLinkCmd.CMD_UNIT_ON.getNumber() : OmniLinkCmd.CMD_UNIT_OFF.getNumber(),
+                duration, thingID);
+    }
+
+    protected void handleOnOff(ChannelUID channelUID, OnOffType command) {
+        logger.debug("handleOnOff called for channel: {}, command: {}", channelUID, command);
+        sendOmnilinkCommand(OnOffType.ON.equals(command) ? OmniLinkCmd.CMD_UNIT_ON.getNumber()
+                : OmniLinkCmd.CMD_UNIT_OFF.getNumber(), 0, thingID);
+    }
+
+    @Override
+    public void updateChannels(ExtendedUnitStatus status) {
+        logger.debug("updateChannels called for Unit status: {}", status);
+        int unitStatus = status.getStatus();
+        int level = 0;
+
+        if (unitStatus == Status.UNIT_OFF) {
+            level = 0;
+        } else if (unitStatus == Status.UNIT_ON) {
+            level = 100;
+        } else if ((unitStatus >= Status.UNIT_SCENE_A) && (unitStatus <= Status.UNIT_SCENE_L)) {
+            level = 100;
+        } else if ((unitStatus >= Status.UNIT_LEVEL_0) && (unitStatus <= Status.UNIT_LEVEL_100)) {
+            level = unitStatus - Status.UNIT_LEVEL_0;
+        }
+
+        updateState(CHANNEL_UNIT_LEVEL, PercentType.valueOf(Integer.toString(level)));
+        updateState(CHANNEL_UNIT_SWITCH, OnOffType.from(level != 0));
+    }
+
+    @Override
+    protected Optional<ExtendedUnitStatus> retrieveStatus() {
+        try {
+            final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+            if (bridgeHandler != null) {
+                ObjectStatus objectStatus = bridgeHandler.requestObjectStatus(Message.OBJ_TYPE_UNIT, thingID, thingID,
+                        true);
+                return Optional.of((ExtendedUnitStatus) objectStatus.getStatuses()[0]);
+            } else {
+                logger.debug("Received null bridge while updating Unit status!");
+                return Optional.empty();
+            }
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Received exception while refreshing Unit status: {}", e.getMessage());
+            return Optional.empty();
+        }
+    }
+
+    private static class Status {
+        private static final int UNIT_OFF = 0;
+        private static final int UNIT_ON = 1;
+        private static final int UNIT_SCENE_A = 2;
+        private static final int UNIT_SCENE_L = 13;
+        private static final int UNIT_LEVEL_0 = 100;
+        private static final int UNIT_LEVEL_100 = 200;
+    }
+
+    /**
+     * Handle a switch press event by triggering the appropriate channel.
+     *
+     * @param switchPressEvent
+     */
+    public void handleSwitchPressEvent(SwitchPressEvent switchPressEvent) {
+        ChannelUID activateChannel = new ChannelUID(getThing().getUID(), TRIGGER_CHANNEL_SWITCH_PRESS_EVENT);
+        triggerChannel(activateChannel, Integer.toString(switchPressEvent.getSwitchValue()));
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ZoneHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/ZoneHandler.java
new file mode 100644 (file)
index 0000000..572de0b
--- /dev/null
@@ -0,0 +1,201 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler;
+
+import static com.digitaldan.jomnilinkII.MessageTypes.properties.AuxSensorProperties.SENSOR_TYPE_PROGRAMMABLE_ENERGY_SAVER_MODULE;
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequest;
+import org.openhab.binding.omnilink.internal.discovery.ObjectPropertyRequests;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.Message;
+import com.digitaldan.jomnilinkII.MessageTypes.ObjectStatus;
+import com.digitaldan.jomnilinkII.MessageTypes.SecurityCodeValidation;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.AreaProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.properties.ZoneProperties;
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedZoneStatus;
+import com.digitaldan.jomnilinkII.OmniInvalidResponseException;
+import com.digitaldan.jomnilinkII.OmniUnknownMessageTypeException;
+
+/**
+ * The {@link ZoneHandler} defines some methods that are used to
+ * interface with an OmniLink Zone. This by extension also defines the
+ * OmniPro Zone thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ */
+@NonNullByDefault
+public class ZoneHandler extends AbstractOmnilinkStatusHandler<ExtendedZoneStatus> {
+    private final Logger logger = LoggerFactory.getLogger(ZoneHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    public ZoneHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        super.initialize();
+        final OmnilinkBridgeHandler bridgeHandler = getOmnilinkBridgeHandler();
+        if (bridgeHandler != null) {
+            updateZoneProperties(bridgeHandler);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    "Received null bridge while initializing Zone!");
+        }
+    }
+
+    private void updateZoneProperties(OmnilinkBridgeHandler bridgeHandler) {
+        final List<AreaProperties> areas = super.getAreaProperties();
+        if (areas != null) {
+            for (AreaProperties areaProperties : areas) {
+                int areaFilter = super.bitFilterForArea(areaProperties);
+
+                ObjectPropertyRequest<ZoneProperties> objectPropertyRequest = ObjectPropertyRequest
+                        .builder(bridgeHandler, ObjectPropertyRequests.ZONE, getThingNumber(), 0).selectNamed()
+                        .areaFilter(areaFilter).build();
+
+                for (ZoneProperties zoneProperties : objectPropertyRequest) {
+                    if (zoneProperties.getZoneType() <= SENSOR_TYPE_PROGRAMMABLE_ENERGY_SAVER_MODULE) {
+                        Map<String, String> properties = editProperties();
+                        properties.put(THING_PROPERTIES_NAME, zoneProperties.getName());
+                        properties.put(THING_PROPERTIES_AREA, Integer.toString(areaProperties.getNumber()));
+                        updateProperties(properties);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+        int mode;
+
+        if (command instanceof RefreshType) {
+            retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE,
+                    ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!"));
+            return;
+        }
+
+        if (!(command instanceof StringType)) {
+            logger.debug("Invalid command: {}, must be StringType", command);
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_ZONE_BYPASS:
+                mode = OmniLinkCmd.CMD_SECURITY_BYPASS_ZONE.getNumber();
+                break;
+            case CHANNEL_ZONE_RESTORE:
+                mode = OmniLinkCmd.CMD_SECURITY_RESTORE_ZONE.getNumber();
+                break;
+            default:
+                mode = -1;
+        }
+        int areaNumber = getAreaNumber();
+        logger.debug("mode {} on zone {} with code {}", mode, thingID, command.toFullString());
+        char[] code = command.toFullString().toCharArray();
+        if (code.length != 4) {
+            logger.warn("Invalid code length, code must be 4 digits");
+        } else {
+            try {
+                final OmnilinkBridgeHandler bridge = getOmnilinkBridgeHandler();
+                if (bridge != null) {
+                    SecurityCodeValidation codeValidation = bridge.reqSecurityCodeValidation(areaNumber,
+                            Character.getNumericValue(code[0]), Character.getNumericValue(code[1]),
+                            Character.getNumericValue(code[2]), Character.getNumericValue(code[3]));
+                    /*
+                     * 0 Invalid code
+                     * 1 Master
+                     * 2 Manager
+                     * 3 User
+                     */
+                    logger.debug("User code number: {} level: {}", codeValidation.getCodeNumber(),
+                            codeValidation.getAuthorityLevel());
+                    /*
+                     * Valid user code number are 1-99, 251 is duress code, 0 means code does not exist
+                     */
+                    if ((codeValidation.getCodeNumber() > 0 && codeValidation.getCodeNumber() <= 99)
+                            && codeValidation.getAuthorityLevel() > 0) {
+                        sendOmnilinkCommand(mode, codeValidation.getCodeNumber(), thingID);
+                    } else {
+                        logger.warn("System reported an invalid code");
+                    }
+                } else {
+                    logger.debug("Received null bridge while sending zone command!");
+                }
+            } catch (OmniInvalidResponseException e) {
+                logger.debug("Zone command failed: {}", e.getMessage());
+            } catch (OmniUnknownMessageTypeException | BridgeOfflineException e) {
+                logger.debug("Could not send zone command: {}", e.getMessage());
+            }
+        }
+        // This is a send only channel, so don't store the user code
+        updateState(channelUID, UnDefType.UNDEF);
+    }
+
+    @Override
+    protected void updateChannels(ExtendedZoneStatus zoneStatus) {
+        // 0 Secure. 1 Not ready, 3 Trouble
+        int current = ((zoneStatus.getStatus() >> 0) & 0x03);
+        // 0 Secure, 1 Tripped, 2 Reset, but previously tripped
+        int latched = ((zoneStatus.getStatus() >> 2) & 0x03);
+        // 0 Disarmed, 1 Armed, 2 Bypass user, 3 Bypass system
+        int arming = ((zoneStatus.getStatus() >> 4) & 0x03);
+        State contactState = Integer.valueOf(current).equals(0) ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
+        logger.debug("handling Zone Status change to state: {}, current: {}, latched: {}, arming: {}", contactState,
+                current, latched, arming);
+        updateState(CHANNEL_ZONE_CONTACT, contactState);
+        updateState(CHANNEL_ZONE_CURRENT_CONDITION, new DecimalType(current));
+        updateState(CHANNEL_ZONE_LATCHED_ALARM_STATUS, new DecimalType(latched));
+        updateState(CHANNEL_ZONE_ARMING_STATUS, new DecimalType(arming));
+    }
+
+    @Override
+    protected Optional<ExtendedZoneStatus> retrieveStatus() {
+        try {
+            final OmnilinkBridgeHandler bridge = getOmnilinkBridgeHandler();
+            if (bridge != null) {
+                ObjectStatus objStatus = bridge.requestObjectStatus(Message.OBJ_TYPE_ZONE, thingID, thingID, true);
+                return Optional.of((ExtendedZoneStatus) objStatus.getStatuses()[0]);
+            } else {
+                logger.debug("Received null bridge while updating Zone status!");
+                return Optional.empty();
+            }
+        } catch (OmniInvalidResponseException | OmniUnknownMessageTypeException | BridgeOfflineException e) {
+            logger.debug("Received exception while refreshing Zone status: {}", e.getMessage());
+            return Optional.empty();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/DimmableUnitHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/DimmableUnitHandler.java
new file mode 100644 (file)
index 0000000..632197f
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler.units;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.CHANNEL_UNIT_LEVEL;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.handler.OmniLinkCmd;
+import org.openhab.binding.omnilink.internal.handler.UnitHandler;
+import org.openhab.core.library.types.IncreaseDecreaseType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link DimmableUnitHandler} defines some methods that are used to
+ * interface with an OmniLink Dimmable Unit. This by extension also defines the
+ * Dimmable Unit things that openHAB will be able to pick up and interface with.
+ *
+ * @author Brian O'Connell - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class DimmableUnitHandler extends UnitHandler {
+    private final Logger logger = LoggerFactory.getLogger(DimmableUnitHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    public DimmableUnitHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+        switch (channelUID.getId()) {
+            case CHANNEL_UNIT_LEVEL:
+                handleUnitLevel(channelUID, command);
+                break;
+            default:
+                logger.debug("Unknown channel for Dimmable Unit thing: {}", channelUID);
+                super.handleCommand(channelUID, command);
+        }
+    }
+
+    private void handleUnitLevel(ChannelUID channelUID, Command command) {
+        logger.debug("handleUnitLevel called for channel: {}, command: {}", channelUID, command);
+        if (command instanceof PercentType) {
+            handlePercent(channelUID, (PercentType) command);
+        } else if (command instanceof IncreaseDecreaseType) {
+            handleIncreaseDecrease(channelUID, (IncreaseDecreaseType) command);
+        } else {
+            // Only handle percent or increase/decrease.
+            super.handleCommand(channelUID, command);
+        }
+    }
+
+    private void handlePercent(ChannelUID channelUID, PercentType command) {
+        logger.debug("handlePercent called for channel: {}, command: {}", channelUID, command);
+        int lightLevel = command.intValue();
+
+        if (lightLevel == 0) {
+            super.handleOnOff(channelUID, OnOffType.OFF);
+        } else if (lightLevel == 100) {
+            super.handleOnOff(channelUID, OnOffType.ON);
+        } else {
+            sendOmnilinkCommand(OmniLinkCmd.CMD_UNIT_PERCENT.getNumber(), lightLevel, thingID);
+        }
+    }
+
+    private void handleIncreaseDecrease(ChannelUID channelUID, IncreaseDecreaseType command) {
+        logger.debug("handleIncreaseDecrease called for channel: {}, command: {}", channelUID, command);
+        sendOmnilinkCommand(
+                IncreaseDecreaseType.INCREASE.equals(command) ? OmniLinkCmd.CMD_UNIT_UNIT_BRIGHTEN_STEP_1.getNumber()
+                        : OmniLinkCmd.CMD_UNIT_UNIT_DIM_STEP_1.getNumber(),
+                0, thingID);
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/FlagHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/FlagHandler.java
new file mode 100644 (file)
index 0000000..56874e7
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler.units;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.handler.OmniLinkCmd;
+import org.openhab.binding.omnilink.internal.handler.UnitHandler;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedUnitStatus;
+
+/**
+ * The {@link FlagHandler} defines some methods that are used to
+ * interface with an OmniLink Flag. This by extension also defines the
+ * Flag thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class FlagHandler extends UnitHandler {
+    private final Logger logger = LoggerFactory.getLogger(FlagHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    public FlagHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+
+        if (command instanceof RefreshType) {
+            retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE,
+                    ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!"));
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_FLAG_VALUE:
+                if (command instanceof DecimalType) {
+                    sendOmnilinkCommand(OmniLinkCmd.CMD_UNIT_SET_COUNTER.getNumber(),
+                            ((DecimalType) command).intValue(), thingID);
+                } else {
+                    logger.debug("Invalid command: {}, must be DecimalType", command);
+                }
+                break;
+            case CHANNEL_FLAG_SWITCH:
+                if (command instanceof OnOffType) {
+                    handleOnOff(channelUID, (OnOffType) command);
+                } else {
+                    logger.debug("Invalid command: {}, must be OnOffType", command);
+                }
+                break;
+            default:
+                logger.debug("Unknown channel for Flag thing: {}", channelUID);
+                super.handleCommand(channelUID, command);
+        }
+    }
+
+    @Override
+    public void updateChannels(ExtendedUnitStatus status) {
+        logger.debug("updateChannels called for Flag status: {}", status);
+        updateState(CHANNEL_FLAG_VALUE, DecimalType.valueOf(Integer.toString(status.getStatus())));
+        updateState(CHANNEL_FLAG_SWITCH, OnOffType.from(status.getStatus() == 1));
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/OutputHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/OutputHandler.java
new file mode 100644 (file)
index 0000000..5bfb194
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler.units;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.handler.UnitHandler;
+import org.openhab.core.thing.Thing;
+
+/**
+ * The {@link OutputHandler} defines some methods that are used to
+ * interface with an OmniLink Output. This by extension also defines the
+ * Output thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Brian O'Connell - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class OutputHandler extends UnitHandler {
+    public @Nullable String number;
+
+    public OutputHandler(Thing thing) {
+        super(thing);
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/UpbRoomHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/UpbRoomHandler.java
new file mode 100644 (file)
index 0000000..f59344f
--- /dev/null
@@ -0,0 +1,168 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler.units;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.handler.OmniLinkCmd;
+import org.openhab.binding.omnilink.internal.handler.UnitHandler;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.digitaldan.jomnilinkII.MessageTypes.statuses.ExtendedUnitStatus;
+
+/**
+ * The {@link UpbRoomHandler} defines some methods that are used to
+ * interface with an OmniLink UPB Room. This by extension also defines the
+ * OmniPro UPB Room thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class UpbRoomHandler extends UnitHandler {
+    private final Logger logger = LoggerFactory.getLogger(UpbRoomHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    public UpbRoomHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+
+        if (command instanceof RefreshType) {
+            retrieveStatus().ifPresentOrElse(this::updateChannels, () -> updateStatus(ThingStatus.OFFLINE,
+                    ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Received null staus update!"));
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_ROOM_SCENE_A:
+            case CHANNEL_ROOM_SCENE_B:
+            case CHANNEL_ROOM_SCENE_C:
+            case CHANNEL_ROOM_SCENE_D:
+                if (command instanceof OnOffType) {
+                    handleRoomScene(channelUID, (OnOffType) command);
+                } else {
+                    logger.debug("Invalid command: {}, must be OnOffType", command);
+                }
+                break;
+            case CHANNEL_ROOM_SWITCH:
+                if (command instanceof OnOffType) {
+                    super.handleOnOff(channelUID, (OnOffType) command);
+                } else {
+                    logger.debug("Invalid command: {}, must be OnOffType", command);
+                }
+                break;
+            case CHANNEL_ROOM_STATE:
+                if (command instanceof DecimalType) {
+                    handleRoomState(channelUID, (DecimalType) command);
+                } else {
+                    logger.debug("Invalid command: {}, must be DecimalType", command);
+                }
+                break;
+            default:
+                logger.debug("Unknown channel for UPB Room thing: {}", channelUID);
+                super.handleCommand(channelUID, command);
+        }
+    }
+
+    private void handleRoomScene(ChannelUID channelUID, OnOffType command) {
+        logger.debug("handleRoomScene called for channel: {}, command: {}", channelUID, command);
+        int linkNum;
+
+        switch (channelUID.getId()) {
+            case "scene_a":
+                linkNum = 0;
+                break;
+            case "scene_b":
+                linkNum = 1;
+                break;
+            case "scene_c":
+                linkNum = 2;
+                break;
+            case "scene_d":
+                linkNum = 3;
+                break;
+            default:
+                logger.warn("Unexpected UPB Room scene: {}", channelUID);
+                return;
+        }
+        int roomNum = (thingID + 7) / 8;
+        int param2 = ((roomNum * 6) - 3) + linkNum;
+        sendOmnilinkCommand(OnOffType.ON.equals(command) ? OmniLinkCmd.CMD_UNIT_UPB_LINK_ON.getNumber()
+                : OmniLinkCmd.CMD_UNIT_UPB_LINK_OFF.getNumber(), 0, param2);
+    }
+
+    private void handleRoomState(ChannelUID channelUID, DecimalType command) {
+        logger.debug("handleRoomState called for channel: {}, command: {}", channelUID, command);
+        final int cmdValue = command.intValue();
+        int cmd;
+        int param2;
+
+        switch (cmdValue) {
+            case 0:
+                cmd = OmniLinkCmd.CMD_UNIT_OFF.getNumber();
+                param2 = thingID;
+                break;
+            case 1:
+                cmd = OmniLinkCmd.CMD_UNIT_ON.getNumber();
+                param2 = thingID;
+                break;
+            case 2:
+            case 3:
+            case 4:
+            case 5:
+                cmd = OmniLinkCmd.CMD_UNIT_UPB_LINK_ON.getNumber();
+                /*
+                 * A little magic with the link #'s: 0 and 1 are off and on, respectively.
+                 * So A ends up being 2, but OmniLink Protocol expects an offset of 0. That
+                 * is why we subtract the 2.
+                 */
+                int roomNum = (thingID + 7) / 8;
+                param2 = ((roomNum * 6) - 3) + cmdValue - 2;
+                break;
+            default:
+                logger.warn("Unexpected UPB Room state: {}", Integer.toString(cmdValue));
+                return;
+        }
+
+        sendOmnilinkCommand(cmd, 0, param2);
+    }
+
+    @Override
+    public void updateChannels(ExtendedUnitStatus status) {
+        logger.debug("updateChannels called for UPB Room status: {}", status);
+        int unitStatus = status.getStatus();
+
+        updateState(CHANNEL_ROOM_STATE, new DecimalType(unitStatus));
+        updateState(CHANNEL_ROOM_SWITCH, OnOffType.from(unitStatus == 1));
+        updateState(CHANNEL_ROOM_SCENE_A, OnOffType.from(unitStatus == 2));
+        updateState(CHANNEL_ROOM_SCENE_B, OnOffType.from(unitStatus == 3));
+        updateState(CHANNEL_ROOM_SCENE_C, OnOffType.from(unitStatus == 4));
+        updateState(CHANNEL_ROOM_SCENE_D, OnOffType.from(unitStatus == 5));
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/dimmable/UpbUnitHandler.java b/bundles/org.openhab.binding.omnilink/src/main/java/org/openhab/binding/omnilink/internal/handler/units/dimmable/UpbUnitHandler.java
new file mode 100644 (file)
index 0000000..2fda1ce
--- /dev/null
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2010-2021 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.omnilink.internal.handler.units.dimmable;
+
+import static org.openhab.binding.omnilink.internal.OmnilinkBindingConstants.CHANNEL_UPB_STATUS;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.omnilink.internal.handler.OmniLinkCmd;
+import org.openhab.binding.omnilink.internal.handler.units.DimmableUnitHandler;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link UpbUnitHandler} defines some methods that are used to
+ * interface with an OmniLink UPB Unit. This by extension also defines the
+ * UPB Unit thing that openHAB will be able to pick up and interface with.
+ *
+ * @author Craig Hamilton - Initial contribution
+ * @author Ethan Dye - openHAB3 rewrite
+ */
+@NonNullByDefault
+public class UpbUnitHandler extends DimmableUnitHandler {
+    private final Logger logger = LoggerFactory.getLogger(UpbUnitHandler.class);
+    private final int thingID = getThingNumber();
+    public @Nullable String number;
+
+    public UpbUnitHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand called for channel: {}, command: {}", channelUID, command);
+
+        if (command instanceof RefreshType) {
+            updateState(CHANNEL_UPB_STATUS, UnDefType.UNDEF);
+            return;
+        }
+
+        switch (channelUID.getId()) {
+            case CHANNEL_UPB_STATUS:
+                if (command instanceof StringType) {
+                    sendOmnilinkCommand(OmniLinkCmd.CMD_UNIT_UPB_REQ_STATUS.getNumber(), 0, thingID);
+                    updateState(CHANNEL_UPB_STATUS, UnDefType.UNDEF);
+                } else {
+                    logger.debug("Invalid command: {}, must be StringType", command);
+                }
+                break;
+            default:
+                logger.debug("Unknown channel for UPB Unit thing: {}", channelUID);
+                super.handleCommand(channelUID, command);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..929d6ad
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="omnilink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>OmniLink Binding</name>
+       <description>This is the binding for OmniLink, a security system that interfaces with many devices.</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/area.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/area.xml
new file mode 100644 (file)
index 0000000..10475f6
--- /dev/null
@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- Omni Area Thing -->
+       <thing-type id="area">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Omni Area</label>
+               <description>An Omni area configured in the controller.</description>
+               <channels>
+                       <channel id="activate_keypad_emergency" typeId="omni_activate_keypad_emergency"/>
+                       <channel id="alarm_burglary" typeId="area_alarm"/>
+                       <channel id="alarm_fire" typeId="area_alarm"/>
+                       <channel id="alarm_gas" typeId="area_alarm"/>
+                       <channel id="alarm_auxiliary" typeId="area_alarm"/>
+                       <channel id="alarm_freeze" typeId="area_alarm"/>
+                       <channel id="alarm_water" typeId="area_alarm"/>
+                       <channel id="alarm_duress" typeId="area_alarm"/>
+                       <channel id="alarm_temperature" typeId="area_alarm"/>
+                       <channel id="mode" typeId="omni_area_mode"/>
+                       <channel id="disarm" typeId="area_command"/>
+                       <channel id="day" typeId="area_command"/>
+                       <channel id="night" typeId="area_command"/>
+                       <channel id="away" typeId="area_command"/>
+                       <channel id="vacation" typeId="area_command"/>
+                       <channel id="day_instant" typeId="area_command"/>
+                       <channel id="night_delayed" typeId="area_command"/>
+                       <channel id="all_on_off_event" typeId="all_on_off_event"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Area Number</label>
+                               <description>The area number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Lumina Area Thing -->
+       <thing-type id="lumina_area">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Lumina Area</label>
+               <description>An Lumina area configured in the controller.</description>
+               <channels>
+                       <channel id="mode" typeId="lumina_area_mode"/>
+                       <channel id="home" typeId="area_command"/>
+                       <channel id="sleep" typeId="area_command"/>
+                       <channel id="away" typeId="area_command"/>
+                       <channel id="vacation" typeId="area_command"/>
+                       <channel id="party" typeId="area_command"/>
+                       <channel id="special" typeId="area_command"/>
+                       <channel id="all_on_off_event" typeId="all_on_off_event"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+               </properties>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Area Number</label>
+                               <description>The area number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Area channels -->
+       <channel-type id="area_alarm">
+               <item-type>Switch</item-type>
+               <label>Area Alarm</label>
+               <description>Indicates if an alarm is active.</description>
+               <category>Alarm</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="omni_area_mode">
+               <item-type>Number</item-type>
+               <label>Security Mode</label>
+               <description>Represents the area security mode.</description>
+               <category>Alarm</category>
+               <state readOnly="true">
+                       <options>
+                               <option value="0">Off</option>
+                               <option value="1">Day</option>
+                               <option value="2">Night</option>
+                               <option value="3">Away</option>
+                               <option value="4">Vacation</option>
+                               <option value="5">Day instant</option>
+                               <option value="6">Night delayed</option>
+                               <option value="9">Arming day</option>
+                               <option value="10">Arming night</option>
+                               <option value="11">Arming away</option>
+                               <option value="12">Arming vacation</option>
+                               <option value="13">Arming day instant</option>
+                               <option value="14">Arming night delayed</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="area_command">
+               <item-type>String</item-type>
+               <label>Security Command</label>
+               <description>Sends a 4 digit user code to activate the area command.</description>
+               <category>Alarm</category>
+       </channel-type>
+
+       <channel-type id="omni_activate_keypad_emergency">
+               <item-type>Number</item-type>
+               <label>Activate Keypad Emergency</label>
+               <description>Activate a burglary, fire, or auxiliary keypad emergency alarm on Omni based models.</description>
+               <category>Alarm</category>
+               <state>
+                       <options>
+                               <option value="1">Burglary</option>
+                               <option value="2">Fire</option>
+                               <option value="3">Auxiliary</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="lumina_area_mode">
+               <item-type>Number</item-type>
+               <label>Security Mode</label>
+               <description>Represents the area security mode.</description>
+               <category>Alarm</category>
+               <state readOnly="true">
+                       <options>
+                               <option value="1">Home</option>
+                               <option value="2">Sleep</option>
+                               <option value="3">Away</option>
+                               <option value="4">Vacation</option>
+                               <option value="5">Party</option>
+                               <option value="6">Special</option>
+                               <option value="9">Setting home</option>
+                               <option value="10">Setting sleep</option>
+                               <option value="11">Setting away</option>
+                               <option value="12">Setting vacation</option>
+                               <option value="13">Setting party</option>
+                               <option value="14">Setting special</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="all_on_off_event">
+               <kind>trigger</kind>
+               <label>All On/Off Event</label>
+               <description>Event sent when an all on/off event occurs.</description>
+               <event>
+                       <options>
+                               <option value="OFF">Off</option>
+                               <option value="ON">On</option>
+                       </options>
+               </event>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/audio-source.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/audio-source.xml
new file mode 100644 (file)
index 0000000..314ff9c
--- /dev/null
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- Audio Source Thing -->
+       <thing-type id="audio_source">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Audio Source</label>
+               <description>An audio source configured in the controller.</description>
+               <channels>
+                       <channel id="source_text_1" typeId="audio_source_text"/>
+                       <channel id="source_text_2" typeId="audio_source_text"/>
+                       <channel id="source_text_3" typeId="audio_source_text"/>
+                       <channel id="source_text_4" typeId="audio_source_text"/>
+                       <channel id="source_text_5" typeId="audio_source_text"/>
+                       <channel id="source_text_6" typeId="audio_source_text"/>
+                       <channel id="polling" typeId="audio_source_polling"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Audio Source Number</label>
+                               <description>The audio source number.</description>
+                       </parameter>
+                       <parameter name="autostart" type="boolean" required="false">
+                               <label>Autostart Polling</label>
+                               <description>Autostart polling of audio source on creation of thing.</description>
+                               <default>true</default>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Audio Source Channels -->
+       <channel-type id="audio_source_text">
+               <item-type>String</item-type>
+               <label>Source Data</label>
+               <description>A line of metadata from this audio source.</description>
+               <category>Text</category>
+       </channel-type>
+
+       <channel-type id="audio_source_polling">
+               <item-type>Switch</item-type>
+               <label>Audio Source Polling</label>
+               <description>Enable or disable polling of this audio source.</description>
+               <category>Switch</category>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/audio-zone.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/audio-zone.xml
new file mode 100644 (file)
index 0000000..27cfce3
--- /dev/null
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- Audio Zone Thing -->
+       <thing-type id="audio_zone">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Audio Zone</label>
+               <description>An audio zone configured in the controller.</description>
+               <channels>
+                       <channel id="zone_power" typeId="audio_zone_power"/>
+                       <channel id="zone_mute" typeId="audio_zone_mute"/>
+                       <channel id="zone_volume" typeId="audio_zone_volume"/>
+                       <channel id="zone_source" typeId="audio_zone_source"/>
+                       <channel id="zone_control" typeId="audio_zone_control"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Audio Zone Number</label>
+                               <description>The audio zone number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+
+       <!-- Audio Zone Channels -->
+       <channel-type id="audio_zone_power">
+               <item-type>Switch</item-type>
+               <label>Audio Zone Power</label>
+               <description>Power status of this audio zone.</description>
+               <category>Switch</category>
+       </channel-type>
+
+       <channel-type id="audio_zone_mute">
+               <item-type>Switch</item-type>
+               <label>Audio Zone Mute</label>
+               <description>Mute status of this audio zone.</description>
+               <category>Switch</category>
+       </channel-type>
+
+       <channel-type id="audio_zone_volume">
+               <item-type>Dimmer</item-type>
+               <label>Audio Zone Volume</label>
+               <description>Volume level of this audio zone.</description>
+               <category>Slider</category>
+               <state min="0" max="100"/>
+       </channel-type>
+
+       <channel-type id="audio_zone_source">
+               <item-type>Number</item-type>
+               <label>Source</label>
+               <description>Source for this audio zone.</description>
+               <category>MediaControl</category>
+               <state min="1" max="100"/>
+       </channel-type>
+
+       <channel-type id="audio_zone_control">
+               <item-type>Player</item-type>
+               <label>Control</label>
+               <description>Control the audio zone, e.g. start/stop/next/previous.</description>
+               <category>MediaControl</category>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/bridge.xml
new file mode 100644 (file)
index 0000000..fef71b5
--- /dev/null
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- OmniLink Controller Bridge -->
+       <bridge-type id="controller">
+               <label>OmniLink Controller</label>
+               <description>An OmniLink controller.</description>
+               <channels>
+                       <channel id="sysdate" typeId="sysDate"/>
+                       <channel id="enable_disable_beeper" typeId="console_enable_disable_beeper">
+                               <label>Console Beepers</label>
+                       </channel>
+                       <channel id="beep" typeId="console_beep">
+                               <label>Beep Consoles</label>
+                       </channel>
+                       <channel id="last_log" typeId="last_log"/>
+                       <channel id="phone_line_event" typeId="phone_line_event"/>
+                       <channel id="ac_power_event" typeId="ac_power_event"/>
+                       <channel id="battery_event" typeId="battery_event"/>
+                       <channel id="dcm_event" typeId="dcm_event"/>
+                       <channel id="energy_cost_event" typeId="energy_cost_event"/>
+                       <channel id="camera_trigger_event" typeId="camera_trigger_event"/>
+                       <channel id="upb_link_activated_event" typeId="upb_link_activated_event"/>
+                       <channel id="upb_link_deactivated_event" typeId="upb_link_deactivated_event"/>
+               </channels>
+               <properties>
+                       <property name="model number"/>
+                       <property name="major version"/>
+                       <property name="minor version"/>
+                       <property name="revision"/>
+                       <property name="phone number"/>
+               </properties>
+               <config-description>
+                       <parameter name="ipAddress" type="text" required="true">
+                               <context>network-address</context>
+                               <label>IP or Host Name</label>
+                               <description>The IP or host name of the controller.</description>
+                       </parameter>
+                       <parameter name="port" type="integer" required="true">
+                               <label>Port</label>
+                               <description>The port of the controller.</description>
+                               <default>4369</default>
+                       </parameter>
+                       <parameter name="key1" type="text" required="true">
+                               <label>Key 1</label>
+                               <description>The first network encription key.</description>
+                       </parameter>
+                       <parameter name="key2" type="text" required="true">
+                               <label>Key 2</label>
+                               <description>The second network encription key.</description>
+                       </parameter>
+                       <parameter name="logPollingInterval" type="integer" required="true">
+                               <label>Log Polling Interval</label>
+                               <description>The interval to poll for new log messages on the controller.</description>
+                               <default>1</default>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+
+       <!-- Controller Channels -->
+       <channel-type id="sysDate">
+               <item-type>DateTime</item-type>
+               <label>Date/Time</label>
+               <description>Set controller date/time.</description>
+               <category>Time</category>
+               <state pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"/>
+       </channel-type>
+
+       <channel-type id="last_log">
+               <item-type>String</item-type>
+               <label>Last Log Entry</label>
+               <description>Last log message on the controller, represented in JSON.</description>
+               <category>Text</category>
+       </channel-type>
+
+       <channel-type id="upb_link_activated_event">
+               <kind>trigger</kind>
+               <label>UPB Link</label>
+               <description>Event sent when a UPB link is activated.</description>
+       </channel-type>
+
+       <channel-type id="upb_link_deactivated_event">
+               <kind>trigger</kind>
+               <label>UPB Link</label>
+               <description>Event sent when a UPB link is deactivated.</description>
+       </channel-type>
+
+       <channel-type id="phone_line_event">
+               <kind>trigger</kind>
+               <label>Phone Line Event</label>
+               <description>Event sent when the phone line changes state.</description>
+               <event>
+                       <options>
+                               <option value="ON_HOOK">On Hook</option>
+                               <option value="OFF_HOOK">Off Hook</option>
+                               <option value="DEAD">Dead</option>
+                               <option value="RING">Ring</option>
+                       </options>
+               </event>
+       </channel-type>
+
+       <channel-type id="ac_power_event">
+               <kind>trigger</kind>
+               <label>AC Power Event</label>
+               <description>Event sent when AC trouble conditions are detected.</description>
+               <event>
+                       <options>
+                               <option value="OFF">Off</option>
+                               <option value="RESTORED">Restored</option>
+                       </options>
+               </event>
+       </channel-type>
+
+       <channel-type id="battery_event">
+               <kind>trigger</kind>
+               <label>Battery Event</label>
+               <description>Event sent when battery trouble conditions are detected.</description>
+               <event>
+                       <options>
+                               <option value="LOW">Low</option>
+                               <option value="OK">OK</option>
+                       </options>
+               </event>
+       </channel-type>
+
+       <channel-type id="dcm_event">
+               <kind>trigger</kind>
+               <label>DCM Event</label>
+               <description>Event sent when digital communicator trouble conditions are detected.</description>
+               <event>
+                       <options>
+                               <option value="TROUBLE">Trouble</option>
+                               <option value="OK">OK</option>
+                       </options>
+               </event>
+       </channel-type>
+
+       <channel-type id="energy_cost_event">
+               <kind>trigger</kind>
+               <label>Energy Cost Event</label>
+               <description>Event sent when the cost of energy changes.</description>
+               <event>
+                       <options>
+                               <option value="LOW">Trouble</option>
+                               <option value="MID">Mid</option>
+                               <option value="HIGH">High</option>
+                               <option value="CRITCAL">Critical</option>
+                       </options>
+               </event>
+       </channel-type>
+
+       <channel-type id="camera_trigger_event">
+               <kind>trigger</kind>
+               <label>Camera Trigger Event</label>
+               <description>Event sent when a camera trigger is detected.</description>
+               <event>
+                       <options>
+                               <option value="1">Camera 1</option>
+                               <option value="2">Camera 2</option>
+                               <option value="3">Camera 3</option>
+                               <option value="4">Camera 4</option>
+                               <option value="5">Camera 5</option>
+                               <option value="6">Camera 6</option>
+                       </options>
+               </event>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/button.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/button.xml
new file mode 100644 (file)
index 0000000..3b5364f
--- /dev/null
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- Button Thing -->
+       <thing-type id="button">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Button</label>
+               <description>A button configured in the controller.</description>
+               <channels>
+                       <channel id="press" typeId="button_press"/>
+                       <channel id="activated_event" typeId="button_activated"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Button Number</label>
+                               <description>The button number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Button Channels -->
+       <channel-type id="button_press">
+               <item-type>Switch</item-type>
+               <label>Button Press</label>
+               <description>Sends a button event to the controller.</description>
+               <category>Switch</category>
+       </channel-type>
+
+       <channel-type id="button_activated">
+               <kind>trigger</kind>
+               <label>Button Activated</label>
+               <description>Event sent when a button is activated.</description>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/console.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/console.xml
new file mode 100644 (file)
index 0000000..7143577
--- /dev/null
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- Console Thing -->
+       <thing-type id="console">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Console</label>
+               <description>A console configured in the controller.</description>
+               <channels>
+                       <channel id="enable_disable_beeper" typeId="console_enable_disable_beeper"/>
+                       <channel id="beep" typeId="console_beep"/>
+               </channels>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Console Number</label>
+                               <description>The console number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Console Channels -->
+       <channel-type id="console_enable_disable_beeper">
+               <item-type>String</item-type>
+               <label>Enable/Disable Console Beeper</label>
+               <description>Enable/Disable the beeper for this/all console(s).</description>
+               <category>Switch</category>
+               <command>
+                       <options>
+                               <option value="OFF">Off</option>
+                               <option value="ON">On</option>
+                       </options>
+               </command>
+       </channel-type>
+
+       <channel-type id="console_beep">
+               <item-type>Number</item-type>
+               <label>Beep Console</label>
+               <description>Send a beep command to this/all console(s).</description>
+               <category>SoundVolume</category>
+               <state>
+                       <options>
+                               <option value="0">Off</option>
+                               <option value="1">Indefinitely</option>
+                               <option value="2">1 time</option>
+                               <option value="3">2 times</option>
+                               <option value="4">3 times</option>
+                               <option value="5">4 times</option>
+                               <option value="6">5 times</option>
+                       </options>
+               </state>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/humidity-sensor.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/humidity-sensor.xml
new file mode 100644 (file)
index 0000000..af126b6
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- Humidity Thing -->
+       <thing-type id="humidity_sensor">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Humidity Sensor</label>
+               <description>A humidity sensor configured in the controller.</description>
+               <channels>
+                       <channel id="humidity" typeId="sensor_humidity"/>
+                       <channel id="low_setpoint" typeId="sensor_humidity_low_setpoint"/>
+                       <channel id="high_setpoint" typeId="sensor_humidity_high_setpoint"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Humidity Sensor Number</label>
+                               <description>The humidity sensor number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Humidity Channels -->
+       <channel-type id="sensor_humidity">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Humidity</label>
+               <description>The current relative humidity at this humidity sensor.</description>
+               <category>Humidity</category>
+               <state readOnly="true" min="0" max="100" pattern="%d %%"/>
+       </channel-type>
+
+       <channel-type id="sensor_humidity_low_setpoint">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Low SetPoint</label>
+               <description>The current low setpoint for this humidity sensor.</description>
+               <category>Humidity</category>
+               <state min="0" max="100" pattern="%d %%"/>
+       </channel-type>
+
+       <channel-type id="sensor_humidity_high_setpoint">
+               <item-type>Number:Dimensionless</item-type>
+               <label>High SetPoint</label>
+               <description>The current high setpoint for this humidity sensor.</description>
+               <category>Humidity</category>
+               <state min="0" max="100" pattern="%d %%"/>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/lock.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/lock.xml
new file mode 100644 (file)
index 0000000..8364b84
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- Lock Thing -->
+       <thing-type id="lock">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Lock</label>
+               <description>An access control reader lock configured in the controller.</description>
+               <channels>
+                       <channel id="switch" typeId="lock_switch"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Lock Number</label>
+                               <description>The lock number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Lock Channels -->
+       <channel-type id="lock_switch">
+               <item-type>Switch</item-type>
+               <label>Lock/Unlock</label>
+               <description>Lock or unlock this lock.</description>
+               <category>Switch</category>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/temp-sensor.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/temp-sensor.xml
new file mode 100644 (file)
index 0000000..a7fd4ec
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- Temperature Sensor Thing -->
+       <thing-type id="temp_sensor">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Temperature Sensor</label>
+               <description>A temperature sensor configured in the controller.</description>
+               <channels>
+                       <channel id="temperature" typeId="sensor_temperature"/>
+                       <channel id="low_setpoint" typeId="sensor_temp_low_setpoint"/>
+                       <channel id="high_setpoint" typeId="sensor_temp_high_setpoint"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Temperature Sensor Number</label>
+                               <description>The temperature sensor number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Temperature Sensor Channels -->
+       <channel-type id="sensor_temperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Temperature</label>
+               <description>The current temperature at this temperature sensor.</description>
+               <category>Temperature</category>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="sensor_temp_low_setpoint">
+               <item-type>Number:Temperature</item-type>
+               <label>Low SetPoint</label>
+               <description>The current low setpoint of this temperature sensor.</description>
+               <category>Temperature</category>
+               <state pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="sensor_temp_high_setpoint">
+               <item-type>Number:Temperature</item-type>
+               <label>High SetPoint</label>
+               <description>The current high setpoint of this temperature sensor.</description>
+               <category>Temperature</category>
+               <state pattern="%.1f %unit%"/>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/thermostat.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/thermostat.xml
new file mode 100644 (file)
index 0000000..1504f62
--- /dev/null
@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- Thermostat Thing -->
+       <thing-type id="thermostat">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Thermostat</label>
+               <description>A thermostat configured in the controller.</description>
+               <channels>
+                       <channel id="freeze_alarm" typeId="thermostat_freeze_alarm"/>
+                       <channel id="comm_failure" typeId="thermostat_comm_failure"/>
+                       <channel id="status" typeId="thermostat_status"/>
+                       <channel id="temperature" typeId="thermostat_temperature"/>
+                       <channel id="outdoor_temperature" typeId="thermostat_outdoor_temperature"/>
+                       <channel id="heat_setpoint" typeId="thermostat_heat_setpoint"/>
+                       <channel id="cool_setpoint" typeId="thermostat_cool_setpoint"/>
+                       <channel id="humidity" typeId="thermostat_humidity"/>
+                       <channel id="humidify_setpoint" typeId="thermostat_humidify_setpoint"/>
+                       <channel id="dehumidify_setpoint" typeId="thermostat_dehumidify_setpoint"/>
+                       <channel id="system_mode" typeId="thermostat_system_mode"/>
+                       <channel id="fan_mode" typeId="thermostat_fan_mode"/>
+                       <channel id="hold_status" typeId="thermostat_hold_status"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Thermostat Number</label>
+                               <description>The thermostat number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Thermostat Channels -->
+       <channel-type id="thermostat_freeze_alarm">
+               <item-type>Contact</item-type>
+               <label>Thermostat Freeze Alarm</label>
+               <description>Closed when freeze alarm is triggered by this thermostat.</description>
+               <category>Alarm</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="thermostat_comm_failure">
+               <item-type>Contact</item-type>
+               <label>Thermostat Communications Failure</label>
+               <description>Closed during a communications failure with this thermostat.</description>
+               <category>Contact</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="thermostat_status">
+               <item-type>Number</item-type>
+               <label>Thermostat Status</label>
+               <description>The current status of this thermostat.</description>
+               <category>Heating</category>
+               <state readOnly="true" pattern="%d">
+                       <options>
+                               <option value="0">Idle</option>
+                               <option value="1">Heating</option>
+                               <option value="2">Cooling</option>
+                               <option value="3">Humidifying</option>
+                               <option value="4">Dehumidifying</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="thermostat_temperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Temperature</label>
+               <description>The current temperature at this thermostat.</description>
+               <category>Temperature</category>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="thermostat_outdoor_temperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Outdoor Temperature</label>
+               <description>The current outdoor temperature detected by this thermostat.</description>
+               <category>Temperature</category>
+               <state readOnly="true" pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="thermostat_heat_setpoint">
+               <item-type>Number:Temperature</item-type>
+               <label>Heat SetPoint</label>
+               <description>The current low/heating setpoint of this thermostat.</description>
+               <category>Temperature</category>
+               <state pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="thermostat_cool_setpoint">
+               <item-type>Number:Temperature</item-type>
+               <label>Cool SetPoint</label>
+               <description>The current high/cooling setpoint of this thermostat.</description>
+               <category>Temperature</category>
+               <state pattern="%.1f %unit%"/>
+       </channel-type>
+
+       <channel-type id="thermostat_humidity">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Humidity</label>
+               <description>The relative humidity at this thermostat.</description>
+               <category>Humidity</category>
+               <state readOnly="true" min="0" max="100" pattern="%d %%"/>
+       </channel-type>
+
+       <channel-type id="thermostat_humidify_setpoint">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Humidify SetPoint</label>
+               <description>The current low/humidify setpoint for this thermostat.</description>
+               <category>Humidity</category>
+               <state min="0" max="100" pattern="%d %%"/>
+       </channel-type>
+
+       <channel-type id="thermostat_dehumidify_setpoint">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Dehumidify SetPoint</label>
+               <description>The current high/dehumidify setpoint for this thermostat.</description>
+               <category>Humidity</category>
+               <state min="0" max="100" pattern="%d %%"/>
+       </channel-type>
+
+       <channel-type id="thermostat_system_mode">
+               <item-type>Number</item-type>
+               <label>System Mode</label>
+               <description>The current system mode of this thermostat.</description>
+               <category>Heating</category>
+               <state pattern="%d">
+                       <options>
+                               <option value="0">Off</option>
+                               <option value="1">Heat</option>
+                               <option value="2">Cool</option>
+                               <option value="3">Auto</option>
+                               <option value="4">Emergency heat</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="thermostat_fan_mode">
+               <item-type>Number</item-type>
+               <label>Fan Mode</label>
+               <description>The current fan mode of this thermostat.</description>
+               <category>Flow</category>
+               <state>
+                       <options>
+                               <option value="0">Auto</option>
+                               <option value="1">On</option>
+                               <option value="2">Cycle</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="thermostat_hold_status">
+               <item-type>Number</item-type>
+               <label>Hold Status</label>
+               <description>The current hold status of this thermostat.</description>
+               <category>Heating</category>
+               <state>
+                       <options>
+                               <option value="0">Off</option>
+                               <option value="1">Hold</option>
+                               <option value="2">Vacation hold</option>
+                       </options>
+               </state>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/unit.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/unit.xml
new file mode 100644 (file)
index 0000000..bc96c6e
--- /dev/null
@@ -0,0 +1,323 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- Unit Thing -->
+       <thing-type id="unit">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Unit</label>
+               <description>A basic unit configured in the controller.</description>
+               <channels>
+                       <channel id="level" typeId="unit_level"/>
+                       <channel id="switch" typeId="unit_switch"/>
+                       <channel id="on_for_seconds" typeId="on_for_seconds"/>
+                       <channel id="off_for_seconds" typeId="off_for_seconds"/>
+                       <channel id="on_for_minutes" typeId="on_for_minutes"/>
+                       <channel id="off_for_minutes" typeId="off_for_minutes"/>
+                       <channel id="on_for_hours" typeId="on_for_hours"/>
+                       <channel id="off_for_hours" typeId="off_for_hours"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Unit Number</label>
+                               <description>The unit number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Dimmable Thing -->
+       <thing-type id="dimmable">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Dimmable Unit</label>
+               <description>A dimmable unit configured in the controller.</description>
+               <channels>
+                       <channel id="level" typeId="unit_level"/>
+                       <channel id="switch" typeId="unit_switch"/>
+                       <channel id="on_for_seconds" typeId="on_for_seconds"/>
+                       <channel id="off_for_seconds" typeId="off_for_seconds"/>
+                       <channel id="on_for_minutes" typeId="on_for_minutes"/>
+                       <channel id="off_for_minutes" typeId="off_for_minutes"/>
+                       <channel id="on_for_hours" typeId="on_for_hours"/>
+                       <channel id="off_for_hours" typeId="off_for_hours"/>
+                       <channel id="switch_press_event" typeId="switch_press_event"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Dimmable Unit Number</label>
+                               <description>The dimmable unit number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- UPB Thing -->
+       <thing-type id="upb">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>UPB Unit</label>
+               <description>A UPB unit configured in the controller.</description>
+               <channels>
+                       <channel id="level" typeId="unit_level"/>
+                       <channel id="switch" typeId="unit_switch"/>
+                       <channel id="on_for_seconds" typeId="on_for_seconds"/>
+                       <channel id="off_for_seconds" typeId="off_for_seconds"/>
+                       <channel id="on_for_minutes" typeId="on_for_minutes"/>
+                       <channel id="off_for_minutes" typeId="off_for_minutes"/>
+                       <channel id="on_for_hours" typeId="on_for_hours"/>
+                       <channel id="off_for_hours" typeId="off_for_hours"/>
+                       <channel id="upb_status" typeId="upb_status"/>
+                       <channel id="switch_press_event" typeId="switch_press_event"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>UPB Unit Number</label>
+                               <description>The UPB unit number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Flag Thing -->
+       <thing-type id="flag">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Flag</label>
+               <description>A flag configured in the controller.</description>
+               <channels>
+                       <channel id="value" typeId="flag_value"/>
+                       <channel id="switch" typeId="flag_switch"/>
+                       <channel id="on_for_seconds" typeId="on_for_seconds"/>
+                       <channel id="off_for_seconds" typeId="off_for_seconds"/>
+                       <channel id="on_for_minutes" typeId="on_for_minutes"/>
+                       <channel id="off_for_minutes" typeId="off_for_minutes"/>
+                       <channel id="on_for_hours" typeId="on_for_hours"/>
+                       <channel id="off_for_hours" typeId="off_for_hours"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Flag Number</label>
+                               <description>The flag number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Output Thing -->
+       <thing-type id="output">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Voltage Output</label>
+               <description>A voltage output configured in the controller.</description>
+               <channels>
+                       <channel id="switch" typeId="unit_switch"/>
+                       <channel id="on_for_seconds" typeId="on_for_seconds"/>
+                       <channel id="off_for_seconds" typeId="off_for_seconds"/>
+                       <channel id="on_for_minutes" typeId="on_for_minutes"/>
+                       <channel id="off_for_minutes" typeId="off_for_minutes"/>
+                       <channel id="on_for_hours" typeId="on_for_hours"/>
+                       <channel id="off_for_hours" typeId="off_for_hours"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Voltage Output Number</label>
+                               <description>The voltage output number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Room Thing -->
+       <thing-type id="room">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Room</label>
+               <description>A room configured in the controller.</description>
+               <channels>
+                       <channel id="switch" typeId="room_switch"/>
+                       <channel id="scene_a" typeId="scene_toggle">
+                               <label>Scene A</label>
+                       </channel>
+                       <channel id="scene_b" typeId="scene_toggle">
+                               <label>Scene B</label>
+                       </channel>
+                       <channel id="scene_c" typeId="scene_toggle">
+                               <label>Scene C</label>
+                       </channel>
+                       <channel id="scene_d" typeId="scene_toggle">
+                               <label>Scene D</label>
+                       </channel>
+                       <channel id="state" typeId="room_state"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Room Number</label>
+                               <description>The room number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Unit channels -->
+       <channel-type id="unit_level">
+               <item-type>Dimmer</item-type>
+               <label>Unit Level</label>
+               <description>Increase/Decrease the level of this unit.</description>
+               <category>Slider</category>
+               <state min="0" max="100" pattern="%d %%"/>
+       </channel-type>
+
+       <channel-type id="unit_switch">
+               <item-type>Switch</item-type>
+               <label>Switch</label>
+               <description>Turn this unit on/off.</description>
+               <category>Switch</category>
+       </channel-type>
+
+       <channel-type id="on_for_seconds">
+               <item-type>Number</item-type>
+               <label>On for Seconds</label>
+               <description>Turn on this unit for a specified number of seconds.</description>
+               <category>Switch</category>
+               <state min="1" max="99"/>
+       </channel-type>
+
+       <channel-type id="off_for_seconds">
+               <item-type>Number</item-type>
+               <label>Off for Seconds</label>
+               <description>Turn off this unit for a specified number of seconds.</description>
+               <category>Switch</category>
+               <state min="1" max="99"/>
+       </channel-type>
+
+       <channel-type id="on_for_minutes">
+               <item-type>Number</item-type>
+               <label>On for Minutes</label>
+               <description>Turn on this unit for a specified number of minutes.</description>
+               <category>Switch</category>
+               <state min="1" max="99"/>
+       </channel-type>
+
+       <channel-type id="off_for_minutes">
+               <item-type>Number</item-type>
+               <label>Off for Minutes</label>
+               <description>Turn off this unit for a specified number of minutes.</description>
+               <category>Switch</category>
+               <state min="1" max="99"/>
+       </channel-type>
+
+       <channel-type id="on_for_hours">
+               <item-type>Number</item-type>
+               <label>On for Hours</label>
+               <description>Turn on this unit for a specified number of hours.</description>
+               <category>Switch</category>
+               <state min="1" max="18"/>
+       </channel-type>
+
+       <channel-type id="off_for_hours">
+               <item-type>Number</item-type>
+               <label>Off for Hours</label>
+               <description>Turn off this unit for a specified number of hours.</description>
+               <category>Switch</category>
+               <state min="1" max="18"/>
+       </channel-type>
+
+       <channel-type id="upb_status">
+               <item-type>String</item-type>
+               <label>UPB Status</label>
+               <description>Send a UPB status request message for this unit to the controller.</description>
+               <category>Status</category>
+               <command>
+                       <options>
+                               <option value="GET STATUS">Get Status</option>
+                       </options>
+               </command>
+       </channel-type>
+
+       <channel-type id="room_switch">
+               <item-type>Switch</item-type>
+               <label>Switch</label>
+               <description>Turn this room on/off.</description>
+               <category>Switch</category>
+       </channel-type>
+
+       <channel-type id="scene_toggle">
+               <item-type>Switch</item-type>
+               <label>Scene Toggle</label>
+               <description>Turn this scene on/off.</description>
+               <category>Switch</category>
+       </channel-type>
+
+       <channel-type id="room_state">
+               <item-type>Number</item-type>
+               <label>State</label>
+               <description>The current state of this room.</description>
+               <category>Switch</category>
+               <state pattern="%d">
+                       <options>
+                               <option value="0">Off</option>
+                               <option value="1">On</option>
+                               <option value="2">Scene A</option>
+                               <option value="3">Scene B</option>
+                               <option value="4">Scene C</option>
+                               <option value="5">Scene D</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="flag_value">
+               <item-type>Number</item-type>
+               <label>Flag Value</label>
+               <description>Numeric value of this flag.</description>
+               <category>Number</category>
+               <state min="0" max="255" pattern="%d"/>
+       </channel-type>
+
+       <channel-type id="flag_switch">
+               <item-type>Switch</item-type>
+               <label>Flag Switch</label>
+               <description>Turn this flag on/off.</description>
+               <category>Switch</category>
+       </channel-type>
+
+       <channel-type id="switch_press_event">
+               <kind>trigger</kind>
+               <label>Switch Press Event</label>
+               <description>Event sent when an ALC, UPB, Radio RA, or Starlite switch is pressed.</description>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/zone.xml b/bundles/org.openhab.binding.omnilink/src/main/resources/OH-INF/thing/zone.xml
new file mode 100644 (file)
index 0000000..48f09b1
--- /dev/null
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="omnilink"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       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">
+
+       <!-- Zone Thing -->
+       <thing-type id="zone">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>Zone</label>
+               <description>A zone configured in the controller.</description>
+               <channels>
+                       <channel id="contact" typeId="zone_contact"/>
+                       <channel id="current_condition" typeId="zone_current_condition"/>
+                       <channel id="latched_alarm_status" typeId="zone_latched_alarm_status"/>
+                       <channel id="arming_status" typeId="zone_arming_status"/>
+                       <channel id="bypass" typeId="zone_bypass"/>
+                       <channel id="restore" typeId="zone_restore"/>
+               </channels>
+               <properties>
+                       <property name="name"/>
+                       <property name="area"/>
+               </properties>
+               <representation-property>number</representation-property>
+               <config-description>
+                       <parameter name="number" type="integer" required="true">
+                               <label>Zone Number</label>
+                               <description>The zone number.</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <!-- Zone Channels -->
+       <channel-type id="zone_contact">
+               <item-type>Contact</item-type>
+               <label>Contact State</label>
+               <description>Contact state information of this zone.</description>
+               <category>Contact</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="zone_current_condition">
+               <item-type>Number</item-type>
+               <label>Current Condition</label>
+               <description>Current condition of this zone.</description>
+               <category>Contact</category>
+               <state readOnly="true" pattern="%d">
+                       <options>
+                               <option value="0">Secure</option>
+                               <option value="1">Not Ready</option>
+                               <option value="2">Trouble</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="zone_latched_alarm_status">
+               <item-type>Number</item-type>
+               <label>Latched Alarm Status</label>
+               <description>Latched alarm status of this zone.</description>
+               <category>Contact</category>
+               <state readOnly="true" pattern="%d">
+                       <options>
+                               <option value="0">Secure</option>
+                               <option value="1">Tripped</option>
+                               <option value="2">Reset, but previously tripped</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="zone_arming_status">
+               <item-type>Number</item-type>
+               <label>Arming Status</label>
+               <description>Arming status of this zone.</description>
+               <category>Contact</category>
+               <state readOnly="true" pattern="%d">
+                       <options>
+                               <option value="0">Disarmed</option>
+                               <option value="1">Armed</option>
+                               <option value="2">Bypassed by user</option>
+                               <option value="3">Bypassed by system</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="zone_bypass">
+               <item-type>String</item-type>
+               <label>Bypass Zone</label>
+               <description>Send a 4 digit user code to bypass this zone.</description>
+               <category>Alarm</category>
+       </channel-type>
+
+       <channel-type id="zone_restore">
+               <item-type>String</item-type>
+               <label>Restore Zone</label>
+               <description>Send a 4 digit user code to restore this zone.</description>
+               <category>Alarm</category>
+       </channel-type>
+
+</thing:thing-descriptions>
index f557f63b034da739a0569a264f318dd565d065a7..6df37e99c4b45ed794aac005fa0a30f5ea20401a 100644 (file)
     <module>org.openhab.binding.oceanic</module>
     <module>org.openhab.binding.ojelectronics</module>
     <module>org.openhab.binding.omnikinverter</module>
+    <module>org.openhab.binding.omnilink</module>
     <module>org.openhab.binding.onebusaway</module>
     <module>org.openhab.binding.onewiregpio</module>
     <module>org.openhab.binding.onewire</module>