Build Your Own Web Things
The WebThings Framework is a collection of re-usable software components to help you build your own web things, which directly expose the Web Thing API. This means they can be discovered by a Web of Things gateway or client, which can then automatically detect the device's capabilities and monitor and control it over the web.
Installation
$ npm install webthing
$ pip install webthing
Maven:
<dependencies>
<dependency>
<groupId>io.webthings</groupId>
<artifactId>webthing</artifactId>
<version>0.13.0</version>
</dependency>
</dependencies>
Gradle:
dependencies {
runtime(
[group: 'io.webthings', name: 'webthing', version: '0.13.0'],
)
}
Cargo:
[dependencies]
webthing = "0.13"
- Arduino IDE:
- Add the webthing and ArduinoJson libraries to your project.
- PlatformIO:
- Add the webthing-arduino and ArduinoJson libraries to your project.
Example
const {
Action,
Event,
Property,
SingleThing,
Thing,
Value,
WebThingServer,
} = require('webthing');
const uuidv4 = require('uuid/v4');
class OverheatedEvent extends Event {
constructor(thing, data) {
super(thing, 'overheated', data);
}
}
class FadeAction extends Action {
constructor(thing, input) {
super(uuidv4(), thing, 'fade', input);
}
performAction() {
return new Promise((resolve) => {
setTimeout(() => {
this.thing.setProperty('brightness', this.input.brightness);
this.thing.addEvent(new OverheatedEvent(this.thing, 102));
resolve();
}, this.input.duration);
});
}
}
function makeThing() {
const thing = new Thing('urn:dev:ops:my-lamp-1234',
'My Lamp',
['OnOffSwitch', 'Light'],
'A web connected lamp');
thing.addProperty(
new Property(thing,
'on',
new Value(true),
{
'@type': 'OnOffProperty',
title: 'On/Off',
type: 'boolean',
description: 'Whether the lamp is turned on',
}));
thing.addProperty(
new Property(thing,
'brightness',
new Value(50),
{
'@type': 'BrightnessProperty',
title: 'Brightness',
type: 'integer',
description: 'The level of light from 0-100',
minimum: 0,
maximum: 100,
unit: 'percent',
}));
thing.addAvailableAction(
'fade',
{
title: 'Fade',
description: 'Fade the lamp to a given level',
input: {
type: 'object',
required: [
'brightness',
'duration',
],
properties: {
brightness: {
type: 'integer',
minimum: 0,
maximum: 100,
unit: 'percent',
},
duration: {
type: 'integer',
minimum: 1,
unit: 'milliseconds',
},
},
},
},
FadeAction);
thing.addAvailableEvent(
'overheated',
{
description: 'The lamp has exceeded its safe operating temperature',
type: 'number',
unit: 'degree celsius',
});
return thing;
}
function runServer() {
const thing = makeThing();
// If adding more than one thing, use MultipleThings() with a name.
// In the single thing case, the thing's name will be broadcast.
const server = new WebThingServer(new SingleThing(thing), 8888);
process.on('SIGINT', () => {
server.stop().then(() => process.exit()).catch(() => process.exit());
});
server.start().catch(console.error);
}
runServer();
from __future__ import division
from webthing import (Action, Event, Property, SingleThing, Thing, Value,
WebThingServer)
import logging
import time
import uuid
class OverheatedEvent(Event):
def __init__(self, thing, data):
Event.__init__(self, thing, 'overheated', data=data)
class FadeAction(Action):
def __init__(self, thing, input_):
Action.__init__(self, uuid.uuid4().hex, thing, 'fade', input_=input_)
def perform_action(self):
time.sleep(self.input['duration'] / 1000)
self.thing.set_property('brightness', self.input['brightness'])
self.thing.add_event(OverheatedEvent(self.thing, 102))
def make_thing():
thing = Thing(
'urn:dev:ops:my-lamp-1234',
'My Lamp',
['OnOffSwitch', 'Light'],
'A web connected lamp'
)
thing.add_property(
Property(thing,
'on',
Value(True),
metadata={
'@type': 'OnOffProperty',
'title': 'On/Off',
'type': 'boolean',
'description': 'Whether the lamp is turned on',
}))
thing.add_property(
Property(thing,
'brightness',
Value(50),
metadata={
'@type': 'BrightnessProperty',
'title': 'Brightness',
'type': 'integer',
'description': 'The level of light from 0-100',
'minimum': 0,
'maximum': 100,
'unit': 'percent',
}))
thing.add_available_action(
'fade',
{
'title': 'Fade',
'description': 'Fade the lamp to a given level',
'input': {
'type': 'object',
'required': [
'brightness',
'duration',
],
'properties': {
'brightness': {
'type': 'integer',
'minimum': 0,
'maximum': 100,
'unit': 'percent',
},
'duration': {
'type': 'integer',
'minimum': 1,
'unit': 'milliseconds',
},
},
},
},
FadeAction)
thing.add_available_event(
'overheated',
{
'description':
'The lamp has exceeded its safe operating temperature',
'type': 'number',
'unit': 'degree celsius',
})
return thing
def run_server():
thing = make_thing()
# If adding more than one thing, use MultipleThings() with a name.
# In the single thing case, the thing's name will be broadcast.
server = WebThingServer(SingleThing(thing), port=8888)
try:
logging.info('starting the server')
server.start()
except KeyboardInterrupt:
logging.info('stopping the server')
server.stop()
logging.info('done')
if __name__ == '__main__':
logging.basicConfig(
level=10,
format="%(asctime)s %(filename)s:%(lineno)s %(levelname)s %(message)s"
)
run_server()
package io.webthings.webthing.example;
import org.json.JSONArray;
import org.json.JSONObject;
import io.webthings.webthing.Action;
import io.webthings.webthing.Event;
import io.webthings.webthing.Property;
import io.webthings.webthing.Thing;
import io.webthings.webthing.Value;
import io.webthings.webthing.WebThingServer;
import io.webthings.webthing.errors.PropertyError;
import java.io.IOException;
import java.util.Arrays;
import java.util.UUID;
public class SingleThing {
public static Thing makeThing() {
Thing thing = new Thing("urn:dev:ops:my-lamp-1234",
"My Lamp",
new JSONArray(Arrays.asList("OnOffSwitch",
"Light")),
"A web connected lamp");
JSONObject onDescription = new JSONObject();
onDescription.put("@type", "OnOffProperty");
onDescription.put("title", "On/Off");
onDescription.put("type", "boolean");
onDescription.put("description", "Whether the lamp is turned on");
thing.addProperty(new Property(thing,
"on",
new Value(true),
onDescription));
JSONObject brightnessDescription = new JSONObject();
brightnessDescription.put("@type", "BrightnessProperty");
brightnessDescription.put("title", "Brightness");
brightnessDescription.put("type", "integer");
brightnessDescription.put("description",
"The level of light from 0-100");
brightnessDescription.put("minimum", 0);
brightnessDescription.put("maximum", 100);
brightnessDescription.put("unit", "percent");
thing.addProperty(new Property(thing,
"brightness",
new Value(50),
brightnessDescription));
JSONObject fadeMetadata = new JSONObject();
JSONObject fadeInput = new JSONObject();
JSONObject fadeProperties = new JSONObject();
JSONObject fadeBrightness = new JSONObject();
JSONObject fadeDuration = new JSONObject();
fadeMetadata.put("title", "Fade");
fadeMetadata.put("description", "Fade the lamp to a given level");
fadeInput.put("type", "object");
fadeInput.put("required",
new JSONArray(Arrays.asList("brightness", "duration")));
fadeBrightness.put("type", "integer");
fadeBrightness.put("minimum", 0);
fadeBrightness.put("maximum", 100);
fadeBrightness.put("unit", "percent");
fadeDuration.put("type", "integer");
fadeDuration.put("minimum", 1);
fadeDuration.put("unit", "milliseconds");
fadeProperties.put("brightness", fadeBrightness);
fadeProperties.put("duration", fadeDuration);
fadeInput.put("properties", fadeProperties);
fadeMetadata.put("input", fadeInput);
thing.addAvailableAction("fade", fadeMetadata, FadeAction.class);
JSONObject overheatedMetadata = new JSONObject();
overheatedMetadata.put("description",
"The lamp has exceeded its safe operating temperature");
overheatedMetadata.put("type", "number");
overheatedMetadata.put("unit", "degree celsius");
thing.addAvailableEvent("overheated", overheatedMetadata);
return thing;
}
public static void main(String[] args) {
Thing thing = makeThing();
WebThingServer server;
try {
// If adding more than one thing, use MultipleThings() with a name.
// In the single thing case, the thing's name will be broadcast.
server = new WebThingServer(new WebThingServer.SingleThing(thing),
8888);
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
server.stop();
}
});
server.start(false);
} catch (IOException e) {
System.out.println(e);
System.exit(1);
}
}
public static class OverheatedEvent extends Event {
public OverheatedEvent(Thing thing, int data) {
super(thing, "overheated", data);
}
}
public static class FadeAction extends Action {
public FadeAction(Thing thing, JSONObject input) {
super(UUID.randomUUID().toString(), thing, "fade", input);
}
@Override
public void performAction() {
Thing thing = this.getThing();
JSONObject input = this.getInput();
try {
Thread.sleep(input.getInt("duration"));
} catch (InterruptedException e) {
}
try {
thing.setProperty("brightness", input.getInt("brightness"));
thing.addEvent(new OverheatedEvent(thing, 102));
} catch (PropertyError e) {
}
}
}
}
use actix_rt;
use serde_json::json;
use std::sync::{Arc, RwLock, Weak};
use std::{thread, time};
use uuid::Uuid;
use webthing::{
Action, BaseAction, BaseEvent, BaseProperty, BaseThing, Thing, ThingsType, WebThingServer,
};
use webthing::server::ActionGenerator;
pub struct FadeAction(BaseAction);
impl FadeAction {
fn new(
input: Option<serde_json::Map<String, serde_json::Value>>,
thing: Weak<RwLock<Box<dyn Thing>>>,
) -> FadeAction {
FadeAction(BaseAction::new(
Uuid::new_v4().to_string(),
"fade".to_owned(),
input,
thing,
))
}
}
impl Action for FadeAction {
fn set_href_prefix(&mut self, prefix: String) {
self.0.set_href_prefix(prefix)
}
fn get_id(&self) -> String {
self.0.get_id()
}
fn get_name(&self) -> String {
self.0.get_name()
}
fn get_href(&self) -> String {
self.0.get_href()
}
fn get_status(&self) -> String {
self.0.get_status()
}
fn get_time_requested(&self) -> String {
self.0.get_time_requested()
}
fn get_time_completed(&self) -> Option<String> {
self.0.get_time_completed()
}
fn get_input(&self) -> Option<serde_json::Map<String, serde_json::Value>> {
self.0.get_input()
}
fn get_thing(&self) -> Option<Arc<RwLock<Box<dyn Thing>>>> {
self.0.get_thing()
}
fn set_status(&mut self, status: String) {
self.0.set_status(status)
}
fn start(&mut self) {
self.0.start()
}
fn perform_action(&mut self) {
let thing = self.get_thing();
if thing.is_none() {
return;
}
let thing = thing.unwrap();
let input = self.get_input().unwrap().clone();
let name = self.get_name();
let id = self.get_id();
thread::spawn(move || {
thread::sleep(time::Duration::from_millis(
input.get("duration").unwrap().as_u64().unwrap(),
));
let thing = thing.clone();
let mut thing = thing.write().unwrap();
let _ = thing.set_property(
"brightness".to_owned(),
input.get("brightness").unwrap().clone(),
);
thing.add_event(Box::new(BaseEvent::new(
"overheated".to_owned(),
Some(json!(102)),
)));
thing.finish_action(name, id);
});
}
fn cancel(&mut self) {
self.0.cancel()
}
fn finish(&mut self) {
self.0.finish()
}
}
struct Generator;
impl ActionGenerator for Generator {
fn generate(
&self,
thing: Weak<RwLock<Box<dyn Thing>>>,
name: String,
input: Option<&serde_json::Value>,
) -> Option<Box<dyn Action>> {
let input = match input {
Some(v) => match v.as_object() {
Some(o) => Some(o.clone()),
None => None,
},
None => None,
};
let name: &str = &name;
match name {
"fade" => Some(Box::new(FadeAction::new(input, thing))),
_ => None,
}
}
}
fn make_thing() -> Arc<RwLock<Box<dyn Thing + 'static>>> {
let mut thing = BaseThing::new(
"urn:dev:ops:my-lamp-1234".to_owned(),
"My Lamp".to_owned(),
Some(vec!["OnOffSwitch".to_owned(), "Light".to_owned()]),
Some("A web connected lamp".to_owned()),
);
let on_description = json!({
"@type": "OnOffProperty",
"title": "On/Off",
"type": "boolean",
"description": "Whether the lamp is turned on"
});
let on_description = on_description.as_object().unwrap().clone();
thing.add_property(Box::new(BaseProperty::new(
"on".to_owned(),
json!(true),
None,
Some(on_description),
)));
let brightness_description = json!({
"@type": "BrightnessProperty",
"title": "Brightness",
"type": "integer",
"description": "The level of light from 0-100",
"minimum": 0,
"maximum": 100,
"unit": "percent"
});
let brightness_description = brightness_description.as_object().unwrap().clone();
thing.add_property(Box::new(BaseProperty::new(
"brightness".to_owned(),
json!(50),
None,
Some(brightness_description),
)));
let fade_metadata = json!({
"title": "Fade",
"description": "Fade the lamp to a given level",
"input": {
"type": "object",
"required": [
"brightness",
"duration"
],
"properties": {
"brightness": {
"type": "integer",
"minimum": 0,
"maximum": 100,
"unit": "percent"
},
"duration": {
"type": "integer",
"minimum": 1,
"unit": "milliseconds"
}
}
}
});
let fade_metadata = fade_metadata.as_object().unwrap().clone();
thing.add_available_action("fade".to_owned(), fade_metadata);
let overheated_metadata = json!({
"description": "The lamp has exceeded its safe operating temperature",
"type": "number",
"unit": "degree celsius"
});
let overheated_metadata = overheated_metadata.as_object().unwrap().clone();
thing.add_available_event("overheated".to_owned(), overheated_metadata);
Arc::new(RwLock::new(Box::new(thing)))
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
let thing = make_thing();
// If adding more than one thing, use ThingsType::Multiple() with a name.
// In the single thing case, the thing's name will be broadcast.
let mut server = WebThingServer::new(
ThingsType::Single(thing),
Some(8888),
None,
None,
Box::new(Generator),
None,
);
server.start(None).await
}
#define LARGE_JSON_BUFFERS 1
#include <Arduino.h>
#include <Thing.h>
#include <WebThingAdapter.h>
#ifdef ESP32
#include <analogWrite.h>
#endif
const char *ssid = "......";
const char *password = "..........";
#if defined(LED_BUILTIN)
const int lampPin = LED_BUILTIN;
#else
const int lampPin = 13; // manually configure LED pin
#endif
ThingActionObject *action_generator(DynamicJsonDocument *);
WebThingAdapter *adapter;
const char *lampTypes[] = {"OnOffSwitch", "Light", nullptr};
ThingDevice lamp("urn:dev:ops:my-lamp-1234", "My Lamp", lampTypes);
ThingProperty lampOn("on", "Whether the lamp is turned on", BOOLEAN,
"OnOffProperty");
ThingProperty lampLevel("brightness", "The level of light from 0-100", INTEGER,
"BrightnessProperty");
StaticJsonDocument<256> fadeInput;
JsonObject fadeInputObj = fadeInput.to<JsonObject>();
ThingAction fade("fade", "Fade", "Fade the lamp to a given level",
"FadeAction", &fadeInputObj, action_generator);
ThingEvent overheated("overheated",
"The lamp has exceeded its safe operating temperature",
NUMBER, "OverheatedEvent");
bool lastOn = true;
void setup(void) {
pinMode(lampPin, OUTPUT);
digitalWrite(lampPin, HIGH);
Serial.begin(115200);
Serial.println("");
Serial.print("Connecting to \"");
Serial.print(ssid);
Serial.println("\"");
#if defined(ESP8266) || defined(ESP32)
WiFi.mode(WIFI_STA);
#endif
WiFi.begin(ssid, password);
Serial.println("");
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
adapter = new WebThingAdapter("led-lamp", WiFi.localIP());
lamp.description = "A web connected lamp";
lampOn.title = "On/Off";
lamp.addProperty(&lampOn);
lampLevel.title = "Brightness";
lampLevel.minimum = 0;
lampLevel.maximum = 100;
lampLevel.unit = "percent";
lamp.addProperty(&lampLevel);
fadeInputObj["type"] = "object";
JsonObject fadeInputProperties =
fadeInputObj.createNestedObject("properties");
JsonObject brightnessInput =
fadeInputProperties.createNestedObject("brightness");
brightnessInput["type"] = "integer";
brightnessInput["minimum"] = 0;
brightnessInput["maximum"] = 100;
brightnessInput["unit"] = "percent";
JsonObject durationInput =
fadeInputProperties.createNestedObject("duration");
durationInput["type"] = "integer";
durationInput["minimum"] = 1;
durationInput["unit"] = "milliseconds";
lamp.addAction(&fade);
overheated.unit = "degree celsius";
lamp.addEvent(&overheated);
adapter->addDevice(&lamp);
adapter->begin();
Serial.println("HTTP server started");
Serial.print("http://");
Serial.print(WiFi.localIP());
Serial.print("/things/");
Serial.println(lamp.id);
#ifdef analogWriteRange
analogWriteRange(255);
#endif
// set initial values
ThingPropertyValue initialOn = {.boolean = true};
lampOn.setValue(initialOn);
(void)lampOn.changedValueOrNull();
ThingPropertyValue initialLevel = {.integer = 50};
lampLevel.setValue(initialLevel);
(void)lampLevel.changedValueOrNull();
analogWrite(lampPin, 128);
randomSeed(analogRead(0));
}
void loop(void) {
adapter->update();
bool on = lampOn.getValue().boolean;
if (on) {
int level = map(lampLevel.getValue().number, 0, 100, 255, 0);
analogWrite(lampPin, level);
} else {
analogWrite(lampPin, 255);
}
if (lastOn != on) {
lastOn = on;
}
}
void do_fade(const JsonVariant &input) {
JsonObject inputObj = input.as<JsonObject>();
long long int duration = inputObj["duration"];
long long int brightness = inputObj["brightness"];
delay(duration);
ThingDataValue value = {.integer = brightness};
lampLevel.setValue(value);
int level = map(brightness, 0, 100, 255, 0);
analogWrite(lampPin, level);
ThingDataValue val;
val.number = 102;
ThingEventObject *ev = new ThingEventObject("overheated", NUMBER, val);
lamp.queueEventObject(ev);
}
ThingActionObject *action_generator(DynamicJsonDocument *input) {
return new ThingActionObject("fade", input, do_fade, nullptr);
}