diff --git a/setup.py b/setup.py index 85f5ae6..8c8858c 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='webthing', - version='0.15.0', + version='0.16.0a', description='HTTP Web Thing implementation', long_description=long_description, url='https://github.com/WebThingsIO/webthing-python', @@ -24,7 +24,7 @@ packages=find_packages(exclude=['contrib', 'docs', 'tests']), install_requires=[ 'ifaddr>=0.1.0', - 'jsonschema>=3.2.0', + 'jsonschema>=4.26.0', 'pyee>=8.1.0', 'tornado>=6.1.0', 'zeroconf>=0.28.0', @@ -33,16 +33,16 @@ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14' ], license='MPL-2.0', project_urls={ 'Source': 'https://github.com/WebThingsIO/webthing-python', 'Tracker': 'https://github.com/WebThingsIO/webthing-python/issues', }, - python_requires='>=3.5, <4', + python_requires='>=3.10, <4', ) diff --git a/webthing/action.py b/webthing/action.py index 9c0a95e..01c5667 100644 --- a/webthing/action.py +++ b/webthing/action.py @@ -102,6 +102,10 @@ def cancel(self): """Override this with the code necessary to cancel the action.""" pass + def get_output(self): + """Override this with the code necessary to get the output of the action.""" + pass + def finish(self): """Finish performing the action.""" self.status = 'completed' diff --git a/webthing/property.py b/webthing/property.py index ef7c049..b4f7a37 100644 --- a/webthing/property.py +++ b/webthing/property.py @@ -53,12 +53,20 @@ def as_property_description(self): """ description = deepcopy(self.metadata) - if 'links' not in description: - description['links'] = [] + if 'forms' not in description: + description['forms'] = [] - description['links'].append( + # TODO: The assumption is that all properties are at least readable - but that's probably not true + op = ['readproperty'] + if not self.metadata.get('readOnly'): + op.append('writeproperty') + + if self.metadata.get('observable'): + op.extend(('observeproperty', 'unobserveproperty')) + + description['forms'].append( { - 'rel': 'property', + 'op': op, 'href': self.href_prefix + self.href, } ) diff --git a/webthing/server.py b/webthing/server.py index 9e7edae..4224f85 100644 --- a/webthing/server.py +++ b/webthing/server.py @@ -419,6 +419,36 @@ def get(self, thing_id='0'): self.set_header('Content-Type', 'application/json') self.write(json.dumps(thing.get_properties())) + def put(self, thing_id='0'): + """ + Handle a PUT request. + + thing_id -- ID of the thing this request is for + """ + thing = self.get_thing(thing_id) + if thing is None: + self.set_status(404) + return + + try: + properties = json.loads(self.request.body.decode()) + except ValueError: + self.set_status(400) + return + + for property_name, value in properties.items(): + if thing.has_property(property_name): + try: + thing.set_property(property_name, value) + except PropertyError: + self.set_status(400) + return + else: + self.set_status(404) + return + + self.set_status(204) + class PropertyHandler(BaseHandler): """Handle a request to /properties/.""" @@ -437,9 +467,7 @@ def get(self, thing_id='0', property_name=None): if thing.has_property(property_name): self.set_header('Content-Type', 'application/json') - self.write(json.dumps({ - property_name: thing.get_property(property_name), - })) + self.write(json.dumps(thing.get_property(property_name))) else: self.set_status(404) @@ -456,32 +484,25 @@ def put(self, thing_id='0', property_name=None): return try: - args = json.loads(self.request.body.decode()) + value = json.loads(self.request.body.decode()) except ValueError: self.set_status(400) return - if property_name not in args: - self.set_status(400) - return - if thing.has_property(property_name): try: - thing.set_property(property_name, args[property_name]) + thing.set_property(property_name, value) except PropertyError: self.set_status(400) return - self.set_header('Content-Type', 'application/json') - self.write(json.dumps({ - property_name: thing.get_property(property_name), - })) + self.set_status(204) else: self.set_status(404) class ActionsHandler(BaseHandler): - """Handle a request to /actions.""" + """Handle a request to /actions.""" # TODO: Should this feature be removed? def get(self, thing_id='0'): """ @@ -572,43 +593,34 @@ def post(self, thing_id='0', action_name=None): return try: - message = json.loads(self.request.body.decode()) + input_ = json.loads(self.request.body.decode()) except ValueError: self.set_status(400) return - keys = list(message.keys()) - if len(keys) != 1: - self.set_status(400) - return - - if keys[0] != action_name: - self.set_status(400) - return - - action_params = message[action_name] - input_ = None - if 'input' in action_params: - input_ = action_params['input'] + # Allow payloads wrapped inside `value` field + #TODO: remove this in the future + if 'value' in input_: + input_ = input_['value'] action = thing.perform_action(action_name, input_) if action: - response = action.as_action_description() + # Start the action tornado.ioloop.IOLoop.current().spawn_callback( perform_action, action, ) - - self.set_status(201) - self.write(json.dumps(response)) + self.set_header('Content-Type', 'application/json') + self.set_status(200) + self.write(json.dumps(action.get_output())) else: self.set_status(400) class ActionIDHandler(BaseHandler): - """Handle a request to /actions//.""" + """Handle a request to /actions//.""" # TODO: Should this feature be removed? def get(self, thing_id='0', action_name=None, action_id=None): """ diff --git a/webthing/thing.py b/webthing/thing.py index d87f922..04b0d4c 100644 --- a/webthing/thing.py +++ b/webthing/thing.py @@ -20,7 +20,8 @@ def __init__(self, id_, title, type_=[], description=''): type_ = [type_] self.id = id_ - self.context = 'https://webthings.io/schemas' + self.context = ['https://www.w3.org/ns/wot-next/td', 'https://webthings.io/schemas'] + self.profiles = ["https://www.w3.org/2022/wot/profile/http-basic/v1"] self.type = type_ self.title = title self.description = description @@ -32,6 +33,9 @@ def __init__(self, id_, title, type_=[], description=''): self.subscribers = set() self.href_prefix = '' self.ui_href = None + self.securityDefinitions = { "nosec_sc": { "scheme": "nosec" }} + self.security = "nosec_sc" + def as_thing_description(self): """ @@ -43,47 +47,49 @@ def as_thing_description(self): 'id': self.id, 'title': self.title, '@context': self.context, + 'profile': self.profiles, 'properties': self.get_property_descriptions(), 'actions': {}, 'events': {}, - 'links': [ + 'forms': [ { - 'rel': 'properties', + 'op': "readallproperties", 'href': '{}/properties'.format(self.href_prefix), + 'contentType': 'application/json', }, { - 'rel': 'actions', - 'href': '{}/actions'.format(self.href_prefix), - }, - { - 'rel': 'events', - 'href': '{}/events'.format(self.href_prefix), - }, + 'op': "writemultipleproperties", + 'href': '{}/properties'.format(self.href_prefix), + 'contentType': 'application/json', + } ], + 'securityDefinitions': self.securityDefinitions, + 'security': self.security } for name, action in self.available_actions.items(): thing['actions'][name] = action['metadata'] - thing['actions'][name]['links'] = [ + thing['actions'][name]['synchronous'] = True + thing['actions'][name]['forms'] = [ { - 'rel': 'action', + 'op': ['invokeaction'], 'href': '{}/actions/{}'.format(self.href_prefix, name), }, ] for name, event in self.available_events.items(): thing['events'][name] = event['metadata'] - thing['events'][name]['links'] = [ + thing['events'][name]['forms'] = [ { - 'rel': 'event', + 'op': ['subscribeevent', 'unsubscribeevent'], 'href': '{}/events/{}'.format(self.href_prefix, name), }, ] if self.ui_href is not None: - thing['links'].append({ + thing['forms'].append({ 'rel': 'alternate', - 'mediaType': 'text/html', + 'type': 'text/html', 'href': self.ui_href, })