diff --git a/pid_handler.py b/pid_handler.py index 9ff3bb59..04a34bd3 100644 --- a/pid_handler.py +++ b/pid_handler.py @@ -19,6 +19,7 @@ import json import memcache_keys import common +import itertools from model import Manufacturer, Pid, Responder, SUBDEVICE_RANGE_DICT from utils import StringToInt from google.appengine.api import memcache @@ -112,11 +113,7 @@ def GetResults(self): return [] -class DisplayPid(common.BasePageHandler): - """Display information about a particular PID.""" - - TEMPLATE = 'templates/display_pid.tmpl' - +class PidRoute(webapp.RequestHandler): def LookupPIDFromRequest(self): pid_id = self.request.get('pid') try: @@ -138,6 +135,12 @@ def LookupPIDFromRequest(self): else: return None + +class DisplayPid(common.BasePageHandler, PidRoute): + """Display information about a particular PID.""" + + TEMPLATE = 'templates/display_pid.tmpl' + def CopyToDict(self, input_d, output_d, keys): for key in keys: if key in input_d: @@ -240,6 +243,195 @@ def GetTemplateData(self): output['set_command'] = command return output +class DisplayPidJSON(PidRoute): + """Fetch E1.37-5 format JSON about a PID.""" + + CUSTOM_CAPITALIZE_TESTS = [ + ("dmx_start_address", "DMX Start Address"), + ("foo-dmx", "Foo DMX"), + ("mini_dmxter_device", "Mini Dmxter Device"), + ("this-is_a_test", "This Is A Test"), + ("ip_address", "IP Address"), + ("controller_ip_address", "Controller IP Address"), + ("dazzled_led_type", "Dazzled LED Type"), + ("device_rdm_uid", "Device RDM UID"), + ("dns_via_ipv4_dhcp", "DNS Via IPv4 DHCP"), + ] + + @staticmethod + def CustomCapitalizeLabel(label): + TRANSFORMS = { + 'asc', + 'dhcp', + 'dmx', + 'dns', + 'ip', + 'json', + 'led', + 'nsc', + 'pdl', + 'pid', + 'rdm', + 'uid', + 'url', + } + TRANSFORMS = {x:x.upper() for x in TRANSFORMS} + TRANSFORMS['ipv4'] = 'IPv4' + TRANSFORMS['ipv6'] = 'IPv6' + TRANSFORMS['mdmx'] = 'mDMX' + + label = label.replace('-', ' ').replace('_', ' ').strip() + if len(label) == 0: return '' + + end = [] + for part in label.split(' '): + if TRANSFORMS.get(part.lower()): + end.append(TRANSFORMS[part.lower()]) + elif len(part) > 0: + end.append(part[0].upper() + part[1:]) + else: + end.append('') + + return ' '.join(end) + + @staticmethod + def ConvertItems(items): + out = [] + for item in items: + a = {'name':item['name'], 'displayName':DisplayPidJSON.CustomCapitalizeLabel(item['name'])} + if item['type'] == 'bool': + a['type'] = 'boolean' + out.append(a) + elif item['type'] in ['uint8', 'uint16', 'uint32', 'uint64', 'int8', 'int16', 'int32', 'int64']: + a['type'] = item['type'] + if item.get('multiplier'): + a['prefixPower'] = item['multiplier'] + if item.get('range'): + a['ranges'] = [{'minimum':r[0], 'maximum':r[1]} for r in item['range']] + if item.get('labels'): + a['labels'] = [{'value':l[0], 'name':l[1]} for l in item['labels']] + if not item.get('ranges'): + a['restrictToLabeled'] = True + out.append(a) + elif item['type'] == 'string': + a['type'] = item['type'] + if item.get('max_size'): a['maxBytes'] = item['max_size'] + if item.get('min_size'): a['minBytes'] = item['min_size'] + # could restrictToASCII here?? + out.append(a) + elif item['type'] == 'ipv4': + a['type'] = 'bytes' + a['format'] = 'ipv4' + out.append(a) + elif item['type'] == 'ipv6': + a['type'] = 'bytes' + a['format'] = 'ipv6' + out.append(a) + elif item['type'] == 'mac': + a['type'] = 'bytes' + a['format'] = 'mac-address' + out.append(a) + elif item['type'] == 'uid': + a['type'] = 'bytes' + a['format'] = 'uid' + out.append(a) + elif item['type'] == 'group': + a['type'] = 'list' + if item.get('max_size'): a['minItems'] = item['max_size'] + if item.get('min_size'): a['maxItems'] = item['min_size'] + converted = DisplayPidJSON.ConvertItems(item['items']) + if len(converted) == 1: + a['itemType'] = converted[0] + else: + a['itemType'] = { + 'type': 'compound', + 'subtypes': converted + } + out.append(a) + else: + # WARNING: invalid item type?? + pass + return out + + @staticmethod + def SubDeviceRange(subs): + if subs == 0: + return ['root'] + elif subs == 1: + return ['root', 'subdevices', 'broadcast'] + elif subs == 2: + return ['root', 'subdevices'] + elif subs == 3: + return ['subdevices'] + else: # should never happen + return [] + + @staticmethod + def BuildCommand(output, command, prefix): + request = eval(command.request) + response = eval(command.response) + output[prefix+'_request'] = DisplayPidJSON.ConvertItems(request['items']) + output[prefix+'_response'] = DisplayPidJSON.ConvertItems(response['items']) + output[prefix+'_request_subdevice_range'] = DisplayPidJSON.SubDeviceRange(command.sub_device_range) + # response subdevices are always 'match' which is the default + + @staticmethod + def CollapseMessages(output, message_a, message_b): + if isinstance(output.get(message_a), list) and isinstance(output.get(message_b), list): + if output[message_a] == output[message_b] and len(output[message_a]) != 0: + output[message_b] = message_a + + @staticmethod + def ConvertPid(pid): + output = { + 'name': pid.name, + 'manufacturer_id': pid.manufacturer.esta_id, + 'pid': pid.pid_id, + 'version': 0, + } + if pid.notes: + output['notes'] = pid.notes + if pid.link: + output['resources'] = [pid.link] + + # add get information + if pid.get_command: + DisplayPidJSON.BuildCommand(output, pid.get_command, 'get') + if pid.set_command: + DisplayPidJSON.BuildCommand(output, pid.set_command, 'set') + + # collapse repeated commands + message_names = ['get_request', 'get_response', 'set_request', 'set_response'] + for (a, b) in itertools.combinations(message_names, 2): + DisplayPidJSON.CollapseMessages(output, a, b) + + # collapse repeated properties + # we get a list of them all + # and because python objects are references, + # we can just update this list in-place + prop_list = [] + for m in message_names: + if output.get(m) and isinstance(output[m], list): + prop_list += [('#/'+m+'/'+str(i), prop) for (i, prop) in enumerate(output[m])] + for i in xrange(1, len(prop_list)): + for j in xrange(0, i): + if prop_list[i][1] == prop_list[j][1]: + prop_list[i][1].clear() + prop_list[i][1]['$ref'] = prop_list[j][0] + + return output + + def get(self): + pid = self.LookupPIDFromRequest() + if not pid: + self.error(404) + return + + converted = DisplayPidJSON.ConvertPid(pid) + + # use the Content-Type recommended in E1.37-5 + self.response.headers['Content-Type'] = 'application/schema-instance+json' + self.response.out.write(json.dumps(converted, indent=2)) pid_application = webapp.WSGIApplication( [ @@ -247,5 +439,6 @@ def GetTemplateData(self): ('/pid/name', SearchByName), ('/pid/id', SearchById), ('/pid/display', DisplayPid), + ('/pid/display.json', DisplayPidJSON), ], debug=True)