Repository: craigjmidwinter/total-connect-client

Issue:
Honeywell ProA7Plus Errors
I got some errors in my log, looking at the times and the zones these areas are from I believe they are codes for Zone Tamper and Arming Away with a bypass. Attached are the errors. Let me know if you need any specific information and I can get it for you. 

unknown partition ArmingState 10231
[Arming Away with a bypass.txt](https://github.com/craigjmidwinter/total-connect-client/files/8019223/Arming.Away.with.a.bypass.txt)

and

unknown ZoneStatus 16
[zone in tamper state.txt](https://github.com/craigjmidwinter/total-connect-client/files/8019222/zone.in.tamper.state.txt)

Discussion:

Patch:
diff --git a/ALARM_STATUS.md b/ALARM_STATUS.md
index b0d9b90..1e35f93 100644
--- a/ALARM_STATUS.md
+++ b/ALARM_STATUS.md
@@ -22,8 +22,15 @@ Code | Status
 10213 | Alarming Carbon Monoxide
 10214 | Disarmed not ready to arm - zone(s) faulted
 10218 | Armed Stay Night
+10219 | Armed Stay Night Bypass.  Reported by ProA7 panel.
+10220 | Armed Stay Night Instant.  Reported by ProA7 panel.
+10221 | Armed Stay Night Instant Bypass.  Reported by ProA7 panel.
 10223 | Armed custom bypass
 10226 | Armed Stay.  Suddenly reported for Lynx Touch 7000 in November 2021, which previously reported as 10203
+10230 | Armed Stay.  Reported by ProA7 panel.
+10231 | Armed Stay Bypass. Reported by ProA7 panel.
+10232 | Armed Stay Instant. Reported by ProA7 panel.
+10233 | Armed Stay Instant Bypass. Reported by ProA7 panel.
 10307 | Arming
 10308 | Disarming
 
@@ -39,6 +46,7 @@ Code | Status
 2 | Fault
 8 | Trouble/Tampered
 10 | Fault + Trouble
+16 | Tamper (ProA7)
 64 | Low Battery
 65 | Low Battery + Bypassed
 72 | Trouble + Low Battery
@@ -54,7 +62,7 @@ Bit | Integer | Status
 2 | 2 | Fault
 3 | 4 | ???
 4 | 8 | Tamper/Trouble
-5 | 16 | ???
+5 | 16 | Tamper (on ProA7 see #176) 
 6 | 32 | ???
 7 | 64 | Low Battery
 8 | 128 | ???
diff --git a/setup.py b/setup.py
index 1563797..fe88fb9 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ from setuptools import setup
 
 setup(
     name="total_connect_client",
-    version="2022.1",
+    version="2022.2",
     description="Interact with Total Connect 2 alarm systems",
     author="Craig J. Midwinter",
     author_email="craig.j.midwinter@gmail.com",
diff --git a/total_connect_client/const.py b/total_connect_client/const.py
index ea43e74..70398aa 100644
--- a/total_connect_client/const.py
+++ b/total_connect_client/const.py
@@ -16,19 +16,27 @@ class ArmType(Enum):
 class ArmingState(Enum):
     DISARMED = 10200
     DISARMED_BYPASS = 10211
-    DISARMED_ZONE_FAULTED = 10214  # seems to apply to location, not to partition.  See issue #144
+    DISARMED_ZONE_FAULTED = (
+        10214  # seems to apply to location, not to partition.  See issue #144
+    )
 
     ARMED_AWAY = 10201
     ARMED_AWAY_BYPASS = 10202
     ARMED_STAY = 10203
     ARMED_STAY_OTHER = 10226
-    ARMED_STAY_10230 = 10230    # issue #173
+    ARMED_STAY_PROA7 = 10230  # issue #173
+    ARMED_STAY_BYPASS_PROA7 = 10231  # issue #177
+    ARMED_STAY_INSTANT_PROA7 = 10232  # issue #177
+    ARMED_STAY_INSTANT_BYPASS_PROA7 = 10233  # issue #177
     ARMED_STAY_BYPASS = 10204
     ARMED_AWAY_INSTANT = 10205
     ARMED_AWAY_INSTANT_BYPASS = 10206
     ARMED_STAY_INSTANT = 10209
     ARMED_STAY_INSTANT_BYPASS = 10210
     ARMED_STAY_NIGHT = 10218
+    ARMED_STAY_NIGHT_BYPASS_PROA7 = 10219  # issue #177
+    ARMED_STAY_NIGHT_INSTANT_PROA7 = 10220  # issue #177
+    ARMED_STAY_NIGHT_INSTANT_BYPASS_PROA7 = 10221  # issue #177
     ARMED_CUSTOM_BYPASS = 10223
 
     ALARMING = 10207
@@ -53,7 +61,9 @@ class ArmingState(Enum):
     def is_disarmed(self):
         """Return True if the system is disarmed."""
         return self in (
-            ArmingState.DISARMED, ArmingState.DISARMED_BYPASS, ArmingState.DISARMED_ZONE_FAULTED
+            ArmingState.DISARMED,
+            ArmingState.DISARMED_BYPASS,
+            ArmingState.DISARMED_ZONE_FAULTED,
         )
 
     def is_armed_away(self):
@@ -73,23 +83,36 @@ class ArmingState(Enum):
         """Return True if the system is armed home/stay in any way."""
         return self in (
             ArmingState.ARMED_STAY,
+            ArmingState.ARMED_STAY_PROA7,
             ArmingState.ARMED_STAY_BYPASS,
+            ArmingState.ARMED_STAY_BYPASS_PROA7,
             ArmingState.ARMED_STAY_INSTANT,
+            ArmingState.ARMED_STAY_INSTANT_PROA7,
             ArmingState.ARMED_STAY_INSTANT_BYPASS,
+            ArmingState.ARMED_STAY_INSTANT_BYPASS_PROA7,
             ArmingState.ARMED_STAY_NIGHT,
+            ArmingState.ARMED_STAY_NIGHT_BYPASS_PROA7,
+            ArmingState.ARMED_STAY_NIGHT_INSTANT_PROA7,
+            ArmingState.ARMED_STAY_NIGHT_INSTANT_BYPASS_PROA7,
             ArmingState.ARMED_STAY_OTHER,
-            ArmingState.ARMED_STAY_10230,
         )
 
     def is_armed_night(self):
         """Return True if the system is armed night in any way."""
-        return self == ArmingState.ARMED_STAY_NIGHT
+        return self in (
+            ArmingState.ARMED_STAY_NIGHT,
+            ArmingState.ARMED_STAY_NIGHT_BYPASS_PROA7,
+            ArmingState.ARMED_STAY_NIGHT_INSTANT_PROA7,
+            ArmingState.ARMED_STAY_NIGHT_INSTANT_BYPASS_PROA7,
+        )
 
     def is_armed(self):
         """Return True if the system is armed in any way."""
         return (
-            self.is_armed_away() or self.is_armed_home() or  # noqa: W504
-            self.is_armed_night() or self.is_armed_custom_bypass()
+            self.is_armed_away()
+            or self.is_armed_home()
+            or self.is_armed_night()  # noqa: W504
+            or self.is_armed_custom_bypass()
         )
 
     def is_triggered_police(self):
@@ -107,7 +130,9 @@ class ArmingState(Enum):
     def is_triggered(self):
         """Return True if the system is triggered in any way."""
         return (
-            self.is_triggered_fire() or self.is_triggered_gas() or self.is_triggered_police()
+            self.is_triggered_fire()
+            or self.is_triggered_gas()
+            or self.is_triggered_police()
         )
 
 
diff --git a/total_connect_client/zone.py b/total_connect_client/zone.py
index fccee95..fd3a51f 100644
--- a/total_connect_client/zone.py
+++ b/total_connect_client/zone.py
@@ -14,9 +14,10 @@ class ZoneStatus(IntFlag):
     BYPASSED = 1
     FAULT = 2
     TROUBLE = 8  # is also Tampered
+    TAMPER = 16 # Tamper for ProA7, see #176
     LOW_BATTERY = 64
     TRIGGERED = 256
-    KNOWN = NORMAL | BYPASSED | FAULT | TROUBLE | LOW_BATTERY | TRIGGERED
+    KNOWN = NORMAL | BYPASSED | FAULT | TROUBLE | TAMPER | LOW_BATTERY | TRIGGERED
 
 
 class ZoneType(Enum):
@@ -115,7 +116,7 @@ class TotalConnectZone:
 
     def is_tampered(self):
         """Return true if zone is tampered."""
-        return self.status & ZoneStatus.TROUBLE > 0
+        return (self.status & ZoneStatus.TROUBLE > 0) or (self.status & ZoneStatus.TAMPER > 0)
 
     def is_low_battery(self):
         """Return true if low battery."""


Test patch:
diff --git a/tests/const.py b/tests/const.py
index 0bb1d3f..b5c1371 100644
--- a/tests/const.py
+++ b/tests/const.py
@@ -135,7 +135,7 @@ PARTITION_ARMED_AWAY = {
     "ArmingState": ArmingState.ARMED_AWAY.value,
 }
 
-PARTITION_ARMED_STAY_10230 = {
+PARTITION_ARMED_STAY_PROA7 = {
     "PartitionID": 1,
     "ArmingState": 10230,
     "PartitionName": "Test_10230",
diff --git a/tests/test_partition.py b/tests/test_partition.py
index b2dfcb8..f23c6b7 100644
--- a/tests/test_partition.py
+++ b/tests/test_partition.py
@@ -4,9 +4,10 @@ from copy import deepcopy
 from unittest.mock import Mock
 
 import pytest
-from const import PARTITION_ARMED_STAY_10230, PARTITION_DETAILS_1, PARTITION_DISARMED
+from const import PARTITION_DETAILS_1, PARTITION_DISARMED
 
 from total_connect_client.client import ArmingHelper
+from total_connect_client.const import ArmingState
 from total_connect_client.exceptions import PartialResponseError, TotalConnectError
 from total_connect_client.partition import TotalConnectPartition
 
@@ -78,7 +79,40 @@ def tests_arming_state():
     assert partition.arming_state.is_triggered_gas() is False
     assert partition.arming_state.is_triggered() is False
 
-    # test recreates issue #173
-    partition = TotalConnectPartition(PARTITION_ARMED_STAY_10230, None)
-    assert partition.arming_state.is_armed_home() is True
-    
\ No newline at end of file
+
+def tests_proa7_states():
+    """Test ProA7 status functions."""
+
+    partition_info = {
+        "PartitionID": 1,
+        "PartitionName": "Test1",
+    }
+
+    armed_stay = [
+        ArmingState.ARMED_STAY_PROA7,
+        ArmingState.ARMED_STAY_BYPASS_PROA7,
+        ArmingState.ARMED_STAY_INSTANT_PROA7,
+        ArmingState.ARMED_STAY_INSTANT_BYPASS_PROA7
+    ]
+
+    armed_night = [
+        ArmingState.ARMED_STAY_NIGHT_BYPASS_PROA7,
+        ArmingState.ARMED_STAY_NIGHT_INSTANT_PROA7,
+        ArmingState.ARMED_STAY_NIGHT_INSTANT_BYPASS_PROA7,
+    ]
+
+    for state in armed_stay:
+        partition_info["ArmingState"] = state
+        partition = TotalConnectPartition(partition_info, None)
+        assert partition.arming_state.is_armed() is True
+        assert partition.arming_state.is_armed_home() is True
+        assert partition.arming_state.is_armed_night() is False
+        assert partition.arming_state.is_armed_away() is False
+
+    for state in armed_night:
+        partition_info["ArmingState"] = state
+        partition = TotalConnectPartition(partition_info, None)
+        assert partition.arming_state.is_armed() is True
+        assert partition.arming_state.is_armed_home() is True
+        assert partition.arming_state.is_armed_night() is True
+        assert partition.arming_state.is_armed_away() is False
diff --git a/tests/test_zone.py b/tests/test_zone.py
index 83efe9e..96f27d5 100644
--- a/tests/test_zone.py
+++ b/tests/test_zone.py
@@ -315,12 +315,13 @@ def test_proa7_zones():
         "PartitionId": "1",
         "ZoneTypeId": ZoneType.PROA7_MEDICAL,
         "CanBeBypassed": 0,
-        "ZoneStatus": ZoneStatus.NORMAL,
+        "ZoneStatus": ZoneStatus.TAMPER,
     }
 
     zone = tcz(zone_medical)
     assert zone.is_type_medical() is True
     assert zone.is_type_button() is True
+    assert zone.is_tampered() is True
 
 def test_unknown_type():
     """Test unknown ZoneType."""