aioads 0.1.0.dev8__tar.gz → 0.1.0.dev9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/PKG-INFO +1 -1
  2. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/ads_symbol_parser.py +39 -0
  3. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/pyproject.toml +2 -1
  4. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/test_ads_symbol_parser.py +155 -11
  5. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/.github/dependabot.yml +0 -0
  6. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/.github/workflows/ci.yml +0 -0
  7. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/.github/workflows/publish.yml +0 -0
  8. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/.gitignore +0 -0
  9. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/.vscode/settings.json +0 -0
  10. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/LICENSE +0 -0
  11. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/README.md +0 -0
  12. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/ads_client.py +0 -0
  13. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/ads_error_codes.py +0 -0
  14. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/ads_notifications.py +0 -0
  15. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/ads_symbol_cache.py +0 -0
  16. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/ams_address.py +0 -0
  17. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/ams_header.py +0 -0
  18. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/ams_service_port.py +0 -0
  19. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/ams_tcp_header.py +0 -0
  20. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/commands/ads_add_notification.py +0 -0
  21. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/commands/ads_command.py +0 -0
  22. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/commands/ads_delete_notification.py +0 -0
  23. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/commands/ads_read.py +0 -0
  24. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/commands/ads_read_device_info.py +0 -0
  25. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/commands/ads_read_state.py +0 -0
  26. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/commands/ads_read_write.py +0 -0
  27. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/commands/ads_write.py +0 -0
  28. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/commands/ads_write_state.py +0 -0
  29. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/commands/errors.py +0 -0
  30. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/errors.py +0 -0
  31. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/functions/ads_enable_route.py +0 -0
  32. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/functions/ads_function.py +0 -0
  33. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/functions/ads_sum_read.py +0 -0
  34. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/functions/ads_sum_read_write.py +0 -0
  35. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/functions/ads_symbol_datatype_by_name.py +0 -0
  36. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/functions/ads_symbol_datatype_upload.py +0 -0
  37. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/functions/ads_symbol_info_by_name_ex.py +0 -0
  38. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/functions/ads_symbol_table_version.py +0 -0
  39. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/functions/ads_symbol_upload.py +0 -0
  40. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/functions/ads_symbol_upload_info.py +0 -0
  41. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/py.typed +0 -0
  42. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/stream.py +0 -0
  43. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/transport.py +0 -0
  44. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/aioads/utils/local_ip.py +0 -0
  45. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/docs/transmission_mode.md +0 -0
  46. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/docs/unittest_style_guide.html +0 -0
  47. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/examples/read_cmd_reuse_mqtt.py +0 -0
  48. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/examples/read_cycles.py +0 -0
  49. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/examples/read_cycles_mqtt.py +0 -0
  50. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/examples/read_multiple.py +0 -0
  51. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/examples/read_multiple_mqtt.py +0 -0
  52. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/examples/read_single.py +0 -0
  53. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/pdm.lock +0 -0
  54. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/__init__.py +0 -0
  55. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/builders.py +0 -0
  56. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/integration/README.md +0 -0
  57. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/integration/__init__.py +0 -0
  58. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/integration/base.py +0 -0
  59. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/integration/config.example.toml +0 -0
  60. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/integration/config.py +0 -0
  61. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/integration/test_connection.py +0 -0
  62. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/integration/test_memory.py +0 -0
  63. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/integration/test_performance.py +0 -0
  64. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/integration/test_read_symbols.py +0 -0
  65. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/__init__.py +0 -0
  66. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/commands/__init__.py +0 -0
  67. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/commands/test_ads_add_notification.py +0 -0
  68. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/commands/test_ads_command.py +0 -0
  69. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/commands/test_ads_delete_notification.py +0 -0
  70. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/commands/test_ads_read.py +0 -0
  71. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/commands/test_ads_read_device_info.py +0 -0
  72. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/commands/test_ads_read_state.py +0 -0
  73. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/commands/test_ads_read_write.py +0 -0
  74. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/commands/test_ads_write.py +0 -0
  75. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/commands/test_ads_write_state.py +0 -0
  76. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/commands/test_errors.py +0 -0
  77. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/functions/__init__.py +0 -0
  78. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/functions/test_ads_enable_route.py +0 -0
  79. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/functions/test_ads_function.py +0 -0
  80. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/functions/test_ads_sum_read.py +0 -0
  81. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/functions/test_ads_sum_read_write.py +0 -0
  82. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/functions/test_ads_symbol_datatype_by_name.py +0 -0
  83. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/functions/test_ads_symbol_datatype_upload.py +0 -0
  84. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/functions/test_ads_symbol_info_by_name_ex.py +0 -0
  85. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/functions/test_ads_symbol_table_version.py +0 -0
  86. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/functions/test_ads_symbol_upload.py +0 -0
  87. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/functions/test_ads_symbol_upload_info.py +0 -0
  88. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/test_ads_client.py +0 -0
  89. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/test_ads_error_codes.py +0 -0
  90. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/test_ads_notifications.py +0 -0
  91. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/test_ads_symbol_cache.py +0 -0
  92. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/test_ams_address.py +0 -0
  93. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/test_ams_header.py +0 -0
  94. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/test_ams_tcp_header.py +0 -0
  95. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/test_errors.py +0 -0
  96. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/test_stream.py +0 -0
  97. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/test_transport.py +0 -0
  98. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/utils/__init__.py +0 -0
  99. {aioads-0.1.0.dev8 → aioads-0.1.0.dev9}/tests/unit/utils/test_local_ip.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioads
3
- Version: 0.1.0.dev8
3
+ Version: 0.1.0.dev9
4
4
  Summary: An asynchronous Python library for communicating with Beckhoff TwinCAT PLCs
5
5
  Author-email: MkKiefer <102972583+MkKiefer@users.noreply.github.com>
6
6
  License: MIT
@@ -134,6 +134,37 @@ class AdsSymbolParser(ISymbolParser):
134
134
 
135
135
  def __init__(self, data_types: list[SymbolDataTypeResponse]):
136
136
  self._type_lookup = {dt.name: dt for dt in data_types}
137
+ # Resolution of IEC 61131-3 elementary type names to their wire encoding.
138
+ self._elementary_type_names: dict[str, AdsSymbolDataType] = {
139
+ "BOOL": AdsSymbolDataType.BIT,
140
+ "BIT": AdsSymbolDataType.BIT,
141
+ "SINT": AdsSymbolDataType.INT8,
142
+ "USINT": AdsSymbolDataType.UINT8,
143
+ "BYTE": AdsSymbolDataType.UINT8,
144
+ "INT": AdsSymbolDataType.INT16,
145
+ "UINT": AdsSymbolDataType.UINT16,
146
+ "WORD": AdsSymbolDataType.UINT16,
147
+ "DINT": AdsSymbolDataType.INT32,
148
+ "UDINT": AdsSymbolDataType.UINT32,
149
+ "DWORD": AdsSymbolDataType.UINT32,
150
+ "LINT": AdsSymbolDataType.INT64,
151
+ "ULINT": AdsSymbolDataType.UINT64,
152
+ "LWORD": AdsSymbolDataType.UINT64,
153
+ "REAL": AdsSymbolDataType.REAL32,
154
+ "LREAL": AdsSymbolDataType.REAL64,
155
+ "TIME": AdsSymbolDataType.UINT32,
156
+ "TOD": AdsSymbolDataType.UINT32,
157
+ "TIME_OF_DAY": AdsSymbolDataType.UINT32,
158
+ "DATE": AdsSymbolDataType.UINT32,
159
+ "DT": AdsSymbolDataType.UINT32,
160
+ "DATE_AND_TIME": AdsSymbolDataType.UINT32,
161
+ "LTIME": AdsSymbolDataType.UINT64,
162
+ "LTOD": AdsSymbolDataType.UINT64,
163
+ "LTIME_OF_DAY": AdsSymbolDataType.UINT64,
164
+ "LDATE": AdsSymbolDataType.UINT64,
165
+ "LDT": AdsSymbolDataType.UINT64,
166
+ "LDATE_AND_TIME": AdsSymbolDataType.UINT64,
167
+ }
137
168
  self._const_type_names: dict[str, Callable[[str, AdsStream], Any]] = {
138
169
  "Tc2_System.T_MaxString": lambda type_name, raw_data: raw_data.read(255)
139
170
  .decode("latin-1")
@@ -142,6 +173,7 @@ class AdsSymbolParser(ISymbolParser):
142
173
  .decode("latin-1")
143
174
  .rstrip("\x00"),
144
175
  }
176
+
145
177
  self._primitive_parser = PrimitiveTypeParser()
146
178
 
147
179
  def update_datatypes(self, data_types: list[SymbolDataTypeResponse]) -> None:
@@ -265,6 +297,13 @@ class AdsSymbolParser(ISymbolParser):
265
297
  if data_type != AdsSymbolDataType.BIGTYPE:
266
298
  return self._primitive_parser.parse(data_type, type_name, raw_data)
267
299
 
300
+ # TwinCAT 2 reports BIGTYPE for some elementary types
301
+ # like INT, UINT, REAL, etc. So we need to resolve them by name.
302
+ if type_name in self._elementary_type_names:
303
+ return self._primitive_parser.parse(
304
+ self._elementary_type_names[type_name], type_name, raw_data
305
+ )
306
+
268
307
  # Parsing of structured types
269
308
  if data_type == AdsSymbolDataType.BIGTYPE:
270
309
  return self.parse_struct(type_name, raw_data)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aioads"
3
- version = "0.1.0.dev8"
3
+ version = "0.1.0.dev9"
4
4
  description = "An asynchronous Python library for communicating with Beckhoff TwinCAT PLCs"
5
5
  authors = [
6
6
  { name = "MkKiefer", email = "102972583+MkKiefer@users.noreply.github.com" },
@@ -78,4 +78,5 @@ disable = [
78
78
  "too-many-locals",
79
79
  "too-few-public-methods",
80
80
  "broad-exception-caught",
81
+ "too-many-return-statements",
81
82
  ]
@@ -3,6 +3,8 @@
3
3
  import struct
4
4
  import unittest
5
5
 
6
+ from parameterized import parameterized
7
+
6
8
  from aioads.ads_symbol_parser import AdsSymbolParser, PrimitiveTypeParser
7
9
  from aioads.functions.ads_symbol_datatype_by_name import SymbolDataTypeResponse
8
10
  from aioads.functions.ads_symbol_info_by_name_ex import (
@@ -89,7 +91,8 @@ class TestPrimitiveTypeParser(unittest.TestCase):
89
91
  stream = make_stream(b"Hi\x00\x00\x00\x00")
90
92
 
91
93
  # Act
92
- result = self.parser.parse(AdsSymbolDataType.STRING, "STRING(5)", stream)
94
+ result = self.parser.parse(
95
+ AdsSymbolDataType.STRING, "STRING(5)", stream)
93
96
 
94
97
  # Assert
95
98
  self.assertEqual(result, "Hi")
@@ -110,7 +113,8 @@ class TestPrimitiveTypeParser(unittest.TestCase):
110
113
  stream = make_stream(raw)
111
114
 
112
115
  # Act
113
- result = self.parser.parse(AdsSymbolDataType.WSTRING, "WSTRING(3)", stream)
116
+ result = self.parser.parse(
117
+ AdsSymbolDataType.WSTRING, "WSTRING(3)", stream)
114
118
 
115
119
  # Assert
116
120
  self.assertEqual(result, "Hi")
@@ -136,7 +140,8 @@ class TestAdsSymbolParser(unittest.TestCase):
136
140
  stream = make_stream(bytes(range(8)))
137
141
 
138
142
  # Act
139
- result = self.parser.parse(AdsSymbolDataType.BIGTYPE, "REFERENCE TO ST_Foo", stream)
143
+ result = self.parser.parse(
144
+ AdsSymbolDataType.BIGTYPE, "REFERENCE TO ST_Foo", stream)
140
145
 
141
146
  # Assert
142
147
  self.assertEqual(result, "0001020304050607^")
@@ -146,7 +151,8 @@ class TestAdsSymbolParser(unittest.TestCase):
146
151
  stream = make_stream(bytes(range(8)))
147
152
 
148
153
  # Act
149
- result = self.parser.parse(AdsSymbolDataType.BIGTYPE, "POINTER TO ST_Foo", stream)
154
+ result = self.parser.parse(
155
+ AdsSymbolDataType.BIGTYPE, "POINTER TO ST_Foo", stream)
150
156
 
151
157
  # Assert
152
158
  self.assertEqual(result, "0001020304050607*")
@@ -181,7 +187,8 @@ class TestAdsSymbolParser(unittest.TestCase):
181
187
 
182
188
  # Act / Assert
183
189
  with self.assertRaises(ValueError) as ctx:
184
- self.parser.parse(AdsSymbolDataType.INT16, "ARRAY OF nonsense", stream)
190
+ self.parser.parse(AdsSymbolDataType.INT16,
191
+ "ARRAY OF nonsense", stream)
185
192
 
186
193
  self.assertIn("Cannot parse array type", str(ctx.exception))
187
194
 
@@ -200,14 +207,18 @@ class TestAdsSymbolParser(unittest.TestCase):
200
207
 
201
208
  def test_parse_struct_with_sub_items_returns_dict(self) -> None:
202
209
  # Arrange
203
- sub_a = make_datatype("a", size=2, data_type=AdsSymbolDataType.INT16, type_name="INT16", offs=0)
204
- sub_b = make_datatype("b", size=2, data_type=AdsSymbolDataType.INT16, type_name="INT16", offs=2)
205
- struct_type = make_datatype("ST_Pair", size=4, sub_items=[sub_a, sub_b])
210
+ sub_a = make_datatype(
211
+ "a", size=2, data_type=AdsSymbolDataType.INT16, type_name="INT16", offs=0)
212
+ sub_b = make_datatype(
213
+ "b", size=2, data_type=AdsSymbolDataType.INT16, type_name="INT16", offs=2)
214
+ struct_type = make_datatype(
215
+ "ST_Pair", size=4, sub_items=[sub_a, sub_b])
206
216
  self.parser.update_datatypes([struct_type])
207
217
  stream = make_stream(struct.pack("<hh", 10, 20))
208
218
 
209
219
  # Act
210
- result = self.parser.parse(AdsSymbolDataType.BIGTYPE, "ST_Pair", stream)
220
+ result = self.parser.parse(
221
+ AdsSymbolDataType.BIGTYPE, "ST_Pair", stream)
211
222
 
212
223
  # Assert
213
224
  self.assertEqual(result, {"a": 10, "b": 20})
@@ -222,18 +233,151 @@ class TestAdsSymbolParser(unittest.TestCase):
222
233
 
223
234
  self.assertIn("unknown type name", str(ctx.exception))
224
235
 
236
+ @parameterized.expand([
237
+ ("BOOL", b"\x01", True),
238
+ ("INT", struct.pack("<h", -7), -7),
239
+ ("WORD", struct.pack("<H", 512), 512),
240
+ ("DINT", struct.pack("<i", -100_000), -100_000),
241
+ ("LWORD", struct.pack("<Q", 2**40), 2**40),
242
+ ("LREAL", struct.pack("<d", 2.5), 2.5),
243
+ ])
244
+ def test_parse_bigtype_with_elementary_name_resolves_by_name(
245
+ self, type_name: str, raw: bytes, expected: object
246
+ ) -> None:
247
+ """TwinCAT 2 reports BIGTYPE even for elementary types like INT."""
248
+ # Arrange
249
+ stream = make_stream(raw)
250
+
251
+ # Act
252
+ result = self.parser.parse(AdsSymbolDataType.BIGTYPE, type_name, stream)
253
+
254
+ # Assert
255
+ self.assertEqual(result, expected)
256
+
257
+ def test_parse_time_returns_milliseconds(self) -> None:
258
+ # Arrange
259
+ stream = make_stream(struct.pack("<I", 90_500))
260
+
261
+ # Act
262
+ result = self.parser.parse(AdsSymbolDataType.BIGTYPE, "TIME", stream)
263
+
264
+ # Assert
265
+ self.assertEqual(result, 90_500)
266
+
267
+ def test_parse_ltime_returns_nanoseconds(self) -> None:
268
+ # Arrange
269
+ stream = make_stream(struct.pack("<Q", 1_500_000_000))
270
+
271
+ # Act
272
+ result = self.parser.parse(AdsSymbolDataType.BIGTYPE, "LTIME", stream)
273
+
274
+ # Assert
275
+ self.assertEqual(result, 1_500_000_000)
276
+
277
+ def test_parse_time_of_day_returns_milliseconds_since_midnight(self) -> None:
278
+ # Arrange
279
+ milliseconds_since_midnight = (13 * 3600 + 37 * 60 + 5) * 1000 + 250
280
+ stream = make_stream(struct.pack("<I", milliseconds_since_midnight))
281
+
282
+ # Act
283
+ result = self.parser.parse(AdsSymbolDataType.BIGTYPE, "TOD", stream)
284
+
285
+ # Assert
286
+ self.assertEqual(result, milliseconds_since_midnight)
287
+
288
+ def test_parse_date_returns_epoch_seconds(self) -> None:
289
+ # Arrange
290
+ stream = make_stream(struct.pack("<I", 1_752_364_800)) # 2025-07-13
291
+
292
+ # Act
293
+ result = self.parser.parse(AdsSymbolDataType.BIGTYPE, "DATE", stream)
294
+
295
+ # Assert
296
+ self.assertEqual(result, 1_752_364_800)
297
+
298
+ def test_parse_date_and_time_returns_epoch_seconds(self) -> None:
299
+ # Arrange
300
+ # 2025-07-13T11:45:06Z
301
+ stream = make_stream(struct.pack("<I", 1_752_407_106))
302
+
303
+ # Act
304
+ result = self.parser.parse(AdsSymbolDataType.BIGTYPE, "DT", stream)
305
+
306
+ # Assert
307
+ self.assertEqual(result, 1_752_407_106)
308
+
309
+ @parameterized.expand([
310
+ ("LTIME",),
311
+ ("LTOD",),
312
+ ("LTIME_OF_DAY",),
313
+ ("LDATE",),
314
+ ("LDT",),
315
+ ("LDATE_AND_TIME",),
316
+ ])
317
+ def test_parse_long_datetime_types_return_nanoseconds(self, type_name: str) -> None:
318
+ # Arrange
319
+ stream = make_stream(struct.pack("<Q", 1_752_407_106_000_000_000))
320
+
321
+ # Act
322
+ result = self.parser.parse(AdsSymbolDataType.BIGTYPE, type_name, stream)
323
+
324
+ # Assert
325
+ self.assertEqual(result, 1_752_407_106_000_000_000)
326
+
327
+ @parameterized.expand([
328
+ ("TIME_OF_DAY",),
329
+ ("DATE_AND_TIME",),
330
+ ])
331
+ def test_parse_long_form_datetime_aliases_return_integer(self, type_name: str) -> None:
332
+ # Arrange
333
+ stream = make_stream(struct.pack("<I", 42_000))
334
+
335
+ # Act
336
+ result = self.parser.parse(AdsSymbolDataType.BIGTYPE, type_name, stream)
337
+
338
+ # Assert
339
+ self.assertEqual(result, 42_000)
340
+
341
+ def test_parse_array_of_time_returns_millisecond_list(self) -> None:
342
+ # Arrange
343
+ stream = make_stream(struct.pack("<II", 1000, 2000))
344
+
345
+ # Act
346
+ result = self.parser.parse(
347
+ AdsSymbolDataType.BIGTYPE, "ARRAY [0..1] OF TIME", stream
348
+ )
349
+
350
+ # Assert
351
+ self.assertEqual(result, [1000, 2000])
352
+
353
+ def test_parse_struct_with_time_sub_item_returns_millisecond_field(self) -> None:
354
+ # Arrange
355
+ sub_time = make_datatype("duration", size=4, type_name="TIME", offs=0)
356
+ struct_type = make_datatype("ST_Timed", size=4, sub_items=[sub_time])
357
+ self.parser.update_datatypes([struct_type])
358
+ stream = make_stream(struct.pack("<I", 250))
359
+
360
+ # Act
361
+ result = self.parser.parse(
362
+ AdsSymbolDataType.BIGTYPE, "ST_Timed", stream)
363
+
364
+ # Assert
365
+ self.assertEqual(result, {"duration": 250})
366
+
225
367
  def test_update_datatypes_registers_new_type_for_lookup(self) -> None:
226
368
  # Arrange
227
369
  struct_type = make_datatype(
228
370
  "ST_Single",
229
371
  size=2,
230
- sub_items=[make_datatype("v", size=2, data_type=AdsSymbolDataType.INT16, type_name="INT16")],
372
+ sub_items=[make_datatype(
373
+ "v", size=2, data_type=AdsSymbolDataType.INT16, type_name="INT16")],
231
374
  )
232
375
 
233
376
  # Act
234
377
  self.parser.update_datatypes([struct_type])
235
378
  result = self.parser.parse(
236
- AdsSymbolDataType.BIGTYPE, "ST_Single", make_stream(struct.pack("<h", 42))
379
+ AdsSymbolDataType.BIGTYPE, "ST_Single", make_stream(
380
+ struct.pack("<h", 42))
237
381
  )
238
382
 
239
383
  # Assert
File without changes
File without changes
File without changes
File without changes
File without changes