Skip to content

Commit 25c192d

Browse files
committed
Add real-time support for plugin devices.
1 parent 342d31f commit 25c192d

File tree

3 files changed

+96
-47
lines changed

3 files changed

+96
-47
lines changed

myDevices/devices/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def removeDevice(name):
6868
if name in DEVICES:
6969
if name in DYNAMIC_DEVICES:
7070
if hasattr(DEVICES[name]["device"], 'close'):
71-
DEVICES[name]["device"].close()
71+
DEVICES[name]["device"].close()
7272
del DEVICES[name]
7373
del DYNAMIC_DEVICES[name]
7474
json_devices = getJSON(DYNAMIC_DEVICES)

myDevices/plugins/manager.py

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@
1616

1717

1818
class PluginManager():
19-
"""Loads plugins and reads/writes plugin data"""
19+
"""Loads plugins and reads/writes plugin data."""
2020

21-
def __init__(self):
22-
"""Initializes the plugin manager and loads the plugin list"""
21+
def __init__(self, callback=None):
22+
"""Initializes the plugin manager and loads the plugin list."""
2323
self.plugin_folder = PLUGIN_FOLDER
24+
self.callback = callback
2425
self.plugins = {}
2526
self.load_plugins()
2627

2728
def load_plugin_from_file(self, filename):
28-
"""Loads a plugin from a specified plugin config file and adds it to the plugin list"""
29+
"""Loads a plugin from a specified plugin config file and adds it to the plugin list."""
2930
try:
3031
info('Loading plugin: {}'.format(filename))
3132
loaded = []
@@ -41,8 +42,9 @@ def load_plugin_from_file(self, filename):
4142
'filename': filename,
4243
'section': section,
4344
'channel': config.get(section, 'channel'),
44-
'name': config.get(section, 'name', section)
45+
'name': config.get(section, 'name', section),
4546
}
47+
plugin['id'] = plugin_name + ':' + plugin['channel']
4648
inherit_items = {}
4749
if inherit in config.sections():
4850
if inherit == section:
@@ -70,7 +72,16 @@ def load_plugin_from_file(self, filename):
7072
self.override_plugin_value(config, section, 'write', plugin)
7173
except:
7274
pass
73-
self.plugins[plugin_name + ':' + plugin['channel']] = plugin
75+
try:
76+
self.override_plugin_value(config, section, 'register_callback', plugin)
77+
getattr(plugin['instance'], plugin['register_callback'])(lambda x: self.data_changed(x, plugin))
78+
except:
79+
pass
80+
try:
81+
self.override_plugin_value(config, section, 'unregister_callback', plugin)
82+
except:
83+
pass
84+
self.plugins[plugin['id']] = plugin
7485
loaded.append(section)
7586
except Exception as e:
7687
error(e)
@@ -79,13 +90,13 @@ def load_plugin_from_file(self, filename):
7990
info('Loaded sections: {}'.format(loaded))
8091

8192
def load_plugins(self):
82-
"""Loads plugins from any plugin config files found in the plugin folder"""
93+
"""Loads plugins from any plugin config files found in the plugin folder."""
8394
for root, dirnames, filenames in os.walk(self.plugin_folder):
8495
for filename in fnmatch.filter(filenames, '*.plugin'):
8596
self.load_plugin_from_file(os.path.join(root, filename))
8697

8798
def get_plugin(self, filename, section):
88-
"""Return the plugin for the corresponding filename and section"""
99+
"""Return the plugin for the corresponding filename and section."""
89100
return next(plugin for plugin in self.plugins.values() if plugin['filename'] == filename and plugin['section'] == section)
90101

91102
def override_plugin_value(self, config, section, key, plugin):
@@ -96,7 +107,7 @@ def override_plugin_value(self, config, section, key, plugin):
96107
plugin[key] = config.get(section, key, plugin[key])
97108

98109
def get_plugin_readings(self):
99-
"""Return a list with current readings for all plugins"""
110+
"""Return a list with current readings for all plugins."""
100111
readings = []
101112
for key, plugin in self.plugins.items():
102113
try:
@@ -111,7 +122,7 @@ def get_plugin_readings(self):
111122
return readings
112123

113124
def convert_to_dict(self, value):
114-
"""Convert a tuple value to a dict containing value, type and unit"""
125+
"""Convert a tuple value to a dict containing value, type and unit."""
115126
value_dict = {}
116127
try:
117128
if value is None or value[0] is None:
@@ -123,18 +134,19 @@ def convert_to_dict(self, value):
123134
if not value_dict:
124135
value_dict['value'] = value
125136
if 'type' in value_dict and 'unit' not in value_dict:
126-
if value_dict['type'] == 'digital_actuator':
137+
if value_dict['type'] in ('digital_sensor', 'digital_actuator'):
127138
value_dict['unit'] = 'd'
128-
elif value_dict['type'] == 'analog_actuator':
139+
elif value_dict['type'] in ('analog_sensor', 'analog_actuator'):
129140
value_dict['unit'] = 'null'
130141
return value_dict
131142

132143
def is_plugin(self, plugin, channel=None):
133-
"""Returns True if the specified plugin or plugin:channel are valid plugins"""
144+
"""Returns True if the specified plugin or plugin:channel are valid plugins."""
134145
try:
135146
key = plugin
136147
if channel is not None:
137148
key = plugin + ':' + channel
149+
info('Checking for {} in {}'.format(key, self.plugins.keys()))
138150
return key in self.plugins.keys()
139151
except:
140152
return False
@@ -155,18 +167,37 @@ def write_value(self, plugin, channel, value):
155167
return False
156168
return True
157169

158-
def disable(self, plugin):
159-
"""Disable the specified plugin"""
170+
def disable(self, plugin_id):
171+
"""Disable the specified plugin."""
160172
disabled = False
161173
try:
162-
output, result = executeCommand('sudo python3 -m myDevices.plugins.disable "{}" "{}"'.format(self.plugins[plugin]['filename'], self.plugins[plugin]['section']))
174+
plugin = self.plugins[plugin_id]
175+
output, result = executeCommand('sudo python3 -m myDevices.plugins.disable "{}" "{}"'.format(plugin['filename'], plugin['section']))
163176
if result == 0:
164177
disabled = True
165-
info('Plugin \'{}\' disabled'.format(plugin))
178+
info('Plugin \'{}\' disabled'.format(plugin_id))
166179
else:
167-
info('Plugin \'{}\' not disabled'.format(plugin))
168-
del self.plugins[plugin]
180+
info('Plugin \'{}\' not disabled'.format(plugin_id))
181+
if 'unregister_callback' in plugin:
182+
getattr(plugin['instance'], plugin['unregister_callback'])()
183+
del self.plugins[plugin_id]
169184
except Exception as e:
170185
info(e)
171186
pass
172-
return disabled
187+
return disabled
188+
189+
def register_callback(self, callback):
190+
"""Register the callback to use when plugin data has changed."""
191+
self.callback = callback
192+
193+
def unregister_callback(self):
194+
"""Unregister the callback to use when plugin data has changed."""
195+
self.callback = None
196+
197+
def data_changed(self, value, plugin):
198+
"""Callback that is called when data has changed."""
199+
if self.callback:
200+
data = self.convert_to_dict(value)
201+
data['name'] = plugin['name']
202+
data['id'] = plugin['id']
203+
self.callback(data)

myDevices/sensors/sensors.py

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from myDevices.utils.types import M_JSON
2424

2525
REFRESH_FREQUENCY = 15 #seconds
26-
DIGITAL_FREQUENCY = 60/55 #Seconds/messages, this is done to keep messages under the rate limit
26+
REAL_TIME_FREQUENCY = 60/55 #Seconds/messages, this is done to keep messages under the rate limit
2727

2828
class SensorsClient():
2929
"""Class for interfacing with sensors and actuators"""
@@ -36,8 +36,8 @@ def __init__(self):
3636
self.onDataChanged = None
3737
self.systemData = []
3838
self.currentSystemState = []
39-
self.currentDigitalData = {}
40-
self.queuedDigitalData = {}
39+
self.currentRealTimeData = {}
40+
self.queuedRealTimeData = {}
4141
self.disabledSensors = {}
4242
self.disabledSensorTable = "disabled_sensors"
4343
checkAllBus()
@@ -50,8 +50,8 @@ def __init__(self):
5050
if results:
5151
for row in results:
5252
self.disabledSensors[row[0]] = 1
53-
self.pluginManager = PluginManager()
54-
self.digitalMonitorRunning = False
53+
self.realTimeMonitorRunning = False
54+
self.pluginManager = PluginManager(self.OnPluginChange)
5555
self.InitCallbacks()
5656
self.StartMonitoring()
5757

@@ -72,10 +72,28 @@ def OnSensorChange(self, device, value):
7272
"""
7373
debug('OnSensorChange: {}, {}'.format(device, value))
7474
with self.digitalMutex:
75-
if device['name'] not in self.currentDigitalData:
76-
self.currentDigitalData[device['name']] = {'device': device, 'value': value}
75+
data = {'name': device['description'], 'value': value, 'type': 'digital_sensor', 'unit': 'd'}
76+
if 'args' in device:
77+
data['args'] = device['args']
78+
if device['name'] not in self.currentRealTimeData:
79+
self.currentRealTimeData[device['name']] = data
7780
else:
78-
self.queuedDigitalData[device['name']] = {'device': device, 'value': value}
81+
self.queuedRealTimeData[device['name']] = data
82+
83+
def OnPluginChange(self, data):
84+
"""Callback that is called when digital sensor data has changed
85+
86+
Args:
87+
data: The new data value
88+
"""
89+
debug('OnPluginChange: {}'.format(data))
90+
with self.digitalMutex:
91+
if data['id'] not in self.currentRealTimeData:
92+
self.currentRealTimeData[data['id']] = data
93+
else:
94+
self.queuedRealTimeData[data['id']] = data
95+
if not self.realTimeMonitorRunning:
96+
ThreadPool.Submit(self.RealTimeMonitor)
7997

8098
def InitCallbacks(self):
8199
"""Set callback function for any digital devices that support them"""
@@ -85,8 +103,8 @@ def InitCallbacks(self):
85103
if 'DigitalSensor' in device['type'] and hasattr(sensor, 'setCallback'):
86104
debug('Set callback for {}'.format(sensor))
87105
sensor.setCallback(self.OnSensorChange, device)
88-
if not self.digitalMonitorRunning:
89-
ThreadPool.Submit(self.DigitalMonitor)
106+
if not self.realTimeMonitorRunning:
107+
ThreadPool.Submit(self.RealTimeMonitor)
90108

91109
def RemoveCallbacks(self):
92110
"""Remove callback function for all digital devices"""
@@ -136,35 +154,35 @@ def Monitor(self):
136154
exception('Monitoring sensors and os resources failed')
137155
debug('Monitoring sensors and os resources finished')
138156

139-
def DigitalMonitor(self):
140-
"""Monitor digital state changes and report changed data via callbacks"""
141-
self.digitalMonitorRunning = True
142-
info('Monitoring digital sensor changes')
157+
def RealTimeMonitor(self):
158+
"""Monitor real-time state changes and report changed data via callbacks"""
159+
self.realTimeMonitorRunning = True
160+
info('Monitoring real-time state changes')
143161
nextTime = datetime.now()
144162
while not self.exiting.is_set():
145163
try:
146164
if not self.exiting.wait(0.5):
147165
if datetime.now() > nextTime:
148-
nextTime = datetime.now() + timedelta(seconds=DIGITAL_FREQUENCY)
166+
nextTime = datetime.now() + timedelta(seconds=REAL_TIME_FREQUENCY)
149167
data = []
150168
with self.digitalMutex:
151-
if self.currentDigitalData:
152-
for name, item in self.currentDigitalData.items():
153-
cayennemqtt.DataChannel.add_unique(data, cayennemqtt.DEV_SENSOR, name, value=item['value'], name=item['device']['description'], type='digital_sensor', unit='d')
169+
if self.currentRealTimeData:
170+
for name, item in self.currentRealTimeData.items():
171+
cayennemqtt.DataChannel.add_unique(data, cayennemqtt.DEV_SENSOR, name, value=item['value'], name=item['name'], type=item['type'], unit=item['unit'])
154172
try:
155-
cayennemqtt.DataChannel.add_unique(data, cayennemqtt.SYS_GPIO, item['device']['args']['channel'], cayennemqtt.VALUE, item['value'])
173+
cayennemqtt.DataChannel.add_unique(data, cayennemqtt.SYS_GPIO, item['args']['channel'], cayennemqtt.VALUE, item['value'])
156174
except:
157175
pass
158-
if name in self.queuedDigitalData and self.queuedDigitalData[name]['value'] == item['value']:
159-
del self.queuedDigitalData[name]
160-
self.currentDigitalData = self.queuedDigitalData
161-
self.queuedDigitalData = {}
176+
if name in self.queuedRealTimeData and self.queuedRealTimeData[name]['value'] == item['value']:
177+
del self.queuedRealTimeData[name]
178+
self.currentRealTimeData = self.queuedRealTimeData
179+
self.queuedRealTimeData = {}
162180
if data:
163181
self.onDataChanged(data)
164182
except:
165-
exception('Monitoring digital sensor changes failed')
166-
debug('Monitoring digital sensor changes finished')
167-
self.digitalMonitorRunning = False
183+
exception('Monitoring real-time changes failed')
184+
debug('Monitoring real-time changes finished')
185+
self.realTimeMonitorRunning = False
168186

169187
def MonitorSensors(self):
170188
"""Check sensor states for changes"""

0 commit comments

Comments
 (0)