Skip to content

Commit 1b71c6c

Browse files
authored
Merge pull request #15 from myDevicesIoT/development
Development
2 parents 0ab787f + 0fcecf5 commit 1b71c6c

File tree

19 files changed

+710
-1012
lines changed

19 files changed

+710
-1012
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.0'
4+
__version__ = '2.0.1'

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
@@ -68,7 +70,7 @@ def add(data_list, prefix, channel=None, suffix=None, value=None, type=None, uni
6870
class CayenneMQTTClient:
6971
"""Cayenne MQTT Client class.
7072
71-
This is the main client class for connecting to Cayenne and sending and recFUeiving data.
73+
This is the main client class for connecting to Cayenne and sending and receiving data.
7274
7375
Standard usage:
7476
* Set on_message callback, if you are receiving data.
@@ -150,6 +152,26 @@ def disconnect_callback(self, client, userdata, rc):
150152
print("Reconnect failed, retrying")
151153
time.sleep(5)
152154

155+
def transform_command(self, command, payload=[], channel=[]):
156+
"""Transform a command message into an object.
157+
158+
command is the command object that will be transformed in place.
159+
payload is an optional list of payload data items.
160+
channel is an optional list containing channel and suffix data.
161+
"""
162+
if not payload:
163+
command['payload'] = command.pop('value')
164+
channel = command['channel'].split('/')[-1].split(';')
165+
else:
166+
if len(payload) > 1:
167+
command['cmdId'] = payload[0]
168+
command['payload'] = payload[1]
169+
else:
170+
command['payload'] = payload[0]
171+
command['channel'] = channel[0]
172+
if len(channel) > 1:
173+
command['suffix'] = channel[1]
174+
153175
def message_callback(self, client, userdata, msg):
154176
"""The callback for when a message is received from the server.
155177
@@ -160,21 +182,10 @@ def message_callback(self, client, userdata, msg):
160182
try:
161183
message = {}
162184
if msg.topic[-len(COMMAND_JSON_TOPIC):] == COMMAND_JSON_TOPIC:
163-
payload = loads(msg.payload.decode())
164-
message['payload'] = payload['value']
165-
message['cmdId'] = payload['cmdId']
166-
channel = payload['channel'].split('/')[-1].split(';')
185+
message = loads(msg.payload.decode())
186+
self.transform_command(message)
167187
else:
168-
payload = msg.payload.decode().split(',')
169-
if len(payload) > 1:
170-
message['cmdId'] = payload[0]
171-
message['payload'] = payload[1]
172-
else:
173-
message['payload'] = payload[0]
174-
channel = msg.topic.split('/')[-1].split(';')
175-
message['channel'] = channel[0]
176-
if len(channel) > 1:
177-
message['suffix'] = channel[1]
188+
self.transform_command(message, msg.payload.decode().split(','), msg.topic.split('/')[-1].split(';'))
178189
debug('message_callback: {}'.format(message))
179190
if self.on_message:
180191
self.on_message(message)

myDevices/cloud/client.py

Lines changed: 97 additions & 27 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.download_speed import DownloadSpeed
1818
from myDevices.cloud.updater import Updater
1919
from myDevices.system.systemconfig import SystemConfig
@@ -110,16 +110,24 @@ def run(self):
110110
if self.cloudClient.mqttClient.connected == False:
111111
info('WriterThread mqttClient not connected')
112112
continue
113+
got_packet = False
113114
topic, message = self.cloudClient.DequeuePacket()
114-
if message:
115-
# debug('WriterThread, topic: {} {}'.format(topic, message))
116-
if not isinstance(message, str):
117-
message = dumps(message)
118-
self.cloudClient.mqttClient.publish_packet(topic, message)
119-
message = None
120-
self.cloudClient.writeQueue.task_done()
115+
if topic or message:
116+
got_packet = True
117+
try:
118+
if message or topic == cayennemqtt.JOBS_TOPIC:
119+
# debug('WriterThread, topic: {} {}'.format(topic, message))
120+
if not isinstance(message, str):
121+
message = dumps(message)
122+
self.cloudClient.mqttClient.publish_packet(topic, message)
123+
message = None
124+
except:
125+
exception("WriterThread publish packet error")
126+
finally:
127+
if got_packet:
128+
self.cloudClient.writeQueue.task_done()
121129
except:
122-
exception("WriterThread Unexpected error")
130+
exception("WriterThread unexpected error")
123131
return
124132

125133
def stop(self):
@@ -187,7 +195,7 @@ def Start(self):
187195
if not self.Connect():
188196
error('Error starting agent')
189197
return
190-
# self.schedulerEngine = SchedulerEngine(self, 'client_scheduler')
198+
self.schedulerEngine = SchedulerEngine(self, 'client_scheduler')
191199
self.sensorsClient = sensors.SensorsClient()
192200
self.readQueue = Queue()
193201
self.writeQueue = Queue()
@@ -206,6 +214,8 @@ def Start(self):
206214
TimerThread(self.SendSystemState, 30, 5)
207215
self.updater = Updater(self.config)
208216
self.updater.start()
217+
events = self.schedulerEngine.get_scheduled_events()
218+
self.EnqueuePacket(events, cayennemqtt.JOBS_TOPIC)
209219
# self.sentHistoryData = {}
210220
# self.historySendFails = 0
211221
# self.historyThread = Thread(target=self.SendHistoryData)
@@ -359,8 +369,12 @@ def OnMessage(self, message):
359369

360370
def RunAction(self, action):
361371
"""Run a specified action"""
362-
debug('RunAction')
363-
self.ExecuteMessage(action)
372+
debug('RunAction: {}'.format(action))
373+
result = True
374+
command = action.copy()
375+
self.mqttClient.transform_command(command)
376+
result = self.ExecuteMessage(command)
377+
return result
364378

365379
def ProcessMessage(self):
366380
"""Process a message from the server"""
@@ -373,28 +387,36 @@ def ProcessMessage(self):
373387
self.ExecuteMessage(messageObject)
374388

375389
def ExecuteMessage(self, message):
376-
"""Execute an action described in a message object"""
390+
"""Execute an action described in a message object
391+
392+
Returns: True if action was executed, False otherwise."""
393+
result = False
377394
if not message:
378-
return
395+
return result
379396
channel = message['channel']
380397
info('ExecuteMessage: {}'.format(message))
381398
if channel in (cayennemqtt.SYS_POWER_RESET, cayennemqtt.SYS_POWER_HALT):
382-
self.ProcessPowerCommand(message)
399+
result = self.ProcessPowerCommand(message)
383400
elif channel.startswith(cayennemqtt.DEV_SENSOR):
384-
self.ProcessSensorCommand(message)
401+
result = self.ProcessSensorCommand(message)
385402
elif channel.startswith(cayennemqtt.SYS_GPIO):
386-
self.ProcessGpioCommand(message)
403+
result = self.ProcessGpioCommand(message)
387404
elif channel == cayennemqtt.AGENT_DEVICES:
388-
self.ProcessDeviceCommand(message)
405+
result = self.ProcessDeviceCommand(message)
389406
elif channel in (cayennemqtt.SYS_I2C, cayennemqtt.SYS_SPI, cayennemqtt.SYS_UART, cayennemqtt.SYS_ONEWIRE):
390-
self.ProcessConfigCommand(message)
407+
result = self.ProcessConfigCommand(message)
391408
elif channel == cayennemqtt.AGENT_MANAGE:
392-
self.ProcessAgentCommand(message)
409+
result = self.ProcessAgentCommand(message)
410+
elif channel == cayennemqtt.AGENT_SCHEDULER:
411+
result = self.ProcessSchedulerCommand(message)
393412
else:
394413
info('Unknown message')
414+
return result
395415

396416
def ProcessPowerCommand(self, message):
397-
"""Process command to reboot/shutdown the system"""
417+
"""Process command to reboot/shutdown the system
418+
419+
Returns: True if command was processed, False otherwise."""
398420
error_message = None
399421
try:
400422
self.EnqueueCommandResponse(message, error_message)
@@ -405,6 +427,7 @@ def ProcessPowerCommand(self, message):
405427
cayennemqtt.DataChannel.add(data, message['channel'], value=1)
406428
self.EnqueuePacket(data)
407429
self.writeQueue.join()
430+
info('Calling execute: {}'.format(commands[message['channel']]))
408431
output, result = executeCommand(commands[message['channel']])
409432
debug('ProcessPowerCommand: {}, result: {}, output: {}'.format(message, result, output))
410433
if result != 0:
@@ -416,9 +439,13 @@ def ProcessPowerCommand(self, message):
416439
data = []
417440
cayennemqtt.DataChannel.add(data, message['channel'], value=0)
418441
self.EnqueuePacket(data)
442+
raise ExecuteMessageError(error_message)
443+
return error_message == None
419444

420445
def ProcessAgentCommand(self, message):
421-
"""Process command to manage the agent state"""
446+
"""Process command to manage the agent state
447+
448+
Returns: True if command was processed, False otherwise."""
422449
error = None
423450
try:
424451
if message['suffix'] == 'uninstall':
@@ -439,9 +466,14 @@ def ProcessAgentCommand(self, message):
439466
except Exception as ex:
440467
error = '{}: {}'.format(type(ex).__name__, ex)
441468
self.EnqueueCommandResponse(message, error)
469+
if error:
470+
raise ExecuteMessageError(error)
471+
return error == None
442472

443473
def ProcessConfigCommand(self, message):
444-
"""Process system configuration command"""
474+
"""Process system configuration command
475+
476+
Returns: True if command was processed, False otherwise."""
445477
error = None
446478
try:
447479
value = 1 - int(message['payload']) #Invert the value since the config script uses 0 for enable and 1 for disable
@@ -453,9 +485,12 @@ def ProcessConfigCommand(self, message):
453485
except Exception as ex:
454486
error = '{}: {}'.format(type(ex).__name__, ex)
455487
self.EnqueueCommandResponse(message, error)
456-
488+
return error == None
489+
457490
def ProcessGpioCommand(self, message):
458-
"""Process GPIO command"""
491+
"""Process GPIO command
492+
493+
Returns: True if command was processed, False otherwise."""
459494
error = None
460495
try:
461496
channel = int(message['channel'].replace(cayennemqtt.SYS_GPIO + ':', ''))
@@ -466,9 +501,12 @@ def ProcessGpioCommand(self, message):
466501
except Exception as ex:
467502
error = '{}: {}'.format(type(ex).__name__, ex)
468503
self.EnqueueCommandResponse(message, error)
504+
return error == None
469505

470506
def ProcessSensorCommand(self, message):
471-
"""Process sensor command"""
507+
"""Process sensor command
508+
509+
Returns: True if command was processed, False otherwise."""
472510
error = None
473511
try:
474512
sensor_info = message['channel'].replace(cayennemqtt.DEV_SENSOR + ':', '').split(':')
@@ -483,9 +521,12 @@ def ProcessSensorCommand(self, message):
483521
except Exception as ex:
484522
error = '{}: {}'.format(type(ex).__name__, ex)
485523
self.EnqueueCommandResponse(message, error)
524+
return error == None
486525

487526
def ProcessDeviceCommand(self, message):
488-
"""Process a device command to add/edit/remove a sensor"""
527+
"""Process a device command to add/edit/remove a sensor
528+
529+
Returns: True if command was processed, False otherwise."""
489530
error = None
490531
try:
491532
payload = message['payload']
@@ -504,9 +545,38 @@ def ProcessDeviceCommand(self, message):
504545
except Exception as ex:
505546
error = '{}: {}'.format(type(ex).__name__, ex)
506547
self.EnqueueCommandResponse(message, error)
548+
return error == None
549+
550+
def ProcessSchedulerCommand(self, message):
551+
"""Process command to add/edit/remove a scheduled action
552+
553+
Returns: True if command was processed, False otherwise."""
554+
error = None
555+
try:
556+
if message['suffix'] == 'add':
557+
result = self.schedulerEngine.add_scheduled_event(message['payload'], True)
558+
elif message['suffix'] == 'edit':
559+
result = self.schedulerEngine.update_scheduled_event(message['payload'])
560+
elif message['suffix'] == 'delete':
561+
result = self.schedulerEngine.remove_scheduled_event(message['payload'])
562+
elif message['suffix'] == 'get':
563+
events = self.schedulerEngine.get_scheduled_events()
564+
self.EnqueuePacket(events, cayennemqtt.JOBS_TOPIC)
565+
else:
566+
error = 'Unknown schedule command: {}'.format(message['suffix'])
567+
debug('ProcessSchedulerCommand result: {}'.format(result))
568+
if result is False:
569+
error = 'Schedule command failed'
570+
except Exception as ex:
571+
error = '{}: {}'.format(type(ex).__name__, ex)
572+
self.EnqueueCommandResponse(message, error)
573+
return error == None
507574

508575
def EnqueueCommandResponse(self, message, error):
509576
"""Send response after processing a command message"""
577+
if not 'cmdId' in message:
578+
# If there is no command id we assume this is a scheduled command and don't send a response message.
579+
return
510580
debug('EnqueueCommandResponse error: {}'.format(error))
511581
if error:
512582
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)