class RpiDriver(DriverPlatformInterface): """An output on a Rasoberry Pi.""" def __init__(self, number, config, platform): """Initialise output.""" super().__init__(config, number) self.platform = platform # type: RaspberryPiHardwarePlatform self.gpio = int(self.number) self.delay = DelayManager(self.platform.machine.delayRegistry) def get_board_name(self): """Return name.""" return "Raspberry Pi" def pulse(self, pulse_settings: PulseSettings): """Pulse output.""" self.enable(pulse_settings, None) def enable(self, pulse_settings: PulseSettings, hold_settings: Optional[HoldSettings]): """Enable output.""" self.platform.send_command(self.platform.pi.write(self.gpio, 1)) if hold_settings and hold_settings.power == 1: # do nothing. just keep driver enabled pass elif hold_settings and hold_settings.power > 0: # schedule pwm self.delay.add(pulse_settings.duration, self._pwm, hold_power=hold_settings.power) else: # no hold. disable after pulse self.delay.add(pulse_settings.duration, self.disable) def _pwm(self, hold_power): """Set up pwm.""" self.platform.send_command( self.platform.pi.set_PWM_dutycycle(self.gpio, hold_power * 255)) def disable(self): """Disable output.""" self.platform.send_command(self.platform.pi.write(self.gpio, 0)) # clear all delays self.delay.clear()
class Mode(LogMixin): """Base class for a mode.""" __slots__ = [ "machine", "config", "name", "path", "priority", "_active", "_starting", "_mode_start_wait_queue", "stop_methods", "start_callback", "stop_callbacks", "event_handlers", "switch_handlers", "mode_stop_kwargs", "mode_devices", "start_event_kwargs", "stopping", "delay", "player", "auto_stop_on_ball_end", "restart_on_next_ball", "asset_paths" ] # pylint: disable-msg=too-many-arguments def __init__(self, machine: "MachineController", config, name: str, path, asset_paths) -> None: """Initialise mode. Args: ---- machine: the machine controller config: config dict for mode name: name of mode path: path of mode asset_paths: all paths to consider for assets in this mode """ super().__init__() self.machine = machine # type: MachineController self.config = config # type: ignore self.name = name self.path = path self.asset_paths = asset_paths self.priority = 0 self._active = False self._starting = False self._mode_start_wait_queue = None # type: Optional[QueuedEvent] self.stop_methods = list( ) # type: List[Tuple[Callable[[Any], None], Any]] self.start_callback = None # type: Optional[Callable[[], None]] self.stop_callbacks = [] # type: List[Callable[[], None]] self.event_handlers = set() # type: Set[EventHandlerKey] self.switch_handlers = list() # type: List[SwitchHandler] self.mode_stop_kwargs = dict() # type: Dict[str, Any] self.mode_devices = set() # type: Set[ModeDevice] self.start_event_kwargs = {} # type: Dict[str, Any] self.stopping = False self.delay = DelayManager(self.machine) '''DelayManager instance for delays in this mode. Note that all delays scheduled here will be automatically canceled when the mode stops.''' self.player = None # type: Optional[Player] '''Reference to the current player object.''' self.configure_logging('Mode.' + name, self.config['mode']['console_log'], self.config['mode']['file_log']) self.configure_mode_settings(config.get('mode', dict())) self.auto_stop_on_ball_end = self.config['mode']['stop_on_ball_end'] '''Controls whether this mode is stopped when the ball ends, regardless of its stop_events settings. ''' self.restart_on_next_ball = self.config['mode']['restart_on_next_ball'] '''Controls whether this mode will restart on the next ball. This only works if the mode was running when the ball ended. It's tracked per- player in the 'restart_modes_on_next_ball' player variable. ''' if self.config['mode'][ 'game_mode'] and not self.config['mode']['stop_on_ball_end']: self.raise_config_error( "All game modes need to stop at ball end. If you want to set stop_on_ball_end to " "False also set game_mode to False.", 1) @staticmethod def get_config_spec() -> str: """Return config spec for mode_settings.""" return ''' __valid_in__: mode __allow_others__: ''' def __repr__(self): """Return string representation.""" return '<Mode.{}>'.format(self.name) @property def active(self) -> bool: """Return *True* if this mode is active.""" return self._active @active.setter def active(self, new_active: bool): """Setter for _active.""" if self._active != new_active: self._active = new_active self.machine.mode_controller.set_mode_state(self, self._active) def configure_mode_settings(self, config: dict) -> None: """Process this mode's configuration settings from a config dictionary.""" self.config['mode'] = self.machine.config_validator.validate_config( config_spec='mode', source=config, section_name='mode') for event in self.config['mode']['start_events']: self.machine.events.add_handler( event=event, handler=self.start, priority=self.config['mode']['priority'] + self.config['mode']['start_priority']) @property def is_game_mode(self) -> bool: """Return true if this is a game mode.""" return bool(self.config['mode']['game_mode']) def start(self, mode_priority=None, callback=None, **kwargs) -> None: """Start this mode. Args: ---- mode_priority: Integer value of what you want this mode to run at. If you don't specify one, it will use the "Mode: priority" setting from this mode's configuration file. callback: Callback to call when this mode has been started. **kwargs: Catch-all since this mode might start from events with who-knows-what keyword arguments. Warning: You can safely call this method, but do not override it in your mode code. If you want to write your own mode code by subclassing Mode, put whatever code you want to run when this mode starts in the mode_start method which will be called automatically. """ # remove argument so we do not repost this kwargs.pop('_from_bcp', None) self.debug_log("Received request to start") if self.config['mode']['game_mode'] and not (self.machine.game and self.player): self.warning_log( "Can only start mode %s during a game. Aborting start.", self.name) return if self._active: self.debug_log("Mode is already active. Aborting start.") return if self._starting: self.debug_log("Mode already starting. Aborting start.") return self._starting = True self.machine.events.post('mode_{}_will_start'.format(self.name), **kwargs) '''event: mode_(name)_will_start desc: Posted when a mode is about to start. The "name" part is replaced with the actual name of the mode, so the actual event posted is something like *mode_attract_will_start*, *mode_base_will_start*, etc. This is posted before the "mode_(name)_starting" event. ''' if self.config['mode']['use_wait_queue'] and 'queue' in kwargs: self.debug_log("Registering a mode start wait queue") self._mode_start_wait_queue = kwargs['queue'] assert isinstance(self._mode_start_wait_queue, QueuedEvent) self._mode_start_wait_queue.wait() if isinstance(mode_priority, int): self.priority = mode_priority else: self.priority = self.config['mode']['priority'] self.start_event_kwargs = kwargs # hook for custom code. called before any mode devices are set up self.mode_will_start(**self.start_event_kwargs) self._add_mode_devices() self.debug_log("Registering mode_stop handlers") # register mode stop events if 'stop_events' in self.config['mode']: for event in self.config['mode']['stop_events']: # stop priority is +1 so if two modes of the same priority # start and stop on the same event, the one will stop before # the other starts self.add_mode_event_handler( event=event, handler=self.stop, priority=self.config['mode']['stop_priority'] + 1) self.start_callback = callback self.debug_log("Calling mode_start handlers") for item in self.machine.mode_controller.start_methods: if item.config_section in self.config or not item.config_section: result = item.method(config=self.config.get( item.config_section, self.config), priority=self.priority, mode=self, **item.kwargs) if result: self.stop_methods.append(result) self._setup_device_control_events() self.machine.events.post_queue(event='mode_{}_starting'.format( self.name), callback=self._started, **kwargs) '''event: mode_(name)_starting desc: The mode called "name" is starting. This is a queue event. The mode will not fully start until the queue is cleared. ''' def _started(self, **kwargs) -> None: """Handle result of mode_<name>_starting queue event.""" del kwargs if self.machine.is_shutting_down: self.info_log("Will not start because machine is shutting down.") return self.info_log('Started. Priority: %s', self.priority) self.active = True self._starting = False for event_name in self.config['mode']['events_when_started']: self.machine.events.post(event_name) self.machine.events.post(event='mode_{}_started'.format(self.name), callback=self._mode_started_callback, **self.start_event_kwargs) '''event: mode_(name)_started desc: Posted when a mode has started. The "name" part is replaced with the actual name of the mode, so the actual event posted is something like *mode_attract_started*, *mode_base_started*, etc. This is posted after the "mode_(name)_starting" event. ''' def _mode_started_callback(self, **kwargs) -> None: """Handle result of mode_<name>_started queue event.""" del kwargs self.mode_start(**self.start_event_kwargs) self.start_event_kwargs = dict() if self.start_callback: self.start_callback() self.debug_log('Mode Start process complete.') def stop(self, callback: Any = None, **kwargs) -> bool: """Stop this mode. Args: ---- callback: Method which will be called once this mode has stopped. Will only be called when the mode is running (includes currently stopping) **kwargs: Catch-all since this mode might start from events with who-knows-what keyword arguments. Warning: You can safely call this method, but do not override it in your mode code. If you want to write your own mode code by subclassing Mode, put whatever code you want to run when this mode stops in the mode_stop method which will be called automatically. Returns true if the mode is running. Otherwise false. """ if not self._active: return False if callback: self.stop_callbacks.append(callback) # do not stop twice. only register callback in that case if self.stopping: # mode is still running return True self.machine.events.post('mode_' + self.name + '_will_stop') '''event: mode_(name)_will_stop desc: Posted when a mode is about to stop. The "name" part is replaced with the actual name of the mode, so the actual event posted is something like *mode_attract_will_stop*, *mode_base_will_stop*, etc. This is posted immediately before the "mode_(name)_stopping" event. ''' self.stopping = True self.mode_stop_kwargs = kwargs self.debug_log('Mode Stopping.') self._remove_mode_switch_handlers() self.delay.clear() self.machine.events.post_queue(event='mode_' + self.name + '_stopping', callback=self._stopped) '''event: mode_(name)_stopping desc: The mode called "name" is stopping. This is a queue event. The mode won't actually stop until the queue is cleared. ''' return True def _stopped(self) -> None: self.info_log('Stopped.') self.priority = 0 self.active = False self.stopping = False for item in self.stop_methods: item[0](item[1]) self.stop_methods = list() for event_name in self.config['mode']['events_when_stopped']: self.machine.events.post(event_name) self.machine.events.post('mode_' + self.name + '_stopped', callback=self._mode_stopped_callback) '''event: mode_(name)_stopped desc: Posted when a mode has stopped. The "name" part is replaced with the actual name of the mode, so the actual event posted is something like *mode_attract_stopped*, *mode_base_stopped*, etc. ''' self.machine.events.post('clear', key=self.name) '''event: clear args: key: string name of the configs to clear desc: Posted to cause config players to clear whatever they're running based on the key passed. Typically posted when a show or mode ends. ''' if self._mode_start_wait_queue: self.debug_log("Clearing wait queue") self._mode_start_wait_queue.clear() self._mode_start_wait_queue = None def _mode_stopped_callback(self, **kwargs) -> None: del kwargs # Call the mode_stop() method before removing the devices self.mode_stop(**self.mode_stop_kwargs) self.mode_stop_kwargs = dict() # Clean up the mode handlers and devices self._remove_mode_event_handlers() self._remove_mode_devices() for callback in self.stop_callbacks: callback() self.stop_callbacks = [] def _add_mode_devices(self) -> None: # adds and initializes mode devices which get removed at the end of the mode for collection_name, device_class in (iter( self.machine.device_manager.device_classes.items())): # check if there is config for the device type if device_class.config_section in self.config: for device_name in self.config[device_class.config_section]: collection = getattr(self.machine, collection_name) # get device device = collection[device_name] # Track that this device was added via this mode so we # can remove it when the mode ends. self.mode_devices.add(device) if not self.config['mode'][ 'game_mode'] and not device.can_exist_outside_of_game: raise AssertionError( "Device {} cannot exist in non game-mode {}.". format(device, self.name)) # This lets the device know it was added to a mode device.device_loaded_in_mode(mode=self, player=self.player) def create_mode_devices(self) -> None: """Create new devices that are specified in a mode config that haven't been created in the machine-wide.""" self.debug_log("Scanning config for mode-based devices") for collection_name, device_class in iter( self.machine.device_manager.device_classes.items()): # check if there is config for the device type if device_class.config_section not in self.config: continue for device, settings in iter( self.config[device_class.config_section].items()): collection = getattr(self.machine, collection_name) if device not in collection: # no existing device, create self.debug_log("Creating mode-based device: %s", device) self.machine.device_manager.create_devices( collection.name, {device: settings}) async def load_mode_devices(self) -> None: """Load config of mode devices.""" for collection_name, device_class in iter( self.machine.device_manager.device_classes.items()): # check if there is config for the device type if device_class.config_section not in self.config: continue for device, settings in iter( self.config[device_class.config_section].items()): collection = getattr(self.machine, collection_name) device = collection[device] settings = device.prepare_config(settings, True) settings = device.validate_and_parse_config( settings, True, "mode:" + self.name) if device.config: self.debug_log("Overwrite mode-based device: %s", device) # overload device.overload_config_in_mode(self, settings) else: self.debug_log("Initializing mode-based device: %s", device) # load config device.load_config(settings) for collection_name, device_class in iter( self.machine.device_manager.device_classes.items()): # check if there is config for the device type if device_class.config_section not in self.config: continue for device, settings in iter( self.config[device_class.config_section].items()): collection = getattr(self.machine, collection_name) device = collection[device] await device.device_added_to_mode(mode=self) def _remove_mode_devices(self) -> None: for device in self.mode_devices: device.device_removed_from_mode(self) self.mode_devices = set() def _setup_device_control_events(self) -> None: # registers mode handlers for control events for all devices specified # in this mode's config (not just newly-created devices) self.debug_log("Scanning mode-based config for device control_events") for event, method, delay, device in ( self.machine.device_manager.get_device_control_events( self.config)): if not delay: self.add_mode_event_handler( event=event, handler=method, blocking_facility=device.class_label) else: self.add_mode_event_handler( event=event, handler=self._control_event_handler, callback=method, ms_delay=delay, blocking_facility=device.class_label) # get all devices in the mode device_list = set() # type: Set[ModeDevice] for collection in self.machine.device_manager.collections: if self.machine.device_manager.collections[ collection].config_section in self.config: for device, _ in \ iter(self.config[self.machine.device_manager.collections[collection].config_section].items()): device_list.add( self.machine.device_manager.collections[collection] [device]) for device in device_list: device.add_control_events_in_mode(self) def _control_event_handler(self, callback: Callable[..., None], ms_delay: int = 0, **kwargs) -> None: del kwargs self.debug_log("_control_event_handler: callback: %s,", callback) self.delay.add(ms=ms_delay, callback=callback, mode=self) def add_mode_event_handler(self, event: str, handler: Callable, priority: int = 0, **kwargs) -> EventHandlerKey: """Register an event handler which is automatically removed when this mode stops. This method is similar to the Event Manager's add_handler() method, except this method automatically unregisters the handlers when the mode ends. Args: ---- event: String name of the event you're adding a handler for. Since events are text strings, they don't have to be pre-defined. handler: The method that will be called when the event is fired. priority: An arbitrary integer value that defines what order the handlers will be called in. The default is 1, so if you have a handler that you want to be called first, add it here with a priority of 2. (Or 3 or 10 or 100000.) The numbers don't matter. They're called from highest to lowest. (i.e. priority 100 is called before priority 1.) **kwargs: Any any additional keyword/argument pairs entered here will be attached to the handler and called whenever that handler is called. Note these are in addition to kwargs that could be passed as part of the event post. If there's a conflict, the event-level ones will win. Returns a EventHandlerKey to the handler which you can use to later remove the handler via ``remove_handler_by_key``. Though you don't need to remove the handler since the whole point of this method is they're automatically removed when the mode stops. Note that if you do add a handler via this method and then remove it manually, that's ok too. """ key = self.machine.events.add_handler(event, handler, self.priority + priority, mode=self, **kwargs) self.event_handlers.add(key) return key def _remove_mode_event_handlers(self) -> None: for key in self.event_handlers: self.machine.events.remove_handler_by_key(key) self.event_handlers = set() def _remove_mode_switch_handlers(self) -> None: for handler in self.switch_handlers: self.machine.switch_controller.remove_switch_handler_by_key( handler) self.switch_handlers = list() def initialise_mode(self) -> None: """Initialise this mode.""" self.mode_init() def mode_init(self) -> None: """User-overrideable method which will be called when this mode initializes as part of the MPF boot process.""" def mode_will_start(self, **kwargs) -> None: """User-overrideable method which will be called whenever this mode starts (i.e. before it becomes active).""" def mode_start(self, **kwargs) -> None: """User-overrideable method which will be called whenever this mode starts (i.e. whenever it becomes active).""" def mode_stop(self, **kwargs) -> None: """User-overrideable method which will be called whenever this mode stops."""
class Mode(object): """Parent class for in-game mode code.""" def __init__(self, machine, config: dict, name: str, path): """Initialise mode. Args: machine(mpf.core.machine.MachineController): the machine controller config: config dict for mode name: name of mode path: path of mode Returns: """ self.machine = machine self.config = config self.name = name.lower() self.path = path self.log = logging.getLogger('Mode.' + name) self.delay = DelayManager(self.machine.delayRegistry) self.priority = 0 self._active = False self._mode_start_wait_queue = None self.stop_methods = list() self.timers = dict() self.start_callback = None self.stop_callback = None self.event_handlers = set() self.switch_handlers = list() self.mode_stop_kwargs = dict() self.mode_devices = set() self.start_event_kwargs = None self.stopping = False self.player = None '''Reference to the current player object.''' self._create_mode_devices() self._validate_mode_config() self._initialise_mode_devices() self.configure_mode_settings(config.get('mode', dict())) self.auto_stop_on_ball_end = self.config['mode']['stop_on_ball_end'] '''Controls whether this mode is stopped when the ball ends, regardless of its stop_events settings. ''' self.restart_on_next_ball = self.config['mode']['restart_on_next_ball'] '''Controls whether this mode will restart on the next ball. This only works if the mode was running when the ball ended. It's tracked per- player in the 'restart_modes_on_next_ball' player variable. ''' # Call registered remote loader methods for item in self.machine.mode_controller.loader_methods: if (item.config_section and item.config_section in self.config and self.config[item.config_section]): item.method(config=self.config[item.config_section], mode_path=self.path, mode=self, root_config_dict=self.config, **item.kwargs) elif not item.config_section: item.method(config=self.config, mode_path=self.path, **item.kwargs) self.mode_init() @staticmethod def get_config_spec(): """Return config spec for mode_settings.""" return ''' __valid_in__: mode __allow_others__: ''' def __repr__(self): """Return string representation.""" return '<Mode.{}>'.format(self.name) @property def active(self): """Return true if mode is active.""" return self._active @active.setter def active(self, active): """Setter for _active.""" if self._active != active: self._active = active self.machine.mode_controller.set_mode_state(self, self._active) def configure_mode_settings(self, config): """Process this mode's configuration settings from a config dictionary.""" self.config['mode'] = self.machine.config_validator.validate_config( config_spec='mode', source=config, section_name='mode') for event in self.config['mode']['start_events']: self.machine.events.add_handler( event=event, handler=self.start, priority=self.config['mode']['priority'] + self.config['mode']['start_priority']) def _validate_mode_config(self): """Validate mode config.""" for section in self.machine.config['mpf']['mode_config_sections']: this_section = self.config.get(section, None) # do not double validate devices if section in self.machine.device_manager.device_classes: continue if this_section: if isinstance(this_section, dict): for device, settings in this_section.items(): self.config[section][device] = ( self.machine.config_validator.validate_config( section, settings, "mode:" + self.name)) else: self.config[section] = ( self.machine.config_validator.validate_config( section, this_section)) def _get_merged_settings(self, section_name): """Return a dict of a config section from the machine-wide config with the mode-specific config merged in.""" if section_name in self.machine.config: return_dict = copy.deepcopy(self.machine.config[section_name]) else: return_dict = CaseInsensitiveDict() if section_name in self.config: return_dict = Util.dict_merge(return_dict, self.config[section_name], combine_lists=False) return return_dict def start(self, mode_priority=None, callback=None, **kwargs): """Start this mode. Args: mode_priority: Integer value of what you want this mode to run at. If you don't specify one, it will use the "Mode: priority" setting from this mode's configuration file. **kwargs: Catch-all since this mode might start from events with who-knows-what keyword arguments. Warning: You can safely call this method, but do not override it in your mode code. If you want to write your own mode code by subclassing Mode, put whatever code you want to run when this mode starts in the mode_start method which will be called automatically. """ self.log.debug("Received request to start") if self.config['mode']['game_mode'] and not self.machine.game: raise AssertionError( "Can only start mode {} during a game.".format(self.name)) if self._active: self.log.debug("Mode is already active. Aborting start") return if self.config['mode']['use_wait_queue'] and 'queue' in kwargs: self.log.debug("Registering a mode start wait queue") self._mode_start_wait_queue = kwargs['queue'] self._mode_start_wait_queue.wait() if isinstance(mode_priority, int): self.priority = mode_priority else: self.priority = self.config['mode']['priority'] self.start_event_kwargs = kwargs self.log.info('Mode Starting. Priority: %s', self.priority) self._add_mode_devices() self.log.debug("Registering mode_stop handlers") # register mode stop events if 'stop_events' in self.config['mode']: for event in self.config['mode']['stop_events']: # stop priority is +1 so if two modes of the same priority # start and stop on the same event, the one will stop before # the other starts self.add_mode_event_handler( event=event, handler=self.stop, priority=self.priority + 1 + self.config['mode']['stop_priority']) self.start_callback = callback self.log.debug("Calling mode_start handlers") for item in self.machine.mode_controller.start_methods: if item.config_section in self.config or not item.config_section: self.stop_methods.append( item.method(config=self.config.get(item.config_section, self.config), priority=self.priority, mode=self, **item.kwargs)) self._setup_device_control_events() self.machine.events.post_queue(event='mode_' + self.name + '_starting', callback=self._started) '''event: mode_(name)_starting desc: The mode called "name" is starting. This is a queue event. The mode will not fully start until the queue is cleared. ''' def _started(self): """Called after the mode_<name>_starting queue event has finished.""" self.log.debug('Mode Started. Priority: %s', self.priority) self.active = True if 'timers' in self.config: self._setup_timers() self._start_timers() self.machine.events.post('mode_' + self.name + '_started', callback=self._mode_started_callback) '''event: mode_(name)_started desc: Posted when a mode has started. The "name" part is replaced with the actual name of the mode, so the actual event posted is something like *mode_attract_started*, *mode_base_started*, etc. This is posted after the "mode_(name)_starting" event. ''' def _mode_started_callback(self, **kwargs): """Called after the mode_<name>_started queue event has finished.""" del kwargs self.mode_start(**self.start_event_kwargs) self.start_event_kwargs = dict() if self.start_callback: self.start_callback() self.log.debug('Mode Start process complete.') def stop(self, callback=None, **kwargs): """Stop this mode. Args: **kwargs: Catch-all since this mode might start from events with who-knows-what keyword arguments. Warning: You can safely call this method, but do not override it in your mode code. If you want to write your own mode code by subclassing Mode, put whatever code you want to run when this mode stops in the mode_stop method which will be called automatically. """ if not self._active: return self.stopping = True self.mode_stop_kwargs = kwargs self.log.debug('Mode Stopping.') self._remove_mode_switch_handlers() self.stop_callback = callback self._kill_timers() self.delay.clear() # self.machine.events.remove_handler(self.stop) # todo is this ok here? Or should we only remove ones that we know this # mode added? self.machine.events.post_queue(event='mode_' + self.name + '_stopping', callback=self._stopped) '''event: mode_(name)_stopping desc: The mode called "name" is stopping. This is a queue event. The mode won't actually stop until the queue is cleared. ''' def _stopped(self): self.log.debug('Mode Stopped.') self.priority = 0 self.active = False self.stopping = False for callback in self.machine.mode_controller.stop_methods: callback[0](self) for item in self.stop_methods: item[0](item[1]) self.stop_methods = list() self.machine.events.post('mode_' + self.name + '_stopped', callback=self._mode_stopped_callback) '''event: mode_(name)_stopped desc: Posted when a mode has stopped. The "name" part is replaced with the actual name of the mode, so the actual event posted is something like *mode_attract_stopped*, *mode_base_stopped*, etc. ''' self.machine.events.post('clear', key=self.name) '''event: clear args: key: string name of the configs to clear desc: Posted to cause config players to clear whatever they're running based on the key passed. Typically posted when a show or mode ends. ''' if self._mode_start_wait_queue: self.log.debug("Clearing wait queue") self._mode_start_wait_queue.clear() self._mode_start_wait_queue = None def _mode_stopped_callback(self, **kwargs): del kwargs self._remove_mode_event_handlers() self._remove_mode_devices() self.mode_stop(**self.mode_stop_kwargs) self.mode_stop_kwargs = dict() if self.stop_callback: self.stop_callback() def _add_mode_devices(self): # adds and initializes mode devices which get removed at the end of the mode for collection_name, device_class in (iter( self.machine.device_manager.device_classes.items())): # check if there is config for the device type if device_class.config_section in self.config: for device_name in self.config[device_class.config_section]: collection = getattr(self.machine, collection_name) # get device device = collection[device_name] # Track that this device was added via this mode so we # can remove it when the mode ends. self.mode_devices.add(device) if not self.config['mode'][ 'game_mode'] and not device.can_exist_outside_of_game: raise AssertionError( "Device {} cannot exist in non game-mode {}.". format(device, self.name)) # This lets the device know it was added to a mode device.device_added_to_mode(mode=self, player=self.player) def _create_mode_devices(self): """Create new devices that are specified in a mode config that haven't been created in the machine-wide.""" self.log.debug("Scanning config for mode-based devices") for collection_name, device_class in iter( self.machine.device_manager.device_classes.items()): # check if there is config for the device type if device_class.config_section not in self.config: continue # check if it is supposed to be used in mode if collection_name not in self.machine.config['mpf'][ 'mode_config_sections']: raise AssertionError( "Found config for device {} in mode {} which may not be used in modes" .format(collection_name, self.name)) for device, settings in iter( self.config[device_class.config_section].items()): collection = getattr(self.machine, collection_name) if device not in collection: # no existing device, create self.log.debug("Creating mode-based device: %s", device) self.machine.device_manager.create_devices( collection.name, {device: settings}) def _initialise_mode_devices(self): """Initialise new devices that are specified in a mode config.""" for collection_name, device_class in iter( self.machine.device_manager.device_classes.items()): # check if there is config for the device type if device_class.config_section not in self.config: continue for device, settings in iter( self.config[device_class.config_section].items()): collection = getattr(self.machine, collection_name) device = collection[device] settings = device.prepare_config(settings, True) settings = device.validate_and_parse_config(settings, True) if device.config: self.log.debug("Overwrite mode-based device: %s", device) # overload device.overload_config_in_mode(self, settings) else: self.log.debug("Initialising mode-based device: %s", device) # load config device.load_config(settings) def _remove_mode_devices(self): for device in self.mode_devices: device.device_removed_from_mode(self) self.mode_devices = set() def _setup_device_control_events(self): # registers mode handlers for control events for all devices specified # in this mode's config (not just newly-created devices) self.log.debug("Scanning mode-based config for device control_events") device_list = set() for event, method, delay, device in ( self.machine.device_manager.get_device_control_events( self.config)): try: event, priority = event.split('|') except ValueError: priority = 0 self.add_mode_event_handler(event=event, handler=self._control_event_handler, priority=self.priority + 2 + int(priority), callback=method, ms_delay=delay) device_list.add(device) for device in device_list: device.add_control_events_in_mode(self) def _control_event_handler(self, callback, ms_delay=0, **kwargs): del kwargs self.log.debug("_control_event_handler: callback: %s,", callback) if ms_delay: self.delay.add(name=callback, ms=ms_delay, callback=callback, mode=self) else: callback(mode=self) def add_mode_event_handler(self, event, handler, priority=1, **kwargs): """Register an event handler which is automatically removed when this mode stops. This method is similar to the Event Manager's add_handler() method, except this method automatically unregisters the handlers when the mode ends. Args: event: String name of the event you're adding a handler for. Since events are text strings, they don't have to be pre-defined. handler: The method that will be called when the event is fired. priority: An arbitrary integer value that defines what order the handlers will be called in. The default is 1, so if you have a handler that you want to be called first, add it here with a priority of 2. (Or 3 or 10 or 100000.) The numbers don't matter. They're called from highest to lowest. (i.e. priority 100 is called before priority 1.) **kwargs: Any any additional keyword/argument pairs entered here will be attached to the handler and called whenever that handler is called. Note these are in addition to kwargs that could be passed as part of the event post. If there's a conflict, the event-level ones will win. Returns: A GUID reference to the handler which you can use to later remove the handler via ``remove_handler_by_key``. Though you don't need to remove the handler since the whole point of this method is they're automatically removed when the mode stops. Note that if you do add a handler via this method and then remove it manually, that's ok too. """ key = self.machine.events.add_handler(event, handler, priority, mode=self, **kwargs) self.event_handlers.add(key) return key def _remove_mode_event_handlers(self): for key in self.event_handlers: self.machine.events.remove_handler_by_key(key) self.event_handlers = set() def _remove_mode_switch_handlers(self): for handler in self.switch_handlers: self.machine.switch_controller.remove_switch_handler( switch_name=handler['switch_name'], callback=handler['callback'], state=handler['state'], ms=handler['ms']) self.switch_handlers = list() def _setup_timers(self): # config is localized for timer, settings in self.config['timers'].items(): self.timers[timer] = ModeTimer(machine=self.machine, mode=self, name=timer, config=settings) return self._kill_timers def _start_timers(self): for timer in list(self.timers.values()): if timer.config['start_running']: timer.start() def _kill_timers(self, ): for timer in list(self.timers.values()): timer.kill() self.timers = dict() def mode_init(self): """User-overrideable method which will be called when this mode initializes as part of the MPF boot process.""" pass def mode_start(self, **kwargs): """User-overrideable method which will be called whenever this mode starts (i.e. whenever it becomes active).""" pass def mode_stop(self, **kwargs): """User-overrideable method which will be called whenever this mode stops.""" pass
class ComboSwitch(SystemWideDevice, ModeDevice): """Combo Switch device.""" config_section = 'combo_switches' collection = 'combo_switches' class_label = 'combo_switch' def __init__(self, machine, name): """Initialize Combo Switch.""" super().__init__(machine, name) self.states = ['inactive', 'both', 'one'] self._state = 'inactive' self._switches_1_active = False self._switches_2_active = False self.delay_registry = DelayManagerRegistry(self.machine) self.delay = DelayManager(self.delay_registry) def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str=None) -> dict: """Validate and parse config.""" config = super().validate_and_parse_config(config, is_mode_config, debug_prefix) for state in self.states: if not config['events_when_{}'.format(state)]: config['events_when_{}'.format(state)] = [ "{}_{}".format(self.name, state)] return config def device_added_system_wide(self): """Add event handlers.""" super().device_added_system_wide() self._add_switch_handlers() def device_loaded_in_mode(self, mode: Mode, player: Player): """Add event handlers.""" self._add_switch_handlers() def _add_switch_handlers(self): if self.config['tag_1']: for tag in self.config['tag_1']: for switch in self.machine.switches.items_tagged(tag): self.config['switches_1'].add(switch) if self.config['tag_2']: for tag in self.config['tag_2']: for switch in self.machine.switches.items_tagged(tag): self.config['switches_2'].add(switch) self._register_switch_handlers() @property def state(self): """Return current state.""" return self._state @property def can_exist_outside_of_game(self): """Return true if this device can exist outside of a game.""" return True def device_removed_from_mode(self, mode): """Mode ended. Args: mode: mode which stopped """ del mode self._remove_switch_handlers() self._kill_delays() def _register_switch_handlers(self): for switch in self.config['switches_1']: switch.add_handler(self._switch_1_went_active, state=1) switch.add_handler(self._switch_1_went_inactive, state=0) for switch in self.config['switches_2']: switch.add_handler(self._switch_2_went_active, state=1) switch.add_handler(self._switch_2_went_inactive, state=0) def _remove_switch_handlers(self): for switch in self.config['switches_1']: switch.remove_handler(self._switch_1_went_active, state=1) switch.remove_handler(self._switch_1_went_inactive, state=0) for switch in self.config['switches_2']: switch.remove_handler(self._switch_2_went_active, state=1) switch.remove_handler(self._switch_2_went_inactive, state=0) def _kill_delays(self): self.delay.clear() def _switch_1_went_active(self): self.debug_log('A switch from switches_1 just went active') self.delay.remove('switch_1_inactive') if self._switches_1_active: return if not self.config['hold_time']: self._activate_switches_1() else: self.delay.add_if_doesnt_exist(self.config['hold_time'], self._activate_switches_1, 'switch_1_active') def _switch_2_went_active(self): self.debug_log('A switch from switches_2 just went active') self.delay.remove('switch_2_inactive') if self._switches_2_active: return if not self.config['hold_time']: self._activate_switches_2() else: self.delay.add_if_doesnt_exist(self.config['hold_time'], self._activate_switches_2, 'switch_2_active') def _switch_1_went_inactive(self): self.debug_log('A switch from switches_1 just went inactive') for switch in self.config['switches_1']: if switch.state: # at least one switch is still active return self.delay.remove('switch_1_active') if not self.config['release_time']: self._release_switches_1() else: self.delay.add_if_doesnt_exist(self.config['release_time'], self._release_switches_1, 'switch_1_inactive') def _switch_2_went_inactive(self): self.debug_log('A switch from switches_2 just went inactive') for switch in self.config['switches_2']: if switch.state: # at least one switch is still active return self.delay.remove('switch_2_active') if not self.config['release_time']: self._release_switches_2() else: self.delay.add_if_doesnt_exist(self.config['release_time'], self._release_switches_2, 'switch_2_inactive') def _activate_switches_1(self): self.debug_log('Switches_1 has passed the hold time and is now ' 'active') self._switches_1_active = self.machine.clock.get_time() if self._switches_2_active: if (self.config['max_offset_time'] >= 0 and (self._switches_1_active - self._switches_2_active > self.config['max_offset_time'])): self.debug_log("Switches_2 is active, but the " "max_offset_time=%s which is largest than when " "a Switches_2 switch was first activated, so " "the state will not switch to 'both'", self.config['max_offset_time']) return self._switch_state('both') def _activate_switches_2(self): self.debug_log('Switches_2 has passed the hold time and is now ' 'active') self._switches_2_active = self.machine.clock.get_time() if self._switches_1_active: if (self.config['max_offset_time'] >= 0 and (self._switches_2_active - self._switches_1_active > self.config['max_offset_time'])): self.debug_log("Switches_2 is active, but the " "max_offset_time=%s which is largest than when " "a Switches_2 switch was first activated, so " "the state will not switch to 'both'", self.config['max_offset_time']) return self._switch_state('both') def _release_switches_1(self): self.debug_log('Switches_1 has passed the release time and is now ' 'releases') self._switches_1_active = None if self._switches_2_active and self._state == 'both': self._switch_state('one') elif self._state == 'one': self._switch_state('inactive') def _release_switches_2(self): self.debug_log('Switches_2 has passed the release time and is now ' 'releases') self._switches_2_active = None if self._switches_1_active and self._state == 'both': self._switch_state('one') elif self._state == 'one': self._switch_state('inactive') def _switch_state(self, state): """Post events for current step.""" if state not in self.states: raise ValueError("Received invalid state: {}".format(state)) if state == self.state: return self._state = state self.debug_log("New State: %s", state) for event in self.config['events_when_{}'.format(state)]: self.machine.events.post(event) '''event: (combo_switch)_(state) desc: Combo switch (name) changed to state (state). Note that these events can be overridden in a combo switch's config. Valid states are: *inactive*, *both*, or *one*. ..rubric:: both A switch from group 1 and group 2 are both active at the same time, having been pressed within the ``max_offset_time:`` and being active for at least the ``hold_time:``. ..rubric:: one Either switch 1 or switch 2 has been released for at least the ``release_time:`` but the other switch is still active. ..rubric:: inactive Both switches are inactive. ''' '''event: flipper_cancel
class Blinkenlight(SystemWideDevice): """A light that alternates between several different colors.""" config_section = 'blinkenlights' collection = 'blinkenlights' class_label = 'blinkenlight' def __init__(self, machine, name): """Initialise blinkenlight.""" super().__init__(machine, name) self._colors = [] self.delay = DelayManager(machine) self._color_duration = None self._cycle_duration = None self.num_colors = 0 self._light_key = 'blinkenlight_{}'.format( self.name) # cache the key as string operations as expensive def load_config(self, config: dict): """Load config.""" super().load_config(config) self._color_duration = self.config['color_duration'] self._cycle_duration = self.config['cycle_duration'] if (self._color_duration is None and self._cycle_duration is None) or \ (self._color_duration is not None and self._cycle_duration is not None): self.raise_config_error( "Either color_duration or cycle_duration must be specified, but not both.", 1) @property def light(self): """Return the light this blinkenlight controls.""" return self.config['light'] @property def num_colors_in_cycle(self): """Similar to num_colors, but adds 1 for the "off" color if there is one between cycles.""" return self.num_colors + (1 if self._off_between_cycles() else 0) def add_color(self, color, key, priority): """Add a color to the blinkenlight.""" # check if this key already exists. If it does, replace it with the incoming color/priority self._colors = [x for x in self._colors if x[1] != key] self._colors.append((color, key, priority)) self.info_log('Color {} with key {} added'.format(color, key)) self._update_light() def remove_all_colors(self): """Remove all colors from the blinkenlight.""" self._colors.clear() self.info_log('All colors removed') self._update_light() def remove_color_with_key(self, key): """Remove a color with a given key from the blinkenlight.""" old_len = len(self._colors) self._colors = [x for x in self._colors if x[1] != key] if len(self._colors) != old_len: self.info_log('Color removed with key {}'.format(key)) self._update_light() def _update_light(self): """Update the underlying light.""" self.delay.clear() self.num_colors = len(self._colors) if self._colors: self._sort_colors() self._perform_step() def _sort_colors(self): """Sort the blinkenlights colors by their priority.""" self._colors = sorted(self._colors, key=itemgetter(2), reverse=True) # priority is item 2 of the tuple def _off_between_cycles(self): return (self.config['off_when_multiple'] and len(self._colors) > 1) or (len(self._colors) == 1) def _get_time_between_colors_ms(self): if self._cycle_duration: delay = self._cycle_duration / self.num_colors_in_cycle else: delay = self._color_duration if delay < 1: delay = 1 return delay def _get_total_cycle_ms(self): if self._cycle_duration: return self._cycle_duration return self._color_duration * self.num_colors_in_cycle def _get_current_color(self): now = self.machine.clock.get_time() offset_ms = (now * 1000) % self._get_total_cycle_ms() color_i = int(offset_ms / self._get_time_between_colors_ms() + 0.5) if color_i >= self.num_colors: return RGBColor("off") return self._colors[color_i][0] def _perform_step(self): if self.num_colors == 0: self.light.remove_from_stack_by_key(self._light_key) return current_color = self._get_current_color() color_duration_ms = self._get_time_between_colors_ms() cycle_ms = self._get_total_cycle_ms() self.light.color(current_color, priority=self.config['priority'], key=self._light_key) delay_ms = color_duration_ms - ( (self.machine.clock.get_time() * 1000) % cycle_ms) % color_duration_ms self.delay.add(delay_ms, self._perform_step, name="perform_step")
class Spinner(EnableDisableMixinSystemWideDevice, SystemWideDevice): """Represents a spinner or spinner group in a pinball machine.""" config_section = 'spinners' collection = 'spinners' class_label = 'spinner' __slots__ = ["hits", "_active_ms", "_active", "_idle", "delay"] def __init__(self, machine: "MachineController", name: str) -> None: """Initialise spinner device.""" super().__init__(machine, name) self._active = False self._idle = True self._active_ms = None self.hits = None self.delay = DelayManager(machine) self.enabled = True # Default to enabled async def _initialize(self): await super()._initialize() self.hits = 0 # Cache this value because it's used a lot in rapid succession self._active_ms = self.config['active_ms'] # Can't read the switch until the switch controller is set up self.machine.events.add_handler('init_phase_4', self._register_switch_handlers, priority=1) def _register_switch_handlers(self, **kwargs): del kwargs labels = dict(zip(self.config['switches'], self.config['labels'])) if self.config['labels'] else None for switch in self.config['switches']: callback_kwargs = {"label": labels[switch]} if labels else None # register for notification of switch active state self.machine.switch_controller.add_switch_handler_obj( switch, self._update_state_from_switch, 1, callback_kwargs=callback_kwargs) def _update_state_from_switch(self, **kwargs): if not self.enabled: return label = kwargs.get("label") if not self._active: self.machine.events.post("spinner_{}_active".format(self.name), label=label) '''event: spinner_(name)_active desc: The idle spinner (name) was just hit and became active. This event will post whenever a spinner switch is hit and the spinner is not already active. args: label: The label of the switch that triggered the activation ''' if label: self.machine.events.post("spinner_{}_{}_active".format(self.name, label)) '''event: spinner_(name)_(label)_active desc: The idle spinner (name) was just hit and became active. This event will post whenever a spinner switch is hit and the spinner is not already active, but only if labels are defined for the spinner. ''' self._active = True self._idle = False self.hits += 1 self.machine.events.post("spinner_{}_hit".format(self.name), hits=self.hits, label=label) '''event: spinner_(name)_hit desc: The spinner (name) was just hit. This event will post whenever a spinner switch is hit. args: hits: The number of switch hits the spinner has had since it became active label: The label of the switch that was hit ''' if label: self.machine.events.post("spinner_{}_{}_hit".format(self.name, label)) '''event: spinner_(name)_(label)_hit desc: The spinner (name) was just hit on the switch labelled (label). This event will post whenever a spinner switch is hit and labels are defined for the spinner ''' self.delay.clear() self.delay.add(self._active_ms, self._deactivate) def _deactivate(self, **kwargs): """Post an 'inactive' event after no switch hits for the active_ms duration.""" del kwargs self.machine.events.post("spinner_{}_inactive".format(self.name), hits=self.hits) '''event: spinner_(name)_inactive desc: The spinner (name) is no longer receiving hits This event will post whenever a spinner has not received hits and its active_ms has timed out. args: hits: The number of switch hits the spinner had while it was active ''' self._active = False if self.config['idle_ms']: self.delay.add(self.config['idle_ms'], self._on_idle) if self.config['reset_when_inactive']: self.hits = 0 else: self._idle = True def _on_idle(self, **kwargs): """Post an 'idle' event if the spinner has been inactive for the idle_ms duration.""" del kwargs self.machine.events.post("spinner_{}_idle".format(self.name), hits=self.hits) '''event: spinner_(name)_idle desc: The spinner (name) is now idle This event will post whenever a spinner has not received hits and its idle_ms has timed out. If no idle_ms is defined, this event will not post. args: hits: The number of switch hits the spinner had while it was active ''' self.hits = 0 self._idle = True def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str = None): """Validate and parse spinner config.""" config = super().validate_and_parse_config(config, is_mode_config, debug_prefix) for switch in config['switch']: if switch not in config['switches']: config['switches'].append(switch) if config['labels'] and len(config['labels']) != len(config['switches']): raise ConfigFileError("Spinner labels must be the same number as switches", 1, self.name) return config @property def active(self): """Return whether the spinner is actively spinning.""" return self._active @property def idle(self): """Return whether the spinner is idle.""" return self._idle
class ComboSwitch(SystemWideDevice, ModeDevice): """Combo Switch device.""" config_section = 'combo_switches' collection = 'combo_switches' class_label = 'combo_switch' __slots__ = [ "states", "_state", "_switches_1_active", "_switches_2_active", "delay", "_switch_handlers" ] def __init__(self, machine, name): """Initialize Combo Switch.""" super().__init__(machine, name) self.states = ['inactive', 'both', 'one'] self._state = 'inactive' self._switches_1_active = False self._switches_2_active = False self.delay = DelayManager(self.machine) self._switch_handlers = [] def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str = None) -> dict: """Validate and parse config.""" config = super().validate_and_parse_config(config, is_mode_config, debug_prefix) for state in self.states + ["switches_1", "switches_2"]: if not config['events_when_{}'.format(state)]: config['events_when_{}'.format(state)] = [ "{}_{}".format(self.name, state) ] return config async def device_added_system_wide(self): """Add event handlers.""" await super().device_added_system_wide() self._add_switch_handlers() def device_loaded_in_mode(self, mode: Mode, player: Player): """Add event handlers.""" self._add_switch_handlers() def _add_switch_handlers(self): if self.config['tag_1']: for tag in self.config['tag_1']: for switch in self.machine.switches.items_tagged(tag): self.config['switches_1'].add(switch) if self.config['tag_2']: for tag in self.config['tag_2']: for switch in self.machine.switches.items_tagged(tag): self.config['switches_2'].add(switch) self._register_switch_handlers() @property def state(self): """Return current state.""" return self._state @property def can_exist_outside_of_game(self): """Return true if this device can exist outside of a game.""" return True def device_removed_from_mode(self, mode): """Mode ended. Args: ---- mode: mode which stopped """ del mode self._remove_switch_handlers() self._kill_delays() def _register_switch_handlers(self): for switch in self.config['switches_1']: self._switch_handlers.append( switch.add_handler(self._switch_1_went_active, state=1, return_info=True)) self._switch_handlers.append( switch.add_handler(self._switch_1_went_inactive, state=0, return_info=True)) for switch in self.config['switches_2']: self._switch_handlers.append( switch.add_handler(self._switch_2_went_active, state=1, return_info=True)) self._switch_handlers.append( switch.add_handler(self._switch_2_went_inactive, state=0, return_info=True)) def _remove_switch_handlers(self): self.machine.switch_controller.remove_switch_handler_by_keys( self._switch_handlers) self._switch_handlers = [] def _kill_delays(self): self.delay.clear() def _switch_1_went_active(self, switch_name, **kwargs): del kwargs self.debug_log('A switch from switches_1 just went active') self.delay.remove('switch_1_inactive') if self._switches_1_active: return if not self.config['hold_time']: self._activate_switches_1(switch_name) else: self.delay.add_if_doesnt_exist( self.config['hold_time'], partial(self._activate_switches_1, switch_name), 'switch_1_active') def _switch_2_went_active(self, switch_name, **kwargs): del kwargs self.debug_log('A switch from switches_2 just went active') self.delay.remove('switch_2_inactive') if self._switches_2_active: return if not self.config['hold_time']: self._activate_switches_2(switch_name) else: self.delay.add_if_doesnt_exist( self.config['hold_time'], partial(self._activate_switches_2, switch_name), 'switch_2_active') def _switch_1_went_inactive(self, switch_name, **kwargs): del kwargs self.debug_log('A switch from switches_1 just went inactive') for switch in self.config['switches_1']: if switch.state: # at least one switch is still active return self.delay.remove('switch_1_active') if not self.config['release_time']: self._release_switches_1(switch_name) else: self.delay.add_if_doesnt_exist( self.config['release_time'], partial(self._release_switches_1, switch_name), 'switch_1_inactive') def _switch_2_went_inactive(self, switch_name, **kwargs): del kwargs self.debug_log('A switch from switches_2 just went inactive') for switch in self.config['switches_2']: if switch.state: # at least one switch is still active return self.delay.remove('switch_2_active') if not self.config['release_time']: self._release_switches_2(switch_name) else: self.delay.add_if_doesnt_exist( self.config['release_time'], partial(self._release_switches_2, switch_name), 'switch_2_inactive') def _activate_switches_1(self, switch_name): self.debug_log('Switches_1 has passed the hold time and is now ' 'active') self._switches_1_active = self.machine.clock.get_time() self.delay.remove("switch_2_only") if self._switches_2_active: if (self.config['max_offset_time'] >= 0 and (self._switches_1_active - self._switches_2_active > self.config['max_offset_time'])): self.debug_log( "Switches_2 is active, but the " "max_offset_time=%s which is largest than when " "a Switches_2 switch was first activated, so " "the state will not switch to 'both'", self.config['max_offset_time']) return self._switch_state('both', group=1, switch=switch_name) elif self.config['max_offset_time'] >= 0: self.delay.add_if_doesnt_exist(self.config['max_offset_time'] * 1000, self._post_only_one_active_event, "switch_1_only", number=1) def _activate_switches_2(self, switch_name): self.debug_log('Switches_2 has passed the hold time and is now ' 'active') self._switches_2_active = self.machine.clock.get_time() self.delay.remove("switch_1_only") if self._switches_1_active: if (self.config['max_offset_time'] >= 0 and (self._switches_2_active - self._switches_1_active > self.config['max_offset_time'])): self.debug_log( "Switches_2 is active, but the " "max_offset_time=%s which is largest than when " "a Switches_2 switch was first activated, so " "the state will not switch to 'both'", self.config['max_offset_time']) return self._switch_state('both', group=2, switch=switch_name) elif self.config['max_offset_time'] >= 0: self.delay.add_if_doesnt_exist(self.config['max_offset_time'] * 1000, self._post_only_one_active_event, "switch_2_only", number=2) def _post_only_one_active_event(self, number): for event in self.config['events_when_switches_{}'.format(number)]: self.machine.events.post(event) def _release_switches_1(self, switch_name): self.debug_log('Switches_1 has passed the release time and is now ' 'releases') self._switches_1_active = None if self._switches_2_active and self._state == 'both': self._switch_state('one', group=1, switch=switch_name) elif self._state == 'one': self._switch_state('inactive', group=1, switch=switch_name) def _release_switches_2(self, switch_name): self.debug_log('Switches_2 has passed the release time and is now ' 'releases') self._switches_2_active = None if self._switches_1_active and self._state == 'both': self._switch_state('one', group=2, switch=switch_name) elif self._state == 'one': self._switch_state('inactive', group=2, switch=switch_name) def _switch_state(self, state, group, switch): """Post events for current step.""" if state not in self.states: raise ValueError("Received invalid state: {}".format(state)) if state == self.state: return self._state = state self.debug_log("New State: %s", state) for event in self.config['events_when_{}'.format(state)]: self.machine.events.post(event, triggering_group=group, triggering_switch=switch) '''event: (name)_one config_attribute: events_when_one desc: Combo switch (name) changed to state one. Either switch 1 or switch 2 has been released for at least the ``release_time:`` but the other switch is still active. ''' '''event: (name)_both config_attribute: events_when_both desc: Combo switch (name) changed to state both. A switch from group 1 and group 2 are both active at the same time, having been pressed within the ``max_offset_time:`` and being active for at least the ``hold_time:``. ''' '''event: (name)_inactive config_attribute: events_when_inactive desc: Combo switch (name) changed to state inactive. Both switches are inactive. ''' '''event: (name)_switches_1 config_attribute: events_when_switches_1 desc: Combo switch (name) changed to state switches_1. Only switches_1 is active. max_offset_time has passed and this hit cannot become both later on. Only emited when ``max_offset_time:`` is defined. ''' '''event: (name)_switches_2