slmp-connect-python 1.0.0__tar.gz → 1.0.1__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 (24) hide show
  1. {slmp_connect_python-1.0.0/slmp_connect_python.egg-info → slmp_connect_python-1.0.1}/PKG-INFO +1 -1
  2. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/pyproject.toml +1 -1
  3. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/__init__.py +1 -1
  4. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/_operations.py +12 -12
  5. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/async_client.py +1 -1
  6. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/device_ranges.py +1 -1
  7. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/utils.py +34 -44
  8. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1/slmp_connect_python.egg-info}/PKG-INFO +1 -1
  9. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/LICENSE +0 -0
  10. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/MANIFEST.in +0 -0
  11. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/README.md +0 -0
  12. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/setup.cfg +0 -0
  13. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/cli.py +0 -0
  14. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/client.py +0 -0
  15. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/constants.py +0 -0
  16. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/core.py +0 -0
  17. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/error_codes.py +0 -0
  18. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/errors.py +0 -0
  19. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp/py.typed +0 -0
  20. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp_connect_python.egg-info/SOURCES.txt +0 -0
  21. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp_connect_python.egg-info/dependency_links.txt +0 -0
  22. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp_connect_python.egg-info/entry_points.txt +0 -0
  23. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp_connect_python.egg-info/requires.txt +0 -0
  24. {slmp_connect_python-1.0.0 → slmp_connect_python-1.0.1}/slmp_connect_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slmp-connect-python
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: SLMP Connect Python: client library for MELSEC SLMP binary communication
5
5
  Author: fa-yoshinobu
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "slmp-connect-python"
7
- version = "1.0.0"
7
+ version = "1.0.1"
8
8
  description = "SLMP Connect Python: client library for MELSEC SLMP binary communication"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -10,7 +10,7 @@ The primary user-facing entry points are:
10
10
  - ``poll``
11
11
  """
12
12
 
13
- __version__ = "0.8.0"
13
+ __version__ = "1.0.1"
14
14
 
15
15
  from .async_client import AsyncSlmpClient
16
16
  from .client import SlmpClient
@@ -85,7 +85,7 @@ def _effective_series(series: PLCSeries | str | None, default_series: PLCSeries)
85
85
  return PLCSeries(series) if series is not None else default_series
86
86
 
87
87
 
88
- def _parse_device_for_family(device: str | DeviceRef, address_profile: object | None) -> DeviceRef:
88
+ def _parse_device_for_address_profile(device: str | DeviceRef, address_profile: object | None) -> DeviceRef:
89
89
  ref = parse_device(device, plc_profile=address_profile)
90
90
  return _require_explicit_plc_profile_for_xy(device, address_profile, ref)
91
91
 
@@ -110,7 +110,7 @@ def build_read_devices_request(
110
110
  ) -> OperationRequest:
111
111
  _check_direct_device_points(points, bit_unit=bit_unit, name="read_devices")
112
112
  effective_series = _effective_series(series, default_series)
113
- ref = _parse_device_for_family(device, address_profile)
113
+ ref = _parse_device_for_address_profile(device, address_profile)
114
114
  _validate_direct_read_device(ref, points=points, bit_unit=bit_unit)
115
115
  _check_temporarily_unsupported_device(ref)
116
116
  _warn_practical_device_path(ref, series=effective_series, access_kind="direct")
@@ -154,7 +154,7 @@ def build_write_devices_request(
154
154
  raise ValueError("values must not be empty")
155
155
  _check_direct_device_points(len(values), bit_unit=bit_unit, name="write_devices")
156
156
  effective_series = _effective_series(series, default_series)
157
- ref = _parse_device_for_family(device, address_profile)
157
+ ref = _parse_device_for_address_profile(device, address_profile)
158
158
  _validate_direct_write_device(ref, bit_unit=bit_unit)
159
159
  _check_temporarily_unsupported_device(ref)
160
160
  _warn_practical_device_path(ref, series=effective_series, access_kind="direct")
@@ -188,7 +188,7 @@ def build_read_dwords_request(
188
188
  ) -> OperationRequest:
189
189
  if count < 1:
190
190
  raise ValueError("count must be >= 1")
191
- ref = _parse_device_for_family(device, address_profile)
191
+ ref = _parse_device_for_address_profile(device, address_profile)
192
192
  _validate_direct_dword_read_device(ref)
193
193
  return build_read_devices_request(
194
194
  ref,
@@ -342,12 +342,12 @@ def build_register_monitor_devices_request(
342
342
  word_refs: list[DeviceRef] = []
343
343
  dword_refs: list[DeviceRef] = []
344
344
  for dev in word_devices:
345
- ref = _parse_device_for_family(dev, address_profile)
345
+ ref = _parse_device_for_address_profile(dev, address_profile)
346
346
  _check_temporarily_unsupported_device(ref)
347
347
  payload += encode_device_spec(ref, series=effective_series)
348
348
  word_refs.append(ref)
349
349
  for dev in dword_devices:
350
- ref = _parse_device_for_family(dev, address_profile)
350
+ ref = _parse_device_for_address_profile(dev, address_profile)
351
351
  _check_temporarily_unsupported_device(ref)
352
352
  payload += encode_device_spec(ref, series=effective_series)
353
353
  dword_refs.append(ref)
@@ -499,8 +499,8 @@ def build_read_random_request(
499
499
  )
500
500
  subcommand = resolve_device_subcommand(bit_unit=False, series=effective_series, extension=False)
501
501
 
502
- words = [_parse_device_for_family(device, address_profile) for device in word_devices]
503
- dwords = [_parse_device_for_family(device, address_profile) for device in dword_devices]
502
+ words = [_parse_device_for_address_profile(device, address_profile) for device in word_devices]
503
+ dwords = [_parse_device_for_address_profile(device, address_profile) for device in dword_devices]
504
504
  _validate_random_read_devices(words, dwords)
505
505
  _check_temporarily_unsupported_devices(words)
506
506
  _check_temporarily_unsupported_devices(dwords)
@@ -718,7 +718,7 @@ def build_read_block_request(
718
718
  normalized_bit: list[tuple[DeviceRef, int]] = []
719
719
  for device, points in word_blocks:
720
720
  _check_points_u16(points, "word_block points")
721
- ref = _parse_device_for_family(device, address_profile)
721
+ ref = _parse_device_for_address_profile(device, address_profile)
722
722
  _check_temporarily_unsupported_device(ref)
723
723
  _warn_practical_device_path(ref, series=effective_series, access_kind="direct")
724
724
  normalized_word.append((ref, points))
@@ -726,7 +726,7 @@ def build_read_block_request(
726
726
  payload += points.to_bytes(2, "little")
727
727
  for device, points in bit_blocks:
728
728
  _check_points_u16(points, "bit_block points")
729
- ref = _parse_device_for_family(device, address_profile)
729
+ ref = _parse_device_for_address_profile(device, address_profile)
730
730
  _check_temporarily_unsupported_device(ref)
731
731
  _warn_practical_device_path(ref, series=effective_series, access_kind="direct")
732
732
  normalized_bit.append((ref, points))
@@ -784,13 +784,13 @@ def build_write_block_request(
784
784
  word_refs: list[DeviceRef] = []
785
785
  bit_refs: list[DeviceRef] = []
786
786
  for device, values in word_blocks:
787
- ref = _parse_device_for_family(device, address_profile)
787
+ ref = _parse_device_for_address_profile(device, address_profile)
788
788
  _check_temporarily_unsupported_device(ref)
789
789
  _warn_practical_device_path(ref, series=effective_series, access_kind="direct")
790
790
  _check_points_u16(len(values), "word block size")
791
791
  word_refs.append(ref)
792
792
  for device, values in bit_blocks:
793
- ref = _parse_device_for_family(device, address_profile)
793
+ ref = _parse_device_for_address_profile(device, address_profile)
794
794
  _check_temporarily_unsupported_device(ref)
795
795
  _warn_practical_device_path(ref, series=effective_series, access_kind="direct")
796
796
  _check_points_u16(len(values), "bit block size")
@@ -91,7 +91,7 @@ class AsyncSlmpClient:
91
91
 
92
92
  The standard async client route requires ``plc_profile`` and fixes the
93
93
  frame type, access profile, and address/range handling from that one
94
- explicit family.
94
+ explicit PLC profile.
95
95
  """
96
96
  self.host = host
97
97
  self.transport_type = transport.lower()
@@ -518,7 +518,7 @@ def normalize_plc_profile(value: SlmpPlcProfile | str) -> SlmpPlcProfile:
518
518
 
519
519
  if isinstance(value, SlmpPlcProfile):
520
520
  return value
521
- normalized = str(value).strip().lower()
521
+ normalized = str(value).strip()
522
522
  if normalized in _CANONICAL_FAMILIES:
523
523
  return _CANONICAL_FAMILIES[normalized]
524
524
  supported = ", ".join(sorted(_CANONICAL_FAMILIES))
@@ -13,7 +13,6 @@ from .constants import DEVICE_CODES, DeviceUnit, FrameType, PLCSeries
13
13
  from .core import (
14
14
  DeviceRef,
15
15
  SlmpTarget,
16
- _normalize_plc_profile_hint,
17
16
  _require_explicit_plc_profile_for_xy,
18
17
  _resolve_connection_profile,
19
18
  _resolve_plc_profile_defaults,
@@ -142,30 +141,30 @@ class SlmpAddress:
142
141
 
143
142
 
144
143
  def _client_address_profile(client: object) -> str | None:
145
- family = getattr(client, "address_profile", None)
146
- if family is None:
144
+ address_profile = getattr(client, "address_profile", None)
145
+ if address_profile is None:
147
146
  return None
148
- if isinstance(family, str):
149
- return family
150
- value = getattr(family, "value", None)
147
+ if isinstance(address_profile, str):
148
+ return address_profile
149
+ value = getattr(address_profile, "value", None)
151
150
  if isinstance(value, str):
152
151
  return value
153
152
  return None
154
153
 
155
154
 
156
- def _parse_device_for_family(
155
+ def _parse_device_for_address_profile(
157
156
  device: str | DeviceRef,
158
- family: object | None = None,
157
+ address_profile: object | None = None,
159
158
  ) -> DeviceRef:
160
- ref = parse_device(device, plc_profile=family)
161
- return _require_explicit_plc_profile_for_xy(device, family, ref)
159
+ ref = parse_device(device, plc_profile=address_profile)
160
+ return _require_explicit_plc_profile_for_xy(device, address_profile, ref)
162
161
 
163
162
 
164
163
  def _parse_device_for_client(
165
164
  client: object,
166
165
  device: str | DeviceRef,
167
166
  ) -> DeviceRef:
168
- return _parse_device_for_family(device, _client_address_profile(client))
167
+ return _parse_device_for_address_profile(device, _client_address_profile(client))
169
168
 
170
169
 
171
170
  def _validate_dword_read_target(client: object, device: str | DeviceRef) -> DeviceRef:
@@ -183,7 +182,7 @@ async def read_typed(
183
182
  client: AsyncSlmpClient,
184
183
  device: str | DeviceRef,
185
184
  dtype: str,
186
- ) -> int | float:
185
+ ) -> int | float | bool:
187
186
  """Read one logical value and convert it to a Python scalar.
188
187
 
189
188
  Args:
@@ -225,7 +224,7 @@ async def write_typed(
225
224
  client: AsyncSlmpClient,
226
225
  device: str | DeviceRef,
227
226
  dtype: str,
228
- value: int | float,
227
+ value: int | float | bool,
229
228
  ) -> None:
230
229
  """Write one logical value using the requested application type.
231
230
 
@@ -266,7 +265,7 @@ def read_typed_sync(
266
265
  client: SlmpClient,
267
266
  device: str | DeviceRef,
268
267
  dtype: str,
269
- ) -> int | float:
268
+ ) -> int | float | bool:
270
269
  """Synchronously read one logical value as a Python scalar."""
271
270
  ref = _parse_device_for_client(client, device)
272
271
  key = dtype.upper()
@@ -297,7 +296,7 @@ def write_typed_sync(
297
296
  client: SlmpClient,
298
297
  device: str | DeviceRef,
299
298
  dtype: str,
300
- value: int | float,
299
+ value: int | float | bool,
301
300
  ) -> None:
302
301
  """Synchronously write one logical value using the requested type."""
303
302
  ref = _parse_device_for_client(client, device)
@@ -413,7 +412,7 @@ async def read_named(
413
412
  The address list is compiled once, then grouped into random reads where
414
413
  possible. Use ``.bit`` notation only with word devices.
415
414
  """
416
- plan = _compile_read_plan(addresses, family=_client_address_profile(client))
415
+ plan = _compile_read_plan(addresses, address_profile=_client_address_profile(client))
417
416
  return await _read_named_with_plan(client, plan)
418
417
 
419
418
 
@@ -422,7 +421,7 @@ def read_named_sync(
422
421
  addresses: list[str],
423
422
  ) -> dict[str, int | float | bool]:
424
423
  """Synchronously read a mixed logical snapshot by address string."""
425
- plan = _compile_read_plan(addresses, family=_client_address_profile(client))
424
+ plan = _compile_read_plan(addresses, address_profile=_client_address_profile(client))
426
425
  return _read_named_with_plan_sync(client, plan)
427
426
 
428
427
 
@@ -440,14 +439,14 @@ async def write_named(
440
439
  ``D50.3`` updates one bit inside one word. Direct bit devices such as
441
440
  ``M1000`` are normalized to ``"BIT"`` writes.
442
441
  """
443
- family = _client_address_profile(client)
442
+ address_profile = _client_address_profile(client)
444
443
  for address, value in updates.items():
445
444
  base, dtype, bit_idx = _parse_address(address)
446
445
  if dtype == "BIT_IN_WORD":
447
- _validate_bit_in_word_target(address, _parse_device_for_family(base, family))
446
+ _validate_bit_in_word_target(address, _parse_device_for_address_profile(base, address_profile))
448
447
  await write_bit_in_word(client, base, bit_idx or 0, bool(value))
449
448
  else:
450
- device = _parse_device_for_family(base, family)
449
+ device = _parse_device_for_address_profile(base, address_profile)
451
450
  resolved_dtype = _resolve_dtype_for_address(address, device, dtype, bit_idx)
452
451
  _validate_long_timer_entry(address, device, resolved_dtype)
453
452
  await write_typed(client, base, resolved_dtype, value)
@@ -458,14 +457,14 @@ def write_named_sync(
458
457
  updates: dict[str, int | float | bool],
459
458
  ) -> None:
460
459
  """Synchronously write a mixed logical snapshot by address string."""
461
- family = _client_address_profile(client)
460
+ address_profile = _client_address_profile(client)
462
461
  for address, value in updates.items():
463
462
  base, dtype, bit_idx = _parse_address(address)
464
463
  if dtype == "BIT_IN_WORD":
465
- _validate_bit_in_word_target(address, _parse_device_for_family(base, family))
464
+ _validate_bit_in_word_target(address, _parse_device_for_address_profile(base, address_profile))
466
465
  write_bit_in_word_sync(client, base, bit_idx or 0, bool(value))
467
466
  else:
468
- device = _parse_device_for_family(base, family)
467
+ device = _parse_device_for_address_profile(base, address_profile)
469
468
  resolved_dtype = _resolve_dtype_for_address(address, device, dtype, bit_idx)
470
469
  _validate_long_timer_entry(address, device, resolved_dtype)
471
470
  write_typed_sync(client, base, resolved_dtype, value)
@@ -497,15 +496,10 @@ def _parse_address(address: str) -> tuple[str, str, int | None]:
497
496
  def _effective_address_profile(
498
497
  *,
499
498
  plc_profile: object | None = None,
500
- family: object | None = None,
501
499
  ) -> object | None:
502
- if plc_profile is not None and family is not None:
503
- raise ValueError("Pass either plc_profile or family, not both.")
504
500
  if plc_profile is not None:
505
501
  defaults = _resolve_plc_profile_defaults(plc_profile)
506
502
  return None if defaults is None else defaults.address_profile
507
- if family is not None:
508
- return _normalize_plc_profile_hint(family)
509
503
  return None
510
504
 
511
505
 
@@ -513,7 +507,6 @@ def parse_address(
513
507
  address: str | DeviceRef,
514
508
  *,
515
509
  plc_profile: object | None = None,
516
- family: object | None = None,
517
510
  ) -> SlmpAddress:
518
511
  """Parse public SLMP helper-layer address notation.
519
512
 
@@ -525,10 +518,10 @@ def parse_address(
525
518
  text = str(address)
526
519
  return SlmpAddress(text=text, base_device=text, dtype="U")
527
520
 
528
- effective_family = _effective_address_profile(plc_profile=plc_profile, family=family)
521
+ effective_address_profile = _effective_address_profile(plc_profile=plc_profile)
529
522
  raw_text = address.strip()
530
523
  base, dtype, bit_index = _parse_address(raw_text)
531
- device = _parse_device_for_family(base, effective_family)
524
+ device = _parse_device_for_address_profile(base, effective_address_profile)
532
525
  canonical_base = str(device)
533
526
 
534
527
  if bit_index is not None:
@@ -561,12 +554,11 @@ def try_parse_address(
561
554
  address: str | DeviceRef,
562
555
  *,
563
556
  plc_profile: object | None = None,
564
- family: object | None = None,
565
557
  ) -> SlmpAddress | None:
566
558
  """Return parsed address information, or ``None`` when parsing fails."""
567
559
 
568
560
  try:
569
- return parse_address(address, plc_profile=plc_profile, family=family)
561
+ return parse_address(address, plc_profile=plc_profile)
570
562
  except Exception:
571
563
  return None
572
564
 
@@ -575,14 +567,13 @@ def format_address(
575
567
  address: SlmpAddress | str | DeviceRef,
576
568
  *,
577
569
  plc_profile: object | None = None,
578
- family: object | None = None,
579
570
  ) -> str:
580
571
  """Return canonical public SLMP address text."""
581
572
 
582
573
  if not isinstance(address, SlmpAddress):
583
- return parse_address(address, plc_profile=plc_profile, family=family).text
574
+ return parse_address(address, plc_profile=plc_profile).text
584
575
 
585
- canonical_base = normalize_address(address.base_device, plc_profile=plc_profile, family=family)
576
+ canonical_base = normalize_address(address.base_device, plc_profile=plc_profile)
586
577
  if address.dtype == "BIT_IN_WORD":
587
578
  if address.bit_index is None or not 0 <= address.bit_index <= 15:
588
579
  raise ValueError("bit-in-word address requires bit_index 0-F")
@@ -598,7 +589,6 @@ def normalize_address(
598
589
  address: str | DeviceRef,
599
590
  *,
600
591
  plc_profile: object | None = None,
601
- family: object | None = None,
602
592
  ) -> str:
603
593
  """Return the canonical helper-layer form of one SLMP device address.
604
594
 
@@ -610,14 +600,14 @@ def normalize_address(
610
600
  if not isinstance(address, str):
611
601
  return str(address)
612
602
 
613
- effective_family = _effective_address_profile(plc_profile=plc_profile, family=family)
603
+ effective_address_profile = _effective_address_profile(plc_profile=plc_profile)
614
604
 
615
605
  text = address.strip()
616
606
  if ":" not in text and "." not in text:
617
- return str(parse_device(text, plc_profile=effective_family))
607
+ return str(parse_device(text, plc_profile=effective_address_profile))
618
608
 
619
609
  base, dtype, bit_index = _parse_address(text)
620
- canonical_base = str(parse_device(base, plc_profile=effective_family))
610
+ canonical_base = str(parse_device(base, plc_profile=effective_address_profile))
621
611
  if bit_index is not None:
622
612
  return f"{canonical_base}.{bit_index:X}"
623
613
  if ":" in text:
@@ -791,7 +781,7 @@ def _read_long_family_value_sync(
791
781
  def _compile_read_plan(
792
782
  addresses: list[str],
793
783
  *,
794
- family: object | None = None,
784
+ address_profile: object | None = None,
795
785
  ) -> _ReadPlan:
796
786
  entries: list[_ReadPlanEntry] = []
797
787
  word_devices: list[DeviceRef] = []
@@ -801,7 +791,7 @@ def _compile_read_plan(
801
791
 
802
792
  for address in addresses:
803
793
  base, dtype, bit_index = _parse_address(address)
804
- device = _parse_device_for_family(base, family)
794
+ device = _parse_device_for_address_profile(base, address_profile)
805
795
  dtype = _resolve_dtype_for_address(address, device, dtype, bit_index)
806
796
  _validate_long_timer_entry(address, device, dtype)
807
797
  batch_kind: str | None = None
@@ -1105,7 +1095,7 @@ async def poll(
1105
1095
 
1106
1096
  The address list is compiled once and reused for every cycle.
1107
1097
  """
1108
- plan = _compile_read_plan(addresses, family=_client_address_profile(client))
1098
+ plan = _compile_read_plan(addresses, address_profile=_client_address_profile(client))
1109
1099
  while True:
1110
1100
  yield await _read_named_with_plan(client, plan)
1111
1101
  await asyncio.sleep(interval)
@@ -1117,7 +1107,7 @@ def poll_sync(
1117
1107
  interval: float,
1118
1108
  ) -> Iterator[dict[str, int | float | bool]]:
1119
1109
  """Synchronously yield mixed snapshots at a fixed interval."""
1120
- plan = _compile_read_plan(addresses, family=_client_address_profile(client))
1110
+ plan = _compile_read_plan(addresses, address_profile=_client_address_profile(client))
1121
1111
  while True:
1122
1112
  yield _read_named_with_plan_sync(client, plan)
1123
1113
  time.sleep(interval)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slmp-connect-python
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: SLMP Connect Python: client library for MELSEC SLMP binary communication
5
5
  Author: fa-yoshinobu
6
6
  License-Expression: MIT