diff --git a/pyproject.toml b/pyproject.toml index ebcdf11..6c9d2b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta:__legacy__" [project] name="total_connect_client" -version="2024.12" +version="2024.12.1" authors = [ { name="Craig J. Midwinter", email="craig.j.midwinter@gmail.com" }, ] diff --git a/total_connect_client/client.py b/total_connect_client/client.py index f6e4d84..01525d8 100644 --- a/total_connect_client/client.py +++ b/total_connect_client/client.py @@ -200,7 +200,7 @@ def _send_one_request(self, operation_name, args): API_APP_ID = "14588" API_APP_VERSION = "1.0.34" - def request(self, operation_name, args, attempts_remaining=5): + def request(self, operation_name, args, attempts_remaining: int=5): """Send a SOAP request. args is a list or tuple defining the parameters to the operation. """ @@ -313,7 +313,7 @@ def authenticate(self): LOGGER.info(f"{self.username} authenticated: {len(self._locations)} locations") self.times["authenticate()"] = time.time() - start_time - def validate_usercode(self, device_id, usercode): + def validate_usercode(self, device_id, usercode:str)-> bool: """Return True if the usercode is valid for the device.""" response = self.request( "ValidateUserCode", (self.token, device_id, str(usercode)) @@ -328,7 +328,7 @@ def validate_usercode(self, device_id, usercode): return False return True - def is_logged_in(self): + def is_logged_in(self)->bool: """Return true if the client is logged into the Total Connect service with valid credentials. """ @@ -344,7 +344,7 @@ def log_out(self): LOGGER.info("Logout Successful") self.token = None - def get_number_locations(self): + def get_number_locations(self)->int: """Return the number of locations. Home Assistant needs a way to force the locations to load inside a callable function. """ diff --git a/total_connect_client/const.py b/total_connect_client/const.py index b4b1dab..f100bcd 100644 --- a/total_connect_client/const.py +++ b/total_connect_client/const.py @@ -6,6 +6,8 @@ class ArmType(Enum): + """Represent ArmingType.""" + AWAY = 0 STAY = 1 STAY_INSTANT = 2 @@ -14,6 +16,8 @@ class ArmType(Enum): class ArmingState(Enum): + """Represent ArmingState.""" + DISARMED = 10200 DISARMED_BYPASS = 10211 DISARMED_ZONE_FAULTED = ( @@ -141,7 +145,9 @@ def is_triggered(self): class _ResultCode(Enum): - """As suggested by the leading underscore, this class is not used by + """Represent ResultCode. + + As suggested by the leading underscore, this class is not used by callers of the API. """ @@ -150,8 +156,11 @@ def from_response(response_dict): try: return _ResultCode(response_dict["ResultCode"]) except TypeError: - # sometimes when there are server issues, it returns empty responses - see issue #228 - raise ServiceUnavailable(f"Server returned empty response, check server status at {STATUS_URL}") from None + # sometimes when there are server issues, + # it returns empty responses - see issue #228 + raise ServiceUnavailable( + f"Server returned empty response, check server status at {STATUS_URL}" + ) from None except ValueError: raise BadResultCodeError( f"unknown result code {response_dict['ResultCode']}", response_dict @@ -170,7 +179,8 @@ def from_response(response_dict): USER_CODE_INVALID = -4106 FAILED_TO_CONNECT = -4104 - # Invalid Parameter returned when requesting SyncPanelStatus using non-existant JobID. + # Invalid Parameter returned when requesting SyncPanelStatus + # using non-existant JobID. INVALID_PARAMETER = -501 BAD_OBJECT_REFERENCE = -400 @@ -183,4 +193,4 @@ def from_response(response_dict): PROJECT_URL = "https://github.com/craigjmidwinter/total-connect-client" -STATUS_URL = "https://status.resideo.com/" \ No newline at end of file +STATUS_URL = "https://status.resideo.com/" diff --git a/total_connect_client/device.py b/total_connect_client/device.py index da25637..c16e4bd 100644 --- a/total_connect_client/device.py +++ b/total_connect_client/device.py @@ -4,7 +4,7 @@ class TotalConnectDevice: """Device class for Total Connect.""" - def __init__(self, info): + def __init__(self, info: dict): """Initialize device based on DeviceInfoBasic.""" self.deviceid = info.get("DeviceID") self.name = info.get("DeviceName") @@ -61,7 +61,7 @@ def doorbell_info(self, data): if data: self._doorbell_info = data - def is_doorbell(self): + def is_doorbell(self) -> bool: """Return true if a doorbell.""" if self._doorbell_info and self._doorbell_info["IsExistingDoorBellUser"] == 1: return True diff --git a/total_connect_client/arm.py b/total_connect_client/live/arm.py similarity index 91% rename from total_connect_client/arm.py rename to total_connect_client/live/arm.py index 492bbe7..b781c47 100644 --- a/total_connect_client/arm.py +++ b/total_connect_client/live/arm.py @@ -4,8 +4,8 @@ import logging import sys -from .const import ArmType -from .client import TotalConnectClient +from ..const import ArmType +from ..client import TotalConnectClient logging.basicConfig(filename="test.log", level=logging.DEBUG) diff --git a/total_connect_client/bypass_all.py b/total_connect_client/live/bypass_all.py similarity index 100% rename from total_connect_client/bypass_all.py rename to total_connect_client/live/bypass_all.py diff --git a/total_connect_client/disarm.py b/total_connect_client/live/disarm.py similarity index 100% rename from total_connect_client/disarm.py rename to total_connect_client/live/disarm.py diff --git a/total_connect_client/sync.py b/total_connect_client/live/sync.py similarity index 100% rename from total_connect_client/sync.py rename to total_connect_client/live/sync.py diff --git a/total_connect_client/location.py b/total_connect_client/location.py index 1c83b56..3ebd159 100644 --- a/total_connect_client/location.py +++ b/total_connect_client/location.py @@ -190,7 +190,7 @@ def _build_partition_list(self, partition_id=None): ) return {"int": [partition_id]} - def arm(self, arm_type, partition_id=None): + def arm(self, arm_type: int, partition_id=None): """Arm the given partition. If no partition is given, arm all partitions.""" # see https://rs.alarmnet.com/TC21api/tc2.asmx?op=ArmSecuritySystemPartitionsV1 assert isinstance(arm_type, ArmType) diff --git a/total_connect_client/partition.py b/total_connect_client/partition.py index 0c1b121..ee0a328 100644 --- a/total_connect_client/partition.py +++ b/total_connect_client/partition.py @@ -38,7 +38,7 @@ def __str__(self): return data - def arm(self, arm_type): + def arm(self, arm_type: int): """Arm the partition.""" self.parent.arm(arm_type, self.partitionid) @@ -46,7 +46,7 @@ def disarm(self): """Disarm the partition.""" self.parent.disarm(self.partitionid) - def _update(self, info): + def _update(self, info: dict): """Update partition based on PartitionInfo.""" astate = (info or {}).get("ArmingState") if astate is None: diff --git a/total_connect_client/user.py b/total_connect_client/user.py index 39f581d..f6a7b2d 100644 --- a/total_connect_client/user.py +++ b/total_connect_client/user.py @@ -27,7 +27,7 @@ def __init__(self, user_info): "with minimal permissions." ) - def security_problem(self): + def security_problem(self)-> bool: """Run security checks. Return true if problem.""" problem = False diff --git a/total_connect_client/zone.py b/total_connect_client/zone.py index f6eba43..45830f9 100644 --- a/total_connect_client/zone.py +++ b/total_connect_client/zone.py @@ -10,15 +10,26 @@ class ZoneStatus(IntFlag): + """Class to represent ZoneStatus.""" + NORMAL = 0 BYPASSED = 1 FAULT = 2 TROUBLE = 8 # is also Tampered - TAMPER = 16 # Tamper for ProA7, see #176 - COMMUNICATION_FAILURE = 32 # see #191 + TAMPER = 16 # Tamper for ProA7, see #176 + COMMUNICATION_FAILURE = 32 # see #191 LOW_BATTERY = 64 TRIGGERED = 256 - KNOWN = NORMAL | BYPASSED | FAULT | TROUBLE | TAMPER | COMMUNICATION_FAILURE | LOW_BATTERY | TRIGGERED + KNOWN = ( + NORMAL + | BYPASSED + | FAULT + | TROUBLE + | TAMPER + | COMMUNICATION_FAILURE + | LOW_BATTERY + | TRIGGERED + ) class ZoneType(Enum): @@ -57,8 +68,8 @@ class ZoneType(Enum): 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 + # According to the VISTA docs, these can be programmed via downloader + # software or from a keypad using data fields *182-*185 VISTA_CONFIGURABLE_90 = 90 VISTA_CONFIGURABLE_91 = 91 @@ -108,34 +119,36 @@ def __str__(self): f"Device Type: {self.device_type}\n\n" ) - def is_bypassed(self): + def is_bypassed(self) -> bool: """Return true if the zone is bypassed.""" return self.status & ZoneStatus.BYPASSED > 0 - def is_faulted(self): + def is_faulted(self) -> bool: """Return true if the zone is faulted.""" return self.status & ZoneStatus.FAULT > 0 - def is_tampered(self): + def is_tampered(self) -> bool: """Return true if zone is tampered.""" - return (self.status & ZoneStatus.TROUBLE > 0) or (self.status & ZoneStatus.TAMPER > 0) + return (self.status & ZoneStatus.TROUBLE > 0) or ( + self.status & ZoneStatus.TAMPER > 0 + ) - def is_low_battery(self): + def is_low_battery(self) -> bool: """Return true if low battery.""" return self.status & ZoneStatus.LOW_BATTERY > 0 - def is_troubled(self): + def is_troubled(self) -> bool: """Return true if zone is troubled.""" return self.status & ZoneStatus.TROUBLE > 0 - def is_triggered(self): + def is_triggered(self) -> bool: """Return true if zone is triggered.""" return self.status & ZoneStatus.TRIGGERED > 0 - def is_type_button(self): + def is_type_button(self) -> bool: """Return true if zone is a button.""" - - # as seen so far, any security zone that cannot be bypassed is a button on a panel + # as seen so far, any security zone that cannot be bypassed + # is a button on a panel if self.is_type_security() and not self.can_be_bypassed: return True @@ -151,9 +164,8 @@ def is_type_button(self): return False - def is_type_security(self): + def is_type_security(self) -> bool: """Return true if zone type is security.""" - return self.zone_type_id in ( ZoneType.SECURITY, ZoneType.ENTRY_EXIT1, @@ -168,27 +180,27 @@ def is_type_security(self): ZoneType.PROA7_GARAGE_MONITOR, ) - def is_type_motion(self): + def is_type_motion(self) -> bool: """Return true if zone type is motion.""" return self.zone_type_id == ZoneType.INTERIOR_FOLLOWER - def is_type_fire(self): + def is_type_fire(self) -> bool: """Return true if zone type is fire or smoke.""" return self.zone_type_id == ZoneType.FIRE_SMOKE - def is_type_temperature(self): + def is_type_temperature(self) -> bool: """Return true if zone monitors the temperature.""" return self.zone_type_id == ZoneType.MONITOR - def is_type_carbon_monoxide(self): + def is_type_carbon_monoxide(self) -> bool: """Return true if zone type is carbon monoxide.""" return self.zone_type_id == ZoneType.CARBON_MONOXIDE - def is_type_medical(self): + def is_type_medical(self) -> bool: """Return true if zone type is medical.""" return self.zone_type_id == ZoneType.PROA7_MEDICAL - def is_type_keypad(self): + def is_type_keypad(self) -> bool: """Return true if zone type is keypad.""" return self.zone_type_id == ZoneType.LYRIC_KEYPAD @@ -199,7 +211,8 @@ def _update(self, zone): assert self.zoneid == zid, (self.zoneid, zid) self.description = zone.get("ZoneDescription") - # ZoneInfo gives 'PartitionID' but ZoneStatusInfoWithPartitionId gives 'PartitionId' + # ZoneInfo gives 'PartitionID' but + # ZoneStatusInfoWithPartitionId gives 'PartitionId' if "PartitionId" in zone: # ...and PartitionId gives an int instead of a string self.partition = str(zone["PartitionId"])