smartmetertx2mongo 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- smartmetertx2mongo-1.2.0/PKG-INFO +8 -0
- smartmetertx2mongo-1.2.0/README.md +46 -0
- smartmetertx2mongo-1.2.0/bin/fetchMeterReads.cron.py +7 -0
- smartmetertx2mongo-1.2.0/bin/smtx-server.py +7 -0
- smartmetertx2mongo-1.2.0/lib/smartmetertx/__init__.py +4 -0
- smartmetertx2mongo-1.2.0/lib/smartmetertx/api.py +108 -0
- smartmetertx2mongo-1.2.0/lib/smartmetertx/controller.py +121 -0
- smartmetertx2mongo-1.2.0/lib/smartmetertx/server.py +176 -0
- smartmetertx2mongo-1.2.0/lib/smartmetertx/smtx2mongo.py +110 -0
- smartmetertx2mongo-1.2.0/lib/smartmetertx/utils.py +22 -0
- smartmetertx2mongo-1.2.0/lib/ui/index.css +49 -0
- smartmetertx2mongo-1.2.0/lib/ui/index.html +37 -0
- smartmetertx2mongo-1.2.0/lib/ui/index.js +151 -0
- smartmetertx2mongo-1.2.0/setup.cfg +24 -0
- smartmetertx2mongo-1.2.0/setup.py +99 -0
- smartmetertx2mongo-1.2.0/smartmetertx2mongo.egg-info/PKG-INFO +8 -0
- smartmetertx2mongo-1.2.0/smartmetertx2mongo.egg-info/SOURCES.txt +19 -0
- smartmetertx2mongo-1.2.0/smartmetertx2mongo.egg-info/dependency_links.txt +1 -0
- smartmetertx2mongo-1.2.0/smartmetertx2mongo.egg-info/requires.txt +8 -0
- smartmetertx2mongo-1.2.0/smartmetertx2mongo.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: smartmetertx2mongo
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Implementation of smartmetertx to save records to mongodb with config driven via YAML.
|
|
5
|
+
Home-page: https://markizano.net/
|
|
6
|
+
Author: Markizano Draconus
|
|
7
|
+
Author-email: markizano@markizano.net
|
|
8
|
+
License: GNU
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Original Class Object
|
|
2
|
+
https://github.com/cmulk/python_smartmetertx
|
|
3
|
+
|
|
4
|
+
# python-smartmetertx
|
|
5
|
+
SmartMeterTX/SmartMeter Texas Python class provides a JSON interface to the electricity usage data available at https://www.smartmetertexas.com.
|
|
6
|
+
You must have an account established at the site.
|
|
7
|
+
|
|
8
|
+
Additions done by [@Markizano](http://github.com/markizano) to support updates since JAN 2024.
|
|
9
|
+
API seems to be the same.
|
|
10
|
+
|
|
11
|
+
More details can be found: https://github.com/mrand/smart_meter_texas
|
|
12
|
+
|
|
13
|
+
Depends on a MongoDB server to be running in the environment of sorts.
|
|
14
|
+
|
|
15
|
+
Will have to later build support for sqlite3 for local DB setup installs
|
|
16
|
+
that require no further software than this package.
|
|
17
|
+
|
|
18
|
+
More documentation in [doc](./doc).
|
|
19
|
+
|
|
20
|
+
Notable files below:
|
|
21
|
+
|
|
22
|
+
# bin/fetchMeterReads.cron.py
|
|
23
|
+
Run this on a CRON to collect meter reads at least once a day to store data offline from the
|
|
24
|
+
SmartMeterTexas.com site.
|
|
25
|
+
|
|
26
|
+
# bin/smtx-server.py
|
|
27
|
+
Run this to start up the local server.
|
|
28
|
+
Configure with `~/.config/smartmetertx/config.yml`.
|
|
29
|
+
Starts on port 7689 by default.
|
|
30
|
+
|
|
31
|
+
Passwords are encrypted using gpg. You can store the PGP armored message block in your configuration
|
|
32
|
+
file and this app will attempt to decrypt using your key (pending you manage the password/key/chain requirements beyond this app).
|
|
33
|
+
|
|
34
|
+
Encrypt the password using:
|
|
35
|
+
|
|
36
|
+
$ echo -en "my-secret-password" | gpg -aer 0x0000
|
|
37
|
+
|
|
38
|
+
Where `0x0000` is the key you want to use for this encryption.
|
|
39
|
+
In this way, sensitive credentials are not stored in plain text in files.
|
|
40
|
+
|
|
41
|
+
Loads a simple web page that can be used to visualize the data you want.
|
|
42
|
+
|
|
43
|
+
Extend as you please from here :)
|
|
44
|
+
|
|
45
|
+
# Screenshots
|
|
46
|
+

|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from pprint import pformat
|
|
6
|
+
from kizano import getLogger
|
|
7
|
+
|
|
8
|
+
# BEGIN: #StackOverflow
|
|
9
|
+
# @Source: https://stackoverflow.com/a/16630836/2769671
|
|
10
|
+
# These two lines enable debugging at httplib level (requests->urllib3->http.client)
|
|
11
|
+
# You will see the REQUEST, including HEADERS and DATA, and RESPONSE with HEADERS but without DATA.
|
|
12
|
+
# The only thing missing will be the response.body which is not logged.
|
|
13
|
+
if os.getenv('DEBUG', False):
|
|
14
|
+
requests_log = getLogger("requests.urllib3", 10)
|
|
15
|
+
requests_log.propagate = True
|
|
16
|
+
# END: #StackOverflow
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MeterReader:
|
|
20
|
+
HOSTNAME = 'www.smartmetertexas.com'
|
|
21
|
+
HOST = f'https://{HOSTNAME}'
|
|
22
|
+
USER_AGENT = 'API Calls (python3; Linux x86_64) Track your own metrics with SmartMeterTX: https://github.com/markizano/smartmetertx'
|
|
23
|
+
TIMEOUT = 30
|
|
24
|
+
|
|
25
|
+
def __init__(self, timeout=10):
|
|
26
|
+
self.log = getLogger(__name__)
|
|
27
|
+
self.logged_in = False
|
|
28
|
+
self.session = requests.Session()
|
|
29
|
+
self.timeout = timeout
|
|
30
|
+
self.session.headers['Authority'] = MeterReader.HOSTNAME
|
|
31
|
+
self.session.headers['Origin'] = MeterReader.HOST
|
|
32
|
+
self.session.headers['Accept'] = 'application/json, text/plain, */*'
|
|
33
|
+
self.session.headers['Accept-Language'] = 'en-US,en;q=0.9'
|
|
34
|
+
self.session.headers['Content-Type'] = 'application/json; charset=UTF-8'
|
|
35
|
+
self.session.headers['dnt'] = '1'
|
|
36
|
+
self.session.headers['sec-ch-ua'] = '".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"'
|
|
37
|
+
self.session.headers['sec-ch-ua-mobile'] = '?0'
|
|
38
|
+
self.session.headers['sec-ch-ua-platform'] = 'Linux'
|
|
39
|
+
self.session.headers['sec-fetch-dest'] = 'empty'
|
|
40
|
+
self.session.headers['sec-fetch-mode'] = 'cors'
|
|
41
|
+
self.session.headers['sec-fetch-site'] = 'same-origin'
|
|
42
|
+
self.session.headers['User-Agent'] = MeterReader.USER_AGENT
|
|
43
|
+
|
|
44
|
+
def api_call(self, url, json):
|
|
45
|
+
'''
|
|
46
|
+
Generic API call that can be made to the site for JSON results back.
|
|
47
|
+
@param url :string: Where to send POST request.
|
|
48
|
+
@param json :object: Data to send to the server.
|
|
49
|
+
@return :object: JSON response back or ERROR
|
|
50
|
+
'''
|
|
51
|
+
self.log.debug(f'MeterReader.api_call(url={url}, json={json})')
|
|
52
|
+
try:
|
|
53
|
+
return self.session.post(
|
|
54
|
+
url=url,
|
|
55
|
+
json=json,
|
|
56
|
+
timeout=self.timeout,
|
|
57
|
+
verify=False
|
|
58
|
+
)
|
|
59
|
+
except Exception as ex:
|
|
60
|
+
self.log.error(repr(ex))
|
|
61
|
+
raise ex
|
|
62
|
+
|
|
63
|
+
def login(self, username, password):
|
|
64
|
+
'''
|
|
65
|
+
Make API call to login and acquire a session token.
|
|
66
|
+
@param username :string: Username or email used to Login to the webpage.
|
|
67
|
+
@param password :string: Password used to login.
|
|
68
|
+
@return :string: The login token that will be used going forward.
|
|
69
|
+
'''
|
|
70
|
+
creds = {
|
|
71
|
+
"username": username,
|
|
72
|
+
"password": password
|
|
73
|
+
}
|
|
74
|
+
url = f"{MeterReader.HOST}/commonapi/user/authenticate"
|
|
75
|
+
r = self.api_call(url, json=creds)
|
|
76
|
+
if r.status_code != 200:
|
|
77
|
+
self.log.error("Login failed.")
|
|
78
|
+
self.log.debug(pformat(r.headers.__dict__))
|
|
79
|
+
self.log.debug(r.text)
|
|
80
|
+
self.log.debug(self.session.cookies.__dict__)
|
|
81
|
+
return False
|
|
82
|
+
else:
|
|
83
|
+
self.token = r.json()['token']
|
|
84
|
+
self.log.info("Login successful!")
|
|
85
|
+
self.log.debug(f"Got \x1b[33m{self.token}\x1b[0m as token.")
|
|
86
|
+
self.session.headers["Authorization"] = f"Bearer {self.token}"
|
|
87
|
+
self.logged_in = True
|
|
88
|
+
return self.token
|
|
89
|
+
|
|
90
|
+
def get_daily_read(self, esiid, start_date, end_date):
|
|
91
|
+
if self.logged_in == False:
|
|
92
|
+
self.log.error("You must login first.")
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
json = {
|
|
96
|
+
"esiid": esiid,
|
|
97
|
+
"endDate": end_date,
|
|
98
|
+
"startDate": start_date,
|
|
99
|
+
}
|
|
100
|
+
url = f"{MeterReader.HOST}/api/usage/daily"
|
|
101
|
+
r = self.api_call(url, json=json)
|
|
102
|
+
if r.status_code != 200 or "error" in r.text.lower():
|
|
103
|
+
self.log.warning("Failed fetching daily read!")
|
|
104
|
+
self.log.debug(r.text)
|
|
105
|
+
self.log.debug(pformat(r.headers.__dict__))
|
|
106
|
+
return False
|
|
107
|
+
else:
|
|
108
|
+
return r.json()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import cherrypy
|
|
5
|
+
import traceback as tb
|
|
6
|
+
|
|
7
|
+
class SmartMeterController(object):
|
|
8
|
+
'''
|
|
9
|
+
Base Controller abstract.
|
|
10
|
+
If a method is common among all controllers it goes here.
|
|
11
|
+
If a method should be implemented by all controllers, it could be included
|
|
12
|
+
here for all controllers to enjoy.
|
|
13
|
+
'''
|
|
14
|
+
ERRORS = {
|
|
15
|
+
'request-method': 'Request method not allowed.',
|
|
16
|
+
'accept-json': 'Accept header must be application/json.',
|
|
17
|
+
'not-json': 'Content-Type must be application/json.',
|
|
18
|
+
'content-length': 'Content-Length header required.',
|
|
19
|
+
'empty-body': 'Empty request body.',
|
|
20
|
+
'invalid-json': 'Invalid JSON: %s',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def isValidJSONRequest(self, request, body):
|
|
24
|
+
'''
|
|
25
|
+
Collection of validations against the client request to this server.
|
|
26
|
+
request: cherrypy.request object
|
|
27
|
+
body: The result to attempt to parse as a string.
|
|
28
|
+
|
|
29
|
+
400: bad request
|
|
30
|
+
405: method not allowed
|
|
31
|
+
406: not acceptable
|
|
32
|
+
411: content-length required
|
|
33
|
+
415: unsupported media type
|
|
34
|
+
417: expectation failed
|
|
35
|
+
422: Unprocessable entity
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
|
|
39
|
+
isValid, errMesgs = self.isValidJSONRequest(request, payload)
|
|
40
|
+
if not isValid:
|
|
41
|
+
return { 'result': False, 'messages': errMesgs }
|
|
42
|
+
'''
|
|
43
|
+
result = True
|
|
44
|
+
mesgs = []
|
|
45
|
+
if request.get('REQUEST_METHOD', '') not in ['GET', 'POST', 'PUT']:
|
|
46
|
+
result = False
|
|
47
|
+
mesgs.append(SmartMeterController.ERRORS['request-method'])
|
|
48
|
+
cherrypy.response.status = 405
|
|
49
|
+
if request.get('REQUEST_URI', '').startswith('/api'):
|
|
50
|
+
accept = request.get('HTTP_ACCEPT', '')
|
|
51
|
+
if ('application/json' not in accept) and ('*/*' not in accept):
|
|
52
|
+
result = False
|
|
53
|
+
mesgs.append(SmartMeterController.ERRORS['accept-json'])
|
|
54
|
+
cherrypy.response.status = 415
|
|
55
|
+
if request['REQUEST_METHOD'] == 'POST':
|
|
56
|
+
if 'application/json' not in request.get('CONTENT_TYPE', ''):
|
|
57
|
+
result = False
|
|
58
|
+
mesgs.append(SmartMeterController.ERRORS['not-json'])
|
|
59
|
+
cherrypy.response.status = 400
|
|
60
|
+
cl = request.get('CONTENT_LENGTH', 0)
|
|
61
|
+
if not cl or int(cl) < 1:
|
|
62
|
+
result = False
|
|
63
|
+
mesgs.append(SmartMeterController.ERRORS['content-length'])
|
|
64
|
+
cherrypy.response.status = 411
|
|
65
|
+
if not body:
|
|
66
|
+
result = False
|
|
67
|
+
mesgs.append(SmartMeterController.ERRORS['empty-body'])
|
|
68
|
+
cherrypy.response.status = 411
|
|
69
|
+
else:
|
|
70
|
+
try:
|
|
71
|
+
j = json.loads(body)
|
|
72
|
+
except ValueError as e:
|
|
73
|
+
result = False
|
|
74
|
+
mesgs.append(SmartMeterController.ERRORS['invalid-json'] % e)
|
|
75
|
+
cherrypy.response.status = 417
|
|
76
|
+
return result, mesgs
|
|
77
|
+
|
|
78
|
+
def returnError(self, err, mesgs):
|
|
79
|
+
'''
|
|
80
|
+
Return a [err] http response code and send the status message as an object.
|
|
81
|
+
Usage:
|
|
82
|
+
|
|
83
|
+
return self.returnError(500, ['no such file or directory'])
|
|
84
|
+
'''
|
|
85
|
+
kwargs = {}
|
|
86
|
+
if not cherrypy.response.status or ( cherrypy.response.status >= 200 and cherrypy.response.status <= 299 ):
|
|
87
|
+
cherrypy.response.status = int(err.get('status', 500))
|
|
88
|
+
if isinstance(mesgs, str):
|
|
89
|
+
mesgs = [mesgs]
|
|
90
|
+
result = {
|
|
91
|
+
'error': True,
|
|
92
|
+
'mesgs': err.get('mesgs', mesgs),
|
|
93
|
+
'value': None,
|
|
94
|
+
}
|
|
95
|
+
if 'DEBUG' in os.environ:
|
|
96
|
+
kwargs['indent'] = 2
|
|
97
|
+
kwargs['sort_keys'] = True
|
|
98
|
+
if err.has_key('e'):
|
|
99
|
+
result['exception'] = tb.format_exc(err['e'])
|
|
100
|
+
return json.dumps(result, **kwargs) + "\n"
|
|
101
|
+
|
|
102
|
+
def returnValue(self, result, value):
|
|
103
|
+
'''
|
|
104
|
+
Returns the value of the result you desire.
|
|
105
|
+
result: Boolean to return as a result of the operation.
|
|
106
|
+
value: object to return describing the result.
|
|
107
|
+
@returns :string: JSON dump of a wrapped object of the input params.
|
|
108
|
+
'''
|
|
109
|
+
if not cherrypy.response.status:
|
|
110
|
+
cherrypy.response.status = 200
|
|
111
|
+
kwargs = {}
|
|
112
|
+
if 'DEBUG' in os.environ:
|
|
113
|
+
kwargs['indent'] = 2
|
|
114
|
+
kwargs['sort_keys'] = True
|
|
115
|
+
return {
|
|
116
|
+
'error': False,
|
|
117
|
+
'status': 200,
|
|
118
|
+
'value': value,
|
|
119
|
+
'return': result,
|
|
120
|
+
}
|
|
121
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
|
|
2
|
+
import os, sys
|
|
3
|
+
import cherrypy
|
|
4
|
+
import jinja2
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from kizano import getLogger, getConfig, Config
|
|
9
|
+
log = getLogger('smartmetertx.server', log_format='json')
|
|
10
|
+
|
|
11
|
+
from .utils import getMongoConnection
|
|
12
|
+
from .controller import SmartMeterController
|
|
13
|
+
|
|
14
|
+
DEFAULT_UI_PATH = os.path.join( sys.exec_prefix, 'share', 'smartmetertx' )
|
|
15
|
+
|
|
16
|
+
class MeterServer(SmartMeterController):
|
|
17
|
+
mongo = None
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: Config):
|
|
20
|
+
super(MeterServer, self)
|
|
21
|
+
self.config = config
|
|
22
|
+
self.db = getMongoConnection(config).get_database(config['mongo'].get('dbname', 'smartmetertx'))
|
|
23
|
+
|
|
24
|
+
def __del__(self):
|
|
25
|
+
self.close()
|
|
26
|
+
|
|
27
|
+
def close(self):
|
|
28
|
+
if self.mongo:
|
|
29
|
+
self.mongo.close()
|
|
30
|
+
self.mongo = None
|
|
31
|
+
|
|
32
|
+
@cherrypy.expose
|
|
33
|
+
def index(self):
|
|
34
|
+
'''
|
|
35
|
+
Home page!
|
|
36
|
+
'''
|
|
37
|
+
return self.returnValue(True, {'hello': 'world'})
|
|
38
|
+
|
|
39
|
+
@cherrypy.expose
|
|
40
|
+
@cherrypy.tools.json_out()
|
|
41
|
+
def meterRead(self, date: str = None):
|
|
42
|
+
'''
|
|
43
|
+
Return a meter read for a specified date.
|
|
44
|
+
Gets the full meter read from the DB.
|
|
45
|
+
'''
|
|
46
|
+
result = {}
|
|
47
|
+
if date is None:
|
|
48
|
+
return self.returnValue(False, 'No date specified.')
|
|
49
|
+
try:
|
|
50
|
+
import dateparser
|
|
51
|
+
queryDate = dateparser.parse(date)
|
|
52
|
+
timerange = {
|
|
53
|
+
'$gte': queryDate.replace( hour=max(0, queryDate.hour-1) ),
|
|
54
|
+
'$lt': queryDate.replace( hour=min(23, queryDate.hour+1) )
|
|
55
|
+
}
|
|
56
|
+
result = self.db.meterReads.find_one({'datetime': timerange })
|
|
57
|
+
if result is None:
|
|
58
|
+
return self.returnValue(False, f'No meter read found for {date}')
|
|
59
|
+
log.debug(result)
|
|
60
|
+
del result['_id']
|
|
61
|
+
result['datetime'] = result['datetime'].strftime('%F/%R:%S')
|
|
62
|
+
return self.returnValue(True, result)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
import traceback as tb
|
|
65
|
+
log.error(f'Error getting meter read for {date}: {e}')
|
|
66
|
+
log.error(tb.format_exc())
|
|
67
|
+
return self.returnValue(False, 'uhm, well, this is embarassing :S')
|
|
68
|
+
|
|
69
|
+
@cherrypy.expose
|
|
70
|
+
@cherrypy.tools.json_out()
|
|
71
|
+
def meterReads(self, fdate: str = None, tdate: str = None):
|
|
72
|
+
'''
|
|
73
|
+
Return a list of meter reads for a specified date range.
|
|
74
|
+
Gets only the list of values paired with the date as an object/key-value pairing.
|
|
75
|
+
'''
|
|
76
|
+
result = []
|
|
77
|
+
if fdate is None:
|
|
78
|
+
return self.returnValue(False, 'No From Date Specified. Need `fdate`.')
|
|
79
|
+
if tdate is None:
|
|
80
|
+
return self.returnValue(False, 'No To Date Specified. Need `tdate`.')
|
|
81
|
+
import dateparser
|
|
82
|
+
fromDate = dateparser.parse(fdate)
|
|
83
|
+
toDate = dateparser.parse(tdate)
|
|
84
|
+
timerange = {
|
|
85
|
+
'$gte': fromDate,
|
|
86
|
+
'$lt': toDate
|
|
87
|
+
}
|
|
88
|
+
projection = { '_id': False, 'reading': True, 'datetime': True}
|
|
89
|
+
reads = list( self.db.meterReads.find({'datetime': timerange }, projection) )
|
|
90
|
+
for mRead in reads:
|
|
91
|
+
sdate = mRead['datetime'].strftime('%F')
|
|
92
|
+
result.append( [sdate, mRead['reading'] ] )
|
|
93
|
+
return self.returnValue(True, result)
|
|
94
|
+
|
|
95
|
+
@cherrypy.expose
|
|
96
|
+
def shutdown(self):
|
|
97
|
+
'''
|
|
98
|
+
Shutdown the server.
|
|
99
|
+
'''
|
|
100
|
+
log.info('Server shutting down...')
|
|
101
|
+
cherrypy.engine.exit()
|
|
102
|
+
log.info('CherryPy Server exit.')
|
|
103
|
+
|
|
104
|
+
class GoogleGraphsFS(SmartMeterController):
|
|
105
|
+
def __init__(self, uiPath: str = None):
|
|
106
|
+
log.info(f'Serving files from {uiPath}')
|
|
107
|
+
fsloader = jinja2.FileSystemLoader( uiPath )
|
|
108
|
+
self.view = jinja2.Environment(loader=fsloader)
|
|
109
|
+
cherrypy.response.headers['Access-Control-Allow-Origin'] = '*'
|
|
110
|
+
cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
|
111
|
+
|
|
112
|
+
@cherrypy.expose
|
|
113
|
+
def index(self, **kwargs):
|
|
114
|
+
return self.view.get_template('index.html').render(
|
|
115
|
+
page='<p>Index Page.</p>',
|
|
116
|
+
navigation='<li><a href="/user/login">Login</a></li>'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def main():
|
|
120
|
+
'''
|
|
121
|
+
Main application/API entry point.
|
|
122
|
+
'''
|
|
123
|
+
cherrypy._cplogging.LogManager.time = lambda self: datetime.now().strftime('%F %T')
|
|
124
|
+
# Default access_log_format '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
|
|
125
|
+
# h - remote.ip, l - "-", u - login (or "-"), t - time, r - request line, s - status, b - content length
|
|
126
|
+
# f - referer, a - User Agent, o - Host or -, i - request.unique_id, z - UtcTime
|
|
127
|
+
cherrypy._cplogging.LogManager.access_log_format = '{' + json.dumps({
|
|
128
|
+
'time': '{t}',
|
|
129
|
+
'from': '{h}',
|
|
130
|
+
'user': '{u}',
|
|
131
|
+
'host': '{o}',
|
|
132
|
+
'status': '{s}',
|
|
133
|
+
'bytes': '{b}',
|
|
134
|
+
'referer': '{f}',
|
|
135
|
+
'agent': '{a}'
|
|
136
|
+
}) + '}'
|
|
137
|
+
#'{t} from={h} user={u} host={o} status={s} bytes={b} referer="{f}" agent="{a}"'
|
|
138
|
+
config = getConfig()
|
|
139
|
+
ui_path = os.path.realpath( config.get('server', {}).get('ui.path', DEFAULT_UI_PATH) )
|
|
140
|
+
if not os.path.exists(ui_path):
|
|
141
|
+
possible_paths = [
|
|
142
|
+
os.path.join(os.getenv('HOME', '/home/markizano'), '.local', 'share', 'smartmetertx'),
|
|
143
|
+
os.path.join(sys.prefix, 'local', 'share', 'smartmetertx'),
|
|
144
|
+
os.path.join(sys.prefix, 'share', 'smartmetertx'),
|
|
145
|
+
]
|
|
146
|
+
for ppath in possible_paths:
|
|
147
|
+
if os.path.exists(ppath):
|
|
148
|
+
ui_path = ppath
|
|
149
|
+
break
|
|
150
|
+
else:
|
|
151
|
+
log.debug(f'Could not find UI in path: {ppath}')
|
|
152
|
+
if not os.path.exists(ui_path):
|
|
153
|
+
log.error(f'Could not find UI path: {ui_path}')
|
|
154
|
+
return 1
|
|
155
|
+
apiConfig = {
|
|
156
|
+
'tools.trailing_slash.on': False,
|
|
157
|
+
'tools.json_in.on': True,
|
|
158
|
+
'tools.staticdir.on': False,
|
|
159
|
+
}
|
|
160
|
+
serverConfig = {
|
|
161
|
+
'tools.trailing_slash.on': False,
|
|
162
|
+
'tools.staticdir.on': True,
|
|
163
|
+
'tools.staticdir.dir': ui_path
|
|
164
|
+
}
|
|
165
|
+
log.info('Starting SmartMeterTX Server...')
|
|
166
|
+
log.debug(config)
|
|
167
|
+
smtx = MeterServer(config)
|
|
168
|
+
content = GoogleGraphsFS( serverConfig['tools.staticdir.dir'] )
|
|
169
|
+
log.debug(f'Got config: {config}')
|
|
170
|
+
cherrypy.config.update(config.get('daemon', {}).get('cherrypy', {}))
|
|
171
|
+
cherrypy.tree.mount(smtx, '/api', { '/': apiConfig } )
|
|
172
|
+
cherrypy.tree.mount(content, '/', {'/': serverConfig })
|
|
173
|
+
cherrypy.engine.start()
|
|
174
|
+
cherrypy.engine.block()
|
|
175
|
+
return 0
|
|
176
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'''
|
|
2
|
+
\x1b[31mModule-Level Documentation!\x1b[0m
|
|
3
|
+
'''
|
|
4
|
+
import os
|
|
5
|
+
import dateparser
|
|
6
|
+
import pymongo
|
|
7
|
+
import gnupg
|
|
8
|
+
import json
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from kizano import getConfig, getLogger
|
|
12
|
+
from smartmetertx.api import MeterReader
|
|
13
|
+
from smartmetertx.utils import getMongoConnection
|
|
14
|
+
|
|
15
|
+
log = getLogger(__name__)
|
|
16
|
+
HOME = os.getenv('HOME', '')
|
|
17
|
+
SMTX_FROM = dateparser.parse(os.environ.get('SMTX_FROM', 'day before yesterday'))
|
|
18
|
+
SMTX_TO = dateparser.parse(os.environ.get('SMTX_TO', 'today'))
|
|
19
|
+
|
|
20
|
+
class Smtx2Mongo(object):
|
|
21
|
+
'''
|
|
22
|
+
SmartMeterTexas -> MongoDB
|
|
23
|
+
Model object to take the records we get from https://smartmetertexas.com and insert them into
|
|
24
|
+
a mongodb we control for data preservation and other analytics on our electric usage data we
|
|
25
|
+
would want to undertake.
|
|
26
|
+
'''
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.config = getConfig()
|
|
30
|
+
self.mongo = getMongoConnection(self.config)
|
|
31
|
+
self.db = self.mongo.get_database(self.config['mongo'].get('dbname', 'smartmetertx'))
|
|
32
|
+
self.getSMTX()
|
|
33
|
+
self.ensureIndexes()
|
|
34
|
+
|
|
35
|
+
def close(self):
|
|
36
|
+
if self.mongo:
|
|
37
|
+
self.mongo.close()
|
|
38
|
+
self.mongo = None
|
|
39
|
+
|
|
40
|
+
def ensureIndexes(self):
|
|
41
|
+
self.db.meterReads.create_index(
|
|
42
|
+
[('reading', 1)],
|
|
43
|
+
background=True
|
|
44
|
+
)
|
|
45
|
+
self.db.meterReads.create_index(
|
|
46
|
+
[('datetime', 1)],
|
|
47
|
+
background=True,
|
|
48
|
+
unique=True
|
|
49
|
+
)
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def getSMTX(self):
|
|
53
|
+
log.info('Connecting to SmartMeterTX...')
|
|
54
|
+
gpg = gnupg.GPG(gnupghome=os.path.join(os.getenv('HOME', '/home/markizano') , '.gnupg'), use_agent=True)
|
|
55
|
+
self.smtx = MeterReader()
|
|
56
|
+
smtx_user = self.config['smartmetertx']['user']
|
|
57
|
+
smtx_pass = gpg.decrypt(self.config['smartmetertx']['pass']).data.decode('utf-8').strip()
|
|
58
|
+
self.smtx.login(smtx_user, smtx_pass)
|
|
59
|
+
log.info('Success! Getting meter reads...')
|
|
60
|
+
return self.smtx
|
|
61
|
+
|
|
62
|
+
def getDailyReads(self):
|
|
63
|
+
# Get the meter reads and print the date in the format their API expects.
|
|
64
|
+
reads = self.smtx.get_daily_read(self.config['smartmetertx']['esiid'], SMTX_FROM.strftime('%m/%d/%Y'), SMTX_TO.strftime('%m/%d/%Y'))
|
|
65
|
+
if not reads:
|
|
66
|
+
log.warn('Failed to get records from meterReads()')
|
|
67
|
+
else:
|
|
68
|
+
log.info('Acquired %d meter reads! Inserting into DB...' % len(reads['dailyData']))
|
|
69
|
+
return reads
|
|
70
|
+
|
|
71
|
+
def filterDailyReads(self, dailyData):
|
|
72
|
+
results = []
|
|
73
|
+
log.debug(json.dumps(dailyData, indent=2))
|
|
74
|
+
for meterRead in dailyData:
|
|
75
|
+
meterRead['datetime'] = datetime.strptime('%(date)s %(starttime)s' % meterRead, '%m/%d/%Y %H:%M%p')
|
|
76
|
+
del meterRead['date'], meterRead['starttime']
|
|
77
|
+
meterRead['startreading'] = float(meterRead['startreading'])
|
|
78
|
+
meterRead['endreading'] = float(meterRead['endreading'])
|
|
79
|
+
results.append(meterRead)
|
|
80
|
+
return results
|
|
81
|
+
|
|
82
|
+
def insertDailyData(self, dailyData):
|
|
83
|
+
results = []
|
|
84
|
+
log.info('Inserting %d reads into the DB.' % len(dailyData))
|
|
85
|
+
try:
|
|
86
|
+
insertResult = self.db.meterReads.insert_many(dailyData)
|
|
87
|
+
log.debug(insertResult)
|
|
88
|
+
results.append(insertResult)
|
|
89
|
+
except pymongo.errors.BulkWriteError as e:
|
|
90
|
+
errs = list(filter( lambda x: x['code'] != 11000, e.details['writeErrors'] ))
|
|
91
|
+
if errs:
|
|
92
|
+
raise errs
|
|
93
|
+
log.info('Complete!')
|
|
94
|
+
|
|
95
|
+
def main():
|
|
96
|
+
log.info('Gathering records from %s to %s' % ( SMTX_FROM.strftime('%F/%R'), SMTX_TO.strftime('%F/%R') ) )
|
|
97
|
+
smtx2mongo = Smtx2Mongo()
|
|
98
|
+
reads = smtx2mongo.getDailyReads()
|
|
99
|
+
if not reads:
|
|
100
|
+
log.error('Failed to read smartmetertexas API...')
|
|
101
|
+
return 2
|
|
102
|
+
|
|
103
|
+
dailyData = smtx2mongo.filterDailyReads(reads['dailyData'])
|
|
104
|
+
if dailyData:
|
|
105
|
+
smtx2mongo.insertDailyData(dailyData)
|
|
106
|
+
else:
|
|
107
|
+
log.warning('No records inserted!')
|
|
108
|
+
smtx2mongo.close()
|
|
109
|
+
return 0
|
|
110
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import pymongo
|
|
4
|
+
import gnupg
|
|
5
|
+
import kizano
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
|
|
8
|
+
log = kizano.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
def getMongoConnection(config: kizano.Config):
|
|
11
|
+
# Establish connections to various sources and targets.
|
|
12
|
+
log.info('Connecting to MongoDB...')
|
|
13
|
+
gpg = gnupg.GPG(gnupghome=os.path.join(os.environ['HOME'], '.gnupg'), use_agent=True)
|
|
14
|
+
mongo_user = config['mongo']['username']
|
|
15
|
+
mongo_pass = gpg.decrypt(config['mongo']['password']).data.decode('utf-8')
|
|
16
|
+
mongo_host = config['mongo']['host']
|
|
17
|
+
mongo_opts = urlencode(config['mongo']['opts'])
|
|
18
|
+
mongo_url = f'mongodb://{mongo_user}:{mongo_pass}@{mongo_host}/?{mongo_opts}'
|
|
19
|
+
db = pymongo.MongoClient(mongo_url)
|
|
20
|
+
log.info('Connected!')
|
|
21
|
+
return db
|
|
22
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
/*
|
|
3
|
+
If a designer wants to come make this look more pretty, you're more than welcome!
|
|
4
|
+
Feel free to submit a PR!
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
#mychart {
|
|
8
|
+
max-width: 1280px;
|
|
9
|
+
max-height: 800px;
|
|
10
|
+
margin: 0 auto;
|
|
11
|
+
min-height: 400px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#mytable {
|
|
15
|
+
max-width: 1280px;
|
|
16
|
+
max-height: 800px;
|
|
17
|
+
margin: 0 auto;
|
|
18
|
+
border: 2px solid black;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#mytable thead {
|
|
22
|
+
font-size: 1.7rem;
|
|
23
|
+
font-weight: bold;
|
|
24
|
+
background-color: #f2f2f2;
|
|
25
|
+
padding: 0.5rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#mytable th {
|
|
29
|
+
padding: 0.5rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#mytable td {
|
|
33
|
+
font-size: 1.2rem;
|
|
34
|
+
padding: 0.3rem;
|
|
35
|
+
border-bottom: 1px solid black;
|
|
36
|
+
margin: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.kwh input {
|
|
40
|
+
width: 75px;
|
|
41
|
+
height: 24px;
|
|
42
|
+
font-size: 20px;
|
|
43
|
+
}
|
|
44
|
+
.kwh::before {
|
|
45
|
+
content: '$';
|
|
46
|
+
}
|
|
47
|
+
.kwh::after {
|
|
48
|
+
content: '/kWh';
|
|
49
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>SMT Mongo Metric Reader</title>
|
|
5
|
+
<link rel="stylesheet" type="text/css" href="index.css" />
|
|
6
|
+
<!-- jQuery Core -->
|
|
7
|
+
<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.js"></script>
|
|
8
|
+
|
|
9
|
+
<!-- jQueryUI Dependency -->
|
|
10
|
+
<script type="text/javascript" src="https://code.jquery.com/ui/1.11.3/jquery-ui.js"></script>
|
|
11
|
+
<link rel="stylesheet" href="//code.jquery.com/ui/1.13.1/themes/base/jquery-ui.css">
|
|
12
|
+
|
|
13
|
+
<!-- Google Charts -->
|
|
14
|
+
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
|
|
15
|
+
|
|
16
|
+
<!-- Main Entrypoint to Script for this page. -->
|
|
17
|
+
<script type="text/javascript" src="index.js"></script>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div id="page">
|
|
21
|
+
<header>
|
|
22
|
+
<h2>SmartMeter Texas Meter Reader</h2>
|
|
23
|
+
<p>
|
|
24
|
+
This chart shows data collected from the SmartMeter reader stored in the configured mongodb.
|
|
25
|
+
This software allows the overlay of additional data to draw correlations from other related
|
|
26
|
+
data that can be overlayed on top of this line chart.
|
|
27
|
+
</p>
|
|
28
|
+
</header>
|
|
29
|
+
<controls>
|
|
30
|
+
From Date: <input type="text" id="fdate" />
|
|
31
|
+
To Date: <input type="text" id="tdate" /><br />
|
|
32
|
+
</controls>
|
|
33
|
+
<div id="mychart"></div>
|
|
34
|
+
<table id="mytable"></table>
|
|
35
|
+
</div>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
|
|
2
|
+
let MeterRender = {};
|
|
3
|
+
(function(MeterRender){
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Event handler for refreshing the table with the new kWh rate based on what's in the textbox.
|
|
7
|
+
* @param {Event} event The event that triggered the request.
|
|
8
|
+
* @returns {void}
|
|
9
|
+
*/
|
|
10
|
+
MeterRender.onKwhChange = function onKwhChange(event) {
|
|
11
|
+
let kwh = $('#kwh').val();
|
|
12
|
+
let tbody = $('#mytable tbody');
|
|
13
|
+
let rows = tbody.find('tr');
|
|
14
|
+
rows.each((i, r) => {
|
|
15
|
+
let row = $(r);
|
|
16
|
+
let consumption = parseFloat(row.find('td').eq(2).text());
|
|
17
|
+
row.find('td').eq(3).text('$' + (consumption * kwh).toFixed(2));
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Draw a table of the monthly aggregate of consumption from the meter reads.
|
|
23
|
+
* The meter reads will come with a day and a meter read. This function will
|
|
24
|
+
* group them by month and show the consumption in a monthly listing.
|
|
25
|
+
* @param {Object} meterReads Meter read data from server.
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
MeterRender.drawTable = function drawTable(meterReads) {
|
|
29
|
+
var table = $('#mytable');
|
|
30
|
+
table.empty();
|
|
31
|
+
let months = meterReads.value.reduce((m, x) => {
|
|
32
|
+
// get the month as 2 digits always
|
|
33
|
+
let d = ((d) => `${d.getFullYear()}-${('0'+d.getMonth()).slice(-2)}`)(new Date(x[0]));
|
|
34
|
+
console.log(d);
|
|
35
|
+
if ( typeof m[d] === "undefined" ) m[d] = 0;
|
|
36
|
+
if ( typeof x[1] !== "number")
|
|
37
|
+
console.log("!!!" + x[1]);
|
|
38
|
+
m[d] += x[1];
|
|
39
|
+
return m;
|
|
40
|
+
}, {});
|
|
41
|
+
let monthKeys = Object.keys(months);
|
|
42
|
+
monthKeys.sort();
|
|
43
|
+
var thead = $('<thead>');
|
|
44
|
+
var tr = $('<tr><th>Year</th><th>Month</th><th>Consumption</th></tr>')
|
|
45
|
+
var kwh = $('<input>')
|
|
46
|
+
.attr('type', 'number')
|
|
47
|
+
.attr('id', 'kwh')
|
|
48
|
+
.attr('name', 'kwh')
|
|
49
|
+
.attr('placeholder', 'kWh')
|
|
50
|
+
.attr('step', 0.01)
|
|
51
|
+
.val(0.13)
|
|
52
|
+
.on('change', MeterRender.onKwhChange);
|
|
53
|
+
var th = $('<th>').addClass('kwh').append(kwh);
|
|
54
|
+
tr.append(th);
|
|
55
|
+
thead.append(tr);
|
|
56
|
+
var tbody = $('<tbody>');
|
|
57
|
+
monthKeys.forEach((k) => {
|
|
58
|
+
let date = k.split('-');
|
|
59
|
+
let row = $('<tr>');
|
|
60
|
+
row.append($(`<td></td>`).text(date[0]));
|
|
61
|
+
row.append($(`<td></td>`).text(parseInt(date[1]) + 1));
|
|
62
|
+
row.append($(`<td></td>`).text(months[k].toFixed(2) + ' kWh'));
|
|
63
|
+
row.append($(`<td></td>`).text('$' + (months[k] * kwh.val()).toFixed(2)));
|
|
64
|
+
tbody.append(row);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
table.append(thead);
|
|
68
|
+
table.append(tbody);
|
|
69
|
+
console.log('Done populating table of meter reads.')
|
|
70
|
+
console.log(meterReads);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Google handler for drawing the chart.
|
|
76
|
+
* @param {Object} meterReads Meter read data from server.
|
|
77
|
+
* @returns {void}
|
|
78
|
+
*/
|
|
79
|
+
MeterRender.drawChart = function drawChart(meterReads) {
|
|
80
|
+
var data = new google.visualization.DataTable();
|
|
81
|
+
data.addColumn('number', 'Date');
|
|
82
|
+
data.addColumn('number', 'Reading');
|
|
83
|
+
let reads = [...meterReads.value];
|
|
84
|
+
reads.unshift(['Date', 'Reading']);
|
|
85
|
+
var data = google.visualization.arrayToDataTable(reads);
|
|
86
|
+
var options = {
|
|
87
|
+
title: 'SmartMeter Texas Meter Reads',
|
|
88
|
+
legend: { position: 'bottom' }
|
|
89
|
+
};
|
|
90
|
+
var chart = new google.visualization.LineChart(document.getElementById('mychart'));
|
|
91
|
+
chart.draw(data, options);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Request the meter reads from the server.
|
|
96
|
+
* Sample response from the server looks like:
|
|
97
|
+
* {
|
|
98
|
+
* error: false,
|
|
99
|
+
* status: 200,
|
|
100
|
+
* value: [
|
|
101
|
+
* [
|
|
102
|
+
* "2022-01-01",
|
|
103
|
+
* 00.000
|
|
104
|
+
* ]
|
|
105
|
+
* }
|
|
106
|
+
* }
|
|
107
|
+
*
|
|
108
|
+
* @param {Event} event The event that triggered the request.
|
|
109
|
+
* @returns {void}
|
|
110
|
+
*/
|
|
111
|
+
MeterRender.requestMetrics = function requestMetrics(event) {
|
|
112
|
+
let fdate = encodeURI( $('#fdate').val() ), tdate = encodeURI( $('#tdate').val() );
|
|
113
|
+
$.ajax({
|
|
114
|
+
url: `/api/meterReads?fdate=${fdate}&tdate=${tdate}`,
|
|
115
|
+
}).done(meterReads => {
|
|
116
|
+
MeterRender.drawChart(meterReads);
|
|
117
|
+
MeterRender.drawTable(meterReads);
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* On load event handler.
|
|
123
|
+
* @param {Event} event The event that triggered the request.
|
|
124
|
+
* @returns {void}
|
|
125
|
+
*/
|
|
126
|
+
MeterRender.onLoad = function onLoad(event) {
|
|
127
|
+
let fdate = $('#fdate'), tdate = $('#tdate');
|
|
128
|
+
let sixmonths = new Date( Date.now() - (86400 * 1000 * 365) );
|
|
129
|
+
let yday = new Date(Date.now() - 86400000);
|
|
130
|
+
let dtopts = {
|
|
131
|
+
changeMonth: true,
|
|
132
|
+
changeYear: true,
|
|
133
|
+
dateFormat: "yy-mm-dd"
|
|
134
|
+
};
|
|
135
|
+
fdate.datepicker(dtopts);
|
|
136
|
+
tdate.datepicker(dtopts);
|
|
137
|
+
|
|
138
|
+
fdate.val(`${sixmonths.getFullYear()}-${sixmonths.getMonth()+1}-${sixmonths.getDate()}`);
|
|
139
|
+
tdate.val(`${yday.getFullYear()}-${yday.getMonth()+1}-${yday.getDate()}`);
|
|
140
|
+
|
|
141
|
+
fdate.change(MeterRender.requestMetrics)
|
|
142
|
+
tdate.change(MeterRender.requestMetrics)
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
google.charts.load('current', {'packages': ['corechart']});
|
|
146
|
+
google.charts.setOnLoadCallback(MeterRender.requestMetrics);
|
|
147
|
+
return MeterRender;
|
|
148
|
+
})(MeterRender);
|
|
149
|
+
window.MeterRenderer = MeterRender;
|
|
150
|
+
|
|
151
|
+
$(document).ready(MeterRender.onLoad);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[options]
|
|
2
|
+
include_package_data = True
|
|
3
|
+
|
|
4
|
+
[tool:pytest]
|
|
5
|
+
testpaths = tests
|
|
6
|
+
python_files = tests/runtests.py
|
|
7
|
+
|
|
8
|
+
[nosetests]
|
|
9
|
+
detailed-errors = 1
|
|
10
|
+
with-coverage = 1
|
|
11
|
+
cover-package = smartmetertx
|
|
12
|
+
cover-inclusive = 1
|
|
13
|
+
cover-html = 1
|
|
14
|
+
cover-html-dir = tests/coverage/
|
|
15
|
+
debug = nose.loader
|
|
16
|
+
pdb = 0
|
|
17
|
+
pdb-failures = 0
|
|
18
|
+
nocapture = 1
|
|
19
|
+
tests = tests/runtests.py
|
|
20
|
+
|
|
21
|
+
[egg_info]
|
|
22
|
+
tag_build =
|
|
23
|
+
tag_date = 0
|
|
24
|
+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os, io
|
|
4
|
+
import sys
|
|
5
|
+
from glob import glob
|
|
6
|
+
from pprint import pprint
|
|
7
|
+
from setuptools import setup
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import yaml
|
|
11
|
+
except ImportError:
|
|
12
|
+
os.system('pip3 install PyYAML')
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.abspath('.'))
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
PATCH = io.open('.build-id').read().strip()
|
|
19
|
+
except:
|
|
20
|
+
try:
|
|
21
|
+
pkginfo = yaml.safe_load(io.open('PKG-INFO').read())
|
|
22
|
+
PATCH = pkginfo['Version'].split('.').pop()
|
|
23
|
+
except Exception as e:
|
|
24
|
+
PATCH = '0'
|
|
25
|
+
|
|
26
|
+
setup_opts = {
|
|
27
|
+
'name' : 'smartmetertx2mongo',
|
|
28
|
+
# We change this default each time we tag a release.
|
|
29
|
+
'version' : f'1.2.{PATCH}',
|
|
30
|
+
'description' : 'Implementation of smartmetertx to save records to mongodb with config driven via YAML.',
|
|
31
|
+
'author' : 'Markizano Draconus',
|
|
32
|
+
'author_email' : 'markizano@markizano.net',
|
|
33
|
+
'url' : 'https://markizano.net/',
|
|
34
|
+
'license' : 'GNU',
|
|
35
|
+
'tests_require' : ['nose', 'mock', 'coverage'],
|
|
36
|
+
'install_requires' : [
|
|
37
|
+
'dateparser',
|
|
38
|
+
'kizano',
|
|
39
|
+
'pymongo',
|
|
40
|
+
'requests',
|
|
41
|
+
'cherrypy',
|
|
42
|
+
'PyYAML',
|
|
43
|
+
'python-gnupg',
|
|
44
|
+
'jinja2',
|
|
45
|
+
],
|
|
46
|
+
'package_dir' : { 'smartmetertx': 'lib/smartmetertx' },
|
|
47
|
+
'packages' : [
|
|
48
|
+
'smartmetertx',
|
|
49
|
+
],
|
|
50
|
+
'scripts' : glob('bin/*'),
|
|
51
|
+
'test_suite' : 'tests',
|
|
52
|
+
'data_files' : [
|
|
53
|
+
('share/smartmetertx', glob('lib/ui/*')),
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
import argparse
|
|
59
|
+
HAS_ARGPARSE = True
|
|
60
|
+
except:
|
|
61
|
+
HAS_ARGPARSE = False
|
|
62
|
+
|
|
63
|
+
if not HAS_ARGPARSE: setup_opts['install_requires'].append('argparse')
|
|
64
|
+
|
|
65
|
+
# I botch this too many times.
|
|
66
|
+
if sys.argv[1] == 'test':
|
|
67
|
+
sys.argv[1] = 'nosetests'
|
|
68
|
+
|
|
69
|
+
if 'DEBUG' in os.environ: pprint(setup_opts)
|
|
70
|
+
|
|
71
|
+
setup(**setup_opts)
|
|
72
|
+
|
|
73
|
+
if 'sdist' in sys.argv:
|
|
74
|
+
import gnupg, hashlib
|
|
75
|
+
gpg = gnupg.GPG()
|
|
76
|
+
for artifact in glob('dist/*.tar.gz'):
|
|
77
|
+
# Detach sign the artifact in dist/ folder.
|
|
78
|
+
fd = open(artifact, 'rb')
|
|
79
|
+
checksums = open('dist/CHECKSUMS.txt', 'w+b')
|
|
80
|
+
status = gpg.sign_file(fd, detach=True, output=f'{artifact}.asc')
|
|
81
|
+
print(f'Signed {artifact} with {status.fingerprint}')
|
|
82
|
+
|
|
83
|
+
# create a MD5, SHA1 and SHA256 hash of the artifact.
|
|
84
|
+
for hashname in ['md5', 'sha1', 'sha256']:
|
|
85
|
+
hasher = getattr(hashlib, hashname)()
|
|
86
|
+
fd.seek(0,0)
|
|
87
|
+
hasher.update(fd.read())
|
|
88
|
+
digest = hasher.hexdigest()
|
|
89
|
+
checksums.write(f'''{hashname.upper()}:
|
|
90
|
+
{digest} {artifact}
|
|
91
|
+
|
|
92
|
+
'''.encode('utf-8'))
|
|
93
|
+
print(f'Got {artifact}.{hashname} as {digest}')
|
|
94
|
+
checksums.seek(0, 0)
|
|
95
|
+
chk_status = gpg.sign_file(checksums, detach=True, output=f'dist/CHECKSUMS.txt.asc')
|
|
96
|
+
checksums.close()
|
|
97
|
+
fd.close()
|
|
98
|
+
print(f'Signed CHECKSUMS.txt with {chk_status.fingerprint}')
|
|
99
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: smartmetertx2mongo
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Implementation of smartmetertx to save records to mongodb with config driven via YAML.
|
|
5
|
+
Home-page: https://markizano.net/
|
|
6
|
+
Author: Markizano Draconus
|
|
7
|
+
Author-email: markizano@markizano.net
|
|
8
|
+
License: GNU
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.cfg
|
|
3
|
+
setup.py
|
|
4
|
+
bin/fetchMeterReads.cron.py
|
|
5
|
+
bin/smtx-server.py
|
|
6
|
+
lib/smartmetertx/__init__.py
|
|
7
|
+
lib/smartmetertx/api.py
|
|
8
|
+
lib/smartmetertx/controller.py
|
|
9
|
+
lib/smartmetertx/server.py
|
|
10
|
+
lib/smartmetertx/smtx2mongo.py
|
|
11
|
+
lib/smartmetertx/utils.py
|
|
12
|
+
lib/ui/index.css
|
|
13
|
+
lib/ui/index.html
|
|
14
|
+
lib/ui/index.js
|
|
15
|
+
smartmetertx2mongo.egg-info/PKG-INFO
|
|
16
|
+
smartmetertx2mongo.egg-info/SOURCES.txt
|
|
17
|
+
smartmetertx2mongo.egg-info/dependency_links.txt
|
|
18
|
+
smartmetertx2mongo.egg-info/requires.txt
|
|
19
|
+
smartmetertx2mongo.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
smartmetertx
|