PageRenderTime 74ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/xbeewifiapp/apps/dashboard/views.py

https://github.com/ecybertech/xbeewificloudkit
Python | 1106 lines | 867 code | 71 blank | 168 comment | 71 complexity | 4bf26001b803470a8036b1e6fb6dc553 MD5 | raw file
Possible License(s): Apache-2.0
  1. #
  2. # This Source Code Form is subject to the terms of the Mozilla Public License,
  3. # v. 2.0. If a copy of the MPL was not distributed with this file, You can
  4. # obtain one at http://mozilla.org/MPL/2.0/.
  5. #
  6. # Copyright (c) 2013 Digi International Inc., All Rights Reserved.
  7. #
  8. import logging
  9. from django.shortcuts import render_to_response
  10. from django.contrib.auth import get_user_model
  11. from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
  12. from django.contrib.auth import login, logout, authenticate
  13. from rest_framework.decorators import api_view, authentication_classes,\
  14. permission_classes
  15. from rest_framework.views import APIView
  16. from rest_framework.response import Response
  17. from rest_framework.reverse import reverse
  18. from rest_framework import viewsets
  19. from rest_framework import permissions
  20. from rest_framework import status
  21. from models import Dashboard
  22. from serializers import DashboardSerializer, UserSerializer
  23. from permissions import IsOwner
  24. from authentication import MonitorBasicAuthentication
  25. from django.conf import settings
  26. from signals import MONITOR_TOPIC_SIGNAL_MAP
  27. from util import get_credentials, is_key_in_nested_dict
  28. from xbeewifiapp.libs.digi.devicecloud import DeviceCloudConnector
  29. from requests.exceptions import HTTPError, ConnectionError
  30. import re
  31. from datetime import datetime, timedelta
  32. from urllib import unquote
  33. from distutils.util import strtobool
  34. import base64
  35. from xbee import compare_config_with_stock
  36. logger = logging.getLogger(__name__)
  37. # Ensure the csrf cookie is set here, client may do posts via ajax on pages
  38. # with no templated form
  39. @ensure_csrf_cookie
  40. def placeholder(request):
  41. return render_to_response('placeholder.html')
  42. def socket_test(request):
  43. return render_to_response('sockettest.html',
  44. {'request': request, 'user': request.user})
  45. # API-Related Views
  46. @csrf_exempt
  47. @api_view(['PUT'])
  48. @authentication_classes((MonitorBasicAuthentication,))
  49. @permission_classes(())
  50. def monitor_receiver(request):
  51. """
  52. Push Monitor endpoint - Recieves data from Device Cloud
  53. """
  54. # Because we have no permission class, any authenticated user can access
  55. # this view.
  56. # Further restrict only to the device cloud user specified in settings
  57. # Note that this User object is a one-off, where we set the credentials
  58. # directly in special basic auth class and is not persisted to the db
  59. secret_user = settings.SECRET_DEVICE_CLOUD_MONITOR_AUTH_USER
  60. secret_pass = settings.SECRET_DEVICE_CLOUD_MONITOR_AUTH_PASS
  61. if (request.user.username != secret_user
  62. or request.user.password != secret_pass):
  63. return Response(status=status.HTTP_403_FORBIDDEN)
  64. logger.info('Recieved Device Cloud Push')
  65. # Iterate through payload, sending messages keyed off topic
  66. try:
  67. messages = request.DATA['Document']['Msg']
  68. except KeyError:
  69. return Response(status=status.HTTP_400_BAD_REQUEST)
  70. # Msg may be a list or object, depending on if multiple events.
  71. # Handle single case by making list of len 1
  72. if type(messages) is not list:
  73. messages = [messages]
  74. monitor_has_listeners = False
  75. for msg in messages:
  76. try:
  77. topic_full = msg['topic']
  78. except KeyError:
  79. return Response(status=status.HTTP_400_BAD_REQUEST)
  80. # reported topics come in a /##/Topic/Sub/topic/...,
  81. # grab parts we care about
  82. (topic, subtopic) = topic_full.split('/', 2)[1:]
  83. subtopic = unquote(subtopic)
  84. try:
  85. signal_map = MONITOR_TOPIC_SIGNAL_MAP[topic]
  86. except KeyError:
  87. logger.warning(
  88. 'No signal map exists for monitor topic %s!' % topic)
  89. # Each topic may be handled differently. For example, datapoint signals
  90. # are keyed off device id
  91. signal = None
  92. args = {}
  93. if topic == 'DataPoint':
  94. # Attempt to extract a device_id from the subtopic
  95. reg_pattern = "\S*(?P<dev_id>((-?([0-9A-F]{8})){4}))"
  96. match = re.match(reg_pattern, subtopic)
  97. if match:
  98. device_id = match.groupdict()['dev_id']
  99. signal = signal_map[device_id]
  100. args['device_id'] = device_id
  101. args['data'] = msg
  102. else:
  103. logger.warning(
  104. 'Error - No deviceId found in DataPoint subtopic!')
  105. elif topic == 'DeviceCore':
  106. # Device id not in topic, need to parse out from
  107. # message body directly...
  108. try:
  109. device_id = msg['DeviceCore']['devConnectwareId']
  110. signal = signal_map[device_id]
  111. args['device_id'] = device_id
  112. args['data'] = msg
  113. except KeyError:
  114. logger.warning('No DeviceId found in DeviceCore event')
  115. else:
  116. logger.warning('No handler for push topic type %s!' % topic)
  117. # If we have no receivers, monitor should be marked inactive
  118. # As of 2.10, Device Cloud will retry up to 16 min apart over 24 hours,
  119. # then flag
  120. if signal is not None and len(signal.receivers):
  121. monitor_has_listeners = True
  122. logger.debug(
  123. "%d registered receivers found for this push, sending signal"
  124. % len(signal.receivers))
  125. signal.send_robust(sender=None, **args)
  126. if monitor_has_listeners:
  127. logger.info('Push event with receivers handled')
  128. return Response()
  129. else:
  130. # TODO what status code to return? DC will use anything > 3xx
  131. logger.info("Received a push with no receivers, responding with 503 " +
  132. "to make monitor inactive")
  133. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  134. # *************
  135. @api_view(['GET'])
  136. def monitor_setup(request, device_id):
  137. """
  138. View to handle monitor setup for a device
  139. Will query for existing monitors, create a new one if none found, else
  140. kickstart existing monitor.
  141. Returns the monitor information from Device Cloud
  142. ------------------------------------------
  143. """
  144. username, password, cloud_fqdn = get_credentials(request)
  145. if not username or not password or not cloud_fqdn:
  146. return Response(status=status.HTTP_400_BAD_REQUEST)
  147. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  148. endpoint_url = reverse(monitor_receiver, request=request)
  149. # Device cloud won't allow monitors pointing to localhost, etc,
  150. # so don't even try
  151. if 'localhost' in endpoint_url or '127.0.0.1' in endpoint_url:
  152. logger.error('Rejecting attempt to create monitor to ' + endpoint_url)
  153. return Response(status=status.HTTP_400_BAD_REQUEST)
  154. try:
  155. monitors = conn.get_datapoint_monitor_for_device(device_id,
  156. endpoint_url)
  157. if monitors['resultSize'] == "0":
  158. # No existing monitors found for this device on this account,
  159. # create a new one
  160. # NOTE: The full url is generated by information passed in the
  161. # request. If the same backend is being routed to from multiple
  162. # places (reverse proxies, etc), each will generate a different url
  163. logger.info('Creating a new DataPoint monitor for device %s'
  164. % device_id)
  165. resp = conn.create_datapoint_monitor(
  166. device_id,
  167. endpoint_url,
  168. settings.SECRET_DEVICE_CLOUD_MONITOR_AUTH_USER,
  169. settings.SECRET_DEVICE_CLOUD_MONITOR_AUTH_PASS,
  170. description="XBee Wi-Fi Cloud Kit Monitor")
  171. else:
  172. # Should only have one monitor for a given device/topic
  173. if len(monitors['items']) > 1:
  174. logger.warning("Found multiple monitors for this device! " +
  175. "This should not happen!")
  176. monitor = monitors['items'][0]
  177. logger.info(
  178. 'Found an existing DataPoint monitor for %s, kicking it'
  179. % device_id)
  180. conn.kick_monitor(monitor['monId'])
  181. # Return the original info
  182. resp = monitors
  183. except HTTPError, e:
  184. return Response(status=e.response.status_code, data=e.response.text)
  185. except ConnectionError, e:
  186. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  187. return Response(data=resp)
  188. @api_view(['GET'])
  189. def monitor_devicecore_setup(request):
  190. """
  191. View to handle DeviceCore monitor setup for an user
  192. Will query for existing monitors, create a new one if none found, else
  193. kickstart existing monitor.
  194. Returns the monitor information from Device Cloud
  195. ------------------------------------------
  196. """
  197. username, password, cloud_fqdn = get_credentials(request)
  198. if not username or not password or not cloud_fqdn:
  199. return Response(status=status.HTTP_400_BAD_REQUEST)
  200. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  201. endpoint_url = reverse(monitor_receiver, request=request)
  202. # Device cloud won't allow monitors pointing to localhost, etc,
  203. # so don't even try
  204. if 'localhost' in endpoint_url or '127.0.0.1' in endpoint_url:
  205. logger.error('Rejecting attempt to create monitor to ' + endpoint_url)
  206. return Response(status=status.HTTP_400_BAD_REQUEST)
  207. try:
  208. monitors = conn.get_devicecore_monitor(endpoint_url)
  209. if monitors['resultSize'] == "0":
  210. # No existing monitors found for this device on this account,
  211. # create a new one
  212. # NOTE: The full url is generated by information passed in the
  213. # request. If the same backend is being routed to from multiple
  214. # places (reverse proxies, etc), each will generate a different
  215. # url.
  216. logger.info('Creating a new DeviceCore monitor for user %s'
  217. % username)
  218. resp = conn.create_devicecore_monitor(
  219. endpoint_url,
  220. settings.SECRET_DEVICE_CLOUD_MONITOR_AUTH_USER,
  221. settings.SECRET_DEVICE_CLOUD_MONITOR_AUTH_PASS,
  222. description="XBee Wi-Fi Cloud Kit Monitor")
  223. else:
  224. # Should only have one monitor for a given device/topic
  225. if len(monitors['items']) > 1:
  226. logger.warning("Found multiple monitors for user %s! " +
  227. "This should not happen!" % username)
  228. monitor = monitors['items'][0]
  229. logger.info(
  230. 'Found an existing DeviceCore monitor for user, kicking it')
  231. conn.kick_monitor(monitor['monId'])
  232. # Return the original info
  233. resp = monitors
  234. except HTTPError, e:
  235. return Response(status=e.response.status_code, data=e.response.text)
  236. except ConnectionError, e:
  237. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  238. return Response(data=resp)
  239. @api_view(['GET'])
  240. @permission_classes(())
  241. def api_root(request, format=None):
  242. """
  243. Welcome to the XBee Wi-Fi Cloud Kit API Explorer
  244. ------------------------------------------------
  245. From here, you can browse and interact with API resources right in your
  246. browser, using the same calls made by the frontend. Unauthenticated users
  247. will have limited visibility into the system, users can log in via the
  248. login API or via the link in the header.
  249. From the API root, you will find links for the following resources:
  250. * `login/logout` - Used for establishing session based authentication
  251. * `user` - View the user resource associated with your account
  252. * `devices` - Explore device information for status, data, and
  253. configuration
  254. * `dashboards` - Explore the dashboard resource rendered by the frontend
  255. """
  256. return Response({
  257. 'login': reverse('api_login', request=request, format=format),
  258. 'logout': reverse('api_logout', request=request, format=format),
  259. 'user': reverse('deviceclouduser-list',
  260. request=request, format=format),
  261. 'dashboards': reverse('dashboard-list', request=request,
  262. format=format),
  263. 'devices': reverse('devices-list', request=request, format=format),
  264. })
  265. @ensure_csrf_cookie
  266. @api_view(['POST'])
  267. @authentication_classes(())
  268. @permission_classes(())
  269. def login_user(request):
  270. """
  271. View to log user into the app for session-based auth
  272. ------------------------------------------
  273. *POST* - form encoded data or json containing the following required
  274. fields:
  275. * `username` - device cloud username
  276. * `password` - device cloud password
  277. * `cloud_fqdn` - cloud server fully qualified domain name (ex
  278. _login.etherios.com_)
  279. The following fields are optional:
  280. * `persistent_session` - boolean value, default False. Specifies whether
  281. session is remembered, or should expire when
  282. browser is closed.
  283. """
  284. try:
  285. username = request.DATA['username']
  286. password = request.DATA['password']
  287. except KeyError:
  288. return Response(status=status.HTTP_400_BAD_REQUEST)
  289. # Check that values are non-empty
  290. if not username or not password:
  291. return Response(status=status.HTTP_401_UNAUTHORIZED)
  292. try:
  293. cloud_fqdn = request.DATA['cloud_fqdn']
  294. except KeyError:
  295. if 'DEFAULT_CLOUD_SERVER' in settings.LIB_DIGI_DEVICECLOUD:
  296. cloud_fqdn = settings.LIB_DIGI_DEVICECLOUD['DEFAULT_CLOUD_SERVER']
  297. else:
  298. return Response(status=status.HTTP_400_BAD_REQUEST)
  299. # generate combo username/cloud expected by auth
  300. usercloudid = username + \
  301. settings.LIB_DIGI_DEVICECLOUD['USERNAME_CLOUD_DELIMETER'] + cloud_fqdn
  302. if username and password and cloud_fqdn:
  303. user = authenticate(username=usercloudid,
  304. password=password)
  305. if user is not None:
  306. login(request, user)
  307. # If specified, set the session cookie to expire when user's Web
  308. # Browser is closed
  309. persist_session = request.DATA.get('persistent_session', None)
  310. if (type(persist_session) is str or
  311. type(persist_session) is unicode):
  312. persist_session = strtobool(persist_session)
  313. if not persist_session:
  314. request.session.set_expiry(0)
  315. return Response()
  316. else:
  317. return Response(status=status.HTTP_401_UNAUTHORIZED)
  318. else:
  319. return Response(status=status.HTTP_400_BAD_REQUEST)
  320. @api_view(['GET'])
  321. @authentication_classes(())
  322. @permission_classes(())
  323. def logout_user(request):
  324. """
  325. View to log user out of the app, clearing any session data
  326. ------------------------------------------
  327. """
  328. logout(request)
  329. return Response()
  330. class DashboardsViewSet(viewsets.ModelViewSet):
  331. """
  332. This endpoint presents Dashboard resources
  333. ------------------------------------------
  334. The `widgets` field contains a json object defining dashboard state.
  335. _Authentication Required_ - Authenticated user will have access only to
  336. Dashboards they own
  337. """
  338. serializer_class = DashboardSerializer
  339. permission_classes = (permissions.IsAuthenticated, IsOwner,)
  340. def get_queryset(self):
  341. # Filter query to only dashboards user owns
  342. return Dashboard.objects.filter(owner=self.request.user)
  343. def pre_save(self, obj):
  344. obj.owner = self.request.user
  345. class UserViewSet(viewsets.ReadOnlyModelViewSet):
  346. """
  347. This endpoint presents the users in the system.
  348. ------------------------------------------
  349. _Authentication Required_ - Authenticated user will only have read-only
  350. access their own profile
  351. """
  352. User = get_user_model()
  353. serializer_class = UserSerializer
  354. permission_classes = (permissions.IsAuthenticated,)
  355. def get_queryset(self):
  356. # Filter query to show currently authenticated user
  357. return self.User.objects.filter(
  358. username=self.request.user.username,
  359. cloud_fqdn=self.request.user.cloud_fqdn)
  360. class DevicesList(APIView):
  361. """
  362. View to list devices belonging to the user
  363. ------------------------------------------
  364. *GET* - List devices from the user's Device Cloud account
  365. *POST* - Provision a new device to user's Device Cloud account.
  366. Required field:
  367. `mac` - MAC address of the module to provision
  368. _Authentication Required_
  369. """
  370. def get(self, request, format=None):
  371. """
  372. Return a list of Xbee WiFi devices on the authenticated user's Device
  373. Cloud account
  374. """
  375. username, password, cloud_fqdn = get_credentials(request)
  376. if not username or not password or not cloud_fqdn:
  377. return Response(status=status.HTTP_400_BAD_REQUEST)
  378. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  379. try:
  380. devices = conn.get_device_list(
  381. device_types=settings.SUPPORTED_DEVICE_TYPES)
  382. except HTTPError, e:
  383. return Response(status=e.response.status_code,
  384. data=e.response.text)
  385. except ConnectionError, e:
  386. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  387. if 'items' in devices:
  388. # Save a cached local list of devices for this user, which we can
  389. # check to control access to signals, etc
  390. request.session['user_devices'] = \
  391. [device['devConnectwareId'] for device in devices['items']]
  392. # Inject a url to each item pointing to the individual view for
  393. # that device
  394. for device in devices['items']:
  395. device['url'] = reverse(
  396. 'devices-detail',
  397. kwargs={'device_id': str(device['devConnectwareId'])},
  398. request=request)
  399. return Response(data=devices)
  400. def post(self, request, format=None):
  401. """
  402. Provision a new device to authenticated user's Device Cloud account
  403. """
  404. username, password, cloud_fqdn = get_credentials(request)
  405. if not username or not password or not cloud_fqdn:
  406. return Response(status=status.HTTP_400_BAD_REQUEST)
  407. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  408. if 'mac' in request.DATA:
  409. mac = request.DATA['mac']
  410. else:
  411. return Response(status.HTTP_400_BAD_REQUEST,
  412. data="MAC address field required")
  413. try:
  414. resp = conn.provision_device(mac)
  415. except HTTPError, e:
  416. return Response(status=e.response.status_code,
  417. data=e.response.text)
  418. except ConnectionError, e:
  419. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  420. # clear out the session device list cache
  421. if 'user_devices' in request.session:
  422. del request.session['user_devices']
  423. return Response(data=resp)
  424. class DevicesDetail(APIView):
  425. """
  426. View to show details for an individual devices
  427. ------------------------------------------
  428. *GET* - Show DeviceCore data for the the specified device
  429. _Authentication Required_
  430. """
  431. def get(self, request, device_id=None, format=None):
  432. """
  433. Return a single Xbee WiFi devices, and provide links to data and config
  434. views
  435. """
  436. username, password, cloud_fqdn = get_credentials(request)
  437. if not username or not password or not cloud_fqdn:
  438. return Response(status=status.HTTP_400_BAD_REQUEST)
  439. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  440. try:
  441. device = conn.get_device_list(device_id=device_id)
  442. except HTTPError, e:
  443. return Response(status=e.response.status_code,
  444. data=e.response.text)
  445. except ConnectionError, e:
  446. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  447. if 'items' in device:
  448. # Inject a url pointing to the config and data views
  449. for dev in device['items']:
  450. dev['io-url'] = reverse(
  451. 'device-io',
  452. kwargs={'device_id': str(dev['devConnectwareId'])},
  453. request=request)
  454. dev['config-url'] = reverse(
  455. 'device-config',
  456. kwargs={'device_id': str(dev['devConnectwareId'])},
  457. request=request)
  458. dev['serial-url'] = reverse(
  459. 'device-serial',
  460. kwargs={'device_id': str(dev['devConnectwareId'])},
  461. request=request)
  462. dev['data-url'] = reverse(
  463. 'device-datastream-list',
  464. kwargs={'device_id': str(dev['devConnectwareId'])},
  465. request=request)
  466. return Response(data=device)
  467. class DeviceIO(APIView):
  468. """
  469. View to handle changing I/O state of the device.
  470. ----------------------------------------
  471. *PUT* - Change output levels. Takes name/state pairs.
  472. When name matches DIO#, state is expected to be a boolean for new
  473. output level (ex. {"DIO0":true}).
  474. For convenience, two character AT commands can also be used for
  475. changing persistent InputOutput settings via rci set_setting command,
  476. state is passed as-is (ex. {"M0":"0x100"})
  477. """
  478. # Fast string stripping, see http://stackoverflow.com/a/1280823
  479. delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
  480. def put(self, request, device_id):
  481. io_command_pairs = []
  482. io_set_setting_pairs = []
  483. io_serial_data_values = []
  484. # Sanitize and sort inputs
  485. for name, value in request.DATA.iteritems():
  486. # May get '/' seperators, mixed case, etc. Strip out non-alphanum
  487. # chars, make uppercase.
  488. name = str(name).translate(None, self.delchars).upper()
  489. # Check for DIO and extract the bit position of this pin
  490. match = re.match(r"(DIO)(?P<bit>[0-9]+)", name)
  491. if match:
  492. bit = match.group('bit')
  493. # Convert boolean-ish values to True/False
  494. if type(value) == str or type(value) == unicode:
  495. try:
  496. value = bool(strtobool(value))
  497. except ValueError:
  498. # Try to catch "high"/"low"
  499. if value.lower() == "high":
  500. value = True
  501. elif value.lower() == "low":
  502. value = False
  503. else:
  504. return Response(status=status.HTTP_400_BAD_REQUEST)
  505. io_command_pairs.append((int(bit), value))
  506. # Else see if it looks like a traditional AT command
  507. elif len(name) == 2: # M0, etc AT command, used for PWM setting
  508. # Some commands require hex strings, others integers, others
  509. # arbitrary text...ug
  510. try:
  511. # Hex
  512. if name in ['M0', 'M1', 'IC', 'PR', 'PD', 'DS']:
  513. val_str = hex(int(value))
  514. elif (name in ['LT', 'RP', 'IR', 'IF'] or
  515. name.startswith('T') or name.startswith('Q')):
  516. val_str = str(int(value))
  517. else:
  518. # Use as is
  519. val_str = str(value)
  520. except ValueError:
  521. return Response(status=status.HTTP_400_BAD_REQUEST)
  522. io_set_setting_pairs.append((name, val_str))
  523. # Handle serial output. Currently don't support sending to
  524. # different targets, so combine all
  525. # serial messages into a single payload
  526. elif name.startswith("SERIAL"):
  527. io_serial_data_values.append(value)
  528. else:
  529. # Unknown command provided
  530. return Response(status=status.HTTP_400_BAD_REQUEST)
  531. username, password, cloud_fqdn = get_credentials(request)
  532. if not username or not password or not cloud_fqdn:
  533. return Response(status=status.HTTP_400_BAD_REQUEST)
  534. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  535. resp = {}
  536. try:
  537. # For IO command, need to generate two bitmasks - enable and level
  538. if len(io_command_pairs):
  539. enable_mask = 0
  540. output_mask = 0
  541. for bit, value in io_command_pairs:
  542. enable_mask |= 1 << int(bit)
  543. output_mask |= value << int(bit)
  544. resp = conn.set_output(device_id,
  545. hex(enable_mask), hex(output_mask))
  546. if len(io_set_setting_pairs):
  547. # Because these settings belong to a single known group, we can
  548. # construct the request for the user
  549. new_settings = {'InputOutput': {}}
  550. for name, val in io_set_setting_pairs:
  551. new_settings['InputOutput'][name] = val
  552. resp = conn.set_device_settings(device_id, new_settings)
  553. if len(io_serial_data_values):
  554. data = "".join(io_serial_data_values)
  555. data = base64.b64encode(data)
  556. resp = conn.send_serial_data(device_id, data)
  557. except HTTPError, e:
  558. return Response(status=e.response.status_code,
  559. data=e.response.text)
  560. except ConnectionError, e:
  561. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  562. if is_key_in_nested_dict(resp, 'error'):
  563. return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  564. data=resp)
  565. return Response(data=resp)
  566. class DeviceConfig(APIView):
  567. """
  568. View to show settings configuration for an individual device
  569. If a settings group is specified (e.g. /config/<group>), query will be
  570. limited in scope to just that group
  571. ------------------------------------------
  572. *GET* - Show SCI/RCI query_setting for the the specified device.
  573. - Query params: ?cache="true/false" - Whether to return cached settings,
  574. or query the device. Default false.
  575. *PUT* - Set device settings via SCI/RCI set_setting. Accepts a json object
  576. of the form `{"setting_group" : {"key":"value", ...}, ...}`
  577. _Authentication Required_
  578. """
  579. def get(self, request, device_id=None, format=None):
  580. """
  581. Query Device Cloud to return current device settings
  582. """
  583. username, password, cloud_fqdn = get_credentials(request)
  584. if not username or not password or not cloud_fqdn:
  585. return Response(status=status.HTTP_400_BAD_REQUEST)
  586. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  587. cache = bool(strtobool(request.QUERY_PARAMS.get('cache', 'False')))
  588. try:
  589. settings = conn.get_device_settings(device_id, cache=cache)
  590. except HTTPError, e:
  591. return Response(status=e.response.status_code,
  592. data=e.response.text)
  593. except ConnectionError, e:
  594. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  595. # If we're at the top level, for each setting group found, inject a
  596. # link to that subview
  597. try:
  598. device = settings['sci_reply']['send_message']['device']
  599. settings_resp = device['rci_reply']['query_setting']
  600. for group_name, dict in settings_resp.items():
  601. settings_resp[str(group_name) + '-url'] = reverse(
  602. 'device-config-group',
  603. kwargs={
  604. 'device_id': device_id,
  605. 'settings_group': str(group_name)
  606. },
  607. request=request)
  608. except KeyError:
  609. settings_resp = {}
  610. # We need a bit of extra parsing to determine if the request really
  611. # succeeded. Device cloud always returns 200, errors are shown as
  612. # elements in the body. Our (overly) simple scheme will check for the
  613. # presence of any error element
  614. if is_key_in_nested_dict(settings, 'error'):
  615. return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  616. data=settings)
  617. # Compare the config with Cloud Kit stock settings, return diff
  618. resp = settings_resp
  619. settings['config-kit-stock-values'] = compare_config_with_stock(resp)
  620. settings['config-kit-stock-apply-url'] = reverse(
  621. 'device-config-stock',
  622. kwargs={'device_id': device_id},
  623. request=request)
  624. return Response(data=settings)
  625. def put(self, request, device_id=None):
  626. # Basic sanity check on the values we're trying to send
  627. for group, settings in request.DATA.items():
  628. if not type(settings) == dict:
  629. return Response(status=status.HTTP_400_BAD_REQUEST)
  630. else:
  631. for key, val in settings.items():
  632. if not isinstance(val, (int, float, bool, str, unicode)):
  633. return Response(status=status.HTTP_400_BAD_REQUEST)
  634. username, password, cloud_fqdn = get_credentials(request)
  635. if not username or not password or not cloud_fqdn:
  636. return Response(status=status.HTTP_400_BAD_REQUEST)
  637. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  638. try:
  639. settings = conn.set_device_settings(device_id,
  640. settings=request.DATA)
  641. except HTTPError, e:
  642. return Response(status=e.response.status_code,
  643. data=e.response.text)
  644. except ConnectionError, e:
  645. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  646. # We need a bit of extra parsing to determine if the request really
  647. # succeeded. Device cloud always returns 200, errors are shown as
  648. # elements in the body. Our (overly) simple scheme will check for the
  649. # presence of any error element
  650. if is_key_in_nested_dict(settings, 'error'):
  651. return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  652. data=settings)
  653. return Response(data=settings)
  654. class DeviceConfigGroup(APIView):
  655. """
  656. View to show a specific settings group configuration for an individual
  657. device
  658. ------------------------------------------
  659. *GET* - Show SCI/RCI query_setting for the the specified device & settings
  660. group
  661. - Query params: ?cache="true/false" - Whether to return cached settings,
  662. or query the device. Default false.
  663. *PUT* - Set device settings via SCI/RCI set_setting. Takes key/value pairs
  664. of setting name/value
  665. _Authentication Required_
  666. """
  667. def get(self, request, device_id, settings_group, format=None):
  668. """
  669. Query Device Cloud to return current device settings
  670. """
  671. username, password, cloud_fqdn = get_credentials(request)
  672. if not username or not password or not cloud_fqdn:
  673. return Response(status=status.HTTP_400_BAD_REQUEST)
  674. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  675. cache = bool(strtobool(request.QUERY_PARAMS.get('cache', 'False')))
  676. try:
  677. settings = conn.get_device_settings(
  678. device_id, settings_group=settings_group, cache=cache)
  679. except HTTPError, e:
  680. return Response(status=e.response.status_code,
  681. data=e.response.text)
  682. except ConnectionError, e:
  683. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  684. # We need a bit of extra parsing to determine if the request really
  685. # succeeded. Device cloud always returns 200, errors are shown as
  686. # elements in the body. Our (overly) simple scheme will check for the
  687. # presence of any error element
  688. if is_key_in_nested_dict(settings, 'error'):
  689. return Response(status=status.HTTP_504_GATEWAY_TIMEOUT,
  690. data=settings)
  691. return Response(data=settings)
  692. def put(self, request, device_id, settings_group, format=None):
  693. """
  694. Apply new settings to the device
  695. """
  696. # Because these settings belong to a single known group, we can
  697. # construct the request for the user
  698. new_settings = {settings_group: request.DATA}
  699. username, password, cloud_fqdn = get_credentials(request)
  700. if not username or not password or not cloud_fqdn:
  701. return Response(status=status.HTTP_400_BAD_REQUEST)
  702. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  703. try:
  704. settings = conn.set_device_settings(device_id, new_settings)
  705. except HTTPError, e:
  706. return Response(status=e.response.status_code,
  707. data=e.response.text)
  708. except ConnectionError, e:
  709. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  710. # We need a bit of extra parsing to determine if the request really
  711. # succeeded. Device cloud always returns 200, errors are shown as
  712. # elements in the body. Our (overly) simple scheme will check for the
  713. # presence of any error element
  714. if is_key_in_nested_dict(settings, 'error'):
  715. return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  716. data=settings)
  717. return Response(data=settings)
  718. class DeviceConfigStock(APIView):
  719. """
  720. Apply the default Kit configuration to the XBee module
  721. ------------------------------------------
  722. *PUT* - Set device settings to Kit defaults. No request content required.
  723. _Authentication Required_
  724. """
  725. def put(self, request, device_id=None):
  726. # First query for existing config, calculate diff
  727. username, password, cloud_fqdn = get_credentials(request)
  728. if not username or not password or not cloud_fqdn:
  729. return Response(status=status.HTTP_400_BAD_REQUEST)
  730. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  731. try:
  732. settings = conn.get_device_settings(device_id)
  733. except HTTPError, e:
  734. return Response(status=e.response.status_code,
  735. data=e.response.text)
  736. except ConnectionError, e:
  737. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  738. # Check for any errors in query
  739. if is_key_in_nested_dict(settings, 'error'):
  740. return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  741. data=settings)
  742. try:
  743. device = settings['sci_reply']['send_message']['device']
  744. settings_resp = device['rci_reply']['query_setting']
  745. except KeyError:
  746. return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  747. data=settings)
  748. # Compare the config with Cloud Kit stock settings, return diff
  749. settings_diff = compare_config_with_stock(settings_resp)
  750. # If there is a difference, send the new config to device
  751. if settings_diff:
  752. try:
  753. settings_response = conn.set_device_settings(
  754. device_id, settings=settings_diff)
  755. except HTTPError, e:
  756. return Response(status=e.response.status_code,
  757. data=e.response.text)
  758. except ConnectionError, e:
  759. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  760. # Check for any errors in response
  761. if is_key_in_nested_dict(settings_response, 'error'):
  762. return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  763. data=settings_response)
  764. return Response(data=settings_response)
  765. else:
  766. return Response(status=status.HTTP_200_OK)
  767. class DeviceSerial(APIView):
  768. """
  769. Send data out serial port of devices
  770. ------------------------------------------
  771. *POST* - Send binary data to module's serial port.
  772. The following field is required:
  773. * `data` - payload
  774. The following field is optional:
  775. * `is_base64` - Whether 'data' is Base64 encoded. Defaults to false, in
  776. which case server will perform encoding.
  777. _Authentication Required_
  778. """
  779. def post(self, request, device_id):
  780. """
  781. Send data to device serial
  782. """
  783. try:
  784. data = request.DATA['data']
  785. except KeyError:
  786. return Response(status=status.HTTP_400_BAD_REQUEST)
  787. try:
  788. is_encoded = request.DATA['is_base64']
  789. needs_encoding = not strtobool(is_encoded)
  790. except KeyError:
  791. needs_encoding = True
  792. except ValueError:
  793. return Response(status=status.HTTP_400_BAD_REQUEST)
  794. if needs_encoding:
  795. data = base64.b64encode(data)
  796. username, password, cloud_fqdn = get_credentials(request)
  797. if not username or not password or not cloud_fqdn:
  798. return Response(status=status.HTTP_400_BAD_REQUEST)
  799. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  800. try:
  801. response = conn.send_serial_data(device_id, data)
  802. except HTTPError, e:
  803. return Response(status=e.response.status_code,
  804. data=e.response.text)
  805. except ConnectionError, e:
  806. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  807. # We need a bit of extra parsing to determine if the request really
  808. # succeeded. Device cloud always returns 200, errors are shown as
  809. # elements in the body. Our (overly) simple scheme will check for the
  810. # presence of any error element
  811. if is_key_in_nested_dict(response, 'error'):
  812. return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  813. data=response)
  814. return Response(data=response)
  815. class DevicesDatastreamList(APIView):
  816. """
  817. View to show Data Streams available for an individual device
  818. ------------------------------------------
  819. *GET* - Show available Data Streams for this device
  820. _Authentication Required_
  821. """
  822. def get(self, request, device_id=None, format=None):
  823. """
  824. Query Device Cloud to return current device settings
  825. """
  826. username, password, cloud_fqdn = get_credentials(request)
  827. if not username or not password or not cloud_fqdn:
  828. return Response(status=status.HTTP_400_BAD_REQUEST)
  829. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  830. try:
  831. data_streams = conn.get_datastream_list(device_id=device_id)
  832. except HTTPError, e:
  833. return Response(status=e.response.status_code,
  834. data=e.response.text)
  835. except ConnectionError, e:
  836. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  837. if 'items' in data_streams:
  838. # Inject a url pointing to the config and data views
  839. for stream in data_streams['items']:
  840. stream['datapoint-url'] = reverse(
  841. 'device-datapoint-list',
  842. kwargs={
  843. 'device_id': device_id,
  844. 'stream_id': str(stream['streamId'])
  845. },
  846. request=request)
  847. return Response(data=data_streams)
  848. class DevicesDatapointList(APIView):
  849. """
  850. View to show DataPoints in a given DataStream
  851. ------------------------------------------
  852. *GET* - Show historical DataPoints for the specified stream
  853. Optional Query Parameters:
  854. * `startTime` - POSIX timestamp in seconds. Defaults to 5 minutes in the
  855. past.
  856. _Authentication Required_
  857. """
  858. def get(self, request, device_id=None, stream_id=None, format=None):
  859. """
  860. Query Device Cloud for DataPoints
  861. """
  862. username, password, cloud_fqdn = get_credentials(request)
  863. if not username or not password or not cloud_fqdn:
  864. return Response(status=status.HTTP_400_BAD_REQUEST)
  865. conn = DeviceCloudConnector(username, password, cloud_fqdn)
  866. # Only show the data from the last x minutes
  867. if 'startTime' in request.GET:
  868. try:
  869. time = datetime.utcfromtimestamp(
  870. float(request.GET['startTime']))
  871. except ValueError:
  872. return Response(status=status.HTTP_400_BAD_REQUEST)
  873. else:
  874. time = datetime.utcnow() - timedelta(minutes=5)
  875. time_no_micro = time.replace(microsecond=0)
  876. iso_time = time_no_micro.isoformat()+'z'
  877. try:
  878. data_points = conn.get_datapoints(stream_id, iso_time)
  879. except HTTPError, e:
  880. return Response(status=e.response.status_code,
  881. data=e.response.text)
  882. except ConnectionError, e:
  883. return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
  884. return Response(data=data_points)