pypck 0.8.5__tar.gz → 0.8.7__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 (27) hide show
  1. {pypck-0.8.5/pypck.egg-info → pypck-0.8.7}/PKG-INFO +3 -2
  2. pypck-0.8.7/VERSION +1 -0
  3. {pypck-0.8.5 → pypck-0.8.7}/pypck/inputs.py +156 -5
  4. {pypck-0.8.5 → pypck-0.8.7}/pypck/lcn_defs.py +48 -7
  5. {pypck-0.8.5 → pypck-0.8.7}/pypck/module.py +38 -10
  6. {pypck-0.8.5 → pypck-0.8.7}/pypck/pck_commands.py +122 -37
  7. {pypck-0.8.5 → pypck-0.8.7}/pypck/request_handlers.py +24 -1
  8. {pypck-0.8.5 → pypck-0.8.7/pypck.egg-info}/PKG-INFO +3 -2
  9. {pypck-0.8.5 → pypck-0.8.7}/tests/test_commands.py +102 -53
  10. {pypck-0.8.5 → pypck-0.8.7}/tests/test_messages.py +29 -0
  11. pypck-0.8.5/VERSION +0 -1
  12. {pypck-0.8.5 → pypck-0.8.7}/LICENSE +0 -0
  13. {pypck-0.8.5 → pypck-0.8.7}/README.md +0 -0
  14. {pypck-0.8.5 → pypck-0.8.7}/pypck/__init__.py +0 -0
  15. {pypck-0.8.5 → pypck-0.8.7}/pypck/connection.py +0 -0
  16. {pypck-0.8.5 → pypck-0.8.7}/pypck/helpers.py +0 -0
  17. {pypck-0.8.5 → pypck-0.8.7}/pypck/lcn_addr.py +0 -0
  18. {pypck-0.8.5 → pypck-0.8.7}/pypck/timeout_retry.py +0 -0
  19. {pypck-0.8.5 → pypck-0.8.7}/pypck.egg-info/SOURCES.txt +0 -0
  20. {pypck-0.8.5 → pypck-0.8.7}/pypck.egg-info/dependency_links.txt +0 -0
  21. {pypck-0.8.5 → pypck-0.8.7}/pypck.egg-info/not-zip-safe +0 -0
  22. {pypck-0.8.5 → pypck-0.8.7}/pypck.egg-info/top_level.txt +0 -0
  23. {pypck-0.8.5 → pypck-0.8.7}/pyproject.toml +0 -0
  24. {pypck-0.8.5 → pypck-0.8.7}/setup.cfg +0 -0
  25. {pypck-0.8.5 → pypck-0.8.7}/tests/test_connection.py +0 -0
  26. {pypck-0.8.5 → pypck-0.8.7}/tests/test_dyn_text.py +0 -0
  27. {pypck-0.8.5 → pypck-0.8.7}/tests/test_vars.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: pypck
3
- Version: 0.8.5
3
+ Version: 0.8.7
4
4
  Summary: LCN-PCK library
5
5
  Home-page: https://github.com/alengwenus/pypck
6
6
  Author-email: Andre Lengwenus <alengwenus@gmail.com>
@@ -18,6 +18,7 @@ Classifier: Topic :: Home Automation
18
18
  Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
+ Dynamic: license-file
21
22
 
22
23
  # pypck - Asynchronous LCN-PCK library written in Python
23
24
 
pypck-0.8.7/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.8.7
@@ -398,8 +398,10 @@ class ModSn(ModInput):
398
398
  ValueError
399
399
  ): # unconventional manufacturer code (e.g., due to LinHK VM)
400
400
  manu = 0xFF
401
- _LOGGER.debug(
402
- "Unconventional manufacturer code: %s. Defaulting to 0x%02X",
401
+ _LOGGER.warning(
402
+ "Unconventional manufacturer code for module (S%d, M%d): %s. Defaulting to 0x%02X",
403
+ addr.seg_id,
404
+ addr.addr_id,
403
405
  matcher.group("manu"),
404
406
  manu,
405
407
  )
@@ -596,7 +598,10 @@ class ModStatusOutputNative(ModInput):
596
598
 
597
599
 
598
600
  class ModStatusRelays(ModInput):
599
- """Status of 8 relays received from an LCN module."""
601
+ """Status of 8 relays received from an LCN module.
602
+
603
+ Includes helper functions for motor states based on LCN wiring.
604
+ """
600
605
 
601
606
  def __init__(self, physical_source_addr: LcnAddr, states: list[bool]):
602
607
  """Construct ModInput object."""
@@ -614,6 +619,43 @@ class ModStatusRelays(ModInput):
614
619
  """
615
620
  return self.states[relay_id]
616
621
 
622
+ def get_motor_onoff_relay(self, motor_id: int) -> int:
623
+ """Get the motor on/off relay id."""
624
+ if 0 > motor_id > 3:
625
+ raise ValueError("Motor id must be in range 0..3")
626
+ return motor_id * 2
627
+
628
+ def get_motor_updown_relay(self, motor_id: int) -> int:
629
+ """Get the motor up/down relay id."""
630
+ if 0 > motor_id > 3:
631
+ raise ValueError("Motor id must be in range 0..3")
632
+ return motor_id * 2 + 1
633
+
634
+ def motor_is_on(self, motor_id: int) -> bool:
635
+ """Check if a motor is on."""
636
+ return self.states[self.get_motor_onoff_relay(motor_id)]
637
+
638
+ def is_opening(self, motor_id: int) -> bool:
639
+ """Check if a motor is opening."""
640
+ if self.motor_is_on(motor_id):
641
+ return not self.states[self.get_motor_updown_relay(motor_id)]
642
+ return False
643
+
644
+ def is_closing(self, motor_id: int) -> bool:
645
+ """Check if a motor is closing."""
646
+ if self.motor_is_on(motor_id):
647
+ return self.states[self.get_motor_updown_relay(motor_id)]
648
+ return False
649
+
650
+ def is_assumed_closed(self, motor_id: int) -> bool:
651
+ """Check if a motor is closed.
652
+
653
+ The closed state is assumed if the motor direction is down and the motor is switched off."
654
+ """
655
+ if not self.motor_is_on(motor_id):
656
+ return self.states[self.get_motor_updown_relay(motor_id)]
657
+ return False
658
+
617
659
  @staticmethod
618
660
  def try_parse(data: str) -> list[Input] | None:
619
661
  """Try to parse the given input text.
@@ -1024,13 +1066,120 @@ class ModStatusSceneOutputs(ModInput):
1024
1066
  if matcher:
1025
1067
  addr = LcnAddr(int(matcher.group("seg_id")), int(matcher.group("mod_id")))
1026
1068
  scene_id = int(matcher.group("scene_id"))
1027
- values = [int(matcher.group(f"output{i+1:d}")) for i in range(4)]
1028
- ramps = [int(matcher.group(f"ramp{i+1:d}")) for i in range(4)]
1069
+ values = [int(matcher.group(f"output{i + 1:d}")) for i in range(4)]
1070
+ ramps = [int(matcher.group(f"ramp{i + 1:d}")) for i in range(4)]
1029
1071
  return [ModStatusSceneOutputs(addr, scene_id, values, ramps)]
1030
1072
 
1031
1073
  return None
1032
1074
 
1033
1075
 
1076
+ class ModStatusMotorPositionBS4(ModInput):
1077
+ """Status of motor positions (if BS4 connected) received from an LCN module.
1078
+
1079
+ Position and limit is in percent. 0%: cover closed, 100%: cover open.
1080
+ """
1081
+
1082
+ def __init__(
1083
+ self,
1084
+ physical_source_addr: LcnAddr,
1085
+ motor: int,
1086
+ position: float,
1087
+ limit: float | None = None,
1088
+ time_down: int | None = None,
1089
+ time_up: int | None = None,
1090
+ ):
1091
+ """Construct ModInput object."""
1092
+ super().__init__(physical_source_addr)
1093
+ self.motor = motor
1094
+ self.position = position
1095
+ self.limit = limit
1096
+ self.time_down = time_down
1097
+ self.time_up = time_up
1098
+
1099
+ @staticmethod
1100
+ def try_parse(data: str) -> list[Input] | None:
1101
+ """Try to parse the given input text.
1102
+
1103
+ Will return a list of parsed Inputs. The list might be empty (but not
1104
+ null).
1105
+
1106
+ :param data str: The input data received from LCN-PCHK
1107
+
1108
+ :return: The parsed Inputs (never null)
1109
+ :rtype: List with instances of :class:`~pypck.input.Input`
1110
+ """
1111
+ matcher = PckParser.PATTERN_STATUS_MOTOR_POSITION_BS4.match(data)
1112
+ if matcher:
1113
+ motor_status_inputs: list[Input] = []
1114
+ addr = LcnAddr(int(matcher.group("seg_id")), int(matcher.group("mod_id")))
1115
+ for idx in (1, 2):
1116
+ motor = matcher.group(f"motor{idx}_id")
1117
+ position = matcher.group(f"position{idx}")
1118
+ limit = matcher.group(f"limit{idx}")
1119
+ time_down = matcher.group(f"time_down{idx}")
1120
+ time_up = matcher.group(f"time_up{idx}")
1121
+
1122
+ motor_status_inputs.append(
1123
+ ModStatusMotorPositionBS4(
1124
+ addr,
1125
+ int(motor) - 1,
1126
+ (200 - int(position)) / 2,
1127
+ None if limit == "?" else (200 - int(limit)) / 2,
1128
+ None if time_down == "?" else int(time_down),
1129
+ None if time_up == "?" else int(time_up),
1130
+ )
1131
+ )
1132
+ return motor_status_inputs
1133
+
1134
+ return None
1135
+
1136
+
1137
+ class ModStatusMotorPositionModule(ModInput):
1138
+ """Status of motor positions received from an LCN module.
1139
+
1140
+ Position is in percent. 0%: cover closed, 100%: cover open.
1141
+ """
1142
+
1143
+ def __init__(
1144
+ self,
1145
+ physical_source_addr: LcnAddr,
1146
+ motor: int,
1147
+ position: float,
1148
+ ):
1149
+ """Construct ModInput object."""
1150
+ super().__init__(physical_source_addr)
1151
+ self.motor = motor
1152
+ self.position = position
1153
+
1154
+ @staticmethod
1155
+ def try_parse(data: str) -> list[Input] | None:
1156
+ """Try to parse the given input text.
1157
+
1158
+ Will return a list of parsed Inputs. The list might be empty (but not
1159
+ null).
1160
+
1161
+ :param data str: The input data received from LCN-PCHK
1162
+
1163
+ :return: The parsed Inputs (never null)
1164
+ :rtype: List with instances of :class:`~pypck.input.Input`
1165
+ """
1166
+ matcher = PckParser.PATTERN_STATUS_MOTOR_POSITION_MODULE.match(data)
1167
+ if matcher:
1168
+ addr = LcnAddr(int(matcher.group("seg_id")), int(matcher.group("mod_id")))
1169
+ motor = matcher.group("motor_id")
1170
+ position = matcher.group("position")
1171
+
1172
+ return [
1173
+ ModStatusMotorPositionModule(
1174
+ addr,
1175
+ int(motor) - 1,
1176
+ float(position),
1177
+ )
1178
+ ]
1179
+
1180
+ return None
1181
+
1182
+
1034
1183
  class ModSendCommandHost(ModInput):
1035
1184
  """Send command to host message from module."""
1036
1185
 
@@ -1200,6 +1349,8 @@ class InputParser:
1200
1349
  ModStatusKeyLocks,
1201
1350
  ModStatusAccessControl,
1202
1351
  ModStatusSceneOutputs,
1352
+ ModStatusMotorPositionBS4,
1353
+ ModStatusMotorPositionModule,
1203
1354
  ModSendCommandHost,
1204
1355
  ModSendKeysHost,
1205
1356
  Unknown,
@@ -228,7 +228,7 @@ def ramp_value_to_time(ramp_value: int) -> int:
228
228
  :rtype: int
229
229
  """
230
230
  if not 0 <= ramp_value <= 250:
231
- raise ValueError("Ramp value has to be in range 0..250.")
231
+ raise ValueError("Ramp value has to be in range 0..250")
232
232
 
233
233
  if ramp_value < 10:
234
234
  times = [0, 250, 500, 660, 1000, 1400, 2000, 3000, 4000, 5000]
@@ -250,7 +250,7 @@ def time_to_native_value(time_msec: int) -> int:
250
250
  :rtype: int
251
251
  """
252
252
  if not 0 <= time_msec <= 240960:
253
- raise ValueError("Time has to be in range 0..240960ms.")
253
+ raise ValueError("Time has to be in range 0..240960ms")
254
254
  time_scaled = time_msec / (1000 * 0.03 * 32.0) + 1.0
255
255
 
256
256
  pre_decimal = int(time_scaled).bit_length() - 1
@@ -264,7 +264,6 @@ def native_value_to_time(value: int) -> int:
264
264
  """Convert native LCN value to time.
265
265
 
266
266
  Scales the given byte value (0..255) to a time value in milliseconds.
267
- Inverse to time_to_native_value().
268
267
 
269
268
  :param int value: Duration of timer in native LCN units
270
269
 
@@ -272,7 +271,7 @@ def native_value_to_time(value: int) -> int:
272
271
  :rtype: int
273
272
  """
274
273
  if not 0 <= value <= 255:
275
- raise ValueError("Value has to be in range 0..255.")
274
+ raise ValueError("Value has to be in range 0..255")
276
275
  pre_decimal = value // 32
277
276
  decimal = value / 32 - pre_decimal
278
277
 
@@ -282,6 +281,39 @@ def native_value_to_time(value: int) -> int:
282
281
  return int(time_msec)
283
282
 
284
283
 
284
+ def motor_position_time_to_native_value(time_msec: int) -> int:
285
+ """Convert time to native LCN time value.
286
+
287
+ Scales the given time value in milliseconds to a two-byte value.
288
+
289
+ :param int time_msec: Duration of timer in milliseconds (1001..65535000)
290
+
291
+ :returns: The duration in native LCN units
292
+ :rtype: int
293
+ """
294
+ if not 1001 <= time_msec <= 65535000:
295
+ raise ValueError("Time has to be in range 1001..65535000ms")
296
+ value = 0xFFFF * 1000 / time_msec
297
+ return int(value)
298
+
299
+
300
+ def native_value_to_motor_position_time(value: int) -> int:
301
+ """Convert native LCN value to time.
302
+
303
+ Scales the given two-byte value (1..65535) to a time value in milliseconds.
304
+
305
+ :param int value: Duration of timer in native LCN units
306
+
307
+ :returns: The duration in milliseconds
308
+ :rtype: int
309
+ """
310
+ if not 1 <= value <= 0xFFFF:
311
+ raise ValueError("Value has to be in range 1..65535")
312
+
313
+ time_msec = 0xFFFF * 1000 / value
314
+ return int(time_msec)
315
+
316
+
285
317
  class Var(Enum):
286
318
  """LCN variable types."""
287
319
 
@@ -1242,9 +1274,18 @@ class MotorReverseTime(Enum):
1242
1274
  For modules with FW<190C the release time has to be specified.
1243
1275
  """
1244
1276
 
1245
- RT70 = auto() # 70ms
1246
- RT600 = auto() # 600ms
1247
- RT1200 = auto() # 1200ms
1277
+ RT70 = "RT70" # 70ms
1278
+ RT600 = "RT600" # 600ms
1279
+ RT1200 = "RT1200" # 1200ms
1280
+
1281
+
1282
+ class MotorPositioningMode(Enum):
1283
+ """Motor positioning mode used in LCN commands."""
1284
+
1285
+ NONE = "NONE"
1286
+ BS4 = "BS4"
1287
+ MODULE = "MODULE"
1288
+ # EMULATED = "EMULATED"
1248
1289
 
1249
1290
 
1250
1291
  class RelVarRef(Enum):
@@ -232,22 +232,46 @@ class AbstractConnection:
232
232
  self.wants_ack, PckGenerator.control_relays_timer(time_msec, states)
233
233
  )
234
234
 
235
- async def control_motors_relays(
236
- self, states: list[lcn_defs.MotorStateModifier]
235
+ async def control_motor_relays(
236
+ self,
237
+ motor_id: int,
238
+ state: lcn_defs.MotorStateModifier,
239
+ mode: lcn_defs.MotorPositioningMode = lcn_defs.MotorPositioningMode.NONE,
237
240
  ) -> bool:
238
241
  """Send a command to control motors via relays.
239
242
 
240
- :param states: The 4 modifiers for the cover states as a list
241
- :type states: list(:class: `~pypck.lcn-defs.MotorStateModifier`)
243
+ :param int motor_id: The motor id 0..3
244
+ :param MotorStateModifier state: The modifier for the
245
+ :param MotorPositioningMode mode: The motor positioning mode (ooptional)
246
+
247
+ :returns: True if command was sent successfully, False otherwise
248
+ :rtype: bool
249
+ """
250
+ return await self.send_command(
251
+ self.wants_ack, PckGenerator.control_motor_relays(motor_id, state, mode)
252
+ )
253
+
254
+ async def control_motor_relays_position(
255
+ self,
256
+ motor_id: int,
257
+ position: float,
258
+ mode: lcn_defs.MotorPositioningMode,
259
+ ) -> bool:
260
+ """Control motor position via relays and BS4.
261
+
262
+ :param int motor_id: The motor port of the LCN module
263
+ :param float position: The position to set in percentage (0..100)
264
+ :param MotorPositioningMode mode: The motor positioning mode
242
265
 
243
266
  :returns: True if command was sent successfully, False otherwise
244
267
  :rtype: bool
245
268
  """
246
269
  return await self.send_command(
247
- self.wants_ack, PckGenerator.control_motors_relays(states)
270
+ self.wants_ack,
271
+ PckGenerator.control_motor_relays_position(motor_id, position, mode),
248
272
  )
249
273
 
250
- async def control_motors_outputs(
274
+ async def control_motor_outputs(
251
275
  self,
252
276
  state: lcn_defs.MotorStateModifier,
253
277
  reverse_time: lcn_defs.MotorReverseTime | None = None,
@@ -264,7 +288,7 @@ class AbstractConnection:
264
288
  """
265
289
  return await self.send_command(
266
290
  self.wants_ack,
267
- PckGenerator.control_motors_outputs(state, reverse_time),
291
+ PckGenerator.control_motor_outputs(state, reverse_time),
268
292
  )
269
293
 
270
294
  async def activate_scene(
@@ -736,7 +760,7 @@ class GroupConnection(AbstractConnection):
736
760
  result &= await super().var_rel(var, value, software_serial=0)
737
761
  return result
738
762
 
739
- async def activate_status_request_handler(self, item: Any) -> None:
763
+ async def activate_status_request_handler(self, item: Any, option: Any) -> None:
740
764
  """Activate a specific TimeoutRetryHandler for status requests."""
741
765
  await self.conn.segment_scan_completed_event.wait()
742
766
 
@@ -849,9 +873,13 @@ class ModuleConnection(AbstractConnection):
849
873
  """
850
874
  await self.acknowledges.put(code)
851
875
 
852
- async def activate_status_request_handler(self, item: Any) -> None:
876
+ async def activate_status_request_handler(
877
+ self, item: Any, option: Any = None
878
+ ) -> None:
853
879
  """Activate a specific TimeoutRetryHandler for status requests."""
854
- self.task_registry.create_task(self.status_requests_handler.activate(item))
880
+ self.task_registry.create_task(
881
+ self.status_requests_handler.activate(item, option)
882
+ )
855
883
 
856
884
  async def activate_status_request_handlers(self) -> None:
857
885
  """Activate all TimeoutRetryHandlers for status requests."""
@@ -192,6 +192,21 @@ class PckParser:
192
192
  r"(?P<code1>\d{3})(?P<code2>\d{3})(?P<code3>\d{3})"
193
193
  )
194
194
 
195
+ # Pattern to parse motor position BS4 status messages.
196
+ PATTERN_STATUS_MOTOR_POSITION_BS4 = re.compile(
197
+ r"=M(?P<seg_id>\d{3})(?P<mod_id>\d{3})\."
198
+ r"RM(?P<motor1_id>[1-4])(?P<position1>[0-9]{3})(?P<limit1>[0-9]{3}|\?)"
199
+ r"(?P<time_down1>[0-9]{5}|\?)(?P<time_up1>[0-9]{5}|\?)"
200
+ r"RM(?P<motor2_id>[1-4])(?P<position2>[0-9]{3})(?P<limit2>[0-9]{3}|\?)"
201
+ r"(?P<time_down2>[0-9]{5}|\?)(?P<time_up2>[0-9]{5}|\?)"
202
+ )
203
+
204
+ # Pattern to parse motor position module status messages.
205
+ PATTERN_STATUS_MOTOR_POSITION_MODULE = re.compile(
206
+ r":M(?P<seg_id>\d{3})(?P<mod_id>\d{3})"
207
+ r"P(?P<motor_id>[1-4])(?P<position>[0-9]{3})"
208
+ )
209
+
195
210
  @staticmethod
196
211
  def get_boolean_value(input_byte: int) -> list[bool]:
197
212
  """Get boolean representation for the given byte.
@@ -536,44 +551,119 @@ class PckGenerator:
536
551
  return ret
537
552
 
538
553
  @staticmethod
539
- def control_motors_relays(states: list[lcn_defs.MotorStateModifier]) -> str:
554
+ def control_motor_relays(
555
+ motor_id: int,
556
+ state: lcn_defs.MotorStateModifier,
557
+ mode: lcn_defs.MotorPositioningMode = lcn_defs.MotorPositioningMode.NONE,
558
+ ) -> str:
540
559
  """Generate a command to control motors via relays.
541
560
 
542
- :param MotorStateModifier states: The 4 modifiers for the
543
- motor states as a list
561
+ :param int motor_id: The motor id 0..3
562
+ :param MotorStateModifier state: The modifier for the
563
+ motor state
544
564
  :return: The PCK command (without address header) as text
545
565
  :rtype: str
546
566
  """
547
- if len(states) != 4:
548
- raise ValueError("Invalid states length.")
549
- ret = "R8"
550
- for state in states:
551
- if state == lcn_defs.MotorStateModifier.UP:
552
- ret += lcn_defs.RelayStateModifier.ON.value
553
- ret += lcn_defs.RelayStateModifier.OFF.value
554
- elif state == lcn_defs.MotorStateModifier.DOWN:
555
- ret += lcn_defs.RelayStateModifier.ON.value
556
- ret += lcn_defs.RelayStateModifier.ON.value
567
+ if 0 > motor_id > 3:
568
+ raise ValueError("Invalid motor id")
569
+
570
+ if mode not in lcn_defs.MotorPositioningMode:
571
+ raise ValueError("Wrong motor position mode")
572
+
573
+ if mode == lcn_defs.MotorPositioningMode.BS4:
574
+ new_motor_id = [1, 2, 5, 6][motor_id]
575
+ if state == lcn_defs.MotorStateModifier.DOWN:
576
+ # AU=window open / cover down
577
+ action = "AU"
578
+ elif state == lcn_defs.MotorStateModifier.UP:
579
+ # ZU=window close / cover up
580
+ action = "ZU"
557
581
  elif state == lcn_defs.MotorStateModifier.STOP:
558
- ret += lcn_defs.RelayStateModifier.OFF.value
559
- ret += lcn_defs.RelayStateModifier.NOCHANGE.value
560
- elif state == lcn_defs.MotorStateModifier.TOGGLEONOFF:
561
- ret += lcn_defs.RelayStateModifier.TOGGLE.value
562
- ret += lcn_defs.RelayStateModifier.NOCHANGE.value
563
- elif state == lcn_defs.MotorStateModifier.TOGGLEDIR:
564
- ret += lcn_defs.RelayStateModifier.NOCHANGE.value
565
- ret += lcn_defs.RelayStateModifier.TOGGLE.value
566
- elif state == lcn_defs.MotorStateModifier.CYCLE:
567
- ret += lcn_defs.RelayStateModifier.TOGGLE.value
568
- ret += lcn_defs.RelayStateModifier.TOGGLE.value
569
- elif state == lcn_defs.MotorStateModifier.NOCHANGE:
570
- ret += lcn_defs.RelayStateModifier.NOCHANGE.value
571
- ret += lcn_defs.RelayStateModifier.NOCHANGE.value
582
+ action = "ST"
583
+ else:
584
+ raise ValueError("Invalid motor state for BS4 mode")
572
585
 
573
- return ret
586
+ return f"R8M{new_motor_id}{action}"
587
+
588
+ # lcn_defs.MotorPositioningMode.NONE
589
+ # lcn_defs.MotorPositioningMode.MODULE
590
+ if state == lcn_defs.MotorStateModifier.UP:
591
+ port_onoff = lcn_defs.RelayStateModifier.ON
592
+ port_updown = lcn_defs.RelayStateModifier.OFF
593
+ elif state == lcn_defs.MotorStateModifier.DOWN:
594
+ port_onoff = lcn_defs.RelayStateModifier.ON
595
+ port_updown = lcn_defs.RelayStateModifier.ON
596
+ elif state == lcn_defs.MotorStateModifier.STOP:
597
+ port_onoff = lcn_defs.RelayStateModifier.OFF
598
+ port_updown = lcn_defs.RelayStateModifier.NOCHANGE
599
+ elif state == lcn_defs.MotorStateModifier.TOGGLEONOFF:
600
+ port_onoff = lcn_defs.RelayStateModifier.TOGGLE
601
+ port_updown = lcn_defs.RelayStateModifier.NOCHANGE
602
+ elif state == lcn_defs.MotorStateModifier.TOGGLEDIR:
603
+ port_onoff = lcn_defs.RelayStateModifier.NOCHANGE
604
+ port_updown = lcn_defs.RelayStateModifier.TOGGLE
605
+ elif state == lcn_defs.MotorStateModifier.CYCLE:
606
+ port_onoff = lcn_defs.RelayStateModifier.TOGGLE
607
+ port_updown = lcn_defs.RelayStateModifier.TOGGLE
608
+ elif state == lcn_defs.MotorStateModifier.NOCHANGE:
609
+ port_onoff = lcn_defs.RelayStateModifier.NOCHANGE
610
+ port_updown = lcn_defs.RelayStateModifier.NOCHANGE
611
+ else:
612
+ raise ValueError("Invalid motor state")
613
+
614
+ states = [lcn_defs.RelayStateModifier.NOCHANGE] * 8
615
+ states[motor_id * 2] = port_onoff
616
+ states[motor_id * 2 + 1] = port_updown
617
+ return "R8" + "".join([state.value for state in states])
618
+
619
+ @staticmethod
620
+ def control_motor_relays_position(
621
+ motor_id: int, position: float, mode: lcn_defs.MotorPositioningMode
622
+ ) -> str:
623
+ """Control motor position via relays and BS4 or module.
624
+
625
+ :param int motor_id: The motor port of the LCN module
626
+ :param float position: The position to set in percentage (0..100)
627
+ (0: closed cover, 100: open cover)
628
+ :param MotorPositioningMode mode: The motor positioning mode
629
+
630
+ :return: The PCK command (without address header) as text
631
+ :rtype: str
632
+ """
633
+ if mode not in (
634
+ lcn_defs.MotorPositioningMode.BS4,
635
+ lcn_defs.MotorPositioningMode.MODULE,
636
+ ):
637
+ raise ValueError("Wrong motor positioning mode")
638
+
639
+ if 0 > motor_id > 3:
640
+ raise ValueError("Invalid motor")
641
+
642
+ if mode == lcn_defs.MotorPositioningMode.BS4:
643
+ new_motor_id = [1, 2, 5, 6][motor_id]
644
+ action = f"GP{int(200 - 2 * position):03d}"
645
+ return f"R8M{new_motor_id}{action}"
646
+ elif mode == lcn_defs.MotorPositioningMode.MODULE:
647
+ new_motor_id = 1 << motor_id
648
+ return f"JH{position:03d}{new_motor_id:03d}"
649
+
650
+ return ""
651
+
652
+ @staticmethod
653
+ def request_motor_position_status(motor_pair: int) -> str:
654
+ """Generate a motor position status request for BS4.
655
+
656
+ :param int motor_pair: Motor pair 0: 1, 2; 1: 3, 4
657
+
658
+ :return: The PCK command (without address header) as text
659
+ :rtype: str
660
+ """
661
+ if motor_pair not in [0, 1]:
662
+ raise ValueError("Invalid motor_pair.")
663
+ return f"R8M{7 if motor_pair else 3}P{motor_pair + 1}"
574
664
 
575
665
  @staticmethod
576
- def control_motors_outputs(
666
+ def control_motor_outputs(
577
667
  state: lcn_defs.MotorStateModifier,
578
668
  reverse_time: lcn_defs.MotorReverseTime | None = None,
579
669
  ) -> str:
@@ -728,16 +818,11 @@ class PckGenerator:
728
818
  if var_id == 0:
729
819
  # Old command for variable 1 / T-var (compatible with all
730
820
  # modules)
731
- pck = "Z" f"{'A' if value >= 0 else 'S'}" f"{abs(value)}"
821
+ pck = f"Z{'A' if value >= 0 else 'S'}{abs(value)}"
732
822
  else:
733
823
  # New command for variable 1-12 (compatible with all modules,
734
824
  # since LCN-PCHK 2.8)
735
- pck = (
736
- "Z"
737
- f"{'+' if value >= 0 else '-'}"
738
- f"{var_id + 1:03d}"
739
- f"{abs(value)}"
740
- )
825
+ pck = f"Z{'+' if value >= 0 else '-'}{var_id + 1:03d}{abs(value)}"
741
826
  return pck
742
827
 
743
828
  set_point_id = lcn_defs.Var.to_set_point_id(var)
@@ -1051,7 +1136,7 @@ class PckGenerator:
1051
1136
  raise ValueError("Wrong target_value.")
1052
1137
  if (target_value != -1) and (software_serial >= 0x120301) and state:
1053
1138
  reg_byte = reg_id * 0x40 + 0x07
1054
- return f"X2{0x1E:03d}{reg_byte:03d}{int(2*target_value):03d}"
1139
+ return f"X2{0x1E:03d}{reg_byte:03d}{int(2 * target_value):03d}"
1055
1140
  return f"RE{'A' if reg_id == 0 else 'B'}X{'S' if state else 'A'}"
1056
1141
 
1057
1142
  @staticmethod
@@ -466,6 +466,17 @@ class StatusRequestsHandler:
466
466
  self.request_status_relays_timeout
467
467
  )
468
468
 
469
+ # Motor positions request status (1, 2 and 3, 4)
470
+ self.request_status_motor_positions = []
471
+ for motor_pair in range(2):
472
+ trh = TimeoutRetryHandler(
473
+ self.task_registry, -1, self.settings["MAX_STATUS_POLLED_VALUEAGE"]
474
+ )
475
+ trh.set_timeout_callback(
476
+ self.request_status_motor_positions_timeout, motor_pair
477
+ )
478
+ self.request_status_motor_positions.append(trh)
479
+
469
480
  # Binary-sensors request status (all 8)
470
481
  self.request_status_bin_sensors = TimeoutRetryHandler(
471
482
  self.task_registry, -1, self.settings["MAX_STATUS_EVENTBASED_VALUEAGE"]
@@ -542,6 +553,15 @@ class StatusRequestsHandler:
542
553
  False, PckGenerator.request_relays_status()
543
554
  )
544
555
 
556
+ async def request_status_motor_positions_timeout(
557
+ self, failed: bool = False, motor_pair: int = 0
558
+ ) -> None:
559
+ """Is called on motor position status request timeout."""
560
+ if not failed:
561
+ await self.addr_conn.send_command(
562
+ False, PckGenerator.request_motor_position_status(motor_pair)
563
+ )
564
+
545
565
  async def request_status_bin_sensors_timeout(self, failed: bool = False) -> None:
546
566
  """Is called on binary sensor status request timeout."""
547
567
  if not failed:
@@ -589,7 +609,7 @@ class StatusRequestsHandler:
589
609
  False, PckGenerator.request_key_lock_status()
590
610
  )
591
611
 
592
- async def activate(self, item: Any) -> None:
612
+ async def activate(self, item: Any, option: Any = None) -> None:
593
613
  """Activate status requests for given item."""
594
614
  await self.addr_conn.conn.segment_scan_completed_event.wait()
595
615
  # handle variables independently
@@ -608,6 +628,8 @@ class StatusRequestsHandler:
608
628
  self.request_status_relays.activate()
609
629
  elif item in lcn_defs.MotorPort:
610
630
  self.request_status_relays.activate()
631
+ if option == lcn_defs.MotorPositioningMode.BS4:
632
+ self.request_status_motor_positions[item.value // 2].activate()
611
633
  elif item in lcn_defs.BinSensorPort:
612
634
  self.request_status_bin_sensors.activate()
613
635
  elif item in lcn_defs.LedPort:
@@ -627,6 +649,7 @@ class StatusRequestsHandler:
627
649
  await self.request_status_relays.cancel()
628
650
  elif item in lcn_defs.MotorPort:
629
651
  await self.request_status_relays.cancel()
652
+ await self.request_status_motor_positions[item.value // 2].cancel()
630
653
  elif item in lcn_defs.BinSensorPort:
631
654
  await self.request_status_bin_sensors.cancel()
632
655
  elif item in lcn_defs.LedPort:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: pypck
3
- Version: 0.8.5
3
+ Version: 0.8.7
4
4
  Summary: LCN-PCK library
5
5
  Home-page: https://github.com/alengwenus/pypck
6
6
  Author-email: Andre Lengwenus <alengwenus@gmail.com>
@@ -18,6 +18,7 @@ Classifier: Topic :: Home Automation
18
18
  Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
+ Dynamic: license-file
21
22
 
22
23
  # pypck - Asynchronous LCN-PCK library written in Python
23
24
 
@@ -7,6 +7,7 @@ from pypck.lcn_defs import (
7
7
  BeepSound,
8
8
  KeyLockStateModifier,
9
9
  LedStatus,
10
+ MotorPositioningMode,
10
11
  MotorReverseTime,
11
12
  MotorStateModifier,
12
13
  OutputPort,
@@ -71,9 +72,9 @@ COMMANDS = {
71
72
  # General status commands
72
73
  "SK": (PckGenerator.segment_coupler_scan,),
73
74
  "SN": (PckGenerator.request_serial,),
74
- **{f"NMN{block+1}": (PckGenerator.request_name, block) for block in range(2)},
75
- **{f"NMK{block+1}": (PckGenerator.request_comment, block) for block in range(3)},
76
- **{f"NMO{block+1}": (PckGenerator.request_oem_text, block) for block in range(4)},
75
+ **{f"NMN{block + 1}": (PckGenerator.request_name, block) for block in range(2)},
76
+ **{f"NMK{block + 1}": (PckGenerator.request_comment, block) for block in range(3)},
77
+ **{f"NMO{block + 1}": (PckGenerator.request_oem_text, block) for block in range(4)},
77
78
  "GP": (PckGenerator.request_group_membership_static,),
78
79
  "GD": (PckGenerator.request_group_membership_dynamic,),
79
80
  # Output, relay, binsensors, ... status commands
@@ -87,7 +88,7 @@ COMMANDS = {
87
88
  "STX": (PckGenerator.request_key_lock_status,),
88
89
  # Variable status (new commands)
89
90
  **{
90
- f"MWT{Var.to_var_id(var)+1:03d}": (
91
+ f"MWT{Var.to_var_id(var) + 1:03d}": (
91
92
  PckGenerator.request_var_status,
92
93
  var,
93
94
  NEW_VAR_SW_AGE,
@@ -95,7 +96,7 @@ COMMANDS = {
95
96
  for var in Var.variables # type: ignore
96
97
  },
97
98
  **{
98
- f"MWS{Var.to_set_point_id(var)+1:03d}": (
99
+ f"MWS{Var.to_set_point_id(var) + 1:03d}": (
99
100
  PckGenerator.request_var_status,
100
101
  var,
101
102
  NEW_VAR_SW_AGE,
@@ -103,7 +104,7 @@ COMMANDS = {
103
104
  for var in Var.set_points # type: ignore
104
105
  },
105
106
  **{
106
- f"MWC{Var.to_s0_id(var)+1:03d}": (
107
+ f"MWC{Var.to_s0_id(var) + 1:03d}": (
107
108
  PckGenerator.request_var_status,
108
109
  var,
109
110
  NEW_VAR_SW_AGE,
@@ -111,7 +112,7 @@ COMMANDS = {
111
112
  for var in Var.s0s # type: ignore
112
113
  },
113
114
  **{
114
- f"SE{Var.to_thrs_register_id(var)+1:03d}": (
115
+ f"SE{Var.to_thrs_register_id(var) + 1:03d}": (
115
116
  PckGenerator.request_var_status,
116
117
  var,
117
118
  NEW_VAR_SW_AGE,
@@ -131,11 +132,11 @@ COMMANDS = {
131
132
  },
132
133
  # Output manipulation
133
134
  **{
134
- f"A{output+1:d}DI050123": (PckGenerator.dim_output, output, 50.0, 123)
135
+ f"A{output + 1:d}DI050123": (PckGenerator.dim_output, output, 50.0, 123)
135
136
  for output in range(4)
136
137
  },
137
138
  **{
138
- f"O{output+1:d}DI101123": (PckGenerator.dim_output, output, 50.5, 123)
139
+ f"O{output + 1:d}DI101123": (PckGenerator.dim_output, output, 50.5, 123)
139
140
  for output in range(4)
140
141
  },
141
142
  "OY100100100100123": (PckGenerator.dim_all_outputs, 50.0, 123, 0x180501),
@@ -145,23 +146,23 @@ COMMANDS = {
145
146
  "AE123": (PckGenerator.dim_all_outputs, 100.0, 123, 0x180500),
146
147
  "AH050": (PckGenerator.dim_all_outputs, 50.0, 123, 0x180500),
147
148
  **{
148
- f"A{output+1:d}AD050": (PckGenerator.rel_output, output, 50.0)
149
+ f"A{output + 1:d}AD050": (PckGenerator.rel_output, output, 50.0)
149
150
  for output in range(4)
150
151
  },
151
152
  **{
152
- f"A{output+1:d}SB050": (PckGenerator.rel_output, output, -50.0)
153
+ f"A{output + 1:d}SB050": (PckGenerator.rel_output, output, -50.0)
153
154
  for output in range(4)
154
155
  },
155
156
  **{
156
- f"O{output+1:d}AD101": (PckGenerator.rel_output, output, 50.5)
157
+ f"O{output + 1:d}AD101": (PckGenerator.rel_output, output, 50.5)
157
158
  for output in range(4)
158
159
  },
159
160
  **{
160
- f"O{output+1:d}SB101": (PckGenerator.rel_output, output, -50.5)
161
+ f"O{output + 1:d}SB101": (PckGenerator.rel_output, output, -50.5)
161
162
  for output in range(4)
162
163
  },
163
164
  **{
164
- f"A{output+1:d}TA123": (PckGenerator.toggle_output, output, 123)
165
+ f"A{output + 1:d}TA123": (PckGenerator.toggle_output, output, 123)
165
166
  for output in range(4)
166
167
  },
167
168
  "AU123": (PckGenerator.toggle_all_outputs, 123),
@@ -193,60 +194,108 @@ COMMANDS = {
193
194
  RelayStateModifier.OFF,
194
195
  ],
195
196
  ),
196
- "R810110---": (
197
- PckGenerator.control_motors_relays,
198
- [
199
- MotorStateModifier.UP,
200
- MotorStateModifier.DOWN,
201
- MotorStateModifier.STOP,
202
- MotorStateModifier.NOCHANGE,
203
- ],
197
+ # Motor state manipulation
198
+ "R8--10----": (
199
+ PckGenerator.control_motor_relays,
200
+ 1,
201
+ MotorStateModifier.UP,
204
202
  ),
205
- "R8U--UUU--": (
206
- PckGenerator.control_motors_relays,
207
- [
208
- MotorStateModifier.TOGGLEONOFF,
209
- MotorStateModifier.TOGGLEDIR,
210
- MotorStateModifier.CYCLE,
211
- MotorStateModifier.NOCHANGE,
212
- ],
203
+ "R8-----U--": (
204
+ PckGenerator.control_motor_relays,
205
+ 2,
206
+ MotorStateModifier.TOGGLEDIR,
207
+ ),
208
+ "R8UU------": (
209
+ PckGenerator.control_motor_relays,
210
+ 0,
211
+ MotorStateModifier.CYCLE,
212
+ ),
213
+ "R8M1ZU": (
214
+ PckGenerator.control_motor_relays,
215
+ 0,
216
+ MotorStateModifier.UP,
217
+ MotorPositioningMode.BS4,
218
+ ),
219
+ "R8M2AU": (
220
+ PckGenerator.control_motor_relays,
221
+ 1,
222
+ MotorStateModifier.DOWN,
223
+ MotorPositioningMode.BS4,
224
+ ),
225
+ "R8M5ST": (
226
+ PckGenerator.control_motor_relays,
227
+ 2,
228
+ MotorStateModifier.STOP,
229
+ MotorPositioningMode.BS4,
230
+ ),
231
+ "R8M1GP200": (
232
+ PckGenerator.control_motor_relays_position,
233
+ 0,
234
+ 0.0,
235
+ MotorPositioningMode.BS4,
236
+ ),
237
+ "R8M6GP100": (
238
+ PckGenerator.control_motor_relays_position,
239
+ 3,
240
+ 50.0,
241
+ MotorPositioningMode.BS4,
242
+ ),
243
+ "R8M3P1": (
244
+ PckGenerator.request_motor_position_status,
245
+ 0,
246
+ ),
247
+ "R8M7P2": (
248
+ PckGenerator.request_motor_position_status,
249
+ 1,
250
+ ),
251
+ "JH050001": (
252
+ PckGenerator.control_motor_relays_position,
253
+ 0,
254
+ 50,
255
+ MotorPositioningMode.MODULE,
256
+ ),
257
+ "JH100004": (
258
+ PckGenerator.control_motor_relays_position,
259
+ 2,
260
+ 100,
261
+ MotorPositioningMode.MODULE,
213
262
  ),
214
263
  "X2001228000": (
215
- PckGenerator.control_motors_outputs,
264
+ PckGenerator.control_motor_outputs,
216
265
  MotorStateModifier.UP,
217
266
  MotorReverseTime.RT70,
218
267
  ),
219
268
  "A1DI100008": (
220
- PckGenerator.control_motors_outputs,
269
+ PckGenerator.control_motor_outputs,
221
270
  MotorStateModifier.UP,
222
271
  MotorReverseTime.RT600,
223
272
  ),
224
273
  "A1DI100011": (
225
- PckGenerator.control_motors_outputs,
274
+ PckGenerator.control_motor_outputs,
226
275
  MotorStateModifier.UP,
227
276
  MotorReverseTime.RT1200,
228
277
  ),
229
278
  "X2001000228": (
230
- PckGenerator.control_motors_outputs,
279
+ PckGenerator.control_motor_outputs,
231
280
  MotorStateModifier.DOWN,
232
281
  MotorReverseTime.RT70,
233
282
  ),
234
283
  "A2DI100008": (
235
- PckGenerator.control_motors_outputs,
284
+ PckGenerator.control_motor_outputs,
236
285
  MotorStateModifier.DOWN,
237
286
  MotorReverseTime.RT600,
238
287
  ),
239
288
  "A2DI100011": (
240
- PckGenerator.control_motors_outputs,
289
+ PckGenerator.control_motor_outputs,
241
290
  MotorStateModifier.DOWN,
242
291
  MotorReverseTime.RT1200,
243
292
  ),
244
293
  "AY000000": (
245
- PckGenerator.control_motors_outputs,
294
+ PckGenerator.control_motor_outputs,
246
295
  MotorStateModifier.STOP,
247
296
  ),
248
297
  "JE": (
249
- PckGenerator.control_motors_outputs,
298
+ PckGenerator.control_motor_outputs,
250
299
  MotorStateModifier.CYCLE,
251
300
  ),
252
301
  # Variable manipulation
@@ -277,7 +326,7 @@ COMMANDS = {
277
326
  if var != Var.TVAR
278
327
  },
279
328
  **{
280
- f"RE{('A','B')[nvar]}S{('A','P')[nref]}-500": (
329
+ f"RE{('A', 'B')[nvar]}S{('A', 'P')[nref]}-500": (
281
330
  PckGenerator.var_rel,
282
331
  var,
283
332
  ref,
@@ -289,7 +338,7 @@ COMMANDS = {
289
338
  for sw_age in (0x170206, 0x170205)
290
339
  },
291
340
  **{
292
- f"RE{('A','B')[nvar]}S{('A','P')[nref]}+500": (
341
+ f"RE{('A', 'B')[nvar]}S{('A', 'P')[nref]}+500": (
293
342
  PckGenerator.var_rel,
294
343
  var,
295
344
  ref,
@@ -301,7 +350,7 @@ COMMANDS = {
301
350
  for sw_age in (0x170206, 0x170205)
302
351
  },
303
352
  **{
304
- f"SS{('R','E')[nref]}0500SR{r+1}{i+1}": (
353
+ f"SS{('R', 'E')[nref]}0500SR{r + 1}{i + 1}": (
305
354
  PckGenerator.var_rel,
306
355
  Var.thresholds[r][i], # type: ignore
307
356
  ref,
@@ -313,7 +362,7 @@ COMMANDS = {
313
362
  for nref, ref in enumerate(RelVarRef)
314
363
  },
315
364
  **{
316
- f"SS{('R','E')[nref]}0500AR{r+1}{i+1}": (
365
+ f"SS{('R', 'E')[nref]}0500AR{r + 1}{i + 1}": (
317
366
  PckGenerator.var_rel,
318
367
  Var.thresholds[r][i], # type: ignore
319
368
  ref,
@@ -325,7 +374,7 @@ COMMANDS = {
325
374
  for nref, ref in enumerate(RelVarRef)
326
375
  },
327
376
  **{
328
- f"SS{('R','E')[nref]}0500S{1<<(4-i):05b}": (
377
+ f"SS{('R', 'E')[nref]}0500S{1 << (4 - i):05b}": (
329
378
  PckGenerator.var_rel,
330
379
  Var.thresholds[0][i], # type: ignore
331
380
  ref,
@@ -336,7 +385,7 @@ COMMANDS = {
336
385
  for nref, ref in enumerate(RelVarRef)
337
386
  },
338
387
  **{
339
- f"SS{('R','E')[nref]}0500A{1<<(4-i):05b}": (
388
+ f"SS{('R', 'E')[nref]}0500A{1 << (4 - i):05b}": (
340
389
  PckGenerator.var_rel,
341
390
  Var.thresholds[0][i], # type: ignore
342
391
  ref,
@@ -348,7 +397,7 @@ COMMANDS = {
348
397
  },
349
398
  # Led manipulation
350
399
  **{
351
- f"LA{led+1:03d}{state.value}": (PckGenerator.control_led, led, state)
400
+ f"LA{led + 1:03d}{state.value}": (PckGenerator.control_led, led, state)
352
401
  for led in range(12)
353
402
  for state in LedStatus
354
403
  },
@@ -378,7 +427,7 @@ COMMANDS = {
378
427
  if dcmd != SendKeyCommand.DONTSEND
379
428
  },
380
429
  **{
381
- f"TV{('A','B','C','D')[table]}040{unit.value}11001110": (
430
+ f"TV{('A', 'B', 'C', 'D')[table]}040{unit.value}11001110": (
382
431
  PckGenerator.send_keys_hit_deferred,
383
432
  table,
384
433
  40,
@@ -390,7 +439,7 @@ COMMANDS = {
390
439
  },
391
440
  # Lock keys
392
441
  **{
393
- f"TX{('A','B','C','D')[table]}10U--01U": (
442
+ f"TX{('A', 'B', 'C', 'D')[table]}10U--01U": (
394
443
  PckGenerator.lock_keys,
395
444
  table,
396
445
  [
@@ -417,15 +466,15 @@ COMMANDS = {
417
466
  },
418
467
  # Lock regulator
419
468
  **{
420
- f"RE{('A','B')[reg]:s}XS": (PckGenerator.lock_regulator, reg, True, -1)
469
+ f"RE{('A', 'B')[reg]:s}XS": (PckGenerator.lock_regulator, reg, True, -1)
421
470
  for reg in range(2)
422
471
  },
423
472
  **{
424
- f"RE{('A','B')[reg]:s}XA": (PckGenerator.lock_regulator, reg, False, -1)
473
+ f"RE{('A', 'B')[reg]:s}XA": (PckGenerator.lock_regulator, reg, False, -1)
425
474
  for reg in range(2)
426
475
  },
427
476
  **{
428
- f"X2030{0x40*reg + 0x07:03d}{2*value:03d}": (
477
+ f"X2030{0x40 * reg + 0x07:03d}{2 * value:03d}": (
429
478
  PckGenerator.lock_regulator,
430
479
  reg,
431
480
  True,
@@ -436,11 +485,11 @@ COMMANDS = {
436
485
  for value in (0, 50, 100)
437
486
  },
438
487
  **{
439
- f"RE{('A','B')[reg]:s}XS": (PckGenerator.lock_regulator, reg, True, 0x120301)
488
+ f"RE{('A', 'B')[reg]:s}XS": (PckGenerator.lock_regulator, reg, True, 0x120301)
440
489
  for reg in range(2)
441
490
  },
442
491
  **{
443
- f"RE{('A','B')[reg]:s}XA": (PckGenerator.lock_regulator, reg, False, 0x120301)
492
+ f"RE{('A', 'B')[reg]:s}XA": (PckGenerator.lock_regulator, reg, False, 0x120301)
444
493
  for reg in range(2)
445
494
  },
446
495
  # scenes
@@ -490,7 +539,7 @@ COMMANDS = {
490
539
  ),
491
540
  # dynamic text
492
541
  **{
493
- f"GTDT{row+1:d}{part+1:d}asdfasdfasdf".encode(): (
542
+ f"GTDT{row + 1:d}{part + 1:d}asdfasdfasdf".encode(): (
494
543
  PckGenerator.dyn_text_part,
495
544
  row,
496
545
  part,
@@ -15,6 +15,8 @@ from pypck.inputs import (
15
15
  ModStatusGroups,
16
16
  ModStatusKeyLocks,
17
17
  ModStatusLedsAndLogicOps,
18
+ ModStatusMotorPositionBS4,
19
+ ModStatusMotorPositionModule,
18
20
  ModStatusOutput,
19
21
  ModStatusOutputNative,
20
22
  ModStatusRelays,
@@ -239,6 +241,33 @@ MESSAGES = {
239
241
  [150, 100, 0, 200],
240
242
  )
241
243
  ],
244
+ # Status motor position via BS4
245
+ "=M000010.RM1100?1234567890RM2200200??": [
246
+ (
247
+ ModStatusMotorPositionBS4,
248
+ 0,
249
+ 50,
250
+ None,
251
+ 12345,
252
+ 67890,
253
+ ),
254
+ (
255
+ ModStatusMotorPositionBS4,
256
+ 1,
257
+ 0,
258
+ 0,
259
+ None,
260
+ None,
261
+ ),
262
+ ],
263
+ # Status motor position via module
264
+ ":M000010P1050": [
265
+ (
266
+ ModStatusMotorPositionModule,
267
+ 0,
268
+ 50,
269
+ )
270
+ ],
242
271
  # SKH
243
272
  "+M004000010.SKH000001": [(ModSendCommandHost, (0, 1))],
244
273
  "+M004000010.SKH000001002003004005": [
pypck-0.8.5/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.8.5
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes