docs: def do_query(self, query, timeout=DEFAULT_TIMEOUT, tz=pytz.timezone("US/Pacific")):
          """
          Query structure is as follows:
  
          query = {
              # We bind UUIDs found as the result of a Brick query to a variable name
              # that we can use later.
              # Each variable definition has the following:
              #  - name: how we will refer to this group of UUIDs
              #  - definition: a Brick query. The SELECT clause should return variables that end in '_uuid', which can be found as the
              #          object of a 'bf:uuid' relationship
              #  - units: what units we want to retrieve this stream as. Currently supports W/kW, Wh/kWh, F/C, Lux
              "Variables": [
                  {"Name": "meter",
                   "Definition": "SELECT ?meter_uuid WHERE { ?meter rdf:type/rdfs:subClassOf* brick:Electric_Meter . ?meter bf:uuid ?meter_uuid . };",
                   "Units": "kW",
                  },
                  {"Name": "temp",
                   "Definition": "SELECT ?temp_uuid WHERE { ?temp rdf:type/rdfs:subClassOf* brick:Temperature_Sensor . ?temp bf:uuid ?temp_uuid . };",
                   "Units": "F",
                  },
              ],
  
              # this is the composition of the data matrix we are returning. Below, all the uuids for the "meter" variable will be placed before
              # all of the uuids for the "temp" variable. We cannot guarantee order of uuids within those groups, but the ordering of the groups
              # will be preserved. Explicit UUIDs can also be used here
              "Composition": ["meter", "temp"],
  
              # If we are retrieving statistical data, then we need to say which statistical elements we want to download.
              # The options are RAW, MEAN, MIN, MAX and COUNT. To query multiple, you can OR them together (e.g. MEAN|MAX).
              # This maps 1-1 to the "Composition" field
              "Selectors": [MEAN, MEAN],
  
              # Themporal parameters for the query. Retrieves data in the range [T0, T1]. By convention, T0 < T1,
              # but MDAL will take care of it if this is reversed.
              # WindowSize is the size of the resample window in nanoseconds
              # if Aligned is true, then MDAL will snap all data to the begining of the window (e.g. if 5min window + Aligned=true,
              # then all timestamps will be on 00:05:00, 00:10:00, 00:15:00, etc)
              "Time": {
                  "T0": "2017-08-01 00:00:00",
                  "T1": "2017-08-08 00:00:00",
                  "WindowSize": '2h',
                  "Aligned": True,
              },
          }
          """
          nonce = str(random.randint(0, 2**32))
          query['Nonce'] = nonce
          ev = threading.Event()
          response = {}
          def _handleresult(msg):
              got_response = False
              for po in msg.payload_objects:
                  if po.type_dotted != (2,0,10,4): continue
                  data = msgpack.unpackb(po.content)
                  if data['Nonce'] != query['Nonce']: continue
  
                  if 'error' in data:
                      response['error'] = data['error']
                      response['df'] = None
                      got_response=True
                      continue
  
                  uuids = [str(uuid.UUID(bytes=x)) for x in data['Rows']]
                  data = data_capnp.StreamCollection.from_bytes_packed(data['Data'])
                  if hasattr(data, 'times') and len(data.times):
                      times = list(data.times)
                      if len(times) == 0:
                          response['df'] = pd.DataFrame(columns=uuids)
                          got_response = True
                          break
                      df = pd.DataFrame(index=pd.to_datetime(times, unit='ns', utc=False))
                      for idx, s in enumerate(data.streams):
                          if len(s.values) == 0:
                              df[uuids[idx]] = None
                          else:
                              df[uuids[idx]] = s.values
                      df.index = df.index.tz_localize(pytz.utc).tz_convert(tz)
                      response['df'] = df
                      got_response = True
                  else:
                      df = pd.DataFrame()
                      for idx, s in enumerate(data.streams):
                          if hasattr(s, 'times'):
                              newdf = pd.DataFrame(list(s.values), index=list(s.times), columns=[uuids[idx]])
                              newdf.index = pd.to_datetime(newdf.index, unit='ns').tz_localize(pytz.utc).tz_convert(tz)
                              df = df.join(newdf, how='outer')
                          else:
                              raise Exception("Does this ever happen? Tell gabe!")
                      response['df'] = df
                      got_response = True
              df = response.get('df')
              if df is not None:
                  response['df'] = df#[df.index.duplicated(keep='first')]
              if got_response:
                  ev.set()
          h = self.c.subscribe("{0}/s.mdal/_/i.mdal/signal/{1}".format(self.url, self.vk[:-1]), _handleresult)
          po = PayloadObject((2,0,10,3), None, msgpack.packb(query))
          self.c.publish("{0}/s.mdal/_/i.mdal/slot/query".format(self.url), payload_objects=(po,))
          ev.wait(timeout)
          self.c.unsubscribe(h)
          if 'error' in response:
              raise Exception(response['error'])
          return response