From: Benjamin Braatz Date: Sun, 21 Mar 2021 15:05:41 +0000 (+0100) Subject: Rename to controlpi_plugins (naming conventions). X-Git-Tag: v0.3.0~47 X-Git-Url: http://git.graph-it.com/?a=commitdiff_plain;h=70ce96c2fbdeffd26b5db801f1fcb33b045cc459;p=graphit%2Fcontrolpi.git Rename to controlpi_plugins (naming conventions). --- diff --git a/controlpi-plugins/state.py b/controlpi-plugins/state.py deleted file mode 100644 index bd9d128..0000000 --- a/controlpi-plugins/state.py +++ /dev/null @@ -1,580 +0,0 @@ -"""Provide state plugins for all kinds of systems. - -- State represents a Boolean state. -- StateAlias translates to another state-like client. -- AndState combines several state clients by conjunction. -- OrState combines several state clients by disjunction. - -All these plugins use the following conventions: - -- If their state changes they send a message containing "event": "changed" - and "state": . -- If their state is reported due to a message, but did not change they send - a message containing just "state": . -- If they receive a message containing "target": and - "command": "get state" they report their current state. -- If State (or any other settable state using these conventions) receives - a message containing "target": , "command": "set state" and - "new state": it changes the state accordingly. If this - was really a change the corresponding event is sent. If it was already in - this state a report message without "event": "changed" is sent. -- AndState and OrState instances cannot be set. -- AndState and OrState can combine any message bus clients using these - conventions, not just State instances. They only use the "get state" - command (to initialise their own internal state) and react to messages - containing "state" information. - ->>> import asyncio ->>> import controlpi ->>> asyncio.run(controlpi.test( -... {"Test State": {"plugin": "State"}, -... "Test State 2": {"plugin": "State"}, -... "Test StateAlias": {"plugin": "StateAlias", -... "alias for": "Test State 2"}, -... "Test AndState": {"plugin": "AndState", -... "states": ["Test State", "Test StateAlias"]}, -... "Test OrState": {"plugin": "OrState", -... "states": ["Test State", "Test StateAlias"]}}, -... [{"target": "Test AndState", -... "command": "get state"}, -... {"target": "Test OrState", -... "command": "get state"}, -... {"target": "Test State", -... "command": "set state", "new state": True}, -... {"target": "Test StateAlias", -... "command": "set state", "new state": True}, -... {"target": "Test State", -... "command": "set state", "new state": False}])) -... # doctest: +NORMALIZE_WHITESPACE -test(): {'sender': '', 'event': 'registered', - 'client': 'Test State', 'plugin': 'State', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test State'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test State'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}]} -test(): {'sender': '', 'event': 'registered', - 'client': 'Test State 2', 'plugin': 'State', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test State 2'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test State 2'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}]} -test(): {'sender': '', 'event': 'registered', - 'client': 'Test StateAlias', 'plugin': 'StateAlias', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}, - {'target': {'const': 'Test State 2'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test State 2'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test StateAlias'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test StateAlias'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}, - {'sender': {'const': 'Test State 2'}, - 'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'sender': {'const': 'Test State 2'}, - 'state': {'type': 'boolean'}}]} -test(): {'sender': '', 'event': 'registered', - 'client': 'Test AndState', 'plugin': 'AndState', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test AndState'}, - 'command': {'const': 'get state'}}, - {'sender': {'const': 'Test State'}, - 'state': {'type': 'boolean'}}, - {'sender': {'const': 'Test StateAlias'}, - 'state': {'type': 'boolean'}}]} -test(): {'sender': '', 'event': 'registered', - 'client': 'Test OrState', 'plugin': 'OrState', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test OrState'}, - 'command': {'const': 'get state'}}, - {'sender': {'const': 'Test State'}, - 'state': {'type': 'boolean'}}, - {'sender': {'const': 'Test StateAlias'}, - 'state': {'type': 'boolean'}}]} -test(): {'sender': 'test()', 'target': 'Test AndState', - 'command': 'get state'} -test(): {'sender': 'Test AndState', 'state': False} -test(): {'sender': 'test()', 'target': 'Test OrState', - 'command': 'get state'} -test(): {'sender': 'Test OrState', 'state': False} -test(): {'sender': 'test()', 'target': 'Test State', - 'command': 'set state', 'new state': True} -test(): {'sender': 'Test State', 'event': 'changed', 'state': True} -test(): {'sender': 'Test OrState', 'event': 'changed', 'state': True} -test(): {'sender': 'test()', 'target': 'Test StateAlias', - 'command': 'set state', 'new state': True} -test(): {'sender': 'Test StateAlias', 'target': 'Test State 2', - 'command': 'set state', 'new state': True} -test(): {'sender': 'Test State 2', 'event': 'changed', 'state': True} -test(): {'sender': 'Test StateAlias', 'event': 'changed', 'state': True} -test(): {'sender': 'Test AndState', 'event': 'changed', 'state': True} -test(): {'sender': 'test()', 'target': 'Test State', - 'command': 'set state', 'new state': False} -test(): {'sender': 'Test State', 'event': 'changed', 'state': False} -test(): {'sender': 'Test AndState', 'event': 'changed', 'state': False} -""" -from controlpi import BasePlugin, Message, MessageTemplate - -from typing import Dict - - -class State(BasePlugin): - """Provide a Boolean state. - - The state of a State plugin instance can be queried with the "get state" - command and set with the "set state" command to the new state given by - the "new state" key: - >>> import asyncio - >>> import controlpi - >>> asyncio.run(controlpi.test( - ... {"Test State": {"plugin": "State"}}, - ... [{"target": "Test State", "command": "get state"}, - ... {"target": "Test State", "command": "set state", - ... "new state": True}, - ... {"target": "Test State", "command": "set state", - ... "new state": True}, - ... {"target": "Test State", "command": "get state"}])) - ... # doctest: +NORMALIZE_WHITESPACE - test(): {'sender': '', 'event': 'registered', - 'client': 'Test State', 'plugin': 'State', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test State'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test State'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}]} - test(): {'sender': 'test()', 'target': 'Test State', - 'command': 'get state'} - test(): {'sender': 'Test State', 'state': False} - test(): {'sender': 'test()', 'target': 'Test State', - 'command': 'set state', 'new state': True} - test(): {'sender': 'Test State', 'event': 'changed', 'state': True} - test(): {'sender': 'test()', 'target': 'Test State', - 'command': 'set state', 'new state': True} - test(): {'sender': 'Test State', 'state': True} - test(): {'sender': 'test()', 'target': 'Test State', - 'command': 'get state'} - test(): {'sender': 'Test State', 'state': True} - """ - - CONF_SCHEMA = True - """Schema for State plugin configuration. - - There are no required or optional configuration keys. - """ - - async def receive(self, message: Message) -> None: - """Process commands to get/set state.""" - if message['command'] == 'get state': - await self.bus.send(Message(self.name, {'state': self.state})) - elif message['command'] == 'set state': - if self.state != message['new state']: - assert isinstance(message['new state'], bool) - self.state: bool = message['new state'] - await self.bus.send(Message(self.name, - {'event': 'changed', - 'state': self.state})) - else: - await self.bus.send(Message(self.name, - {'state': self.state})) - - def process_conf(self) -> None: - """Register plugin as bus client.""" - self.state = False - sends = [MessageTemplate({'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}), - MessageTemplate({'state': {'type': 'boolean'}})] - receives = [MessageTemplate({'target': {'const': self.name}, - 'command': {'const': 'get state'}}), - MessageTemplate({'target': {'const': self.name}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}})] - self.bus.register(self.name, 'State', sends, receives, self.receive) - - async def run(self) -> None: - """Run no code proactively.""" - pass - - -class StateAlias(BasePlugin): - """Define an alias for another state. - - The "alias for" configuration key gets the name for the other state that - is aliased by the StateAlias plugin instance. - - The "get state" and "set state" commands are forwarded to and the - "changed" events and "state" messages are forwarded from this other - state: - >>> import asyncio - >>> import controlpi - >>> asyncio.run(controlpi.test( - ... {"Test State": {"plugin": "State"}, - ... "Test StateAlias": {"plugin": "StateAlias", - ... "alias for": "Test State"}}, - ... [{"target": "Test State", "command": "get state"}, - ... {"target": "Test StateAlias", "command": "set state", - ... "new state": True}, - ... {"target": "Test State", "command": "set state", - ... "new state": True}, - ... {"target": "Test StateAlias", "command": "get state"}])) - ... # doctest: +NORMALIZE_WHITESPACE - test(): {'sender': '', 'event': 'registered', - 'client': 'Test State', 'plugin': 'State', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test State'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test State'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}]} - test(): {'sender': '', 'event': 'registered', - 'client': 'Test StateAlias', 'plugin': 'StateAlias', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}, - {'target': {'const': 'Test State'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test State'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test StateAlias'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test StateAlias'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}, - {'sender': {'const': 'Test State'}, - 'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'sender': {'const': 'Test State'}, - 'state': {'type': 'boolean'}}]} - test(): {'sender': 'test()', 'target': 'Test State', - 'command': 'get state'} - test(): {'sender': 'Test State', 'state': False} - test(): {'sender': 'Test StateAlias', 'state': False} - test(): {'sender': 'test()', 'target': 'Test StateAlias', - 'command': 'set state', 'new state': True} - test(): {'sender': 'Test StateAlias', 'target': 'Test State', - 'command': 'set state', 'new state': True} - test(): {'sender': 'Test State', 'event': 'changed', 'state': True} - test(): {'sender': 'Test StateAlias', 'event': 'changed', 'state': True} - test(): {'sender': 'test()', 'target': 'Test State', - 'command': 'set state', 'new state': True} - test(): {'sender': 'Test State', 'state': True} - test(): {'sender': 'Test StateAlias', 'state': True} - test(): {'sender': 'test()', 'target': 'Test StateAlias', - 'command': 'get state'} - test(): {'sender': 'Test StateAlias', 'target': 'Test State', - 'command': 'get state'} - test(): {'sender': 'Test State', 'state': True} - test(): {'sender': 'Test StateAlias', 'state': True} - """ - - CONF_SCHEMA = {'properties': {'alias for': {'type': 'string'}}, - 'required': ['alias for']} - """Schema for StateAlias plugin configuration. - - Required configuration key: - - - 'alias for': name of aliased state. - """ - - async def receive(self, message: Message) -> None: - """Translate states from and commands to aliased state.""" - alias_message = Message(self.name) - if ('target' in message and message['target'] == self.name and - 'command' in message): - alias_message['target'] = self.conf['alias for'] - if message['command'] == 'get state': - alias_message['command'] = 'get state' - await self.bus.send(alias_message) - elif (message['command'] == 'set state' and - 'new state' in message): - alias_message['command'] = 'set state' - alias_message['new state'] = message['new state'] - await self.bus.send(alias_message) - if (message['sender'] == self.conf['alias for'] and - 'state' in message): - if 'event' in message and message['event'] == 'changed': - alias_message['event'] = 'changed' - alias_message['state'] = message['state'] - await self.bus.send(alias_message) - - def process_conf(self) -> None: - """Register plugin as bus client.""" - sends = [MessageTemplate({'target': {'const': self.conf['alias for']}, - 'command': {'const': 'get state'}}), - MessageTemplate({'target': {'const': self.conf['alias for']}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}), - MessageTemplate({'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}), - MessageTemplate({'state': {'type': 'boolean'}})] - receives = [MessageTemplate({'target': {'const': self.name}, - 'command': {'const': 'get state'}}), - MessageTemplate({'target': {'const': self.name}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}), - MessageTemplate({'sender': - {'const': self.conf['alias for']}, - 'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}), - MessageTemplate({'sender': - {'const': self.conf['alias for']}, - 'state': {'type': 'boolean'}})] - self.bus.register(self.name, 'StateAlias', - sends, receives, self.receive) - - async def run(self) -> None: - """Run no code proactively.""" - pass - - -class AndState(BasePlugin): - """Implement conjunction of states. - - The "states" configuration key gets an array of states to be combined. - An AndState plugin client reacts to "get state" commands and sends - "changed" events when a change in one of the combined states leads to - a change for the conjunction: - >>> import asyncio - >>> import controlpi - >>> asyncio.run(controlpi.test( - ... {"Test State 1": {"plugin": "State"}, - ... "Test State 2": {"plugin": "State"}, - ... "Test AndState": {"plugin": "AndState", - ... "states": ["Test State 1", "Test State 2"]}}, - ... [{"target": "Test State 1", "command": "set state", - ... "new state": True}, - ... {"target": "Test State 2", "command": "set state", - ... "new state": True}, - ... {"target": "Test State 1", "command": "set state", - ... "new state": False}, - ... {"target": "Test AndState", "command": "get state"}])) - ... # doctest: +NORMALIZE_WHITESPACE - test(): {'sender': '', 'event': 'registered', - 'client': 'Test State 1', 'plugin': 'State', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test State 1'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test State 1'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}]} - test(): {'sender': '', 'event': 'registered', - 'client': 'Test State 2', 'plugin': 'State', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test State 2'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test State 2'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}]} - test(): {'sender': '', 'event': 'registered', - 'client': 'Test AndState', 'plugin': 'AndState', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test AndState'}, - 'command': {'const': 'get state'}}, - {'sender': {'const': 'Test State 1'}, - 'state': {'type': 'boolean'}}, - {'sender': {'const': 'Test State 2'}, - 'state': {'type': 'boolean'}}]} - test(): {'sender': 'test()', 'target': 'Test State 1', - 'command': 'set state', 'new state': True} - test(): {'sender': 'Test State 1', 'event': 'changed', 'state': True} - test(): {'sender': 'test()', 'target': 'Test State 2', - 'command': 'set state', 'new state': True} - test(): {'sender': 'Test State 2', 'event': 'changed', 'state': True} - test(): {'sender': 'Test AndState', 'event': 'changed', 'state': True} - test(): {'sender': 'test()', 'target': 'Test State 1', - 'command': 'set state', 'new state': False} - test(): {'sender': 'Test State 1', 'event': 'changed', 'state': False} - test(): {'sender': 'Test AndState', 'event': 'changed', 'state': False} - test(): {'sender': 'test()', 'target': 'Test AndState', - 'command': 'get state'} - test(): {'sender': 'Test AndState', 'state': False} - """ - - CONF_SCHEMA = {'properties': {'states': {'type': 'array', - 'items': {'type': 'string'}}}, - 'required': ['states']} - """Schema for AndState plugin configuration. - - Required configuration key: - - - 'states': list of names of combined states. - """ - - async def receive(self, message: Message) -> None: - """.""" - if ('target' in message and message['target'] == self.name and - 'command' in message and message['command'] == 'get state'): - await self.bus.send(Message(self.name, {'state': self.state})) - if 'state' in message and message['sender'] in self.conf['states']: - assert isinstance(message['sender'], str) - assert isinstance(message['state'], bool) - self.states[message['sender']] = message['state'] - new_state = all(self.states.values()) - if self.state != new_state: - self.state: bool = new_state - await self.bus.send(Message(self.name, - {'event': 'changed', - 'state': self.state})) - - def process_conf(self) -> None: - """Register plugin as bus client.""" - sends = [MessageTemplate({'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}), - MessageTemplate({'state': {'type': 'boolean'}})] - receives = [MessageTemplate({'target': {'const': self.name}, - 'command': {'const': 'get state'}})] - self.states: Dict[str, bool] = {} - for state in self.conf['states']: - receives.append(MessageTemplate({'sender': {'const': state}, - 'state': {'type': 'boolean'}})) - self.states[state] = False - self.state = all(self.states.values()) - self.bus.register(self.name, 'AndState', - sends, receives, self.receive) - - async def run(self) -> None: - """Run no code proactively.""" - pass - - -class OrState(BasePlugin): - """Implement disjunction of states. - - The "states" configuration key gets an array of states to be combined. - An OrState plugin client reacts to "get state" commands and sends - "changed" events when a change in one of the combined states leads to - a change for the disjunction: - >>> import asyncio - >>> import controlpi - >>> asyncio.run(controlpi.test( - ... {"Test State 1": {"plugin": "State"}, - ... "Test State 2": {"plugin": "State"}, - ... "Test OrState": {"plugin": "OrState", - ... "states": ["Test State 1", "Test State 2"]}}, - ... [{"target": "Test State 1", "command": "set state", - ... "new state": True}, - ... {"target": "Test State 2", "command": "set state", - ... "new state": True}, - ... {"target": "Test State 1", "command": "set state", - ... "new state": False}, - ... {"target": "Test OrState", "command": "get state"}])) - ... # doctest: +NORMALIZE_WHITESPACE - test(): {'sender': '', 'event': 'registered', - 'client': 'Test State 1', 'plugin': 'State', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test State 1'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test State 1'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}]} - test(): {'sender': '', 'event': 'registered', - 'client': 'Test State 2', 'plugin': 'State', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test State 2'}, - 'command': {'const': 'get state'}}, - {'target': {'const': 'Test State 2'}, - 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}]} - test(): {'sender': '', 'event': 'registered', - 'client': 'Test OrState', 'plugin': 'OrState', - 'sends': [{'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}, - {'state': {'type': 'boolean'}}], - 'receives': [{'target': {'const': 'Test OrState'}, - 'command': {'const': 'get state'}}, - {'sender': {'const': 'Test State 1'}, - 'state': {'type': 'boolean'}}, - {'sender': {'const': 'Test State 2'}, - 'state': {'type': 'boolean'}}]} - test(): {'sender': 'test()', 'target': 'Test State 1', - 'command': 'set state', 'new state': True} - test(): {'sender': 'Test State 1', 'event': 'changed', 'state': True} - test(): {'sender': 'Test OrState', 'event': 'changed', 'state': True} - test(): {'sender': 'test()', 'target': 'Test State 2', - 'command': 'set state', 'new state': True} - test(): {'sender': 'Test State 2', 'event': 'changed', 'state': True} - test(): {'sender': 'test()', 'target': 'Test State 1', - 'command': 'set state', 'new state': False} - test(): {'sender': 'Test State 1', 'event': 'changed', 'state': False} - test(): {'sender': 'test()', 'target': 'Test OrState', - 'command': 'get state'} - test(): {'sender': 'Test OrState', 'state': True} - """ - - CONF_SCHEMA = {'properties': {'states': {'type': 'array', - 'items': {'type': 'string'}}}, - 'required': ['states']} - """Schema for OrState plugin configuration. - - Required configuration key: - - - 'states': list of names of combined states. - """ - - async def receive(self, message: Message) -> None: - """.""" - if ('target' in message and message['target'] == self.name and - 'command' in message and message['command'] == 'get state'): - await self.bus.send(Message(self.name, {'state': self.state})) - if 'state' in message and message['sender'] in self.conf['states']: - assert isinstance(message['sender'], str) - assert isinstance(message['state'], bool) - self.states[message['sender']] = message['state'] - new_state = any(self.states.values()) - if self.state != new_state: - self.state: bool = new_state - await self.bus.send(Message(self.name, - {'event': 'changed', - 'state': self.state})) - - def process_conf(self) -> None: - """Register plugin as bus client.""" - sends = [MessageTemplate({'event': {'const': 'changed'}, - 'state': {'type': 'boolean'}}), - MessageTemplate({'state': {'type': 'boolean'}})] - receives = [MessageTemplate({'target': {'const': self.name}, - 'command': {'const': 'get state'}})] - self.states: Dict[str, bool] = {} - for state in self.conf['states']: - receives.append(MessageTemplate({'sender': {'const': state}, - 'state': {'type': 'boolean'}})) - self.states[state] = False - self.state = any(self.states.values()) - self.bus.register(self.name, 'OrState', - sends, receives, self.receive) - - async def run(self) -> None: - """Run no code proactively.""" - pass diff --git a/controlpi-plugins/util.py b/controlpi-plugins/util.py deleted file mode 100644 index 313e50b..0000000 --- a/controlpi-plugins/util.py +++ /dev/null @@ -1,397 +0,0 @@ -"""Provide utility plugins for all kinds of systems. - -- Log logs messages on stdout. -- Init sends list of messages on startup and on demand. -- Alias translates messages to an alias. - ->>> import controlpi ->>> asyncio.run(controlpi.test( -... {"Test Log": {"plugin": "Log", -... "filter": [{"sender": {"const": "Test Alias"}}]}, -... "Test Init": {"plugin": "Init", -... "messages": [{"id": 42, "content": "Test Message"}]}, -... "Test Alias": {"plugin": "Alias", -... "from": {"sender": {"const": "Test Init"}, -... "id": {"const": 42}}, -... "to": {"id": "translated"}}}, [])) -... # doctest: +NORMALIZE_WHITESPACE -test(): {'sender': '', 'event': 'registered', - 'client': 'Test Log', 'plugin': 'Log', - 'sends': [], 'receives': [{'sender': {'const': 'Test Alias'}}]} -test(): {'sender': '', 'event': 'registered', - 'client': 'Test Init', 'plugin': 'Init', - 'sends': [{'id': {'const': 42}, - 'content': {'const': 'Test Message'}}], - 'receives': [{'target': {'const': 'Test Init'}, - 'command': {'const': 'execute'}}]} -test(): {'sender': '', 'event': 'registered', - 'client': 'Test Alias', 'plugin': 'Alias', - 'sends': [{'id': {'const': 'translated'}}], - 'receives': [{'sender': {'const': 'Test Init'}, - 'id': {'const': 42}}]} -test(): {'sender': 'Test Init', 'id': 42, - 'content': 'Test Message'} -test(): {'sender': 'Test Alias', 'id': 'translated', - 'content': 'Test Message'} -Test Log: {'sender': 'Test Alias', 'id': 'translated', - 'content': 'Test Message'} -""" -import asyncio - -from controlpi import BasePlugin, Message, MessageTemplate - - -class Log(BasePlugin): - """Log messages on stdout. - - The "filter" configuration key gets a list of message templates defining - the messages that should be logged by the plugin instance. - - In the following example the first and third message match the given - template and are logged by the instance "Test Log", while the second - message does not match and is only logged by the test, but not by the - Log instance: - >>> import controlpi - >>> asyncio.run(controlpi.test( - ... {"Test Log": {"plugin": "Log", - ... "filter": [{"id": {"const": 42}}]}}, - ... [{"id": 42, "message": "Test Message"}, - ... {"id": 42.42, "message": "Second Message"}, - ... {"id": 42, "message": "Third Message"}])) - ... # doctest: +NORMALIZE_WHITESPACE - test(): {'sender': '', 'event': 'registered', - 'client': 'Test Log', 'plugin': 'Log', - 'sends': [], 'receives': [{'id': {'const': 42}}]} - test(): {'sender': 'test()', 'id': 42, 'message': 'Test Message'} - Test Log: {'sender': 'test()', 'id': 42, 'message': 'Test Message'} - test(): {'sender': 'test()', 'id': 42.42, 'message': 'Second Message'} - test(): {'sender': 'test()', 'id': 42, 'message': 'Third Message'} - Test Log: {'sender': 'test()', 'id': 42, 'message': 'Third Message'} - - The "filter" key is required: - >>> asyncio.run(controlpi.test( - ... {"Test Log": {"plugin": "Log"}}, [])) - 'filter' is a required property - - Failed validating 'required' in schema: - {'properties': {'filter': {'items': {'type': 'object'}, - 'type': 'array'}}, - 'required': ['filter']} - - On instance: - {'plugin': 'Log'} - Configuration for 'Test Log' is not valid. - - The "filter" key has to contain a list of message templates, i.e., - JSON objects: - >>> asyncio.run(controlpi.test( - ... {"Test Log": {"plugin": "Log", - ... "filter": [42]}}, [])) - 42 is not of type 'object' - - Failed validating 'type' in schema['properties']['filter']['items']: - {'type': 'object'} - - On instance['filter'][0]: - 42 - Configuration for 'Test Log' is not valid. - """ - - CONF_SCHEMA = {'properties': {'filter': {'type': 'array', - 'items': {'type': 'object'}}}, - 'required': ['filter']} - """Schema for Log plugin configuration. - - Required configuration key: - - - 'filter': list of message templates to be logged. - """ - - async def log(self, message: Message) -> None: - """Log received message on stdout using own name as prefix.""" - print(f"{self.name}: {message}") - - def process_conf(self) -> None: - """Register plugin as bus client.""" - self.bus.register(self.name, 'Log', [], self.conf['filter'], self.log) - - async def run(self) -> None: - """Run no code proactively.""" - pass - - -class Init(BasePlugin): - """Send list of messages on startup and on demand. - - The "messages" configuration key gets a list of messages to be sent on - startup. The same list is sent in reaction to a message with - "target": and "command": "execute". - - In the example, the two configured messages are sent twice, once at - startup and a second time in reaction to the "execute" command sent by - the test: - >>> import controlpi - >>> asyncio.run(controlpi.test( - ... {"Test Init": {"plugin": "Init", - ... "messages": [{"id": 42, - ... "content": "Test Message"}, - ... {"id": 42.42, - ... "content": "Second Message"}]}}, - ... [{"target": "Test Init", "command": "execute"}])) - ... # doctest: +NORMALIZE_WHITESPACE - test(): {'sender': '', 'event': 'registered', - 'client': 'Test Init', 'plugin': 'Init', - 'sends': [{'id': {'const': 42}, - 'content': {'const': 'Test Message'}}, - {'id': {'const': 42.42}, - 'content': {'const': 'Second Message'}}], - 'receives': [{'target': {'const': 'Test Init'}, - 'command': {'const': 'execute'}}]} - test(): {'sender': 'Test Init', 'id': 42, 'content': 'Test Message'} - test(): {'sender': 'Test Init', 'id': 42.42, 'content': 'Second Message'} - test(): {'sender': 'test()', 'target': 'Test Init', 'command': 'execute'} - test(): {'sender': 'Test Init', 'id': 42, 'content': 'Test Message'} - test(): {'sender': 'Test Init', 'id': 42.42, 'content': 'Second Message'} - - The "messages" key is required: - >>> asyncio.run(controlpi.test( - ... {"Test Init": {"plugin": "Init"}}, [])) - 'messages' is a required property - - Failed validating 'required' in schema: - {'properties': {'messages': {'items': {'type': 'object'}, - 'type': 'array'}}, - 'required': ['messages']} - - On instance: - {'plugin': 'Init'} - Configuration for 'Test Init' is not valid. - - The "messages" key has to contain a list of (partial) messages, i.e., - JSON objects: - >>> asyncio.run(controlpi.test( - ... {"Test Init": {"plugin": "Init", - ... "messages": [42]}}, [])) - 42 is not of type 'object' - - Failed validating 'type' in schema['properties']['messages']['items']: - {'type': 'object'} - - On instance['messages'][0]: - 42 - Configuration for 'Test Init' is not valid. - """ - - CONF_SCHEMA = {'properties': {'messages': {'type': 'array', - 'items': {'type': 'object'}}}, - 'required': ['messages']} - """Schema for Init plugin configuration. - - Required configuration key: - - - 'messages': list of messages to be sent. - """ - - async def execute(self, message: Message) -> None: - """Send configured messages.""" - for message in self.conf['messages']: - await self.bus.send(Message(self.name, message)) - # Give immediate reactions to messages opportunity to happen: - await asyncio.sleep(0) - - def process_conf(self) -> None: - """Register plugin as bus client.""" - receives = [MessageTemplate({'target': {'const': self.name}, - 'command': {'const': 'execute'}})] - sends = [MessageTemplate.from_message(message) - for message in self.conf['messages']] - self.bus.register(self.name, 'Init', sends, receives, self.execute) - - async def run(self) -> None: - """Send configured messages on startup.""" - for message in self.conf['messages']: - await self.bus.send(Message(self.name, message)) - - -class Execute(BasePlugin): - """Send configurable list of messages on demand. - - An Execute plugin instance receives two kinds of commands. - The "set messages" command has a "messages" key with a list of (partial) - messages, which are sent by the Execute instance in reaction to an - "execute" command. - - In the example, the first command sent by the test sets two messages, - which are then sent in reaction to the second command sent by the test: - >>> import controlpi - >>> asyncio.run(controlpi.test( - ... {"Test Execute": {"plugin": "Execute"}}, - ... [{"target": "Test Execute", "command": "set messages", - ... "messages": [{"id": 42, "content": "Test Message"}, - ... {"id": 42.42, "content": "Second Message"}]}, - ... {"target": "Test Execute", "command": "execute"}])) - ... # doctest: +NORMALIZE_WHITESPACE - test(): {'sender': '', 'event': 'registered', - 'client': 'Test Execute', 'plugin': 'Execute', - 'sends': [{}], - 'receives': [{'target': {'const': 'Test Execute'}, - 'command': {'const': 'set messages'}, - 'messages': {'type': 'array', - 'items': {'type': 'object'}}}, - {'target': {'const': 'Test Execute'}, - 'command': {'const': 'execute'}}]} - test(): {'sender': 'test()', 'target': 'Test Execute', - 'command': 'set messages', - 'messages': [{'id': 42, 'content': 'Test Message'}, - {'id': 42.42, 'content': 'Second Message'}]} - test(): {'sender': 'test()', 'target': 'Test Execute', - 'command': 'execute'} - test(): {'sender': 'Test Execute', 'id': 42, - 'content': 'Test Message'} - test(): {'sender': 'Test Execute', 'id': 42.42, - 'content': 'Second Message'} - """ - - CONF_SCHEMA = True - """Schema for Execute plugin configuration. - - There are no required or optional configuration keys. - """ - - async def execute(self, message: Message) -> None: - """Set or send configured messages.""" - if message['command'] == 'set messages': - assert isinstance(message['messages'], list) - self.messages = list(message['messages']) - elif message['command'] == 'execute': - for message in self.messages: - await self.bus.send(Message(self.name, message)) - # Give immediate reactions to messages opportunity to happen: - await asyncio.sleep(0) - - def process_conf(self) -> None: - """Register plugin as bus client.""" - self.messages = [] - receives = [MessageTemplate({'target': {'const': self.name}, - 'command': {'const': 'set messages'}, - 'messages': - {'type': 'array', - 'items': {'type': 'object'}}}), - MessageTemplate({'target': {'const': self.name}, - 'command': {'const': 'execute'}})] - sends = [MessageTemplate()] - self.bus.register(self.name, 'Execute', sends, receives, self.execute) - - async def run(self) -> None: - """Run no code proactively.""" - pass - - -class Alias(BasePlugin): - """Translate messages to an alias. - - The "from" configuration key gets a message template and the - configuration key "to" a (partial) message. All messages matching the - template are received by the Alias instance and a message translated by - removing the keys of the "from" template and adding the keys and values - of the "to" message is sent. Keys that are neither in "from" nor in "to" - are retained. - - In the example, the two messages sent by the test are translated by the - Alias instance and the translated messages are sent by it preserving - the "content" keys: - >>> import controlpi - >>> asyncio.run(controlpi.test( - ... {"Test Alias": {"plugin": "Alias", - ... "from": {"id": {"const": 42}}, - ... "to": {"id": "translated"}}}, - ... [{"id": 42, "content": "Test Message"}, - ... {"id": 42, "content": "Second Message"}])) - ... # doctest: +NORMALIZE_WHITESPACE - test(): {'sender': '', 'event': 'registered', - 'client': 'Test Alias', 'plugin': 'Alias', - 'sends': [{'id': {'const': 'translated'}}], - 'receives': [{'id': {'const': 42}}]} - test(): {'sender': 'test()', 'id': 42, - 'content': 'Test Message'} - test(): {'sender': 'Test Alias', 'id': 'translated', - 'content': 'Test Message'} - test(): {'sender': 'test()', 'id': 42, - 'content': 'Second Message'} - test(): {'sender': 'Test Alias', 'id': 'translated', - 'content': 'Second Message'} - - The "from" and "to" keys are required: - >>> asyncio.run(controlpi.test( - ... {"Test Alias": {"plugin": "Alias"}}, [])) - 'from' is a required property - - Failed validating 'required' in schema: - {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}}, - 'required': ['from', 'to']} - - On instance: - {'plugin': 'Alias'} - 'to' is a required property - - Failed validating 'required' in schema: - {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}}, - 'required': ['from', 'to']} - - On instance: - {'plugin': 'Alias'} - Configuration for 'Test Alias' is not valid. - - The "from" key has to contain a message template and the "to" key a - (partial) message, i.e., both have to be JSON objects: - >>> asyncio.run(controlpi.test( - ... {"Test Alias": {"plugin": "Alias", - ... "from": 42, - ... "to": 42}}, [])) - 42 is not of type 'object' - - Failed validating 'type' in schema['properties']['from']: - {'type': 'object'} - - On instance['from']: - 42 - 42 is not of type 'object' - - Failed validating 'type' in schema['properties']['to']: - {'type': 'object'} - - On instance['to']: - 42 - Configuration for 'Test Alias' is not valid. - """ - - CONF_SCHEMA = {'properties': {'from': {'type': 'object'}, - 'to': {'type': 'object'}}, - 'required': ['from', 'to']} - """Schema for Alias plugin configuration. - - Required configuration keys: - - - 'from': template of messages to be translated. - - 'to': translated message to be sent. - """ - - async def alias(self, message: Message) -> None: - """Translate and send message.""" - alias_message = Message(self.name) - alias_message.update(self.conf['to']) - for key in message: - if key != 'sender' and key not in self.conf['from']: - alias_message[key] = message[key] - await self.bus.send(alias_message) - - def process_conf(self) -> None: - """Register plugin as bus client.""" - self.bus.register(self.name, 'Alias', - [MessageTemplate.from_message(self.conf['to'])], - [self.conf['from']], - self.alias) - - async def run(self) -> None: - """Run no code proactively.""" - pass diff --git a/controlpi-plugins/wait.py b/controlpi-plugins/wait.py deleted file mode 100644 index afe5431..0000000 --- a/controlpi-plugins/wait.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Provide waiting/sleeping plugins for all kinds of systems. - -TODO: documentation, doctests -""" -import asyncio - -from controlpi import BasePlugin, Message - - -class Wait(BasePlugin): - CONF_SCHEMA = {'properties': {'seconds': {'type': 'number'}}, - 'required': ['seconds']} - - async def wait(self, message: Message) -> None: - await asyncio.sleep(self.conf['seconds']) - await self.bus.send({'sender': self.name, 'event': 'finished'}) - - def process_conf(self) -> None: - receives = [{'target': {'const': self.name}, - 'command': {'const': 'wait'}}] - sends = [{'event': {'const': 'finished'}}] - self.bus.register(self.name, 'Wait', sends, receives, self.wait) - - async def run(self) -> None: - pass - - -class GenericWait(BasePlugin): - CONF_SCHEMA = True - - async def wait(self, message: Message) -> None: - await asyncio.sleep(message['seconds']) - await self.bus.send(Message(self.name, {'id': message['id']})) - - def process_conf(self) -> None: - receives = [{'target': {'const': self.name}, - 'command': {'const': 'wait'}, - 'seconds': {'type': 'number'}, - 'id': {'type': 'string'}}] - sends = [{'id': {'type': 'string'}}] - self.bus.register(self.name, 'GenericWait', sends, receives, self.wait) - - async def run(self) -> None: - pass diff --git a/controlpi/__init__.py b/controlpi/__init__.py index e3e5d56..658b775 100644 --- a/controlpi/__init__.py +++ b/controlpi/__init__.py @@ -34,7 +34,7 @@ def _process_conf(message_bus: MessageBus, valid = False if not valid: return [] - plugins = PluginRegistry('controlpi-plugins', BasePlugin) + plugins = PluginRegistry('controlpi_plugins', BasePlugin) coroutines = [message_bus.run()] for instance_name in conf: instance_conf = conf[instance_name] diff --git a/controlpi_plugins/state.py b/controlpi_plugins/state.py new file mode 100644 index 0000000..bd9d128 --- /dev/null +++ b/controlpi_plugins/state.py @@ -0,0 +1,580 @@ +"""Provide state plugins for all kinds of systems. + +- State represents a Boolean state. +- StateAlias translates to another state-like client. +- AndState combines several state clients by conjunction. +- OrState combines several state clients by disjunction. + +All these plugins use the following conventions: + +- If their state changes they send a message containing "event": "changed" + and "state": . +- If their state is reported due to a message, but did not change they send + a message containing just "state": . +- If they receive a message containing "target": and + "command": "get state" they report their current state. +- If State (or any other settable state using these conventions) receives + a message containing "target": , "command": "set state" and + "new state": it changes the state accordingly. If this + was really a change the corresponding event is sent. If it was already in + this state a report message without "event": "changed" is sent. +- AndState and OrState instances cannot be set. +- AndState and OrState can combine any message bus clients using these + conventions, not just State instances. They only use the "get state" + command (to initialise their own internal state) and react to messages + containing "state" information. + +>>> import asyncio +>>> import controlpi +>>> asyncio.run(controlpi.test( +... {"Test State": {"plugin": "State"}, +... "Test State 2": {"plugin": "State"}, +... "Test StateAlias": {"plugin": "StateAlias", +... "alias for": "Test State 2"}, +... "Test AndState": {"plugin": "AndState", +... "states": ["Test State", "Test StateAlias"]}, +... "Test OrState": {"plugin": "OrState", +... "states": ["Test State", "Test StateAlias"]}}, +... [{"target": "Test AndState", +... "command": "get state"}, +... {"target": "Test OrState", +... "command": "get state"}, +... {"target": "Test State", +... "command": "set state", "new state": True}, +... {"target": "Test StateAlias", +... "command": "set state", "new state": True}, +... {"target": "Test State", +... "command": "set state", "new state": False}])) +... # doctest: +NORMALIZE_WHITESPACE +test(): {'sender': '', 'event': 'registered', + 'client': 'Test State', 'plugin': 'State', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test State'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test State'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}]} +test(): {'sender': '', 'event': 'registered', + 'client': 'Test State 2', 'plugin': 'State', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test State 2'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test State 2'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}]} +test(): {'sender': '', 'event': 'registered', + 'client': 'Test StateAlias', 'plugin': 'StateAlias', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}, + {'target': {'const': 'Test State 2'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test State 2'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test StateAlias'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test StateAlias'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}, + {'sender': {'const': 'Test State 2'}, + 'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'sender': {'const': 'Test State 2'}, + 'state': {'type': 'boolean'}}]} +test(): {'sender': '', 'event': 'registered', + 'client': 'Test AndState', 'plugin': 'AndState', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test AndState'}, + 'command': {'const': 'get state'}}, + {'sender': {'const': 'Test State'}, + 'state': {'type': 'boolean'}}, + {'sender': {'const': 'Test StateAlias'}, + 'state': {'type': 'boolean'}}]} +test(): {'sender': '', 'event': 'registered', + 'client': 'Test OrState', 'plugin': 'OrState', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test OrState'}, + 'command': {'const': 'get state'}}, + {'sender': {'const': 'Test State'}, + 'state': {'type': 'boolean'}}, + {'sender': {'const': 'Test StateAlias'}, + 'state': {'type': 'boolean'}}]} +test(): {'sender': 'test()', 'target': 'Test AndState', + 'command': 'get state'} +test(): {'sender': 'Test AndState', 'state': False} +test(): {'sender': 'test()', 'target': 'Test OrState', + 'command': 'get state'} +test(): {'sender': 'Test OrState', 'state': False} +test(): {'sender': 'test()', 'target': 'Test State', + 'command': 'set state', 'new state': True} +test(): {'sender': 'Test State', 'event': 'changed', 'state': True} +test(): {'sender': 'Test OrState', 'event': 'changed', 'state': True} +test(): {'sender': 'test()', 'target': 'Test StateAlias', + 'command': 'set state', 'new state': True} +test(): {'sender': 'Test StateAlias', 'target': 'Test State 2', + 'command': 'set state', 'new state': True} +test(): {'sender': 'Test State 2', 'event': 'changed', 'state': True} +test(): {'sender': 'Test StateAlias', 'event': 'changed', 'state': True} +test(): {'sender': 'Test AndState', 'event': 'changed', 'state': True} +test(): {'sender': 'test()', 'target': 'Test State', + 'command': 'set state', 'new state': False} +test(): {'sender': 'Test State', 'event': 'changed', 'state': False} +test(): {'sender': 'Test AndState', 'event': 'changed', 'state': False} +""" +from controlpi import BasePlugin, Message, MessageTemplate + +from typing import Dict + + +class State(BasePlugin): + """Provide a Boolean state. + + The state of a State plugin instance can be queried with the "get state" + command and set with the "set state" command to the new state given by + the "new state" key: + >>> import asyncio + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test State": {"plugin": "State"}}, + ... [{"target": "Test State", "command": "get state"}, + ... {"target": "Test State", "command": "set state", + ... "new state": True}, + ... {"target": "Test State", "command": "set state", + ... "new state": True}, + ... {"target": "Test State", "command": "get state"}])) + ... # doctest: +NORMALIZE_WHITESPACE + test(): {'sender': '', 'event': 'registered', + 'client': 'Test State', 'plugin': 'State', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test State'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test State'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}]} + test(): {'sender': 'test()', 'target': 'Test State', + 'command': 'get state'} + test(): {'sender': 'Test State', 'state': False} + test(): {'sender': 'test()', 'target': 'Test State', + 'command': 'set state', 'new state': True} + test(): {'sender': 'Test State', 'event': 'changed', 'state': True} + test(): {'sender': 'test()', 'target': 'Test State', + 'command': 'set state', 'new state': True} + test(): {'sender': 'Test State', 'state': True} + test(): {'sender': 'test()', 'target': 'Test State', + 'command': 'get state'} + test(): {'sender': 'Test State', 'state': True} + """ + + CONF_SCHEMA = True + """Schema for State plugin configuration. + + There are no required or optional configuration keys. + """ + + async def receive(self, message: Message) -> None: + """Process commands to get/set state.""" + if message['command'] == 'get state': + await self.bus.send(Message(self.name, {'state': self.state})) + elif message['command'] == 'set state': + if self.state != message['new state']: + assert isinstance(message['new state'], bool) + self.state: bool = message['new state'] + await self.bus.send(Message(self.name, + {'event': 'changed', + 'state': self.state})) + else: + await self.bus.send(Message(self.name, + {'state': self.state})) + + def process_conf(self) -> None: + """Register plugin as bus client.""" + self.state = False + sends = [MessageTemplate({'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}), + MessageTemplate({'state': {'type': 'boolean'}})] + receives = [MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'get state'}}), + MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}})] + self.bus.register(self.name, 'State', sends, receives, self.receive) + + async def run(self) -> None: + """Run no code proactively.""" + pass + + +class StateAlias(BasePlugin): + """Define an alias for another state. + + The "alias for" configuration key gets the name for the other state that + is aliased by the StateAlias plugin instance. + + The "get state" and "set state" commands are forwarded to and the + "changed" events and "state" messages are forwarded from this other + state: + >>> import asyncio + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test State": {"plugin": "State"}, + ... "Test StateAlias": {"plugin": "StateAlias", + ... "alias for": "Test State"}}, + ... [{"target": "Test State", "command": "get state"}, + ... {"target": "Test StateAlias", "command": "set state", + ... "new state": True}, + ... {"target": "Test State", "command": "set state", + ... "new state": True}, + ... {"target": "Test StateAlias", "command": "get state"}])) + ... # doctest: +NORMALIZE_WHITESPACE + test(): {'sender': '', 'event': 'registered', + 'client': 'Test State', 'plugin': 'State', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test State'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test State'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}]} + test(): {'sender': '', 'event': 'registered', + 'client': 'Test StateAlias', 'plugin': 'StateAlias', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}, + {'target': {'const': 'Test State'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test State'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test StateAlias'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test StateAlias'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}, + {'sender': {'const': 'Test State'}, + 'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'sender': {'const': 'Test State'}, + 'state': {'type': 'boolean'}}]} + test(): {'sender': 'test()', 'target': 'Test State', + 'command': 'get state'} + test(): {'sender': 'Test State', 'state': False} + test(): {'sender': 'Test StateAlias', 'state': False} + test(): {'sender': 'test()', 'target': 'Test StateAlias', + 'command': 'set state', 'new state': True} + test(): {'sender': 'Test StateAlias', 'target': 'Test State', + 'command': 'set state', 'new state': True} + test(): {'sender': 'Test State', 'event': 'changed', 'state': True} + test(): {'sender': 'Test StateAlias', 'event': 'changed', 'state': True} + test(): {'sender': 'test()', 'target': 'Test State', + 'command': 'set state', 'new state': True} + test(): {'sender': 'Test State', 'state': True} + test(): {'sender': 'Test StateAlias', 'state': True} + test(): {'sender': 'test()', 'target': 'Test StateAlias', + 'command': 'get state'} + test(): {'sender': 'Test StateAlias', 'target': 'Test State', + 'command': 'get state'} + test(): {'sender': 'Test State', 'state': True} + test(): {'sender': 'Test StateAlias', 'state': True} + """ + + CONF_SCHEMA = {'properties': {'alias for': {'type': 'string'}}, + 'required': ['alias for']} + """Schema for StateAlias plugin configuration. + + Required configuration key: + + - 'alias for': name of aliased state. + """ + + async def receive(self, message: Message) -> None: + """Translate states from and commands to aliased state.""" + alias_message = Message(self.name) + if ('target' in message and message['target'] == self.name and + 'command' in message): + alias_message['target'] = self.conf['alias for'] + if message['command'] == 'get state': + alias_message['command'] = 'get state' + await self.bus.send(alias_message) + elif (message['command'] == 'set state' and + 'new state' in message): + alias_message['command'] = 'set state' + alias_message['new state'] = message['new state'] + await self.bus.send(alias_message) + if (message['sender'] == self.conf['alias for'] and + 'state' in message): + if 'event' in message and message['event'] == 'changed': + alias_message['event'] = 'changed' + alias_message['state'] = message['state'] + await self.bus.send(alias_message) + + def process_conf(self) -> None: + """Register plugin as bus client.""" + sends = [MessageTemplate({'target': {'const': self.conf['alias for']}, + 'command': {'const': 'get state'}}), + MessageTemplate({'target': {'const': self.conf['alias for']}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}), + MessageTemplate({'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}), + MessageTemplate({'state': {'type': 'boolean'}})] + receives = [MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'get state'}}), + MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}), + MessageTemplate({'sender': + {'const': self.conf['alias for']}, + 'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}), + MessageTemplate({'sender': + {'const': self.conf['alias for']}, + 'state': {'type': 'boolean'}})] + self.bus.register(self.name, 'StateAlias', + sends, receives, self.receive) + + async def run(self) -> None: + """Run no code proactively.""" + pass + + +class AndState(BasePlugin): + """Implement conjunction of states. + + The "states" configuration key gets an array of states to be combined. + An AndState plugin client reacts to "get state" commands and sends + "changed" events when a change in one of the combined states leads to + a change for the conjunction: + >>> import asyncio + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test State 1": {"plugin": "State"}, + ... "Test State 2": {"plugin": "State"}, + ... "Test AndState": {"plugin": "AndState", + ... "states": ["Test State 1", "Test State 2"]}}, + ... [{"target": "Test State 1", "command": "set state", + ... "new state": True}, + ... {"target": "Test State 2", "command": "set state", + ... "new state": True}, + ... {"target": "Test State 1", "command": "set state", + ... "new state": False}, + ... {"target": "Test AndState", "command": "get state"}])) + ... # doctest: +NORMALIZE_WHITESPACE + test(): {'sender': '', 'event': 'registered', + 'client': 'Test State 1', 'plugin': 'State', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test State 1'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test State 1'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}]} + test(): {'sender': '', 'event': 'registered', + 'client': 'Test State 2', 'plugin': 'State', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test State 2'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test State 2'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}]} + test(): {'sender': '', 'event': 'registered', + 'client': 'Test AndState', 'plugin': 'AndState', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test AndState'}, + 'command': {'const': 'get state'}}, + {'sender': {'const': 'Test State 1'}, + 'state': {'type': 'boolean'}}, + {'sender': {'const': 'Test State 2'}, + 'state': {'type': 'boolean'}}]} + test(): {'sender': 'test()', 'target': 'Test State 1', + 'command': 'set state', 'new state': True} + test(): {'sender': 'Test State 1', 'event': 'changed', 'state': True} + test(): {'sender': 'test()', 'target': 'Test State 2', + 'command': 'set state', 'new state': True} + test(): {'sender': 'Test State 2', 'event': 'changed', 'state': True} + test(): {'sender': 'Test AndState', 'event': 'changed', 'state': True} + test(): {'sender': 'test()', 'target': 'Test State 1', + 'command': 'set state', 'new state': False} + test(): {'sender': 'Test State 1', 'event': 'changed', 'state': False} + test(): {'sender': 'Test AndState', 'event': 'changed', 'state': False} + test(): {'sender': 'test()', 'target': 'Test AndState', + 'command': 'get state'} + test(): {'sender': 'Test AndState', 'state': False} + """ + + CONF_SCHEMA = {'properties': {'states': {'type': 'array', + 'items': {'type': 'string'}}}, + 'required': ['states']} + """Schema for AndState plugin configuration. + + Required configuration key: + + - 'states': list of names of combined states. + """ + + async def receive(self, message: Message) -> None: + """.""" + if ('target' in message and message['target'] == self.name and + 'command' in message and message['command'] == 'get state'): + await self.bus.send(Message(self.name, {'state': self.state})) + if 'state' in message and message['sender'] in self.conf['states']: + assert isinstance(message['sender'], str) + assert isinstance(message['state'], bool) + self.states[message['sender']] = message['state'] + new_state = all(self.states.values()) + if self.state != new_state: + self.state: bool = new_state + await self.bus.send(Message(self.name, + {'event': 'changed', + 'state': self.state})) + + def process_conf(self) -> None: + """Register plugin as bus client.""" + sends = [MessageTemplate({'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}), + MessageTemplate({'state': {'type': 'boolean'}})] + receives = [MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'get state'}})] + self.states: Dict[str, bool] = {} + for state in self.conf['states']: + receives.append(MessageTemplate({'sender': {'const': state}, + 'state': {'type': 'boolean'}})) + self.states[state] = False + self.state = all(self.states.values()) + self.bus.register(self.name, 'AndState', + sends, receives, self.receive) + + async def run(self) -> None: + """Run no code proactively.""" + pass + + +class OrState(BasePlugin): + """Implement disjunction of states. + + The "states" configuration key gets an array of states to be combined. + An OrState plugin client reacts to "get state" commands and sends + "changed" events when a change in one of the combined states leads to + a change for the disjunction: + >>> import asyncio + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test State 1": {"plugin": "State"}, + ... "Test State 2": {"plugin": "State"}, + ... "Test OrState": {"plugin": "OrState", + ... "states": ["Test State 1", "Test State 2"]}}, + ... [{"target": "Test State 1", "command": "set state", + ... "new state": True}, + ... {"target": "Test State 2", "command": "set state", + ... "new state": True}, + ... {"target": "Test State 1", "command": "set state", + ... "new state": False}, + ... {"target": "Test OrState", "command": "get state"}])) + ... # doctest: +NORMALIZE_WHITESPACE + test(): {'sender': '', 'event': 'registered', + 'client': 'Test State 1', 'plugin': 'State', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test State 1'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test State 1'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}]} + test(): {'sender': '', 'event': 'registered', + 'client': 'Test State 2', 'plugin': 'State', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test State 2'}, + 'command': {'const': 'get state'}}, + {'target': {'const': 'Test State 2'}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}}]} + test(): {'sender': '', 'event': 'registered', + 'client': 'Test OrState', 'plugin': 'OrState', + 'sends': [{'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], + 'receives': [{'target': {'const': 'Test OrState'}, + 'command': {'const': 'get state'}}, + {'sender': {'const': 'Test State 1'}, + 'state': {'type': 'boolean'}}, + {'sender': {'const': 'Test State 2'}, + 'state': {'type': 'boolean'}}]} + test(): {'sender': 'test()', 'target': 'Test State 1', + 'command': 'set state', 'new state': True} + test(): {'sender': 'Test State 1', 'event': 'changed', 'state': True} + test(): {'sender': 'Test OrState', 'event': 'changed', 'state': True} + test(): {'sender': 'test()', 'target': 'Test State 2', + 'command': 'set state', 'new state': True} + test(): {'sender': 'Test State 2', 'event': 'changed', 'state': True} + test(): {'sender': 'test()', 'target': 'Test State 1', + 'command': 'set state', 'new state': False} + test(): {'sender': 'Test State 1', 'event': 'changed', 'state': False} + test(): {'sender': 'test()', 'target': 'Test OrState', + 'command': 'get state'} + test(): {'sender': 'Test OrState', 'state': True} + """ + + CONF_SCHEMA = {'properties': {'states': {'type': 'array', + 'items': {'type': 'string'}}}, + 'required': ['states']} + """Schema for OrState plugin configuration. + + Required configuration key: + + - 'states': list of names of combined states. + """ + + async def receive(self, message: Message) -> None: + """.""" + if ('target' in message and message['target'] == self.name and + 'command' in message and message['command'] == 'get state'): + await self.bus.send(Message(self.name, {'state': self.state})) + if 'state' in message and message['sender'] in self.conf['states']: + assert isinstance(message['sender'], str) + assert isinstance(message['state'], bool) + self.states[message['sender']] = message['state'] + new_state = any(self.states.values()) + if self.state != new_state: + self.state: bool = new_state + await self.bus.send(Message(self.name, + {'event': 'changed', + 'state': self.state})) + + def process_conf(self) -> None: + """Register plugin as bus client.""" + sends = [MessageTemplate({'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}), + MessageTemplate({'state': {'type': 'boolean'}})] + receives = [MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'get state'}})] + self.states: Dict[str, bool] = {} + for state in self.conf['states']: + receives.append(MessageTemplate({'sender': {'const': state}, + 'state': {'type': 'boolean'}})) + self.states[state] = False + self.state = any(self.states.values()) + self.bus.register(self.name, 'OrState', + sends, receives, self.receive) + + async def run(self) -> None: + """Run no code proactively.""" + pass diff --git a/controlpi_plugins/util.py b/controlpi_plugins/util.py new file mode 100644 index 0000000..313e50b --- /dev/null +++ b/controlpi_plugins/util.py @@ -0,0 +1,397 @@ +"""Provide utility plugins for all kinds of systems. + +- Log logs messages on stdout. +- Init sends list of messages on startup and on demand. +- Alias translates messages to an alias. + +>>> import controlpi +>>> asyncio.run(controlpi.test( +... {"Test Log": {"plugin": "Log", +... "filter": [{"sender": {"const": "Test Alias"}}]}, +... "Test Init": {"plugin": "Init", +... "messages": [{"id": 42, "content": "Test Message"}]}, +... "Test Alias": {"plugin": "Alias", +... "from": {"sender": {"const": "Test Init"}, +... "id": {"const": 42}}, +... "to": {"id": "translated"}}}, [])) +... # doctest: +NORMALIZE_WHITESPACE +test(): {'sender': '', 'event': 'registered', + 'client': 'Test Log', 'plugin': 'Log', + 'sends': [], 'receives': [{'sender': {'const': 'Test Alias'}}]} +test(): {'sender': '', 'event': 'registered', + 'client': 'Test Init', 'plugin': 'Init', + 'sends': [{'id': {'const': 42}, + 'content': {'const': 'Test Message'}}], + 'receives': [{'target': {'const': 'Test Init'}, + 'command': {'const': 'execute'}}]} +test(): {'sender': '', 'event': 'registered', + 'client': 'Test Alias', 'plugin': 'Alias', + 'sends': [{'id': {'const': 'translated'}}], + 'receives': [{'sender': {'const': 'Test Init'}, + 'id': {'const': 42}}]} +test(): {'sender': 'Test Init', 'id': 42, + 'content': 'Test Message'} +test(): {'sender': 'Test Alias', 'id': 'translated', + 'content': 'Test Message'} +Test Log: {'sender': 'Test Alias', 'id': 'translated', + 'content': 'Test Message'} +""" +import asyncio + +from controlpi import BasePlugin, Message, MessageTemplate + + +class Log(BasePlugin): + """Log messages on stdout. + + The "filter" configuration key gets a list of message templates defining + the messages that should be logged by the plugin instance. + + In the following example the first and third message match the given + template and are logged by the instance "Test Log", while the second + message does not match and is only logged by the test, but not by the + Log instance: + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test Log": {"plugin": "Log", + ... "filter": [{"id": {"const": 42}}]}}, + ... [{"id": 42, "message": "Test Message"}, + ... {"id": 42.42, "message": "Second Message"}, + ... {"id": 42, "message": "Third Message"}])) + ... # doctest: +NORMALIZE_WHITESPACE + test(): {'sender': '', 'event': 'registered', + 'client': 'Test Log', 'plugin': 'Log', + 'sends': [], 'receives': [{'id': {'const': 42}}]} + test(): {'sender': 'test()', 'id': 42, 'message': 'Test Message'} + Test Log: {'sender': 'test()', 'id': 42, 'message': 'Test Message'} + test(): {'sender': 'test()', 'id': 42.42, 'message': 'Second Message'} + test(): {'sender': 'test()', 'id': 42, 'message': 'Third Message'} + Test Log: {'sender': 'test()', 'id': 42, 'message': 'Third Message'} + + The "filter" key is required: + >>> asyncio.run(controlpi.test( + ... {"Test Log": {"plugin": "Log"}}, [])) + 'filter' is a required property + + Failed validating 'required' in schema: + {'properties': {'filter': {'items': {'type': 'object'}, + 'type': 'array'}}, + 'required': ['filter']} + + On instance: + {'plugin': 'Log'} + Configuration for 'Test Log' is not valid. + + The "filter" key has to contain a list of message templates, i.e., + JSON objects: + >>> asyncio.run(controlpi.test( + ... {"Test Log": {"plugin": "Log", + ... "filter": [42]}}, [])) + 42 is not of type 'object' + + Failed validating 'type' in schema['properties']['filter']['items']: + {'type': 'object'} + + On instance['filter'][0]: + 42 + Configuration for 'Test Log' is not valid. + """ + + CONF_SCHEMA = {'properties': {'filter': {'type': 'array', + 'items': {'type': 'object'}}}, + 'required': ['filter']} + """Schema for Log plugin configuration. + + Required configuration key: + + - 'filter': list of message templates to be logged. + """ + + async def log(self, message: Message) -> None: + """Log received message on stdout using own name as prefix.""" + print(f"{self.name}: {message}") + + def process_conf(self) -> None: + """Register plugin as bus client.""" + self.bus.register(self.name, 'Log', [], self.conf['filter'], self.log) + + async def run(self) -> None: + """Run no code proactively.""" + pass + + +class Init(BasePlugin): + """Send list of messages on startup and on demand. + + The "messages" configuration key gets a list of messages to be sent on + startup. The same list is sent in reaction to a message with + "target": and "command": "execute". + + In the example, the two configured messages are sent twice, once at + startup and a second time in reaction to the "execute" command sent by + the test: + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test Init": {"plugin": "Init", + ... "messages": [{"id": 42, + ... "content": "Test Message"}, + ... {"id": 42.42, + ... "content": "Second Message"}]}}, + ... [{"target": "Test Init", "command": "execute"}])) + ... # doctest: +NORMALIZE_WHITESPACE + test(): {'sender': '', 'event': 'registered', + 'client': 'Test Init', 'plugin': 'Init', + 'sends': [{'id': {'const': 42}, + 'content': {'const': 'Test Message'}}, + {'id': {'const': 42.42}, + 'content': {'const': 'Second Message'}}], + 'receives': [{'target': {'const': 'Test Init'}, + 'command': {'const': 'execute'}}]} + test(): {'sender': 'Test Init', 'id': 42, 'content': 'Test Message'} + test(): {'sender': 'Test Init', 'id': 42.42, 'content': 'Second Message'} + test(): {'sender': 'test()', 'target': 'Test Init', 'command': 'execute'} + test(): {'sender': 'Test Init', 'id': 42, 'content': 'Test Message'} + test(): {'sender': 'Test Init', 'id': 42.42, 'content': 'Second Message'} + + The "messages" key is required: + >>> asyncio.run(controlpi.test( + ... {"Test Init": {"plugin": "Init"}}, [])) + 'messages' is a required property + + Failed validating 'required' in schema: + {'properties': {'messages': {'items': {'type': 'object'}, + 'type': 'array'}}, + 'required': ['messages']} + + On instance: + {'plugin': 'Init'} + Configuration for 'Test Init' is not valid. + + The "messages" key has to contain a list of (partial) messages, i.e., + JSON objects: + >>> asyncio.run(controlpi.test( + ... {"Test Init": {"plugin": "Init", + ... "messages": [42]}}, [])) + 42 is not of type 'object' + + Failed validating 'type' in schema['properties']['messages']['items']: + {'type': 'object'} + + On instance['messages'][0]: + 42 + Configuration for 'Test Init' is not valid. + """ + + CONF_SCHEMA = {'properties': {'messages': {'type': 'array', + 'items': {'type': 'object'}}}, + 'required': ['messages']} + """Schema for Init plugin configuration. + + Required configuration key: + + - 'messages': list of messages to be sent. + """ + + async def execute(self, message: Message) -> None: + """Send configured messages.""" + for message in self.conf['messages']: + await self.bus.send(Message(self.name, message)) + # Give immediate reactions to messages opportunity to happen: + await asyncio.sleep(0) + + def process_conf(self) -> None: + """Register plugin as bus client.""" + receives = [MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'execute'}})] + sends = [MessageTemplate.from_message(message) + for message in self.conf['messages']] + self.bus.register(self.name, 'Init', sends, receives, self.execute) + + async def run(self) -> None: + """Send configured messages on startup.""" + for message in self.conf['messages']: + await self.bus.send(Message(self.name, message)) + + +class Execute(BasePlugin): + """Send configurable list of messages on demand. + + An Execute plugin instance receives two kinds of commands. + The "set messages" command has a "messages" key with a list of (partial) + messages, which are sent by the Execute instance in reaction to an + "execute" command. + + In the example, the first command sent by the test sets two messages, + which are then sent in reaction to the second command sent by the test: + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test Execute": {"plugin": "Execute"}}, + ... [{"target": "Test Execute", "command": "set messages", + ... "messages": [{"id": 42, "content": "Test Message"}, + ... {"id": 42.42, "content": "Second Message"}]}, + ... {"target": "Test Execute", "command": "execute"}])) + ... # doctest: +NORMALIZE_WHITESPACE + test(): {'sender': '', 'event': 'registered', + 'client': 'Test Execute', 'plugin': 'Execute', + 'sends': [{}], + 'receives': [{'target': {'const': 'Test Execute'}, + 'command': {'const': 'set messages'}, + 'messages': {'type': 'array', + 'items': {'type': 'object'}}}, + {'target': {'const': 'Test Execute'}, + 'command': {'const': 'execute'}}]} + test(): {'sender': 'test()', 'target': 'Test Execute', + 'command': 'set messages', + 'messages': [{'id': 42, 'content': 'Test Message'}, + {'id': 42.42, 'content': 'Second Message'}]} + test(): {'sender': 'test()', 'target': 'Test Execute', + 'command': 'execute'} + test(): {'sender': 'Test Execute', 'id': 42, + 'content': 'Test Message'} + test(): {'sender': 'Test Execute', 'id': 42.42, + 'content': 'Second Message'} + """ + + CONF_SCHEMA = True + """Schema for Execute plugin configuration. + + There are no required or optional configuration keys. + """ + + async def execute(self, message: Message) -> None: + """Set or send configured messages.""" + if message['command'] == 'set messages': + assert isinstance(message['messages'], list) + self.messages = list(message['messages']) + elif message['command'] == 'execute': + for message in self.messages: + await self.bus.send(Message(self.name, message)) + # Give immediate reactions to messages opportunity to happen: + await asyncio.sleep(0) + + def process_conf(self) -> None: + """Register plugin as bus client.""" + self.messages = [] + receives = [MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'set messages'}, + 'messages': + {'type': 'array', + 'items': {'type': 'object'}}}), + MessageTemplate({'target': {'const': self.name}, + 'command': {'const': 'execute'}})] + sends = [MessageTemplate()] + self.bus.register(self.name, 'Execute', sends, receives, self.execute) + + async def run(self) -> None: + """Run no code proactively.""" + pass + + +class Alias(BasePlugin): + """Translate messages to an alias. + + The "from" configuration key gets a message template and the + configuration key "to" a (partial) message. All messages matching the + template are received by the Alias instance and a message translated by + removing the keys of the "from" template and adding the keys and values + of the "to" message is sent. Keys that are neither in "from" nor in "to" + are retained. + + In the example, the two messages sent by the test are translated by the + Alias instance and the translated messages are sent by it preserving + the "content" keys: + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test Alias": {"plugin": "Alias", + ... "from": {"id": {"const": 42}}, + ... "to": {"id": "translated"}}}, + ... [{"id": 42, "content": "Test Message"}, + ... {"id": 42, "content": "Second Message"}])) + ... # doctest: +NORMALIZE_WHITESPACE + test(): {'sender': '', 'event': 'registered', + 'client': 'Test Alias', 'plugin': 'Alias', + 'sends': [{'id': {'const': 'translated'}}], + 'receives': [{'id': {'const': 42}}]} + test(): {'sender': 'test()', 'id': 42, + 'content': 'Test Message'} + test(): {'sender': 'Test Alias', 'id': 'translated', + 'content': 'Test Message'} + test(): {'sender': 'test()', 'id': 42, + 'content': 'Second Message'} + test(): {'sender': 'Test Alias', 'id': 'translated', + 'content': 'Second Message'} + + The "from" and "to" keys are required: + >>> asyncio.run(controlpi.test( + ... {"Test Alias": {"plugin": "Alias"}}, [])) + 'from' is a required property + + Failed validating 'required' in schema: + {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}}, + 'required': ['from', 'to']} + + On instance: + {'plugin': 'Alias'} + 'to' is a required property + + Failed validating 'required' in schema: + {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}}, + 'required': ['from', 'to']} + + On instance: + {'plugin': 'Alias'} + Configuration for 'Test Alias' is not valid. + + The "from" key has to contain a message template and the "to" key a + (partial) message, i.e., both have to be JSON objects: + >>> asyncio.run(controlpi.test( + ... {"Test Alias": {"plugin": "Alias", + ... "from": 42, + ... "to": 42}}, [])) + 42 is not of type 'object' + + Failed validating 'type' in schema['properties']['from']: + {'type': 'object'} + + On instance['from']: + 42 + 42 is not of type 'object' + + Failed validating 'type' in schema['properties']['to']: + {'type': 'object'} + + On instance['to']: + 42 + Configuration for 'Test Alias' is not valid. + """ + + CONF_SCHEMA = {'properties': {'from': {'type': 'object'}, + 'to': {'type': 'object'}}, + 'required': ['from', 'to']} + """Schema for Alias plugin configuration. + + Required configuration keys: + + - 'from': template of messages to be translated. + - 'to': translated message to be sent. + """ + + async def alias(self, message: Message) -> None: + """Translate and send message.""" + alias_message = Message(self.name) + alias_message.update(self.conf['to']) + for key in message: + if key != 'sender' and key not in self.conf['from']: + alias_message[key] = message[key] + await self.bus.send(alias_message) + + def process_conf(self) -> None: + """Register plugin as bus client.""" + self.bus.register(self.name, 'Alias', + [MessageTemplate.from_message(self.conf['to'])], + [self.conf['from']], + self.alias) + + async def run(self) -> None: + """Run no code proactively.""" + pass diff --git a/controlpi_plugins/wait.py b/controlpi_plugins/wait.py new file mode 100644 index 0000000..afe5431 --- /dev/null +++ b/controlpi_plugins/wait.py @@ -0,0 +1,44 @@ +"""Provide waiting/sleeping plugins for all kinds of systems. + +TODO: documentation, doctests +""" +import asyncio + +from controlpi import BasePlugin, Message + + +class Wait(BasePlugin): + CONF_SCHEMA = {'properties': {'seconds': {'type': 'number'}}, + 'required': ['seconds']} + + async def wait(self, message: Message) -> None: + await asyncio.sleep(self.conf['seconds']) + await self.bus.send({'sender': self.name, 'event': 'finished'}) + + def process_conf(self) -> None: + receives = [{'target': {'const': self.name}, + 'command': {'const': 'wait'}}] + sends = [{'event': {'const': 'finished'}}] + self.bus.register(self.name, 'Wait', sends, receives, self.wait) + + async def run(self) -> None: + pass + + +class GenericWait(BasePlugin): + CONF_SCHEMA = True + + async def wait(self, message: Message) -> None: + await asyncio.sleep(message['seconds']) + await self.bus.send(Message(self.name, {'id': message['id']})) + + def process_conf(self) -> None: + receives = [{'target': {'const': self.name}, + 'command': {'const': 'wait'}, + 'seconds': {'type': 'number'}, + 'id': {'type': 'string'}}] + sends = [{'id': {'type': 'string'}}] + self.bus.register(self.name, 'GenericWait', sends, receives, self.wait) + + async def run(self) -> None: + pass