Skip to content

Commit f9c1687

Browse files
authored
Merge branch 'development' into feature/digital-edge-polling
2 parents 65710c2 + 16a1bab commit f9c1687

22 files changed

+768
-1041
lines changed

README.rst

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,10 @@ Requirements
2222

2323
sudo apt-get install python3-setuptools
2424

25-
* libiw-dev - Wireless tools development file package. Via `apt-get` this can be installed with:
26-
::
27-
28-
sudo apt-get install libiw-dev
29-
3025
All of the above packages can be installed at once via `apt-get` by running:
3126
::
3227

33-
sudo apt-get install python3-pip python3-dev python3-setuptools libiw-dev
28+
sudo apt-get install python3-pip python3-dev python3-setuptools
3429

3530
***************
3631
Getting Started

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/apiclient.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
from myDevices.requests_futures.sessions import FuturesSession
2-
from concurrent.futures import ThreadPoolExecutor
31
import json
4-
from myDevices.utils.logger import error, exception
5-
from myDevices.system.hardware import Hardware
6-
from myDevices.system.systeminfo import SystemInfo
2+
from concurrent.futures import ThreadPoolExecutor
3+
4+
from myDevices import __version__
75
from myDevices.cloud import cayennemqtt
86
from myDevices.devices.digital.gpio import NativeGPIO
7+
from myDevices.requests_futures.sessions import FuturesSession
8+
from myDevices.system.hardware import Hardware
9+
from myDevices.system.systeminfo import SystemInfo
10+
from myDevices.utils.config import Config, APP_SETTINGS
11+
from myDevices.utils.logger import error, exception
12+
913

1014
class CayenneApiClient:
1115
def __init__(self, host):
@@ -56,6 +60,8 @@ def getMessageBody(self, inviteCode):
5660
system_data = []
5761
cayennemqtt.DataChannel.add(system_data, cayennemqtt.SYS_HARDWARE_MAKE, value=hardware.getManufacturer(), type='string', unit='utf8')
5862
cayennemqtt.DataChannel.add(system_data, cayennemqtt.SYS_HARDWARE_MODEL, value=hardware.getModel(), type='string', unit='utf8')
63+
config = Config(APP_SETTINGS)
64+
cayennemqtt.DataChannel.add(system_data, cayennemqtt.AGENT_VERSION, value=config.get('Agent', 'Version', __version__))
5965
system_info = SystemInfo()
6066
capacity_data = system_info.getMemoryInfo((cayennemqtt.CAPACITY,))
6167
capacity_data += system_info.getDiskInfo((cayennemqtt.CAPACITY,))

myDevices/cloud/cayennemqtt.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
COMMAND_TOPIC = 'cmd'
1010
COMMAND_JSON_TOPIC = 'cmd.json'
1111
COMMAND_RESPONSE_TOPIC = 'response'
12+
JOBS_TOPIC = 'jobs.json'
1213

1314
# Data Channels
1415
SYS_HARDWARE_MAKE = 'sys:hw:make'
@@ -30,6 +31,7 @@
3031
AGENT_VERSION = 'agent:version'
3132
AGENT_DEVICES = 'agent:devices'
3233
AGENT_MANAGE = 'agent:manage'
34+
AGENT_SCHEDULER = 'agent:scheduler'
3335
DEV_SENSOR = 'dev'
3436

3537
# Channel Suffixes
@@ -89,7 +91,7 @@ def add_unique(data_list, prefix, channel=None, suffix=None, value=None, type=No
8991
class CayenneMQTTClient:
9092
"""Cayenne MQTT Client class.
9193
92-
This is the main client class for connecting to Cayenne and sending and recFUeiving data.
94+
This is the main client class for connecting to Cayenne and sending and receiving data.
9395
9496
Standard usage:
9597
* Set on_message callback, if you are receiving data.
@@ -171,6 +173,26 @@ def disconnect_callback(self, client, userdata, rc):
171173
print("Reconnect failed, retrying")
172174
time.sleep(5)
173175

176+
def transform_command(self, command, payload=[], channel=[]):
177+
"""Transform a command message into an object.
178+
179+
command is the command object that will be transformed in place.
180+
payload is an optional list of payload data items.
181+
channel is an optional list containing channel and suffix data.
182+
"""
183+
if not payload:
184+
command['payload'] = command.pop('value')
185+
channel = command['channel'].split('/')[-1].split(';')
186+
else:
187+
if len(payload) > 1:
188+
command['cmdId'] = payload[0]
189+
command['payload'] = payload[1]
190+
else:
191+
command['payload'] = payload[0]
192+
command['channel'] = channel[0]
193+
if len(channel) > 1:
194+
command['suffix'] = channel[1]
195+
174196
def message_callback(self, client, userdata, msg):
175197
"""The callback for when a message is received from the server.
176198
@@ -181,21 +203,10 @@ def message_callback(self, client, userdata, msg):
181203
try:
182204
message = {}
183205
if msg.topic[-len(COMMAND_JSON_TOPIC):] == COMMAND_JSON_TOPIC:
184-
payload = loads(msg.payload.decode())
185-
message['payload'] = payload['value']
186-
message['cmdId'] = payload['cmdId']
187-
channel = payload['channel'].split('/')[-1].split(';')
206+
message = loads(msg.payload.decode())
207+
self.transform_command(message)
188208
else:
189-
payload = msg.payload.decode().split(',')
190-
if len(payload) > 1:
191-
message['cmdId'] = payload[0]
192-
message['payload'] = payload[1]
193-
else:
194-
message['payload'] = payload[0]
195-
channel = msg.topic.split('/')[-1].split(';')
196-
message['channel'] = channel[0]
197-
if len(channel) > 1:
198-
message['suffix'] = channel[1]
209+
self.transform_command(message, msg.payload.decode().split(','), msg.topic.split('/')[-1].split(';'))
199210
debug('message_callback: {}'.format(message))
200211
if self.on_message:
201212
self.on_message(message)

myDevices/cloud/client.py

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
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
16-
# from myDevices.cloud.scheduler import SchedulerEngine
16+
from myDevices.cloud.scheduler import SchedulerEngine
1717
from myDevices.cloud.updater import Updater
1818
from myDevices.system.systemconfig import SystemConfig
1919
from myDevices.utils.daemon import Daemon
@@ -113,7 +113,7 @@ def run(self):
113113
if topic or message:
114114
got_packet = True
115115
try:
116-
if message:
116+
if message or topic == cayennemqtt.JOBS_TOPIC:
117117
# debug('WriterThread, topic: {} {}'.format(topic, message))
118118
if not isinstance(message, str):
119119
message = dumps(message)
@@ -193,7 +193,7 @@ def Start(self):
193193
if not self.Connect():
194194
error('Error starting agent')
195195
return
196-
# self.schedulerEngine = SchedulerEngine(self, 'client_scheduler')
196+
self.schedulerEngine = SchedulerEngine(self, 'client_scheduler')
197197
self.sensorsClient = sensors.SensorsClient()
198198
self.readQueue = Queue()
199199
self.writeQueue = Queue()
@@ -211,6 +211,8 @@ def Start(self):
211211
# TimerThread(self.SendSystemState, 30, 5)
212212
self.updater = Updater(self.config)
213213
self.updater.start()
214+
events = self.schedulerEngine.get_scheduled_events()
215+
self.EnqueuePacket(events, cayennemqtt.JOBS_TOPIC)
214216
# self.sentHistoryData = {}
215217
# self.historySendFails = 0
216218
# self.historyThread = Thread(target=self.SendHistoryData)
@@ -377,8 +379,12 @@ def OnMessage(self, message):
377379

378380
def RunAction(self, action):
379381
"""Run a specified action"""
380-
debug('RunAction')
381-
self.ExecuteMessage(action)
382+
debug('RunAction: {}'.format(action))
383+
result = True
384+
command = action.copy()
385+
self.mqttClient.transform_command(command)
386+
result = self.ExecuteMessage(command)
387+
return result
382388

383389
def ProcessMessage(self):
384390
"""Process a message from the server"""
@@ -391,28 +397,36 @@ def ProcessMessage(self):
391397
self.ExecuteMessage(messageObject)
392398

393399
def ExecuteMessage(self, message):
394-
"""Execute an action described in a message object"""
400+
"""Execute an action described in a message object
401+
402+
Returns: True if action was executed, False otherwise."""
403+
result = False
395404
if not message:
396-
return
405+
return result
397406
channel = message['channel']
398407
info('ExecuteMessage: {}'.format(message))
399408
if channel in (cayennemqtt.SYS_POWER_RESET, cayennemqtt.SYS_POWER_HALT):
400-
self.ProcessPowerCommand(message)
409+
result = self.ProcessPowerCommand(message)
401410
elif channel.startswith(cayennemqtt.DEV_SENSOR):
402-
self.ProcessSensorCommand(message)
411+
result = self.ProcessSensorCommand(message)
403412
elif channel.startswith(cayennemqtt.SYS_GPIO):
404-
self.ProcessGpioCommand(message)
413+
result = self.ProcessGpioCommand(message)
405414
elif channel == cayennemqtt.AGENT_DEVICES:
406-
self.ProcessDeviceCommand(message)
415+
result = self.ProcessDeviceCommand(message)
407416
elif channel in (cayennemqtt.SYS_I2C, cayennemqtt.SYS_SPI, cayennemqtt.SYS_UART, cayennemqtt.SYS_ONEWIRE):
408-
self.ProcessConfigCommand(message)
417+
result = self.ProcessConfigCommand(message)
409418
elif channel == cayennemqtt.AGENT_MANAGE:
410-
self.ProcessAgentCommand(message)
419+
result = self.ProcessAgentCommand(message)
420+
elif channel == cayennemqtt.AGENT_SCHEDULER:
421+
result = self.ProcessSchedulerCommand(message)
411422
else:
412423
info('Unknown message')
424+
return result
413425

414426
def ProcessPowerCommand(self, message):
415-
"""Process command to reboot/shutdown the system"""
427+
"""Process command to reboot/shutdown the system
428+
429+
Returns: True if command was processed, False otherwise."""
416430
error_message = None
417431
try:
418432
self.EnqueueCommandResponse(message, error_message)
@@ -435,9 +449,13 @@ def ProcessPowerCommand(self, message):
435449
data = []
436450
cayennemqtt.DataChannel.add(data, message['channel'], value=0)
437451
self.EnqueuePacket(data)
452+
raise ExecuteMessageError(error_message)
453+
return error_message == None
438454

439455
def ProcessAgentCommand(self, message):
440-
"""Process command to manage the agent state"""
456+
"""Process command to manage the agent state
457+
458+
Returns: True if command was processed, False otherwise."""
441459
error = None
442460
try:
443461
if message['suffix'] == 'uninstall':
@@ -458,9 +476,14 @@ def ProcessAgentCommand(self, message):
458476
except Exception as ex:
459477
error = '{}: {}'.format(type(ex).__name__, ex)
460478
self.EnqueueCommandResponse(message, error)
479+
if error:
480+
raise ExecuteMessageError(error)
481+
return error == None
461482

462483
def ProcessConfigCommand(self, message):
463-
"""Process system configuration command"""
484+
"""Process system configuration command
485+
486+
Returns: True if command was processed, False otherwise."""
464487
error = None
465488
try:
466489
value = 1 - int(message['payload']) #Invert the value since the config script uses 0 for enable and 1 for disable
@@ -472,9 +495,12 @@ def ProcessConfigCommand(self, message):
472495
except Exception as ex:
473496
error = '{}: {}'.format(type(ex).__name__, ex)
474497
self.EnqueueCommandResponse(message, error)
475-
498+
return error == None
499+
476500
def ProcessGpioCommand(self, message):
477-
"""Process GPIO command"""
501+
"""Process GPIO command
502+
503+
Returns: True if command was processed, False otherwise."""
478504
error = None
479505
try:
480506
channel = int(message['channel'].replace(cayennemqtt.SYS_GPIO + ':', ''))
@@ -485,9 +511,12 @@ def ProcessGpioCommand(self, message):
485511
except Exception as ex:
486512
error = '{}: {}'.format(type(ex).__name__, ex)
487513
self.EnqueueCommandResponse(message, error)
514+
return error == None
488515

489516
def ProcessSensorCommand(self, message):
490-
"""Process sensor command"""
517+
"""Process sensor command
518+
519+
Returns: True if command was processed, False otherwise."""
491520
error = None
492521
try:
493522
sensor_info = message['channel'].replace(cayennemqtt.DEV_SENSOR + ':', '').split(':')
@@ -502,9 +531,12 @@ def ProcessSensorCommand(self, message):
502531
except Exception as ex:
503532
error = '{}: {}'.format(type(ex).__name__, ex)
504533
self.EnqueueCommandResponse(message, error)
534+
return error == None
505535

506536
def ProcessDeviceCommand(self, message):
507-
"""Process a device command to add/edit/remove a sensor"""
537+
"""Process a device command to add/edit/remove a sensor
538+
539+
Returns: True if command was processed, False otherwise."""
508540
error = None
509541
try:
510542
payload = message['payload']
@@ -523,9 +555,38 @@ def ProcessDeviceCommand(self, message):
523555
except Exception as ex:
524556
error = '{}: {}'.format(type(ex).__name__, ex)
525557
self.EnqueueCommandResponse(message, error)
558+
return error == None
559+
560+
def ProcessSchedulerCommand(self, message):
561+
"""Process command to add/edit/remove a scheduled action
562+
563+
Returns: True if command was processed, False otherwise."""
564+
error = None
565+
try:
566+
if message['suffix'] == 'add':
567+
result = self.schedulerEngine.add_scheduled_event(message['payload'], True)
568+
elif message['suffix'] == 'edit':
569+
result = self.schedulerEngine.update_scheduled_event(message['payload'])
570+
elif message['suffix'] == 'delete':
571+
result = self.schedulerEngine.remove_scheduled_event(message['payload'])
572+
elif message['suffix'] == 'get':
573+
events = self.schedulerEngine.get_scheduled_events()
574+
self.EnqueuePacket(events, cayennemqtt.JOBS_TOPIC)
575+
else:
576+
error = 'Unknown schedule command: {}'.format(message['suffix'])
577+
debug('ProcessSchedulerCommand result: {}'.format(result))
578+
if result is False:
579+
error = 'Schedule command failed'
580+
except Exception as ex:
581+
error = '{}: {}'.format(type(ex).__name__, ex)
582+
self.EnqueueCommandResponse(message, error)
583+
return error == None
526584

527585
def EnqueueCommandResponse(self, message, error):
528586
"""Send response after processing a command message"""
587+
if not 'cmdId' in message:
588+
# If there is no command id we assume this is a scheduled command and don't send a response message.
589+
return
529590
debug('EnqueueCommandResponse error: {}'.format(error))
530591
if error:
531592
response = 'error,{}={}'.format(message['cmdId'], error)

myDevices/cloud/dbmanager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,6 @@ def test():
112112
cursor = connection.cursor()
113113
except Exception as ex:
114114
error('DbManager failed to initialize: ' + str(ex))
115-
DbManager.CreateTable('scheduled_settings', "id TEXT PRIMARY KEY, data TEXT", ['id', 'data'])
115+
# DbManager.CreateTable('scheduled_events', "id TEXT PRIMARY KEY, data TEXT", ['id', 'data'])
116116
DbManager.CreateTable('disabled_sensors', "id TEXT PRIMARY KEY", ['id'])
117117
DbManager.CreateTable('historical_averages', "id INTEGER PRIMARY KEY, data TEXT, count INTEGER, start TIMESTAMP, end TIMESTAMP, interval TEXT, send TEXT, count_sensor TEXT", ['id', 'data', 'count', 'start', 'end', 'interval', 'send', 'count_sensor'])

0 commit comments

Comments
 (0)