pypck 0.8.4__tar.gz → 0.8.6__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.4/pypck.egg-info → pypck-0.8.6}/PKG-INFO +3 -2
  2. pypck-0.8.6/VERSION +1 -0
  3. {pypck-0.8.4 → pypck-0.8.6}/pypck/connection.py +7 -12
  4. {pypck-0.8.4 → pypck-0.8.6}/pypck/inputs.py +156 -5
  5. {pypck-0.8.4 → pypck-0.8.6}/pypck/lcn_defs.py +48 -7
  6. {pypck-0.8.4 → pypck-0.8.6}/pypck/module.py +38 -10
  7. {pypck-0.8.4 → pypck-0.8.6}/pypck/pck_commands.py +121 -38
  8. {pypck-0.8.4 → pypck-0.8.6}/pypck/request_handlers.py +24 -1
  9. {pypck-0.8.4 → pypck-0.8.6}/pypck/timeout_retry.py +1 -4
  10. {pypck-0.8.4 → pypck-0.8.6/pypck.egg-info}/PKG-INFO +3 -2
  11. {pypck-0.8.4 → pypck-0.8.6}/tests/test_commands.py +84 -53
  12. {pypck-0.8.4 → pypck-0.8.6}/tests/test_messages.py +29 -0
  13. pypck-0.8.4/VERSION +0 -1
  14. {pypck-0.8.4 → pypck-0.8.6}/LICENSE +0 -0
  15. {pypck-0.8.4 → pypck-0.8.6}/README.md +0 -0
  16. {pypck-0.8.4 → pypck-0.8.6}/pypck/__init__.py +0 -0
  17. {pypck-0.8.4 → pypck-0.8.6}/pypck/helpers.py +0 -0
  18. {pypck-0.8.4 → pypck-0.8.6}/pypck/lcn_addr.py +0 -0
  19. {pypck-0.8.4 → pypck-0.8.6}/pypck.egg-info/SOURCES.txt +0 -0
  20. {pypck-0.8.4 → pypck-0.8.6}/pypck.egg-info/dependency_links.txt +0 -0
  21. {pypck-0.8.4 → pypck-0.8.6}/pypck.egg-info/not-zip-safe +0 -0
  22. {pypck-0.8.4 → pypck-0.8.6}/pypck.egg-info/top_level.txt +0 -0
  23. {pypck-0.8.4 → pypck-0.8.6}/pyproject.toml +0 -0
  24. {pypck-0.8.4 → pypck-0.8.6}/setup.cfg +0 -0
  25. {pypck-0.8.4 → pypck-0.8.6}/tests/test_connection.py +0 -0
  26. {pypck-0.8.4 → pypck-0.8.6}/tests/test_dyn_text.py +0 -0
  27. {pypck-0.8.4 → pypck-0.8.6}/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.4
3
+ Version: 0.8.6
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.6/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.8.6
@@ -181,10 +181,8 @@ class PchkConnectionManager:
181
181
  )
182
182
  continue
183
183
  await self.process_message(message)
184
- except asyncio.CancelledError:
185
- pass
186
-
187
- _LOGGER.debug("Read data loop closed")
184
+ finally:
185
+ _LOGGER.debug("Read data loop closed")
188
186
 
189
187
  async def write_data_loop(self) -> None:
190
188
  """Processes queue and writes data."""
@@ -204,14 +202,11 @@ class PchkConnectionManager:
204
202
  self.writer.write(data)
205
203
  await self.writer.drain()
206
204
  self.last_bus_activity = time.time()
207
- except asyncio.CancelledError:
208
- pass
209
-
210
- # empty the queue
211
- while not self.buffer.empty():
212
- await self.buffer.get()
213
-
214
- _LOGGER.debug("Write data loop closed")
205
+ finally:
206
+ # empty the queue
207
+ while not self.buffer.empty():
208
+ await self.buffer.get()
209
+ _LOGGER.debug("Write data loop closed")
215
210
 
216
211
  # Open/close connection, authentication & setup.
217
212
 
@@ -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,117 @@ 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
557
- 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
567
+ if 0 > motor_id > 3:
568
+ raise ValueError("Invalid motor id")
572
569
 
573
- return ret
570
+ if mode not in lcn_defs.MotorPositioningMode:
571
+ raise ValueError("Wrong motor position mode")
572
+
573
+ if mode == lcn_defs.MotorPositioningMode.BS4:
574
+ if state not in [
575
+ lcn_defs.MotorStateModifier.UP,
576
+ lcn_defs.MotorStateModifier.DOWN,
577
+ ]:
578
+ raise ValueError("Invalid motor state for BS4 mode")
579
+
580
+ new_motor_id = [1, 2, 5, 6][motor_id]
581
+ # AU=window open / cover down
582
+ # ZU=window close / cover up
583
+ action = "AU" if state == lcn_defs.MotorStateModifier.DOWN else "ZU"
584
+ return f"R8M{new_motor_id}{action}"
585
+
586
+ # lcn_defs.MotorPositioningMode.NONE
587
+ # lcn_defs.MotorPositioningMode.MODULE
588
+ if state == lcn_defs.MotorStateModifier.UP:
589
+ port_onoff = lcn_defs.RelayStateModifier.ON
590
+ port_updown = lcn_defs.RelayStateModifier.OFF
591
+ elif state == lcn_defs.MotorStateModifier.DOWN:
592
+ port_onoff = lcn_defs.RelayStateModifier.ON
593
+ port_updown = lcn_defs.RelayStateModifier.ON
594
+ elif state == lcn_defs.MotorStateModifier.STOP:
595
+ port_onoff = lcn_defs.RelayStateModifier.OFF
596
+ port_updown = lcn_defs.RelayStateModifier.NOCHANGE
597
+ elif state == lcn_defs.MotorStateModifier.TOGGLEONOFF:
598
+ port_onoff = lcn_defs.RelayStateModifier.TOGGLE
599
+ port_updown = lcn_defs.RelayStateModifier.NOCHANGE
600
+ elif state == lcn_defs.MotorStateModifier.TOGGLEDIR:
601
+ port_onoff = lcn_defs.RelayStateModifier.NOCHANGE
602
+ port_updown = lcn_defs.RelayStateModifier.TOGGLE
603
+ elif state == lcn_defs.MotorStateModifier.CYCLE:
604
+ port_onoff = lcn_defs.RelayStateModifier.TOGGLE
605
+ port_updown = lcn_defs.RelayStateModifier.TOGGLE
606
+ elif state == lcn_defs.MotorStateModifier.NOCHANGE:
607
+ port_onoff = lcn_defs.RelayStateModifier.NOCHANGE
608
+ port_updown = lcn_defs.RelayStateModifier.NOCHANGE
609
+ else:
610
+ raise ValueError("Invalid motor state")
611
+
612
+ states = [lcn_defs.RelayStateModifier.NOCHANGE] * 8
613
+ states[motor_id * 2] = port_onoff
614
+ states[motor_id * 2 + 1] = port_updown
615
+ return "R8" + "".join([state.value for state in states])
574
616
 
575
617
  @staticmethod
576
- def control_motors_outputs(
618
+ def control_motor_relays_position(
619
+ motor_id: int, position: float, mode: lcn_defs.MotorPositioningMode
620
+ ) -> str:
621
+ """Control motor position via relays and BS4 or module.
622
+
623
+ :param int motor_id: The motor port of the LCN module
624
+ :param float position: The position to set in percentage (0..100)
625
+ (0: closed cover, 100: open cover)
626
+ :param MotorPositioningMode mode: The motor positioning mode
627
+
628
+ :return: The PCK command (without address header) as text
629
+ :rtype: str
630
+ """
631
+ if mode not in (
632
+ lcn_defs.MotorPositioningMode.BS4,
633
+ lcn_defs.MotorPositioningMode.MODULE,
634
+ ):
635
+ raise ValueError("Wrong motor positioning mode")
636
+
637
+ if 0 > motor_id > 3:
638
+ raise ValueError("Invalid motor")
639
+
640
+ if mode == lcn_defs.MotorPositioningMode.BS4:
641
+ new_motor_id = [1, 2, 5, 6][motor_id]
642
+ action = f"GP{int(200 - 2 * position):03d}"
643
+ return f"R8M{new_motor_id}{action}"
644
+ elif mode == lcn_defs.MotorPositioningMode.MODULE:
645
+ new_motor_id = 1 << motor_id
646
+ return f"JH{position:03d}{new_motor_id:03d}"
647
+
648
+ return ""
649
+
650
+ @staticmethod
651
+ def request_motor_position_status(motor_pair: int) -> str:
652
+ """Generate a motor position status request for BS4.
653
+
654
+ :param int motor_pair: Motor pair 0: 1, 2; 1: 3, 4
655
+
656
+ :return: The PCK command (without address header) as text
657
+ :rtype: str
658
+ """
659
+ if motor_pair not in [0, 1]:
660
+ raise ValueError("Invalid motor_pair.")
661
+ return f"R8M{7 if motor_pair else 3}P{motor_pair + 1}"
662
+
663
+ @staticmethod
664
+ def control_motor_outputs(
577
665
  state: lcn_defs.MotorStateModifier,
578
666
  reverse_time: lcn_defs.MotorReverseTime | None = None,
579
667
  ) -> str:
@@ -728,16 +816,11 @@ class PckGenerator:
728
816
  if var_id == 0:
729
817
  # Old command for variable 1 / T-var (compatible with all
730
818
  # modules)
731
- pck = "Z" f"{'A' if value >= 0 else 'S'}" f"{abs(value)}"
819
+ pck = f"Z{'A' if value >= 0 else 'S'}{abs(value)}"
732
820
  else:
733
821
  # New command for variable 1-12 (compatible with all modules,
734
822
  # 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
- )
823
+ pck = f"Z{'+' if value >= 0 else '-'}{var_id + 1:03d}{abs(value)}"
741
824
  return pck
742
825
 
743
826
  set_point_id = lcn_defs.Var.to_set_point_id(var)
@@ -1051,7 +1134,7 @@ class PckGenerator:
1051
1134
  raise ValueError("Wrong target_value.")
1052
1135
  if (target_value != -1) and (software_serial >= 0x120301) and state:
1053
1136
  reg_byte = reg_id * 0x40 + 0x07
1054
- return f"X2{0x1E:03d}{reg_byte:03d}{int(2*target_value):03d}"
1137
+ return f"X2{0x1E:03d}{reg_byte:03d}{int(2 * target_value):03d}"
1055
1138
  return f"RE{'A' if reg_id == 0 else 'B'}X{'S' if state else 'A'}"
1056
1139
 
1057
1140
  @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:
@@ -65,10 +65,7 @@ class TimeoutRetryHandler:
65
65
  async def done(self) -> None:
66
66
  """Signal the completion of the TimeoutRetryHandler."""
67
67
  if self.timeout_loop_task is not None:
68
- try:
69
- await self.timeout_loop_task
70
- except asyncio.CancelledError:
71
- pass
68
+ await self.timeout_loop_task
72
69
 
73
70
  async def cancel(self) -> None:
74
71
  """Must be called when a response (requested or not) is received."""
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: pypck
3
- Version: 0.8.4
3
+ Version: 0.8.6
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,90 @@ 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
+ "R8M1GP200": (
214
+ PckGenerator.control_motor_relays_position,
215
+ 0,
216
+ 0.0,
217
+ MotorPositioningMode.BS4,
218
+ ),
219
+ "R8M6GP100": (
220
+ PckGenerator.control_motor_relays_position,
221
+ 3,
222
+ 50.0,
223
+ MotorPositioningMode.BS4,
224
+ ),
225
+ "R8M3P1": (
226
+ PckGenerator.request_motor_position_status,
227
+ 0,
228
+ ),
229
+ "R8M7P2": (
230
+ PckGenerator.request_motor_position_status,
231
+ 1,
232
+ ),
233
+ "JH050001": (
234
+ PckGenerator.control_motor_relays_position,
235
+ 0,
236
+ 50,
237
+ MotorPositioningMode.MODULE,
238
+ ),
239
+ "JH100004": (
240
+ PckGenerator.control_motor_relays_position,
241
+ 2,
242
+ 100,
243
+ MotorPositioningMode.MODULE,
213
244
  ),
214
245
  "X2001228000": (
215
- PckGenerator.control_motors_outputs,
246
+ PckGenerator.control_motor_outputs,
216
247
  MotorStateModifier.UP,
217
248
  MotorReverseTime.RT70,
218
249
  ),
219
250
  "A1DI100008": (
220
- PckGenerator.control_motors_outputs,
251
+ PckGenerator.control_motor_outputs,
221
252
  MotorStateModifier.UP,
222
253
  MotorReverseTime.RT600,
223
254
  ),
224
255
  "A1DI100011": (
225
- PckGenerator.control_motors_outputs,
256
+ PckGenerator.control_motor_outputs,
226
257
  MotorStateModifier.UP,
227
258
  MotorReverseTime.RT1200,
228
259
  ),
229
260
  "X2001000228": (
230
- PckGenerator.control_motors_outputs,
261
+ PckGenerator.control_motor_outputs,
231
262
  MotorStateModifier.DOWN,
232
263
  MotorReverseTime.RT70,
233
264
  ),
234
265
  "A2DI100008": (
235
- PckGenerator.control_motors_outputs,
266
+ PckGenerator.control_motor_outputs,
236
267
  MotorStateModifier.DOWN,
237
268
  MotorReverseTime.RT600,
238
269
  ),
239
270
  "A2DI100011": (
240
- PckGenerator.control_motors_outputs,
271
+ PckGenerator.control_motor_outputs,
241
272
  MotorStateModifier.DOWN,
242
273
  MotorReverseTime.RT1200,
243
274
  ),
244
275
  "AY000000": (
245
- PckGenerator.control_motors_outputs,
276
+ PckGenerator.control_motor_outputs,
246
277
  MotorStateModifier.STOP,
247
278
  ),
248
279
  "JE": (
249
- PckGenerator.control_motors_outputs,
280
+ PckGenerator.control_motor_outputs,
250
281
  MotorStateModifier.CYCLE,
251
282
  ),
252
283
  # Variable manipulation
@@ -277,7 +308,7 @@ COMMANDS = {
277
308
  if var != Var.TVAR
278
309
  },
279
310
  **{
280
- f"RE{('A','B')[nvar]}S{('A','P')[nref]}-500": (
311
+ f"RE{('A', 'B')[nvar]}S{('A', 'P')[nref]}-500": (
281
312
  PckGenerator.var_rel,
282
313
  var,
283
314
  ref,
@@ -289,7 +320,7 @@ COMMANDS = {
289
320
  for sw_age in (0x170206, 0x170205)
290
321
  },
291
322
  **{
292
- f"RE{('A','B')[nvar]}S{('A','P')[nref]}+500": (
323
+ f"RE{('A', 'B')[nvar]}S{('A', 'P')[nref]}+500": (
293
324
  PckGenerator.var_rel,
294
325
  var,
295
326
  ref,
@@ -301,7 +332,7 @@ COMMANDS = {
301
332
  for sw_age in (0x170206, 0x170205)
302
333
  },
303
334
  **{
304
- f"SS{('R','E')[nref]}0500SR{r+1}{i+1}": (
335
+ f"SS{('R', 'E')[nref]}0500SR{r + 1}{i + 1}": (
305
336
  PckGenerator.var_rel,
306
337
  Var.thresholds[r][i], # type: ignore
307
338
  ref,
@@ -313,7 +344,7 @@ COMMANDS = {
313
344
  for nref, ref in enumerate(RelVarRef)
314
345
  },
315
346
  **{
316
- f"SS{('R','E')[nref]}0500AR{r+1}{i+1}": (
347
+ f"SS{('R', 'E')[nref]}0500AR{r + 1}{i + 1}": (
317
348
  PckGenerator.var_rel,
318
349
  Var.thresholds[r][i], # type: ignore
319
350
  ref,
@@ -325,7 +356,7 @@ COMMANDS = {
325
356
  for nref, ref in enumerate(RelVarRef)
326
357
  },
327
358
  **{
328
- f"SS{('R','E')[nref]}0500S{1<<(4-i):05b}": (
359
+ f"SS{('R', 'E')[nref]}0500S{1 << (4 - i):05b}": (
329
360
  PckGenerator.var_rel,
330
361
  Var.thresholds[0][i], # type: ignore
331
362
  ref,
@@ -336,7 +367,7 @@ COMMANDS = {
336
367
  for nref, ref in enumerate(RelVarRef)
337
368
  },
338
369
  **{
339
- f"SS{('R','E')[nref]}0500A{1<<(4-i):05b}": (
370
+ f"SS{('R', 'E')[nref]}0500A{1 << (4 - i):05b}": (
340
371
  PckGenerator.var_rel,
341
372
  Var.thresholds[0][i], # type: ignore
342
373
  ref,
@@ -348,7 +379,7 @@ COMMANDS = {
348
379
  },
349
380
  # Led manipulation
350
381
  **{
351
- f"LA{led+1:03d}{state.value}": (PckGenerator.control_led, led, state)
382
+ f"LA{led + 1:03d}{state.value}": (PckGenerator.control_led, led, state)
352
383
  for led in range(12)
353
384
  for state in LedStatus
354
385
  },
@@ -378,7 +409,7 @@ COMMANDS = {
378
409
  if dcmd != SendKeyCommand.DONTSEND
379
410
  },
380
411
  **{
381
- f"TV{('A','B','C','D')[table]}040{unit.value}11001110": (
412
+ f"TV{('A', 'B', 'C', 'D')[table]}040{unit.value}11001110": (
382
413
  PckGenerator.send_keys_hit_deferred,
383
414
  table,
384
415
  40,
@@ -390,7 +421,7 @@ COMMANDS = {
390
421
  },
391
422
  # Lock keys
392
423
  **{
393
- f"TX{('A','B','C','D')[table]}10U--01U": (
424
+ f"TX{('A', 'B', 'C', 'D')[table]}10U--01U": (
394
425
  PckGenerator.lock_keys,
395
426
  table,
396
427
  [
@@ -417,15 +448,15 @@ COMMANDS = {
417
448
  },
418
449
  # Lock regulator
419
450
  **{
420
- f"RE{('A','B')[reg]:s}XS": (PckGenerator.lock_regulator, reg, True, -1)
451
+ f"RE{('A', 'B')[reg]:s}XS": (PckGenerator.lock_regulator, reg, True, -1)
421
452
  for reg in range(2)
422
453
  },
423
454
  **{
424
- f"RE{('A','B')[reg]:s}XA": (PckGenerator.lock_regulator, reg, False, -1)
455
+ f"RE{('A', 'B')[reg]:s}XA": (PckGenerator.lock_regulator, reg, False, -1)
425
456
  for reg in range(2)
426
457
  },
427
458
  **{
428
- f"X2030{0x40*reg + 0x07:03d}{2*value:03d}": (
459
+ f"X2030{0x40 * reg + 0x07:03d}{2 * value:03d}": (
429
460
  PckGenerator.lock_regulator,
430
461
  reg,
431
462
  True,
@@ -436,11 +467,11 @@ COMMANDS = {
436
467
  for value in (0, 50, 100)
437
468
  },
438
469
  **{
439
- f"RE{('A','B')[reg]:s}XS": (PckGenerator.lock_regulator, reg, True, 0x120301)
470
+ f"RE{('A', 'B')[reg]:s}XS": (PckGenerator.lock_regulator, reg, True, 0x120301)
440
471
  for reg in range(2)
441
472
  },
442
473
  **{
443
- f"RE{('A','B')[reg]:s}XA": (PckGenerator.lock_regulator, reg, False, 0x120301)
474
+ f"RE{('A', 'B')[reg]:s}XA": (PckGenerator.lock_regulator, reg, False, 0x120301)
444
475
  for reg in range(2)
445
476
  },
446
477
  # scenes
@@ -490,7 +521,7 @@ COMMANDS = {
490
521
  ),
491
522
  # dynamic text
492
523
  **{
493
- f"GTDT{row+1:d}{part+1:d}asdfasdfasdf".encode(): (
524
+ f"GTDT{row + 1:d}{part + 1:d}asdfasdfasdf".encode(): (
494
525
  PckGenerator.dyn_text_part,
495
526
  row,
496
527
  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.4/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.8.4
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