diff --git a/Dockerfile b/Dockerfile index 96d167c..fb39d6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ RUN groupadd --gid 5001 tcc-exporter && useradd --no-log-init --no-create-home - RUN mkdir -p /usr/src/tcc-exporter && chown tcc-exporter:tcc-exporter /usr/src/tcc-exporter WORKDIR /usr/src/tcc-exporter RUN pip install dumb-init -ENV TCC_USERNAME= TCC_PASSWORD= TCC_EXPORTER_PORT=9101 TCC_LOGLEVEL=1 +ENV TCC_USERNAME= TCC_PASSWORD= TCC_EXPORTER_PORT=9101 TCC_LOGLEVEL="INFO" COPY --chown=tcc-exporter:tcc-exporter . /usr/src/tcc-exporter/ ENTRYPOINT ["dumb-init", "--"] CMD ["python", "./tcc-exporter"] diff --git a/tcc-exporter b/tcc-exporter index 6f9212c..9c9face 100755 --- a/tcc-exporter +++ b/tcc-exporter @@ -1,25 +1,23 @@ #!/usr/bin/env python3 import codecs +import inspect import json -import time import os import re +import time import urllib.parse import urllib.request from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.error import HTTPError -VERSION = '0.7.2' +VERSION = '0.8.0' PREFIX = 'https://mytotalconnectcomfort.com/' devices = list() -# Log levels. TCC_LOGLEVEL -WARN = 0 -INFO = 1 -DBUG = 2 - +# Set your TCC_LOGLEVEL to either the string or index number for your base logging level. +LOGLEVELS = ['CRITICAL', 'WARNING', 'INFO', 'DEBUG'] class Client(object): _backoff = 0 @@ -54,10 +52,10 @@ class Client(object): data = self.urlopener.open(request) # actually sign in decoded = data.read().decode() if '/portal/Account/LogOff' in decoded: - log(INFO, 'TCC API login success.') + log('INFO', 'TCC API login success.') self._backoff = 0 else: - log(INFO, 'TCC API login failure.') + log('INFO', 'TCC API login failure.') raise ValueError('login_failure') except ValueError: if (self._backoff < self._min_backoff): @@ -66,11 +64,11 @@ class Client(object): self._backoff = self._max_backoff else: self._backoff = self._backoff * 2 - log(INFO, 'Setting backoff to {0} seconds.'.format(self._backoff)) + log('INFO', 'Setting backoff to {0} seconds.'.format(self._backoff)) except Exception as e: - log(WARN, '{0}: {1!r}'.format(type(e).__name__, e.args)) + log('WARNING', '{0} {1}: {2!r}'.format(inspect.currentframe().f_code.co_name, type(e).__name__, e.args)) else: - log(INFO, 'Backoff in effect, login() delayed another {0} seconds.'.format(self._last_login + self._backoff - utc_seconds)) + log('INFO', 'Backoff in effect, login() delayed another {0} seconds.'.format(self._last_login + self._backoff - utc_seconds)) def _request(self, path, data={}, headers={}): if isinstance(data, str): @@ -90,16 +88,16 @@ class Client(object): try: return self.urlopener.open(request) # actually fetch except HTTPError as e: - log(INFO, 'TCC API status: {0} - {1}'.format(e.code, e.reason)) + log('INFO', 'TCC API status: {0} - {1}'.format(e.code, e.reason)) return e.code except Exception as e: - log(WARN, '{0}: {1!r}'.format(type(e).__name__, e.args)) + log('WARNING', '{0} {1}: {2!r}'.format(inspect.currentframe().f_code.co_name, type(e).__name__, e.args)) return None def _request_data(self, path, data={}, headers={}): data = self._request(path, data, headers) if isinstance(data, int) and (data == 401): # Login failure, lets try to login again. - log(INFO, 'Retrying Client.login()') + log('INFO', 'Retrying Client.login()') self.login() data = self._request(path, data, headers) if data is None or isinstance(data, int): @@ -108,9 +106,9 @@ class Client(object): try: return json.load(reader(data)) except json.JSONDecodeError as e: - log(INFO, 'TCC API returned invalid data.') + log('INFO', 'TCC API returned invalid data.') except Exception as e: - log(WARN, '{0}: {1!r}'.format(type(e).__name__, e.args)) + log('WARNING', '{0} {1}: {2!r}'.format(inspect.currentframe().f_code.co_name, type(e).__name__, e.args)) return None def locations(self): @@ -133,7 +131,7 @@ class Client(object): class Server(BaseHTTPRequestHandler): # Simplified logging def log_message(self, format, *args): - log(DBUG, format % args) + log('DEBUG', format % args) def do_GET(self): results = bytes() @@ -154,9 +152,9 @@ class Server(BaseHTTPRequestHandler): try: self.wfile.write(results) except BrokenPipeError as e: - log(DBUG, 'Prometheus closed the connection.') + log('DEBUG', 'Prometheus closed the connection.') except Exception as e: - log(WARN, '{0}: {1!r}'.format(type(e).__name__, e.args)) + log('WARNING', '{0} {1}: {2!r}'.format(inspect.currentframe().f_code.co_name, type(e).__name__, e.args)) return @@ -188,24 +186,37 @@ def do_stuff(device, name, obj): def log(level, *message): - debug_level = int(os.environ.get('TCC_LOGLEVEL')) + # If we were called with a descriptive string, find the index. + if isinstance(level, str): + level = LOGLEVELS.index(level) + # TCC_LOGLEVEL can be either a text or a number. + if os.environ.get('TCC_LOGLEVEL').isdigit(): + debug_level = int(os.environ.get('TCC_LOGLEVEL')) + else: + debug_level = LOGLEVELS.index(os.environ.get('TCC_LOGLEVEL')) if (level <= debug_level): - print(" ".join(message)) + print("[{0}] {1}".format(LOGLEVELS[level], " ".join(message))) def main(): global client, devices client = Client(os.environ.get('TCC_USERNAME'), os.environ.get('TCC_PASSWORD')) - - for location in client.locations(): - for device in location['Devices']: - devices.append({ 'DeviceID': device['DeviceID'], 'LocationID': device['LocationID'], 'MacID': device['MacID'], 'Name': device['Name'] }) + try: + for location in client.locations(): + for device in location['Devices']: + devices.append({ 'DeviceID': device['DeviceID'], 'LocationID': device['LocationID'], 'MacID': device['MacID'], 'Name': device['Name'] }) + except TypeError as e: + log('CRITICAL', 'Failed to collect our list of known locations and devices. Aborting.') + return + except Exception as e: + log('CRITICAL', '{0} {1}: {2!r}'.format(inspect.currentframe().f_code.co_name, type(e).__name__, e.args)) + return httpd = HTTPServer(('', int(os.environ.get('TCC_EXPORTER_PORT'))), Server) httpd.serve_forever() if __name__ == '__main__': - log(INFO, 'Started tcc-exporter/'+VERSION) + log('INFO', 'Started tcc-exporter/'+VERSION) main()