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.
- mpf/_version.py +2 -2
- mpf/config_spec.yaml +7 -7
- mpf/core/device_manager.py +14 -1
- mpf/core/events.py +9 -6
- mpf/core/placeholder_manager.py +21 -1
- mpf/core/utility_functions.py +11 -6
- mpf/devices/light.py +165 -157
- mpf/devices/light_group.py +45 -13
- mpf/devices/magnet.py +1 -1
- mpf/modes/match/code/match.py +15 -5
- mpf/parsers/event_reference_parser.py +10 -6
- mpf/platforms/fast/communicators/exp.py +5 -0
- mpf/platforms/fast/fast.py +168 -117
- mpf/platforms/fast/fast_defines.py +10 -10
- mpf/platforms/fast/fast_exp_board.py +96 -5
- mpf/platforms/fast/fast_led.py +22 -8
- mpf/platforms/pinproc/windows/pinproc.cp313-win32.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp313-win_amd64.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp314-win32.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp314-win_amd64.pyd +0 -0
- mpf/platforms/virtual_pinball/virtual_pinball.py +22 -1
- mpf/tests/machine_files/auditor/config/config.yaml +1 -1
- mpf/tests/machine_files/custom_code/code/test_code.py +4 -0
- mpf/tests/machine_files/event_manager/config/test_event_manager.yaml +6 -0
- mpf/tests/machine_files/event_manager/modes/game_mode/config/game_mode.yaml +5 -0
- mpf/tests/machine_files/fast/config/exp.yaml +27 -7
- mpf/tests/machine_files/light/config/light_groups.yaml +12 -0
- mpf/tests/machine_files/settings/config/config.yaml +27 -0
- mpf/tests/test_CustomCode.py +7 -0
- mpf/tests/test_DeviceManager.py +63 -0
- mpf/tests/test_EventManager.py +45 -0
- mpf/tests/test_Fast_Exp.py +142 -90
- mpf/tests/test_Fast_Led.py +61 -0
- mpf/tests/test_LightGroups.py +48 -16
- mpf/tests/test_LightNumbering.py +2 -3
- mpf/tests/test_MatchMode.py +16 -4
- mpf/tests/test_RandomEventPlayer.py +4 -4
- mpf/tests/test_Settings.py +16 -0
- mpf/tests/test_Utility_Functions.py +54 -7
- mpf/tests/test_VPX.py +71 -0
- {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/METADATA +2 -2
- {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/RECORD +47 -46
- {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/WHEEL +1 -1
- mpf/platforms/pinproc/windows/pinproc.cp38-win32.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp38-win_amd64.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp39-win32.pyd +0 -0
- mpf/platforms/pinproc/windows/pinproc.cp39-win_amd64.pyd +0 -0
- {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/entry_points.txt +0 -0
- {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/licenses/AUTHORS.md +0 -0
- {mpf-0.57.5.dev3.dist-info → mpf-0.58.0.dev3.dist-info}/licenses/LICENSE.md +0 -0
- {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.
|
|
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.
|
|
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|
|
|
674
|
-
type: single|enum(ws2812,apa-102)|ws2812
|
|
675
|
-
|
|
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
|
mpf/core/device_manager.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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):
|
mpf/core/placeholder_manager.py
CHANGED
|
@@ -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."""
|
mpf/core/utility_functions.py
CHANGED
|
@@ -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
|
|
592
|
-
return
|
|
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
|
|
142
|
-
if
|
|
143
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
color_channels, channel_list, self.name), 12)
|
|
145
|
+
if letter == 'g':
|
|
146
|
+
return 'green'
|
|
158
147
|
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
channels[full_color_name].append(channel_list.pop(0))
|
|
151
|
+
if letter == 'w':
|
|
152
|
+
return 'white'
|
|
180
153
|
|
|
181
|
-
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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.
|
|
345
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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)
|