Add S7CommPlus alarm handling and subscriptions - #820
Conversation
| class AlarmText: | ||
| """The texts for one alarm in one PLC language.""" | ||
|
|
||
| language_id: int |
There was a problem hiding this comment.
Everywhere you use a language_id (also on function calls), I would type it with an IntEnum LCID.
This is defined openly here: https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a
You can see that the list from Siemens for the old WinCC does match: https://cache.industry.siemens.com/dl/files/303/22449303/att_94143/v1/wincc_asia_a_01_d.pdf
I know for a fact that some impl. do a coalesce whenever the language is not an exact match: for example, the (1043 Dutch - Netherlands) is requested, but only (2067 Dutch - Belgium) is available, it will select it. Not sure if the PLC is the one doing it, will need to check.
| 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.
You can read alarms both via sampling or subscription. Subscription is in general better, but I think both should be supported (e.g. loss of application state would trigger a sampling). It is also easier than managing subscriptions (one simple loop).
Here is a trace for it:
{"t":470.347,"type":"note","text":"GetActiveAlarms(languageId=1033)"}
{"t":471.350,"layer":"s7cp","dir":"TX","len":59,"hex":"7202003331000004bb0000000870000cb73400000008946b010100000a946e946f9470947194759f6fbd058169bd6d951b04000000000072020000"}
{"t":485.317,"layer":"s7cp","dir":"RX","len":979,"hex":"720203cf32000004bb000000083400000000000ca172007342947984800000a381690015094578706c4461695f32a3946e000d8a0e003900010000a3946f000285a3947000030101a39471001700000d919b120002859b13001018cb0d5cb407beaf9b141014119080821300000000000000000204010100009080800500000000000000000202000290808035000000000000000002020002908080350000000000000000020200029080803500000000000000000202000290a8817e0000000000000000020bfe093d46322b53332d4231000000000000000000000000000000000000000000009c3e0010000000000000000000a39475001700000d919b120002049b13001000000000000000009b14101411000000000000000000000000000000000000000000000000000000000000000000009c3e001018cb0d5cb407beaf00a39f6f000801a3bd05001400110103d50a0000003c100000000021000300a3bd6d0004cd0ca3951b4014a0a480010081094f6e65206f662074686520706172747320697320737475636b206f6e20746865206c696e652c20736f20746865206c696e6520686173206265656e2073746f707065642e20506c656173652061636b6e6f776c656467652074686520616c61726d20616e64207265706f72742074686520697373756520746f2074686520736166657479207465616da0a48002003a4552524f523a2028403525734029204d616368696e65204031256440202d205374617475733a204572726f72202d20537475636b20706172742ea0a48003000b424d4b3a20403525734020a0a48006001c416c61726d20636c6173733a2041636b6e6f776c656467656d656e74a0a480090034436f6e746163743a20284e616d65206f662074686520636f6e7461637420706572736f6e20666f72207468697320616c61726d2900a2a172007342947984800000a381690015094578706c4461695f32a3946e000d8a0e003800010000a3946f000285a3947000030101a39471001700000d919b120002859b13001018cb0d5935e6d7cb9b141014119080821300000000000000000204010100009080800500000000000000000202000190808035000000000000000002020002908080350000000000000000020200009080803500000000000000000202000090a8817e0000000000000000020cfe0a3d46312b41312d4b4531000000000000000000000000000000000000000000009c3e0010000000000000000000a39475001700000d919b120002049b13001000000000000000009b14101411000000000000000000000000000000000000000000000000000000000000000000009c3e0010"}
{"t":485.789,"layer":"s7cp","dir":"RX","len":403,"hex":"7202018b18cb0d5935e6d7cb00a39f6f000801a3bd0500140011010373a60000003b100000000021000300a3bd6d0004cd0ba3951b4014a0a4800100811054686520656d657267656e63792073746f7020627574746f6e20686173206265656e20707265737365642e20506c656173652061636b6e6f776c656467652074686520616c61726d2c20636c656172207468652070726f626c656d206f6e20746865206c696e6520616e64207468656e2072657365742074686520656d657267656e63792073746f7020627574746f6ea0a4800200484552524f523a2028403525734029204d616368696e65204031256440202d205374617475733a204572726f72202d20456d657267656e637920627574746f6e20707265737365642ea0a48003000b424d4b3a20403525734020a0a48006001c416c61726d20636c6173733a2041636b6e6f776c656467656d656e74a0a480090034436f6e746163743a20284e616d65206f662074686520636f6e7461637420706572736f6e20666f72207468697320616c61726d2900a20000000072020000"}| 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[int]] = None) -> list[Alarm]: |
There was a problem hiding this comment.
This works fine for me (some data is binary, they likely reference something) 👍
| 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. |
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 validation is still valuable because alarm object contents vary by firmware.
Fixes #794