smart-sensor-standards
v2.1.3
Published
An implementation for the efficient and safe use of the smart sensor standards
Downloads
67
Readme
World Wide Things - Standards for TypeScript
This is the implementation of the World Wide Things standards in TypeScript.
Config interfaces
There are interfaces for every configuration object.
interface ThingConfigV1 {
ais?: AiConfigV1[]
bluetooth?: BluetoothConfigV1
ethernet?: EthernetConfigV1
id?: string
latitude?: number|null
location?: string|null
locationType?: string|null
longitude?: number|null
monitored?: string|null
monitoredModel?: string|null
monitoredType?: string|null
mqtt?: MqttConfigV1
name?: string|null
organization?: string|null
project?: string|null
sensors?: SensorConfigV1[]
site?: string|null
system?: SystemConfigV1
wifi?: WifiConfigV1
}
Every property is being declared a optional though it might not be. The reason for this is that you might have parsed a config from a JSON string into an object denoting the resulting object to implement the ThingConfigV1
interface.
const json = '{ id: "wwt01" }'
const obj = JSON.parse(json) as ThingConfigV1
As you can see from the JSON string, it only has an id
property, while it is missing a lot properties which a required and thus should be there. This situation does not only occur with faulty configs but also especially when receiving only a partial one that you want to use to update an existing one, which is a totally valid method.
Config descriptions
Config descriptions hold information about the structure and the validation constraints of config objects. There is a config description available for every config object provided by the library.
classDiagram
ConfigDescription *-- ConfigProperty
class ConfigDescription {
+entityName: string
+getConfig(configured: any, mutableOnly = false): T
+setConfig(configured: any, config: T, mutableOnly = false, instantiator?: Instantiator): Change[]
+validate(config: T, options?: ValidatorOptions): Promise<Misfit[]>
+getValidator(options?: ValidatorOptions): Validator<T>
}
class ConfigProperty {
+name: string
+type: 'array'|'boolean'|'number'|'object'|'string'
+idProperty?: string
+objectDescription?: ConfigDescription<any>
+isPrimitive(): bool
+isObject(): bool
+isArray(): bool
}
Validating
You can use them to validate a config object.
import { thingConfigDescriptionV1, ThingConfigV1 } from 'wwt-standards'
const config = {
id: ''
} as ThingConfigV1
const misfits = thingConfigDescriptionV1.validate(config)
The result is an array of Misfit objects. A misfit containing the following properties.
// The name of the constraint that caused the misfit
misfit.constraint == 'Length'
// The list of properties the constraint takes into consideration
misfit.properties == [ 'id' ]
// The values the constraint was parameterized with
misfit.values == {
exact: 5
}
Extracting config properties
You can extract the properties that belong to a config object from an arbitrary object that you provide.
import { thingConfigDescriptionV1 } from 'wwt-standards'
const obj = {
id: 'wwt01',
other: 'unrelated'
}
const config = thingConfigDescriptionV1.getConfig(obj)
config == {
id: 'wwt01'
}
Setting config properties
You can set config properties on an arbitrary object while receiving a list of the changed values.
import { thingConfigDescriptionV1 } from 'wwt-standards'
const config = {
id: 'wwt01',
location: 'Dresden',
other: 'unrelated'
}
const toSet = {
id: 'wwt02',
location: 'Berlin',
other: 'related'
}
const changes = thingConfigDescriptionV1.setConfig(config, toSet)
config == {
id: 'wwt02',
location: 'Berlin',
other: 'unrelated' // did not change
}
The result is a list of the changes that where made.
const changes = thingConfigDescriptionV1.setConfig(config, toSet)
change[0] == {
entityName: 'Thing',
entity: config,
method: 'update', // 'create', 'delete'
properties: [ 'id', 'location' ]
}
The following entity names are possible and are declared in the ConfigEntitiesV1
enum.
enum ConfigEntitiesV1 {
Ai = 'Ai',
Bluetooth = 'Bluetooth',
Ethernet = 'Ethernet',
Mqtt = 'Mqtt',
Sensor = 'Sensor',
Thing = 'Thing',
System = 'System',
Wifi = 'Wifi'
}
You can also declare that you only want to set mutable properties.
import { thingConfigDescriptionV1 } from 'wwt-standards'
const config = {
id: 'wwt01',
location: 'Dresden',
other: 'unrelated'
}
const toSet = {
id: 'wwt02',
location: 'Berlin',
other: 'related'
}
const changes = thingConfigDescriptionV1.setConfig(config, toSet, true)
config == {
id: 'wwt01', // did not change
location: 'Berlin',
other: 'unrelated' // did not change
}
It is also possible to instantiate specific classes in the case you are adding new config objects.
import { SensorConfigV1, thingConfigDescriptionV1 } from 'wwt-standards'
class Sensor implements SensorConfigV1 {
model?: string
port?: number
other?: string
constructor(config?: SensorConfigV1) {
if (config != undefined) {
this.setConfig(config)
}
}
}
const instantiator = {
'Sensor': (config?: SensorConfigV1) => new Sensor(config)
}
const config = {
id: 'wwt01'
}
const toSet = {
sensors: [{
model: 'Infineon IM69D130',
port: 1
}]
}
thingConfigDescriptionV1.setConfig(config, toSet, false, instantiator)
The config
object will now have its sensors
property set with an array containing an object which is an instance of the Ai
class instead of it being an instance of the Object
class which every object in JavaScript is an instance from.
Iterating through config property descriptions
A config description contains a list of config property descriptions in the property properties
. A config property description contains the following information.
class ConfigProperty {
name: string
type: 'array'|'boolean'|'number'|'object'|'string'
idProperty?: string
objectDescription?: ConfigDescription<any>
mutable?: boolean
required?: boolean
nullable?: boolean
minimum?: number // applied to numbers
exclusiveMinimum?: number // applied to numbers
maximum?: number // applied to numbers
exclusiveMaximum?: number // applied to numbers
minLength?: number // applied to strings
maxLength?: number // applied to strings
}
The idProperty
is relevant for config objects that are part of an array like for example Ai
, Sensor
or MqttMessage
. All of these config objects contain a property which uniquely identifies them amongst the other config objects of the same kind in that same array. For Ai
it is the property slot
, for Sensor
it is port
and for MqttMessage
it is name
. Imagine you want to update a property of a specific Ai
. You will need to use the corresponding slot
value to do so.
The objectDescription
is used in case of the property being of type object
or of type array
if it contains object values. It is the config description of this object or those objects inside the array.
The remaining properties are validation constraints that matches those that can be found in JSON schema.
You can use those property descriptions to auto generate an user interface, for example.
Config classes
Beside the config interfaces, there are also implementations of those. Every class has the following capabilities.
classDiagram
Configurable --> ConfigDescription
class Configurable {
+configDescription: ConfigDescription<T>
+instantiator?: Instantiator
+getConfig(mutableOnly = false): T
+setConfig(config: T, mutableOnly = false): Change[]
+validateConfig(checkOnlyWhatIsThere = false): Promise<Misfit[]>
}
class ConfigDescription {
+getConfig(configured: any, mutableOnly = false): T
+setConfig(configured: any, config: T, mutableOnly = false, instantiator?: Instantiator): Change[]
+validate(config: T, options?: ValidatorOptions): Promise<Misfit[]>
}
A constructor which will accept any object from which it will extract the relevant config properties which values it will use to set its own.
import { ThingConfigV1, ThingV1 } from 'wwt-standards'
const config = {
id: 'wwt01',
other: 'unrelated' // will be ignored
} as ThingConfigV1
const thing = new ThingV1(config)
It also accepts an instantiator which you can use if you want to use your own classes. This is best used when you are deriving your own set of config objects.
import { InstantiatorV1, SensorConfigV1, SensorV1, ThingConfigV1, ThingV1 } from 'wwt-standards'
// Your own derived sensor class which has an additional property
class MySensor extends SensorV1 {
working?: boolean
constructor(config?: SensorConfigV1) {
super(config)
}
}
// Your own derived instantiator class which redefines the method for instantiating a sensor
class MyInstantiator extends InstantiatorV1 {
'Sensor': (config?: SensorConfigV1) => Configurable = config => new MySensor(config)
}
const config = {
id: 'wwt01'
} as ThingConfigV1
const thing = new ThingV1(config, new MyInstantiator())
Additionally, every class offers the following three methods which are just calling the equivalents of the corresponding config descriptions. Please refer to the remarks above for detailed explanations.
const thing = new ThingV1
thing.getConfig()
const changes = thing.setConfig({ id: 'wwt01' })
const misfits = thing.validateConfig()
MQTT Messages
The library provides an object for every MQTT message that is sent between the things and their services.
WwtConfigMqttMessageV1
: Contains the config of a thing.WwtDiscoverMqttMessageV1
: Triggers every thing to send its config.WwtGetConfigMqttMessageV1
: Request the config of a specific thing.WwtSetConfigMqttMessageV1
: Mutate the config of a thing.
Every class has methods to help sending and to help receiving the represented MQTT message.
There are also version supporting the payload format of AWS which are AwsConfigMqttMessageV1
, AwsDiscoverMqttMessageV1
, AwsGetConfigMqttMessageV1
and AwsSetConfigMqttMessageV1
.
Receiving
The first thing you need to do is to subscribe to the MQTT topics that you want to receive MQTT messages for. You can either subscribe to any config message or only to specific ones.
import { WwtConfigMqttMessageV1 } from 'wwt-standards'
const message = new WwtConfigMqttMessageV1()
// Subscribe to any Config message
const anyFilter = message.createSubscription()
anyFilter == 'wwt/+/config'
// Subscribe to the Config message of the thing with id 'wwt01'
const specificFilter = message.createSubscription({ id: 'wwt01' })
specificFilter == 'wwt/wwt01/config'
You can also use the constructor to declare the topic parameters that are to be applied.
import { WwtConfigMqttMessageV1 } from 'wwt-standards'
// Create a message object that processes Config messages of the thing with id 'wwt01'
const message = new WwtConfigMqttMessageV1({ id: 'wwt01' })
const specificFilter = message.createSubscription()
specificFilter == 'wwt/wwt01/config'
After receiving an MQTT message, you want to check the MQTT topic to find out which message type you received. If are receiving any Config message, you want to find out to which thing it belongs. This information is part of the MQTT topic string.
import { WwtConfigMqttMessageV1 } from 'wwt-standards'
// Topic string that contains the id 'wwt01'
const topic = 'wwt/wwt01/config'
const message = new WwtConfigMqttMessageV1()
// Match any Config message and ectract the id of the thing
let topicParameters = {}
let matching = message.matchTopic(topic, topicParameters)
matching == true
topicParameters['id'] == 'wwt01'
// Match only Config messages of the thing with id 'wwt01'
topicParameters = { id: 'wwt01' }
matching = message.matchTopic(topic, topicParameters)
matching == true
If you leave out the topicParameters
parameter of the matchTopic
method, the object will use the topic parameters object which were given to it in the constructor.
import { WwtConfigMqttMessageV1 } from 'wwt-standards'
// Topic string that contains the id 'wwt01'
const topic = 'wwt/wwt01/config'
// Match any Config message and ectract the id of the thing
let message = new WwtConfigMqttMessageV1()
let matching = message.matchTopic(topic)
matching == true
message.topicParameters['id'] == 'wwt01'
// Match any Config message and ectract the id of the thing
message = new WwtConfigMqttMessageV1({ id: 'wwt01' })
matching = message.matchTopic(topic)
matching == true
Beware that the MQTT message object will behave differently if the matchTopic
method did add extracted topic parameters to its internal topicParameters
object. In this usage pattern, you want to throw the object away after using it for one occasion.
Now that you know which kind of MQTT message you received, you can unpack its payload, if present. The payload is expected to be of type Uint8Array
which contains raw uninterpreted bytes. The payload gets unpacked into a payload parameters object which contains the initial parameters the payload was built with. That way, the payload can have differing appearances independently from the information that was packed into it.
import { WwtConfigMqttMessageV1 } from 'wwt-standards'
const payload = new Uint8Array(/* Contains the thing config as a JSON string */)
const message = new WwtConfigMqttMessageV1()
const payloadParameters = {}
message.unpackPayload(payload, payloadParameters)
payloadParameters['id'] == 'wwt01'
Similarly to the topicParameters
parameter, you can also leave out the payloadParameters
parameter of the unpackPayload
method. The message object has its own internal payloadParameters
object which it will use.
import { WwtConfigMqttMessageV1 } from 'wwt-standards'
const payload = new Uint8Array(/* Contains the thing config as a JSON string */)
const message = new WwtConfigMqttMessageV1()
message.unpackPayload(payload)
message.payloadParameters['id'] == 'wwt01'
Similarly as noted above, you want to throw the object away when applying this usage pattern, since from this point on, its internal payloadParameters
will contain the values of a specific MQTT message payload.
Sending
If you want to send an MQTT message you need a topic and its payload. When creating a topic that is using topic parameters, you must provide those, otherwise an exception will be thrown.
import { WwtConfigMqttMessageV1 } from 'wwt-standards'
const message = new WwtConfigMqttMessageV1()
const topicParameteres = {
id: 'wwt01'
}
// Topic parameters given as a method parameter
const topic = message.createTopic(topicParameters)
topic == 'wwt/wwt01/config'
You can also use the constructor to store the topic parameters inside of the MQTT message object.
import { WwtConfigMqttMessageV1 } from 'wwt-standards'
const topicParameteres = {
id: 'wwt01'
}
// Topic parameters given as a constructor parameter
const message = new WwtConfigMqttMessageV1(topicParameters)
const topic = message.createTopic()
topic == 'wwt/wwt01/config'
To create a payload you need to provide payload parameters. The payload will be created as an Uint8Array
.
import { ThingConfigV1, WwtConfigMqttMessageV1 } from 'wwt-standards'
const message = new WwtConfigMqttMessageV1()
const payloadParameteres = {
id: 'wwt01'
} as ThingConfigV1
// Payload parameters given as a method parameter
const payload = message.packPayload(payloadParameters)
As with the topic parameters, you can also provide the payload parameters as a constructor parameter to store the inside the MQTT message object. In the case of our example using the WwtConfigMqttMessageV1
, both the topic and the payload parameters parameter are the same.
import { WwtConfigMqttMessageV1 } from 'wwt-standards'
const payloadParameteres = {
id: 'wwt01'
}
// Payload parameters given as a constructor parameter
const message = new WwtConfigMqttMessageV1(payloadParameters)
const payload = message.createPayload()
MQTT API's
To be able to save some repeating code you can use the provided API classes. There is one for the thing role and one for the server role.
- Thing role:
WwtThingMqttApiV1
,AwsThingMqttApiV1
- Server role:
WwtServerMqttApiV1
,AwsServerMqttApiV1
The classes provide means to type-safe react to received MQTT messages and to type-safe create the correct MQTT messages of the corresponding role.
Let us have a look at the thing role.
import { ThingV1, ThingConfigV1, WwtThingMqttApiV1 } from 'wwt-standards'
const thing = new ThingV1({
id: 'wwt01'
} as ThingConfigV1)
const api = new WwtThingMqttApiV1()
api.onDiscover(() => {
const message = api.createConfigMessage(config)
const topic message.createTopic()
const payload = message.createPayload()
})
api.onGetConfig(() => {
const message = api.createConfigMessage(config)
const topic message.createTopic()
const payload = message.createPayload()
})
api.onSetConfig((config: ThingConfigV1) => {
const changes = thing.setConfig(config)
})
Let us have a look at the server role.
import { ThingV1, ThingConfigV1, WwtServerMqttApiV1 } from 'wwt-standards'
const api = new WwtServerMqttApiV1()
api.onConfig((config: ThingConfigV1) => {
const thing = new ThingV1(config)
})
const discover = api.createDiscoverMessage()
const getConfig = api.createGetConfigMessage()
const setConfig = api.createSetConfigMessage({
id: 'wwt01'
} as ThingConfigV1)