Add AndSet and OrSet.
authorBenjamin Braatz <benjamin.braatz@graph-it.com>
Wed, 8 Sep 2021 09:14:43 +0000 (11:14 +0200)
committerBenjamin Braatz <benjamin.braatz@graph-it.com>
Wed, 8 Sep 2021 09:14:43 +0000 (11:14 +0200)
controlpi_plugins/state.py

index ae4ab5978bf056b61b27713d07ea92783c788b2d..75fd561fea354c5be7bb0f48feb2c69781025ea4 100644 (file)
@@ -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