From 1d9bf63d5e263a1de1da59eb6defa35f86c90b1d Mon Sep 17 00:00:00 2001 From: Dan Cunningham Date: Sun, 27 Nov 2022 10:25:31 -0800 Subject: [PATCH] [qolsysiq] Initial contribution of the Qolsys IQ Binding (#13699) * [qolsysiq] Initial contribution of the Qolsys IQ Binding Signed-off-by: Dan Cunningham --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.qolsysiq/NOTICE | 13 + .../org.openhab.binding.qolsysiq/README.md | 125 ++++++ .../doc/qolsysiq4.png | Bin 0 -> 42556 bytes bundles/org.openhab.binding.qolsysiq/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../internal/QolsysIQBindingConstants.java | 41 ++ .../internal/QolsysIQHandlerFactory.java | 68 +++ .../client/QolsysIQClientListener.java | 93 +++++ .../internal/client/QolsysiqClient.java | 390 ++++++++++++++++++ .../internal/client/dto/action/Action.java | 37 ++ .../client/dto/action/ActionType.java | 24 ++ .../client/dto/action/AlarmAction.java | 31 ++ .../client/dto/action/AlarmActionType.java | 24 ++ .../dto/action/ArmAwayArmingAction.java | 39 ++ .../client/dto/action/ArmingAction.java | 43 ++ .../client/dto/action/ArmingActionType.java | 24 ++ .../client/dto/action/InfoAction.java | 31 ++ .../client/dto/action/InfoActionType.java | 23 ++ .../internal/client/dto/event/AlarmEvent.java | 29 ++ .../client/dto/event/ArmingEvent.java | 30 ++ .../internal/client/dto/event/ErrorEvent.java | 28 ++ .../internal/client/dto/event/Event.java | 32 ++ .../internal/client/dto/event/EventType.java | 26 ++ .../internal/client/dto/event/InfoEvent.java | 27 ++ .../client/dto/event/InfoEventType.java | 23 ++ .../client/dto/event/SecureArmInfoEvent.java | 27 ++ .../client/dto/event/SummaryInfoEvent.java | 30 ++ .../client/dto/event/ZoneActiveEvent.java | 28 ++ .../client/dto/event/ZoneAddEvent.java | 28 ++ .../internal/client/dto/event/ZoneEvent.java | 30 ++ .../client/dto/event/ZoneEventType.java | 24 ++ .../client/dto/event/ZoneUpdateEvent.java | 28 ++ .../internal/client/dto/model/AlarmType.java | 29 ++ .../internal/client/dto/model/Partition.java | 28 ++ .../client/dto/model/PartitionStatus.java | 27 ++ .../internal/client/dto/model/Zone.java | 32 ++ .../client/dto/model/ZoneActiveState.java | 23 ++ .../internal/client/dto/model/ZoneStatus.java | 35 ++ .../internal/client/dto/model/ZoneType.java | 119 ++++++ .../config/QolsysIQPanelConfiguration.java | 27 ++ .../QolsysIQPartitionConfiguration.java | 27 ++ .../config/QolsysIQZoneConfiguration.java | 25 ++ .../QolsysIQChildDiscoveryService.java | 89 ++++ .../QolsysIQChildDiscoveryHandler.java | 37 ++ .../handler/QolsysIQPanelHandler.java | 327 +++++++++++++++ .../handler/QolsysIQPartitionHandler.java | 369 +++++++++++++++++ .../internal/handler/QolsysIQZoneHandler.java | 135 ++++++ .../main/resources/OH-INF/binding/binding.xml | 9 + .../resources/OH-INF/i18n/qolsysiq.properties | 72 ++++ .../src/main/resources/OH-INF/thing/panel.xml | 28 ++ .../main/resources/OH-INF/thing/partition.xml | 103 +++++ .../src/main/resources/OH-INF/thing/zone.xml | 63 +++ bundles/pom.xml | 1 + 55 files changed, 3033 insertions(+) create mode 100644 bundles/org.openhab.binding.qolsysiq/NOTICE create mode 100644 bundles/org.openhab.binding.qolsysiq/README.md create mode 100644 bundles/org.openhab.binding.qolsysiq/doc/qolsysiq4.png create mode 100644 bundles/org.openhab.binding.qolsysiq/pom.xml create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/QolsysIQBindingConstants.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/QolsysIQHandlerFactory.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/QolsysIQClientListener.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/QolsysiqClient.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/Action.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ActionType.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/AlarmAction.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/AlarmActionType.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmAwayArmingAction.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmingAction.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmingActionType.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/InfoAction.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/InfoActionType.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/AlarmEvent.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ArmingEvent.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ErrorEvent.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/Event.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/EventType.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/InfoEvent.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/InfoEventType.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/SecureArmInfoEvent.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/SummaryInfoEvent.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneActiveEvent.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneAddEvent.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneEvent.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneEventType.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneUpdateEvent.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/AlarmType.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/Partition.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/PartitionStatus.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/Zone.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneActiveState.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneStatus.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneType.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQPanelConfiguration.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQPartitionConfiguration.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQZoneConfiguration.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/discovery/QolsysIQChildDiscoveryService.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQChildDiscoveryHandler.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQPanelHandler.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQPartitionHandler.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQZoneHandler.java create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/i18n/qolsysiq.properties create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/panel.xml create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/partition.xml create mode 100644 bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/zone.xml diff --git a/CODEOWNERS b/CODEOWNERS index 1d0ebb2377e..2cd894f9638 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -272,6 +272,7 @@ /bundles/org.openhab.binding.pushover/ @cweitkamp /bundles/org.openhab.binding.pushsafer/ @appzer @cweitkamp /bundles/org.openhab.binding.qbus/ @QbusKoen +/bundles/org.openhab.binding.qolsysiq/ @digitaldan /bundles/org.openhab.binding.radiothermostat/ @mlobstein /bundles/org.openhab.binding.regoheatpump/ @crnjan /bundles/org.openhab.binding.revogi/ @andibraeu diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index d5178fa3bc0..eb7f0257249 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1356,6 +1356,11 @@ org.openhab.binding.qbus ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.qolsysiq + ${project.version} + org.openhab.addons.bundles org.openhab.binding.radiothermostat diff --git a/bundles/org.openhab.binding.qolsysiq/NOTICE b/bundles/org.openhab.binding.qolsysiq/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/NOTICE @@ -0,0 +1,13 @@ +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 diff --git a/bundles/org.openhab.binding.qolsysiq/README.md b/bundles/org.openhab.binding.qolsysiq/README.md new file mode 100644 index 00000000000..7be0e7fe23e --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/README.md @@ -0,0 +1,125 @@ +# Qolsys IQ Binding + +This binding directly controls a [Qolsys IQ](https://qolsys.com/security/) security panel. +This allows for local monitoring of alarm and zone statuses as well as arming, disarming and triggering alarms. + +Qolsys (a division of Johnson Controls) is a popular manufacturer of alarm systems. +The Qolsys IQ line of panels supports both wireless and hard wire sensors and features built in Cellular and Wi-Fi dual path communication that natively integrates with Alarm.com monitoring and supervision. + +This binding directly interfaces with the panel and does not require cloud access. + +![Qolsys IQ 4](doc/qolsysiq4.png) + +## Supported Things + +| Thing | Description | Thing Type | Thing UID | +|---------------------|-------------------------------------------------------------------------------------------|------------|-----------| +| Qolsys IQ Panel | A Qolsys IQ security panel (all current models, which is 2+ and 4 at the time of writing) | Bridge | panel | +| Qolsys IQ Partition | A logical partition which can be armed, disarmed, and is responsible for managing zones | Bridge | partition | +| Qolsys IQ Zone | A generic zone sensor | Thing | zone | + +## Discovery + +### Qolsys IQ Panel (Bridge) + +The Qolsys IQ Panel must be manually added using a host name or ip address along with a secure access token from the panel settings. +To enable 3rd party control and retrieve the access token follow the following steps on the security panel touch screen: + +`Settings` --> `Advanced Settings` --> `Installation` --> `Dealer Settings` -> `6 Digit User Code` (set to enabled) + +`Settings` --> `Advanced Settings` --> `Installation` --> `Devices` --> `Wi-Fi Devices` --> `Control4` (set to enabled) + + *Panel will reboot* + +`Settings` --> `Advanced Settings` --> `Installation` --> `Devices` --> `Wi-Fi Devices` --> `Reveal Secure Token` (copy token to use in panel configuration) + +At this point you may add the panel thing in openHAB using the secure token along with the IP or host name of the panel. + +### Partition (Bridge) + +Once a panel is added, partitions will be automatically discovered and appear in the inbox. + +### Zone (Thing) + +Once a partition is added, zones will be automatically discovered and appear in the inbox. + +## Thing Configuration + +### `panel` Thing Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-------------------|---------|-----------------------------------------------------|---------|----------|----------| +| hostname | text | Hostname or IP address of the device | N/A | yes | no | +| port | integer | Port the device is listening on | 12345 | no | no | +| key | text | Access token / key found in the panel settings menu | N/A | yes | no | + +### `partition` Thing Configuration + +| Name | Type | Description | Default | Required | Advanced | +|------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------|----------| +| id | integer | Partition id of the panel, staring with '0' for the first partition | N/A | yes | no | +| disarmCode | text | Optional disarm code to use when receiving a disarm command without a code. Required for integrations like Alexa and Homekit who do not provide codes when disarming. Leave blank to always require a code | blank | no | no | +| armCode | text | Optional arm code to use when receiving arm commands without a code. Only required if the panel has been configured to require arm codes. Leave blank to always require a code | blank | no | yes | + +### `zone` Thing Configuration + +| Name | Type | Description | Default | Required | Advanced | +|---------|---------|---------------------------------------------------------------------------------------------------------|---------|----------|----------| +| id | integer | Id of the zone, staring with '1' for the first zone | N/A | yes | no | + +## Channels + +### Panel Channels + +None. + +### Partition Channels + +| Channel | Type | Read/Write | Description | State Options | Command Options | +|-------------|--------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|----------------------------| +| armState | String | RW | Reports the current partition arm state or sends an arm or disarm command to the system. Security codes can be appended to the command using a colon delimiter (e.g. 'DISARM:123456'). Codes appended to the command will be used in place of the `armCode` configuration property if set. | ALARM, ARM_AWAY, ARM_STAY, DISARM, ENTRY_DELAY, EXIT_DELAY | ARM_AWAY, ARM_STAY, DISARM | +| alarmState | String | RW | Reports on the current alarm state, or triggers an instant alarm | AUXILIARY, FIRE, POLICE, ZONEOPEN, NONE | AUXILIARY, FIRE, POLICE | +| armingDelay | Number | R | The arming delay countdown currently in progress | Seconds remaining | N/A | +| errorEvent | String | R | Last error event message reported by the partition. Clears after 30 seconds | Error text | N/A | + +### Zone Channels + +| Channel | Type | Read/Write | Description | State Options | +|---------|---------|------------|------------------------|---------------------------------------------| +| status | String | R | The zone status | ACTIVE, CLOSED, OPEN, FAILURE, IDLE, TAMPER | +| state | Number | R | The zone state | Number | +| contact | Contact | R | The zone contact state | OPEN, CLOSED | + +## Full Example + +### qolsysiq.things + +``` +Bridge qolsysiq:panel:home "Home Security Panel" [ hostname="192.168.3.123", port=12345, key="AAABBB00" ] { + Bridge partition 0 "Partition Main" [ id=0, armCode="123456" ] { + Thing zone 1 "Window" [ id=1 ] + Thing zone 2 "Motion" [ id=2 ] + } +} +``` + +### qolsysiq.items + +Sample items file with both Alexa and Homekit voice control + +``` +Group PartitionMain "Alarm System" ["Equipment"] {alexa="SecurityPanel", homekit = "SecuritySystem"} +String PartitionMain_PartitionArmState "Partition Arm State" (PartitionMain) ["Point"] {channel="qolsysiq:partition:home:0:armState", alexa="ArmState" [DISARMED="DISARM",ARMED_STAY="ARM_STAY",ARMED_AWAY="ARM_AWAY:EXIT_DELAY"], homekit = "SecuritySystem.CurrentSecuritySystemState,SecuritySystem.TargetSecuritySystemState" [STAY_ARM="ARM_STAY", AWAY_ARM="ARM_AWAY", DISARM="DISARM", DISARMED="DISARM", TRIGGERED="ALARM"]} +String PartitionMain_PartitionAlarmState "Partition Alarm State" (PartitionMain) ["Point"] {channel="qolsysiq:partition:home:0:alarmState"} +Number PartitionMain_PartitionArmingDelay "Partition Arming Delay" (PartitionMain) ["Point"] {channel="qolsysiq:partition:home:0:armingDelay"} +String PartitionMain_ErrorEvent "Error Event" (PartitionMain) ["Point"] {channel="qolsysiq:partition:home:0:errorEvent" } + +Group ZoneKitchenWindows "Qolsys IQ Zone: Kitchen Windows" ["Equipment"] +Number ZoneKitchenWindows_ZoneState "Kitchen Windows Zone State" (ZoneKitchenWindows) ["Point"] {channel="qolsysiq:zone:home:0:1:state"} +String ZoneKitchenWindows_ZoneStatus "Kitchen Windows Zone Status" (ZoneKitchenWindows) ["Point"] {channel="qolsysiq:zone:home:0:1:status"} +Contact ZoneKitchenWindows_ZoneContact "Kitchen Windows Zone Contact" (ZoneKitchenWindows) ["Point"] {channel="qolsysiq:zone:home:0:1:contact"} + +Group ZoneMotionDetector1 "Motion Detector 1" ["Equipment"] +Number ZoneMotionDetector_ZoneState1 "Motion Detector 1 Zone State" (ZoneMotionDetector1) ["Point"] {channel="qolsysiq:zone:home:0:2:state"} +String ZoneMotionDetector_ZoneStatus1 "Motion Detector 1 Zone Status" (ZoneMotionDetector1) ["Point"] {channel="qolsysiq:zone:home:0:2:status"} +``` diff --git a/bundles/org.openhab.binding.qolsysiq/doc/qolsysiq4.png b/bundles/org.openhab.binding.qolsysiq/doc/qolsysiq4.png new file mode 100644 index 0000000000000000000000000000000000000000..35fbb25c1d76d802a7658eb61e884507a1ac2578 GIT binary patch literal 42556 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 SGa@1)A{|ZrC!ZeMM*R=C{`NBf literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.qolsysiq/pom.xml b/bundles/org.openhab.binding.qolsysiq/pom.xml new file mode 100644 index 00000000000..91c7b73da3b --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.4.0-SNAPSHOT + + + org.openhab.binding.qolsysiq + + openHAB Add-ons :: Bundles :: QolsysIQ Binding + + diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/feature/feature.xml b/bundles/org.openhab.binding.qolsysiq/src/main/feature/feature.xml new file mode 100644 index 00000000000..b02bdd6f50b --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.qolsysiq/${project.version} + + diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/QolsysIQBindingConstants.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/QolsysIQBindingConstants.java new file mode 100644 index 00000000000..4028893edb6 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/QolsysIQBindingConstants.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link QolsysIQBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class QolsysIQBindingConstants { + + public static final String BINDING_ID = "qolsysiq"; + + public static final ThingTypeUID THING_TYPE_PANEL = new ThingTypeUID(BINDING_ID, "panel"); + public static final ThingTypeUID THING_TYPE_PARTITION = new ThingTypeUID(BINDING_ID, "partition"); + public static final ThingTypeUID THING_TYPE_ZONE = new ThingTypeUID(BINDING_ID, "zone"); + + public static final String CHANNEL_PARTITION_ARM_STATE = "armState"; + public static final String CHANNEL_PARTITION_ALARM_STATE = "alarmState"; + public static final String CHANNEL_PARTITION_COMMAND_DELAY = "armingDelay"; + public static final String CHANNEL_PARTITION_ERROR_EVENT = "errorEvent"; + + public static final String CHANNEL_ZONE_STATE = "state"; + public static final String CHANNEL_ZONE_STATUS = "status"; + public static final String CHANNEL_ZONE_CONTACT = "contact"; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/QolsysIQHandlerFactory.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/QolsysIQHandlerFactory.java new file mode 100644 index 00000000000..ae1edf8b30b --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/QolsysIQHandlerFactory.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal; + +import static org.openhab.binding.qolsysiq.internal.QolsysIQBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qolsysiq.internal.handler.QolsysIQPanelHandler; +import org.openhab.binding.qolsysiq.internal.handler.QolsysIQPartitionHandler; +import org.openhab.binding.qolsysiq.internal.handler.QolsysIQZoneHandler; +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 QolsysIQHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.qolsysiq", service = ThingHandlerFactory.class) +public class QolsysIQHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PANEL, THING_TYPE_PARTITION, + THING_TYPE_ZONE); + + @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 (THING_TYPE_PANEL.equals(thingTypeUID)) { + return new QolsysIQPanelHandler((Bridge) thing); + } + + if (THING_TYPE_PARTITION.equals(thingTypeUID)) { + return new QolsysIQPartitionHandler((Bridge) thing); + } + + if (THING_TYPE_ZONE.equals(thingTypeUID)) { + return new QolsysIQZoneHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/QolsysIQClientListener.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/QolsysIQClientListener.java new file mode 100644 index 00000000000..61e7566efed --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/QolsysIQClientListener.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qolsysiq.internal.client.dto.event.AlarmEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ArmingEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ErrorEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.SecureArmInfoEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.SummaryInfoEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneActiveEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneAddEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneUpdateEvent; + +/** + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public interface QolsysIQClientListener { + /** + * Callback when the connection has been disconnected + * + * @param reason + */ + void disconnected(Exception reason); + + /** + * {@link AlarmEvent} message callback + * + * @param event + */ + void alarmEvent(AlarmEvent event); + + /** + * {@link ArmingEvent} message callback + * + * @param event + */ + void armingEvent(ArmingEvent event); + + /** + * {@link ErrorEvent} message callback + * + * @param event + */ + void errorEvent(ErrorEvent event); + + /** + * {@link SummaryInfoEvent} message callback + * + * @param event + */ + void summaryInfoEvent(SummaryInfoEvent event); + + /** + * {@link SecureArmInfoEvent} message callback + * + * @param event + */ + void secureArmInfoEvent(SecureArmInfoEvent event); + + /** + * {@link ZoneActiveEvent} message callback + * + * @param event + */ + void zoneActiveEvent(ZoneActiveEvent event); + + /** + * {@link ZoneUpdateEvent} message callback + * + * @param event + */ + void zoneUpdateEvent(ZoneUpdateEvent event); + + /** + * {@link ZoneAddEvent} message callback + * + * @param event + */ + void zoneAddEvent(ZoneAddEvent event); +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/QolsysiqClient.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/QolsysiqClient.java new file mode 100644 index 00000000000..ae738962845 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/QolsysiqClient.java @@ -0,0 +1,390 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.reflect.Type; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qolsysiq.internal.client.dto.action.Action; +import org.openhab.binding.qolsysiq.internal.client.dto.event.AlarmEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ArmingEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ErrorEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.Event; +import org.openhab.binding.qolsysiq.internal.client.dto.event.EventType; +import org.openhab.binding.qolsysiq.internal.client.dto.event.InfoEventType; +import org.openhab.binding.qolsysiq.internal.client.dto.event.SecureArmInfoEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.SummaryInfoEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneActiveEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneAddEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneEventType; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneUpdateEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; + +/** + * A client that can communicate with a Qolsys IQ Panel + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class QolsysiqClient { + private static final String MESSAGE_ACK = "ACK"; + private final Logger logger = LoggerFactory.getLogger(QolsysiqClient.class); + private final Gson gson = new GsonBuilder().registerTypeAdapter(Event.class, new EventDeserializer()) + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + private List listeners = Collections.synchronizedList(new ArrayList<>()); + private @Nullable SSLSocket socket; + private @Nullable BufferedReader reader; + private @Nullable BufferedWriter writer; + private @Nullable Thread readerThread; + private @Nullable ScheduledFuture heartBeatFuture; + private ScheduledExecutorService scheduler; + private Object writeLock = new Object(); + private long lastResponseTime; + private boolean hasACK = false; + private boolean connected; + private String host; + private int port; + private int heartbeatSeconds; + private String threadName; + private SSLSocketFactory sslsocketfactory; + + /** + * Creates a new QolsysiqClient + * + * @param host + * @param port + * @param heartbeatSeconds + * @param scheduler for the heart beat task + * @param threadName + */ + public QolsysiqClient(String host, int port, int heartbeatSeconds, ScheduledExecutorService scheduler, + String threadName) throws IOException { + this.host = host; + this.port = port; + this.heartbeatSeconds = heartbeatSeconds; + this.scheduler = scheduler; + this.threadName = threadName; + + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, acceptAlltrustManagers(), null); + sslsocketfactory = sslContext.getSocketFactory(); + } catch (KeyManagementException | NoSuchAlgorithmException e) { + throw new IOException(e); + } + } + + /** + * Connects to the panel + * + * @throws IOException + */ + public synchronized void connect() throws IOException { + logger.debug("connect"); + if (connected) { + logger.debug("connect: already connected, ignoring"); + return; + } + + SSLSocket socket = (SSLSocket) sslsocketfactory.createSocket(host, port); + socket.startHandshake(); + writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + this.socket = socket; + + Thread readerThread = new Thread(this::readEvents, threadName); + readerThread.setDaemon(true); + readerThread.start(); + this.readerThread = readerThread; + connected = true; + try { + // send an initial message to confirm a connection and record a response time + writeMessage(""); + } catch (IOException e) { + // clean up before bubbling up exception + disconnect(); + throw e; + } + heartBeatFuture = scheduler.scheduleWithFixedDelay(() -> { + if (connected) { + try { + if (System.currentTimeMillis() - lastResponseTime > (heartbeatSeconds + 5) * 1000) { + throw new IOException("No responses received"); + } + writeMessage(""); + } catch (IOException e) { + logger.debug("Problem sending heartbeat", e); + disconnectAndNotify(e); + } + } + }, heartbeatSeconds, heartbeatSeconds, TimeUnit.SECONDS); + } + + /** + * Disconnects from the panel + */ + public void disconnect() { + connected = false; + + ScheduledFuture heartbeatFuture = this.heartBeatFuture; + if (heartbeatFuture != null) { + heartbeatFuture.cancel(true); + } + + Thread readerThread = this.readerThread; + if (readerThread != null && readerThread.isAlive()) { + readerThread.interrupt(); + } + + SSLSocket socket = this.socket; + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + logger.debug("Error closing SSL socket: {}", e.getMessage()); + } + this.socket = null; + } + BufferedReader reader = this.reader; + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + logger.debug("Error closing reader: {}", e.getMessage()); + } + this.reader = null; + } + BufferedWriter writer = this.writer; + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + logger.debug("Error closing writer: {}", e.getMessage()); + } + this.writer = null; + } + } + + /** + * Sends an Action message to the panel + * + * @param action + * @throws IOException + */ + public void sendAction(Action action) throws IOException { + logger.debug("sendAction {}", action.type); + writeMessage(gson.toJson(action)); + } + + /** + * Adds a QolsysIQClientListener + * + * @param listener + */ + public void addListener(QolsysIQClientListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + /** + * Removes a QolsysIQClientListener + * + * @param listener + */ + public void removeListener(QolsysIQClientListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + private synchronized void writeMessage(String message) throws IOException { + if (!connected) { + logger.debug("writeMessage: not connected, ignoring {}", message); + return; + } + synchronized (writeLock) { + hasACK = false; + logger.trace("writeMessage: {}", message); + BufferedWriter writer = this.writer; + if (writer != null) { + writer.write(message); + writer.newLine(); + writer.flush(); + try { + writeLock.wait(5000); + } catch (InterruptedException e) { + logger.debug("write lock interupted"); + } + if (!hasACK) { + logger.trace("writeMessage: no ACK for {}", message); + throw new IOException("No response to message: " + message); + } + } + } + } + + private void readEvents() { + String message; + BufferedReader reader = this.reader; + try { + while (connected && reader != null && (message = reader.readLine()) != null) { + logger.trace("Message: {}", message); + lastResponseTime = System.currentTimeMillis(); + if (MESSAGE_ACK.equals(message)) { + synchronized (writeLock) { + hasACK = true; + writeLock.notify(); + } + continue; + } + try { + Event event = gson.fromJson(message, Event.class); + if (event == null) { + logger.debug("Could not deserialize message: {}", message); + continue; + } + synchronized (listeners) { + if (event instanceof AlarmEvent) { + listeners.forEach(listener -> listener.alarmEvent((AlarmEvent) event)); + } else if (event instanceof ArmingEvent) { + listeners.forEach(listener -> listener.armingEvent((ArmingEvent) event)); + } else if (event instanceof ErrorEvent) { + listeners.forEach(listener -> listener.errorEvent((ErrorEvent) event)); + } else if (event instanceof SecureArmInfoEvent) { + listeners.forEach(listener -> listener.secureArmInfoEvent((SecureArmInfoEvent) event)); + } else if (event instanceof SummaryInfoEvent) { + listeners.forEach(listener -> listener.summaryInfoEvent((SummaryInfoEvent) event)); + } else if (event instanceof ZoneActiveEvent) { + listeners.forEach(listener -> listener.zoneActiveEvent((ZoneActiveEvent) event)); + } else if (event instanceof ZoneUpdateEvent) { + listeners.forEach(listener -> listener.zoneUpdateEvent((ZoneUpdateEvent) event)); + } else if (event instanceof ZoneAddEvent) { + listeners.forEach(listener -> listener.zoneAddEvent((ZoneAddEvent) event)); + } + } + } catch (JsonSyntaxException e) { + logger.debug("Could not parse messge", e); + } + } + if (connected) { + throw new IOException("socket disconencted"); + } + } catch (IOException e) { + disconnectAndNotify(e); + } + } + + private void disconnectAndNotify(Exception e) { + if (connected) { + disconnect(); + synchronized (listeners) { + listeners.forEach(listener -> listener.disconnected(e)); + } + } + } + + private TrustManager[] acceptAlltrustManagers() { + return new TrustManager[] { new X509TrustManager() { + @Override + public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) { + } + + @Override + public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) { + } + + @Override + public X509Certificate @Nullable [] getAcceptedIssuers() { + return null; + } + } }; + } + + class EventDeserializer implements JsonDeserializer { + @Override + public @Nullable Event deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + JsonElement event = jsonObject.get("event"); + if (event != null) { + switch (EventType.valueOf(event.getAsString())) { + case ALARM: + return context.deserialize(jsonObject, AlarmEvent.class); + case ARMING: + return context.deserialize(jsonObject, ArmingEvent.class); + case ERROR: + return context.deserialize(jsonObject, ErrorEvent.class); + case INFO: + JsonElement infoType = jsonObject.get("info_type"); + if (infoType != null) { + switch (InfoEventType.valueOf(infoType.getAsString())) { + case SECURE_ARM: + return context.deserialize(jsonObject, SecureArmInfoEvent.class); + case SUMMARY: + return context.deserialize(jsonObject, SummaryInfoEvent.class); + } + } + break; + case ZONE_EVENT: + JsonElement zoneEventType = jsonObject.get("zone_event_type"); + if (zoneEventType != null) { + switch (ZoneEventType.valueOf(zoneEventType.getAsString())) { + case ZONE_ACTIVE: + return context.deserialize(jsonObject, ZoneActiveEvent.class); + case ZONE_UPDATE: + return context.deserialize(jsonObject, ZoneUpdateEvent.class); + case ZONE_ADD: + return context.deserialize(jsonObject, ZoneAddEvent.class); + default: + break; + } + } + } + } + return null; + } + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/Action.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/Action.java new file mode 100644 index 00000000000..2f7142798b8 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/Action.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.action; + +import com.google.gson.annotations.SerializedName; + +/** + * The base type for various action messages sent to a panel + * + * @author Dan Cunningham - Initial contribution + */ +public abstract class Action { + @SerializedName("action") + public ActionType type; + public Integer version = 0; + public String source = "C4"; + public String token; + + public Action(ActionType type) { + this(type, ""); + } + + public Action(ActionType type, String token) { + this.type = type; + this.token = token; + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ActionType.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ActionType.java new file mode 100644 index 00000000000..af9184ac29a --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ActionType.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.action; + +/** + * The type of {@link Action} sent to a panel + * + * @author Dan Cunningham - Initial contribution + */ +public enum ActionType { + ALARM, + ARMING, + INFO +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/AlarmAction.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/AlarmAction.java new file mode 100644 index 00000000000..b2b6e194054 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/AlarmAction.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.action; + +/** + * An {@link ActionType.ALARM} type of {@link Action} message sent to the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class AlarmAction extends Action { + public AlarmActionType alarmType; + + public AlarmAction(AlarmActionType alarmType) { + this(alarmType, ""); + } + + public AlarmAction(AlarmActionType alarmType, String token) { + super(ActionType.ALARM, token); + this.alarmType = alarmType; + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/AlarmActionType.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/AlarmActionType.java new file mode 100644 index 00000000000..dc8c57721a8 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/AlarmActionType.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.action; + +/** + * The type of {@link AlarmAction} sent to a panel + * + * @author Dan Cunningham - Initial contribution + */ +public enum AlarmActionType { + AUXILIARY, + FIRE, + POLCIE +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmAwayArmingAction.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmAwayArmingAction.java new file mode 100644 index 00000000000..3149b577aaa --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmAwayArmingAction.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.action; + +/** + * An {@link ArmingActionType.ARM_AWAY} type of {@link ArmingAction} message sent to the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class ArmAwayArmingAction extends ArmingAction { + public Integer delay; + + public ArmAwayArmingAction(String token, Integer partitionId, Integer delay) { + super(ArmingActionType.ARM_AWAY, token, partitionId); + this.delay = delay; + } + + public ArmAwayArmingAction(String token, Integer partitionId) { + this(token, partitionId, null); + } + + public ArmAwayArmingAction(Integer partitionId) { + this("", partitionId, null); + } + + public ArmAwayArmingAction(Integer partitionId, Integer delay) { + this("", partitionId, delay); + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmingAction.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmingAction.java new file mode 100644 index 00000000000..fdf0bb88b6d --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmingAction.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.action; + +/** + * An {@link ActionType.ARMING} type of {@link ArmingAction} message sent to the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class ArmingAction extends Action { + public ArmingActionType armingType; + public Integer partitionId; + public String usercode; + + public ArmingAction(ArmingActionType armingType, Integer partitionId) { + this(armingType, "", partitionId, null); + } + + public ArmingAction(ArmingActionType armingType, Integer partitionId, String usercode) { + this(armingType, "", partitionId, usercode); + } + + public ArmingAction(ArmingActionType armingType, String token, Integer partitionId) { + this(armingType, token, partitionId, null); + } + + public ArmingAction(ArmingActionType armingType, String token, Integer partitionId, String usercode) { + super(ActionType.ARMING, token); + this.armingType = armingType; + this.partitionId = partitionId; + this.usercode = usercode; + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmingActionType.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmingActionType.java new file mode 100644 index 00000000000..9951b68184d --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/ArmingActionType.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.action; + +/** + * The type of {@link ArmingAction} sent to a panel + * + * @author Dan Cunningham - Initial contribution + */ +public enum ArmingActionType { + ARM_AWAY, + ARM_STAY, + DISARM; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/InfoAction.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/InfoAction.java new file mode 100644 index 00000000000..4d1e6a5ad0b --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/InfoAction.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.action; + +/** + * An {@link ActionType.INFO} type of {@link InfoAction} message sent to the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class InfoAction extends Action { + public InfoActionType infoType; + + public InfoAction(InfoActionType infoType) { + this(infoType, ""); + } + + public InfoAction(InfoActionType infoType, String token) { + super(ActionType.INFO, token); + this.infoType = infoType; + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/InfoActionType.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/InfoActionType.java new file mode 100644 index 00000000000..a62a7a39119 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/action/InfoActionType.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.action; + +/** + * The type of {@link InfoAction} sent to a panel + * + * @author Dan Cunningham - Initial contribution + */ +public enum InfoActionType { + SUMMARY, + SECURE_ARM +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/AlarmEvent.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/AlarmEvent.java new file mode 100644 index 00000000000..0570e8dcc43 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/AlarmEvent.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +import org.openhab.binding.qolsysiq.internal.client.dto.model.AlarmType; + +/** + * An {@link EventType.ALARM} type of {@link Event} message sent from the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class AlarmEvent extends Event { + public AlarmType alarmType; + public Integer partitionId; + + public AlarmEvent() { + super(EventType.ALARM); + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ArmingEvent.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ArmingEvent.java new file mode 100644 index 00000000000..42bc3c0af26 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ArmingEvent.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +import org.openhab.binding.qolsysiq.internal.client.dto.model.PartitionStatus; + +/** + * An {@link EventType.ARMING} type of {@link Event} message sent from the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class ArmingEvent extends Event { + public PartitionStatus armingType; + public Integer partitionId; + public Integer delay; + + public ArmingEvent() { + super(EventType.ARMING); + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ErrorEvent.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ErrorEvent.java new file mode 100644 index 00000000000..1c04abf9e97 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ErrorEvent.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +/** + * An {@link EventType.ERROR} type of {@link Event} message sent from the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class ErrorEvent extends Event { + public String errorType; + public String description; + public Integer partitionId; + + public ErrorEvent() { + super(EventType.ERROR); + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/Event.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/Event.java new file mode 100644 index 00000000000..77bc1daa4a8 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/Event.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +import com.google.gson.annotations.SerializedName; + +/** + * The base type for various event messages sent by the panel + * + * @author Dan Cunningham - Initial contribution + */ +public abstract class Event { + @SerializedName("event") + public EventType eventType; + public String nonce; + @SerializedName("requestID") + public String requestID; + + public Event(EventType eventType) { + this.eventType = eventType; + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/EventType.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/EventType.java new file mode 100644 index 00000000000..ba2621d937b --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/EventType.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +/** + * The type of {@link Event} sent by the panel + * + * @author Dan Cunningham - Initial contribution + */ +public enum EventType { + ALARM, + ARMING, + ERROR, + INFO, + ZONE_EVENT; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/InfoEvent.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/InfoEvent.java new file mode 100644 index 00000000000..775a2f0808c --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/InfoEvent.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +/** + * An {@link EventType.INFO} type of {@link Event} message sent by the panel + * + * @author Dan Cunningham - Initial contribution + */ +public abstract class InfoEvent extends Event { + public InfoEventType infoType; + + public InfoEvent(InfoEventType infoType) { + super(EventType.INFO); + this.infoType = infoType; + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/InfoEventType.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/InfoEventType.java new file mode 100644 index 00000000000..b6afce9fe7b --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/InfoEventType.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +/** + * The type of {@link InfoEvent} sent by the panel + * + * @author Dan Cunningham - Initial contribution + */ +public enum InfoEventType { + SUMMARY, + SECURE_ARM; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/SecureArmInfoEvent.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/SecureArmInfoEvent.java new file mode 100644 index 00000000000..dee4d302506 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/SecureArmInfoEvent.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +/** + * A {@link InfoEventType.SECURE_ARM} type of {@link InfoEvent} message sent by the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class SecureArmInfoEvent extends InfoEvent { + public Integer partitionId; + public Boolean value; + + public SecureArmInfoEvent() { + super(InfoEventType.SECURE_ARM); + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/SummaryInfoEvent.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/SummaryInfoEvent.java new file mode 100644 index 00000000000..f80b854a3af --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/SummaryInfoEvent.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +import java.util.List; + +import org.openhab.binding.qolsysiq.internal.client.dto.model.Partition; + +/** + * A {@link InfoEventType.SUMMARY} type of {@link InfoEvent} message sent by the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class SummaryInfoEvent extends InfoEvent { + public List partitionList; + + public SummaryInfoEvent() { + super(InfoEventType.SUMMARY); + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneActiveEvent.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneActiveEvent.java new file mode 100644 index 00000000000..1b7d74971ed --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneActiveEvent.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +import org.openhab.binding.qolsysiq.internal.client.dto.model.ZoneActiveState; + +/** + * A {@link ZoneEventType.ZONE_ACTIVE} type of {@link ZoneEvent} message sent by the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class ZoneActiveEvent extends ZoneEvent { + public ZoneActiveState zone; + + public ZoneActiveEvent() { + super(ZoneEventType.ZONE_ACTIVE); + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneAddEvent.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneAddEvent.java new file mode 100644 index 00000000000..999b8a4458e --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneAddEvent.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +import org.openhab.binding.qolsysiq.internal.client.dto.model.Zone; + +/** + * A {@link ZoneEventType.ZONE_ADD} type of {@link ZoneEvent} message sent by the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class ZoneAddEvent extends ZoneEvent { + public Zone zone; + + public ZoneAddEvent() { + super(ZoneEventType.ZONE_ADD); + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneEvent.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneEvent.java new file mode 100644 index 00000000000..26599f216af --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneEvent.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +import com.google.gson.annotations.SerializedName; + +/** + * A Zone {@link Event} message sent by the panel + * + * @author Dan Cunningham - Initial contribution + */ +public abstract class ZoneEvent extends Event { + @SerializedName("zone_event_type") + public ZoneEventType type; + + public ZoneEvent(ZoneEventType type) { + super(EventType.ZONE_EVENT); + this.type = type; + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneEventType.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneEventType.java new file mode 100644 index 00000000000..d40a5be5692 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneEventType.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +/** + * The type of {@link ZoneEvent} sent by the panel + * + * @author Dan Cunningham - Initial contribution + */ +public enum ZoneEventType { + ZONE_ACTIVE, + ZONE_ADD, + ZONE_UPDATE; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneUpdateEvent.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneUpdateEvent.java new file mode 100644 index 00000000000..b01ca09699d --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/event/ZoneUpdateEvent.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.event; + +import org.openhab.binding.qolsysiq.internal.client.dto.model.Zone; + +/** + * A {@link ZoneEventType.ZONE_UPDATE} type of {@link ZoneEvent} message sent by the panel + * + * @author Dan Cunningham - Initial contribution + */ +public class ZoneUpdateEvent extends ZoneEvent { + public Zone zone; + + public ZoneUpdateEvent() { + super(ZoneEventType.ZONE_UPDATE); + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/AlarmType.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/AlarmType.java new file mode 100644 index 00000000000..9535846775a --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/AlarmType.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.model; + +import com.google.gson.annotations.SerializedName; + +/** + * The type of alarm + * + * @author Dan Cunningham - Initial contribution + */ +public enum AlarmType { + AUXILIARY, + FIRE, + POLICE, + @SerializedName("") + ZONEOPEN, + NONE; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/Partition.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/Partition.java new file mode 100644 index 00000000000..fc02d71b97d --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/Partition.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.model; + +import java.util.List; + +/** + * A logical alarm partition that can be armed, report state and contain zones + * + * @author Dan Cunningham - Initial contribution + */ +public class Partition { + public Integer partitionId; + public String name; + public PartitionStatus status; + public Boolean secureArm; + public List zoneList; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/PartitionStatus.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/PartitionStatus.java new file mode 100644 index 00000000000..cc38eddab9b --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/PartitionStatus.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.model; + +/** + * The current status of an alarm panel + * + * @author Dan Cunningham - Initial contribution + */ +public enum PartitionStatus { + ALARM, + ARM_AWAY, + ARM_STAY, + DISARM, + ENTRY_DELAY, + EXIT_DELAY; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/Zone.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/Zone.java new file mode 100644 index 00000000000..39010306c58 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/Zone.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.model; + +/** + * A zone sensor + * + * @author Dan Cunningham - Initial contribution + */ +public class Zone { + public String id; + public String type; + public String name; + public String group; + public ZoneStatus status; + public Integer state; + public Integer zoneId; + public Integer zonePhysicalType; + public Integer zoneAlarmType; + public ZoneType zoneType; + public Integer partitionId; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneActiveState.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneActiveState.java new file mode 100644 index 00000000000..4a05d525cba --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneActiveState.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.model; + +/** + * The active state of a zone + * + * @author Dan Cunningham - Initial contribution + */ +public class ZoneActiveState { + public Integer zoneId; + public ZoneStatus status; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneStatus.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneStatus.java new file mode 100644 index 00000000000..f37a44119e0 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneStatus.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.model; + +import com.google.gson.annotations.SerializedName; + +/** + * Represents the status of a zone + * + * @author Dan Cunningham - Initial contribution + */ +public enum ZoneStatus { + @SerializedName("Active") + ACTIVE, + @SerializedName("Closed") + CLOSED, + @SerializedName("Open") + OPEN, + @SerializedName("Failure") + FAILURE, + @SerializedName("Idle") + IDlE, + @SerializedName("Tamper") + TAMPER; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneType.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneType.java new file mode 100644 index 00000000000..e78f838b442 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/client/dto/model/ZoneType.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.client.dto.model; + +import com.google.gson.annotations.SerializedName; + +/** + * The zone physical type + * + * Big thanks to the folks at https://community.home-assistant.io/t/qolsys-iq-panel-2-and-3rd-party-integration/231405 + * + * @author Dan Cunningham - Initial contribution + */ +public enum ZoneType { + @SerializedName("0") + UNKNOWN, + @SerializedName("1") + CONTACT, + @SerializedName("2") + MOTION, + @SerializedName("3") + SOUND, + @SerializedName("4") + BREAKAGE, + @SerializedName("5") + SMOKE_HEAT, + @SerializedName("6") + CARBON_MONOXIDE, + @SerializedName("7") + RADON, + @SerializedName("8") + TEMPERATURE, + @SerializedName("9") + PANIC_BUTTON, + @SerializedName("10") + CONTROL, + @SerializedName("11") + CAMERA, + @SerializedName("12") + LIGHT, + @SerializedName("13") + GPS, + @SerializedName("14") + SIREN, + @SerializedName("15") + WATER, + @SerializedName("16") + TILT, + @SerializedName("17") + FREEZE, + @SerializedName("18") + TAKEOVER_MODULE, + @SerializedName("19") + GLASSBREAK, + @SerializedName("20") + TRANSLATOR, + @SerializedName("21") + MEDICAL_PENDANT, + @SerializedName("22") + WATER_IQ_FLOOD, + @SerializedName("23") + WATER_OTHER_FLOOD, + @SerializedName("30") + IMAGE_SENSOR, + @SerializedName("100") + WIRED_SENSOR, + @SerializedName("101") + RF_SENSOR, + @SerializedName("102") + KEYFOB, + @SerializedName("103") + WALLFOB, + @SerializedName("104") + RF_KEYPAD, + @SerializedName("105") + PANEL, + @SerializedName("106") + WTTS_OR_SECONDARY, + @SerializedName("107") + SHOCK, + @SerializedName("108") + SHOCK_SENSOR_MULTI_FUNCTION, + @SerializedName("109") + DOOR_BELL, + @SerializedName("110") + CONTACT_MULTI_FUNCTION, + @SerializedName("111") + SMOKE_MULTI_FUNCTION, + @SerializedName("112") + TEMPERATURE_MULTI_FUNCTION, + @SerializedName("113") + SHOCK_OTHERS, + @SerializedName("114") + OCCUPANCY_SENSOR, + @SerializedName("115") + BLUETOOTH, + @SerializedName("116") + PANEL_GLASS_BREAK, + @SerializedName("117") + POWERG_SIREN, + @SerializedName("118") + BLUETOOTH_SPEAKER, + @SerializedName("119") + PANEL_MOTION, + @SerializedName("120") + ZWAVE_SIREN, + @SerializedName("121") + COUNT; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQPanelConfiguration.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQPanelConfiguration.java new file mode 100644 index 00000000000..8d09f3b3749 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQPanelConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link QolsysIQPanelConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class QolsysIQPanelConfiguration { + public String hostname = ""; + public int port = 12345; + public String key = ""; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQPartitionConfiguration.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQPartitionConfiguration.java new file mode 100644 index 00000000000..06107dc3261 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQPartitionConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link QolsysIQPartitionConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class QolsysIQPartitionConfiguration { + public int id = 0; + public String armCode = ""; + public String disarmCode = ""; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQZoneConfiguration.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQZoneConfiguration.java new file mode 100644 index 00000000000..14800fe158d --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/config/QolsysIQZoneConfiguration.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link QolsysIQZoneConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class QolsysIQZoneConfiguration { + public int id = 0; +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/discovery/QolsysIQChildDiscoveryService.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/discovery/QolsysIQChildDiscoveryService.java new file mode 100644 index 00000000000..796c6c480cd --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/discovery/QolsysIQChildDiscoveryService.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.discovery; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qolsysiq.internal.QolsysIQBindingConstants; +import org.openhab.binding.qolsysiq.internal.handler.QolsysIQChildDiscoveryHandler; +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.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple discovery service that can be used by Partition and Zone Handlers + * + * @author Dan Cunningham - Initial contribution + * + */ +@NonNullByDefault +public class QolsysIQChildDiscoveryService extends AbstractDiscoveryService + implements DiscoveryService, ThingHandlerService { + private final Logger logger = LoggerFactory.getLogger(QolsysIQChildDiscoveryService.class); + + private static final Set SUPPORTED_DISCOVERY_THING_TYPES_UIDS = Set + .of(QolsysIQBindingConstants.THING_TYPE_PARTITION, QolsysIQBindingConstants.THING_TYPE_ZONE); + + private @Nullable ThingHandler thingHandler; + + public QolsysIQChildDiscoveryService() throws IllegalArgumentException { + super(SUPPORTED_DISCOVERY_THING_TYPES_UIDS, 5, false); + } + + @Override + public void setThingHandler(ThingHandler handler) { + if (handler instanceof QolsysIQChildDiscoveryHandler) { + ((QolsysIQChildDiscoveryHandler) handler).setDiscoveryService(this); + this.thingHandler = handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return thingHandler; + } + + @Override + protected void startScan() { + ThingHandler handler = this.thingHandler; + if (handler != null) { + ((QolsysIQChildDiscoveryHandler) handler).startDiscovery(); + } + } + + @Override + public void activate() { + super.activate(null); + } + + @Override + public void deactivate() { + super.deactivate(); + } + + public void discoverQolsysIQChildThing(ThingUID thingUID, ThingUID bridgeUID, Integer id, String label) { + logger.trace("discoverQolsysIQChildThing: {} {} {} {}", thingUID, bridgeUID, id, label); + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel(label).withProperty("id", id) + .withRepresentationProperty("id").withBridge(bridgeUID).build(); + thingDiscovered(result); + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQChildDiscoveryHandler.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQChildDiscoveryHandler.java new file mode 100644 index 00000000000..f9b818fc090 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQChildDiscoveryHandler.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qolsysiq.internal.discovery.QolsysIQChildDiscoveryService; + +/** + * Callback for our custom discovery service + * + * @author Dan Cunningham - Initial contribution + * + */ +@NonNullByDefault +public interface QolsysIQChildDiscoveryHandler { + /** + * Sets a {@link QolsysIQChildDiscoveryService} to call when device information is received + * + * @param service + */ + public void setDiscoveryService(QolsysIQChildDiscoveryService service); + + /** + * Initiates the discovery process + */ + public void startDiscovery(); +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQPanelHandler.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQPanelHandler.java new file mode 100644 index 00000000000..c0c736f0f6a --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQPanelHandler.java @@ -0,0 +1,327 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.handler; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qolsysiq.internal.QolsysIQBindingConstants; +import org.openhab.binding.qolsysiq.internal.client.QolsysIQClientListener; +import org.openhab.binding.qolsysiq.internal.client.QolsysiqClient; +import org.openhab.binding.qolsysiq.internal.client.dto.action.Action; +import org.openhab.binding.qolsysiq.internal.client.dto.action.InfoAction; +import org.openhab.binding.qolsysiq.internal.client.dto.action.InfoActionType; +import org.openhab.binding.qolsysiq.internal.client.dto.event.AlarmEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ArmingEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ErrorEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.SecureArmInfoEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.SummaryInfoEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneActiveEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneAddEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneUpdateEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.model.Partition; +import org.openhab.binding.qolsysiq.internal.config.QolsysIQPanelConfiguration; +import org.openhab.binding.qolsysiq.internal.discovery.QolsysIQChildDiscoveryService; +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.ThingUID; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QolsysIQPanelHandler} connects to a security panel and routes messages to child partitions. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class QolsysIQPanelHandler extends BaseBridgeHandler + implements QolsysIQClientListener, QolsysIQChildDiscoveryHandler { + private final Logger logger = LoggerFactory.getLogger(QolsysIQPanelHandler.class); + private static final int QUICK_RETRY_SECONDS = 1; + private static final int LONG_RETRY_SECONDS = 30; + private static final int HEARTBEAT_SECONDS = 30; + private @Nullable QolsysiqClient apiClient; + private @Nullable ScheduledFuture retryFuture; + private @Nullable QolsysIQChildDiscoveryService discoveryService; + private List partitions = Collections.synchronizedList(new LinkedList()); + private String key = ""; + + public QolsysIQPanelHandler(Bridge bridge) { + super(bridge); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("handleCommand {}", command); + if (command instanceof RefreshType) { + refresh(); + } + } + + @Override + public void initialize() { + logger.debug("initialize"); + updateStatus(ThingStatus.UNKNOWN); + scheduler.execute(() -> { + connect(); + }); + } + + @Override + public void dispose() { + stopRetryFuture(); + disconnect(); + } + + @Override + public Collection> getServices() { + return Collections.singleton(QolsysIQChildDiscoveryService.class); + } + + @Override + public void setDiscoveryService(QolsysIQChildDiscoveryService service) { + this.discoveryService = service; + } + + @Override + public void startDiscovery() { + refresh(); + } + + @Override + public void disconnected(Exception reason) { + logger.debug("disconnected", reason); + setOfflineAndReconnect(reason, QUICK_RETRY_SECONDS); + } + + @Override + public void alarmEvent(AlarmEvent event) { + logger.debug("AlarmEvent {}", event.partitionId); + QolsysIQPartitionHandler handler = partitionHandler(event.partitionId); + if (handler != null) { + handler.alarmEvent(event); + } + } + + @Override + public void armingEvent(ArmingEvent event) { + logger.debug("ArmingEvent {}", event.partitionId); + QolsysIQPartitionHandler handler = partitionHandler(event.partitionId); + if (handler != null) { + handler.armingEvent(event); + } + } + + @Override + public void errorEvent(ErrorEvent event) { + logger.debug("ErrorEvent {}", event.partitionId); + QolsysIQPartitionHandler handler = partitionHandler(event.partitionId); + if (handler != null) { + handler.errorEvent(event); + } + } + + @Override + public void summaryInfoEvent(SummaryInfoEvent event) { + logger.debug("SummaryInfoEvent"); + synchronized (partitions) { + partitions.clear(); + partitions.addAll(event.partitionList); + } + updatePartitions(); + discoverChildDevices(); + } + + @Override + public void secureArmInfoEvent(SecureArmInfoEvent event) { + logger.debug("ArmingEvent {}", event.value); + QolsysIQPartitionHandler handler = partitionHandler(event.partitionId); + if (handler != null) { + handler.secureArmInfoEvent(event); + } + } + + @Override + public void zoneActiveEvent(ZoneActiveEvent event) { + logger.debug("ZoneActiveEvent {} {}", event.zone.zoneId, event.zone.status); + partitions.forEach(p -> { + if (p.zoneList.stream().filter(z -> z.zoneId.equals(event.zone.zoneId)).findAny().isPresent()) { + QolsysIQPartitionHandler handler = partitionHandler(p.partitionId); + if (handler != null) { + handler.zoneActiveEvent(event); + } + } + }); + } + + @Override + public void zoneUpdateEvent(ZoneUpdateEvent event) { + logger.debug("ZoneUpdateEvent {}", event.zone.name); + partitions.forEach(p -> { + if (p.zoneList.stream().filter(z -> z.zoneId.equals(event.zone.zoneId)).findAny().isPresent()) { + QolsysIQPartitionHandler handler = partitionHandler(p.partitionId); + if (handler != null) { + handler.zoneUpdateEvent(event); + } + } + }); + } + + @Override + public void zoneAddEvent(ZoneAddEvent event) { + logger.debug("ZoneAddEvent {}", event.zone.name); + partitions.forEach(p -> { + if (p.zoneList.stream().filter(z -> z.zoneId.equals(event.zone.zoneId)).findAny().isPresent()) { + QolsysIQPartitionHandler handler = partitionHandler(p.partitionId); + if (handler != null) { + handler.zoneAddEvent(event); + } + } + }); + } + + /** + * Sends the action to the panel. This will replace the token of the action passed in with the one configured here + * + * @param action + */ + protected void sendAction(Action action) { + action.token = key; + QolsysiqClient client = this.apiClient; + if (client != null) { + try { + client.sendAction(action); + } catch (IOException e) { + logger.debug("Could not send action", e); + setOfflineAndReconnect(e, QUICK_RETRY_SECONDS); + } + } + } + + protected synchronized void refresh() { + sendAction(new InfoAction(InfoActionType.SUMMARY)); + } + + /** + * Connect the client + */ + private synchronized void connect() { + if (getThing().getStatus() == ThingStatus.ONLINE) { + logger.debug("connect: Bridge is already connected"); + return; + } + QolsysIQPanelConfiguration config = getConfigAs(QolsysIQPanelConfiguration.class); + key = config.key; + + try { + QolsysiqClient apiClient = new QolsysiqClient(config.hostname, config.port, HEARTBEAT_SECONDS, scheduler, + "OH-binding-" + getThing().getUID().getAsString()); + apiClient.connect(); + apiClient.addListener(this); + this.apiClient = apiClient; + refresh(); + updateStatus(ThingStatus.ONLINE); + } catch (IOException e) { + logger.debug("Could not connect"); + setOfflineAndReconnect(e, LONG_RETRY_SECONDS); + } + } + + /** + * Disconnects the client and removes listeners + */ + private void disconnect() { + logger.debug("disconnect"); + QolsysiqClient apiClient = this.apiClient; + if (apiClient != null) { + apiClient.removeListener(this); + apiClient.disconnect(); + this.apiClient = null; + } + } + + private void startRetryFuture(int seconds) { + stopRetryFuture(); + logger.debug("startRetryFuture"); + this.retryFuture = scheduler.schedule(this::connect, seconds, TimeUnit.SECONDS); + } + + private void stopRetryFuture() { + logger.debug("stopRetryFuture"); + ScheduledFuture retryFuture = this.retryFuture; + if (retryFuture != null) { + retryFuture.cancel(true); + this.retryFuture = null; + } + } + + private void setOfflineAndReconnect(Exception reason, int seconds) { + logger.debug("setOfflineAndReconnect"); + disconnect(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason.getMessage()); + startRetryFuture(seconds); + } + + private void updatePartitions() { + synchronized (partitions) { + partitions.forEach(p -> { + QolsysIQPartitionHandler handler = partitionHandler(p.partitionId); + if (handler != null) { + handler.updatePartition(p); + } + }); + } + } + + private void discoverChildDevices() { + synchronized (partitions) { + QolsysIQChildDiscoveryService discoveryService = this.discoveryService; + if (discoveryService != null) { + partitions.forEach(p -> { + ThingUID bridgeUID = getThing().getUID(); + ThingUID thingUID = new ThingUID(QolsysIQBindingConstants.THING_TYPE_PARTITION, bridgeUID, + String.valueOf(p.partitionId)); + discoveryService.discoverQolsysIQChildThing(thingUID, bridgeUID, p.partitionId, + "Qolsys IQ Partition: " + p.name); + }); + } + } + } + + private @Nullable QolsysIQPartitionHandler partitionHandler(int partitionId) { + for (Thing thing : getThing().getThings()) { + ThingHandler handler = thing.getHandler(); + if (handler instanceof QolsysIQPartitionHandler) { + if (((QolsysIQPartitionHandler) handler).getPartitionId() == partitionId) { + return (QolsysIQPartitionHandler) handler; + } + } + } + return null; + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQPartitionHandler.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQPartitionHandler.java new file mode 100644 index 00000000000..4dca6fde854 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQPartitionHandler.java @@ -0,0 +1,369 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.handler; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +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.qolsysiq.internal.QolsysIQBindingConstants; +import org.openhab.binding.qolsysiq.internal.client.dto.action.AlarmAction; +import org.openhab.binding.qolsysiq.internal.client.dto.action.AlarmActionType; +import org.openhab.binding.qolsysiq.internal.client.dto.action.ArmingAction; +import org.openhab.binding.qolsysiq.internal.client.dto.action.ArmingActionType; +import org.openhab.binding.qolsysiq.internal.client.dto.event.AlarmEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ArmingEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ErrorEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.SecureArmInfoEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneActiveEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneAddEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneUpdateEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.model.AlarmType; +import org.openhab.binding.qolsysiq.internal.client.dto.model.Partition; +import org.openhab.binding.qolsysiq.internal.client.dto.model.PartitionStatus; +import org.openhab.binding.qolsysiq.internal.client.dto.model.Zone; +import org.openhab.binding.qolsysiq.internal.config.QolsysIQPartitionConfiguration; +import org.openhab.binding.qolsysiq.internal.discovery.QolsysIQChildDiscoveryService; +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.ThingStatusInfo; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QolsysIQPartitionHandler} manages security partitions + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class QolsysIQPartitionHandler extends BaseBridgeHandler implements QolsysIQChildDiscoveryHandler { + private final Logger logger = LoggerFactory.getLogger(QolsysIQPartitionHandler.class); + private static final int CLEAR_ERROR_MESSSAGE_TIME = 30; + private @Nullable QolsysIQChildDiscoveryService discoveryService; + private @Nullable ScheduledFuture delayFuture; + private @Nullable ScheduledFuture errorFuture; + private @Nullable String armCode; + private @Nullable String disarmCode; + private List zones = Collections.synchronizedList(new LinkedList()); + private int partitionId; + + public QolsysIQPartitionHandler(Bridge bridge) { + super(bridge); + } + + @Override + public void initialize() { + QolsysIQPartitionConfiguration config = getConfigAs(QolsysIQPartitionConfiguration.class); + partitionId = config.id; + armCode = config.armCode.isBlank() ? null : config.armCode; + disarmCode = config.disarmCode.isBlank() ? null : config.disarmCode; + logger.debug("initialize partition {}", partitionId); + initializePartition(); + } + + @Override + public void dispose() { + cancelExitDelayJob(); + cancelErrorDelayJob(); + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) { + cancelExitDelayJob(); + cancelErrorDelayJob(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } else { + initializePartition(); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + refresh(); + return; + } + + QolsysIQPanelHandler panel = panelHandler(); + if (panel != null) { + if (channelUID.getId().equals(QolsysIQBindingConstants.CHANNEL_PARTITION_ALARM_STATE)) { + try { + panel.sendAction(new AlarmAction(AlarmActionType.valueOf(command.toString()))); + } catch (IllegalArgumentException e) { + logger.debug("Unknown alarm type {} to channel {}", command, channelUID); + } + return; + } + + // support ARM_AWAY and ARM_AWAY:123456 , same for other arm / disarm modes + if (channelUID.getId().equals(QolsysIQBindingConstants.CHANNEL_PARTITION_ARM_STATE)) { + String armingTypeName = command.toString(); + String code = null; + if (armingTypeName.contains(":")) { + String[] split = armingTypeName.split(":"); + armingTypeName = split[0]; + if (split.length > 1 && split[1].length() > 0) { + code = split[1]; + } + } + try { + ArmingActionType armingType = ArmingActionType.valueOf(armingTypeName); + if (code == null) { + if (armingType == ArmingActionType.DISARM) { + code = disarmCode; + } else { + code = armCode; + } + } + panel.sendAction(new ArmingAction(armingType, getPartitionId(), code)); + } catch (IllegalArgumentException e) { + logger.debug("Unknown arm type {} to channel {}", armingTypeName, channelUID); + } + } + } + } + + @Override + public Collection> getServices() { + return Collections.singleton(QolsysIQChildDiscoveryService.class); + } + + @Override + public void setDiscoveryService(QolsysIQChildDiscoveryService service) { + this.discoveryService = service; + } + + @Override + public void startDiscovery() { + refresh(); + } + + /** + * The partition id + * + * @return + */ + public int getPartitionId() { + return partitionId; + } + + public void zoneActiveEvent(ZoneActiveEvent event) { + QolsysIQZoneHandler handler = zoneHandler(event.zone.zoneId); + if (handler != null) { + handler.zoneActiveEvent(event); + } + } + + public void zoneUpdateEvent(ZoneUpdateEvent event) { + QolsysIQZoneHandler handler = zoneHandler(event.zone.zoneId); + if (handler != null) { + handler.zoneUpdateEvent(event); + } + } + + protected void alarmEvent(AlarmEvent event) { + if (event.alarmType != AlarmType.NONE && event.alarmType != AlarmType.ZONEOPEN) { + updatePartitionStatus(PartitionStatus.ALARM); + } + updateAlarmState(event.alarmType); + } + + protected void armingEvent(ArmingEvent event) { + updatePartitionStatus(event.armingType); + updateDelay(event.delay == null ? 0 : event.delay); + } + + protected void errorEvent(ErrorEvent event) { + cancelErrorDelayJob(); + updateState(QolsysIQBindingConstants.CHANNEL_PARTITION_ERROR_EVENT, new StringType(event.description)); + errorFuture = scheduler.schedule(this::clearErrorEvent, CLEAR_ERROR_MESSSAGE_TIME, TimeUnit.SECONDS); + } + + protected void secureArmInfoEvent(SecureArmInfoEvent event) { + setSecureArm(event.value); + } + + public void zoneAddEvent(ZoneAddEvent event) { + discoverZone(event.zone); + } + + protected void updatePartition(Partition partition) { + updatePartitionStatus(partition.status); + setSecureArm(partition.secureArm); + if (partition.status != PartitionStatus.ALARM) { + updateAlarmState(AlarmType.NONE); + } + synchronized (zones) { + zones.clear(); + zones.addAll(partition.zoneList); + zones.forEach(z -> { + QolsysIQZoneHandler zoneHandler = zoneHandler(z.zoneId); + if (zoneHandler != null) { + zoneHandler.updateZone(z); + } + }); + } + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + discoverChildDevices(); + } + + protected @Nullable Zone getZone(Integer zoneId) { + synchronized (zones) { + return zones.stream().filter(z -> z.zoneId.equals(zoneId)).findAny().orElse(null); + } + } + + private void initializePartition() { + QolsysIQPanelHandler panel = panelHandler(); + if (panel == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED); + } else if (panel.getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } else { + updateStatus(ThingStatus.UNKNOWN); + scheduler.execute(() -> { + panel.refresh(); + }); + } + } + + private void refresh() { + QolsysIQPanelHandler panel = panelHandler(); + if (panel != null) { + panel.refresh(); + } + } + + private void updatePartitionStatus(PartitionStatus status) { + updateState(QolsysIQBindingConstants.CHANNEL_PARTITION_ARM_STATE, new StringType(status.toString())); + cancelErrorDelayJob(); + if (status == PartitionStatus.DISARM) { + updateAlarmState(AlarmType.NONE); + updateDelay(0); + } + } + + private void setSecureArm(Boolean secure) { + Map props = new HashMap(); + props.put("secureArm", String.valueOf(secure)); + getThing().setProperties(props); + } + + private void updateDelay(Integer delay) { + cancelExitDelayJob(); + if (delay <= 0) { + updateState(QolsysIQBindingConstants.CHANNEL_PARTITION_COMMAND_DELAY, new DecimalType(0)); + return; + } + + final long endTime = System.currentTimeMillis() + (delay * 1000); + delayFuture = scheduler.scheduleAtFixedRate(() -> { + long remaining = endTime - System.currentTimeMillis(); + logger.debug("updateDelay remaining {}", remaining / 1000); + if (remaining <= 0) { + cancelExitDelayJob(); + } else { + updateState(QolsysIQBindingConstants.CHANNEL_PARTITION_COMMAND_DELAY, + new DecimalType(remaining / 1000)); + } + }, 1, 1, TimeUnit.SECONDS); + } + + private void updateAlarmState(AlarmType alarmType) { + updateState(QolsysIQBindingConstants.CHANNEL_PARTITION_ALARM_STATE, new StringType(alarmType.toString())); + } + + private void clearErrorEvent() { + updateState(QolsysIQBindingConstants.CHANNEL_PARTITION_ERROR_EVENT, UnDefType.NULL); + } + + private void cancelExitDelayJob() { + ScheduledFuture delayFuture = this.delayFuture; + if (delayFuture != null) { + delayFuture.cancel(true); + this.delayFuture = null; + } + updateState(QolsysIQBindingConstants.CHANNEL_PARTITION_COMMAND_DELAY, new DecimalType(0)); + } + + private void cancelErrorDelayJob() { + ScheduledFuture errorFuture = this.errorFuture; + if (errorFuture != null) { + errorFuture.cancel(true); + this.errorFuture = null; + } + clearErrorEvent(); + } + + private void discoverChildDevices() { + synchronized (zones) { + zones.forEach(z -> discoverZone(z)); + } + } + + private void discoverZone(Zone z) { + QolsysIQChildDiscoveryService discoveryService = this.discoveryService; + if (discoveryService != null) { + ThingUID bridgeUID = getThing().getUID(); + ThingUID thingUID = new ThingUID(QolsysIQBindingConstants.THING_TYPE_ZONE, bridgeUID, + String.valueOf(z.zoneId)); + discoveryService.discoverQolsysIQChildThing(thingUID, bridgeUID, z.zoneId, "Qolsys IQ Zone: " + z.name); + } + } + + private @Nullable QolsysIQZoneHandler zoneHandler(int zoneId) { + for (Thing thing : getThing().getThings()) { + ThingHandler handler = thing.getHandler(); + if (handler instanceof QolsysIQZoneHandler) { + if (((QolsysIQZoneHandler) handler).getZoneId() == zoneId) { + return (QolsysIQZoneHandler) handler; + } + } + } + return null; + } + + private @Nullable QolsysIQPanelHandler panelHandler() { + Bridge bridge = getBridge(); + if (bridge != null) { + BridgeHandler handler = bridge.getHandler(); + if (handler instanceof QolsysIQPanelHandler) { + return (QolsysIQPanelHandler) handler; + } + } + return null; + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQZoneHandler.java b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQZoneHandler.java new file mode 100644 index 00000000000..fbcc4420945 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/java/org/openhab/binding/qolsysiq/internal/handler/QolsysIQZoneHandler.java @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2010-2022 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.qolsysiq.internal.handler; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qolsysiq.internal.QolsysIQBindingConstants; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneActiveEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.event.ZoneUpdateEvent; +import org.openhab.binding.qolsysiq.internal.client.dto.model.Zone; +import org.openhab.binding.qolsysiq.internal.client.dto.model.ZoneStatus; +import org.openhab.binding.qolsysiq.internal.config.QolsysIQZoneConfiguration; +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.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.ThingStatusInfo; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QolsysIQZoneHandler} manages security zones. + * + * @author Dan Cunningham - Initial contribution + */ +@NonNullByDefault +public class QolsysIQZoneHandler extends BaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(QolsysIQZoneHandler.class); + + private int zoneId; + + public QolsysIQZoneHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + logger.debug("initialize"); + zoneId = getConfigAs(QolsysIQZoneConfiguration.class).id; + initializeZone(); + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusChanged) { + logger.debug("bridgeStatusChanged {}", bridgeStatusChanged); + initializeZone(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + public int getZoneId() { + return zoneId; + } + + protected void updateZone(Zone zone) { + logger.debug("updateZone {}", zone.zoneId); + updateState(QolsysIQBindingConstants.CHANNEL_ZONE_STATE, new DecimalType(zone.state)); + updateZoneStatus(zone.status); + Map props = new HashMap(); + props.put("type", zone.type); + props.put("name", zone.name); + props.put("group", zone.group); + props.put("zoneID", zone.id); + props.put("zonePhysicalType", String.valueOf(zone.zonePhysicalType)); + props.put("zoneAlarmType", String.valueOf(zone.zoneAlarmType)); + props.put("zoneType", zone.zoneType.toString()); + props.put("partitionId", String.valueOf(zone.partitionId)); + getThing().setProperties(props); + } + + protected void zoneActiveEvent(ZoneActiveEvent event) { + if (event.zone.zoneId == getZoneId()) { + updateZoneStatus(event.zone.status); + } + } + + protected void zoneUpdateEvent(ZoneUpdateEvent event) { + if (event.zone.zoneId == getZoneId()) { + updateZone(event.zone); + } + } + + private void initializeZone() { + Bridge bridge = getBridge(); + BridgeHandler handler = bridge == null ? null : bridge.getHandler(); + if (bridge != null && handler instanceof QolsysIQPartitionHandler) { + if (handler.getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + return; + } + Zone z = ((QolsysIQPartitionHandler) handler).getZone(getZoneId()); + if (z == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Zone not found in partition"); + return; + } + updateZone(z); + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED); + } + } + + private void updateZoneStatus(@Nullable ZoneStatus status) { + if (status != null) { + updateState(QolsysIQBindingConstants.CHANNEL_ZONE_STATUS, new StringType(status.toString())); + updateState(QolsysIQBindingConstants.CHANNEL_ZONE_CONTACT, + status == ZoneStatus.CLOSED || status == ZoneStatus.IDlE ? OpenClosedType.CLOSED + : OpenClosedType.OPEN); + } else { + logger.debug("updateZoneStatus: null status"); + } + } +} diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 00000000000..1b734542c64 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + QolsysIQ Binding + This is the binding for Qolsys IQ Alarm Systems. + + diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/i18n/qolsysiq.properties b/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/i18n/qolsysiq.properties new file mode 100644 index 00000000000..ad5424eb206 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/i18n/qolsysiq.properties @@ -0,0 +1,72 @@ +## mvn i18n:generate-default-translations + +# binding + +binding.qolsysiq.name = QolsysIQ Binding +binding.qolsysiq.description = This is the binding for Qolsys IQ Alarm Systems. + +# thing types + +thing-type.qolsysiq.panel.label = Qolsys IQ Panel +thing-type.qolsysiq.panel.description = A Qolsys IQ Panel Bridge +thing-type.qolsysiq.partition.label = Partition +thing-type.qolsysiq.partition.description = A Qolsys IQ Partition +thing-type.qolsysiq.zone.label = Zone +thing-type.qolsysiq.zone.description = A Qolsys IQ Zone + +# thing types config + +thing-type.config.qolsysiq.panel.hostname.label = Hostname +thing-type.config.qolsysiq.panel.hostname.description = Hostname or IP address of the panel +thing-type.config.qolsysiq.panel.key.label = key +thing-type.config.qolsysiq.panel.key.description = Key to access the device +thing-type.config.qolsysiq.panel.port.label = Port +thing-type.config.qolsysiq.panel.port.description = The port to connect to on the panel. +thing-type.config.qolsysiq.partition.armCode.label = Arm Code +thing-type.config.qolsysiq.partition.armCode.description = Optional arm code to use when receiving arm commands without a code. Only required if the panel has been configured to require arm codes. Leave blank to always require a code +thing-type.config.qolsysiq.partition.disarmCode.label = Disarm Code +thing-type.config.qolsysiq.partition.disarmCode.description = Optional disarm code to use when receiving a disarm command without a code. Required for integrations like Alexa and Homekit who do not provide codes when disarming. Leave blank to always require a code +thing-type.config.qolsysiq.partition.id.label = Partition ID +thing-type.config.qolsysiq.partition.id.description = The Partition ID. +thing-type.config.qolsysiq.zone.id.label = Zone ID +thing-type.config.qolsysiq.zone.id.description = The Zone ID. + +# channel types + +channel-type.qolsysiq.alarmState.label = Partition Alarm State +channel-type.qolsysiq.alarmState.description = Reports on the current alarm state, or triggers an instant alarm. +channel-type.qolsysiq.alarmState.state.option.AUXILIARY = Auxiliary +channel-type.qolsysiq.alarmState.state.option.FIRE = Fire +channel-type.qolsysiq.alarmState.state.option.POLICE = Police +channel-type.qolsysiq.alarmState.state.option.ZONEOPEN = Zone Open +channel-type.qolsysiq.alarmState.state.option.NONE = None +channel-type.qolsysiq.alarmState.command.option.AUXILIARY = Auxiliary +channel-type.qolsysiq.alarmState.command.option.FIRE = Fire +channel-type.qolsysiq.alarmState.command.option.POLICE = Police +channel-type.qolsysiq.armState.label = Partition Arm State +channel-type.qolsysiq.armState.description = Reports the current partition arm state or sends a arm or disarm command to the system. For security codes, append the 6 digit code to the command separated by a colon (e.g. 'DISARM:123456') +channel-type.qolsysiq.armState.state.option.ALARM = In Alarm +channel-type.qolsysiq.armState.state.option.ARM_AWAY = Armed Away +channel-type.qolsysiq.armState.state.option.ARM_STAY = Armed Stay +channel-type.qolsysiq.armState.state.option.DISARM = Disarmed +channel-type.qolsysiq.armState.state.option.ENTRY_DELAY = Entry Delay +channel-type.qolsysiq.armState.state.option.EXIT_DELAY = Exit Delay +channel-type.qolsysiq.armState.command.option.ARM_AWAY = Arm Away +channel-type.qolsysiq.armState.command.option.ARM_STAY = Arm Stay +channel-type.qolsysiq.armState.command.option.DISARM = Disarm +channel-type.qolsysiq.armingDelay.label = Partition Arming Delay +channel-type.qolsysiq.armingDelay.description = The arming delay currently in progress +channel-type.qolsysiq.contact.label = Zone Contact +channel-type.qolsysiq.contact.description = The zone contact state. +channel-type.qolsysiq.errorEvent.label = Error Event +channel-type.qolsysiq.errorEvent.description = Last error event message reported by the partition. Clears after 30 seconds +channel-type.qolsysiq.zoneState.label = Zone State +channel-type.qolsysiq.zoneState.description = The zone state. +channel-type.qolsysiq.zoneStatus.label = Zone Status +channel-type.qolsysiq.zoneStatus.description = The zone status. +channel-type.qolsysiq.zoneStatus.state.option.ACTIVE = Active +channel-type.qolsysiq.zoneStatus.state.option.CLOSED = Closed +channel-type.qolsysiq.zoneStatus.state.option.OPEN = Open +channel-type.qolsysiq.zoneStatus.state.option.FAILURE = Failure +channel-type.qolsysiq.zoneStatus.state.option.IDlE = Idle +channel-type.qolsysiq.zoneStatus.state.option.TAMPER = Tamper diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/panel.xml b/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/panel.xml new file mode 100644 index 00000000000..65ba24377b4 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/panel.xml @@ -0,0 +1,28 @@ + + + + + A Qolsys IQ Panel Bridge + + + network-address + + Hostname or IP address of the panel + + + + The port to connect to on the panel. + 12345 + true + + + password + + Key to access the device + + + + diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/partition.xml b/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/partition.xml new file mode 100644 index 00000000000..cb56226e544 --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/partition.xml @@ -0,0 +1,103 @@ + + + + + + + + A Qolsys IQ Partition + + + + + + + + false + + id + + + + The Partition ID. + + + + + Optional disarm code to use when receiving a disarm command without a code. Required for integrations + like Alexa and Homekit who do not provide codes when disarming. Leave blank to always require a code + + + + + Optional arm code to use when receiving arm commands without a code. Only required if the panel has + been configured to require arm codes. Leave blank to always require a code + true + + + + + String + + Reports the current partition arm state or sends a arm or disarm command to the system. For security + codes, append the 6 digit code to the command separated by a colon (e.g. 'DISARM:123456') + Alarm + + + + + + + + + + + + + + + + + + veto + + + String + + Reports on the current alarm state, or triggers an instant alarm. + Alarm + + + + + + + + + + + + + + + + + veto + + + Number + + The arming delay currently in progress + Alarm + + + + String + + Last error event message reported by the partition. Clears after 30 seconds + + + diff --git a/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/zone.xml b/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/zone.xml new file mode 100644 index 00000000000..b53a9d5e76f --- /dev/null +++ b/bundles/org.openhab.binding.qolsysiq/src/main/resources/OH-INF/thing/zone.xml @@ -0,0 +1,63 @@ + + + + + + + + A Qolsys IQ Zone + + + + + + + + + + + + + + + + id + + + + The Zone ID. + + + + + String + + The zone status. + + + + + + + + + + + + + Number + + The zone state. + + + + + Contact + + The zone contact state. + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 316b6a9ad2d..08386b72ddf 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -305,6 +305,7 @@ org.openhab.binding.pushover org.openhab.binding.pushsafer org.openhab.binding.qbus + org.openhab.binding.qolsysiq org.openhab.binding.radiothermostat org.openhab.binding.regoheatpump org.openhab.binding.revogi