2018-05-12 14:30:07 +00:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
#
|
|
|
|
|
# radiosonde_auto_rx - Habitat Upload Utilities
|
|
|
|
|
#
|
|
|
|
|
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
|
|
|
|
|
# Released under GNU GPL v3 or later
|
|
|
|
|
#
|
|
|
|
|
import crcmod
|
|
|
|
|
import datetime
|
|
|
|
|
import logging
|
|
|
|
|
import random
|
|
|
|
|
import requests
|
|
|
|
|
import time
|
|
|
|
|
import traceback
|
|
|
|
|
import json
|
|
|
|
|
from base64 import b64encode
|
|
|
|
|
from hashlib import sha256
|
|
|
|
|
from threading import Thread
|
|
|
|
|
try:
|
|
|
|
|
# Python 2
|
|
|
|
|
from Queue import Queue
|
|
|
|
|
except ImportError:
|
|
|
|
|
# Python 3
|
|
|
|
|
from queue import Queue
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# Habitat Uploader Class
|
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
class HabitatUploader(object):
|
|
|
|
|
'''
|
|
|
|
|
Queued Habitat Telemetry Uploader class
|
|
|
|
|
|
|
|
|
|
Packets to be uploaded to Habitat are added to a queue for uploading.
|
|
|
|
|
If an upload attempt times out, the packet is discarded.
|
|
|
|
|
If the queue fills up (probably indicating no network connection, and a fast packet downlink rate),
|
|
|
|
|
it is immediately emptied, to avoid upload of out-of-date packets.
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, user_callsign='N0CALL',
|
|
|
|
|
queue_size=16,
|
|
|
|
|
upload_timeout = 10,
|
|
|
|
|
upload_retries = 5,
|
|
|
|
|
upload_retry_interval = 0.25,
|
|
|
|
|
inhibit = False,
|
|
|
|
|
):
|
|
|
|
|
''' Create a Habitat Uploader object. '''
|
|
|
|
|
|
|
|
|
|
self.user_callsign = user_callsign
|
|
|
|
|
self.upload_timeout = upload_timeout
|
|
|
|
|
self.upload_retries = upload_retries
|
|
|
|
|
self.upload_retry_interval = upload_retry_interval
|
|
|
|
|
self.queue_size = queue_size
|
2018-05-13 12:53:29 +00:00
|
|
|
self.habitat_upload_queue = Queue(queue_size)
|
2018-05-12 14:30:07 +00:00
|
|
|
self.inhibit = inhibit
|
|
|
|
|
|
|
|
|
|
# Start the uploader thread.
|
|
|
|
|
self.habitat_uploader_running = True
|
|
|
|
|
self.uploadthread = Thread(target=self.habitat_upload_thread)
|
|
|
|
|
self.uploadthread.start()
|
|
|
|
|
|
|
|
|
|
def habitat_upload(self, sentence):
|
|
|
|
|
''' Upload a UKHAS-standard telemetry sentence to Habitat '''
|
|
|
|
|
|
|
|
|
|
# Generate payload to be uploaded
|
|
|
|
|
_sentence_b64 = b64encode(sentence)
|
|
|
|
|
_date = datetime.datetime.utcnow().isoformat("T") + "Z"
|
|
|
|
|
_user_call = self.user_callsign
|
|
|
|
|
|
|
|
|
|
_data = {
|
|
|
|
|
"type": "payload_telemetry",
|
|
|
|
|
"data": {
|
|
|
|
|
"_raw": _sentence_b64
|
|
|
|
|
},
|
|
|
|
|
"receivers": {
|
|
|
|
|
_user_call: {
|
|
|
|
|
"time_created": _date,
|
|
|
|
|
"time_uploaded": _date,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The URL to upload to.
|
|
|
|
|
_url = "http://habitat.habhub.org/habitat/_design/payload_telemetry/_update/add_listener/%s" % sha256(_sentence_b64).hexdigest()
|
|
|
|
|
|
|
|
|
|
# Delay for a random amount of time between 0 and upload_retry_interval*2 seconds.
|
|
|
|
|
time.sleep(random.random()*self.upload_retry_interval*2.0)
|
|
|
|
|
|
|
|
|
|
_retries = 0
|
|
|
|
|
|
|
|
|
|
# When uploading, we have three possible outcomes:
|
|
|
|
|
# - Can't connect. No point immediately re-trying in this situation.
|
|
|
|
|
# - The packet is uploaded successfuly (201 / 403)
|
|
|
|
|
# - There is a upload conflict on the Habitat DB end (409). We can retry and it might work.
|
|
|
|
|
while _retries < self.upload_retries:
|
|
|
|
|
# Run the request.
|
|
|
|
|
try:
|
|
|
|
|
_req = requests.put(_url, data=json.dumps(_data), timeout=self.upload_timeout)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logging.error("Habitat - Upload Failed: %s" % str(e))
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if _req.status_code == 201 or _req.status_code == 403:
|
|
|
|
|
# 201 = Success, 403 = Success, sentence has already seen by others.
|
|
|
|
|
logging.info("Habitat - Uploaded sentence to Habitat successfully")
|
|
|
|
|
_upload_success = True
|
|
|
|
|
break
|
|
|
|
|
elif _req.status_code == 409:
|
|
|
|
|
# 409 = Upload conflict (server busy). Sleep for a moment, then retry.
|
|
|
|
|
logging.debug("Habitat - Upload conflict.. retrying.")
|
|
|
|
|
time.sleep(random.random()*self.upload_retry_interval)
|
|
|
|
|
_retries += 1
|
|
|
|
|
else:
|
|
|
|
|
logging.error("Habitat - Error uploading to Habitat. Status Code: %d." % _req.status_code)
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if _retries == self.upload_retries:
|
|
|
|
|
logging.error("Habitat - Upload conflict not resolved with %d retries." % self.upload_retries)
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def habitat_upload_thread(self):
|
|
|
|
|
''' Handle uploading of packets to Habitat '''
|
|
|
|
|
|
|
|
|
|
logging.info("Started Habitat Uploader Thread.")
|
|
|
|
|
|
|
|
|
|
while self.habitat_uploader_running:
|
|
|
|
|
|
|
|
|
|
if self.habitat_upload_queue.qsize() > 0:
|
|
|
|
|
# If the queue is completely full, jump to the most recent telemetry sentence.
|
|
|
|
|
if self.habitat_upload_queue.qsize() == self.queue_size:
|
|
|
|
|
while not self.habitat_upload_queue.empty():
|
|
|
|
|
sentence = self.habitat_upload_queue.get()
|
|
|
|
|
|
|
|
|
|
logging.warning("Habitat uploader queue was full - possible connectivity issue.")
|
|
|
|
|
else:
|
|
|
|
|
# Otherwise, get the first item in the queue.
|
|
|
|
|
sentence = self.habitat_upload_queue.get()
|
|
|
|
|
|
|
|
|
|
# Attempt to upload it.
|
|
|
|
|
self.habitat_upload(sentence)
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
# Wait for a short time before checking the queue again.
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
|
|
|
|
|
logging.info("Stopped Habitat Uploader Thread.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add(self, sentence):
|
|
|
|
|
''' Add a sentence to the upload queue '''
|
|
|
|
|
|
|
|
|
|
if self.inhibit:
|
|
|
|
|
# We have upload inhibited. Return.
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Handling of arbitrary numbers of $$'s at the start of a sentence:
|
|
|
|
|
# Extract the data part of the sentence (i.e. everything after the $$'s')
|
|
|
|
|
sentence = sentence.split('$')[-1]
|
|
|
|
|
# Now add the *correct* number of $$s back on.
|
|
|
|
|
sentence = '$$' +sentence
|
|
|
|
|
|
|
|
|
|
if not (sentence[-1] == '\n'):
|
|
|
|
|
sentence += '\n'
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
self.habitat_upload_queue.put_nowait(sentence)
|
|
|
|
|
except Queue.Full:
|
|
|
|
|
logging.error("Upload Queue is full, sentence discarded.")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logging.error("Error adding sentence to queue: %s" % str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
|
''' Shutdown uploader thread. '''
|
|
|
|
|
self.habitat_uploader_running = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# Functions for uploading telemetry to Habitat
|
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# CRC16 function
|
|
|
|
|
def crc16_ccitt(data):
|
|
|
|
|
"""
|
|
|
|
|
Calculate the CRC16 CCITT checksum of *data*.
|
|
|
|
|
(CRC16 CCITT: start 0xFFFF, poly 0x1021)
|
|
|
|
|
"""
|
|
|
|
|
crc16 = crcmod.predefined.mkCrcFun('crc-ccitt-false')
|
|
|
|
|
return hex(crc16(data))[2:].upper().zfill(4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def telemetry_to_sentence(sonde_data, payload_callsign="RADIOSONDE", comment=None):
|
|
|
|
|
''' Convert a telemetry data dictionary into a UKHAS-compliant telemetry sentence '''
|
|
|
|
|
# RS produces timestamps with microseconds on the end, we only want HH:MM:SS for uploading to habitat.
|
|
|
|
|
data_datetime = datetime.datetime.strptime(sonde_data['datetime_str'],"%Y-%m-%dT%H:%M:%S.%f")
|
|
|
|
|
short_time = data_datetime.strftime("%H:%M:%S")
|
|
|
|
|
|
|
|
|
|
sentence = "$$%s,%d,%s,%.5f,%.5f,%d,%.1f,%.1f,%.1f" % (payload_callsign,sonde_data['frame'],short_time,sonde_data['lat'],
|
|
|
|
|
sonde_data['lon'],int(sonde_data['alt']),sonde_data['vel_h'], sonde_data['temp'], sonde_data['humidity'])
|
|
|
|
|
|
|
|
|
|
# Add on a comment field if provided - note that this will result in a different habitat payload doc being required.
|
|
|
|
|
if comment != None:
|
|
|
|
|
comment = comment.replace(',','_')
|
|
|
|
|
sentence += "," + comment
|
|
|
|
|
|
|
|
|
|
checksum = crc16_ccitt(sentence[2:])
|
|
|
|
|
output = sentence + "*" + checksum + "\n"
|
|
|
|
|
return output
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def habitat_upload_payload_telemetry(uploader, telemetry, payload_callsign = "RADIOSONDE", callsign="N0CALL", comment=None):
|
|
|
|
|
''' Add a packet of radiosonde telemetry to the Habitat uploader queue. '''
|
|
|
|
|
|
|
|
|
|
sentence = telemetry_to_sentence(telemetry, payload_callsign = payload_callsign, comment=comment)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
uploader.add(sentence)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logging.error("Could not add telemetry to Habitat Uploader - %s" % str(e))
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# Functions for uploading a listener position to Habitat.
|
|
|
|
|
# from https://raw.githubusercontent.com/rossengeorgiev/hab-tools/master/spot2habitat_chase.py
|
|
|
|
|
#
|
|
|
|
|
callsign_init = False
|
|
|
|
|
url_habitat_uuids = "http://habitat.habhub.org/_uuids?count=%d"
|
|
|
|
|
url_habitat_db = "http://habitat.habhub.org/habitat/"
|
|
|
|
|
url_check_callsign = "http://spacenear.us/tracker/datanew.php?mode=6hours&type=positions&format=json&max_positions=10&position_id=0&vehicle=%s"
|
|
|
|
|
uuids = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_callsign(callsign, timeout=10):
|
|
|
|
|
'''
|
|
|
|
|
Check if a payload document exists for a given callsign.
|
|
|
|
|
|
|
|
|
|
This is done in a bit of a hack-ish way at the moment. We just check to see if there have
|
|
|
|
|
been any reported packets for the payload callsign on the tracker.
|
|
|
|
|
This should really be replaced with the correct call into the habitat tracker.
|
|
|
|
|
'''
|
|
|
|
|
global url_check_callsign
|
|
|
|
|
|
|
|
|
|
# Perform the request
|
|
|
|
|
_r = requests.get(url_check_callsign % callsign, timeout=timeout)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Read the response in as JSON
|
|
|
|
|
_r_json = _r.json()
|
|
|
|
|
|
|
|
|
|
# Read out the list of positions for the requested callsign
|
|
|
|
|
_positions = _r_json['positions']['position']
|
|
|
|
|
|
|
|
|
|
# If there is at least one position returned, we assume there is a valid payload document.
|
|
|
|
|
if len(_positions) > 0:
|
|
|
|
|
logging.info("Callsign %s already present in Habitat DB, not creating new payload doc." % callsign)
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
# Otherwise, we don't, and go create one.
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
# Handle errors with JSON parsing.
|
|
|
|
|
logging.error("Unable to request payload positions from spacenear.us - %s" % str(e))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Keep an internal cache for which payload docs we've created so we don't spam couchdb with updates
|
|
|
|
|
payload_config_cache = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ISOStringNow():
|
|
|
|
|
return "%sZ" % datetime.datetime.utcnow().isoformat()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def initPayloadDoc(serial, description="Meteorology Radiosonde", frequency=401500000, timeout=20):
|
|
|
|
|
"""Creates a payload in Habitat for the radiosonde before uploading"""
|
|
|
|
|
global url_habitat_db
|
|
|
|
|
global payload_config_cache
|
|
|
|
|
|
|
|
|
|
# First, check if the payload's serial number is already in our local cache.
|
|
|
|
|
if serial in payload_config_cache:
|
|
|
|
|
return payload_config_cache[serial]
|
|
|
|
|
|
|
|
|
|
# Next, check to see if the payload has been observed on the online tracker already.
|
|
|
|
|
_callsign_present = check_callsign(serial)
|
|
|
|
|
|
|
|
|
|
if _callsign_present:
|
|
|
|
|
# Add the callsign to the local cache.
|
|
|
|
|
payload_config_cache[serial] = serial
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Otherwise, proceed to creating a new payload document.
|
|
|
|
|
|
|
|
|
|
payload_data = {
|
|
|
|
|
"type": "payload_configuration",
|
|
|
|
|
"name": serial,
|
|
|
|
|
"time_created": ISOStringNow(),
|
|
|
|
|
"metadata": {
|
|
|
|
|
"description": description
|
|
|
|
|
},
|
|
|
|
|
"transmissions": [
|
|
|
|
|
{
|
|
|
|
|
"frequency": frequency, # Currently a dummy value.
|
|
|
|
|
"modulation": "RTTY",
|
|
|
|
|
"mode": "USB",
|
|
|
|
|
"encoding": "ASCII-8",
|
|
|
|
|
"parity": "none",
|
|
|
|
|
"stop": 2,
|
|
|
|
|
"shift": 350,
|
|
|
|
|
"baud": 50,
|
|
|
|
|
"description": "DUMMY ENTRY, DATA IS VIA radiosonde_auto_rx"
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"sentences": [
|
|
|
|
|
{
|
|
|
|
|
"protocol": "UKHAS",
|
|
|
|
|
"callsign": serial,
|
|
|
|
|
"checksum":"crc16-ccitt",
|
|
|
|
|
"fields":[
|
|
|
|
|
{
|
|
|
|
|
"name": "sentence_id",
|
|
|
|
|
"sensor": "base.ascii_int"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "time",
|
|
|
|
|
"sensor": "stdtelem.time"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "latitude",
|
|
|
|
|
"sensor": "stdtelem.coordinate",
|
|
|
|
|
"format": "dd.dddd"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "longitude",
|
|
|
|
|
"sensor": "stdtelem.coordinate",
|
|
|
|
|
"format": "dd.dddd"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "altitude",
|
|
|
|
|
"sensor": "base.ascii_int"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "speed",
|
|
|
|
|
"sensor": "base.ascii_float"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "temperature_external",
|
|
|
|
|
"sensor": "base.ascii_float"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "humidity",
|
|
|
|
|
"sensor": "base.ascii_float"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "comment",
|
|
|
|
|
"sensor": "base.string"
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"filters":
|
|
|
|
|
{
|
|
|
|
|
"post": [
|
|
|
|
|
{
|
|
|
|
|
"filter": "common.invalid_location_zero",
|
|
|
|
|
"type": "normal"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
"description": "radiosonde_auto_rx to Habitat Bridge"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Perform the POST request to the Habitat DB.
|
|
|
|
|
try:
|
|
|
|
|
_r = requests.post(url_habitat_db, json=payload_data, timeout=timeout)
|
|
|
|
|
|
|
|
|
|
if _r.json()['ok'] is True:
|
|
|
|
|
logging.info("Habitat - Created a payload document for %s" % serial)
|
|
|
|
|
payload_config_cache[serial] = _r.json()
|
|
|
|
|
else:
|
|
|
|
|
logging.error("Habitat - Failed to create a payload document for %s" % serial)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logging.error("Habitat - Failed to create a payload document for %s - %s" % (serial, str(e)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def postListenerData(doc, timeout=10):
|
|
|
|
|
global uuids, url_habitat_db
|
|
|
|
|
# do we have at least one uuid, if not go get more
|
|
|
|
|
if len(uuids) < 1:
|
|
|
|
|
fetchUuids()
|
|
|
|
|
|
|
|
|
|
# Attempt to add UUID and time data to document.
|
|
|
|
|
try:
|
|
|
|
|
doc['_id'] = uuids.pop()
|
|
|
|
|
except IndexError:
|
|
|
|
|
logging.error("Habitat - Unable to post listener data - no UUIDs available.")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
doc['time_uploaded'] = ISOStringNow()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
_r = requests.post(url_habitat_db, json=doc, timeout=timeout)
|
|
|
|
|
return True
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logging.error("Habitat - Could not post listener data - %s" % str(e))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetchUuids(timeout=10):
|
|
|
|
|
global uuids, url_habitat_uuids
|
|
|
|
|
|
|
|
|
|
_retries = 5
|
|
|
|
|
|
|
|
|
|
while _retries > 0:
|
|
|
|
|
try:
|
|
|
|
|
_r = requests.get(url_habitat_uuids % 10, timeout=timeout)
|
|
|
|
|
uuids.extend(_r.json()['uuids'])
|
|
|
|
|
logging.debug("Habitat - Got UUIDs")
|
|
|
|
|
return
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logging.error("Habitat - Unable to fetch UUIDs, retrying in 10 seconds - %s" % str(e))
|
|
|
|
|
time.sleep(10)
|
|
|
|
|
_retries = _retries - 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
logging.error("Habitat - Gave up trying to get UUIDs.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def initListenerCallsign(callsign, version=''):
|
|
|
|
|
doc = {
|
|
|
|
|
'type': 'listener_information',
|
|
|
|
|
'time_created' : ISOStringNow(),
|
|
|
|
|
'data': {
|
|
|
|
|
'callsign': callsign,
|
|
|
|
|
'antenna': '',
|
|
|
|
|
'radio': 'radiosonde_auto_rx %s' % version,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp = postListenerData(doc)
|
|
|
|
|
|
|
|
|
|
if resp is True:
|
|
|
|
|
logging.debug("Habitat - Listener Callsign Initialized.")
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
logging.error("Habitat - Unable to initialize callsign.")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def uploadListenerPosition(callsign, lat, lon, version=''):
|
|
|
|
|
""" Initializer Listener Callsign, and upload Listener Position """
|
|
|
|
|
|
|
|
|
|
# Attempt to initialize the listeners callsign
|
|
|
|
|
resp = initListenerCallsign(callsign, version=version)
|
|
|
|
|
# If this fails, it means we can't contact the Habitat server,
|
|
|
|
|
# so there is no point continuing.
|
|
|
|
|
if resp is False:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
doc = {
|
|
|
|
|
'type': 'listener_telemetry',
|
|
|
|
|
'time_created': ISOStringNow(),
|
|
|
|
|
'data': {
|
|
|
|
|
'callsign': callsign,
|
|
|
|
|
'chase': False,
|
|
|
|
|
'latitude': lat,
|
|
|
|
|
'longitude': lon,
|
|
|
|
|
'altitude': 0,
|
|
|
|
|
'speed': 0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# post position to habitat
|
|
|
|
|
resp = postListenerData(doc)
|
|
|
|
|
if resp is True:
|
|
|
|
|
logging.info("Habitat - Listener information uploaded.")
|
|
|
|
|
else:
|
|
|
|
|
logging.error("Habitat - Unable to upload listener information.")
|
|
|
|
|
|