From: Benjamin Braatz Date: Wed, 8 Sep 2021 09:14:43 +0000 (+0200) Subject: Add AndSet and OrSet. X-Git-Tag: v0.3.0~19 X-Git-Url: http://git.graph-it.com/?a=commitdiff_plain;h=79dd8dd810d32e6a727c567d61c38d2a8e16f383;p=graphit%2Fcontrolpi.git Add AndSet and OrSet. --- diff --git a/controlpi_plugins/state.py b/controlpi_plugins/state.py index ae4ab59..75fd561 100644 --- a/controlpi_plugins/state.py +++ b/controlpi_plugins/state.py @@ -4,6 +4,8 @@ - StateAlias translates to another state-like client. - AndState combines several state-like clients by conjunction. - OrState combines several state-like clients by disjunction. +- AndSet sets a state due to a conjunction of other state-like clients. +- OrSet sets a state due to a disjunction of other state-like clients. All these plugins use the following conventions: @@ -31,12 +33,20 @@ All these plugins use the following conventions: >>> asyncio.run(controlpi.test( ... {"Test State": {"plugin": "State"}, ... "Test State 2": {"plugin": "State"}, +... "Test State 3": {"plugin": "State"}, +... "Test State 4": {"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"]}}, +... "states": ["Test State", "Test StateAlias"]}, +... "Test AndSet": {"plugin": "AndSet", +... "input states": ["Test State", "Test StateAlias"], +... "output state": "Test State 3"}, +... "Test OrSet": {"plugin": "OrSet", +... "input states": ["Test State", "Test StateAlias"], +... "output state": "Test State 4"}}, ... [{"target": "Test AndState", ... "command": "get state"}, ... {"target": "Test OrState", @@ -47,79 +57,19 @@ All these plugins use the following conventions: ... "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'} +... # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS +test(): {'sender': '', 'event': 'registered', ... +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()', '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 OrSet', 'target': 'Test State 4', + 'command': 'set state', 'new state': True} +test(): {'sender': 'Test State 4', 'event': 'changed', 'state': True} test(): {'sender': 'test()', 'target': 'Test StateAlias', 'command': 'set state', 'new state': True} test(): {'sender': 'Test StateAlias', 'target': 'Test State 2', @@ -127,10 +77,16 @@ test(): {'sender': 'Test StateAlias', 'target': 'Test State 2', 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 AndSet', 'target': 'Test State 3', + 'command': 'set state', 'new state': True} +test(): {'sender': 'Test State 3', '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} +test(): {'sender': 'Test AndSet', 'target': 'Test State 3', + 'command': 'set state', 'new state': False} +test(): {'sender': 'Test State 3', 'event': 'changed', 'state': False} """ from controlpi import BasePlugin, Message, MessageTemplate @@ -251,14 +207,14 @@ class StateAlias(BasePlugin): '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'}, + 'sends': [{'target': {'const': 'Test State'}, 'command': {'const': 'get state'}}, {'target': {'const': 'Test State'}, 'command': {'const': 'set state'}, - 'new state': {'type': 'boolean'}}], + 'new state': {'type': 'boolean'}}, + {'event': {'const': 'changed'}, + 'state': {'type': 'boolean'}}, + {'state': {'type': 'boolean'}}], 'receives': [{'target': {'const': 'Test StateAlias'}, 'command': {'const': 'get state'}}, {'target': {'const': 'Test StateAlias'}, @@ -352,7 +308,7 @@ class StateAlias(BasePlugin): class AndState(BasePlugin): - """Implement conjunction of states. + """Define 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 @@ -468,7 +424,7 @@ class AndState(BasePlugin): class OrState(BasePlugin): - """Implement disjunction of states. + """Define 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 @@ -580,3 +536,174 @@ class OrState(BasePlugin): async def run(self) -> None: """Run no code proactively.""" pass + + +class AndSet(BasePlugin): + """Set state based on conjunction of other states. + + The "input states" configuration key gets an array of states used to + determine the state in the "output state" configuration key: + >>> import asyncio + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test State 1": {"plugin": "State"}, + ... "Test State 2": {"plugin": "State"}, + ... "Test State 3": {"plugin": "State"}, + ... "Test AndSet": {"plugin": "AndSet", + ... "input states": ["Test State 1", + ... "Test State 2"], + ... "output state": "Test State 3"}}, + ... [{"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}])) + ... # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS + test(): {'sender': '', 'event': 'registered', ... + 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 AndSet', 'target': 'Test State 3', + 'command': 'set state', 'new state': True} + test(): {'sender': 'Test State 3', '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 AndSet', 'target': 'Test State 3', + 'command': 'set state', 'new state': False} + test(): {'sender': 'Test State 3', 'event': 'changed', 'state': False} + """ + + CONF_SCHEMA = {'properties': {'input states': {'type': 'array', + 'items': {'type': + 'string'}}, + 'output state': {'type': 'string'}}, + 'required': ['input states', 'output state']} + """Schema for AndSet plugin configuration. + + Required configuration keys: + + - 'input states': list of names of combined states. + - 'output state': name of state to be set. + """ + + def process_conf(self) -> None: + """Register plugin as bus client.""" + sends = [MessageTemplate({'target': {'const': + self.conf['output state']}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}})] + receives = [] + self.states: Dict[str, bool] = {} + for state in self.conf['input states']: + receives.append(MessageTemplate({'sender': {'const': state}, + 'state': {'type': 'boolean'}})) + self.states[state] = False + self.state: bool = all(self.states.values()) + self.bus.register(self.name, 'AndSet', + sends, receives, self.receive) + + async def receive(self, message: Message) -> None: + """Process messages of combined 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 = new_state + await self.bus.send(Message(self.name, + {'target': self.conf['output state'], + 'command': 'set state', + 'new state': self.state})) + + async def run(self) -> None: + """Run no code proactively.""" + pass + + +class OrSet(BasePlugin): + """Set state based on disjunction of other states. + + The "input states" configuration key gets an array of states used to + determine the state in the "output state" configuration key: + >>> import asyncio + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test State 1": {"plugin": "State"}, + ... "Test State 2": {"plugin": "State"}, + ... "Test State 3": {"plugin": "State"}, + ... "Test OrSet": {"plugin": "OrSet", + ... "input states": ["Test State 1", + ... "Test State 2"], + ... "output state": "Test State 3"}}, + ... [{"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}])) + ... # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS + test(): {'sender': '', 'event': 'registered', ... + 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 OrSet', 'target': 'Test State 3', + 'command': 'set state', 'new state': True} + test(): {'sender': 'Test State 3', '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} + """ + + CONF_SCHEMA = {'properties': {'input states': {'type': 'array', + 'items': {'type': + 'string'}}, + 'output state': {'type': 'string'}}, + 'required': ['input states', 'output state']} + """Schema for OrSet plugin configuration. + + Required configuration keys: + + - 'input states': list of names of combined states. + - 'output state': name of state to be set. + """ + + def process_conf(self) -> None: + """Register plugin as bus client.""" + sends = [MessageTemplate({'target': {'const': + self.conf['output state']}, + 'command': {'const': 'set state'}, + 'new state': {'type': 'boolean'}})] + receives = [] + self.states: Dict[str, bool] = {} + for state in self.conf['input states']: + receives.append(MessageTemplate({'sender': {'const': state}, + 'state': {'type': 'boolean'}})) + self.states[state] = False + self.state: bool = any(self.states.values()) + self.bus.register(self.name, 'OrSet', + sends, receives, self.receive) + + async def receive(self, message: Message) -> None: + """Process messages of combined 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 = new_state + await self.bus.send(Message(self.name, + {'target': self.conf['output state'], + 'command': 'set state', + 'new state': self.state})) + + async def run(self) -> None: + """Run no code proactively.""" + pass