commit ba6d53e8dac1cefd9dc86f49c5edecba8babc888 Author: Eric Sandeen Date: Tue Jul 8 21:43:18 2014 -0500 btmon: Add Bidgely API 1.0.1 service While the old enersave processor works suitably w/ Bidgely (which is the new incarnation of enersave), it's not clear how long the old API will be available, and I was never able to get it to do proper appliance disambiguation. This patch adds a new Bidgely processor, per the API v1.0.1 specs. A couple notable changes: * It requires the full Bidgely upload URL, rather than only a token because the URL contains both the token and house/meter numbers, so it requires 3 bits of information to construct the URL. * It requires a map, so that one channel can be specified as the "total load" for the house, which (AFAICT) Bidgely requires. There is no good way to guess at defaults, so a map is required for this service. (It might make sense to add an option to specify only 3 items in the map: total load, total generation, and net load, and then iterate over all remaining channels treating them as subcircuit loads, but even this isn't perfect: there can be generation subcircuits as well.) diff --git a/bin/btmon.py b/bin/btmon.py index 927ea00..602c834 100755 --- a/bin/btmon.py +++ b/bin/btmon.py @@ -6,9 +6,9 @@ Collect data from Brultech ECM-1240, ECM-1220, and GEM power monitors. Print the data, save the data to database, or upload the data to a server. Includes support for uploading to the following services: - * MyEnerSave/Bidgely * SmartEnergyGroups * pachube/cosm/xively * WattzOn - * PlotWatt * PeoplePower * thingspeak * Eragy - * emoncms + * MyEnerSave * SmartEnergyGroups * pachube/cosm/xively * WattzOn + * PlotWatt * PeoplePower * thingspeak * Eragy + * emoncms * Bidgely Thanks to: Amit Snyderman @@ -270,7 +270,50 @@ pw_api_key=XXXXXXXXXXXXXXXX pw_map=399999_ch1,1000,399999_ch2,1001 -EnerSave/Bidgely Configuration: +Bidgely Configuration: + +1) Choose "Get Started" at http://www.bidgely.com. +2) Select your zip code, etc. +3) Choose any monitor type during signup; "Brultech ECM-1240" is fine +4) Enter your email and password to create your accoun +5) Choose "Start Setup" then "Connect Energy Monitor" +6) Choose "I have a different Energy Monitor" +7) Choose "Bidgely API" from the list, and "Connect this monitor" +8) Choose "I have downloaded the API document" +9) Note your Upload URL and *SAVE IT* - you cant find it again +10) Add the upload URL to your config file by_url= parameter +11) Click "Connect to Bidgely" and start btmon. + +Define labels for ECM channels as a comma-delimited list of tuples. Each +tuple contains an id, description, type. If no map is specified, data from +all channels will be uploaded, generic labels will be assigned, and the type +for each channel will default to "load." + +Bidgely defines the following types: + + 0 - load (total consumption on site) + 1 - generation (total generation on site) + 2 - net metering (net consumption + generation for all circuits) + 10 - standalone load (DFLT) (subset of total load, i.e. single circuit) + 11 - standalone generation (subset of total generation, i.e. single circuit) + +Note that types 0, 1, and 2 should each only be assigned to one circuit +in the map. Types 10 and 11 may be assigned to several circuits. + +Although type 10 (individual circuit) is the default, you should always define +one map item with type 0, for total load, to make Bidgely happy. + +For this reason, a map is required to use the Bidgely service, and it should +contain at least one channel of type 0 for total load. + +For example, via configuration file: + +[bidgely] +by_url=https://api.bidgely.com/v1/users/VERY_LONG_TOKEN/homes/1/gateways/1/upload +by_map=399999_ch1,mains,0,399999_ch2,solar array,1,399999_aux1,hot tub,10, + + +EnerSave Configuration: (NOTE: Enersave is deprecated. Use Bidgely) 1) create an account 2) obtain a token @@ -945,7 +988,17 @@ PLOTWATT_MAP = '' PLOTWATT_HOUSE_ID = '' PLOTWATT_API_KEY = '' -# EnerSave/Bidgely defaults +# Bidgely defaults +# Minimum upload interval is 60 seconds. +# Recommended sampling interval is 2 to 30 seconds. +# the map is a comma-delimited list of channel,description,type tuples +# 311111_ch1,living room,2,311112_ch2,solar,1,311112_aux4,kitchen,2 +BY_UPLOAD_PERIOD = 60 # seconds +BY_TIMEOUT = 60 # seconds +BY_MAP = '' +BY_DEFAULT_TYPE = 10 + +# EnerSave defaults # Minimum upload interval is 60 seconds. # Recommended sampling interval is 2 to 30 seconds. # the map is a comma-delimited list of channel,description,type tuples @@ -3042,6 +3095,98 @@ class WattzOnProcessor(UploadProcessor): return req +# format for the Bidgely uploads is based on the pdf document called +# 'Bidgely Developer API v1.0.1' from 5/27/13. +# +# the energy measurements must be sorted by timestamp from oldest to newest, +# and the value of the energy reading is a cumulative measurement of energy. +class BidgelyProcessor(UploadProcessor): + def __init__(self, url, map, period, timeout): + super(BidgelyProcessor, self).__init__() + self.url = url + self.map_str = map + self.process_period = int(period) + self.timeout = int(timeout) + + infmsg('BY: upload period: %d' % self.process_period) + infmsg('BY: url: %s' % self.url) + infmsg('BY: map: %s' % self.map_str) + + def setup(self): + if not (self.url and self.map_str): + print 'Bidgely Error: Insufficient parameters' + if not self.url: + print ' No URL' + if not self.map_str: + print ' No Map' + sys.exit(1) + + self.map = self.tuples2dict(self.map_str) + + def tuples2dict(self, s): + items = s.split(',') + m = {} + for k,d,t in zip(items[::3], items[1::3], items[2::3]): + m[k] = { 'desc':d, 'type':t } + return m + + def process_calculated(self, packets): + sensors = {} + wh_readings = {} + w_readings = {} + for p in packets: + for c in PACKET_FORMAT.channels(FILTER_PE_LABELS): + key = mklabel(p['serial'], c) + if key in self.map: + tpl = self.map[key] + dev_id = mklabel(obfuscate_serial(p['serial']), c) + dev_type = tpl['type'] or BY_DEFAULT_TYPE + dev_desc = tpl['desc'] or dev_id + sensors[dev_id] = { 'type':dev_type, 'desc':dev_desc } + if not dev_id in wh_readings: + wh_readings[dev_id] = [] + wh_readings[dev_id].append('' % + (p['time_created'], p[c+'_wh'])) + if not dev_id in w_readings: + w_readings[dev_id] = [] + w_readings[dev_id].append('' % + (p['time_created'], p[c+'_w'])) + s = [] + for key in sensors: + # FIXME different ID for generation + s.append('' % + (key, sensors[key]['type'], sensors[key]['desc'])) + s.append('') + s.append('' % + (sensors[key]['desc'])) + s.append(''.join(wh_readings[key])) + s.append('') + s.append('' % + (sensors[key]['desc'])) + s.append(''.join(w_readings[key])) + s.append('') + s.append('') + s.append('') + if len(s): + s.insert(0, '') + s.insert(1, '') + s.insert(2, '') + s.append('') + s.append('') + self._urlopen(self.url, ''.join(s)) + # FIXME: check for server response + + def _handle_urlopen_error(self, e, url, payload): + errmsg(''.join(['%s Error: %s' % (self.__class__.__name__, e), + '\n URL: ' + url, + '\n data: ' + payload,])) + + def _create_request(self, url): + req = super(BidgelyProcessor, self)._create_request(url) + req.add_header("Content-Type", "application/xml") + return req + + class PlotWattProcessor(UploadProcessor): def __init__(self, api_key, house_id, map, period, timeout): super(PlotWattProcessor, self).__init__() @@ -3880,8 +4025,16 @@ if __name__ == '__main__': group.add_option('--pw-timeout', help='timeout period in seconds', metavar='TIMEOUT') parser.add_option_group(group) + group = optparse.OptionGroup(parser, 'Bidgely options') + group.add_option('--bidgely', action='store_true', dest='bidgely_out', default=False, help='upload data using Bidgely API') + group.add_option('--by-url', help='URL', metavar='URL') + group.add_option('--by-map', help='channel-to-device mapping', metavar='MAP') + group.add_option('--by-upload-period', help='upload period in seconds', metavar='PERIOD') + group.add_option('--by-timeout', help='timeout period in seconds', metavar='TIMEOUT') + parser.add_option_group(group) + group = optparse.OptionGroup(parser, 'EnerSave options') - group.add_option('--enersave', action='store_true', dest='enersave_out', default=False, help='upload data using EnerSave API') + group.add_option('--enersave', action='store_true', dest='enersave_out', default=False, help='upload data using EnerSave API (deprecated)') group.add_option('--es-token', help='token', metavar='TOKEN') group.add_option('--es-url', help='URL', metavar='URL') group.add_option('--es-map', help='channel-to-device mapping', metavar='MAP') @@ -4146,13 +4299,14 @@ if __name__ == '__main__': sys.exit(1) # Packet Processor Setup - if not (options.print_out or options.mysql_out or options.sqlite_out or options.rrd_out or options.wattzon_out or options.plotwatt_out or options.enersave_out or options.peoplepower_out or options.eragy_out or options.smartenergygroups_out or options.thingspeak_out or options.pachube_out or options.oem_out or options.wattvision_out or options.pvo_out): + if not (options.print_out or options.mysql_out or options.sqlite_out or options.rrd_out or options.wattzon_out or options.plotwatt_out or options.bidgely_out or options.enersave_out or options.peoplepower_out or options.eragy_out or options.smartenergygroups_out or options.thingspeak_out or options.pachube_out or options.oem_out or options.wattvision_out or options.pvo_out): print 'Please specify one or more processing options (or \'-h\' for help):' print ' --print print to screen' print ' --mysql write to mysql database' print ' --sqlite write to sqlite database' print ' --rrd write to round-robin database' - print ' --enersave upload to EnerSave' + print ' --bidgely upload to Bidgely' + print ' --enersave upload to EnerSave (deprecated)' print ' --eragy upload to Eragy' print ' --oem upload to OpenEnergyMonitor' print ' --pachube upload to Pachube' @@ -4204,6 +4358,12 @@ if __name__ == '__main__': options.pw_map or PLOTWATT_MAP, options.pw_upload_period or PLOTWATT_UPLOAD_PERIOD, options.pw_timeout or PLOTWATT_TIMEOUT)) + if options.bidgely_out: + procs.append(BidgelyProcessor + (options.by_url or BY_URL, + options.by_map or BY_MAP, + options.by_upload_period or BY_UPLOAD_PERIOD, + options.by_timeout or BY_TIMEOUT)) if options.enersave_out: procs.append(EnerSaveProcessor (options.es_url or ES_URL, diff --git a/etc/btmon-sample.cfg b/etc/btmon-sample.cfg index 4c0001c..a354b94 100644 --- a/etc/btmon-sample.cfg +++ b/etc/btmon-sample.cfg @@ -57,6 +57,11 @@ pw_map=399999_ch1,9990,399999_ch2,9991 pw_house_id = pw_api_key = +[bidgely] +bidgely_out = false +by_url = https://api.bidgely.com/v1/users/VERY_LONG_TOKEN/homes/1/gateways/1/upload + +# Enersave is deprecated, use bidgely for new installs, or migrate [enersave] enersave_out = false es_token =