Skip to content

Commit

Permalink
Merge pull request #235 from austinmroczek/master
Browse files Browse the repository at this point in the history
Initial typing and some linter fixes
  • Loading branch information
austinmroczek authored Dec 24, 2024
2 parents 3cf2eb6 + 3f10c9a commit deb65a7
Show file tree
Hide file tree
Showing 12 changed files with 65 additions and 42 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
]
Expand Down
8 changes: 4 additions & 4 deletions total_connect_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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))
Expand All @@ -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.
"""
Expand All @@ -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.
"""
Expand Down
20 changes: 15 additions & 5 deletions total_connect_client/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


class ArmType(Enum):
"""Represent ArmingType."""

AWAY = 0
STAY = 1
STAY_INSTANT = 2
Expand All @@ -14,6 +16,8 @@ class ArmType(Enum):


class ArmingState(Enum):
"""Represent ArmingState."""

DISARMED = 10200
DISARMED_BYPASS = 10211
DISARMED_ZONE_FAULTED = (
Expand Down Expand Up @@ -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.
"""

Expand All @@ -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
Expand All @@ -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
Expand All @@ -183,4 +193,4 @@ def from_response(response_dict):

PROJECT_URL = "https://github.com/craigjmidwinter/total-connect-client"

STATUS_URL = "https://status.resideo.com/"
STATUS_URL = "https://status.resideo.com/"
4 changes: 2 additions & 2 deletions total_connect_client/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion total_connect_client/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions total_connect_client/partition.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ 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)

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:
Expand Down
2 changes: 1 addition & 1 deletion total_connect_client/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
61 changes: 37 additions & 24 deletions total_connect_client/zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -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

Expand All @@ -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"])
Expand Down

0 comments on commit deb65a7

Please sign in to comment.