Skip to content

Commit 903ce84

Browse files
authored
Merge pull request #19 from myDevicesIoT/development
Development
2 parents 9399c0a + f14173b commit 903ce84

File tree

12 files changed

+375
-104
lines changed

12 files changed

+375
-104
lines changed

myDevices/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""
22
This package contains the Cayenne agent, which is a full featured client for the Cayenne IoT project builder: https://cayenne.mydevices.com. It sends system information as well as sensor and actuator data and responds to actuator messages initiated from the Cayenne dashboard and mobile apps.
33
"""
4-
__version__ = '2.0.1'
4+
__version__ = '2.0.2'

myDevices/cloud/cayennemqtt.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,28 @@ def add(data_list, prefix, channel=None, suffix=None, value=None, type=None, uni
6565
if name is not None:
6666
data['name'] = name
6767
data_list.append(data)
68-
68+
69+
@staticmethod
70+
def add_unique(data_list, prefix, channel=None, suffix=None, value=None, type=None, unit=None, name=None):
71+
"""Create data channel dict and append it to a list if the channel doesn't already exist in the list"""
72+
data_channel = prefix
73+
if channel is not None:
74+
data_channel += ':' + str(channel)
75+
if suffix is not None:
76+
data_channel += ';' + str(suffix)
77+
item = next((item for item in data_list if item['channel'] == data_channel), None)
78+
if not item:
79+
data = {}
80+
data['channel'] = data_channel
81+
data['value'] = value
82+
if type is not None:
83+
data['type'] = type
84+
if unit is not None:
85+
data['unit'] = unit
86+
if name is not None:
87+
data['name'] = name
88+
data_list.append(data)
89+
6990

7091
class CayenneMQTTClient:
7192
"""Cayenne MQTT Client class.

myDevices/cloud/client.py

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@
99
from time import strftime, localtime, tzset, time, sleep
1010
from queue import Queue, Empty
1111
from myDevices import __version__
12-
from myDevices.utils.config import Config
12+
from myDevices.utils.config import Config, APP_SETTINGS, NETWORK_SETTINGS
1313
from myDevices.utils.logger import exception, info, warn, error, debug, logJson
1414
from myDevices.sensors import sensors
1515
from myDevices.system.hardware import Hardware
1616
from myDevices.cloud.scheduler import SchedulerEngine
17-
from myDevices.cloud.download_speed import DownloadSpeed
1817
from myDevices.cloud.updater import Updater
1918
from myDevices.system.systemconfig import SystemConfig
2019
from myDevices.utils.daemon import Daemon
@@ -25,10 +24,9 @@
2524
from myDevices.cloud.apiclient import CayenneApiClient
2625
import myDevices.cloud.cayennemqtt as cayennemqtt
2726

28-
NETWORK_SETTINGS = '/etc/myDevices/Network.ini'
29-
APP_SETTINGS = '/etc/myDevices/AppSettings.ini'
3027
GENERAL_SLEEP_THREAD = 0.20
3128

29+
3230
def GetTime():
3331
"""Return string with the current time"""
3432
tzset()
@@ -203,15 +201,14 @@ def Start(self):
203201
self.oSInfo = OSInfo()
204202
self.count = 10000
205203
self.buff = bytearray(self.count)
206-
self.downloadSpeed = DownloadSpeed(self.config)
207-
self.downloadSpeed.getDownloadSpeed()
208204
self.sensorsClient.SetDataChanged(self.OnDataChanged)
209205
self.writerThread = WriterThread('writer', self)
210206
self.writerThread.start()
211207
self.processorThread = ProcessorThread('processor', self)
212208
self.processorThread.start()
209+
self.systemInfo = []
213210
TimerThread(self.SendSystemInfo, 300)
214-
TimerThread(self.SendSystemState, 30, 5)
211+
# TimerThread(self.SendSystemState, 30, 5)
215212
self.updater = Updater(self.config)
216213
self.updater.start()
217214
events = self.schedulerEngine.get_scheduled_events()
@@ -244,50 +241,63 @@ def Destroy(self):
244241

245242
def OnDataChanged(self, data):
246243
"""Enqueue a packet containing changed system data to send to the server"""
247-
info('Send changed data: {}'.format([{item['channel']:item['value']} for item in data]))
244+
try:
245+
if len(data) > 15:
246+
items = [{item['channel']:item['value']} for item in data if not item['channel'].startswith(cayennemqtt.SYS_GPIO)]
247+
info('Send changed data: {} + {}'.format(items, cayennemqtt.SYS_GPIO))
248+
else:
249+
info('Send changed data: {}'.format([{item['channel']:item['value']} for item in data]))
250+
# items = {}
251+
# gpio_items = {}
252+
# for item in data:
253+
# if not item['channel'].startswith(cayennemqtt.SYS_GPIO):
254+
# items[item['channel']] = item['value']
255+
# else:
256+
# channel = item['channel'].replace(cayennemqtt.SYS_GPIO + ':', '').split(';')
257+
# if not channel[0] in gpio_items:
258+
# gpio_items[channel[0]] = str(item['value'])
259+
# else:
260+
# gpio_items[channel[0]] += ',' + str(item['value'])
261+
# info('Send changed data: {}, {}: {}'.format(items, cayennemqtt.SYS_GPIO, gpio_items))
262+
except:
263+
info('Send changed data')
264+
pass
248265
self.EnqueuePacket(data)
249266

250267
def SendSystemInfo(self):
251268
"""Enqueue a packet containing system info to send to the server"""
252269
try:
253-
data = []
254-
cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_OS_NAME, value=self.oSInfo.ID)
255-
cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_OS_VERSION, value=self.oSInfo.VERSION_ID)
256-
cayennemqtt.DataChannel.add(data, cayennemqtt.AGENT_VERSION, value=self.config.get('Agent', 'Version', __version__))
257-
cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_POWER_RESET, value=0)
258-
cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_POWER_HALT, value=0)
270+
currentSystemInfo = []
271+
cayennemqtt.DataChannel.add(currentSystemInfo, cayennemqtt.SYS_OS_NAME, value=self.oSInfo.ID)
272+
cayennemqtt.DataChannel.add(currentSystemInfo, cayennemqtt.SYS_OS_VERSION, value=self.oSInfo.VERSION_ID)
273+
cayennemqtt.DataChannel.add(currentSystemInfo, cayennemqtt.AGENT_VERSION, value=self.config.get('Agent', 'Version', __version__))
274+
cayennemqtt.DataChannel.add(currentSystemInfo, cayennemqtt.SYS_POWER_RESET, value=0)
275+
cayennemqtt.DataChannel.add(currentSystemInfo, cayennemqtt.SYS_POWER_HALT, value=0)
259276
config = SystemConfig.getConfig()
260277
if config:
261278
channel_map = {'I2C': cayennemqtt.SYS_I2C, 'SPI': cayennemqtt.SYS_SPI, 'Serial': cayennemqtt.SYS_UART,
262279
'OneWire': cayennemqtt.SYS_ONEWIRE, 'DeviceTree': cayennemqtt.SYS_DEVICETREE}
263280
for key, channel in channel_map.items():
264281
try:
265-
cayennemqtt.DataChannel.add(data, channel, value=config[key])
282+
cayennemqtt.DataChannel.add(currentSystemInfo, channel, value=config[key])
266283
except:
267284
pass
268-
info('Send system info: {}'.format([{item['channel']:item['value']} for item in data]))
269-
self.EnqueuePacket(data)
285+
if currentSystemInfo != self.systemInfo:
286+
data = currentSystemInfo
287+
if self.systemInfo:
288+
data = [x for x in data if x not in self.systemInfo]
289+
if data:
290+
self.systemInfo = currentSystemInfo
291+
info('Send system info: {}'.format([{item['channel']:item['value']} for item in data]))
292+
self.EnqueuePacket(data)
270293
except Exception:
271294
exception('SendSystemInfo unexpected error')
272295

273-
def SendSystemState(self):
274-
"""Enqueue a packet containing system information to send to the server"""
275-
try:
276-
data = []
277-
download_speed = self.downloadSpeed.getDownloadSpeed()
278-
if download_speed:
279-
cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_NET, suffix=cayennemqtt.SPEEDTEST, value=download_speed, type='bw', unit='mbps')
280-
data += self.sensorsClient.systemData
281-
info('Send system state: {} items'.format(len(data)))
282-
self.EnqueuePacket(data)
283-
except Exception as e:
284-
exception('ThreadSystemInfo unexpected error: ' + str(e))
285-
286296
def CheckSubscription(self):
287297
"""Check that an invite code is valid"""
288298
inviteCode = self.config.get('Agent', 'InviteCode', fallback=None)
289299
if not inviteCode:
290-
error('No invite code found in {}'.format(APP_SETTINGS))
300+
error('No invite code found in {}'.format(self.config.path))
291301
print('Please input an invite code. This can be retrieved from the Cayenne dashboard by adding a new Raspberry Pi device.\n'
292302
'The invite code will be part of the script name shown there: rpi_[invitecode].sh.')
293303
inviteCode = input('Invite code: ')

myDevices/cloud/doupdatecheck.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from myDevices.cloud.updater import Updater
2-
from myDevices.utils.config import Config
3-
from myDevices.cloud.client import APP_SETTINGS
2+
from myDevices.utils.config import Config, APP_SETTINGS
43
from myDevices.utils.logger import setInfo
54

65
if __name__ == '__main__':

myDevices/devices/digital/gpio.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import errno
1516
import os
1617
import mmap
18+
import select
19+
from threading import Thread
1720
from time import sleep
1821
from myDevices.utils.types import M_JSON
1922
from myDevices.utils.logger import debug, info, error, exception
@@ -37,6 +40,8 @@
3740
class NativeGPIO(Singleton, GPIOPort):
3841
IN = 0
3942
OUT = 1
43+
OUT_LOW = 2
44+
OUT_HIGH = 3
4045

4146
ASUS_GPIO = 44
4247

@@ -63,6 +68,10 @@ def __init__(self):
6368
self.pinFunctionSet = set()
6469
self.valueFile = {pin:None for pin in self.pins}
6570
self.functionFile = {pin:None for pin in self.pins}
71+
self.callbacks = {}
72+
self.edge_poll = select.epoll()
73+
thread = Thread(target=self.pollEdges, daemon=True)
74+
thread.start()
6675
for pin in self.pins:
6776
# Export the pins here to prevent a delay when accessing the values for the
6877
# first time while waiting for the file group to be set
@@ -151,6 +160,7 @@ def close(self):
151160
self.setFunction(gpio, g["func"])
152161
if g["value"] >= 0 and self.getFunction(gpio) == self.OUT:
153162
self.__digitalWrite__(gpio, g["value"])
163+
self.edge_poll.close()
154164

155165
def checkDigitalChannelExported(self, channel):
156166
if not channel in self.pins:
@@ -170,6 +180,9 @@ def __getFunctionFilePath__(self, channel):
170180
def __getValueFilePath__(self, channel):
171181
return "/sys/class/gpio/gpio%s/value" % channel
172182

183+
def __getEdgeFilePath__(self, channel):
184+
return "/sys/class/gpio/gpio%s/edge" % channel
185+
173186
def __checkFilesystemExport__(self, channel):
174187
#debug("checkExport for channel %d" % channel)
175188
if not os.path.isdir("/sys/class/gpio/gpio%s" % channel):
@@ -295,11 +308,9 @@ def __setFunction__(self, channel, value):
295308
self.checkDigitalChannelExported(channel)
296309
self.checkPostingFunctionAllowed()
297310
try:
298-
if value == self.IN:
299-
value = 'in'
300-
else:
301-
value = 'out'
302-
try:
311+
value_dict = {self.IN: 'in', self.OUT: 'out', self.OUT_LOW: 'low', self.OUT_HIGH: 'high'}
312+
value = value_dict[value]
313+
try:
303314
self.functionFile[channel].write(value)
304315
self.functionFile[channel].seek(0)
305316
except:
@@ -324,6 +335,49 @@ def __portWrite__(self, value):
324335
else:
325336
raise Exception("Please limit exported GPIO to write integers")
326337

338+
def setCallback(self, channel, callback, data=None):
339+
debug('Set callback for GPIO pin {}'.format(channel))
340+
self.__checkFilesystemValue__(channel)
341+
with open(self.__getEdgeFilePath__(channel), 'w') as f:
342+
f.write('both')
343+
self.callbacks[channel] = {'function':callback, 'data':data}
344+
try:
345+
self.edge_poll.register(self.valueFile[channel], (select.EPOLLPRI | select.EPOLLET))
346+
except FileExistsError as e:
347+
# Ignore file exists error since it means we already registered the file.
348+
pass
349+
350+
def removeCallback(self, channel):
351+
debug('removeCallback: {}'.format(channel))
352+
self.__checkFilesystemValue__(channel)
353+
with open(self.__getEdgeFilePath__(channel), 'w') as f:
354+
f.write('none')
355+
del self.callbacks[channel]
356+
self.edge_poll.unregister(self.valueFile[channel])
357+
358+
def pollEdges(self):
359+
while True:
360+
try:
361+
events = self.edge_poll.poll(1)
362+
except IOError as e:
363+
if e.errno != errno.EINTR:
364+
error(e)
365+
if len(events) > 0:
366+
self.onEdgeEvent(events)
367+
368+
def onEdgeEvent(self, events):
369+
debug('onEdgeEvent: {}'.format(events))
370+
for fd, event in events:
371+
if not (event & (select.EPOLLPRI | select.EPOLLET)):
372+
continue
373+
for channel, valueFile in self.valueFile.items():
374+
if valueFile and valueFile.fileno() == fd:
375+
value = valueFile.read()
376+
valueFile.seek(0)
377+
callback = self.callbacks[channel]
378+
debug('onEdgeEvent: channel {}, value {}'.format(channel, value))
379+
callback['function'](callback['data'], int(value))
380+
327381
#@request("GET", "*")
328382
@response(contentType=M_JSON)
329383
def wildcard(self, compact=False):

myDevices/devices/digital/helper.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ def setGPIOInstance(self):
4646
if self.gpio:
4747
self.gpio.setFunction(self.channel, GPIO.IN)
4848

49+
def setCallback(self, callback, data=None):
50+
if self.gpioname == "GPIO" and self.__family__() == "DigitalSensor":
51+
self.gpio.setCallback(self.channel, callback, data)
52+
53+
def removeCallback(self):
54+
if self.gpioname == "GPIO" and self.__family__() == "DigitalSensor":
55+
self.gpio.removeCallback(self.channel)
56+
4957
#@request("GET", "value")
5058
@response("%d")
5159
def read(self):
@@ -63,9 +71,17 @@ def __str__(self):
6371
return "MotionSensor"
6472

6573
class DigitalActuator(DigitalSensor):
66-
def __init__(self, gpio, channel, invert=False):
74+
def __init__(self, gpio, channel, invert=False, last_state=None):
6775
DigitalSensor.__init__(self, gpio, channel, invert)
68-
self.gpio.setFunction(self.channel, GPIO.OUT)
76+
function = GPIO.OUT
77+
if gpio == 'GPIO' and last_state is not None:
78+
if self.invert:
79+
last_state = int(not last_state)
80+
if last_state == 1:
81+
function = GPIO.OUT_HIGH
82+
elif last_state == 0:
83+
function = GPIO.OUT_LOW
84+
self.gpio.setFunction(self.channel, function)
6985

7086
def __str__(self):
7187
return "DigitalActuator"
@@ -82,30 +98,29 @@ def write(self, value):
8298
return self.read()
8399

84100
class LightSwitch(DigitalActuator):
85-
def __init__(self, gpio, channel, invert=False):
86-
DigitalActuator.__init__(self, gpio, channel, invert)
101+
def __init__(self, gpio, channel, invert=False, last_state=None):
102+
DigitalActuator.__init__(self, gpio, channel, invert, last_state)
87103

88104
def __str__(self):
89105
return "LightSwitch"
90106

91107
class MotorSwitch(DigitalActuator):
92-
def __init__(self, gpio, channel, invert=False):
93-
DigitalActuator.__init__(self, gpio, channel, invert)
108+
def __init__(self, gpio, channel, invert=False, last_state=None):
109+
DigitalActuator.__init__(self, gpio, channel, invert, last_state)
94110

95111
def __str__(self):
96112
return "MotorSwitch"
97113

98114
class RelaySwitch(DigitalActuator):
99-
def __init__(self, gpio, channel, invert=False):
100-
DigitalActuator.__init__(self, gpio, channel, invert)
115+
def __init__(self, gpio, channel, invert=False, last_state=None):
116+
DigitalActuator.__init__(self, gpio, channel, invert, last_state)
101117

102118
def __str__(self):
103119
return "RelaySwitch"
104120

105121
class ValveSwitch(DigitalActuator):
106-
def __init__(self, gpio, channel, invert=False):
107-
DigitalActuator.__init__(self, gpio, channel, invert)
122+
def __init__(self, gpio, channel, invert=False, last_state=None):
123+
DigitalActuator.__init__(self, gpio, channel, invert, last_state)
108124

109125
def __str__(self):
110126
return "ValveSwitch"
111-

0 commit comments

Comments
 (0)