From 5a2c540a1c56b9dc811621e8a7c6d01b583a740a Mon Sep 17 00:00:00 2001 From: fatadel Date: Sun, 21 Mar 2021 00:08:00 +0100 Subject: [PATCH 1/8] feat: rename links => forms --- webthing/property.py | 6 +++--- webthing/server.py | 4 ++-- webthing/thing.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/webthing/property.py b/webthing/property.py index ef7c049..3cbd017 100644 --- a/webthing/property.py +++ b/webthing/property.py @@ -53,10 +53,10 @@ 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( + description['forms'].append( { 'rel': 'property', 'href': self.href_prefix + self.href, diff --git a/webthing/server.py b/webthing/server.py index 9e7edae..95bb711 100644 --- a/webthing/server.py +++ b/webthing/server.py @@ -151,7 +151,7 @@ def get(self): for thing in self.things.get_things(): description = thing.as_thing_description() description['href'] = thing.get_href() - description['links'].append({ + description['forms'].append({ 'rel': 'alternate', 'href': '{}{}'.format(ws_href, thing.get_href()), }) @@ -243,7 +243,7 @@ def get(self, thing_id='0'): ) description = self.thing.as_thing_description() - description['links'].append({ + description['forms'].append({ 'rel': 'alternate', 'href': '{}{}'.format(ws_href, self.thing.get_href()), }) diff --git a/webthing/thing.py b/webthing/thing.py index d87f922..d3b427d 100644 --- a/webthing/thing.py +++ b/webthing/thing.py @@ -46,7 +46,7 @@ def as_thing_description(self): 'properties': self.get_property_descriptions(), 'actions': {}, 'events': {}, - 'links': [ + 'forms': [ { 'rel': 'properties', 'href': '{}/properties'.format(self.href_prefix), @@ -64,7 +64,7 @@ def as_thing_description(self): for name, action in self.available_actions.items(): thing['actions'][name] = action['metadata'] - thing['actions'][name]['links'] = [ + thing['actions'][name]['forms'] = [ { 'rel': 'action', 'href': '{}/actions/{}'.format(self.href_prefix, name), @@ -73,7 +73,7 @@ def as_thing_description(self): for name, event in self.available_events.items(): thing['events'][name] = event['metadata'] - thing['events'][name]['links'] = [ + thing['events'][name]['forms'] = [ { 'rel': 'event', 'href': '{}/events/{}'.format(self.href_prefix, name), @@ -81,7 +81,7 @@ def as_thing_description(self): ] if self.ui_href is not None: - thing['links'].append({ + thing['forms'].append({ 'rel': 'alternate', 'mediaType': 'text/html', 'href': self.ui_href, From 8bde403f72b61e9042d579ae7c5bab1814a6d52a Mon Sep 17 00:00:00 2001 From: fatadel Date: Sun, 21 Mar 2021 02:50:07 +0100 Subject: [PATCH 2/8] feat: rename mediaType => type --- webthing/thing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webthing/thing.py b/webthing/thing.py index d3b427d..9e9e0b5 100644 --- a/webthing/thing.py +++ b/webthing/thing.py @@ -83,7 +83,7 @@ def as_thing_description(self): if self.ui_href is not None: thing['forms'].append({ 'rel': 'alternate', - 'mediaType': 'text/html', + 'type': 'text/html', 'href': self.ui_href, }) From 055e5cce1e875f20c9482f95cb37b8d2dcf59232 Mon Sep 17 00:00:00 2001 From: fatadel Date: Sun, 21 Mar 2021 16:50:15 +0100 Subject: [PATCH 3/8] feat: remove object wrappers for Property and Action --- webthing/server.py | 38 ++++++++++---------------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/webthing/server.py b/webthing/server.py index 95bb711..d79662c 100644 --- a/webthing/server.py +++ b/webthing/server.py @@ -437,9 +437,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 +454,26 @@ 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.write(json.dumps(thing.get_property(property_name))) 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,24 +564,14 @@ 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 + if 'value' in input_: + input_ = input_['value'] action = thing.perform_action(action_name, input_) if action: @@ -608,7 +590,7 @@ def post(self, thing_id='0', action_name=None): 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): """ From b79f7c1a96a3cb9e2c14a3fdb79dfe6d4789f2ef Mon Sep 17 00:00:00 2001 From: fatadel Date: Sun, 21 Mar 2021 19:55:24 +0100 Subject: [PATCH 4/8] feat: remove rel and add op for Property --- webthing/property.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/webthing/property.py b/webthing/property.py index 3cbd017..b4f7a37 100644 --- a/webthing/property.py +++ b/webthing/property.py @@ -56,9 +56,17 @@ def as_property_description(self): if 'forms' not in description: description['forms'] = [] + # 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, } ) From e1a8ec0ecfd26ff3fceffe10d46a396f62a0f1d2 Mon Sep 17 00:00:00 2001 From: fatadel Date: Sun, 21 Mar 2021 20:04:57 +0100 Subject: [PATCH 5/8] feat: remove rel and add op for Action and Event --- webthing/thing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webthing/thing.py b/webthing/thing.py index 9e9e0b5..9c4a777 100644 --- a/webthing/thing.py +++ b/webthing/thing.py @@ -66,7 +66,7 @@ def as_thing_description(self): thing['actions'][name] = action['metadata'] thing['actions'][name]['forms'] = [ { - 'rel': 'action', + 'op': ['invokeaction'], 'href': '{}/actions/{}'.format(self.href_prefix, name), }, ] @@ -75,7 +75,7 @@ def as_thing_description(self): thing['events'][name] = event['metadata'] thing['events'][name]['forms'] = [ { - 'rel': 'event', + 'op': ['subscribeevent', 'unsubscribeevent'], 'href': '{}/events/{}'.format(self.href_prefix, name), }, ] From 35dd9b9a03e9f7207ae9898f5b76a4cc8862da98 Mon Sep 17 00:00:00 2001 From: William Dove Date: Tue, 10 Feb 2026 15:20:19 +0000 Subject: [PATCH 6/8] updates python versions to current versions --- setup.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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', ) From d88462af49b3c240377f44a16cd429eba652a80f Mon Sep 17 00:00:00 2001 From: William Dove Date: Tue, 10 Feb 2026 17:01:57 +0000 Subject: [PATCH 7/8] updates property handlers to move towards compatibility with http basic profile --- webthing/server.py | 38 ++++++++++++++++++++++++++++++++++---- webthing/thing.py | 24 +++++++++++++++--------- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/webthing/server.py b/webthing/server.py index d79662c..df749bc 100644 --- a/webthing/server.py +++ b/webthing/server.py @@ -151,7 +151,7 @@ def get(self): for thing in self.things.get_things(): description = thing.as_thing_description() description['href'] = thing.get_href() - description['forms'].append({ + description['links'].append({ 'rel': 'alternate', 'href': '{}{}'.format(ws_href, thing.get_href()), }) @@ -243,7 +243,7 @@ def get(self, thing_id='0'): ) description = self.thing.as_thing_description() - description['forms'].append({ + description['links'].append({ 'rel': 'alternate', 'href': '{}{}'.format(ws_href, self.thing.get_href()), }) @@ -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/.""" @@ -466,8 +496,7 @@ def put(self, thing_id='0', property_name=None): self.set_status(400) return - self.set_header('Content-Type', 'application/json') - self.write(json.dumps(thing.get_property(property_name))) + self.set_status(204) else: self.set_status(404) @@ -570,6 +599,7 @@ def post(self, thing_id='0', action_name=None): return # Allow payloads wrapped inside `value` field + #TODO: remove this in the future if 'value' in input_: input_ = input_['value'] diff --git a/webthing/thing.py b/webthing/thing.py index 9c4a777..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,27 +47,29 @@ def as_thing_description(self): 'id': self.id, 'title': self.title, '@context': self.context, + 'profile': self.profiles, 'properties': self.get_property_descriptions(), 'actions': {}, 'events': {}, '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]['synchronous'] = True thing['actions'][name]['forms'] = [ { 'op': ['invokeaction'], From a4b790b95731132397658b986414cb6b16e9c09e Mon Sep 17 00:00:00 2001 From: William Dove Date: Wed, 11 Feb 2026 11:45:17 +0000 Subject: [PATCH 8/8] starts improving actions --- webthing/action.py | 4 ++++ webthing/server.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) 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/server.py b/webthing/server.py index df749bc..4224f85 100644 --- a/webthing/server.py +++ b/webthing/server.py @@ -605,16 +605,16 @@ def post(self, thing_id='0', action_name=None): 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)