+++ /dev/null
-"""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": <new state>.
-- If their state is reported due to a message, but did not change they send
- a message containing just "state": <current state>.
-- If they receive a message containing "target": <name> and
- "command": "get state" they report their current state.
-- If State (or any other settable state using these conventions) receives
- a message containing "target": <name>, "command": "set state" and
- "new state": <state to be set> 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
+++ /dev/null
-"""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
- <BLANKLINE>
- Failed validating 'required' in schema:
- {'properties': {'filter': {'items': {'type': 'object'},
- 'type': 'array'}},
- 'required': ['filter']}
- <BLANKLINE>
- 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'
- <BLANKLINE>
- Failed validating 'type' in schema['properties']['filter']['items']:
- {'type': 'object'}
- <BLANKLINE>
- 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": <name> 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
- <BLANKLINE>
- Failed validating 'required' in schema:
- {'properties': {'messages': {'items': {'type': 'object'},
- 'type': 'array'}},
- 'required': ['messages']}
- <BLANKLINE>
- 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'
- <BLANKLINE>
- Failed validating 'type' in schema['properties']['messages']['items']:
- {'type': 'object'}
- <BLANKLINE>
- 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
- <BLANKLINE>
- Failed validating 'required' in schema:
- {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}},
- 'required': ['from', 'to']}
- <BLANKLINE>
- On instance:
- {'plugin': 'Alias'}
- 'to' is a required property
- <BLANKLINE>
- Failed validating 'required' in schema:
- {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}},
- 'required': ['from', 'to']}
- <BLANKLINE>
- 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'
- <BLANKLINE>
- Failed validating 'type' in schema['properties']['from']:
- {'type': 'object'}
- <BLANKLINE>
- On instance['from']:
- 42
- 42 is not of type 'object'
- <BLANKLINE>
- Failed validating 'type' in schema['properties']['to']:
- {'type': 'object'}
- <BLANKLINE>
- 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
+++ /dev/null
-"""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
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]
--- /dev/null
+"""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": <new state>.
+- If their state is reported due to a message, but did not change they send
+ a message containing just "state": <current state>.
+- If they receive a message containing "target": <name> and
+ "command": "get state" they report their current state.
+- If State (or any other settable state using these conventions) receives
+ a message containing "target": <name>, "command": "set state" and
+ "new state": <state to be set> 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
--- /dev/null
+"""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
+ <BLANKLINE>
+ Failed validating 'required' in schema:
+ {'properties': {'filter': {'items': {'type': 'object'},
+ 'type': 'array'}},
+ 'required': ['filter']}
+ <BLANKLINE>
+ 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'
+ <BLANKLINE>
+ Failed validating 'type' in schema['properties']['filter']['items']:
+ {'type': 'object'}
+ <BLANKLINE>
+ 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": <name> 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
+ <BLANKLINE>
+ Failed validating 'required' in schema:
+ {'properties': {'messages': {'items': {'type': 'object'},
+ 'type': 'array'}},
+ 'required': ['messages']}
+ <BLANKLINE>
+ 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'
+ <BLANKLINE>
+ Failed validating 'type' in schema['properties']['messages']['items']:
+ {'type': 'object'}
+ <BLANKLINE>
+ 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
+ <BLANKLINE>
+ Failed validating 'required' in schema:
+ {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}},
+ 'required': ['from', 'to']}
+ <BLANKLINE>
+ On instance:
+ {'plugin': 'Alias'}
+ 'to' is a required property
+ <BLANKLINE>
+ Failed validating 'required' in schema:
+ {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}},
+ 'required': ['from', 'to']}
+ <BLANKLINE>
+ 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'
+ <BLANKLINE>
+ Failed validating 'type' in schema['properties']['from']:
+ {'type': 'object'}
+ <BLANKLINE>
+ On instance['from']:
+ 42
+ 42 is not of type 'object'
+ <BLANKLINE>
+ Failed validating 'type' in schema['properties']['to']:
+ {'type': 'object'}
+ <BLANKLINE>
+ 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
--- /dev/null
+"""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