mpf 0.57.5.dev3__py3-none-any.whl → 0.58.0.dev3__py3-none-any.whl

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 (51) hide show
  1. mpf/_version.py +2 -2
  2. mpf/config_spec.yaml +7 -7
  3. mpf/core/device_manager.py +14 -1
  4. mpf/core/events.py +9 -6
  5. mpf/core/placeholder_manager.py +21 -1
  6. mpf/core/utility_functions.py +11 -6
  7. mpf/devices/light.py +165 -157
  8. mpf/devices/light_group.py +45 -13
  9. mpf/devices/magnet.py +1 -1
  10. mpf/modes/match/code/match.py +15 -5
  11. mpf/parsers/event_reference_parser.py +10 -6
  12. mpf/platforms/fast/communicators/exp.py +5 -0
  13. mpf/platforms/fast/fast.py +168 -117
  14. mpf/platforms/fast/fast_defines.py +10 -10
  15. mpf/platforms/fast/fast_exp_board.py +96 -5
  16. mpf/platforms/fast/fast_led.py +22 -8
  17. mpf/platforms/pinproc/windows/pinproc.cp313-win32.pyd +0 -0
  18. mpf/platforms/pinproc/windows/pinproc.cp313-win_amd64.pyd +0 -0
  19. mpf/platforms/pinproc/windows/pinproc.cp314-win32.pyd +0 -0
  20. mpf/platforms/pinproc/windows/pinproc.cp314-win_amd64.pyd +0 -0
  21. mpf/platforms/virtual_pinball/virtual_pinball.py +22 -1
  22. mpf/tests/machine_files/auditor/config/config.yaml +1 -1
  23. mpf/tests/machine_files/custom_code/code/test_code.py +4 -0
  24. mpf/tests/machine_files/event_manager/config/test_event_manager.yaml +6 -0
  25. mpf/tests/machine_files/event_manager/modes/game_mode/config/game_mode.yaml +5 -0
  26. mpf/tests/machine_files/fast/config/exp.yaml +27 -7
  27. mpf/tests/machine_files/light/config/light_groups.yaml +12 -0
  28. mpf/tests/machine_files/settings/config/config.yaml +27 -0
  29. mpf/tests/test_CustomCode.py +7 -0
  30. mpf/tests/test_DeviceManager.py +63 -0
  31. mpf/tests/test_EventManager.py +45 -0
  32. mpf/tests/test_Fast_Exp.py +142 -90
  33. mpf/tests/test_Fast_Led.py +61 -0
  34. mpf/tests/test_LightGroups.py +48 -16
  35. mpf/tests/test_LightNumbering.py +2 -3
  36. mpf/tests/test_MatchMode.py +16 -4
  37. mpf/tests/test_RandomEventPlayer.py +4 -4
  38. mpf/tests/test_Settings.py +16 -0
  39. mpf/tests/test_Utility_Functions.py +54 -7
  40. mpf/tests/test_VPX.py +71 -0
  41. {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/METADATA +2 -2
  42. {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/RECORD +47 -46
  43. {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/WHEEL +1 -1
  44. mpf/platforms/pinproc/windows/pinproc.cp38-win32.pyd +0 -0
  45. mpf/platforms/pinproc/windows/pinproc.cp38-win_amd64.pyd +0 -0
  46. mpf/platforms/pinproc/windows/pinproc.cp39-win32.pyd +0 -0
  47. mpf/platforms/pinproc/windows/pinproc.cp39-win_amd64.pyd +0 -0
  48. {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/entry_points.txt +0 -0
  49. {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/licenses/AUTHORS.md +0 -0
  50. {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/licenses/LICENSE.md +0 -0
  51. {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/top_level.txt +0 -0
mpf/_version.py CHANGED
@@ -10,10 +10,10 @@ PyPI.
10
10
 
11
11
  """
12
12
 
13
- __version__ = '0.57.5.dev3' # Also consider whether MPF-MC pyproject.toml should be updated
13
+ __version__ = '0.58.0.dev3' # Also consider whether MPF-MC pyproject.toml should be updated
14
14
  '''The full version of MPF.'''
15
15
 
16
- __short_version__ = '0.57'
16
+ __short_version__ = '0.58'
17
17
  '''The major.minor version of MPF.'''
18
18
 
19
19
  __bcp_version__ = '1.1'
mpf/config_spec.yaml CHANGED
@@ -622,7 +622,7 @@ fast:
622
622
  __type__: config
623
623
  net: single|subconfig(fast_net)|None
624
624
  exp: single|subconfig(fast_exp)|None
625
- exp_int: single|subconfig(fast_exp)|None
625
+ exp_int: single|subconfig(fast_exp)|None
626
626
  aud: single|subconfig(fast_aud)|None
627
627
  seg: single|subconfig(fast_seg)|None
628
628
  dmd: single|subconfig(fast_dmd)|None
@@ -668,11 +668,11 @@ fast_exp_board:
668
668
  fast_breakout:
669
669
  port: single|enum(1,2,3)|
670
670
  model: single|str|
671
- led_ports: list|subconfig(fast_led_port)|None
672
671
  fast_led_port:
673
- port: single|str|
674
- type: single|enum(ws2812,apa-102)|ws2812
675
- leds: single|int|32
672
+ port: single|int|
673
+ type: single|enum(ws2812,apa-102,sk6812,mixed)|ws2812
674
+ count: single|int|32
675
+ rgbw_numbers: list|int|None # only used with the "mixed" type
676
676
  fast_aud:
677
677
  port: list|str|auto
678
678
  baud: single|int|230400
@@ -944,7 +944,7 @@ neoseg_displays:
944
944
  number_template: single|str|None
945
945
  start_x: single|float|None
946
946
  start_y: single|float|None
947
- size: single|enum(2digit,8digit)|8digit
947
+ size: single|enum(2digit,8digit,2digit-rg-swapped,8digit-rg-swapped)|8digit
948
948
  light_template: single|subconfig(lights,device)|
949
949
  lights:
950
950
  __valid_in__: machine
@@ -1573,7 +1573,7 @@ settings:
1573
1573
  label: single|str|
1574
1574
  sort: single|int|
1575
1575
  values: dict|str:str|
1576
- key_type: single|enum(str,float,int)|str
1576
+ key_type: single|enum(str,float,int,bool)|str
1577
1577
  default: single|str|
1578
1578
  machine_var: single|str|None
1579
1579
  settingType: single|str|standard
@@ -172,7 +172,20 @@ class DeviceManager(MpfController):
172
172
  for device_name in config:
173
173
  futures.append(collection[device_name].device_added_system_wide())
174
174
 
175
- await asyncio.wait([asyncio.create_task(futures) for futures in futures])
175
+ fs = [asyncio.create_task(future) for future in futures]
176
+ done, pending = await asyncio.wait(fs, return_when=asyncio.FIRST_EXCEPTION)
177
+
178
+ # Check if any of the futures returned an exception and throw it
179
+ for task in done:
180
+ if task.cancelled():
181
+ continue
182
+
183
+ exc = task.exception()
184
+ if exc is not None:
185
+ for pending_task in pending:
186
+ pending_task.cancel()
187
+ await asyncio.gather(*pending, return_exceptions=True)
188
+ raise exc
176
189
 
177
190
  # pylint: disable-msg=too-many-nested-blocks
178
191
  def get_device_control_events(self, config) -> Generator[Tuple[str, Callable, int, "Device"], None, None]:
mpf/core/events.py CHANGED
@@ -224,14 +224,15 @@ class EventManager(MpfController):
224
224
  'handler you passed?'.format(handler, event))
225
225
 
226
226
  sig = inspect.signature(handler)
227
- if 'kwargs' not in sig.parameters:
228
- raise AssertionError("Handler {} for event '{}' is missing **kwargs. Actual signature: {}".format(
229
- handler, event, sig))
230
-
231
- if sig.parameters['kwargs'].kind != inspect.Parameter.VAR_KEYWORD:
232
- raise AssertionError("Handler {} for event '{}' param kwargs is missing '**'. "
227
+ catchall_name = next((name for name in ('kwargs', '_kwargs') if name in sig.parameters), None)
228
+ if catchall_name is None:
229
+ raise AssertionError("Handler {} for event '{}' is missing **kwargs. "
233
230
  "Actual signature: {}".format(handler, event, sig))
234
231
 
232
+ if sig.parameters[catchall_name].kind != inspect.Parameter.VAR_KEYWORD:
233
+ raise AssertionError("Handler {} for event '{}' param {} is missing '**'. "
234
+ "Actual signature: {}".format(handler, event, catchall_name, sig))
235
+
235
236
  event, condition, additional_priority = self.get_event_and_condition_from_string(event)
236
237
  priority += additional_priority
237
238
 
@@ -457,6 +458,8 @@ class EventManager(MpfController):
457
458
  _future=future,
458
459
  _keys=keys,
459
460
  event=event_name)))
461
+
462
+ future.add_done_callback(lambda f: [self.remove_handler_by_key(k) for k in keys])
460
463
  return future
461
464
 
462
465
  def _wait_handler(self, _future: asyncio.Future, _keys: List[EventHandlerKey], **kwargs):
@@ -4,7 +4,7 @@ import string
4
4
  import asyncio
5
5
  import operator as op
6
6
  import abc
7
- from functools import lru_cache
7
+ from functools import lru_cache, partial
8
8
 
9
9
  import re
10
10
  from typing import Tuple, List, Any, Union
@@ -860,8 +860,28 @@ class BasePlaceholderManager(MpfController):
860
860
  subscriptions.append(self.machine.wait_for_stop())
861
861
  future = Util.any(subscriptions)
862
862
  future = asyncio.ensure_future(future)
863
+
864
+ future.add_done_callback(
865
+ partial(self._cleanup_placeholder_subscriptions, subscriptions=subscriptions)
866
+ )
863
867
  return value, future
864
868
 
869
+ def _cleanup_placeholder_subscriptions(self, future, subscriptions):
870
+ """Cancel child placeholder subscriptions when the parent template frame finishes."""
871
+ del future
872
+
873
+ for sub in subscriptions:
874
+ if isinstance(sub, asyncio.Future):
875
+ if not sub.done():
876
+ # Cancel outstanding asyncio Future/Task instance
877
+ sub.cancel()
878
+ elif asyncio.iscoroutine(sub):
879
+ try:
880
+ # close raw coroutines (like sleep)
881
+ sub.close()
882
+ except RuntimeWarning:
883
+ pass
884
+
865
885
  @lru_cache(typed=True)
866
886
  def parse_conditional_template(self, template, default_number=None):
867
887
  """Parse a template for condition and number and return a dict."""
@@ -57,6 +57,8 @@ class Util:
57
57
  return float(value)
58
58
  if type_name == "str":
59
59
  return str(value)
60
+ if type_name == "bool":
61
+ return str(value).lower() in ['true', 't', 'yes', 'enable', 'on', '1']
60
62
 
61
63
  raise AssertionError("Unknown type {}".format(type_name))
62
64
 
@@ -586,13 +588,16 @@ class Util:
586
588
  if isinstance(time_string, (int, float)):
587
589
  return int(time_string)
588
590
 
589
- time_string = str(time_string).upper()
591
+ time_string = str(time_string).upper().strip()
590
592
 
591
- if time_string.endswith('MS') or time_string.endswith('MSEC'):
592
- return int(time_string[:-2])
593
+ if time_string == 'NONE':
594
+ return 0
595
+
596
+ if time_string.endswith('MS'):
597
+ return int(float(time_string[:-2]))
593
598
 
594
599
  if time_string.endswith('MSEC'):
595
- return int(time_string[:-4])
600
+ return int(float(time_string[:-4]))
596
601
 
597
602
  if time_string.endswith('D'):
598
603
  return int(float(time_string[:-1]) * 86400 * 1000)
@@ -609,7 +614,7 @@ class Util:
609
614
  if time_string.endswith('SEC'):
610
615
  return int(float(time_string[:-3]) * 1000)
611
616
 
612
- return int(time_string)
617
+ return int(float(time_string))
613
618
 
614
619
  @staticmethod
615
620
  def string_to_secs(time_string: str) -> float:
@@ -847,4 +852,4 @@ class Util:
847
852
  bool: True if the bit is set, False otherwise
848
853
  """
849
854
  num = int(hex_string, 16)
850
- return bool(num & (1 << bit))
855
+ return bool(num & (1 << bit))
mpf/devices/light.py CHANGED
@@ -138,47 +138,21 @@ class Light(SystemWideDevice, DevicePositionMixin):
138
138
 
139
139
  check_set.add(key)
140
140
 
141
- def _map_channels_to_colors(self, channel_list) -> dict:
142
- if self.config['type']:
143
- color_channels = self.config['type']
144
- else:
145
- if len(channel_list) == 1:
146
- # for one channel default to a white channel
147
- color_channels = "w"
148
- elif len(channel_list) == 3:
149
- # for three channels default to RGB
150
- color_channels = "rgb"
151
- else:
152
- self.raise_config_error("Please provide a type for light {}. No default for channels {}.".
153
- format(self.name, channel_list), 11)
141
+ def _color_letter_to_name(self, letter):
142
+ if letter == 'r':
143
+ return 'red'
154
144
 
155
- if len(channel_list) != len(color_channels):
156
- self.raise_config_error("Type {} does not match channels {} for light {}".format(
157
- color_channels, channel_list, self.name), 12)
145
+ if letter == 'g':
146
+ return 'green'
158
147
 
159
- channels = {} # type: Dict[str, List[Any]]
160
- for color_name in color_channels:
161
- # red channel
162
- if color_name == 'r':
163
- full_color_name = "red"
164
- # green channel
165
- elif color_name == 'g':
166
- full_color_name = "green"
167
- # blue channel
168
- elif color_name == 'b':
169
- full_color_name = "blue"
170
- # simple white channel
171
- elif color_name == 'w':
172
- full_color_name = "white"
173
- else:
174
- self.raise_config_error("Invalid element {} in type {} of light {}".format(
175
- color_name, self.config['type'], self.name), 13)
148
+ if letter == 'b':
149
+ return 'blue'
176
150
 
177
- if full_color_name not in channels:
178
- channels[full_color_name] = []
179
- channels[full_color_name].append(channel_list.pop(0))
151
+ if letter == 'w':
152
+ return 'white'
180
153
 
181
- return channels
154
+ self.raise_config_error("Invalid element {} in type {} of light {}".format(
155
+ letter, self.config['type'], self.name), 14)
182
156
 
183
157
  def wait_for_loaded(self):
184
158
  """Return future."""
@@ -195,29 +169,64 @@ class Light(SystemWideDevice, DevicePositionMixin):
195
169
  sorted_channels = sorted(all_drivers)
196
170
  return sorted_channels[-1].get_successor_number()
197
171
 
198
- def _load_hw_driver_sequentially(self, next_channel):
199
- if self.config['number'] or self.config['channels']:
200
- self.raise_config_error("Cannot use start_channel/previous and number or channels.", 3)
172
+ async def _initialize(self):
173
+ await super()._initialize()
174
+ try:
175
+ if self.config['previous']:
176
+ self._detect_previous_reference_loop()
177
+ await self.config['previous'].wait_for_loaded()
178
+ start_channel = self.config['previous'].get_successor_number()
179
+ self._load_hw_driver_sequentially(start_channel)
180
+ elif self.config['start_channel']:
181
+ self._load_hw_driver_sequentially(self.config['start_channel'])
182
+ else:
183
+ self._load_hw_drivers()
184
+
185
+ self._drivers_loaded.set_result(True)
186
+ self.config['default_on_color'] = RGBColor(self.config['default_on_color'])
187
+
188
+ self._apply_color_correction_profile(self.config['color_correction_profile'])
189
+ self._apply_fade(self.config['fade_ms'])
190
+ self._apply_rgbw_style()
191
+
192
+ self.debug_log("Initializing Light. CC Profile: %s, Default fade: %sms",
193
+ self._color_correction_profile,
194
+ self.default_fade_ms)
195
+ except Exception:
196
+ self._drivers_loaded.cancel()
197
+ raise
198
+
199
+ def _detect_previous_reference_loop(self):
200
+ if self.config['previous'].name == self.name:
201
+ self.raise_config_error(
202
+ "Failed to configure light {} in platform. 'previous' value cannot refer to itself.".
203
+ format(self.name), 8)
204
+
205
+ # If we are in development mode, do a robust tree traversal to catch infinite light loops
206
+ if not self.machine.options['production']:
207
+ tree = [self.name]
208
+ prev = self.config['previous']
209
+ while prev:
210
+ tree.append(prev.name)
211
+ prev = prev.config.get('previous')
212
+ if prev is not None and prev.name in tree:
213
+ tree.append(prev.name)
214
+ self.raise_config_error("Cyclical light chain found: {}".format(" -> ".join(tree)), 9)
215
+
216
+ def _load_hw_driver_sequentially(self, first_channel):
217
+ if self.config['number']:
218
+ self.raise_config_error("Cannot use number with start_channel/previous.", 15)
219
+
220
+ if self.config['channels']:
221
+ self.raise_config_error("Cannot use channels list with start_channel/previous.", 16)
222
+
201
223
  if not self.config['type']:
202
224
  self.raise_config_error("Cannot use previous or start_channel without type. "
203
225
  "Add a type setting to your light.", 2)
204
226
 
205
- for color_name in self.config['type']:
206
- # red channel
207
- if color_name == 'r':
208
- full_color_name = "red"
209
- # green channel
210
- elif color_name == 'g':
211
- full_color_name = "green"
212
- # blue channel
213
- elif color_name == 'b':
214
- full_color_name = "blue"
215
- # simple white channel
216
- elif color_name == 'w':
217
- full_color_name = "white"
218
- else:
219
- self.raise_config_error("Invalid element {} in type {} of light {}".format(
220
- color_name, self.config['type'], self.name), 14)
227
+ next_channel = first_channel
228
+ for color_letter in self.config['type']:
229
+ full_color_name = self._color_letter_to_name(color_letter)
221
230
 
222
231
  if full_color_name not in self.hw_drivers:
223
232
  self.hw_drivers[full_color_name] = []
@@ -225,37 +234,14 @@ class Light(SystemWideDevice, DevicePositionMixin):
225
234
  'platform_settings': self.config['platform_settings'], 'number': next_channel}
226
235
  channel = self.machine.config_validator.validate_config("light_channels", channel)
227
236
  driver = self._load_hw_driver(channel, full_color_name)
228
- next_channel = driver.get_successor_number()
237
+ next_channel = driver.get_successor_number() # increment for the next loop
229
238
  self.hw_drivers[full_color_name].append(driver)
230
239
 
231
240
  def _load_hw_drivers(self):
232
241
  if not self.config['channels']:
233
- # get channels from number + platform
234
- platform = self.machine.get_platform_sections('lights', self.config['platform'])
235
- platform.assert_has_feature("lights")
236
- try:
237
- channel_list = platform.parse_light_number_to_channels(self.config['number'], self.config['subtype'])
238
- except AssertionError as e:
239
- self.raise_config_error("Failed to parse light number {} in platform. See error above".
240
- format(self.name), 4, source_exception=e)
241
-
242
- # copy platform and platform_settings to all channels
243
- for channel, _ in enumerate(channel_list):
244
- channel_list[channel]['subtype'] = self.config['subtype']
245
- channel_list[channel]['platform'] = self.config['platform']
246
- channel_list[channel]['platform_settings'] = self.config['platform_settings']
247
- # map channels to colors
248
- channels = self._map_channels_to_colors(channel_list)
242
+ channels = self._derive_platform_channels()
249
243
  else:
250
- if self.config['number'] or self.config['platform'] or self.config['platform_settings']:
251
- self.raise_config_error("Light {} cannot contain platform/platform_settings/number and channels".
252
- format(self.name), 5)
253
- # alternatively use channels from config
254
- channels = self.config['channels']
255
- # ensure that we got lists
256
- for channel in channels:
257
- if not isinstance(channels[channel], list):
258
- channels[channel] = [channels[channel]]
244
+ channels = self._prepare_config_channels()
259
245
 
260
246
  if not channels:
261
247
  self.raise_config_error("Light {} has no channels.".format(self.name), 6)
@@ -275,10 +261,7 @@ class Light(SystemWideDevice, DevicePositionMixin):
275
261
  if not platform.features['allow_empty_numbers'] and channel['number'] is None:
276
262
  self.raise_config_error("Light must have a number.", 1)
277
263
 
278
- config = LightConfig(
279
- name=self.name,
280
- color=LightConfigColors[color.upper()]
281
- )
264
+ config = LightConfig(name=self.name, color=LightConfigColors[color.upper()])
282
265
 
283
266
  try:
284
267
  return platform.configure_light(channel['number'], channel['subtype'], config, channel['platform_settings'])
@@ -286,74 +269,93 @@ class Light(SystemWideDevice, DevicePositionMixin):
286
269
  self.raise_config_error("Failed to configure light {} in platform. See error above".format(self.name), 7,
287
270
  source_exception=e)
288
271
 
289
- async def _initialize(self):
290
- await super()._initialize()
272
+ def _prepare_config_channels(self):
273
+ if self.config['number'] or self.config['platform'] or self.config['platform_settings']:
274
+ self.raise_config_error("Light {} cannot contain platform/platform_settings/number and channels".
275
+ format(self.name), 5)
276
+ channels = self.config['channels']
277
+
278
+ # ensure that each color's channels are a list, not single value
279
+ for channel in channels:
280
+ if not isinstance(channels[channel], list):
281
+ channels[channel] = [channels[channel]]
282
+
283
+ return channels
284
+
285
+ def _derive_platform_channels(self):
286
+ # get channels from number + platform
287
+ platform = self.machine.get_platform_sections('lights', self.config['platform'])
288
+ platform.assert_has_feature("lights")
289
+
291
290
  try:
292
- if self.config['previous']:
293
- if self.config['previous'].name == self.name:
294
- self.raise_config_error(
295
- "Failed to configure light {} in platform. 'previous' value cannot refer to itself.".
296
- format(self.name), 8)
297
-
298
- # If we are in development mode, do a robust tree traversal to catch infinite light loops
299
- if not self.machine.options['production']:
300
- tree = [self.name]
301
- prev = self.config['previous']
302
- while prev:
303
- tree.append(prev.name)
304
- prev = prev.config.get('previous')
305
- if prev is not None and prev.name in tree:
306
- tree.append(prev.name)
307
- self.raise_config_error("Cyclical light chain found: {}".format(" -> ".join(tree)), 9)
291
+ channel_list = platform.parse_light_number_to_channels(self.config['number'], self.config['subtype'])
292
+ except AssertionError as e:
293
+ self.raise_config_error("Failed to parse light number {} in platform. See error above".
294
+ format(self.name), 4, source_exception=e)
308
295
 
309
- await self.config['previous'].wait_for_loaded()
310
- start_channel = self.config['previous'].get_successor_number()
311
- self._load_hw_driver_sequentially(start_channel)
312
- elif self.config['start_channel']:
313
- self._load_hw_driver_sequentially(self.config['start_channel'])
314
- else:
315
- self._load_hw_drivers()
316
- self._drivers_loaded.set_result(True)
296
+ # copy platform and platform_settings to all channels
297
+ for channel, _ in enumerate(channel_list):
298
+ channel_list[channel]['subtype'] = self.config['subtype']
299
+ channel_list[channel]['platform'] = self.config['platform']
300
+ channel_list[channel]['platform_settings'] = self.config['platform_settings']
317
301
 
318
- self.config['default_on_color'] = RGBColor(self.config['default_on_color'])
302
+ # map channels to colors
303
+ if self.config['type']:
304
+ color_channels = self.config['type']
305
+ else:
306
+ if len(channel_list) == 1:
307
+ # for one channel default to a white channel
308
+ color_channels = "w"
309
+ elif len(channel_list) == 3:
310
+ # TODO this seems like a bug waiting to happen -- what if the channel list ISNT R G B ordered?
311
+ # -- the blind .pop(0) down below combined with the "for letter in 'rgb'" seems risky
312
+ # and the channel list order is up to the platform to provide, which doesnt seem clear in the
313
+ # parse_light_number_to_channels interface definition at all
319
314
 
320
- if self.config['color_correction_profile'] is not None:
321
- profile_name = self.config['color_correction_profile']
322
- elif 'light_settings' in self.machine.config and \
323
- self.machine.config['light_settings']['default_color_correction_profile'] is not None:
324
- profile_name = self.machine.config['light_settings']['default_color_correction_profile']
325
- else:
326
- profile_name = None
327
-
328
- if profile_name:
329
- if profile_name in self.machine.light_controller.light_color_correction_profiles:
330
- profile = self.machine.light_controller.light_color_correction_profiles[profile_name]
331
-
332
- if profile is not None:
333
- self._set_color_correction_profile(profile)
334
- else: # pragma: no cover
335
- error = "Color correction profile '{}' was specified for light '{}'"\
336
- " but the color correction profile does not exist."\
337
- .format(profile_name, self.name)
338
- self.error_log(error)
339
- raise ValueError(error)
340
-
341
- if self.config['fade_ms'] is not None:
342
- self.default_fade_ms = self.config['fade_ms']
315
+ # for three channels default to RGB
316
+ color_channels = "rgb"
343
317
  else:
344
- self.default_fade_ms = (self.machine.config['light_settings']
345
- ['default_fade_ms'])
318
+ self.raise_config_error("Please provide a type for light {}. No default for channels {}.".
319
+ format(self.name, channel_list), 11)
346
320
 
347
- if len(self.hw_drivers) == 4 and all(channel in self.hw_drivers
348
- for channel in ['red', 'green', 'blue', 'white']):
349
- self._rbgw_style = self.machine.config['mpf']['rgbw_white_behavior']
321
+ if len(channel_list) != len(color_channels):
322
+ self.raise_config_error("Type {} does not match channels {} for light {}".format(
323
+ color_channels, channel_list, self.name), 12)
350
324
 
351
- self.debug_log("Initializing Light. CC Profile: %s, "
352
- "Default fade: %sms", self._color_correction_profile,
353
- self.default_fade_ms)
354
- except Exception:
355
- self._drivers_loaded.cancel()
356
- raise
325
+ channels = {} # type: Dict[str, List[Any]]
326
+ for color_letter in color_channels:
327
+ full_color_name = self._color_letter_to_name(color_letter)
328
+ if full_color_name not in channels:
329
+ channels[full_color_name] = []
330
+
331
+ channels[full_color_name].append(channel_list.pop(0))
332
+
333
+ return channels
334
+
335
+ def _apply_color_correction_profile(self, profile_value):
336
+ profile_name = self._determine_color_correction_profile(profile_value)
337
+ if profile_name:
338
+ if profile_name in self.machine.light_controller.light_color_correction_profiles:
339
+ profile = self.machine.light_controller.light_color_correction_profiles[profile_name]
340
+
341
+ if profile is not None:
342
+ self._set_color_correction_profile(profile)
343
+ else: # pragma: no cover
344
+ error = "Color correction profile '{}' was specified for light '{}'"\
345
+ " but the color correction profile does not exist."\
346
+ .format(profile_name, self.name)
347
+ self.error_log(error)
348
+ raise ValueError(error)
349
+
350
+ def _determine_color_correction_profile(self, profile_value):
351
+ if profile_value is not None:
352
+ return profile_value
353
+
354
+ if 'light_settings' in self.machine.config and \
355
+ self.machine.config['light_settings']['default_color_correction_profile'] is not None:
356
+ return self.machine.config['light_settings']['default_color_correction_profile']
357
+
358
+ return None
357
359
 
358
360
  def _set_color_correction_profile(self, profile):
359
361
  """Apply a color correction profile to this light.
@@ -365,6 +367,19 @@ class Light(SystemWideDevice, DevicePositionMixin):
365
367
  """
366
368
  self._color_correction_profile = profile
367
369
 
370
+ def _apply_fade(self, fade_value):
371
+ if fade_value is not None:
372
+ self.default_fade_ms = fade_value
373
+ else:
374
+ self.default_fade_ms = self.machine.config['light_settings']['default_fade_ms']
375
+
376
+ def _apply_rgbw_style(self):
377
+ rgbw_all_present = all(channel in self.hw_drivers
378
+ for channel in ['red', 'green', 'blue', 'white'])
379
+
380
+ if len(self.hw_drivers) == 4 and rgbw_all_present:
381
+ self._rbgw_style = self.machine.config['mpf']['rgbw_white_behavior']
382
+
368
383
  # pylint: disable-msg=too-many-arguments
369
384
  def color(self, color, fade_ms=None, priority=0, key=None, start_time=None):
370
385
  """Add or update a color entry in this light's stack.
@@ -392,8 +407,7 @@ class Light(SystemWideDevice, DevicePositionMixin):
392
407
  """
393
408
  if self._debug:
394
409
  self.debug_log("Received color() command. color: %s, fade_ms: %s "
395
- "priority: %s, key: %s", color, fade_ms, priority,
396
- key)
410
+ "priority: %s, key: %s", color, fade_ms, priority, key)
397
411
 
398
412
  if isinstance(color, str) and color == "on":
399
413
  color = self.config['default_on_color']
@@ -440,8 +454,7 @@ class Light(SystemWideDevice, DevicePositionMixin):
440
454
  fade_ms: duration of fade
441
455
  """
442
456
  del kwargs
443
- self.color(color=self._off_color, fade_ms=fade_ms, priority=priority,
444
- key=key)
457
+ self.color(color=self._off_color, fade_ms=fade_ms, priority=priority, key=key)
445
458
 
446
459
  # pylint: disable-msg=too-many-arguments
447
460
  def _add_to_stack(self, color, fade_ms, priority, key, start_time):
@@ -473,12 +486,7 @@ class Light(SystemWideDevice, DevicePositionMixin):
473
486
  if self.stack:
474
487
  self._remove_from_stack_by_key(key)
475
488
 
476
- self.stack.append(LightStackEntry(priority,
477
- key,
478
- start_time,
479
- color_below,
480
- dest_time,
481
- color))
489
+ self.stack.append(LightStackEntry(priority, key, start_time, color_below, dest_time, color))
482
490
 
483
491
  if len(self.stack) > 1:
484
492
  self.stack.sort(reverse=True)