Skip to content

Add S7CommPlus alarm handling and subscriptions - #820

Open
gijzelaerr wants to merge 2 commits into
masterfrom
feat/794-s7commplus-alarm-support
Open

Add S7CommPlus alarm handling and subscriptions#820
gijzelaerr wants to merge 2 commits into
masterfrom
feat/794-s7commplus-alarm-support

Conversation

@gijzelaerr

@gijzelaerr gijzelaerr commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • add sync and async alarm subscription create/delete APIs
  • add active-alarm browsing with multilingual text filtering
  • decode unsolicited alarm notifications into public Alarm, AlarmText, and AlarmNotification models
  • default subscriptions to unlimited notification credit so event delivery does not stop after a fixed batch

Testing

  • uv run --frozen pytest (1650 passed, 82 skipped)
  • uv run --frozen pre-commit run --all-files
  • uv build --no-sources

The 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

Comment thread s7commplus/alarm.py Outdated
class AlarmText:
"""The texts for one alarm in one PLC language."""

language_id: int

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"}

Comment thread s7commplus/async_client.py Outdated
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]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"}

@gijzelaerr

Copy link
Copy Markdown
Owner Author

@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 LanguageId is an IntEnum accepted by the public APIs. Byte-exact create/delete fixtures are included.

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.

@gijzelaerr
gijzelaerr requested a review from bvanelli August 20, 2026 06:50
@bvanelli

bvanelli commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

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 timeout

Sent the byte exchange via email.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Alarm handling and subscriptions (S7CommPlus)

2 participants