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 1d0ebb237..2cd894f96 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 d5178fa3b..eb7f02572 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 000000000..38d625e34 --- /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 000000000..7be0e7fe2 --- /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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 + + + 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 000000000..b02bdd6f5 --- /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 000000000..4028893ed --- /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 000000000..ae1edf8b3 --- /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 000000000..61e7566ef --- /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 000000000..ae7389628 --- /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 000000000..2f7142798 --- /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 000000000..af9184ac2 --- /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 000000000..b2b6e1940 --- /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 000000000..dc8c57721 --- /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 000000000..3149b577a --- /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 000000000..fdf0bb88b --- /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 000000000..9951b6818 --- /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 000000000..4d1e6a5ad --- /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 000000000..a62a7a391 --- /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 000000000..0570e8dcc --- /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 000000000..42bc3c0af --- /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 000000000..1c04abf9e --- /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 000000000..77bc1daa4 --- /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 000000000..ba2621d93 --- /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 000000000..775a2f080 --- /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 000000000..b6afce9fe --- /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 000000000..dee4d3025 --- /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 000000000..f80b854a3 --- /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 000000000..1b7d74971 --- /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 000000000..999b8a445 --- /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 000000000..26599f216 --- /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 000000000..d40a5be56 --- /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 000000000..b01ca0969 --- /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 000000000..953584677 --- /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 000000000..fc02d71b9 --- /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 000000000..cc38eddab --- /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 000000000..39010306c --- /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 000000000..4a05d525c --- /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 000000000..f37a44119 --- /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 000000000..e78f838b4 --- /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 000000000..8d09f3b37 --- /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 000000000..06107dc32 --- /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 000000000..14800fe15 --- /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 000000000..796c6c480 --- /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 000000000..f9b818fc0 --- /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 000000000..c0c736f0f --- /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 000000000..4dca6fde8 --- /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 000000000..fbcc44209 --- /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 000000000..1b734542c --- /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 000000000..ad5424eb2 --- /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 000000000..65ba24377 --- /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 000000000..cb56226e5 --- /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 000000000..b53a9d5e7 --- /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 316b6a9ad..08386b72d 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