11# Need to figure out how to resolve the 'Untyped decorator makes function "..." untyped' errors in mypy when using click decorators
22# mypy: disable-error-code="misc"
33import asyncio
4+ import xml .etree .ElementTree as ET
5+ import zlib
6+ from collections import defaultdict
7+ from pathlib import Path
48from typing import Literal , overload
59
610import click
11+ from scapy .layers .inet import UDP
12+ from scapy .utils import rdpcap
713
814from pyomnilogic_local .api import OmniLogicAPI
915from pyomnilogic_local .cli .utils import async_get_mspconfig , async_get_telemetry
1016from pyomnilogic_local .models .filter_diagnostics import FilterDiagnostics
17+ from pyomnilogic_local .models .leadmessage import LeadMessage
18+ from pyomnilogic_local .omnitypes import MessageType
19+ from pyomnilogic_local .protocol import OmniLogicMessage
1120
1221
1322@click .group ()
@@ -34,18 +43,6 @@ def get_telemetry(ctx: click.Context) -> None:
3443 click .echo (telemetry )
3544
3645
37- @debug .command ()
38- @click .pass_context
39- def get_alarm_list (ctx : click .Context ) -> None :
40- alarm_list = asyncio .run (async_get_alarm_list (ctx .obj ["OMNI" ]))
41- click .echo (alarm_list )
42-
43-
44- async def async_get_alarm_list (omni : OmniLogicAPI ) -> str :
45- alarm_list = await omni .async_get_alarm_list ()
46- return alarm_list
47-
48-
4946@debug .command ()
5047@click .option ("--pool-id" , help = "System ID of the Body Of Water the filter is associated with" )
5148@click .option ("--filter-id" , help = "System ID of the filter to request diagnostics for" )
@@ -84,12 +81,107 @@ async def async_get_filter_diagnostics(omni: OmniLogicAPI, pool_id: int, filter_
8481
8582
8683@debug .command ()
84+ @click .argument ("pcap_file" , type = click .Path (exists = True , path_type = Path ))
8785@click .pass_context
88- def get_log_config (ctx : click .Context ) -> None :
89- log_config = asyncio .run (async_get_log_config (ctx .obj ["OMNI" ]))
90- click .echo (log_config )
91-
92-
93- async def async_get_log_config (omni : OmniLogicAPI ) -> str :
94- log_config = await omni .async_get_log_config ()
95- return log_config
86+ def parse_pcap (ctx : click .Context , pcap_file : Path ) -> None :
87+ """Parse a PCAP file and reconstruct Omnilogic protocol communication."""
88+ # Read the PCAP file
89+ try :
90+ packets = rdpcap (str (pcap_file ))
91+ except Exception as e :
92+ click .echo (f"Error reading PCAP file: { e } " , err = True )
93+ raise click .Abort ()
94+
95+ # Track multi-message sequences (LeadMessage + BlockMessages)
96+ # Key: (src_ip, dst_ip, msg_id), Value: list of messages
97+ message_sequences : dict [tuple [str , str , int ], list [OmniLogicMessage ]] = defaultdict (list )
98+
99+ # Process packets in order
100+ for packet in packets :
101+ if not packet .haslayer (UDP ):
102+ click .echo ("Not a UDP packet, skipping..." , err = True )
103+ continue
104+
105+ udp = packet [UDP ]
106+ src_ip = packet .payload .src
107+ dst_ip = packet .payload .dst
108+
109+ # Parse the Omnilogic message
110+ try :
111+ omni_msg = OmniLogicMessage .from_bytes (bytes (udp .payload ))
112+ click .echo (f"Parsed Omnilogic message: { omni_msg } " )
113+ except Exception : # pylint: disable=broad-except
114+ # Not an Omnilogic message, skip it
115+ click .echo ("Not an Omnilogic message, skipping..." , err = True )
116+ continue
117+
118+ # Print the basic packet info
119+ click .echo (f"{ src_ip } sent { omni_msg .type .name } to { dst_ip } " )
120+
121+ # Track LeadMessage/BlockMessage sequences
122+ if omni_msg .type == MessageType .MSP_LEADMESSAGE :
123+ # Start a new sequence
124+ seq_key = (src_ip , dst_ip , omni_msg .id )
125+ message_sequences [seq_key ] = [omni_msg ]
126+ elif omni_msg .type == MessageType .MSP_BLOCKMESSAGE :
127+ # Find the matching LeadMessage sequence
128+ # We need to find the sequence with the same src/dst and highest ID less than or equal to this message
129+ matching_seq : tuple [str , str , int ] = ("" , "" , 0 )
130+ for seq_key in message_sequences :
131+ if seq_key [0 ] == src_ip and seq_key [1 ] == dst_ip :
132+ # Check if this is the right sequence (the LeadMessage should have been received before this block)
133+ if not matching_seq or seq_key [2 ] > matching_seq [2 ]:
134+ matching_seq = seq_key
135+
136+ if matching_seq :
137+ message_sequences [matching_seq ].append (omni_msg )
138+
139+ # Check if we have all the blocks
140+ lead_msg = message_sequences [matching_seq ][0 ]
141+ lead_data = LeadMessage .from_orm (ET .fromstring (lead_msg .payload [:- 1 ]))
142+
143+ # We have LeadMessage + all BlockMessages
144+ if len (message_sequences [matching_seq ]) == lead_data .msg_block_count + 1 :
145+ # Reassemble and decode
146+ try :
147+ decoded_msg = _reassemble_and_decode (message_sequences [matching_seq ])
148+ click .echo (f"\n Message from { src_ip } decoded:" )
149+ click .echo (decoded_msg )
150+ click .echo () # Extra newline for readability
151+ except Exception as e : # pylint: disable=broad-except
152+ click .echo (f"Error decoding message: { e } " , err = True )
153+
154+ # Clean up this sequence
155+ del message_sequences [matching_seq ]
156+
157+
158+ def _reassemble_and_decode (messages : list [OmniLogicMessage ]) -> str :
159+ """
160+ Reassemble a LeadMessage + BlockMessages sequence and decode the payload.
161+
162+ Args:
163+ messages: List containing LeadMessage followed by BlockMessages
164+
165+ Returns:
166+ Decoded message content as string
167+ """
168+ lead_msg = messages [0 ]
169+ block_msgs = messages [1 :]
170+
171+ # Reassemble the blocks
172+ # Sort by message ID to ensure correct order
173+ sorted_blocks = sorted (block_msgs , key = lambda m : m .id )
174+
175+ # Concatenate the block payloads (skip the 8-byte header on each block)
176+ reassembled = b""
177+ for block_msg in sorted_blocks :
178+ reassembled += block_msg .payload [8 :]
179+
180+ # Decompress if necessary
181+ if lead_msg .compressed :
182+ reassembled = zlib .decompress (reassembled )
183+
184+ # Decode to string
185+ decoded = reassembled .decode ("utf-8" ).strip ("\x00 " )
186+
187+ return decoded
0 commit comments