Repository: craigjmidwinter/total-connect-client

Issue:
Error - unknown ZoneType 53
Getting the following error over and over again in the log - 

2021-11-17 00:09:37 ERROR (SyncWorker_5) [total_connect_client.zone] unknown ZoneType 53 in OrderedDict([('AlarmTriggerTime', None), ('AlarmTriggerTimeLocalized', None), ('ZoneTypeID', 53), ('DeviceType', 13), ('ZoneID', 7), ('ZoneDescription', 'South Garage GarageDoor'), ('ZoneStatus', 0), ('PartitionID', 1), ('CanBeBypassed', 0)]): please report at https://github.com/craigjmidwinter/total-connect-client/issues
2021-11-17 00:10:07 ERROR (SyncWorker_6) [total_connect_client.zone] unknown ZoneType 53 in OrderedDict([('AlarmTriggerTime', None), ('AlarmTriggerTimeLocalized', None), ('ZoneTypeID', 53), ('DeviceType', 13), ('ZoneID', 6), ('ZoneDescription', 'North Garage GarageDoor'), ('ZoneStatus', 0), ('PartitionID', 1), ('CanBeBypassed', 0)]): please report at https://github.com/craigjmidwinter/total-connect-client/issues

Discussion:

Patch:
diff --git a/ZONE_TYPES.md b/ZONE_TYPES.md
index 4f9115b..c901f91 100644
--- a/ZONE_TYPES.md
+++ b/ZONE_TYPES.md
@@ -25,6 +25,7 @@ Zone Type # | Type | Notes
 23 | No Alarm Response | Per Vista docs
 24 | Silent Burglary | Per Vista docs
 50 | Keypad | Lyric Keypad
+53 | Garage Monitor | reported by ProA7 with SiX C2W
 77 | Keyswitch | Per Vista20P docs
 81 | AAV Monitor | Tells that audio session in progress
 89 | tbd | Lyric "local alarm"
diff --git a/setup.py b/setup.py
index d78a8bf..3e1601d 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
 
 setup(
     name="total_connect_client",
-    version="2021.11.4",
+    version="2021.11.5",
     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/arm.py b/total_connect_client/arm.py
new file mode 100644
index 0000000..e9b93f6
--- /dev/null
+++ b/total_connect_client/arm.py
@@ -0,0 +1,37 @@
+"""Test your system from the command line."""
+
+import getpass
+import logging
+import sys
+
+from .const import ArmType
+from .client import TotalConnectClient
+
+logging.basicConfig(filename="test.log", level=logging.DEBUG)
+
+def usage():
+    """Print usage instructions."""
+    print("usage:  username location1=usercode1,location2=usercode2 arm_type \n")
+    print("arm types:")
+    for i in list(ArmType):
+        print(f"  {i.name}: {i.value}")
+    sys.exit()
+    
+
+if len(sys.argv) != 4:
+    usage()
+
+USERNAME = sys.argv[1]
+USERCODES = dict(x.split("=") for x in sys.argv[2].split(","))
+arm_type = ArmType(int(sys.argv[3]))
+PASSWORD = getpass.getpass()
+
+TC = TotalConnectClient(USERNAME, PASSWORD, USERCODES)
+
+for location_id in TC.locations:
+    TC.locations[location_id].arm(arm_type)
+
+print(f"Armed: {arm_type}")
+
+sys.exit()
+
diff --git a/total_connect_client/live/arm.py b/total_connect_client/disarm.py
similarity index 52%
rename from total_connect_client/live/arm.py
rename to total_connect_client/disarm.py
index 07a4854..f9d687f 100644
--- a/total_connect_client/live/arm.py
+++ b/total_connect_client/disarm.py
@@ -1,5 +1,6 @@
 """Test your system from the command line."""
 
+import getpass
 import logging
 import sys
 
@@ -7,21 +8,19 @@ from total_connect_client.client import TotalConnectClient
 
 logging.basicConfig(filename="test.log", level=logging.DEBUG)
 
-if len(sys.argv) != 4:
-    print("usage:  username password " "location1=usercode1,location2=usercode2 \n")
+if len(sys.argv) != 3:
+    print("usage:  username location1=usercode1,location2=usercode2 \n")
     sys.exit()
 
-
 USERNAME = sys.argv[1]
-PASSWORD = sys.argv[2]
-USERCODES = dict(x.split("=") for x in sys.argv[3].split(","))
+USERCODES = dict(x.split("=") for x in sys.argv[2].split(","))
+PASSWORD = getpass.getpass()
 
 TC = TotalConnectClient(USERNAME, PASSWORD, USERCODES)
 
 for location_id in TC.locations:
-    if TC.arm_stay(location_id):
-        print("Arm success.")
-    else:
-        print("Arm failure.  Check logs.")
+    TC.locations[location_id].disarm()
+
+print("Disarmed.")
 
 sys.exit()
diff --git a/total_connect_client/live/disarm.py b/total_connect_client/live/disarm.py
deleted file mode 100644
index 4bcf6f6..0000000
--- a/total_connect_client/live/disarm.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""Test your system from the command line."""
-
-import logging
-import sys
-
-from total_connect_client.client import TotalConnectClient
-
-logging.basicConfig(filename="test.log", level=logging.DEBUG)
-
-if len(sys.argv) != 4:
-    print("usage:  username password " "location1=usercode1,location2=usercode2 \n")
-    sys.exit()
-
-USERNAME = sys.argv[1]
-PASSWORD = sys.argv[2]
-USERCODES = dict(x.split("=") for x in sys.argv[3].split(","))
-
-TC = TotalConnectClient(USERNAME, PASSWORD, USERCODES)
-
-for location_id in TC.locations:
-    if TC.disarm(location_id):
-        print("Disarm success.")
-    else:
-        print("Disarm failure.  Check logs.")
-
-sys.exit()
diff --git a/total_connect_client/zone.py b/total_connect_client/zone.py
index 626712a..c6f8b38 100644
--- a/total_connect_client/zone.py
+++ b/total_connect_client/zone.py
@@ -27,31 +27,32 @@ class ZoneType(Enum):
     http://techresource.online/training/ssnw/honeywell/zone-types
     """
 
-    SECURITY          = 0  # for Vista, zone type 0 is not used
-    ENTRY_EXIT1       = 1  # starts countdown timer #1
-    ENTRY_EXIT2       = 2  # like ENTRY_EXIT1 but uses timer #2
-    PERIMETER         = 3  # zone type 3 usually triggers an immediate alarm...
-    PROA7_SECURITY    = 3  # but some panels like Lynx 7000 report timed zones as 3
-    INTERIOR_FOLLOWER = 4  # inactive when armed STAY
-    TROUBLE_ALARM     = 5  # trouble by day, alarm by night
-    SILENT_24HR       = 6  # 24-hour silent alarm (often used for police/hold-up)
-    AUDIBLE_24HR      = 7  # 24-hour audible alarm (often used for police)
-    AUX_24HR          = 8  # no local siren but keypad beeps (often used for medical)
-    FIRE_SMOKE        = 9
-    INTERIOR_DELAY    = 10  # inactive when armed STAY, otherwise like ENTRY_EXIT1
-    MONITOR           = 12  # e.g. temperature or flood
-    CARBON_MONOXIDE   = 14
-    PROA7_MEDICAL     = 15
-    FIRE_W_VERIFICATION = 16  # must trigger twice to cause an alarm
-    RF_ARM_STAY = 20 # keyfob
-    RF_ARM_AWAY = 21 # keyfob
-    RF_DISARM = 22 # keyfob
-    NO_ALARM_RESPONSE = 23 # per Vista docs
-    SILENT_BURGLARY = 24 # per Vista docs
-    LYRIC_KEYPAD = 50
-    KEYSWITCH = 77 # per Vista20P docs
-    AAV_MONITOR = 81 # per Vista20P docs
-    LYRIC_LOCAL_ALARM = 89
+    SECURITY             = 0  # for Vista, zone type 0 is not used
+    ENTRY_EXIT1          = 1  # starts countdown timer #1
+    ENTRY_EXIT2          = 2  # like ENTRY_EXIT1 but uses timer #2
+    PERIMETER            = 3  # zone type 3 usually triggers an immediate alarm...
+    PROA7_SECURITY       = 3  # but some panels like Lynx 7000 report timed zones as 3
+    INTERIOR_FOLLOWER    = 4  # inactive when armed STAY
+    TROUBLE_ALARM        = 5  # trouble by day, alarm by night
+    SILENT_24HR          = 6  # 24-hour silent alarm (often used for police/hold-up)
+    AUDIBLE_24HR         = 7  # 24-hour audible alarm (often used for police)
+    AUX_24HR             = 8  # no local siren but keypad beeps (often used for medical)
+    FIRE_SMOKE           = 9
+    INTERIOR_DELAY       = 10  # inactive when armed STAY, otherwise like ENTRY_EXIT1
+    MONITOR              = 12  # e.g. temperature or flood
+    CARBON_MONOXIDE      = 14
+    PROA7_MEDICAL        = 15
+    FIRE_W_VERIFICATION  = 16  # must trigger twice to cause an alarm
+    RF_ARM_STAY          = 20  # keyfob
+    RF_ARM_AWAY          = 21  # keyfob
+    RF_DISARM            = 22  # keyfob
+    NO_ALARM_RESPONSE    = 23  # per Vista docs
+    SILENT_BURGLARY      = 24  # per Vista docs
+    LYRIC_KEYPAD         = 50
+    PROA7_GARAGE_MONITOR = 53  # issue #167
+    KEYSWITCH            = 77  # per Vista20P docs
+    AAV_MONITOR          = 81  # per Vista20P docs
+    LYRIC_LOCAL_ALARM    = 89
 
     # According to the VISTA docs, these can be programmed via downloader software
     # or from a keypad using data fields *182-*185
@@ -81,6 +82,7 @@ class TotalConnectZone:
         self.supervision_type = None
         self.chime_state = None
         self.device_type = None
+        self._unknown_type_reported = False
         self._update(zone)
 
     def __str__(self):
@@ -159,6 +161,8 @@ class TotalConnectZone:
             ZoneType.AUDIBLE_24HR,
             ZoneType.INTERIOR_DELAY,
             ZoneType.LYRIC_LOCAL_ALARM,
+            ZoneType.PROA7_GARAGE_MONITOR,
+
         )
 
     def is_type_motion(self):
@@ -201,7 +205,7 @@ class TotalConnectZone:
         try:
             self.status = ZoneStatus(zone.get("ZoneStatus"))
         except ValueError:
-            LOGGER.error(f"unknown ZoneStatus in {zone}: please report at {PROJECT_URL}/issues")
+            LOGGER.info(f"unknown ZoneStatus {zone.get('ZoneStatus')} in {zone}: please report at {PROJECT_URL}/issues")
             raise TotalConnectError(f"unknown ZoneStatus in {zone}") from None
         self.can_be_bypassed = zone.get("CanBeBypassed")
 
@@ -210,10 +214,12 @@ class TotalConnectZone:
             # TODO: if zid is None should we raise PartialResponseError?
             self.zone_type_id = None if zid is None else ZoneType(zid)
         except ValueError:
-            LOGGER.error(f"unknown ZoneType {zid} in {zone}: please report at {PROJECT_URL}/issues")
             # if we get an unknown ZoneType we do not raise an exception, because
             # we know there are more zone types than we have in our enum, and
             # having an unknown ZoneType doesn't keep us from doing our work
+            if not self._unknown_type_reported:
+                LOGGER.info(f"unknown ZoneType {zid} in {zone}: please report at {PROJECT_URL}/issues")
+                self._unknown_type_reported = True
             self.zone_type_id = zid
 
         self.battery_level = zone.get("Batterylevel", self.battery_level)
@@ -230,5 +236,4 @@ class TotalConnectZone:
 
     def _mark_as_bypassed(self):
         """Set is_bypassed status."""
-        # TODO: when does this get reset to no longer bypassed?
         self.status |= ZoneStatus.BYPASSED


Test patch:
diff --git a/tests/const.py b/tests/const.py
index 7c493ae..c0f53c9 100644
--- a/tests/const.py
+++ b/tests/const.py
@@ -68,7 +68,7 @@ ZS_NORMAL = {
     "PartitionId": "1",
     "Batterylevel": "-1",
     "Signalstrength": "-1",
-    "zoneAdditionalInfo": None,
+    "zoneAdditionalInfo": {"DeviceType": "test"},
     "ZoneID": "1",
     "ZoneStatus": ZoneStatus.NORMAL,
     "ZoneTypeId": ZoneType.SECURITY,
diff --git a/tests/test_zone.py b/tests/test_zone.py
index 96d3c5a..8992d35 100644
--- a/tests/test_zone.py
+++ b/tests/test_zone.py
@@ -320,3 +320,17 @@ def test_proa7_zones():
     zone = tcz(zone_medical)
     assert zone.is_type_medical() is True
     assert zone.is_type_button() is True
+
+def test_unknown_type():
+    """Test unknown ZoneType."""
+    zone_unknown = {
+        "ZoneDescription": "Unknown",
+        "PartitionId": "1",
+        "ZoneTypeId": 12345,
+        "CanBeBypassed": 0,
+        "ZoneStatus": ZoneStatus.NORMAL,
+    }
+
+    zone = tcz(zone_unknown)
+    assert zone.zone_type_id == 12345
+    assert zone._unknown_type_reported is True