You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

212 lines
6.8 KiB

  1. # -*- coding: utf-8 -*-
  2. """Class for performing HTTP client requests via libcurl"""
  3. import nilmdb
  4. import nilmdb.utils
  5. import nilmdb.client.httpclient
  6. from nilmdb.utils.printf import *
  7. import time
  8. import sys
  9. import re
  10. import os
  11. import simplejson as json
  12. import itertools
  13. version = "1.0"
  14. def float_to_string(f):
  15. # Use repr to maintain full precision in the string output.
  16. return repr(float(f))
  17. class Client(object):
  18. """Main client interface to the Nilm database."""
  19. client_version = version
  20. def __init__(self, url):
  21. self.http = nilmdb.client.httpclient.HTTPClient(url)
  22. def _json_param(self, data):
  23. """Return compact json-encoded version of parameter"""
  24. return json.dumps(data, separators=(',',':'))
  25. def close(self):
  26. self.http.close()
  27. def geturl(self):
  28. """Return the URL we're using"""
  29. return self.http.baseurl
  30. def version(self):
  31. """Return server version"""
  32. return self.http.get("version")
  33. def dbpath(self):
  34. """Return server database path"""
  35. return self.http.get("dbpath")
  36. def dbsize(self):
  37. """Return server database size as human readable string"""
  38. return self.http.get("dbsize")
  39. def stream_list(self, path = None, layout = None):
  40. params = {}
  41. if path is not None:
  42. params["path"] = path
  43. if layout is not None:
  44. params["layout"] = layout
  45. return self.http.get("stream/list", params)
  46. def stream_get_metadata(self, path, keys = None):
  47. params = { "path": path }
  48. if keys is not None:
  49. params["key"] = keys
  50. return self.http.get("stream/get_metadata", params)
  51. def stream_set_metadata(self, path, data):
  52. """Set stream metadata from a dictionary, replacing all existing
  53. metadata."""
  54. params = {
  55. "path": path,
  56. "data": self._json_param(data)
  57. }
  58. return self.http.get("stream/set_metadata", params)
  59. def stream_update_metadata(self, path, data):
  60. """Update stream metadata from a dictionary"""
  61. params = {
  62. "path": path,
  63. "data": self._json_param(data)
  64. }
  65. return self.http.get("stream/update_metadata", params)
  66. def stream_create(self, path, layout):
  67. """Create a new stream"""
  68. params = { "path": path,
  69. "layout" : layout }
  70. return self.http.get("stream/create", params)
  71. def stream_destroy(self, path):
  72. """Delete stream and its contents"""
  73. params = { "path": path }
  74. return self.http.get("stream/destroy", params)
  75. def stream_remove(self, path, start = None, end = None):
  76. """Remove data from the specified time range"""
  77. params = {
  78. "path": path
  79. }
  80. if start is not None:
  81. params["start"] = float_to_string(start)
  82. if end is not None:
  83. params["end"] = float_to_string(end)
  84. return self.http.get("stream/remove", params)
  85. def stream_insert(self, path, data, start = None, end = None):
  86. """Insert data into a stream. data should be a file-like object
  87. that provides ASCII data that matches the database layout for path.
  88. start and end are the starting and ending timestamp of this
  89. stream; all timestamps t in the data must satisfy 'start <= t
  90. < end'. If left unspecified, 'start' is the timestamp of the
  91. first line of data, and 'end' is the timestamp on the last line
  92. of data, plus a small delta of 1μs.
  93. """
  94. params = { "path": path }
  95. # See design.md for a discussion of how much data to send.
  96. # These are soft limits -- actual data might be rounded up.
  97. max_data = 1048576
  98. max_time = 30
  99. end_epsilon = 1e-6
  100. def extract_timestamp(line):
  101. return float(line.split()[0])
  102. def sendit():
  103. # If we have more data after this, use the timestamp of
  104. # the next line as the end. Otherwise, use the given
  105. # overall end time, or add end_epsilon to the last data
  106. # point.
  107. if nextline:
  108. block_end = extract_timestamp(nextline)
  109. if end and block_end > end:
  110. # This is unexpected, but we'll defer to the server
  111. # to return an error in this case.
  112. block_end = end
  113. elif end:
  114. block_end = end
  115. else:
  116. block_end = extract_timestamp(line) + end_epsilon
  117. # Send it
  118. params["start"] = float_to_string(block_start)
  119. params["end"] = float_to_string(block_end)
  120. return self.http.put("stream/insert", block_data, params)
  121. clock_start = time.time()
  122. block_data = ""
  123. block_start = start
  124. result = None
  125. for (line, nextline) in nilmdb.utils.misc.pairwise(data):
  126. # If we don't have a starting time, extract it from the first line
  127. if block_start is None:
  128. block_start = extract_timestamp(line)
  129. clock_elapsed = time.time() - clock_start
  130. block_data += line
  131. # If we have enough data, or enough time has elapsed,
  132. # send this block to the server, and empty things out
  133. # for the next block.
  134. if (len(block_data) > max_data) or (clock_elapsed > max_time):
  135. result = sendit()
  136. block_start = None
  137. block_data = ""
  138. clock_start = time.time()
  139. # One last block?
  140. if len(block_data):
  141. result = sendit()
  142. # Return the most recent JSON result we got back, or None if
  143. # we didn't make any requests.
  144. return result
  145. def stream_intervals(self, path, start = None, end = None):
  146. """
  147. Return a generator that yields each stream interval.
  148. """
  149. params = {
  150. "path": path
  151. }
  152. if start is not None:
  153. params["start"] = float_to_string(start)
  154. if end is not None:
  155. params["end"] = float_to_string(end)
  156. return self.http.get_gen("stream/intervals", params, retjson = True)
  157. def stream_extract(self, path, start = None, end = None, count = False):
  158. """
  159. Extract data from a stream. Returns a generator that yields
  160. lines of ASCII-formatted data that matches the database
  161. layout for the given path.
  162. Specify count=True to just get a count of values rather than
  163. the actual data.
  164. """
  165. params = {
  166. "path": path,
  167. }
  168. if start is not None:
  169. params["start"] = float_to_string(start)
  170. if end is not None:
  171. params["end"] = float_to_string(end)
  172. if count:
  173. params["count"] = 1
  174. return self.http.get_gen("stream/extract", params, retjson = False)