Skip to content

Latest commit

 

History

History
460 lines (345 loc) · 14.3 KB

README.md

File metadata and controls

460 lines (345 loc) · 14.3 KB

Tinkerhub

Tinkerhub is a library for building, connecting, interacting and tinkering with things on your local network. It can be used for home automation, such as turning appliances on and off, monitoring sensor data and other IoT workloads.

The base of Tinkerhub is a network in which a library can choose to expose things and appliances, such as lights, sensors and services. Any NodeJS instance connected to the network can then see and interact with these things.

To setup a network the easiest way is to install tinkerhub-daemon to host plugins and to use tinkerhub-cli to interact with them.

Getting started and joining the local network

Tinkerhub requires at least Node 8.

To get started install the tinkerhub library:

npm install tinkerhub

Tinkerhub will automatically connect to other instances on the same local network and make their devices available to the local instance. This is done automatically on require('tinkerhub').

// Asynchronously connect to instances on the local network
const th = require('tinkerhub');

Discovering things

Things can join or leave the network at any time. It's possible to listen to thing:available and thing:unavailable to be notified when this happens.

th.on('thing:available', thing => {
  console.log('New thing', thing);
});

The library provides access to everything it can reach via collections. A collection is a filtered view of things on the current network.

// Get everything
const allDevices = th.all();

// Filter things based on types, capabilities and tags
const lights = th.get('type:light');

Collections also support the thing:available and thing:unavailable events.

const switchableLights = th.get('type:light', 'cap:switchable');

switchableLights.on('thing:available', light => console.log('Found a light', light));

switchableLights.destroy(); // Destroy the collection and remove all listeners

The event thing:updated can be used to listen for updates, such as changes in name, tags, type or capabilities:

switchableLights.on('thing:updated', light => console.log('Light has changed', light));

Interacting with things

All things have metadata associated with them, which contains information about their unique identifier, name (if any), types and capabilities.

console.log(thing.metadata.id); // "idOfDevice"
console.log(thing.metadata.name); // "Human-readable name if any"
console.log(thing.metadata.types); // Set [ 'light', 'otherType' ]
console.log(thing.metadata.capabilities); // Set [ 'dimmable', 'colors' ]
console.log(thing.metadata.tags); // Set [ 'livingroom', 'type:light', 'cap:dimmable' ]

Types and capabilities are used to indicate what a thing is and what it is capable of doing. Most things you will encounter use a standarized API that is defined in the project called abstract-things.

Performing actions

All things support actions, which can be invoked as normal JavaScript functions. Actions in Tinkerhub always return a promise that will resolve to the result of the invocation.

 // Turn on the thing on asynchronously
thing.turnOn()
  .then(power => console.log('Power is now', power));

// Collections work the same but return a multi result
th.get('type:light')
  .power()
  .then(result => console.log('Power is mostly', result.mostlyTrue()));

Listening for events

Most things also emit events whenever things change. These can be listened to via on:

// Start listening
const handle = thing.on('power', (power, thing) => {
  // Device has either been turned on or off
  console.log('Power of', thing, 'is now', power);
});

// To stop listening
handle.stop();

The same is true for collections, where an event will be trigged if any thing in the collection emits an event:

const collection = th.get('type:light')
  .on('power', (power, thing) => {
    setTimeout(() => thing.turnOff(), 30000);
  });

Note: Collections do not return a event handles, the easiest way to stop listening for events on a collection is to call destroy() on it.

Waiting for things

Tinkerhub connects asynchronously and things can be found at any time so scripting can be difficult if you just want to perform an action or two. Something like this will fail if run via node script.js:

const th = require('tinkerhub');

th.get('type:light').turnOff(); // Don't do this, the collection will be empty

A special function named awaitThings is available for collections that will wait until things are mostly available:

const th = require('tinkerhub');

th.get('type:light')
  .awaitThings()
  .then(things => things.turnOff())
  .catch(th.errorHandler);
  .then(() => process.exit()) // To exit Node

This will wait in chunks of 500 ms for things to be found. After a few seconds it will resolve even if no things have been found.

Building a thing

Things in Tinkerhub are based on the library abstract-things, it contains both generic and specific types and capabilities for things such as sensors, lights, humidifiers, switches and so on.

A very basic thing may look something like this:

const th = require('tinkerhub');
const { Thing } = require('abstract-things');
const { duration } = require('abstract-things/values');

/**
 * Timer that calls itself `timer:global` and that allows timers to be set
 * and listened for in the network.
 */
class Timer extends Thing {
  static get type() {
    return 'timer';
  }

  constructor() {
    super();

    this.id = 'timer:global';
  }

  addTimer(name, delay) {
    if(! name) throw new Error('Timer needs a name');
    if(! delay) throw new Error('Timer needs a delay');

    delay = duration(delay);

    setTimeout(() => {
      this.emitEvent('timer', name);
    }, delay.ms)
  }
}

// Register the timer
th.register(new Timer())
  .then(handle => /* handle.remove() can be used to remove thing */)
  .catch(th.errorHandler);

Organizing Things

Things support user defined tags via their metadata. These tags are persisted on the same machine as a thing is registered. In the API tags are merged with system generated tags such as type tags and capability tags.

This can be used to create groups of things, such as all things found in a certain room. This allows for things such as this:

// Fetch lights in the living room and turn them on
th.get('type:light', 'living-room').turnOn();

The thing metadata object contains an API that can be used to the user defined tags of a device. Currently the prefixes type: and cap: are reserved.

console.log(thing.tags); // Get all of the tags

thing.metadata.addTags('tag1', ..., 'tagN'); // Add tags to the thing

thing.metadata.removeTags('tag1', ..., 'tagN'); // Remove tags from the thing

The easiest way to tag upp things is to use tinkerhub-cli and simply do deviceIdOrTag metadata tag nameOfTag.

Advanced matching

Advanced matching is supported via th.match for example to match all lights that are not tagged with living-room:

// Get lights not tagged with living-room
th.get('type:light', th.match.not('living-room'))

th.match.or and th.match.and can be used to get things using more advanced queries:

// Get things that are either lights or air purifiers
th.get(th.match.or('type:light', 'type:air-purifier'));

// Get things that are either lights or air purifiers that can switch their power
th.get(th.match.or('type:light', 'type:air-purifier'), 'cap:switchable-power');

// Either lights that can switch their power or things with switchable mode
th.get(th.match.or(
  th.match.and('type:light', 'cap:switchable-power'),
  'cap:switchable-mode'
));

Extending things in the network

Tinkerhub automatically merges things with the same identifier which allows one instance to be extended with new capabilities by other libaries.

This is primarily used together with plugins that bridge in things, such as Bluetooth peripherals or Z-wave devices. This allows the bridge to provide a generic API and other libraries to extend these things and make them in to specific types.

/*
 * Get all Bluetooth Low Energy devices that are connected and extend them
 * if they support a certain type.
 */
th.get('type:bluetooth-low-energy', 'cap:ble-connected')
  .extendWith(thing => thing.bleInspect()
    .then(data => {
      if(! data.services[SOME_SERVICE_ID]) return;

      return new SpecificThing(thing);
    })
  );
});

Handling errors

Most things in Tinkerhub return promises and catch should be used to handle errors from all promises. There are three main ways errors should be handled:

  1. Catch the error - but only if you can recover. Catching errors is usually done for non-important errors that can be recovered from by doing things such as retrying requests.
function doBackgroundStuff() {
  getPromiseSomehow()
    .then(result => /* handle result as normal */)
    .catch(err => setTimeout(doBackgroundStuff, 1000); // Retry every second until it succeeds (use a better retry strategy)
}
  1. Ignore the error - but only if you return a promise (or similar). This allows for example another consumer to handle the error in a better way.
function doStuff() {
  return getPromiseSomehow()
    .then(result => /* handle and manipulate results */)
}
  1. Log the error - when the error isn't that important or you can't recover from it. A utility is available that will do this: th.errorHandler.
function doStuff() {
  return getPromiseSomehow()
    .then(result => /* handle and manipulate results */)
    .catch(th.errorHandler);
}

// Or when registering a thing:
th.register(new Thing())
  .then(handle => /* handle points to the thing so it can be removed */)
  .catch(th.errorHandler);

Development helper

When developing a plugin or custom behavior Tinkerhub contains a utility that will log and output errors from things such as unhandled promise rejections. To activate it, put something like this in the main file of the project:

if(! module.parent) {
  // Only activate development mode if this file was run directly via `node nameOfFile.js`
  th.errorHandler.development();
}

This will turn on logging to the console for the namespace th:error. Any uncaught promise rejection or call to th.errorHandler will be displayed in full.

Debug logging

Tinkerhub uses debug for debug logging. Internal Tinkerhub-things live in the namespace th and things belong to the namespace things. Logging for both can be activated with th*:

$ DEBUG=th\* node fileToRun.js

Other interesting namespaces include the ataraxia which outputs information about the network and dwaal that outputs information about the key-value storage used by things (via abstract-things).

State handling

State is important in Tinkerhub, most things will have the capability state. State can be read and inspected by calling the state() action:

collection.state()
  .then(state => console.log('State is', state));

Things can also advertise that they are capabable of capturing and restoring state via the capability restorable-state. Things that are restorable will have these three actions available:

  • restorableState(): Array[string] - Get all of the state keys that can be restored.
  • captureState(): Object - Capture the current state as an object.
  • setState(Object) - Set the state of the thing.

To capture and restore the state the extra functions captureState(collection) and restoreState(collection, state) are available in tinkerhub/state:

const { captureState, restoreState } = require('tinkerhub/state');

An example of using capturing and restoring could be doing something like this to capture the state of lights, turn them off and a few seconds after restore their original state:

const th = require('tinkerhub');
const { captureState, restoreState } = require('tinkerhub/state');

let capturedState;
let lights = th.get('type:light', 'cap:restorable-state')

lights.awaitThings()
  
  // When lights are available, capture their state
  .then(() => captureState(lights))
  
  // Handle the state and request lights to be turned off
  .then(state => {
    capturedState = state;
    return lights.turnOff();
  })

  // Set a timeout for restoring the state five seconds after turning off
  .then(() => setTimeout(() => {
    // Restore the state
    restoreState(lights, capturedState)

      // Log any errors
      .catch(th.errorHandler)

      // Exit the process when state has been restored
      .then(() => process.exit());
  }, 5000))

  // Log any errors
  .catch(th.errorHandler);

Network

Tinkerhub creates a mesh network between instances. It is possible to use the library in several NodeJS instances on a single machine. Tinkerhub will manage the connections to other machines so that only a single network connection between machines exist.

A network may come to look a bit like this, where each machine connect to each other machine, but instances within a machine mainly connect to themselves.

+------------------+
|Machine #1        |
|                  |
| +--+     +--+    |
| |  +-----+  +--------+         +------------------+
| +--+     +-++    |   |         |Machine #2        |
|            |     |   |         |                  |
+------------------+   |         | +--+             |
             +---------------------+  |             |
                       |         | ++-+             |
                       |         |  |               |
                       |         +------------------+
                       |            |
       +---------------------+      |
       |Machine #3     |     |      |
       |               |     |      |
       |  +--+        ++-+   |      |
       |  |  +--------+  +----------+
       |  +--+        ++-+   |
       |               |     |
       |       +--+    |     |
       |       |  +----+     |
       |       +--+          |
       +---------------------+