Skip to content

Commit

Permalink
Attempt (workaround) to process alternative (protocol version 2?) RT …
Browse files Browse the repository at this point in the history
…log responses
  • Loading branch information
vwout committed Dec 4, 2023
1 parent 7c97a99 commit c3269dc
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 15 deletions.
37 changes: 22 additions & 15 deletions c3/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def __init__(self, host: [str | C3DeviceInfo], port: int = consts.C3_PORT_DEFAUL
self._sock.settimeout(2)
self._connected: bool = False
self._session_less = False
self._protocol_version = None
self._session_id: int = 0xFEFE
self._request_nr: int = -258
self._status: C3PanelStatus = C3PanelStatus()
Expand All @@ -51,17 +52,18 @@ def __init__(self, host: [str | C3DeviceInfo], port: int = consts.C3_PORT_DEFAUL
self._device_info: C3DeviceInfo = C3DeviceInfo(host=host, port=port or consts.C3_PORT_DEFAULT)

@classmethod
def _get_message_header(cls, data: [bytes or bytearray]) -> tuple[[int or None], int]:
def _get_message_header(cls, data: [bytes or bytearray]) -> tuple[[int or None], int, int]:
if len(data) >= 5:
if data[0] == consts.C3_MESSAGE_START: # and data[1] == consts.C3_PROTOCOL_VERSION:
version = data[1]
if data[0] == consts.C3_MESSAGE_START: # and version == consts.C3_PROTOCOL_VERSION:
command = data[2]
data_size = data[3] + (data[4] * 255)
else:
raise ValueError("Received reply does not start with start token")
else:
raise ValueError("Received reply of unsufficient length (%d)", len(data))

return command, data_size
return command, data_size, version

@classmethod
def _get_message(cls, data: [bytes or bytearray]) -> bytearray:
Expand Down Expand Up @@ -120,13 +122,13 @@ def _send(self, command: consts.Command, data=None) -> int:
self._request_nr = self._request_nr + 1
return bytes_written

def _receive(self) -> tuple[bytearray, int]:
def _receive(self) -> tuple[bytearray, int, int]:
# Get the first 5 bytes
header = self._sock.recv(5)
self.log.debug("Receiving header: %s", header.hex())

message = bytearray()
received_command, data_size = self._get_message_header(header)
received_command, data_size, protocol_version = self._get_message_header(header)
# Get the optional message data, checksum and end marker
payload = self._sock.recv(data_size + 3)
if data_size > 0:
Expand All @@ -146,7 +148,7 @@ def _receive(self) -> tuple[bytearray, int]:
else:
data_size = 0

return message, data_size
return message, data_size, protocol_version

def _send_receive(self, command: consts.Command, data=None) -> tuple[bytearray, int]:
bytes_received = 0
Expand All @@ -156,7 +158,7 @@ def _send_receive(self, command: consts.Command, data=None) -> tuple[bytearray,
try:
bytes_written = self._send(command, data)
if bytes_written > 0:
receive_data, bytes_received = self._receive()
receive_data, bytes_received, _ = self._receive()
if not self._session_less and bytes_received > 2:
session_offset = 4
session_id = (receive_data[1] << 8) + receive_data[0]
Expand Down Expand Up @@ -280,7 +282,7 @@ def discover(cls, interface_address: str = None, timeout: int = 2) -> list[C3]:
break

if payload:
received_command, data_size = cls._get_message_header(payload)
received_command, data_size, _ = cls._get_message_header(payload)
if received_command == consts.C3_REPLY_OK:
# Get the message data and signature
message = cls._get_message(payload)
Expand Down Expand Up @@ -316,11 +318,12 @@ def connect(self, password: Optional[str] = None) -> bool:
self._sock.connect((self._device_info.host, self._device_info.port))
bytes_written = self._send(consts.Command.CONNECT_SESSION, data)
if bytes_written > 0:
receive_data, bytes_received = self._receive()
receive_data, bytes_received, protocol_version = self._receive()
if bytes_received > 2:
self._session_id = (receive_data[1] << 8) + receive_data[0]
self.log.debug("Connected with Session ID %04x", self._session_id)
self._session_less = False
self._protocol_version = protocol_version
self._connected = True
except ConnectionError as ex:
self.log.debug("Connection attempt with session to %s failed: %s", self._device_info.host, ex)
Expand All @@ -334,9 +337,10 @@ def connect(self, password: Optional[str] = None) -> bool:
self._sock.connect((self._device_info.host, self._device_info.port))
bytes_written = self._send(consts.Command.CONNECT_SESSION_LESS, data)
if bytes_written > 0:
self._receive()
_, _, protocol_version = self._receive()
self.log.debug("Connected without session")
self._session_less = True
self._protocol_version = protocol_version
self._connected = True
except ConnectionError as ex:
self.log.debug("Connection attempt without session to %s failed: %s", self._device_info.host, ex)
Expand Down Expand Up @@ -405,12 +409,15 @@ def get_rt_log(self) -> list[rtlog.EventRecord | rtlog.DoorAlarmStatusRecord]:
logs_messages = [message[i:i+16] for i in range(0, message_length, 16)]
for log_message in logs_messages:
self.log.debug("Received RT Log: %s", log_message.hex())
if log_message[10] == consts.EventType.DOOR_ALARM_STATUS:
records.append(rtlog.DoorAlarmStatusRecord.from_bytes(log_message))
else:
records.append(rtlog.EventRecord.from_bytes(log_message))
records.append(rtlog.factory(log_message))
else:
raise ValueError("Received RT Log(s) size is not a multiple of 16: %d" % message_length)
if self._protocol_version == 2:
# Protocol version (?) 2 response with a different message structure
# For now ignoring all data but last 16 bytes
self.log.debug("Received too many bytes, only using tail: %s", message.hex())
records.append(rtlog.factory(message[-16:]))
else:
raise ValueError("Received RT Log(s) size is not a multiple of 16: %d" % message_length)
else:
raise ConnectionError("No connection to C3 panel.")

Expand Down
7 changes: 7 additions & 0 deletions c3/rtlog.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,10 @@ def __repr__(self):
"%-12s %-10s" % ("door_id", self.door_id)]

return "\r\n".join(repr_arr)


def factory(log_message: bytes) -> RTLogRecord:
if log_message[10] == consts.EventType.DOOR_ALARM_STATUS:
return DoorAlarmStatusRecord.from_bytes(log_message)
else:
return EventRecord.from_bytes(log_message)
35 changes: 35 additions & 0 deletions tests/test_rtlog.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,38 @@ def test_rtlog_unknown_verification_mode():
logs = panel.get_rt_log()
assert len(logs) == 1
assert logs[0].verified == VerificationMode.OTHER


def test_rtlog_version2_response():
with mock.patch('socket.socket') as mock_socket:
panel = C3('localhost')
mock_socket.return_value.send.return_value = 8
mock_socket.return_value.recv.side_effect = [
bytes.fromhex("aa02c90100"), bytes.fromhex("f357d955"),
bytes.fromhex("aa02c80000"), bytes.fromhex("81fe55"),
bytes.fromhex("aa02c84200"), bytes.fromhex("7e53657269616c4e756d6265723d4143"
"59543033323335333632372c4c6f636b"
"436f756e743d342c417578496e436f756"
"e743d342c4175784f7574436f756e743d"
"34599355"),
# Not sure what the message structure of the response use protocol version (?) 02 is;
# for now only the last 16 bytes are used
bytes.fromhex("aa02c82400"), bytes.fromhex("02000000"
"32000000000000000000000000000000"
"0000000000000000c800ff0071c7d42d"
"4d3955"),
bytes.fromhex("aa02c82400"), bytes.fromhex("02000000"
"32000000000000000000000000000000"
"0000000000000000c800ff006b13d52d"
"0b8955")
]

assert panel.connect() is True

logs = panel.get_rt_log()
assert len(logs) == 1
assert isinstance(logs[0], DoorAlarmStatusRecord)

logs = panel.get_rt_log()
assert len(logs) == 1
assert isinstance(logs[0], DoorAlarmStatusRecord)

0 comments on commit c3269dc

Please sign in to comment.