Skip to content

Commit b91423b

Browse files
committed
Migrate Triggers
Moved triggers to new extension - `schedule` -> `cron` - `control` -> `control` - `sensor` -> `sensor` - Added `state` trigger type - TriggerGroup is now `GroupTrigger` - Triggers can now be apart of multiple groups - Added `toggle` trigger type
1 parent 0d9dd74 commit b91423b

16 files changed

+819
-5
lines changed

README.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<p align="center"><img alt="MudPi Smart Automation for the Garden & Home" title="MudPi Smart Automation for the Garden & Home" src="https://mudpi.app/img/mudPI_LOGO_small_grad.png" width="200px"></p>
22

33
# MudPi Smart Automation for the Garden & Home
4-
> A python library to gather sensor readings, trigger components, control solenoids and more in an event based system that can be run on a linux SBC, including Raspberry Pi.
4+
> A python package to gather sensor readings, trigger components, control devices and more in an event based system that can be run on a linux SBC, including Raspberry Pi.
55
66

77
## Documentation
@@ -36,7 +36,7 @@ Breaking.Major.Minor
3636
* Discord - [Join](https://discord.gg/daWg2YH)
3737
* [Twitter.com/MudpiApp](https://twitter.com/mudpiapp)
3838

39-
## Hardware Tested On
39+
<!-- ## Hardware Tested On
4040
These are the devices and sensors I tested and used with MudPi successfully. Many sensors are similar so it will work with a range more than what is listed below.
4141
4242
**Devices**
@@ -71,9 +71,10 @@ These are the devices and sensors I tested and used with MudPi successfully. Man
7171
* [LCD 20 x 4 Display I2C](https://www.dfrobot.com/product-590.html)
7272
* [USB to TTL USB 2.0 Serial Module UART](https://www.amazon.com/gp/product/B07CWKHTLH/ref=oh_aui_detailpage_o04_s00?ie=UTF8&psc=1)
7373
74-
Let me know if you are able to confirm tests on any other devices. Note: This is not a complete list.
74+
Let me know if you are able to confirm tests on any other devices. Note: This is not a complete list. -->
7575

76-
Also check out my [custom circuit boards design around MudPi](https://mudpi.app/boards)
76+
## MudPi Hardware
77+
There are [custom circuit boards designed around MudPi available.](https://mudpi.app/boards)
7778

7879
## License
7980
This project is licensed under the BSD-4-Clause License - see the [LICENSE.md](LICENSE.md) file for details

mudpi/extensions/control/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def invert_state(self):
8080
def fire(self):
8181
""" Fire a control event """
8282
event_data = {
83-
'event': 'ControlUpdate',
83+
'event': 'ControlUpdated',
8484
'component_id': self.id,
8585
'type': self.type,
8686
'name': self.name,

mudpi/extensions/control/trigger.py

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""
2+
Control Trigger Interface
3+
Monitors control state changes and
4+
checks new state against any
5+
thresholds if provided.
6+
"""
7+
from mudpi.utils import decode_event_data
8+
from mudpi.exceptions import ConfigError
9+
from mudpi.extensions import BaseInterface
10+
from mudpi.extensions.trigger import Trigger
11+
from mudpi.logger.Logger import Logger, LOG_LEVEL
12+
13+
14+
class Interface(BaseInterface):
15+
16+
def load(self, config):
17+
""" Load control Trigger component from configs """
18+
trigger = ControlTrigger(self.mudpi, config)
19+
if trigger:
20+
self.add_component(trigger)
21+
return True
22+
23+
def validate(self, config):
24+
""" Validate the trigger config """
25+
if not isinstance(config, list):
26+
config = [config]
27+
28+
for conf in config:
29+
if not conf.get('source'):
30+
raise ConfigError('Missing `source` key in Sensor Trigger config.')
31+
32+
return config
33+
34+
35+
class ControlTrigger(Trigger):
36+
""" A trigger that listens to states
37+
and checks for new state that
38+
matches any thresholds.
39+
"""
40+
41+
# Used for onetime subscribe
42+
_listening = False
43+
44+
45+
def init(self):
46+
""" Listen to the state for changes """
47+
super().init()
48+
if self.mudpi.is_prepared:
49+
if not self._listening:
50+
# TODO: Eventually get a handler returned to unsub just this listener
51+
self.mudpi.events.subscribe('control', self.handle_event)
52+
self._listening = True
53+
return True
54+
55+
""" Methods """
56+
def handle_event(self, event):
57+
""" Handle the event data from the event system """
58+
_event_data = decode_event_data(event)
59+
if _event_data.get('event'):
60+
try:
61+
if _event_data['event'] == 'ControlUpdated':
62+
if _event_data['component_id'] == self.source:
63+
_value = self._parse_data(_event_data["state"])
64+
if self.evaluate_thresholds(_value):
65+
self.active = True
66+
if self._previous_state != self.active:
67+
# Trigger is reset, Fire
68+
self.trigger(_event_data)
69+
else:
70+
# Trigger not reset check if its multi fire
71+
if self.frequency == 'many':
72+
self.trigger(_event_data)
73+
else:
74+
self.active = False
75+
except Exception as error:
76+
Logger.log(LOG_LEVEL["error"],
77+
f'Error evaluating thresholds for trigger {self.id}')
78+
Logger.log(LOG_LEVEL["debug"], error)
79+
self._previous_state = self.active
80+
81+
def unload(self):
82+
# Unsubscribe once bus supports single handler unsubscribes
83+
return
84+
85+
def _parse_data(self, data):
86+
""" Get nested data if set otherwise return the data """
87+
if isinstance(data, dict):
88+
print('dict con')
89+
return data if not self.nested_source else data.get(self.nested_source, None)
90+
return data
91+

mudpi/extensions/cron/__init__.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Cron Extension
3+
Cron schedule support for triggers
4+
to allow scheduling.
5+
"""
6+
from mudpi.extensions import BaseExtension
7+
8+
9+
class Extension(BaseExtension):
10+
namespace = 'cron'
11+
update_interval = 1
12+

mudpi/extensions/cron/extension.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "Cron",
3+
"namespace": "cron",
4+
"details": {
5+
"description": "Cron job schedule support for components.",
6+
"documentation": "https://mudpi.app/docs/triggers"
7+
},
8+
"requirements": ["pycron"]
9+
}

mudpi/extensions/cron/trigger.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Cron Trigger Interface
3+
Cron schedule support for triggers
4+
to allow scheduling.
5+
"""
6+
import time
7+
import pycron
8+
from mudpi.exceptions import ConfigError
9+
from mudpi.extensions import BaseInterface
10+
from mudpi.extensions.trigger import Trigger
11+
from mudpi.logger.Logger import Logger, LOG_LEVEL
12+
13+
14+
class Interface(BaseInterface):
15+
16+
def load(self, config):
17+
""" Load cron trigger component from configs """
18+
trigger = CronTrigger(self.mudpi, config)
19+
if trigger:
20+
self.add_component(trigger)
21+
return True
22+
23+
def validate(self, config):
24+
""" Validate the trigger config """
25+
if not isinstance(config, list):
26+
config = [config]
27+
28+
for conf in config:
29+
if not conf.get('schedule'):
30+
Logger.log(
31+
LOG_LEVEL["debug"],
32+
'Trigger: No `schedule`, defaulting to every 5mins'
33+
)
34+
# raise ConfigError('Missing `schedule` in Trigger config.')
35+
36+
return config
37+
38+
39+
class CronTrigger(Trigger):
40+
""" A trigger that resoponds to time
41+
changes based on cron schedule string
42+
"""
43+
44+
""" Properties """
45+
@property
46+
def schedule(self):
47+
""" Cron schedule string to check time against """
48+
return self.config.get('schedule', '*/5 * * * *')
49+
50+
51+
""" Methods """
52+
def init(self):
53+
""" Pass call to parent """
54+
super().init()
55+
56+
def check(self):
57+
""" Check trigger schedule thresholds """
58+
if self.mudpi.is_running:
59+
try:
60+
if pycron.is_now(self.schedule):
61+
if not self.active:
62+
self.trigger()
63+
self.active = True
64+
else:
65+
self.active = False
66+
except Exception as error:
67+
Logger.log(
68+
LOG_LEVEL["error"],
69+
"Error evaluating time trigger schedule."
70+
)
71+
return
72+

mudpi/extensions/group/__init__.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
Group Extension
3+
Allows grouping of components.
4+
"""
5+
from mudpi.extensions import BaseExtension
6+
7+
8+
class Extension(BaseExtension):
9+
namespace = 'group'
10+
update_interval = 0.2
11+

mudpi/extensions/group/extension.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "Groups",
3+
"namespace": "group",
4+
"details": {
5+
"description": "Enables grouping for components (Triggers).",
6+
"documentation": "https://mudpi.app/docs/triggers"
7+
},
8+
"requirements": []
9+
}

mudpi/extensions/group/trigger.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""
2+
Group Trigger Interface
3+
Allows triggers to be grouped
4+
together for complex conditions.
5+
"""
6+
from mudpi.exceptions import ConfigError
7+
from mudpi.extensions import BaseInterface
8+
from mudpi.extensions.trigger import Trigger
9+
from mudpi.logger.Logger import Logger, LOG_LEVEL
10+
11+
12+
class Interface(BaseInterface):
13+
14+
def load(self, config):
15+
""" Load group trigger component from configs """
16+
trigger = GroupTrigger(self.mudpi, config)
17+
if trigger:
18+
self.add_component(trigger)
19+
return True
20+
21+
def validate(self, config):
22+
""" Validate the trigger config """
23+
if not isinstance(config, list):
24+
config = [config]
25+
26+
for conf in config:
27+
if not conf.get('triggers'):
28+
raise ConfigError('Missing `triggers` keys in Trigger Group')
29+
30+
return config
31+
32+
33+
class GroupTrigger(Trigger):
34+
""" A Group to allow complex combintations
35+
between multiple trigger types.
36+
"""
37+
38+
# List of triggers to monitor
39+
_triggers = []
40+
41+
""" Properties """
42+
@property
43+
def triggers(self):
44+
""" Keys of triggers to group """
45+
return self.config.get('triggers', [])
46+
47+
@property
48+
def trigger_states(self):
49+
""" Keys of triggers to group """
50+
return [trigger.active for trigger in self._triggers]
51+
52+
53+
""" Methods """
54+
def init(self):
55+
""" Load in the triggers for the group """
56+
# Doesnt call super().init() because that is for non-groups
57+
self.cache = self.mudpi.cache.get('trigger', {})
58+
self.cache.setdefault('groups', {})[self.id] = self
59+
60+
for _trigger in self.triggers:
61+
_trig = self.cache.get('triggers', {}).get(_trigger)
62+
if _trig:
63+
self.add_trigger(_trig)
64+
return True
65+
66+
def add_trigger(self, trigger):
67+
""" Add a trigger to monitor """
68+
self._triggers.append(trigger)
69+
70+
def check(self):
71+
""" Check if trigger should fire """
72+
if all(self.trigger_states):
73+
self.active = True
74+
if self._previous_state != self.active:
75+
# Trigger is reset, Fire
76+
self.trigger()
77+
else:
78+
# Trigger not reset check if its multi fire
79+
if self.frequency == 'many':
80+
self.trigger()
81+
else:
82+
self.active = False
83+
self._previous_state = self.active

0 commit comments

Comments
 (0)