2.1.3 • Published 6 months ago

smart-sensor-standards v2.1.3

Weekly downloads
-
License
-
Repository
-
Last release
6 months ago

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)
2.1.3

6 months ago

2.1.2

8 months ago

2.1.1

9 months ago

2.1.0

9 months ago

2.0.26

11 months ago

2.0.24

12 months ago

2.0.25

12 months ago

2.0.22

12 months ago

2.0.23

12 months ago

2.0.20

12 months ago

2.0.21

12 months ago

2.0.15

12 months ago

2.0.3

1 year ago

2.0.16

12 months ago

2.0.2

1 year ago

2.0.13

12 months ago

2.0.5

1 year ago

2.0.14

12 months ago

2.0.4

1 year ago

2.0.11

12 months ago

2.0.7

1 year ago

2.0.12

12 months ago

2.0.6

1 year ago

2.0.9

1 year ago

2.0.10

1 year ago

2.0.8

1 year ago

2.0.1

1 year ago

2.0.0

1 year ago

2.0.19

12 months ago

2.0.17

12 months ago

2.0.18

12 months ago

1.0.17

2 years ago

1.0.16

2 years ago

1.0.15

2 years ago

1.0.14

2 years ago

1.0.13

2 years ago

1.0.12

2 years ago

1.0.11

2 years ago

1.0.10

2 years ago

1.0.9

2 years ago

1.0.8

2 years ago

1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago