Add S7CommPlus alarm handling and subscriptions - #820
Conversation
| await self._send_request(FunctionCode.DELETE_OBJECT, payload) | ||
| logger.info(f"Subscription {subscription_id:#x} deleted") | ||
|
|
||
| async def create_alarm_subscription( |
There was a problem hiding this comment.
Doesn't really work for me, but I got you a trace using the C# library (TX = before TLS encryption, RX = received after TLS decryption)
{"t":531.665,"type":"note","text":"AlarmSubscriptionCreate()"}
{"t":533.610,"layer":"s7cp","dir":"TX","len":252,"hex":"720200f431000004ca0000000870000cb73470000cb80004000000000002a17fffc00187690000a38169001517537562736372697074696f6e5f32313437343637323635a3883a000202a3876a00030000a3876b000900a38810000202a38811000101a3881820040388808480000000a38819000400a3881a000400a3881b000200a3881c000200a3881d0007000aa3881e0003ffffa15101000194660000a381690015125337704472697665725f416c61726d696e67a3876d000203a3946310030a0000000000000000000000000000000000000000a3bc33200301ffffa3bf75200400a3bf6d000101a4946400000008a2a20000000072020000"}
{"t":550.019,"layer":"s7cp","dir":"RX","len":30,"hex":"7202001632000004ca0000000834000187808099390a0000000072020000"}
{"t":550.942,"type":"note","text":"TestWaitForAlarmNotifications(waitTimeout=20000, untilNumberOfAlarms=3, languageId=1033)"}
{"t":7090.215,"layer":"s7cp","dir":"RX","len":590,"hex":"720202463370000cb904000000000001000006596bd6805195010051010001000081a172007382947984800000a3816900150954656d704461695f31a3946e000d8a0e004200010000a3946f000287a3947000030102a39471001700000d919b120002879b13001018cd4d3de5007df69b141014119080821300000000000000000204010100009080800500000000000000000202000690808035000000000000000002020000908080350000000000000000020200049080803500000000000000000202000490a8817e0000000000000000020bfe093d46352b53332d4232000000000000000000000000000000000000000000009c3e0010000000000000000000a39f6f000803a3bd05001400110103af2b00000040010000000022000201a3bd6d0004868718a3951b4014a0a48001003d4d616368696e652077696c6c206f706572617465206174203830252073706565642064756520746f2070726f6475637420726571756972656d656e7473a0a4800200442869292028403525734029204d616368696e65204031256440202d205374617475733a2052756e6e696e67202d2052656475636564206d616368696e652073706565642ea0a48003000b424d4b3a20403525734020a0a48006001d416c61726d20636c6173733a20496e666f726d6174696f6e206f6e6c79a0a48009004f4c6f672066696c653a20284e616d65206f6620746865206c6f672066696c6520776865726520746865736520696e666f726d6174696f6e2d6f6e6c7920616c61726d73206172652073746f7265642900a20000000072020000"}
{"t":7095.333,"layer":"s7cp","dir":"RX","len":243,"hex":"720200eb3370000cb904000000000002010006596bd6807b81010051010001000081a172007383947984800000a3816900150954656d704461695f32a3946e000d8a0e002500010000a3946f000287a3947000030100a39471001700000d919b120002879b13001018cd4d3de51bc86b9b1410141190808213000000000000000002040101000000000000000000000000000000000000000000000000000000000000000000009c3e0010000000000000000000a39f6f000803a3bd05001400110103f3990000004b000000000023000001a3bd6d0004868719a3951b4014a0a4800200045465737400a20000000072020000"}
{"t":15040.527,"layer":"s7cp","dir":"RX","len":617,"hex":"720202613370000cb904000000000003020006596bd6f9abaf010051010001000081a172007382947984800000a3816900150954656d704461695f31a3946e000d8a0e003b00010000a3946f000287a3947000030103a39471001700000d919b120002879b13001018cd4d3fbf25e3909b141014119080821300000000000000000204010100009080800500000000000000000202000390808035000000000000000002020000908080350000000000000000020200019080803500000000000000000202000190a8817e0000000000000000020bfe093d46332b53312d4d31000000000000000000000000000000000000000000009c3e0010000000000000000000a39f6f000802a3bd050014001101034d8a0000003d090000000022000101a3bd6d000486871aa3951b4014a0a4800100745468652073656e736f722069732062726f6b656e2c2062757420746865206c696e652063616e207374696c6c206f7065726174652e20506c65617365207265706f72742074686520697373756520746f20746865206d616e616765722061742074686520656e64206f6620746865207368696674a0a4800200415761726e696e673a2028403525734029204d616368696e65204031256440202d205374617475733a2052756e6e696e67202d2042726f6b656e2073656e736f722ea0a48003000b424d4b3a20403525734020a0a48006001f416c61726d20636c6173733a204e6f2061636b6e6f776c656467656d656e74a0a480090034436f6e746163743a20284e616d65206f662074686520636f6e7461637420706572736f6e20666f72207468697320616c61726d2900a20000000072020000"}
{"t":15043.512,"type":"note","text":"AlarmSubscriptionDelete()"}
{"t":15045.561,"layer":"s7cp","dir":"TX","len":55,"hex":"7202002f31000004d40000000970000cb73470000cb800000004e88969001200000000896a001300896b00040000030000000072020000"}
{"t":15060.838,"layer":"s7cp","dir":"RX","len":28,"hex":"7202001432000004d400000009340070000cb80c0000000072020000"}
{"t":15063.242,"type":"note","text":"Disconnect()"}
{"t":15063.340,"layer":"s7cp","dir":"TX","len":55,"hex":"7202002f31000004d40000000a70000cb73470000cb700000004e88969001200000000896a001300896b00040000040000000072020000"}
{"t":15076.681,"layer":"s7cp","dir":"RX","len":35,"hex":"7202001b32000004d40000000a349088f08080828a802d70000cb70000000072020000"}|
@bvanelli I updated the alarm implementation from your traces: subscription creation/deletion now targets the subscription container with the captured framing, the exact subscription and child names are used, default credits are 10, and Could you please rerun alarm subscription creation, notification delivery, deletion, and an LCID-based active-alarm read against the current branch, then review again if those now work on the PLC? Install: pip install --upgrade "python-snap7 @ git+https://github.com/gijzelaerr/python-snap7.git@feat/794-s7commplus-alarm-support"All 42 CI checks pass; local validation reports 1,827 passed, 78 skipped. |
|
For it it didn't really work, I get a timeout instead when it is about receiving the notification: Traceback (most recent call last):
File "/Users/brunno.vanelli/Documents/git/python-snap7/s7commplus/connection.py", line 1534, in _recv_s7_data
return self._ssl_object.read(65536) # type: ignore[union-attr]
~~~~~~~~~~~~~~~~~~~~~^^^^^^^
File "/Users/brunno.vanelli/.local/share/uv/python/cpython-3.14.5-macos-aarch64-none/lib/python3.14/ssl.py", line 880, in read
v = self._sslobj.read(len)
ssl.SSLWantReadError: The operation did not complete (read) (_ssl.c:2711)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Users/brunno.vanelli/Documents/git/python-snap7/snap7/connection.py", line 466, in _recv_exact
chunk = self.socket.recv(size - len(data))
TimeoutError: timed out
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Users/brunno.vanelli/Documents/git/python-snap7/example/s7commplus1_alarms.py", line 52, in <module>
alarms = client.receive_alarm_notification()
File "/Users/brunno.vanelli/Documents/git/python-snap7/s7commplus/client.py", line 720, in receive_alarm_notification
return parse_alarm_notification(self._connection.receive_notification(), language_ids)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "/Users/brunno.vanelli/Documents/git/python-snap7/s7commplus/connection.py", line 865, in receive_notification
frame = self._recv_s7_data()
File "/Users/brunno.vanelli/Documents/git/python-snap7/s7commplus/connection.py", line 1536, in _recv_s7_data
self._tls_read_incoming()
~~~~~~~~~~~~~~~~~~~~~~~^^
File "/Users/brunno.vanelli/Documents/git/python-snap7/s7commplus/connection.py", line 1548, in _tls_read_incoming
data = self._iso_conn.receive_data()
File "/Users/brunno.vanelli/Documents/git/python-snap7/snap7/connection.py", line 204, in receive_data
tpkt_header = self._recv_exact(4)
File "/Users/brunno.vanelli/Documents/git/python-snap7/snap7/connection.py", line 473, in _recv_exact
raise S7TimeoutError("Receive timeout")
snap7.error.S7TimeoutError: Receive timeoutSent the byte exchange via email. |
|
Thanks — I inspected the exchange you sent by email. I will not copy the byte capture into this PR, tests, or fixtures. The response to Could you confirm whether you deliberately caused a new alarm transition after the subscription was created? Existing active alarms may be returned by
There is also a client-side behavior we should improve independently: an idle sync notification window currently surfaces as |
|
Ah, it makes sense that I did not receive a notification because, no data came until the timeout. By running it a couple of times, I now get data (all alarms are fake, BMKs are random): AlarmNotification(subscription_id=1879051449,
credit_tick=1,
sequence_number=0,
subscription_change_counter=0,
timestamp=1788363027507044,
alarms=(Alarm(cpu_alarm_id=9947888893196042240,
all_states_info=134,
domain=259,
message_type=2,
sequence_counter=4947,
name='TempDai_1',
state='going',
timestamp=1788363027503741271,
acknowledge_timestamp=0,
hmi_info=b'\x01\x033\xb0\x00\x00\x00>'
b'\t\x00\x00\x00\x00"\x00\x01\x01',
associated_values=(b'\x01\x01\x00\x00',
b'\x00\x04',
b'\x00\x00',
b'\x00\x05',
b'\x00\x05',
b'\xfe\t=F6+S2-G1',
b'',
b'',
b'',
b'',
b'',
b'',
b'',
b'',
b'',
b'',
b''),
texts={1033: AlarmText(language_id=1033,
info_text='The product '
'pallet has '
'almost been '
'completely '
'filled. '
'Please '
'exchange it '
'soon',
alarm_text='Warning: '
'(@5%s@) '
'Machine '
'@1%d@ - '
'Status: '
'Running - '
'Pallet '
'almost '
'full.',
additional_texts=('BMK: '
'@5%s@ ',
'',
'',
'Alarm '
'class: '
'No '
'acknowledgement',
'',
'',
'Contact: '
'(Name '
'of '
'the '
'contact '
'person '
'for '
'this '
'alarm)',
'',
''))}),))BTW, I'm not sure where you want to put this (if on the
You cannot compare browse alarms with notifications. Notifications are only sent on change, while browse alarms return the full state. But here is a consecutive read directly after receiving a notification. You see the same |
| frame = await asyncio.wait_for(receive, timeout) if timeout is not None else await receive | ||
| return parse_alarm_notification(frame, language_ids) | ||
|
|
||
| async def browse_alarms(self, language_ids: Optional[list[LanguageId | int]] = None) -> list[Alarm]: |
There was a problem hiding this comment.
I would insttead call it read_alarms for consistency, and hint on the docs that you should use one or the other (or both).
|
|
||
| self._connected = False | ||
| self._session_id = 0 | ||
| self._subscription_container_id = 0 |
There was a problem hiding this comment.
You introduce the same construct in https://github.com/gijzelaerr/python-snap7/pull/825/changes. Could having both data and alarm subscriptions cause problems here?
|
Addressed the hardware-test feedback in d91bbd3: alarm text placeholders now interpolate typed associated values (including the reported @1%d@ and @5%s@ forms), while Alarm.associated_values retains the raw bytes. Added read_alarms() as the canonical snapshot API and retained browse_alarms() as a compatibility alias. The receive methods now document that concurrent data/alarm receive loops on one connection are not supported yet. Local build, full tests (1778 passed, 82 skipped), and pre-commit all pass. |
|
In would not retain My opinion would be to just rename it, to keep it consistent with the other browse (explore). |
|
Agreed — I also merged current |
Summary
Alarm,AlarmText, andAlarmNotificationmodelsTesting
uv run --frozen pytest(1650 passed, 82 skipped)uv run --frozen pre-commit run --all-filesuv build --no-sourcesThe wire-level tests cover subscription filters, active alarm state, coming timestamps, multilingual text payloads, sync/async client APIs, and notification framing. Real PLC notification delivery still needs validation because alarm object contents and delivery behavior vary by firmware.
Fixes #794