├── .eslintignore ├── .gitignore ├── .npmignore ├── docs ├── docutils.conf ├── values │ ├── string.rst │ ├── array.rst │ ├── buffer.rst │ ├── object.rst │ ├── mixed.rst │ ├── percentage.rst │ ├── index.rst │ ├── code.rst │ ├── boolean.rst │ ├── voltage.rst │ ├── power.rst │ ├── angle.rst │ ├── soundPressureLevel.rst │ ├── energy.rst │ ├── temperature.rst │ ├── mass.rst │ ├── length.rst │ ├── duration.rst │ ├── illuminance.rst │ └── color.rst ├── common │ ├── index.rst │ ├── autonomous-charging.rst │ ├── power.rst │ ├── audio-feedback.rst │ ├── nameable.rst │ ├── switchable-mode.rst │ ├── battery-level.rst │ ├── state.rst │ └── error-state.rst ├── climate │ ├── vacuums.rst │ ├── index.rst │ ├── air-purifiers.rst │ ├── humidifiers.rst │ ├── dehumidifiers.rst │ ├── air-monitor.rst │ ├── spot-cleaning.rst │ └── target-humidity.rst ├── controllers │ ├── index.rst │ ├── buttons.rst │ ├── controllers.rst │ ├── remote-controls.rst │ └── wall-controllers.rst ├── electrical │ ├── index.rst │ ├── switches.rst │ ├── channels.rst │ ├── plugs.rst │ ├── outlets.rst │ ├── wall-outlets.rst │ ├── wall-switches.rst │ └── strips.rst ├── Makefile ├── lights │ ├── index.rst │ ├── color-full.rst │ ├── bulbs.rst │ ├── strips.rst │ ├── implementing.rst │ ├── fading.rst │ ├── color-temperature.rst │ └── brightness.rst ├── sensors │ ├── index.rst │ ├── pm10.rst │ ├── voltage.rst │ ├── power-load.rst │ ├── relative-humidity.rst │ ├── illuminance.rst │ ├── power-consumed.rst │ ├── temperature.rst │ ├── atmospheric-pressure.rst │ ├── pm2.5.rst │ └── carbon-monoxide-level.rst ├── make.bat ├── index.rst └── building-things │ ├── events.rst │ ├── naming.rst │ └── index.rst ├── config ├── __tests__ │ ├── .eslintrc │ ├── hierarchy.test.js │ └── parsePath.test.js ├── symbols.js ├── mergePath.js ├── root-group.js └── parsePath.js ├── children └── index.js ├── climate ├── fan.js ├── air-monitor.js ├── air-purifier.js ├── vacuum.js ├── humidifier.js ├── thermostat.js ├── dehumidifier.js ├── spot-cleaning.js ├── fan-speed.js ├── target-humidity.js ├── target-temperature.js ├── index.js ├── adjustable-fan-speed.js ├── autonomous-cleaning.js ├── adjustable-target-humidity.js └── adjustable-target-temperature.js ├── controllers ├── controller.js ├── button.js ├── remote-control.js ├── wall-controller.js ├── index.js └── actions.js ├── electrical ├── power-plug.js ├── power-strip.js ├── wall-outlet.js ├── wall-switch.js ├── power-channel.js ├── power-outlet.js ├── power-switch.js └── index.js ├── media ├── index.js ├── volume.js ├── muted.js ├── muteable.js └── adjustable-volume.js ├── values ├── string.js ├── number.js ├── parseNumber.js ├── boolean.js ├── buffer.js ├── change.js ├── percentage.js └── code.js ├── lights ├── light-bulb.js ├── light-strip.js ├── color-full.js ├── light.js ├── index.js ├── fading.js ├── switchable-power.js ├── light-state.js ├── brightness.js └── color-temperature.js ├── .editorconfig ├── placeholder.js ├── storage ├── index.js └── api.js ├── examples └── simple-thing.js ├── .eslintrc ├── common ├── easy-nameable.js ├── autonomous-charging.js ├── nameable.js ├── audio-feedback.js ├── battery-level.js ├── charging-state.js ├── error-state.js ├── power.js ├── switchable-audio-feedback.js ├── restorable-state.js └── switchable-mode.js ├── sensors ├── pm10.js ├── voltage.js ├── power-load.js ├── temperature.js ├── illuminance.js ├── power-consumed.js ├── sensor.js ├── atmospheric-pressure.js ├── pm2_5.js ├── index.js ├── relative-humidity.js ├── carbon-dioxide-level.js ├── carbon-monoxide-level.js ├── smoke-detection.js ├── water-detection.js ├── motion-detection.js ├── carbon-dioxide-detection.js ├── carbon-monoxide-detection.js └── contact-detection.js ├── package.json ├── utils ├── merge.js ├── metadata.js ├── collectMetadata.js └── converters.js ├── LICENSE.md ├── index.js └── polling.js /.eslintignore: -------------------------------------------------------------------------------- 1 | examples 2 | docs 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | docs/_build 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs 2 | node_modules 3 | npm-debug.log 4 | __tests__ 5 | -------------------------------------------------------------------------------- /docs/docutils.conf: -------------------------------------------------------------------------------- 1 | [restructuredtext parser] 2 | tab_width: 2 3 | -------------------------------------------------------------------------------- /config/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "rules": { 6 | "node/no-unpublished-require": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /children/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.Children = require('../common/children'); 4 | module.exports.ChildSyncer = require('./child-syncer'); 5 | -------------------------------------------------------------------------------- /climate/fan.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | /** 6 | * Fan. 7 | */ 8 | module.exports = Thing.type(Parent => class extends Parent { 9 | static get type() { 10 | return 'fan'; 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /controllers/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | module.exports = Thing.type(Parent => class extends Parent { 6 | 7 | static get type() { 8 | return 'controller'; 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /electrical/power-plug.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | module.exports = Thing.type(Parent => class extends Parent { 6 | 7 | static get type() { 8 | return 'power-plug'; 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /electrical/power-strip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | module.exports = Thing.type(Parent => class extends Parent { 6 | 7 | static get type() { 8 | return 'power-strip'; 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /electrical/wall-outlet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | module.exports = Thing.type(Parent => class extends Parent { 6 | 7 | static get type() { 8 | return 'wall-outlet'; 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /electrical/wall-switch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | module.exports = Thing.type(Parent => class extends Parent { 6 | 7 | static get type() { 8 | return 'wall-switch'; 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /docs/values/string.rst: -------------------------------------------------------------------------------- 1 | String 2 | ====== 3 | 4 | String value type. 5 | 6 | .. sourcecode:: js 7 | 8 | const { string } = require('abstract-things/values'); 9 | 10 | console.log(string('Hello world')); 11 | console.log(string(12)); 12 | -------------------------------------------------------------------------------- /electrical/power-channel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | module.exports = Thing.type(Parent => class extends Parent { 6 | 7 | static get type() { 8 | return 'power-channel'; 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /electrical/power-outlet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | module.exports = Thing.type(Parent => class extends Parent { 6 | 7 | static get type() { 8 | return 'power-outlet'; 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /electrical/power-switch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | module.exports = Thing.type(Parent => class extends Parent { 6 | 7 | static get type() { 8 | return 'power-switch'; 9 | } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /config/symbols.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | /** Symbol used for getting a value with a full key */ 5 | getValue: Symbol('getValue'), 6 | /** Symbol used for setting a value with a full key */ 7 | setValue: Symbol('setValue') 8 | }; 9 | -------------------------------------------------------------------------------- /media/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.Volume = require('./volume'); 4 | module.exports.AdjustableVolume = require('./adjustable-volume'); 5 | 6 | module.exports.Muted = require('./muted'); 7 | module.exports.Muteable = require('./muteable'); 8 | -------------------------------------------------------------------------------- /climate/air-monitor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | /** 6 | * Air Monitor. 7 | */ 8 | module.exports = Thing.type(Parent => class extends Parent { 9 | static get type() { 10 | return 'air-monitor'; 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /climate/air-purifier.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | /** 6 | * Air Purifier. 7 | */ 8 | module.exports = Thing.type(Parent => class extends Parent { 9 | static get type() { 10 | return 'air-purifier'; 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /climate/vacuum.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | /** 6 | * Type for marking a thing as a vacuum. 7 | */ 8 | module.exports = Thing.type(Parent => class extends Parent { 9 | static get type() { 10 | return 'vaccuum'; 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /values/string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * String type. Tries to convert everything to a string. 5 | */ 6 | module.exports = { 7 | create(value) { 8 | return String(value); 9 | }, 10 | 11 | is(value) { 12 | return typeof value === 'string'; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /lights/light-bulb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Light = require('./light'); 5 | 6 | module.exports = Thing.type(Parent => class extends Parent.with(Light) { 7 | 8 | static get type() { 9 | return 'light-bulb'; 10 | } 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /climate/humidifier.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | /** 6 | * Type for marking a thing as a Humidifier. 7 | */ 8 | module.exports = Thing.type(Parent => class extends Parent { 9 | static get type() { 10 | return 'humidifier'; 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /climate/thermostat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | /** 6 | * Type for marking a thing as a thermostat. 7 | */ 8 | module.exports = Thing.type(Parent => class extends Parent { 9 | static get type() { 10 | return 'thermostat'; 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /lights/light-strip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Light = require('./light'); 5 | 6 | module.exports = Thing.type(Parent => class extends Parent.with(Light) { 7 | 8 | static get type() { 9 | return 'light-strip'; 10 | } 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /climate/dehumidifier.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | /** 6 | * Type for marking things that are dehumidifiers. 7 | */ 8 | module.exports = Thing.type(Parent => class extends Parent { 9 | static get type() { 10 | return 'dehumidifer'; 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /controllers/button.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Controller = require('./controller'); 5 | 6 | module.exports = Thing.type(Parent => class extends Parent.with(Controller) { 7 | 8 | static get type() { 9 | return 'button'; 10 | } 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /lights/color-full.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | 5 | /** 6 | * Marker for lights that support a full color range. 7 | */ 8 | module.exports = Thing.mixin(Parent => class extends Parent { 9 | static get capability() { 10 | return 'color:full'; 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /controllers/remote-control.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Controller = require('./controller'); 5 | 6 | module.exports = Thing.type(Parent => class extends Parent.with(Controller) { 7 | 8 | static get type() { 9 | return 'remote-control'; 10 | } 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /controllers/wall-controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Controller = require('./controller'); 5 | 6 | module.exports = Thing.type(Parent => class extends Parent.with(Controller) { 7 | 8 | static get type() { 9 | return 'wall-controller'; 10 | } 11 | 12 | }); 13 | -------------------------------------------------------------------------------- /controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.Actions = require('./actions'); 4 | 5 | module.exports.Controller = require('./controller'); 6 | module.exports.Button = require('./button'); 7 | module.exports.RemoteControl = require('./remote-control'); 8 | module.exports.WallController = require('./wall-controller'); 9 | -------------------------------------------------------------------------------- /docs/values/array.rst: -------------------------------------------------------------------------------- 1 | Array 2 | ===== 3 | 4 | Value type for representing an array. Mostly used when converting to and from 5 | JSON. 6 | 7 | .. sourcecode:: js 8 | 9 | const values = require('abstract-things/values'); 10 | 11 | const json = values.toJSON('array', [ 'one', 'two' ]); 12 | const array = values.fromJSON('array', json); 13 | -------------------------------------------------------------------------------- /docs/values/buffer.rst: -------------------------------------------------------------------------------- 1 | Buffer 2 | ====== 3 | 4 | Buffer value type, for representing binary values. 5 | 6 | .. sourcecode:: js 7 | 8 | const { buffer } = require('abstract-things/values'); 9 | 10 | console.log(buffer(nodeBuffer)); 11 | console.log(buffer('base64-encoded-string-here')); 12 | console.log(buffer([ 100, 20, 240 ])); 13 | 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [package.json] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /docs/values/object.rst: -------------------------------------------------------------------------------- 1 | Object 2 | ===== 3 | 4 | Value type for representing an object. Mostly used when converting to and from 5 | JSON. 6 | 7 | .. sourcecode:: js 8 | 9 | const values = require('abstract-things/values'); 10 | 11 | const json = values.toJSON('object', { key: 'value' }); 12 | const array = values.fromJSON('object', json); 13 | -------------------------------------------------------------------------------- /values/number.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const parseNumber = require('./parseNumber'); 4 | 5 | /** 6 | * Number value type. Supports numbers and parsing numbers from strings. 7 | */ 8 | module.exports = { 9 | create(value) { 10 | return parseNumber(value); 11 | }, 12 | 13 | is(value) { 14 | return typeof value === 'number'; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /docs/common/index.rst: -------------------------------------------------------------------------------- 1 | Common capabilities 2 | =================== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | children 8 | state 9 | restorable-state 10 | nameable 11 | power 12 | switchable-power 13 | mode 14 | switchable-mode 15 | error-state 16 | battery-level 17 | charging-state 18 | autonomous-charging 19 | audio-feedback 20 | switchable-audio-feedback 21 | -------------------------------------------------------------------------------- /placeholder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('./thing'); 4 | 5 | /** 6 | * Mixin used to mark things as placeholders. A placeholder is a thing 7 | * that needs some configuration or authentication before it is usable. 8 | */ 9 | module.exports = Thing.mixin(Parent => class extends Parent { 10 | static get type() { 11 | return 'placeholder'; 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /docs/values/mixed.rst: -------------------------------------------------------------------------------- 1 | Mixed 2 | ===== 3 | 4 | Value type representing mixed values. Mostly used for converting to and from 5 | JSON. A mixed value can be any other value supported. 6 | 7 | .. sourcecode:: js 8 | 9 | const values = require('abstract-things/values'); 10 | 11 | const json = values.toJSON('mixed', somethingToConvert); 12 | const array = values.fromJSON('mixed', json); 13 | -------------------------------------------------------------------------------- /config/mergePath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Merge a path in the same format as used by parsePath. 5 | */ 6 | module.exports = function(...args) { 7 | let result = ''; 8 | for(let arg of args) { 9 | if(typeof arg === 'number') { 10 | result += '[' + arg + ']'; 11 | } else { 12 | if(result.length > 0) { 13 | result += '.'; 14 | } 15 | 16 | result += arg; 17 | } 18 | } 19 | 20 | return result; 21 | }; 22 | -------------------------------------------------------------------------------- /storage/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const api = require('./api'); 5 | const storage = Symbol('storage'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent { 8 | static get storage() { 9 | return api.global(); 10 | } 11 | 12 | get storage() { 13 | if(! this[storage]) { 14 | this[storage] = api.instance(this.id); 15 | } 16 | 17 | return this[storage]; 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /docs/values/percentage.rst: -------------------------------------------------------------------------------- 1 | Percentage 2 | ========== 3 | 4 | Number representing a percentage, forces the number to be between 0 and 100. 5 | 6 | .. sourcecode:: js 7 | 8 | const { percentage } = require('abstract-things/values'); 9 | 10 | console.log(percentage(80.2)); 11 | console.log(percentage('80.2')); 12 | console.log(percentage('80%')); 13 | 14 | String conversion 15 | ----------------- 16 | 17 | String conversion uses ``parseFloat``. 18 | -------------------------------------------------------------------------------- /electrical/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.PowerOutlet = require('./power-outlet'); 4 | module.exports.PowerChannel = require('./power-channel'); 5 | 6 | module.exports.PowerPlug = require('./power-plug'); 7 | module.exports.PowerStrip = require('./power-strip'); 8 | module.exports.WallOutlet = require('./wall-outlet'); 9 | module.exports.PowerSwitch = require('./power-switch'); 10 | module.exports.WallSwitch = require('./wall-switch'); 11 | -------------------------------------------------------------------------------- /docs/climate/vacuums.rst: -------------------------------------------------------------------------------- 1 | ``type:vacuum`` - Vacuum cleaners 2 | ================================= 3 | 4 | Vacuum cleaners are used as a type for both autonomous and non-autonomous 5 | cleaners. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('type:vacuum')) { 10 | // The thing is a vacuum 11 | } 12 | 13 | Implementing type 14 | ----------------- 15 | 16 | .. sourcecode:: js 17 | 18 | const { Vacuum } = require('abstract-things/climate'); 19 | 20 | class Example extends Vacuum.with(...) { 21 | 22 | } 23 | -------------------------------------------------------------------------------- /lights/light.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const { duration } = require('../values'); 5 | 6 | module.exports = Thing.type(Parent => class extends Parent { 7 | /** 8 | * Mark appliance as a `light`. 9 | */ 10 | static get type() { 11 | return 'light'; 12 | } 13 | 14 | static get availableAPI() { 15 | return []; 16 | } 17 | }); 18 | 19 | /** 20 | * Default duration for transitions of things such as brightness and color. 21 | */ 22 | module.exports.DURATION = duration(400); 23 | -------------------------------------------------------------------------------- /docs/climate/index.rst: -------------------------------------------------------------------------------- 1 | Climate 2 | ======= 3 | 4 | Climate types and capabilities are provided for things that have to do with the 5 | climate of a space, such as air purifiers, humidifiers, fans and thermostats. 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | :caption: Capabilities 10 | 11 | target-humidity 12 | adjustable-target-humidity 13 | cleaning-state 14 | autonomous-cleaning 15 | spot-cleaning 16 | 17 | .. toctree:: 18 | :maxdepth: 1 19 | :caption: Types 20 | 21 | air-monitor 22 | air-purifiers 23 | humidifiers 24 | dehumidifiers 25 | vacuums 26 | -------------------------------------------------------------------------------- /docs/controllers/index.rst: -------------------------------------------------------------------------------- 1 | Controllers 2 | ============= 3 | 4 | Controllers are things that control other things, such as remotes and buttons. 5 | If a thing implements the :doc:`actions `-capability it will emit 6 | events when an action occurs such as a button being pressed. The actual actions 7 | available vary from thing to thing. 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | :caption: Capabilities 12 | 13 | actions 14 | 15 | .. toctree:: 16 | :maxdepth: 1 17 | :caption: Types 18 | 19 | controllers 20 | buttons 21 | remote-controls 22 | wall-controllers 23 | -------------------------------------------------------------------------------- /docs/values/index.rst: -------------------------------------------------------------------------------- 1 | Values 2 | ====== 3 | 4 | ``abstract-things`` provides implementations of many commonly used value types, 5 | including conversions from strings and to and from JSON. 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | :caption: Value types 10 | 11 | angle 12 | area 13 | array 14 | boolean 15 | buffer 16 | code 17 | color 18 | duration 19 | energy 20 | illuminance 21 | length 22 | mass 23 | mixed 24 | number 25 | object 26 | percentage 27 | power 28 | pressure 29 | soundPressureLevel 30 | speed 31 | string 32 | temperature 33 | voltage 34 | volume 35 | -------------------------------------------------------------------------------- /docs/values/code.rst: -------------------------------------------------------------------------------- 1 | Code 2 | ===== 3 | 4 | Value type for representing a code with a description. Codes are commonly used 5 | for things like errors, actions and modes that need to be identifiable but also 6 | a human readable description. 7 | 8 | .. sourcecode:: js 9 | 10 | const { code } = require('abstract-things/values'); 11 | 12 | const testCode = code('test'); 13 | console.log(testCode.id); 14 | console.log(testCode.description); 15 | 16 | const testCode2 = code({ id: 'test', description: 'Description for code' }); 17 | const testCode3 = code('test: Description for code'); 18 | 19 | -------------------------------------------------------------------------------- /docs/values/boolean.rst: -------------------------------------------------------------------------------- 1 | Boolean 2 | ======== 3 | 4 | Boolean value type. Supports conversion from many common string values. 5 | 6 | .. sourcecode:: js 7 | 8 | const { boolean } = require('abstract-things/values'); 9 | 10 | console.log(boolean('true')); 11 | console.log(boolean(false)); 12 | console.log(boolean(1)); 13 | console.log(boolean('no')); 14 | 15 | String conversion 16 | ----------------- 17 | 18 | ``true``, ``yes``, ``on``, ``1`` will be treated as ``true``. 19 | ``false``, ``no``, ``off``, ``0`` represent a ``false`` value. Any other 20 | string values will be treated as an error. 21 | -------------------------------------------------------------------------------- /lights/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.Light = require('./light'); 4 | module.exports.LightBulb = require('./light-bulb'); 5 | module.exports.LightStrip = require('./light-strip'); 6 | module.exports.Fading = require('./fading'); 7 | 8 | module.exports.Brightness = require('./brightness'); 9 | module.exports.Dimmable = require('./dimmable'); 10 | module.exports.Colorable = require('./colorable'); 11 | module.exports.ColorTemperature = require('./color-temperature'); 12 | module.exports.ColorFull = require('./color-full'); 13 | module.exports.SwitchablePower = require('./switchable-power'); 14 | -------------------------------------------------------------------------------- /docs/electrical/index.rst: -------------------------------------------------------------------------------- 1 | Electrical 2 | ============= 3 | 4 | Electrical types and capabilities for power plugs, electrical outlets and 5 | sockets and more. The most common type is :doc:`power-outlet ` which is 6 | used to represent a single generic outlet/socket. Such a power outlet may be a 7 | child of other types such as the individual outlets in a 8 | :doc:`power strip ` or a :doc:`wall outlet `. 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | :caption: Types 13 | 14 | outlets 15 | channels 16 | strips 17 | plugs 18 | wall-outlets 19 | switches 20 | wall-switches 21 | -------------------------------------------------------------------------------- /examples/simple-thing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Thing } = require('../'); 4 | 5 | class SimpleThing extends Thing { 6 | // Define the type of the thing 7 | static get type() { 8 | return 'simple-thing'; 9 | } 10 | 11 | // Quick way to define actions available for this thing 12 | static get availableAPI() { 13 | return [ 'hello' ]; 14 | } 15 | 16 | constructor() { 17 | super(); 18 | 19 | // An identifier of the thing should always be set - with a namespace 20 | this.id = 'thing:example-1'; 21 | } 22 | 23 | hello() { 24 | return 'Cookies are tasty'; 25 | } 26 | } 27 | 28 | console.log(new SimpleThing()); 29 | -------------------------------------------------------------------------------- /values/parseNumber.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const amounts = require('amounts'); 4 | 5 | /** 6 | * Parse a string into a number using the generic amount. This allows the same 7 | * format of numbers with generic SI-suffixes as used in most other value 8 | * types. 9 | */ 10 | module.exports = function parseNumber(v) { 11 | if(typeof v === 'number') return v; 12 | if(typeof v !== 'string') { 13 | throw new Error('Can not convert into a number, string is needed'); 14 | } 15 | 16 | try { 17 | return amounts.amount(v).value; 18 | } catch(ex) { 19 | throw new Error('Could not convert into a number, invalid format for string: ' + v); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /values/boolean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Boolean value type. Parses some basic strings. 5 | */ 6 | module.exports = { 7 | create(value) { 8 | if(typeof value === 'boolean') return value; 9 | 10 | value = String(value).toLowerCase(); 11 | switch(value) { 12 | case 'true': 13 | case 'yes': 14 | case 'on': 15 | case '1': 16 | return true; 17 | case 'false': 18 | case 'no': 19 | case 'off': 20 | case '0': 21 | return false; 22 | default: 23 | throw new Error('Can not translate `' + value + '` into a boolean'); 24 | } 25 | }, 26 | 27 | is(value) { 28 | return typeof value === 'boolean'; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /docs/electrical/switches.rst: -------------------------------------------------------------------------------- 1 | ``type:power-switch`` - Power switches 2 | ====================================== 3 | 4 | Things marked with ``power-switch`` are switches that control something. 5 | Switches commonly control :doc:`power outlets `, 6 | :doc:`power channels ` and :doc:`lights `. 7 | 8 | .. sourcecode:: js 9 | 10 | if(thing.matches('type:power-switch')) { 11 | // This is a power switch 12 | } 13 | 14 | Implementing type 15 | ----------------- 16 | 17 | .. sourcecode:: js 18 | 19 | const { PowerSwitch } = require('abstract-things/electrical'); 20 | 21 | class Example extends PowerSwitch.with(...) { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ "node" ], 3 | "extends": [ "eslint:recommended", "plugin:node/recommended" ], 4 | "env": {}, 5 | "globals": {}, 6 | "rules": { 7 | "strict": [ "error", "global" ], 8 | "node/no-unsupported-features": [ 9 | "error", 10 | { 11 | "version": 8 12 | } 13 | ], 14 | "no-irregular-whitespace": 2, 15 | "quotes": [ 16 | 2, 17 | "single" 18 | ], 19 | "no-unused-vars": [ 20 | "error", 21 | { "vars": "all", "args": "none", "ignoreRestSiblings": false } 22 | ], 23 | "eqeqeq": [ "error" ], 24 | "no-throw-literal": [ "error" ], 25 | "semi": [ "error", "always" ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/climate/air-purifiers.rst: -------------------------------------------------------------------------------- 1 | ``type:air-purifier`` - Air purifiers 2 | ===================================== 3 | 4 | Air purifiers are appliances that filter and purify the air. Commonly used 5 | with the :doc:`switchable-power ` and 6 | :doc:`switchable-mode ` capabilities. 7 | 8 | .. sourcecode:: js 9 | 10 | if(thing.matches('type:air-purifier')) { 11 | // The thing is an air purifier 12 | } 13 | 14 | Implementing type 15 | ----------------- 16 | 17 | .. sourcecode:: js 18 | 19 | const { AirPurifier } = require('abstract-things/climate'); 20 | 21 | class Example extends AirPurifier.with(...) { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = Appliances 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/lights/index.rst: -------------------------------------------------------------------------------- 1 | Lights 2 | ====== 3 | 4 | The main type for lights is ``light``. Lights commonly use at least the 5 | :doc:`switchable-power <../common/switchable-power>` capability. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('type:light', 'cap:switchable-power')) { 10 | thing.power(true) 11 | .then(() => console.log('powered on')) 12 | .catch(err => console.log('error occurred', err)); 13 | } 14 | 15 | .. toctree:: 16 | :maxdepth: 1 17 | :caption: Topics 18 | 19 | implementing 20 | bulbs 21 | strips 22 | 23 | .. toctree:: 24 | :maxdepth: 1 25 | :caption: Capabilities 26 | 27 | fading 28 | brightness 29 | dimmable 30 | colorable 31 | color-temperature 32 | color-full 33 | -------------------------------------------------------------------------------- /common/easy-nameable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Nameable = require('./nameable'); 5 | const Storage = require('../storage'); 6 | 7 | /** 8 | * Capability for things that store their own name in the storage. 9 | */ 10 | module.exports = Thing.mixin(Parent => class extends Parent.with(Nameable, Storage) { 11 | constructor(...args) { 12 | super(...args); 13 | } 14 | 15 | initCallback() { 16 | return super.initCallback() 17 | .then(() => this.storage.get('name')) 18 | .then(name => name && (this.metadata.name = name)); 19 | } 20 | 21 | changeName(name) { 22 | return this.storage.set('name', name) 23 | .then(() => this.metadata.name = name); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /docs/lights/color-full.rst: -------------------------------------------------------------------------------- 1 | ``cap:color:full`` - light supports full range of color 2 | ======================================================= 3 | 4 | Capability used to mark lights that support setting any color. 5 | 6 | .. sourcecode:: js 7 | 8 | if(thing.matches('type:light', 'cap:color:full')) { 9 | // This light supports any color 10 | } 11 | 12 | Implementing capability 13 | ----------------------- 14 | 15 | Implementors of this capability have no special requirements placed upon them. 16 | 17 | Example: 18 | 19 | .. sourcecode:: js 20 | 21 | const { Light, ColorFull } = require('abstract-things/lights'); 22 | 23 | class Example extends Light.with(ColorFull) { 24 | 25 | constructor() { 26 | super(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /docs/lights/bulbs.rst: -------------------------------------------------------------------------------- 1 | ``type:light-bulb`` - Light bulbs 2 | ================================= 3 | 4 | The type ``light-bulb`` is a marker used to mark lights that are of the bulb 5 | type. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('cap:light-bulb')) { 10 | // The thing is a light bulb 11 | } 12 | 13 | Implementing capability 14 | ----------------------- 15 | 16 | Light bulbs are an extension to :doc:`lights ` and need to 17 | follow the same implementation guidelines. 18 | 19 | .. sourcecode:: js 20 | 21 | const { LightBulb, SwitchablePower } = require('abstract-things/lights'); 22 | 23 | class Example extends LightBulb.with(SwitchablePower) { 24 | 25 | changePower(power) { 26 | return changePowerOfLight(power); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /docs/lights/strips.rst: -------------------------------------------------------------------------------- 1 | ``type:light-strip`` - Light strips 2 | =================================== 3 | 4 | The type ``light-bulb`` is a marker used to mark lights that are of the strip 5 | type. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('cap:light-strip')) { 10 | // The thing is a light strip 11 | } 12 | 13 | Implementing capability 14 | ----------------------- 15 | 16 | Light strips are an extension to :doc:`lights ` and need to 17 | follow the same implementation guidelines. 18 | 19 | .. sourcecode:: js 20 | 21 | const { LightStrip, SwitchablePower } = require('abstract-things/lights'); 22 | 23 | class Example extends LightStrip.with(SwitchablePower) { 24 | 25 | changePower(power) { 26 | return changePowerOfLight(power); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /docs/climate/humidifiers.rst: -------------------------------------------------------------------------------- 1 | ``type:humidifier`` - Humidifiers 2 | ================================= 3 | 4 | Humidifiers are appliances that increase the humidity of the air. Many 5 | humidifers will support :doc:`switchable-power ` 6 | so that they can be switched on or off. Some implement 7 | :doc:`switchable-mode ` to support different modes, 8 | such as switching between automatic and manual modes. 9 | 10 | .. sourcecode:: js 11 | 12 | if(thing.matches('type:humidifier')) { 13 | // The thing is a humidifier 14 | } 15 | 16 | Implementing type 17 | ----------------- 18 | 19 | .. sourcecode:: js 20 | 21 | const { Humidifier } = require('abstract-things/climate'); 22 | 23 | class Example extends Humidifier.with(...) { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /docs/controllers/buttons.rst: -------------------------------------------------------------------------------- 1 | ``type:button`` - Single button 2 | =============================== 3 | 4 | If a thing is a single button the type ``button`` is commonly used. 5 | Buttons may emit events when buttons are pressed while implementing 6 | the :doc:`actions-capability `. Buttons are automatically marked as 7 | :doc:`controllers `. 8 | 9 | .. sourcecode:: js 10 | 11 | if(thing.matches('type:button')) { 12 | // This is a button 13 | 14 | if(thing.matches('cap:actions')) { 15 | // Button supports listening for actions 16 | } 17 | } 18 | 19 | Implementing type 20 | ----------------- 21 | 22 | .. sourcecode:: js 23 | 24 | const { Button, Actions } = require('abstract-things/controllers'); 25 | 26 | class Example extends Button.with(Actions, ...) { 27 | 28 | } 29 | -------------------------------------------------------------------------------- /lights/fading.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const { duration } = require('../values'); 5 | 6 | const maxChangeTime = Symbol('maxChangeTime'); 7 | 8 | /** 9 | * Marker for lights that support fading effects for dimming and color 10 | * changing. 11 | */ 12 | module.exports = Thing.mixin(Parent => class extends Parent { 13 | static get capability() { 14 | return 'fading'; 15 | } 16 | 17 | static availableAPI(builder) { 18 | builder.action('maxChangeTime') 19 | .description('Get the maximum duration a change can occur over') 20 | .returns('duration') 21 | .done(); 22 | } 23 | 24 | maxChangeTime() { 25 | return Promise.resolve(this[maxChangeTime]); 26 | } 27 | 28 | updateMaxChangeTime(t) { 29 | this[maxChangeTime] = duration(t); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /common/autonomous-charging.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('../common/state'); 5 | const ChargingState = require('./charging-state'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State, ChargingState) { 8 | 9 | static get capability() { 10 | return 'autonomous-cleaning'; 11 | } 12 | 13 | static availableAPI(builder) { 14 | builder.action('charge') 15 | .description('Start charging thing') 16 | .done(); 17 | } 18 | 19 | charge() { 20 | try { 21 | return Promise.resolve(this.activateCharging()) 22 | .then(() => null); 23 | } catch(ex) { 24 | return Promise.reject(ex); 25 | } 26 | } 27 | 28 | activateCharging() { 29 | throw new Error('activateCharging not implemented'); 30 | } 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /docs/controllers/controllers.rst: -------------------------------------------------------------------------------- 1 | ``type:controller`` - Generic controller 2 | ======================================== 3 | 4 | The ``controller`` type is used for things that are controllers and can be 5 | combined with more specific types. 6 | 7 | Controllers commonly emit events and implement the 8 | :doc:`actions-capability `. 9 | 10 | .. sourcecode:: js 11 | 12 | if(thing.matches('type:controller')) { 13 | // This is a wall controller 14 | 15 | if(thing.matches('cap:actions')) { 16 | // Controller supports listening for actions 17 | } 18 | } 19 | 20 | Implementing type 21 | ----------------- 22 | 23 | .. sourcecode:: js 24 | 25 | const { Controller, Actions } = require('abstract-things/controllers'); 26 | 27 | class Example extends Controller.with(Actions, ...) { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /docs/climate/dehumidifiers.rst: -------------------------------------------------------------------------------- 1 | ``type:dehumidifier`` - Dehumidifers 2 | ==================================== 3 | 4 | Dehumidifiers are appliances that decrease the humidity of the air. Many 5 | dehumidifers will support :doc:`switchable-power ` 6 | so that they can be switched on or off. Some implement 7 | :doc:`switchable-mode ` to support different modes, 8 | such as switching between automatic and manual modes. 9 | 10 | .. sourcecode:: js 11 | 12 | if(thing.matches('type:dehumidifier')) { 13 | // The thing is a dehumidifier 14 | } 15 | 16 | Implementing type 17 | ----------------- 18 | 19 | .. sourcecode:: js 20 | 21 | const { Dehumidifier } = require('abstract-things/climate'); 22 | 23 | class Example extends Dehumidifier.with(...) { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /docs/climate/air-monitor.rst: -------------------------------------------------------------------------------- 1 | ``type:air-monitor`` - Air quality monitor 2 | ========================================== 3 | 4 | This type is used for things where the primary function is to monitor 5 | air quality. Commonly these things are :doc:`sensors ` that report 6 | values such as :doc:`PM2.5 `, :doc:`PM!= `, 7 | :doc:`carbon dioxide ` or 8 | :doc:`carbon monoxide `. 9 | 10 | .. sourcecode:: js 11 | 12 | if(thing.matches('type:air-monitor')) { 13 | // The thing is an air monitor 14 | } 15 | 16 | Implementing type 17 | ----------------- 18 | 19 | .. sourcecode:: js 20 | 21 | const { AirMonitor } = require('abstract-things/climate'); 22 | 23 | class Example extends AirMonitor.with(...) { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /config/root-group.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Group = require('./group'); 4 | const { getValue, setValue } = require('./symbols'); 5 | 6 | /** 7 | * Extension for group that works as the root. Will keep track of the actual 8 | * configuration data. 9 | */ 10 | module.exports = class RootGroup extends Group { 11 | 12 | constructor(thing, data) { 13 | super(); 14 | 15 | this.thing = thing; 16 | this.data = new Map(); 17 | } 18 | 19 | [setValue](id, value) { 20 | return this.data.set(id, value); 21 | } 22 | 23 | [getValue](id) { 24 | return this.data.get(id); 25 | } 26 | 27 | getDescription() { 28 | const result = { 29 | children: [] 30 | }; 31 | 32 | for(const child of this.children) { 33 | result.children.push(child.getDescription()); 34 | } 35 | 36 | return result; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /docs/sensors/index.rst: -------------------------------------------------------------------------------- 1 | Sensors 2 | ======= 3 | 4 | The type ``sensor`` is used to mark things that read one or more values. 5 | 6 | .. sourcecode:: js 7 | 8 | if(thing.matches('type:sensor') { 9 | console.log('Sensor values:', thing.values()); 10 | } 11 | 12 | if(thing.matches('type:sensor', 'cap:temperature')) { 13 | console.log('Temperature:', thing.temperature()); 14 | } 15 | 16 | .. toctree:: 17 | :maxdepth: 1 18 | :caption: Capabilities 19 | 20 | atmospheric-pressure 21 | carbon-dioxide-detection 22 | carbon-dioxide-level 23 | carbon-monoxide-detection 24 | carbon-monoxide-level 25 | contact-detection 26 | illuminance 27 | motion-detection 28 | pm2.5 29 | pm10 30 | power-consumed 31 | power-load 32 | relative-humidity 33 | smoke-detection 34 | temperature 35 | voltage 36 | water-detection 37 | -------------------------------------------------------------------------------- /climate/spot-cleaning.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('../common/state'); 5 | const CleaningState = require('./cleaning-state'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State, CleaningState) { 8 | 9 | static get capability() { 10 | return 'spot-cleaning'; 11 | } 12 | 13 | static availableAPI(builder) { 14 | builder.action('cleanSpot') 15 | .description('Request that the thing performs spot cleaning') 16 | .done(); 17 | } 18 | 19 | cleanSpot() { 20 | try { 21 | return Promise.resolve(this.activateCleanSpot()) 22 | .then(() => null); 23 | } catch(ex) { 24 | return Promise.reject(ex); 25 | } 26 | } 27 | 28 | activateCleanSpot() { 29 | throw new Error('activateCleanSpot not implemented'); 30 | } 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /lights/switchable-power.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const SwitchablePower = require('../common/switchable-power'); 5 | const LightState = require('./light-state'); 6 | const { boolean } = require('../values'); 7 | 8 | module.exports = Thing.mixin(Parent => class extends Parent.with(SwitchablePower, LightState) { 9 | 10 | changePowerState(state) { 11 | // Do nothing, setLightState handles power changes instead 12 | } 13 | 14 | setLightState(state) { 15 | return super.setLightState(state) 16 | .then(() => { 17 | if(typeof state.power !== 'undefined') { 18 | return this.changePower(state.power); 19 | } 20 | }); 21 | } 22 | 23 | mapLightState(state) { 24 | super.mapLightState(state); 25 | 26 | if(typeof state.power !== 'undefined') { 27 | state.power = boolean(state.power); 28 | } 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /docs/controllers/remote-controls.rst: -------------------------------------------------------------------------------- 1 | ``type:remote-control`` - Remote controls 2 | ========================================= 3 | 4 | Remote controls are marked with the type ``remote-control``. Many remote 5 | controls are capable of emitting events when buttons are pressed and implement 6 | the :doc:`actions-capability `. Remote controls are automatically 7 | marked as :doc:`controllers `. 8 | 9 | .. sourcecode:: js 10 | 11 | if(thing.matches('type:remote-control')) { 12 | // This is a remote control 13 | 14 | if(thing.matches('cap:actions')) { 15 | // Remote control supports listening for actions 16 | } 17 | } 18 | 19 | Implementing type 20 | ----------------- 21 | 22 | .. sourcecode:: js 23 | 24 | const { RemoteControl, Actions } = require('abstract-things/controllers'); 25 | 26 | class Example extends RemoteControl.with(Actions, ...) { 27 | 28 | } 29 | -------------------------------------------------------------------------------- /values/buffer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Buffer type. Handles Base64 encoded strings and direct instances. 5 | * 6 | * TODO: Support for Uint8Array 7 | */ 8 | module.exports = { 9 | create(value) { 10 | if(value instanceof Buffer) { 11 | return value; 12 | } 13 | 14 | if(Array.isArray(value)) { 15 | // Assume this is an array with octets 16 | return Buffer.from(value); 17 | } else if(typeof value === 'object') { 18 | value = value.encoded; 19 | } 20 | 21 | if(typeof value === 'string') { 22 | // Assume this is Base-64 encoded string 23 | return Buffer.from(value, 'base64'); 24 | } else { 25 | throw new Error('Can not create buffer from value'); 26 | } 27 | }, 28 | 29 | is(value) { 30 | return value instanceof Buffer; 31 | }, 32 | 33 | toJSON(value) { 34 | return { 35 | encoded: value.toString('base64') 36 | }; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /sensors/pm10.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { number } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 8 | static get capability() { 9 | return 'pm10'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('pm10Changed') 14 | .type('number') 15 | .description('PM10 density has changed') 16 | .done(); 17 | 18 | builder.action('pm10') 19 | .description('Get the current PM10 density') 20 | .getterForState('pm10') 21 | .returns('number', 'Current PM10 density') 22 | .done(); 23 | } 24 | 25 | get sensorTypes() { 26 | return [ ...super.sensorTypes, 'pm10' ]; 27 | } 28 | 29 | pm10() { 30 | return this.value('pm10'); 31 | } 32 | 33 | updatePM10(value) { 34 | this.updateValue('pm10', number(value)); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /common/nameable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('./state'); 5 | 6 | /** 7 | * Capability for things that can be renamed. 8 | */ 9 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 10 | static availableAPI(builder) { 11 | builder.action('setName') 12 | .description('Set the name of this appliance') 13 | .argument('string', 'The name of the appliance') 14 | .done(); 15 | } 16 | 17 | static get capability() { 18 | return 'nameable'; 19 | } 20 | 21 | constructor(...args) { 22 | super(...args); 23 | } 24 | 25 | setName(name) { 26 | try { 27 | return Promise.resolve(this.changeName(name)) 28 | .then(() => this.metadata.name); 29 | } catch(ex) { 30 | return Promise.reject(ex); 31 | } 32 | } 33 | 34 | changeName(name) { 35 | throw new Error('changeName has not been implemented'); 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /sensors/voltage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { voltage } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 8 | static get capability() { 9 | return 'voltage'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('voltageChanged') 14 | .type('voltage') 15 | .description('Measured voltage changed') 16 | .done(); 17 | 18 | builder.action('voltage') 19 | .description('Get the current measured voltage') 20 | .getterForState('voltage') 21 | .returns('voltage', 'Current measured voltage') 22 | .done(); 23 | } 24 | 25 | get sensorTypes() { 26 | return [ ...super.sensorTypes, 'voltage' ]; 27 | } 28 | 29 | voltage() { 30 | return this.value('voltage'); 31 | } 32 | 33 | updateVoltage(value) { 34 | this.updateValue('voltage', voltage(value)); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /sensors/power-load.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { power } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 8 | static get capability() { 9 | return 'power-load'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('powerLoadChanged') 14 | .type('power') 15 | .description('The power load has changed') 16 | .done(); 17 | 18 | builder.action('powerLoad') 19 | .description('Get the current power load') 20 | .getterForState('powerLoad') 21 | .returns('power', 'Current power load') 22 | .done(); 23 | } 24 | 25 | get sensorTypes() { 26 | return [ ...super.sensorTypes, 'powerLoad' ]; 27 | } 28 | 29 | powerLoad() { 30 | return this.value('powerLoad'); 31 | } 32 | 33 | updatePowerLoad(value) { 34 | this.updateValue('powerLoad', power(value)); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /climate/fan-speed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('../common/state'); 5 | const { percentage } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 8 | 9 | static get capability() { 10 | return 'fan-speed'; 11 | } 12 | 13 | static availableAPI(builder) { 14 | builder.event('fanSpeedChanged') 15 | .type('percentage') 16 | .description('The fan speed has changed') 17 | .done(); 18 | 19 | builder.action('fanSpeed') 20 | .description('Get the fan speed') 21 | .returns('percentage', 'Current fan speed') 22 | .done(); 23 | } 24 | 25 | fanSpeed() { 26 | return Promise.resolve(this.getState('fanSpeed')); 27 | } 28 | 29 | updateFanSpeed(fanSpeed) { 30 | fanSpeed = percentage(fanSpeed); 31 | 32 | if(this.updateState('fanSpeed', fanSpeed)) { 33 | this.emitEvent('fanSpeedChanged', fanSpeed); 34 | } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=Appliances 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/controllers/wall-controllers.rst: -------------------------------------------------------------------------------- 1 | ``type:wall-controller`` - Controllers mounted on a wall 2 | ======================================================= 3 | 4 | ``wall-controller`` is used for controllers that are commonly mounted on a 5 | wall, such as switches and scene controllers. Wall controllers are 6 | automatically marked as :doc:`controllers `. 7 | 8 | Wall controllers may emit events when buttons are pressed while implementing 9 | the :doc:`actions-capability `. 10 | 11 | .. sourcecode:: js 12 | 13 | if(thing.matches('type:wall-controller')) { 14 | // This is a wall controller 15 | 16 | if(thing.matches('cap:actions')) { 17 | // Controller supports listening for actions 18 | } 19 | } 20 | 21 | Implementing type 22 | ----------------- 23 | 24 | .. sourcecode:: js 25 | 26 | const { WallController, Actions } = require('abstract-things/controllers'); 27 | 28 | class Example extends WallController.with(Actions, ...) { 29 | 30 | } 31 | -------------------------------------------------------------------------------- /sensors/temperature.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { temperature } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 8 | static get capability() { 9 | return 'temperature'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('temperatureChanged') 14 | .type('temperature') 15 | .description('Current temperature has changed') 16 | .done(); 17 | 18 | builder.action('temperature') 19 | .description('Get the current temperature') 20 | .getterForState('temperature') 21 | .returns('temperature', 'Current temperature') 22 | .done(); 23 | } 24 | 25 | get sensorTypes() { 26 | return [ ...super.sensorTypes, 'temperature' ]; 27 | } 28 | 29 | temperature() { 30 | return this.value('temperature'); 31 | } 32 | 33 | updateTemperature(temp) { 34 | this.updateValue('temperature', temperature(temp)); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /media/volume.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('../common/state'); 5 | const { percentage } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 8 | static get capability() { 9 | return 'audio-volume'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('volumeChanged') 14 | .type('percentage') 15 | .description('Current volume has changed') 16 | .done(); 17 | 18 | builder.action('volume') 19 | .description('Get the volume level as a percentage between 0 and 100') 20 | .returns('percentage', 'Current volume') 21 | .done(); 22 | } 23 | 24 | volume() { 25 | return Promise.resolve(this.getState('volume')); 26 | } 27 | 28 | updateVolume(volume) { 29 | volume = percentage(volume, { min: 0, max: 100, precision: 1 }); 30 | 31 | if(this.updateState('volume', volume)) { 32 | this.emitEvent('volumeChanged', volume); 33 | } 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /sensors/illuminance.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { illuminance } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 8 | static get capability() { 9 | return 'illuminance'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('illuminanceChanged') 14 | .type('illuminance') 15 | .description('Current illuminance has changed') 16 | .done(); 17 | 18 | builder.action('illuminance') 19 | .description('Get the current illuminance') 20 | .getterForState('illuminance') 21 | .returns('illuminance', 'Current illuminance') 22 | .done(); 23 | } 24 | 25 | get sensorTypes() { 26 | return [ ...super.sensorTypes, 'illuminance' ]; 27 | } 28 | 29 | illuminance() { 30 | return this.value('illuminance'); 31 | } 32 | 33 | updateIlluminance(value) { 34 | this.updateValue('illuminance', illuminance(value)); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /sensors/power-consumed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { energy } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 8 | static get capability() { 9 | return 'power-consumed'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('powerConsumedChanged') 14 | .type('energy') 15 | .description('The amount of power consumed') 16 | .done(); 17 | 18 | builder.action('powerConsumed') 19 | .description('Get the amount of power consumed') 20 | .getterForState('powerConsumed') 21 | .returns('energy', 'Amount of power consumed') 22 | .done(); 23 | } 24 | 25 | get sensorTypes() { 26 | return [ ...super.sensorTypes, 'powerConsumed' ]; 27 | } 28 | 29 | powerConsumed() { 30 | return this.value('powerConsumed'); 31 | } 32 | 33 | updatePowerConsumed(value) { 34 | this.updateValue('powerConsumed', energy(value)); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /docs/lights/implementing.rst: -------------------------------------------------------------------------------- 1 | Implementing lights 2 | =================== 3 | 4 | Protected methods 5 | ----------------- 6 | 7 | .. js:function:: setLightState(state) 8 | 9 | Set the state of the light. Light capabilities use this as a hook for 10 | restoring state. If this is not overriden capabilities implement a default 11 | behavior. 12 | 13 | :param object state: The state to set. 14 | :returns: Promise that resolves when the state is set. 15 | 16 | Example: 17 | 18 | .. sourcecode 19 | 20 | setLightState(state) { 21 | return doCustomStateRestore(state); 22 | } 23 | 24 | Power switching 25 | --------------- 26 | 27 | To support proper restoring of power the implementors of lights should use 28 | a custom ``SwitchablePower``: 29 | 30 | .. sourcecode:: js 31 | 32 | const { Light, SwitchablePower } = require('abstract-things/lights'); 33 | 34 | class Example extends Light.with(SwitchablePower) { 35 | 36 | changePower(power) { 37 | return changePowerOfLight(power); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /climate/target-humidity.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('../common/state'); 5 | const { percentage } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 8 | 9 | static get capability() { 10 | return 'target-humidity'; 11 | } 12 | 13 | static availableAPI(builder) { 14 | builder.event('targetHumidityChanged') 15 | .type('percentage') 16 | .description('The target humidity (relative) has changed') 17 | .done(); 18 | 19 | builder.action('targetHumidity') 20 | .description('Get the target humidity') 21 | .returns('percentage', 'The target humidity') 22 | .done(); 23 | } 24 | 25 | targetHumidity() { 26 | return Promise.resolve(this.getState('targetHumidity')); 27 | } 28 | 29 | updateTargetHumidity(target) { 30 | target = percentage(target); 31 | 32 | if(this.updateState('targetHumidity', target)) { 33 | this.emitEvent('targetHumidityChanged', target); 34 | } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /common/audio-feedback.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('./state'); 5 | const { boolean } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 8 | static get capability() { 9 | return 'audio-feedback'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('audioFeedbackChanged') 14 | .type('boolean') 15 | .description('Audio feedback is either enabled or disabled') 16 | .done(); 17 | 18 | builder.action('audioFeedback') 19 | .description('Get if the thing emits audio feedback') 20 | .returns('boolean', 'If audio feedback is enabled') 21 | .done(); 22 | } 23 | 24 | audioFeedback() { 25 | return Promise.resolve(this.getState('audioFeedback')); 26 | } 27 | 28 | updateAudioFeedback(enabled) { 29 | enabled = boolean(enabled, true); 30 | 31 | if(this.updateState('audioFeedback', enabled)) { 32 | this.emitEvent('audioFeedbackChanged', enabled); 33 | } 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /climate/target-temperature.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('../common/state'); 5 | const { temperature } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 8 | 9 | static get capability() { 10 | return 'target-temperature'; 11 | } 12 | 13 | static availableAPI(builder) { 14 | builder.event('targetTemperatureChanged') 15 | .type('temperature') 16 | .description('The target temperature has changed') 17 | .done(); 18 | 19 | builder.action('targetTemperature') 20 | .description('Get the target temperature') 21 | .returns('temperature', 'The target temperature') 22 | .done(); 23 | } 24 | 25 | targetHumidity() { 26 | return Promise.resolve(this.getState('targetTemperature')); 27 | } 28 | 29 | updateTargetHumidity(target) { 30 | target = temperature(target); 31 | 32 | if(this.updateState('targetTemperature', target)) { 33 | this.emitEvent('targetTemperatureChanged', target); 34 | } 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abstract-things", 3 | "version": "0.9.0", 4 | "license": "MIT", 5 | "description": "Base for building libraries that interact with physical things, such as IoT-devices", 6 | "repository": "tinkerhub/abstract-things", 7 | "keywords": [], 8 | "scripts": { 9 | "lint": "eslint .", 10 | "test": "jest && node_modules/.bin/eslint ." 11 | }, 12 | "engines": { 13 | "node": ">=8.0.0" 14 | }, 15 | "dependencies": { 16 | "amounts": "^0.5.0", 17 | "appdirectory": "^0.1.0", 18 | "color-convert": "^1.9.1", 19 | "color-string": "^1.5.2", 20 | "color-temperature": "^0.2.7", 21 | "debug": "^3.1.0", 22 | "deep-equal": "^1.0.1", 23 | "dwaal": "^0.2.0", 24 | "foibles": "^0.2.0", 25 | "is-mergeable-object": "^1.1.0", 26 | "mkdirp": "^0.5.1", 27 | "tinkerhub-discovery": "^0.3.1" 28 | }, 29 | "devDependencies": { 30 | "dumbfound-jest": "^0.3.1", 31 | "eslint": "^4.12.0", 32 | "eslint-plugin-node": "^5.2.1", 33 | "jest": "^22.2.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sensors/sensor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('../common/state'); 5 | 6 | module.exports = Thing.type(Parent => class extends Parent.with(State) { 7 | /** 8 | * Mark appliance as a `sensor`. 9 | */ 10 | static get type() { 11 | return 'sensor'; 12 | } 13 | 14 | static availableAPI(builder) { 15 | builder.action('values') 16 | .description('Get all sensor values') 17 | .returns('object') 18 | .done(); 19 | } 20 | 21 | value(sensorType) { 22 | return Promise.resolve(this.getState(sensorType, undefined)); 23 | } 24 | 25 | get sensorTypes() { 26 | return []; 27 | } 28 | 29 | values() { 30 | const result = {}; 31 | for(const type of this.sensorTypes) { 32 | result[type] = this.getState(type); 33 | } 34 | return Promise.resolve(result); 35 | } 36 | 37 | updateValue(sensorType, value) { 38 | if(this.updateState(sensorType, value)) { 39 | this.emitEvent(sensorType + 'Changed', value); 40 | return true; 41 | } else { 42 | return false; 43 | } 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /sensors/atmospheric-pressure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { pressure } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 8 | static get capability() { 9 | return 'atmospheric-pressure'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('atmosphericPressureChanged') 14 | .type('pressure') 15 | .description('Current atmospheric pressure has changed') 16 | .done(); 17 | 18 | builder.action('atmosphericPressure') 19 | .description('Get the current atmospheric pressure') 20 | .getterForState('pressure') 21 | .returns('pressure', 'Current atmospheric pressure') 22 | .done(); 23 | } 24 | 25 | get sensorTypes() { 26 | return [ ...super.sensorTypes, 'atmosphericPressure' ]; 27 | } 28 | 29 | get atmosphericPressure() { 30 | return this.value('atmosphericPressure'); 31 | } 32 | 33 | updateAtmosphericPressure(value) { 34 | this.updateValue('atmosphericPressure', pressure(value)); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /utils/merge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isMergeableObject = require('is-mergeable-object'); 4 | 5 | function merge(a, b) { 6 | if(Array.isArray(a)) { 7 | return mergeArray(a, b); 8 | } else if(a instanceof Set) { 9 | return mergeSet(a, b); 10 | } else if(isMergeableObject(a)) { 11 | return mergeObject(a, b); 12 | } else { 13 | return b; 14 | } 15 | } 16 | 17 | function mergeObject(a, b) { 18 | if(! b) return a; 19 | 20 | for(const key of Object.keys(b)) { 21 | const value = b[key]; 22 | if(typeof a[key] === 'undefined') { 23 | a[key] = value; 24 | } else { 25 | a[key] = merge(a[key], value); 26 | } 27 | } 28 | return a; 29 | } 30 | 31 | function mergeArray(a, b) { 32 | if(! b) return a; 33 | 34 | for(const value of b) { 35 | if(a.indexOf(value) < 0) { 36 | a.push(value); 37 | } 38 | } 39 | return a; 40 | } 41 | 42 | function mergeSet(a, b) { 43 | if(! b) return a; 44 | 45 | for(const value of b) { 46 | a.add(value); 47 | } 48 | return a; 49 | } 50 | 51 | module.exports = merge; 52 | module.exports.customMerge = Symbol('merge'); 53 | -------------------------------------------------------------------------------- /climate/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.AirMonitor = require('./air-monitor'); 4 | module.exports.AirPurifier = require('./air-purifier'); 5 | 6 | module.exports.Fan = require('./fan'); 7 | 8 | module.exports.Humidifier = require('./humidifier'); 9 | module.exports.Dehumidifier = require('./dehumidifier'); 10 | 11 | module.exports.Vacuum = require('./vacuum'); 12 | 13 | module.exports.TargetHumidity = require('./target-humidity'); 14 | module.exports.AdjustableTargetHumidity = require('./adjustable-target-humidity'); 15 | 16 | module.exports.FanSpeed = require('./fan-speed'); 17 | module.exports.AdjustableFanSpeed = require('./adjustable-fan-speed'); 18 | 19 | module.exports.CleaningState = require('./cleaning-state'); 20 | module.exports.AutonomousCleaning = require('./autonomous-cleaning'); 21 | module.exports.SpotCleaning = require('./spot-cleaning'); 22 | 23 | module.exports.Thermostat = require('./thermostat'); 24 | 25 | module.exports.TargetTemperature = require('./target-temperature'); 26 | module.exports.AdjustableTargetTemperature = require('./adjustable-target-temperature'); 27 | -------------------------------------------------------------------------------- /docs/values/voltage.rst: -------------------------------------------------------------------------------- 1 | Voltage 2 | ============ 3 | 4 | Representation of a voltage. Returns objects created by `amounts 5 | `_. 6 | 7 | .. sourcecode:: js 8 | 9 | const { voltage } = require('abstract-things/values'); 10 | 11 | // With no unit - volts is the default unit 12 | const v = voltage(20); 13 | console.log(v.value); 14 | console.log(v.volts); // convert to volts 15 | 16 | // With a unit 17 | console.log(voltage(50, 'V')); 18 | 19 | // String (with our without unit) 20 | console.log(voltage('220 volts')); 21 | 22 | Units 23 | ----- 24 | 25 | +------+-----+-----------------------------------+ 26 | | Unit | SI | Names | 27 | +======+=====+===================================+ 28 | | Volt | Yes | ``V``, ``v``, ``volt``, ``volts`` | 29 | +------+-----+-----------------------------------+ 30 | 31 | String conversion 32 | ----------------- 33 | 34 | Strings are parsed the same as for :doc:`numbers ` with the addition 35 | of units being parsed. The default unit is volts. 36 | 37 | Examples: ``20``, ``20 V``, ``100 volts`` 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Andreas Holstenson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /climate/adjustable-fan-speed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const FanSpeed = require('./fan-speed'); 5 | const { percentage } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(FanSpeed) { 8 | 9 | static get capability() { 10 | return 'adjustable-fan-speed'; 11 | } 12 | 13 | static availableAPI(builder) { 14 | builder.action('fanSpeed') 15 | .description('Get or set the fan speed') 16 | .argument('percentage', true, 'Optional fan speed to set') 17 | .returns('percentage', 'The fan speed') 18 | .done(); 19 | } 20 | 21 | fanSpeed(speed) { 22 | if(typeof speed === 'undefined') { 23 | return super.fanSpeed(); 24 | } 25 | 26 | return this.setFanSpeed(speed); 27 | } 28 | 29 | setFanSpeed(speed) { 30 | speed = percentage(speed, true); 31 | 32 | try { 33 | return Promise.resolve(this.changeFanSpeed(speed)) 34 | .then(() => super.fanSpeed()); 35 | } catch(ex) { 36 | return Promise.reject(ex); 37 | } 38 | } 39 | 40 | changeFanSpeed(speed) { 41 | throw new Error('changeFanSpeed not implemented'); 42 | } 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /sensors/pm2_5.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { number } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 8 | static get capability() { 9 | return 'pm2.5'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('pm2.5Changed') 14 | .type('number') 15 | .description('PM2.5 density has changed') 16 | .done(); 17 | 18 | builder.action('pm2_5') 19 | .description('Get the current PM2.5 density') 20 | .getterForState('pm2.5') 21 | .returns('number', 'Current PM2.5 density') 22 | .done(); 23 | 24 | builder.action('pm2.5') 25 | .description('Get the current PM2.5 density') 26 | .getterForState('pm2.5') 27 | .returns('number', 'Current PM2.5 density') 28 | .done(); 29 | } 30 | 31 | get sensorTypes() { 32 | return [ ...super.sensorTypes, 'pm2.5' ]; 33 | } 34 | 35 | pm2_5() { 36 | return this.value('pm2.5'); 37 | } 38 | 39 | ['pm2.5']() { 40 | return this.value('pm2.5'); 41 | } 42 | 43 | updatePM2_5(value) { 44 | this.updateValue('pm2.5', number(value)); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /climate/autonomous-cleaning.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('../common/state'); 5 | const CleaningState = require('./cleaning-state'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State, CleaningState) { 8 | 9 | static get capability() { 10 | return 'autonomous-cleaning'; 11 | } 12 | 13 | static availableAPI(builder) { 14 | builder.action('clean') 15 | .description('Start cleaning') 16 | .done(); 17 | 18 | builder.action('stop') 19 | .description('Stop cleaning') 20 | .done(); 21 | } 22 | 23 | clean() { 24 | try { 25 | return Promise.resolve(this.activateCleaning()) 26 | .then(() => null); 27 | } catch(ex) { 28 | return Promise.reject(ex); 29 | } 30 | } 31 | 32 | stop() { 33 | try { 34 | return Promise.resolve(this.deactivateCleaning()) 35 | .then(() => null); 36 | } catch(ex) { 37 | return Promise.reject(ex); 38 | } 39 | } 40 | 41 | activateCleaning() { 42 | throw new Error('activateCleaning not implemented'); 43 | } 44 | 45 | deactivateCleaning() { 46 | throw new Error('deactivateCleaning not implemented'); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /docs/electrical/channels.rst: -------------------------------------------------------------------------------- 1 | ``type:power-channel`` - Power channels 2 | ======================================= 3 | 4 | Things marked with ``power-channel`` represent a single channel of power. 5 | Power channels are usually virtual, such as individual power lines in a 6 | :doc:`power switch `. 7 | 8 | The :doc:`power ` and 9 | :doc:`switchable-power ` capability is commonly used 10 | with channels to support switch the power. Channels can also be 11 | :doc:`sensors ` if they report 12 | :doc:`power load ` or 13 | :doc:`power consumption `. 14 | 15 | .. sourcecode:: js 16 | 17 | if(thing.matches('type:power-channel')) { 18 | // This is a power channel 19 | 20 | if(thing.matches('cap:switchable-power')) { 21 | // And it also supports power switching 22 | thing.turnOn() 23 | .then(...) 24 | .catch(...); 25 | } 26 | } 27 | 28 | Implementing type 29 | ----------------- 30 | 31 | .. sourcecode:: js 32 | 33 | const { PowerChannel } = require('abstract-things/electrical'); 34 | 35 | class Example extends PowerChannel.with(...) { 36 | 37 | } 38 | -------------------------------------------------------------------------------- /values/change.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Change { 4 | constructor(value, type) { 5 | this.value = value; 6 | this.type = type; 7 | } 8 | 9 | get isIncrease() { 10 | return this.type === 'increase'; 11 | } 12 | 13 | get isDecrease() { 14 | return this.type === 'decrease'; 15 | } 16 | 17 | get isSet() { 18 | return this.type === 'set'; 19 | } 20 | } 21 | 22 | const CHANGE = /^\s*([+-])(.+)$/; 23 | module.exports = function(delegate) { 24 | const create = function(value) { 25 | if(typeof value === 'string') { 26 | const parsed = CHANGE.exec(value); 27 | 28 | if(parsed) { 29 | const value = delegate.create(parsed[2]); 30 | return new Change(value, parsed[1] === '+' ? 'increase' : 'decrease'); 31 | } 32 | 33 | return new Change(delegate.create(value), 'set'); 34 | } else if(typeof value === 'object') { 35 | return new Change(value.value, value.type); 36 | } else { 37 | throw new Error('Unable to create change for ' + value); 38 | } 39 | }; 40 | 41 | create.toJSON = function(value) { 42 | return { 43 | value: delegate.toJSON(value.value), 44 | type: value.type 45 | }; 46 | }; 47 | 48 | return create; 49 | }; 50 | -------------------------------------------------------------------------------- /docs/electrical/plugs.rst: -------------------------------------------------------------------------------- 1 | ``type:power-plug`` - Power plugs 2 | ================================= 3 | 4 | Things marked with ``power-plug`` are plugs that can be plugged in to an outlet. 5 | Most plugs are also :doc:`power outlets ` in that appliances can be 6 | plugged in to them. 7 | 8 | The :doc:`power ` and 9 | :doc:`switchable-power ` capability is commonly used 10 | with plugs to switch the power of the outlet of the plug. They can also be 11 | :doc:`sensors ` if they report 12 | :doc:`power load ` or 13 | :doc:`power consumption `. 14 | 15 | .. sourcecode:: js 16 | 17 | if(thing.matches('type:power-plug')) { 18 | // This is a power plug 19 | 20 | if(thing.matches('cap:switchable-power')) { 21 | // And it also supports power switching 22 | thing.turnOn() 23 | .then(...) 24 | .catch(...); 25 | } 26 | } 27 | 28 | Implementing type 29 | ----------------- 30 | 31 | .. sourcecode:: js 32 | 33 | const { PowerPlug, PowerOutlet } = require('abstract-things/electrical'); 34 | 35 | class Example extends PowerPlug.with(PowerOutlet, ...) { 36 | 37 | } 38 | -------------------------------------------------------------------------------- /sensors/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.Sensor = require('./sensor'); 4 | 5 | module.exports.AtmosphericPressure = require('./atmospheric-pressure'); 6 | module.exports.CarbonDioxideDetection = require('./carbon-dioxide-detection'); 7 | module.exports.CarbonDioxideLevel = require('./carbon-dioxide-level'); 8 | module.exports.CarbonMonoxideDetection = require('./carbon-monoxide-detection'); 9 | module.exports.CarbonMonoxideLevel = require('./carbon-monoxide-level'); 10 | module.exports.ContactDetection = require('./contact-detection'); 11 | module.exports.Illuminance = require('./illuminance'); 12 | module.exports.MotionDetection = require('./motion-detection'); 13 | module.exports.PM2_5 = require('./pm2_5'); 14 | module.exports.PM10 = require('./pm10'); 15 | module.exports.PowerConsumed = require('./power-consumed'); 16 | module.exports.PowerLoad = require('./power-load'); 17 | module.exports.RelativeHumidity = require('./relative-humidity'); 18 | module.exports.Temperature = require('./temperature'); 19 | module.exports.SmokeDetection = require('./smoke-detection'); 20 | module.exports.Voltage = require('./voltage'); 21 | module.exports.WaterDetection = require('./water-detection'); 22 | -------------------------------------------------------------------------------- /values/percentage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const parseNumber = require('./parseNumber'); 4 | 5 | /** 6 | * Percentage type. Supports numbers and strings that may end with %. 7 | */ 8 | module.exports = { 9 | create(value, options) { 10 | if(typeof value === 'string') { 11 | value = value.trim(); 12 | 13 | if(value.endsWith('%')) { 14 | // Cut off % at the end 15 | value = value.substring(0, value.length - 1); 16 | } 17 | 18 | value = parseNumber(value); 19 | } else if(typeof value !== 'number') { 20 | throw new Error('Can not translate to a percentage'); 21 | } 22 | 23 | if(typeof options !== 'undefined') { 24 | const min = options.min; 25 | if(typeof min !== 'undefined') { 26 | if(value < min) { 27 | value = min; 28 | } 29 | } 30 | 31 | const max = options.max; 32 | if(typeof max !== 'undefined') { 33 | if(value > max) { 34 | value = max; 35 | } 36 | } 37 | 38 | const precision = options.precision; 39 | if(typeof precision !== 'undefined') { 40 | const p = Math.pow(10, precision); 41 | value = Math.round(value * p) / p; 42 | } 43 | } 44 | 45 | return value; 46 | }, 47 | 48 | comparable: true 49 | }; 50 | -------------------------------------------------------------------------------- /docs/electrical/outlets.rst: -------------------------------------------------------------------------------- 1 | ``type:power-outlet`` - Power outlets 2 | ===================================== 3 | 4 | Things marked with ``power-outlet`` represent a single outlet that can take a 5 | single plug. Outlets can be both stand-alone and children of another thing, 6 | such as a :doc:`power strip ` or :doc:`wall outlet `. 7 | 8 | The :doc:`power ` and 9 | :doc:`switchable-power ` capability is commonly used 10 | with outlets to switch the power of the outlet. Outlets can also be 11 | :doc:`sensors ` if they report 12 | :doc:`power load ` or 13 | :doc:`power consumption `. 14 | 15 | .. sourcecode:: js 16 | 17 | if(thing.matches('type:power-outlet')) { 18 | // This is a power outlet 19 | 20 | if(thing.matches('cap:switchable-power')) { 21 | // And it also supports power switching 22 | thing.turnOn() 23 | .then(...) 24 | .catch(...); 25 | } 26 | } 27 | 28 | Implementing type 29 | ----------------- 30 | 31 | .. sourcecode:: js 32 | 33 | const { PowerOutlet } = require('abstract-things/electrical'); 34 | 35 | class Example extends PowerOutlet.with(...) { 36 | 37 | } 38 | -------------------------------------------------------------------------------- /media/muted.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('../common/state'); 5 | const { boolean } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 8 | static get capability() { 9 | return 'audio-muted'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('mutedChanged') 14 | .type('boolean') 15 | .description('Current mute status has changed') 16 | .done(); 17 | 18 | builder.event('muted') 19 | .description('Audio has been muted') 20 | .done(); 21 | 22 | builder.event('unmuted') 23 | .description('Audio has been unmuted') 24 | .done(); 25 | 26 | builder.action('muted') 27 | .description('Get if currently muted') 28 | .returns('boolean', 'Current mute status') 29 | .done(); 30 | } 31 | 32 | muted() { 33 | return Promise.resolve(this.getState('muted')); 34 | } 35 | 36 | updateMuted(muted) { 37 | muted = boolean(muted); 38 | 39 | if(this.updateState('muted', muted)) { 40 | this.emitEvent('mutedChanged', muted); 41 | 42 | if(muted) { 43 | this.emitEvent('muted'); 44 | } else { 45 | this.emitEvent('umuted'); 46 | } 47 | } 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /docs/values/power.rst: -------------------------------------------------------------------------------- 1 | Power 2 | ============ 3 | 4 | Representation of power. Returns objects created by `amounts 5 | `_. 6 | 7 | .. sourcecode:: js 8 | 9 | const { power } = require('abstract-things/values'); 10 | 11 | // With no unit - watt is the default unit 12 | const v = power(200); 13 | console.log(v.value); 14 | console.log(v.hp); // convert to horsepower 15 | console.log(v.watt); // convert to watts 16 | 17 | // With a unit 18 | console.log(power(1, 'hp')); 19 | 20 | // String (with our without unit) 21 | console.log(power('200 W')); 22 | 23 | Units 24 | ----- 25 | 26 | +------------+-----+------------------------+ 27 | | Unit | SI | Names | 28 | +============+=====+========================+ 29 | | Watt | Yes | ``w``, ``W``, ``watt`` | 30 | +------------+-----+------------------------+ 31 | | Horsepower | No | ``hp``, ``horsepower`` | 32 | +------------+-----+------------------------+ 33 | 34 | String conversion 35 | ----------------- 36 | 37 | Strings are parsed the same as for :doc:`numbers ` with the addition 38 | of units being parsed. The default unit is watt. 39 | 40 | Examples: ``200``, ``200 W``, ``1 hp``, ``200 horsepower`` 41 | -------------------------------------------------------------------------------- /docs/values/angle.rst: -------------------------------------------------------------------------------- 1 | Angle 2 | ===== 3 | 4 | Representation of an angle. Returns objects created by `amounts 5 | `_. 6 | 7 | .. sourcecode:: js 8 | 9 | const { angle } = require('abstract-things/values'); 10 | 11 | // With no unit - degrees are the default unit 12 | const v = angle(200); 13 | console.log(v.value); 14 | console.log(v.rad); // number converted to radians 15 | 16 | // With a unit 17 | console.log(angle(5, 'rad')); 18 | 19 | // String (with our without unit) 20 | console.log(angle('5 rad')); 21 | 22 | Units 23 | ----- 24 | 25 | +--------+-----+----------------------------------+ 26 | | Unit | SI | Names | 27 | +========+=====+==================================+ 28 | | Degree | No | ``deg``, ``degree``, ``degrees`` | 29 | +--------+-----+----------------------------------+ 30 | | Radian | Yes | ``rad``, ``radian``, ``radians`` | 31 | +--------+-----+----------------------------------+ 32 | 33 | String conversion 34 | ----------------- 35 | 36 | Strings are parsed the same as for :doc:`numbers ` with the addition 37 | of units being parsed. The default unit is degrees. 38 | 39 | Examples: ``200``, ``200 deg``, ``5 rad``, ``5 radians`` 40 | -------------------------------------------------------------------------------- /common/battery-level.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('./state'); 5 | const { percentage } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 8 | 9 | static availableAPI(builder) { 10 | builder.state('batteryLevel') 11 | .type('percentage') 12 | .description('Current battery level of the appliance') 13 | .done(); 14 | 15 | builder.event('batteryLevelChanged') 16 | .type('percentage') 17 | .description('Battery level of the appliance has changed') 18 | .done(); 19 | 20 | builder.action('batteryLevel') 21 | .description('Get the battery level of the appliance') 22 | .returns('percentage', 'Current battery level') 23 | .done(); 24 | } 25 | 26 | static get capability() { 27 | return 'battery-level'; 28 | } 29 | 30 | constructor(...args) { 31 | super(...args); 32 | 33 | this.updateState('batteryLevel', -1); 34 | } 35 | 36 | batteryLevel() { 37 | return Promise.resolve(this.getState('batteryLevel')); 38 | } 39 | 40 | updateBatteryLevel(level) { 41 | level = percentage(level); 42 | if(this.updateState('batteryLevel', level)) { 43 | this.emitEvent('batteryLevelChanged', level); 44 | } 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /lights/light-state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const RestorableState = require('../common/restorable-state'); 5 | 6 | module.exports = Thing.mixin(Parent => class extends Parent.with(RestorableState) { 7 | /** 8 | * Set the state of this light. The default implementation simply 9 | * delegates to `change`-methods in capabilities. 10 | * 11 | * For light implementations that support changing several properties 12 | * at once it is recommended to override this method to do manual state 13 | * switching. 14 | * 15 | * @param {object} state 16 | */ 17 | setLightState(state) { 18 | return Promise.resolve(); 19 | } 20 | 21 | mapLightState(state) { 22 | } 23 | 24 | /** 25 | * Restore the state of this light. Hooks into those lights that support 26 | * action and delegates restore to `restoreLightState`. 27 | * 28 | * @param {object} state 29 | */ 30 | changeState(state) { 31 | try { 32 | return Promise.resolve(super.changeState(state)) 33 | .then(() => { 34 | const stateCopy = Object.assign({}, state); 35 | this.mapLightState(stateCopy); 36 | return this.setLightState(stateCopy); 37 | }); 38 | } catch(ex) { 39 | return Promise.reject(ex); 40 | } 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | abstract-things 2 | ====================================== 3 | 4 | `abstract-things` is a JavaScript library that provides a simple base for 5 | building libraries that interact with physical things, such as IoT-devices, and virtual things. 6 | 7 | This library provides a base class named `Thing` that supports mixins of 8 | various types. Things are described using two types of tags, one describing 9 | the type of the thing and one describing its capabilities. Things are also 10 | expected to describe their public API, to make remote use easier. 11 | 12 | Types and capabilities are designed to be stable and to be combined. When 13 | combined they describe a thing and what it can do. 14 | 15 | .. note:: 16 | 17 | This documentation is a work in progress. Things are missing and may 18 | sometimes be inaccurate. Please open issues on `Github 19 | `_ if you find something 20 | that seems wrong. 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | :caption: Getting started 25 | 26 | using-things 27 | building-things/index 28 | values/index 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | :caption: Types and capabilities 33 | 34 | common/index 35 | controllers/index 36 | lights/index 37 | sensors/index 38 | climate/index 39 | electrical/index 40 | -------------------------------------------------------------------------------- /docs/values/soundPressureLevel.rst: -------------------------------------------------------------------------------- 1 | Sound Pressure Level 2 | ==================== 3 | 4 | Representation of a sound pressure level. Returns objects created by `amounts 5 | `_. 6 | 7 | .. sourcecode:: js 8 | 9 | const { soundPressureLevel } = require('abstract-things/values'); 10 | 11 | // With no unit - decibel is the default unit 12 | const v = soundPressureLevel(40.2); 13 | console.log(v.value); 14 | console.log(v.db); // convert to decibel 15 | 16 | // With a unit 17 | console.log(soundPressureLevel(50, 'dB')); 18 | 19 | // String (with our without unit) 20 | console.log(soundPressureLevel('20 decibels')); 21 | 22 | Units 23 | ----- 24 | 25 | +----------+----+----------------------------------------------------+ 26 | | Unit | SI | Names | 27 | +==========+====+====================================================+ 28 | | Decibels | No | ``dB``, ``db``, ``dbs``, ``decibel``, ``decibels`` | 29 | +----------+----+----------------------------------------------------+ 30 | 31 | String conversion 32 | ----------------- 33 | 34 | Strings are parsed the same as for :doc:`numbers ` with the addition 35 | of units being parsed. The default unit is decibel. 36 | 37 | Examples: ``20``, ``45.5 dB``, ``100 decibels`` 38 | -------------------------------------------------------------------------------- /sensors/relative-humidity.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { percentage } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 8 | static get capability() { 9 | return 'relative-humidity'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('relativeHumidityChanged') 14 | .type('percentage') 15 | .description('Current relative humidity has changed') 16 | .done(); 17 | 18 | builder.action('relativeHumidity') 19 | .description('Get the current relative humidity') 20 | .getterForState('relativeHumidity') 21 | .returns('percentage', 'Current relative humidity') 22 | .done(); 23 | 24 | builder.action('rh') 25 | .description('Get the current relative humidity') 26 | .getterForState('relativeHumidity') 27 | .returns('percentage', 'Current relative humidity') 28 | .done(); 29 | } 30 | 31 | get sensorTypes() { 32 | return [ ...super.sensorTypes, 'relativeHumidity' ]; 33 | } 34 | 35 | relativeHumidity() { 36 | return this.value('relativeHumidity'); 37 | } 38 | 39 | rh() { 40 | return this.value('relativeHumidity'); 41 | } 42 | 43 | updateRelativeHumidity(value) { 44 | this.updateValue('relativeHumidity', percentage(value)); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /sensors/carbon-dioxide-level.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { number } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 8 | static get capability() { 9 | return 'carbon-dioxide-level'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('carbonDioxideLevelChanged') 14 | .type('number') 15 | .description('Carbon dioxide level has changed') 16 | .done(); 17 | 18 | builder.action('carbonDioxideLevel') 19 | .description('Get the current carbon dioxide level') 20 | .getterForState('carbonDioxide') 21 | .returns('number', 'Current carbon dixoide level') 22 | .done(); 23 | 24 | builder.action('co2Level') 25 | .description('Get the current carbon dioxide level') 26 | .getterForState('carbonDioxide') 27 | .returns('number', 'Current carbon dixoide level') 28 | .done(); 29 | } 30 | 31 | get sensorTypes() { 32 | return [ ...super.sensorTypes, 'carbonDioxideLevel' ]; 33 | } 34 | 35 | carbonDioxideLevel() { 36 | return this.value('carbonDioxideLevel'); 37 | } 38 | 39 | co2Level() { 40 | return this.value('carbonDioxideLevel'); 41 | } 42 | 43 | updateCarbonDioxideLevel(value) { 44 | this.updateValue('carbonDioxideLevel', number(value)); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /docs/values/energy.rst: -------------------------------------------------------------------------------- 1 | Energy 2 | ======= 3 | 4 | Representation of an energy amount. Returns objects created by `amounts 5 | `_. 6 | 7 | .. sourcecode:: js 8 | 9 | const { energy } = require('abstract-things/values'); 10 | 11 | // With no unit - joules are assumed 12 | const v = energy(200); 13 | console.log(v.value); 14 | console.log(v.wh); // number converted to watt hours 15 | 16 | // With a unit 17 | console.log(energy(3.5, 'Wh')); 18 | 19 | // String (with our without unit) 20 | console.log(energy('5 J')); 21 | 22 | Units 23 | ----- 24 | 25 | +------------+------+-----------------------------------------------+ 26 | | Unit | SI | Names | 27 | +============+======+===============================================+ 28 | | Joules | Yes | ``J``, ``j``, ``joule``, ``joules`` | 29 | +------------+------+-----------------------------------------------+ 30 | | Watt hours | True | ``Wh``, ``wh``, ``watt hour``, ``watt hours`` | 31 | +------------+------+-----------------------------------------------+ 32 | 33 | String conversion 34 | ----------------- 35 | 36 | Strings are parsed the same as for :doc:`numbers ` with the addition 37 | of units being parsed. The default unit is joules. 38 | 39 | Examples: ``200``, ``200 J``, ``3.5 Wh``, ``40 kJ`` 40 | -------------------------------------------------------------------------------- /sensors/carbon-monoxide-level.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { number } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 8 | static get capability() { 9 | return 'carbon-monoxide-level'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.event('carbonMonoxideLevelChanged') 14 | .type('number') 15 | .description('Carbon monoxide level has changed') 16 | .done(); 17 | 18 | builder.action('carbonMonoxideLevel') 19 | .description('Get the current carbon monoxide level') 20 | .getterForState('carbonMonoxide') 21 | .returns('number', 'Current carbon monoxide level') 22 | .done(); 23 | 24 | builder.action('coLevel') 25 | .description('Get the current carbon monoxide level') 26 | .getterForState('carbonMonoxide') 27 | .returns('number', 'Current carbon monoxide level') 28 | .done(); 29 | } 30 | 31 | get sensorTypes() { 32 | return [ ...super.sensorTypes, 'carbonMonoxideLevel' ]; 33 | } 34 | 35 | carbonMonoxideLevel() { 36 | return this.value('carbonMonoxideLevel'); 37 | } 38 | 39 | coLevel() { 40 | return this.value('carbonMonoxideLevel'); 41 | } 42 | 43 | updateCarbonMonoxideLevel(value) { 44 | this.updateValue('carbonMonoxideLevel', number(value)); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.Thing = require('./thing'); 4 | 5 | module.exports.Configurable = require('./config/configurable'); 6 | 7 | module.exports.Discovery = require('./discovery'); 8 | module.exports.Polling = require('./polling'); 9 | 10 | module.exports.State = require('./common/state'); 11 | module.exports.RestorableState = require('./common/restorable-state'); 12 | 13 | module.exports.ErrorState = require('./common/error-state'); 14 | 15 | module.exports.Storage = require('./storage'); 16 | 17 | module.exports.Children = require('./common/children'); 18 | 19 | module.exports.Nameable = require('./common/nameable'); 20 | module.exports.EasyNameable = require('./common/easy-nameable'); 21 | 22 | module.exports.Power = require('./common/power'); 23 | module.exports.SwitchablePower = require('./common/switchable-power'); 24 | 25 | module.exports.Mode = require('./common/mode'); 26 | module.exports.SwitchableMode = require('./common/switchable-mode'); 27 | 28 | module.exports.BatteryLevel = require('./common/battery-level'); 29 | module.exports.ChargingState = require('./common/charging-state'); 30 | module.exports.AutonomousCharging = require('./common/autonomous-charging'); 31 | 32 | module.exports.AudioFeedback = require('./common/audio-feedback'); 33 | module.exports.SwitchableAudioFeedback = require('./common/switchable-audio-feedback'); 34 | 35 | module.exports.Placeholder = require('./placeholder'); 36 | -------------------------------------------------------------------------------- /config/__tests__/hierarchy.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { mixed } = require('../../values'); 4 | const Thing = require('../../thing'); 5 | 6 | const RootGroup = require('../root-group'); 7 | const Group = require('../group'); 8 | const Property = require('../property'); 9 | 10 | describe('Configuration', () => { 11 | describe('Group and Property hierarchy', () => { 12 | 13 | test('Property on root settable and gettable', () => { 14 | const root = new RootGroup(new Thing()); 15 | const property = new Property(root, 'test', { 16 | type: mixed 17 | }); 18 | 19 | property.update('test'); 20 | const current = property.get(); 21 | 22 | expect(current).toBe('test'); 23 | }); 24 | 25 | test('Property on sub group findable', () => { 26 | const root = new RootGroup(new Thing()); 27 | const group = new Group(root, 'group'); 28 | const property = new Property(group, 'test', { 29 | type: mixed 30 | }); 31 | 32 | const foundProperty = root.get('group.test'); 33 | expect(foundProperty).toBe(property); 34 | }); 35 | 36 | test('Property on sub group settable and gettable', () => { 37 | const root = new RootGroup(new Thing()); 38 | const group = new Group(root, 'group'); 39 | const property = new Property(group, 'test', { 40 | type: mixed 41 | }); 42 | 43 | property.update('test'); 44 | const current = property.get(); 45 | 46 | expect(current).toBe('test'); 47 | }); 48 | 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /config/__tests__/parsePath.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const parsePath = require('../parsePath'); 4 | const { randomizedTest } = require('dumbfound-jest'); 5 | 6 | describe('Configuration', () => { 7 | describe('parsePath', () => { 8 | 9 | test('Single segment', () => { 10 | const p = parsePath('key'); 11 | 12 | expect(p).toEqual([ 'key' ]); 13 | }); 14 | 15 | test('Multiple segments', () => { 16 | const p = parsePath('one.two'); 17 | 18 | expect(p).toEqual([ 'one', 'two' ]); 19 | }); 20 | 21 | randomizedTest('Single key, random', random => { 22 | const v = random.asciiAlphaNumeric(); 23 | const p = parsePath(v); 24 | 25 | expect(p).toEqual([ v ]); 26 | }); 27 | 28 | test('Single array selector', () => { 29 | const p = parsePath('[0]'); 30 | 31 | expect(p).toEqual([ 0 ]); 32 | }); 33 | 34 | test('Multiple array selectors', () => { 35 | const p = parsePath('[0][1245]'); 36 | 37 | expect(p).toEqual([ 0, 1245 ]); 38 | }); 39 | 40 | test('Segment, array selector', () => { 41 | const p = parsePath('abc[0]'); 42 | 43 | expect(p).toEqual([ 'abc', 0 ]); 44 | }); 45 | 46 | test('Segment, array selector, segment', () => { 47 | const p = parsePath('abc[0].def'); 48 | 49 | expect(p).toEqual([ 'abc', 0, 'def' ]); 50 | }); 51 | 52 | test('Array selector after dot', () => { 53 | const p = parsePath('abc.[0].def'); 54 | 55 | expect(p).toEqual([ 'abc', 0, 'def' ]); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /climate/adjustable-target-humidity.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const TargetHumidity = require('./target-humidity'); 5 | const { percentage } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(TargetHumidity) { 8 | 9 | static get capability() { 10 | return 'adjustable-target-humidity'; 11 | } 12 | 13 | static availableAPI(builder) { 14 | builder.action('targetHumidity') 15 | .description('Get or set the target humidity') 16 | .argument('percentage', true, 'Optional target humidity to set') 17 | .returns('percentage', 'The current or set target humidity') 18 | .done(); 19 | 20 | builder.action('setTargetHumidity') 21 | .description('Set the target humidity') 22 | .argument('percentage', false, 'Target humidity to set') 23 | .returns('percentage', 'The target humidity') 24 | .done(); 25 | } 26 | 27 | targetHumidity(humidity) { 28 | if(typeof humidity === 'undefined') { 29 | return super.targetHumidity(); 30 | } 31 | 32 | return this.setTargetHumidity(humidity); 33 | } 34 | 35 | setTargetHumidity(humidity) { 36 | try { 37 | humidity = percentage(humidity, true); 38 | 39 | return Promise.resolve(this.changeTargetHumidity(humidity)) 40 | .then(() => super.targetHumidity()); 41 | } catch(ex) { 42 | return Promise.reject(ex); 43 | } 44 | } 45 | 46 | changeTargetHumidity(humidity) { 47 | throw new Error('changeTargetHumidity not implemented'); 48 | } 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /docs/electrical/wall-outlets.rst: -------------------------------------------------------------------------------- 1 | ``type:wall-outlet`` - Wall outlets 2 | =================================== 3 | 4 | The ``wall-outlet`` type is used to mark things that represent a wall mounted 5 | power outlet. Wall outlets like :doc:`power strips ` can expose their 6 | individual outlets as :doc:`children `. 7 | 8 | .. sourcecode:: js 9 | 10 | if(thing.matches('type:wall-outlet')) { 11 | // This is a wall outlet 12 | 13 | if(thing.matches('cap:children')) { 14 | // Each outlet is available as a child 15 | const firstOutlet = thing.getChild('1'); // depends on the implementation 16 | } 17 | } 18 | 19 | Implementing type 20 | ----------------- 21 | 22 | Without any children: 23 | 24 | .. sourcecode:: js 25 | 26 | const { WallOutlet } = require('abstract-things/electrical'); 27 | 28 | class Example extends WallOutlet.with(...) { 29 | 30 | } 31 | 32 | With outlets as children: 33 | 34 | .. sourcecode:: js 35 | 36 | const { Children } = require('abstract-things'); 37 | const { WallOutlet, PowerOutlet } = require('abstract-things/electrical'); 38 | 39 | class Example extends WallOutlet.with(Children, ...) { 40 | 41 | constructor() { 42 | super(); 43 | 44 | this.addChild(new ExampleOutlet(this, 1)); 45 | this.addChild(new ExampleOutlet(this, 2)); 46 | } 47 | 48 | } 49 | 50 | class ExampleOutlet extends PowerOutlet.with(...) { 51 | 52 | constructor(parent, idx) { 53 | this.parent = parent; 54 | this.id = parent.id + ':' + idx; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /docs/electrical/wall-switches.rst: -------------------------------------------------------------------------------- 1 | ``type:wall-switch`` - Wall switches 2 | ==================================== 3 | 4 | The ``wall-switch`` type is used to mark things that represent a wall mounted 5 | power switch. A wall switch is commonly used to control 6 | :doc:`lights ` or :doc:`power channels `. 7 | 8 | .. sourcecode:: js 9 | 10 | if(thing.matches('type:wall-switch')) { 11 | // This is a wall switch 12 | 13 | if(thing.matches('cap:children')) { 14 | // Lights or power channels available as children 15 | const firstChild= thing.getChild('1'); // depends on the implementation 16 | } 17 | } 18 | 19 | Implementing type 20 | ----------------- 21 | 22 | Without any children: 23 | 24 | .. sourcecode:: js 25 | 26 | const { WallSwitch } = require('abstract-things/electrical'); 27 | 28 | class Example extends WallOutlet.with(...) { 29 | 30 | } 31 | 32 | With power channels as children: 33 | 34 | .. sourcecode:: js 35 | 36 | const { Children } = require('abstract-things'); 37 | const { WallSwitch, PowerChannel } = require('abstract-things/electrical'); 38 | 39 | class Example extends WallSwitch.with(Children, ...) { 40 | 41 | constructor() { 42 | super(); 43 | 44 | this.addChild(new ExampleChild(this, 1)); 45 | this.addChild(new ExampleChild(this, 2)); 46 | } 47 | 48 | } 49 | 50 | class ExampleChild extends PowerChannel.with(...) { 51 | 52 | constructor(parent, idx) { 53 | this.parent = parent; 54 | this.id = parent.id + ':' + idx; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /docs/electrical/strips.rst: -------------------------------------------------------------------------------- 1 | ``type:power-strip`` - Power strips 2 | =================================== 3 | 4 | Things marked with ``power-strip`` represent a power strip with several outlets. 5 | Power strips can expose their individual outlets as children, in which case 6 | they implement the :doc:`children ` capability. 7 | 8 | .. sourcecode:: js 9 | 10 | if(thing.matches('type:power-strip')) { 11 | // This is a power strip 12 | 13 | if(thing.matches('cap:children')) { 14 | // Each outlet in the strip is available as a child 15 | const firstOutlet = thing.getChild('1'); // depends on the implementation 16 | } 17 | } 18 | 19 | Implementing type 20 | ----------------- 21 | 22 | Without any children: 23 | 24 | .. sourcecode:: js 25 | 26 | const { PowerStrip } = require('abstract-things/electrical'); 27 | 28 | class Example extends PowerStrip.with(...) { 29 | 30 | } 31 | 32 | With outlets as children: 33 | 34 | .. sourcecode:: js 35 | 36 | const { Children } = require('abstract-things'); 37 | const { PowerStrip, PowerOutlet } = require('abstract-things/electrical'); 38 | 39 | class Example extends PowerStrip.with(Children, ...) { 40 | 41 | constructor() { 42 | super(); 43 | 44 | this.addChild(new ExampleOutlet(this, 1)); 45 | this.addChild(new ExampleOutlet(this, 2)); 46 | } 47 | 48 | } 49 | 50 | class ExampleOutlet extends PowerOutlet.with(...) { 51 | 52 | constructor(parent, idx) { 53 | this.parent = parent; 54 | this.id = parent.id + ':' + idx; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /docs/lights/fading.rst: -------------------------------------------------------------------------------- 1 | ``cap:fading`` - support for fading changes 2 | =========================================== 3 | 4 | Capability used to mark lights that support fading of changes. When this 5 | capability is present the ``duration`` argument for other methods is available. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('type:light', 'cap:fading')) { 10 | // This light supports fading 11 | const time = await this.maxChangeTime(); 12 | console.log('Maximum fading time in milliseconds:', time.ms); 13 | } 14 | 15 | API 16 | --- 17 | 18 | .. js:attribute:: maxChangeTime 19 | 20 | The maximum :doc:`duration ` of time a change can be. 21 | 22 | Protected methods 23 | ----------------- 24 | 25 | .. js:function:: updateMaxChangeTime(time) 26 | 27 | :param duration time: 28 | The maximum time the light can fade as a 29 | :doc:`duration `. 30 | 31 | Example: 32 | 33 | .. sourcecode:: js 34 | 35 | this.updateMaxChangeTime('20s'); 36 | 37 | Implementing capability 38 | ----------------------- 39 | 40 | Implementing this capability requires that the maximum change time is set 41 | either in the constructor or in the ``initCallback()``. 42 | 43 | Example: 44 | 45 | .. sourcecode:: js 46 | 47 | const { Light, Fading } = require('abstract-things/lights'); 48 | 49 | class Example extends Light.with(Fading) { 50 | 51 | initCallback() { 52 | return super.initCallback() 53 | // Set the maximum change time to 5 seconds 54 | .then(() => this.updateMaxChangeTime('5s')); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /climate/adjustable-target-temperature.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const TargetTemperature = require('./target-humidity'); 5 | const { temperature } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(TargetTemperature) { 8 | 9 | static get capability() { 10 | return 'adjustable-target-temperature'; 11 | } 12 | 13 | static availableAPI(builder) { 14 | builder.action('targetTemperature') 15 | .description('Get or set the target temperature') 16 | .argument('temperature', true, 'Optional target temperature to set') 17 | .returns('temperature', 'The current or set target temperature') 18 | .done(); 19 | 20 | builder.action('setTargetTemperature') 21 | .description('Set the target temperature') 22 | .argument('temperature', false, 'Target temperature to set') 23 | .returns('temperature', 'The target temperature') 24 | .done(); 25 | } 26 | 27 | targetTemperature(temperature) { 28 | if(typeof temperature === 'undefined') { 29 | return super.targetTemperature(); 30 | } 31 | 32 | return this.setTargetTemperature(temperature); 33 | } 34 | 35 | setTargetTemperature(target) { 36 | try { 37 | target = temperature(target, true); 38 | 39 | return Promise.resolve(this.changeTargetTemperature(temperature)) 40 | .then(() => super.targetTemperature()); 41 | } catch(ex) { 42 | return Promise.reject(ex); 43 | } 44 | } 45 | 46 | changeTargetTemperature(humidity) { 47 | throw new Error('changeTargetTemperature not implemented'); 48 | } 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /docs/values/temperature.rst: -------------------------------------------------------------------------------- 1 | Temperature 2 | ============ 3 | 4 | Representation of a temperature. Returns objects created by `amounts 5 | `_. 6 | 7 | .. sourcecode:: js 8 | 9 | const { temperature } = require('abstract-things/values'); 10 | 11 | // With no unit - celsius is the default unit 12 | const v = temperature(20); 13 | console.log(v.value); 14 | console.log(v.F); // convert to fahrenheit 15 | console.log(v.celsius); // convert to celsius 16 | 17 | // With a unit 18 | console.log(temperature(50, 'F')); 19 | 20 | // String (with our without unit) 21 | console.log(temperature('220 K')); 22 | 23 | Units 24 | ----- 25 | 26 | +------------+-----+-----------------------------------------------+ 27 | | Unit | SI | Names | 28 | +============+=====+===============================================+ 29 | | Celsius | No | ``C``, ``c``, ``celsius`` | 30 | +------------+-----+-----------------------------------------------+ 31 | | Kelvin | Yes | ``K``, ``kelvin``, ``kelvins`` | 32 | +------------+-----+-----------------------------------------------+ 33 | | Fahrenheit | No | ``F``, ``f``, ``fahrenheit``, ``fahrenheits`` | 34 | +------------+-----+-----------------------------------------------+ 35 | 36 | String conversion 37 | ----------------- 38 | 39 | Strings are parsed the same as for :doc:`numbers ` with the addition 40 | of units being parsed. The default unit is Celsius. 41 | 42 | Examples: ``20``, ``20 C``, ``100 kelvins``, ``30 F`` 43 | -------------------------------------------------------------------------------- /values/code.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Handler for codes. Can be constructed from strings, numbers and arrays 5 | * and objects. 6 | */ 7 | module.exports = { 8 | create(value) { 9 | if(typeof value === 'object') { 10 | if(Array.isArray(value)) { 11 | /* 12 | * This is an array, assume first item is the code and the 13 | * second is the description. 14 | */ 15 | return new Code(value[0], value[1]); 16 | } else { 17 | /* 18 | * Regular object, id is either in `id` or `code`. 19 | * Description is in either `description` or `message`. 20 | */ 21 | return new Code(value.id || value.code, value.description || value.message); 22 | } 23 | } else if(typeof value === 'string') { 24 | // String code, parse it via the string parser 25 | return parse(value); 26 | } else if(typeof value === 'number') { 27 | // Numbers are treated as a code 28 | return new Code(String(value)); 29 | } 30 | 31 | throw new Error('Can not convert into code'); 32 | }, 33 | 34 | is(value) { 35 | return value instanceof Code; 36 | } 37 | }; 38 | 39 | /** 40 | * Class that represents a code. 41 | */ 42 | const Code = module.exports.Code = class Code { 43 | constructor(id, description) { 44 | this.id = id; 45 | this.description = description; 46 | } 47 | }; 48 | 49 | function parse(value) { 50 | const idx = value.indexOf(':'); 51 | if(idx >= 0) { 52 | return new Code( 53 | value.substring(0, idx).trim(), 54 | value.substring(idx+1).trim() 55 | ); 56 | } else { 57 | return new Code(value.trim()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lights/brightness.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('../common/state'); 5 | const { percentage } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 8 | /** 9 | * Expose the brightness API. 10 | */ 11 | static availableAPI(builder) { 12 | builder.state('brightness') 13 | .type('percentage') 14 | .description('The brightness of this light') 15 | .done(); 16 | 17 | builder.event('brightnessChanged') 18 | .type('percentage') 19 | .description('The brightness of the light has changed') 20 | .done(); 21 | 22 | builder.action('brightness') 23 | .description('Get or set the brightness of this light') 24 | .returns('percentage', 'The brightness of the light') 25 | .getterForState('brightness') 26 | .done(); 27 | } 28 | 29 | /** 30 | * Mark the light as dimmable. 31 | */ 32 | static get capabilities() { 33 | return [ 'brightness' ]; 34 | } 35 | 36 | /** 37 | * Get or change the brightness of this light. 38 | */ 39 | brightness() { 40 | return this.getState('brightness', 0); 41 | } 42 | 43 | /** 44 | * Update the current brightness value, such as from a call to `changeBrightness` or 45 | * if the value has been changed externally. 46 | * 47 | * @param {*} brightness 48 | */ 49 | updateBrightness(brightness) { 50 | brightness = percentage(brightness, { min: 0, max: 100, precision: 1 }); 51 | 52 | if(this.updateState('brightness', brightness)) { 53 | this.emitEvent('brightnessChanged', brightness); 54 | } 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /lights/color-temperature.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isDeepEqual = require('deep-equal'); 4 | 5 | const Thing = require('../thing'); 6 | const { number } = require('../values'); 7 | 8 | const colorTemperatureRange = Symbol('colorTemperatureRange'); 9 | 10 | /** 11 | * Marker for lights that supports setting temperature as the color. 12 | */ 13 | module.exports = Thing.mixin(Parent => class extends Parent { 14 | static get capability() { 15 | return 'color:temperature'; 16 | } 17 | 18 | static availableAPI(builder) { 19 | builder.action('colorTemperatureRange') 20 | .description('Get the temperature range this light supports') 21 | .returns('object') 22 | .done(); 23 | 24 | builder.event('colorTemperatureRangeChanged') 25 | .type('object') 26 | .description('The supported color temperature range has changed') 27 | .done(); 28 | } 29 | 30 | colorTemperatureRange() { 31 | const range = this[colorTemperatureRange]; 32 | 33 | if(! range) { 34 | return Promise.reject(new Error('Temperature range has not been set')); 35 | } 36 | 37 | return Promise.resolve({ 38 | min: range.min, 39 | max: range.max 40 | }); 41 | } 42 | 43 | updateColorTemperatureRange(min, max) { 44 | min = number(min); 45 | max = number(max); 46 | 47 | if(min > max) { 48 | const temp = max; 49 | max = min; 50 | min = temp; 51 | } 52 | 53 | const range = { min, max }; 54 | if(! isDeepEqual(this[colorTemperatureRange], range)) { 55 | this[colorTemperatureRange] = range; 56 | this.emitEvent('colorTemperatureRangeChanged', range); 57 | } 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /common/charging-state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('./state'); 5 | const { boolean } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 8 | 9 | static availableAPI(builder) { 10 | builder.state('charging') 11 | .type('boolean') 12 | .description('If the thing is charging') 13 | .done(); 14 | 15 | builder.event('chargingChanged') 16 | .type('boolean') 17 | .description('Charging state of the thing has changed') 18 | .done(); 19 | 20 | builder.event('chargingStarted') 21 | .description('Charging has started') 22 | .done(); 23 | 24 | builder.event('chargingStoppped') 25 | .description('Charging has stopped') 26 | .done(); 27 | 28 | builder.action('charging') 29 | .description('Get charging state') 30 | .returns('boolean', 'If thing is charging') 31 | .done(); 32 | } 33 | 34 | static get capability() { 35 | return 'charging-state'; 36 | } 37 | 38 | constructor(...args) { 39 | super(...args); 40 | 41 | this.updateState('charging', false); 42 | } 43 | 44 | /** 45 | * Get if thing is charging. 46 | */ 47 | get charging() { 48 | return Promise.resolve(this.getState('charging')); 49 | } 50 | 51 | updateCharging(charging) { 52 | charging = boolean(charging); 53 | if(this.updateState('charging', charging)) { 54 | this.emitEvent('chargingChanged', charging); 55 | 56 | if(charging) { 57 | this.emitEvent('chargingStarted'); 58 | } else { 59 | this.emitEvent('chargingStopped'); 60 | } 61 | } 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /common/error-state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('./state'); 5 | const { code } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 8 | 9 | static availableAPI(builder) { 10 | builder.state('error') 11 | .type('string') 12 | .description('Error code, or null if no error') 13 | .done(); 14 | 15 | builder.event('errorChanged') 16 | .type('code') 17 | .description('If the current error code changes') 18 | .done(); 19 | 20 | builder.event('error') 21 | .type('code') 22 | .description('Thing has encountered an error') 23 | .done(); 24 | 25 | builder.event('errorCleared') 26 | .description('Error of thing has been cleared') 27 | .done(); 28 | 29 | builder.action('error') 30 | .description('Get the current error state') 31 | .returns('code', 'The current error state or null if no error') 32 | .done(); 33 | } 34 | 35 | static get capability() { 36 | return 'error-state'; 37 | } 38 | 39 | constructor(...args) { 40 | super(...args); 41 | 42 | this.updateState('error', null); 43 | } 44 | 45 | /** 46 | * Get if thing has an error. 47 | */ 48 | get error() { 49 | return Promise.resolve(this.getState('error')); 50 | } 51 | 52 | updateError(error) { 53 | if(error) { 54 | error = code(error); 55 | } else { 56 | error = null; 57 | } 58 | 59 | if(this.updateState('error', error)) { 60 | this.emitEvent('errorChanged', error); 61 | 62 | if(error) { 63 | this.emitEvent('error', error); 64 | } else { 65 | this.emitEvent('errorCleared'); 66 | } 67 | } 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /docs/values/mass.rst: -------------------------------------------------------------------------------- 1 | Mass 2 | ============ 3 | 4 | Representation of a mass. Returns objects created by `amounts 5 | `_. 6 | 7 | .. sourcecode:: js 8 | 9 | const { mass } = require('abstract-things/values'); 10 | 11 | // With no unit - grams is the default unit 12 | const v = mass(200); 13 | console.log(v.value); 14 | console.log(v.kg); // convert to kilograms 15 | console.log(v.lb); // convert to pounds 16 | 17 | // With a unit 18 | console.log(mass(5, 'lbs')); 19 | 20 | // String (with our without unit) 21 | console.log(mass('20 oz')); 22 | 23 | Units 24 | ----- 25 | 26 | +-------+-----+-----------------------------------------------------+ 27 | | Unit | SI | Names | 28 | +=======+=====+=====================================================+ 29 | | Gram | Yes | ``g``, ``gram``, ``grams``, ``gramme``, ``grammes`` | 30 | +-------+-----+-----------------------------------------------------+ 31 | | Pound | No | ``lb``, ``lbs``, ``pound``, ``pounds``, ``#`` | 32 | +-------+-----+-----------------------------------------------------+ 33 | | Ounce | No | ``oz``, ``ounce``, ``ounces`` | 34 | +-------+-----+-----------------------------------------------------+ 35 | | Stone | No | ``st``, ``stone``, ``stones`` | 36 | +-------+-----+-----------------------------------------------------+ 37 | 38 | String conversion 39 | ----------------- 40 | 41 | Strings are parsed the same as for :doc:`numbers ` with the addition 42 | of units being parsed. The default unit is grams. 43 | 44 | Examples: ``200``, ``200 g``, ``5 kg``, ``20 lbs`` 45 | -------------------------------------------------------------------------------- /docs/common/autonomous-charging.rst: -------------------------------------------------------------------------------- 1 | ``cap:autonomous-charging`` - request charging 2 | =============================================== 3 | 4 | The ``autonomous-charging`` capability is used for things that have a battery 5 | and can charge it on request. This is commonly things such as vacuum robots 6 | that can head to a charging station to recharge. 7 | 8 | .. sourcecode:: js 9 | 10 | if(thing.matches('cap:autonomous-charging')) { 11 | thing.charge() 12 | .then(() => console.log('Charging has been requested')) 13 | .catch(...); 14 | } 15 | 16 | API 17 | --- 18 | 19 | .. js:function:: charge() 20 | 21 | Request that the thing charges. 22 | 23 | :returns: Promise that resolves to ``null`` 24 | 25 | Example: 26 | 27 | .. sourcecode:: js 28 | 29 | thing.charge() 30 | .then(...) 31 | .catch(...); 32 | 33 | await thing.charge(); 34 | 35 | 36 | Protected methods 37 | ----------------- 38 | 39 | .. js:function:: activateCharging() 40 | 41 | Activate charging of the thing. Called by ``charge()``. 42 | 43 | :returns: Promise that resolves when activation is performed. 44 | 45 | Example: 46 | 47 | .. sourcecode:: js 48 | 49 | activateCharging() { 50 | return activateChargingSomehow(); 51 | } 52 | 53 | Implementing capability 54 | ----------------------- 55 | 56 | When implementing this capability the implementor needs to implement the 57 | method ``activateCharging``. 58 | 59 | .. sourcecode:: js 60 | 61 | const { Thing, AutonomousCharging } = require('abstract-things'); 62 | 63 | class Example extends Thing.with(AutonomousCharging) { 64 | 65 | activateCharging() { 66 | // Create a promise that resolves when charging has been activated 67 | return activateChargingSomehow(); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /sensors/smoke-detection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { boolean, duration } = require('../values'); 6 | 7 | const idleTimer = Symbol('autoIdle'); 8 | 9 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 10 | static get capability() { 11 | return 'smoke-detection'; 12 | } 13 | 14 | static availableAPI(builder) { 15 | builder.event('smokeDetectedChanged') 16 | .type('boolean') 17 | .description('Change in smoke detected') 18 | .done(); 19 | 20 | builder.event('smoke') 21 | .description('Smoke has been detected') 22 | .done(); 23 | 24 | builder.event('smokeCleared') 25 | .description('Smoke is no longer detected') 26 | .done(); 27 | 28 | builder.action('smokeDetected') 29 | .description('Get if smoke is currently detected') 30 | .returns('boolean', 'Current smoke detected status') 31 | .done(); 32 | } 33 | 34 | get sensorTypes() { 35 | return [ ...super.sensorTypes, 'smokeDetected' ]; 36 | } 37 | 38 | smokeDetected() { 39 | return this.value('smokeDetected'); 40 | } 41 | 42 | updateSmokeDetected(smoke, autoIdleTimeout=null) { 43 | smoke = boolean(smoke); 44 | if(this.updateValue('smokeDetected', smoke)) { 45 | if(smoke) { 46 | this.emitEvent('smoke'); 47 | } else { 48 | this.emitEvent('smokeCleared'); 49 | } 50 | } 51 | 52 | // Always clear the idle timer 53 | clearTimeout(this[idleTimer]); 54 | 55 | if(smoke && autoIdleTimeout) { 56 | /* 57 | * When smoke has been detected and automatic idle is requested 58 | * set a timer. 59 | */ 60 | const ms = duration(autoIdleTimeout).ms; 61 | this[idleTimer] = setTimeout(() => this.updateSmokeDetected(false), ms); 62 | } 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /sensors/water-detection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { boolean, duration } = require('../values'); 6 | 7 | const idleTimer = Symbol('autoIdle'); 8 | 9 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 10 | static get capability() { 11 | return 'water-detection'; 12 | } 13 | 14 | static availableAPI(builder) { 15 | builder.event('waterDetectedChanged') 16 | .type('boolean') 17 | .description('Change in water detected') 18 | .done(); 19 | 20 | builder.event('water') 21 | .description('Water has been detected') 22 | .done(); 23 | 24 | builder.event('waterCleared') 25 | .description('Water is no longer detected') 26 | .done(); 27 | 28 | builder.action('waterDetected') 29 | .description('Get if water is currently detected') 30 | .returns('boolean', 'Current water detected status') 31 | .done(); 32 | } 33 | 34 | get sensorTypes() { 35 | return [ ...super.sensorTypes, 'waterDetected' ]; 36 | } 37 | 38 | waterDetected() { 39 | return this.value('waterDetected'); 40 | } 41 | 42 | updateWaterDetected(water, autoIdleTimeout=null) { 43 | water = boolean(water); 44 | if(this.updateValue('waterDetected', water)) { 45 | if(water) { 46 | this.emitEvent('water'); 47 | } else { 48 | this.emitEvent('waterCleared'); 49 | } 50 | } 51 | 52 | // Always clear the idle timer 53 | clearTimeout(this[idleTimer]); 54 | 55 | if(water && autoIdleTimeout) { 56 | /* 57 | * When water has been detected and automatic idle is requested 58 | * set a timer. 59 | */ 60 | const ms = duration(autoIdleTimeout).ms; 61 | this[idleTimer] = setTimeout(() => this.updateWaterDetected(false), ms); 62 | } 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /config/parsePath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function getNonQuoted(path, start, end) { 4 | if(path.charAt(start) === '"') start += 1; 5 | if(path.charAt(end - 1) === '"') end -= 1; 6 | 7 | return path.substring(start, end); 8 | } 9 | 10 | /** 11 | * Parse a dot-separated path into its parts. Supports quoting individual 12 | * key and array indexes. 13 | * 14 | * @param {string} path 15 | * The path to parse 16 | */ 17 | module.exports = function parsePath(path) { 18 | if(typeof path !== 'string') { 19 | throw new Error('Path needs to be specified as a string'); 20 | } 21 | 22 | let parts = []; 23 | 24 | let quoted = false; 25 | let lastIndex = 0; 26 | let targetChar = '.'; 27 | 28 | function push(i) { 29 | // Make sure that only non-empty parts are added 30 | if(lastIndex === i) { 31 | // Also make sure that we indicate that the trigger char has been consumed 32 | lastIndex++; 33 | return; 34 | } 35 | 36 | // Get the non-quoted path 37 | const str = getNonQuoted(path, lastIndex, i); 38 | 39 | // Update the last index to be after this push 40 | lastIndex = i + 1; 41 | 42 | if(targetChar === ']') { 43 | parts.push(parseInt(str)); 44 | } else { 45 | parts.push(str); 46 | } 47 | } 48 | 49 | for(let i=0; i class extends Parent.with(State) { 11 | /** 12 | * Define the API of appliances that can manage their power. 13 | */ 14 | static availableAPI(builder) { 15 | builder.state('power') 16 | .type('boolean') 17 | .description('The power state of this appliance') 18 | .done(); 19 | 20 | builder.event('powerChanged') 21 | .type('boolean') 22 | .description('The power state of the appliance has changed') 23 | .done(); 24 | 25 | builder.action('power') 26 | .description('Get the power state of this appliance') 27 | .returns('boolean', 'The power state of the appliance') 28 | .getterForState('power') 29 | .done(); 30 | } 31 | 32 | /** 33 | * Get that this provides the power capability. 34 | */ 35 | static get capability() { 36 | return 'power'; 37 | } 38 | 39 | constructor(...args) { 40 | super(...args); 41 | 42 | this.updateState('power', false); 43 | } 44 | 45 | /** 46 | * Get the power state of this appliance. 47 | * 48 | * @returns 49 | * boolean indicating the power level 50 | */ 51 | power() { 52 | return Promise.resolve(this.getState('power')); 53 | } 54 | 55 | /** 56 | * Update the state of power to the appliance. Implementations should call 57 | * this whenever the power changes, either from an external event or when 58 | * changePower is called. 59 | * 60 | * @param {boolean} power 61 | */ 62 | updatePower(power) { 63 | if(this.updateState('power', power)) { 64 | this.emitEvent('powerChanged', power); 65 | } 66 | } 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /storage/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const mkdirp = require('mkdirp'); 5 | 6 | const AppDirectory = require('appdirectory'); 7 | const Storage = require('dwaal'); 8 | 9 | const values = require('../values'); 10 | 11 | let storage; 12 | let parent; 13 | 14 | function resolveDataDir() { 15 | if(! parent) { 16 | if(process.env.THING_STORAGE) { 17 | parent = process.env.THING_STORAGE; 18 | } else { 19 | const dirs = new AppDirectory('abstract-things'); 20 | parent = dirs.userData(); 21 | } 22 | 23 | mkdirp.sync(parent); 24 | } 25 | 26 | return parent; 27 | } 28 | 29 | function resolveStorage() { 30 | if(storage) return storage; 31 | 32 | let parent = resolveDataDir(); 33 | const p = path.join(parent, 'storage'); 34 | mkdirp.sync(p); 35 | 36 | storage = new Storage({ 37 | path: p 38 | }); 39 | return storage; 40 | } 41 | 42 | class SubStorage { 43 | constructor(storage, sub) { 44 | this._storage = storage; 45 | this._path = sub; 46 | } 47 | 48 | get(key, type='mixed') { 49 | return this._storage.get(this._path + '/' + key) 50 | .then(json => values.fromJSON(type, json)); 51 | } 52 | 53 | set(key, value, type='mixed') { 54 | return this._storage.set(this._path + '/' + key, values.toJSON(type, value)); 55 | } 56 | 57 | sub(key) { 58 | return new SubStorage(this._storage, this._path + '/' + key); 59 | } 60 | 61 | inspect() { 62 | return 'Storage[' + this._path + ']'; 63 | } 64 | 65 | toString() { 66 | return this.inspect(); 67 | } 68 | } 69 | 70 | module.exports = { 71 | get dataDir() { 72 | return resolveDataDir(); 73 | }, 74 | 75 | global() { 76 | return new SubStorage(resolveStorage(), 'global'); 77 | }, 78 | 79 | instance(id) { 80 | return new SubStorage(resolveStorage(), 'instance/' + id); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /docs/climate/spot-cleaning.rst: -------------------------------------------------------------------------------- 1 | ``cap:spot-cleaning`` - support for spot cleaning 2 | ================================================= 3 | 4 | This capability is commonly used together with 5 | :doc:`autonomous cleaning ` to also support cleaning a 6 | specific spot. 7 | 8 | .. sourcecode:: js 9 | 10 | if(thing.matches('cap:spot-cleaning')) { 11 | // Do some cleaning around this spot 12 | await thing.cleanSpot(); 13 | } 14 | 15 | API 16 | --- 17 | 18 | .. js:function:: cleanSpot() 19 | 20 | Activate cleaning for the current spot. 21 | 22 | :returns: 23 | Promise that resolves to ``null``. 24 | 25 | Example: 26 | 27 | .. sourcecode:: js 28 | 29 | // Using async/await 30 | await thing.cleanSpot(); 31 | 32 | // Using promise then/catch 33 | thing.cleanSpot() 34 | .then(...) 35 | .catch(...); 36 | 37 | Protected methods 38 | ----------------- 39 | 40 | .. js:function:: activateCleanSpot() 41 | 42 | **Abstract.** Activate spot cleaning for this thing. Should call 43 | ``updateCleaning`` when spot cleaning is activated. 44 | 45 | :returns: 46 | Promise if asynchronous. 47 | 48 | Example: 49 | 50 | .. sourcecode:: js 51 | 52 | activateCleanSpot() { 53 | return activateViaPromise(...); 54 | } 55 | 56 | Implementing capability 57 | ----------------------- 58 | 59 | When implementing this capability refer to the requirements of 60 | :doc:`cleaning-state `. In addition to that the method 61 | ``activateCleanSpot`` needs to be implemented. 62 | 63 | Example: 64 | 65 | .. sourcecode:: js 66 | 67 | const { Thing } = require('abstract-things'); 68 | const { SpotCleaning } = require('abstract-things/climate'); 69 | 70 | class Example extends Thing.with(SpotCleaning) { 71 | 72 | activateCleanSpot() { 73 | return ...; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /common/switchable-audio-feedback.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const AudioFeedback = require('./audio-feedback'); 5 | const { boolean } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(AudioFeedback) { 8 | 9 | static get capability() { 10 | return 'switchable-audio-feedback'; 11 | } 12 | 13 | static availableAPI(builder) { 14 | builder.action('audioFeedback') 15 | .description('Get if the thing emits audio feedback') 16 | .argument('boolean', true, 'Optional boolean to switch audio feedback on or off') 17 | .returns('boolean', 'If audio feedback is enabled') 18 | .done(); 19 | 20 | builder.action('setAudioFeedback') 21 | .description('Set if thing emits audio feedback') 22 | .argument('boolean', false, 'Boolean indicating if audio feedback is enabled') 23 | .returns('boolean', 'If audio feedback is enabled') 24 | .done(); 25 | 26 | builder.action('toggleAudioFeedback') 27 | .description('Toggle the current audio feedback state') 28 | .returns('boolean', 'If audio feedback is enabled') 29 | .done(); 30 | } 31 | 32 | audioFeedback(enabled) { 33 | if(typeof enabled === 'undefined') { 34 | return super.audioFeedback(); 35 | } 36 | 37 | return this.setAudioFeedback(enabled); 38 | } 39 | 40 | setAudioFeedback(enabled) { 41 | try { 42 | enabled = boolean(enabled, true); 43 | 44 | return Promise.resolve(this.changeAudioFeedback(enabled)) 45 | .then(() => this.audioFeedback()); 46 | } catch(ex) { 47 | return Promise.reject(ex); 48 | } 49 | } 50 | 51 | toggleAudioFeedback() { 52 | return this.audioFeedback() 53 | .then(on => this.setAudioFeedback(! on)); 54 | } 55 | 56 | changeAudioFeedback(speed) { 57 | throw new Error('changeAudioFeedback not implemented'); 58 | } 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /docs/values/length.rst: -------------------------------------------------------------------------------- 1 | Length 2 | ============ 3 | 4 | Representation of a length. Returns objects created by `amounts 5 | `_. 6 | 7 | .. sourcecode:: js 8 | 9 | const { length } = require('abstract-things/values'); 10 | 11 | // With no unit - metre is the default unit 12 | const v = length(2); 13 | console.log(v.value); 14 | console.log(v.cm); // convert to centimetres 15 | console.log(v.ft); // convert to feet 16 | 17 | // With a unit 18 | console.log(length(5, 'in')); 19 | 20 | // String (with our without unit) 21 | console.log(length('200 cm')); 22 | 23 | Units 24 | ----- 25 | 26 | +-------+-----+-----------------------------------------------------+ 27 | | Unit | SI | Names | 28 | +=======+=====+=====================================================+ 29 | | Metre | Yes | ``m``, ``meter``, ``meters``, ``metre``, ``metres`` | 30 | +-------+-----+-----------------------------------------------------+ 31 | | Inch | No | ``in``, ``inch``, ``inches`` | 32 | +-------+-----+-----------------------------------------------------+ 33 | | Feet | No | ``ft``, ``foot``, ``feet`` | 34 | +-------+-----+-----------------------------------------------------+ 35 | | Yard | No | ``yd``, ``yard``, ``yards`` | 36 | +-------+-----+-----------------------------------------------------+ 37 | | Mile | No | ``mi``, ``mile``, ``miles`` | 38 | +-------+-----+-----------------------------------------------------+ 39 | 40 | String conversion 41 | ----------------- 42 | 43 | Strings are parsed the same as for :doc:`numbers ` with the addition 44 | of units being parsed. The default unit is metre. 45 | 46 | Examples: ``200``, ``200 cm``, ``5 ft``, ``20 inches`` 47 | -------------------------------------------------------------------------------- /docs/building-things/events.rst: -------------------------------------------------------------------------------- 1 | Handling events 2 | =============== 3 | 4 | Events are emitted quite often by capabilities. In most cases the capability 5 | will automatically emit events as needed, but when implementing custom 6 | capabilities simply call ``emitEvent``. 7 | 8 | API 9 | --- 10 | 11 | .. js:function:: emitEvent(name[, payload[, options]]) 12 | 13 | Emit an event with the given name. By default only a single event will be 14 | emitted during a tick. So doing ``emitEvent('test')`` twice in a row will 15 | only emit a single event, see the options to change the behavior. 16 | 17 | :param string name: The name of the event. 18 | :param payload: 19 | Optional payload of the event. Can be any object that can be converted to 20 | JSON. If omitted will be equal to ``null``. 21 | 22 | :param options: 23 | Optional object containing options for event emittal. The only option 24 | available is ``multiple`` which can be set to allow multiple events 25 | to be emitted during the same tick. 26 | 27 | Example: 28 | 29 | .. sourcecode:: js 30 | 31 | this.emitEvent('test'); 32 | this.emitEvent('rotation', angle(102)); 33 | this.emitEvent('action', { name: 'test' }); 34 | this.emitEvent('test', null, { multiple: true }); 35 | 36 | For information about how to listen for events see :doc:`../using-things`. 37 | 38 | Common patterns 39 | --------------- 40 | 41 | It is recommended to emit as few events as possible, such as only emitting an 42 | event when something changes. 43 | 44 | As many capabilities extend :doc:`state <../common/state>` a common pattern for 45 | event emittal looks something like this: 46 | 47 | .. sourcecode:: js 48 | 49 | updatePower(newPowerValue) { 50 | if(this.updateState('power', newPowerValue)) { 51 | // Emit event if new value was different from the previous value 52 | this.emitEvent('power', newPowerValue); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/lights/color-temperature.rst: -------------------------------------------------------------------------------- 1 | ``cap:color:temperature`` - light supports temperature 2 | ====================================================== 3 | 4 | Capability used to mark lights that support setting color temperature natively. 5 | 6 | .. sourcecode:: js 7 | 8 | if(thing.matches('cap:color:temperature')) { 9 | console.log('Range is', thing.colorTemperatureRange); 10 | } 11 | 12 | API 13 | --- 14 | 15 | .. js:attribute:: colorTemperatureRange 16 | 17 | Get the range of temperatures this color supports. 18 | 19 | :returns: Object with ``min`` and ``max`` in Kelvin. 20 | 21 | Example: 22 | 23 | .. sourcecode:: js 24 | 25 | console.log('Min temperature:', thing.colorTemperatureRange.min); 26 | console.log('Max temperature:', thing.colorTemperatureRange.max); 27 | 28 | Events 29 | ------ 30 | 31 | .. js:data:: colorTemperatureRangeChanged 32 | 33 | The range of temperature the light supports has changed. 34 | 35 | .. sourcecode:: js 36 | 37 | thing.on('colorTemperatureRangeChanged', range => console.log('Range is now', range)); 38 | 39 | Protected methods 40 | ----------------- 41 | 42 | .. js:function:: updateColorTemperatureRange(min, max) 43 | 44 | Set the color temperature range the light support. 45 | 46 | :param number min: The minimum color temperature in Kelvin. 47 | :param number max: The maximum color temperature in Kelvin. 48 | 49 | Implementing capability 50 | ----------------------- 51 | 52 | Implementors of this capability should call ``setColorTemperatureRange`` 53 | either in the constructor or ``initCallback``. 54 | 55 | Example: 56 | 57 | .. sourcecode:: js 58 | 59 | const { Light, ColorTemperature } = require('abstract-things/lights'); 60 | 61 | class Example extends Light.with(ColorTemperature) { 62 | 63 | constructor() { 64 | super(); 65 | 66 | this.updateColorTemperatureRange(2000, 5000); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /docs/values/duration.rst: -------------------------------------------------------------------------------- 1 | Duration 2 | ======== 3 | 4 | Representation of a duration of time. Returns objects created by `amounts 5 | `_. 6 | 7 | .. sourcecode:: js 8 | 9 | const { duration } = require('abstract-things/values'); 10 | 11 | // With no unit - milliseconds are the default unit 12 | const v = duration(2000); 13 | console.log(v.value); 14 | console.log(v.seconds); // number converted to seconds 15 | 16 | // With a unit 17 | console.log(duration(2, 's')); 18 | 19 | // String (with our without unit) 20 | console.log(duration('2 s')); 21 | console.log(duration('1m 10s')); 22 | console.log(duration('2 hours 5 m')); 23 | 24 | Units 25 | ----- 26 | 27 | +--------------+----+-------------------------------------------+ 28 | | Unit | SI | Names | 29 | +==============+====+===========================================+ 30 | | Milliseconds | No | ``ms``, ``millisecond``, ``milliseconds`` | 31 | +--------------+----+-------------------------------------------+ 32 | | Seconds | No | ``s``, ``second``, ``seconds`` | 33 | +--------------+----+-------------------------------------------+ 34 | | Minutes | No | ``m``, ``minute``, ``minutes`` | 35 | +--------------+----+-------------------------------------------+ 36 | | Hours | No | ``h``, ``hour``, ``hours`` | 37 | +--------------+----+-------------------------------------------+ 38 | | Days | No | ``d``, ``day``, ``days`` | 39 | +--------------+----+-------------------------------------------+ 40 | 41 | String conversion 42 | ----------------- 43 | 44 | Values in the string are parsed the same as for :doc:`numbers ` with 45 | multiple values with units supported. 46 | 47 | Examples: ``2000``, ``2000 ms``, ``5 s``, ``5 seconds``, ``1 hour, 10 minutes``, 48 | ``1d 5m`` 49 | -------------------------------------------------------------------------------- /docs/sensors/pm10.rst: -------------------------------------------------------------------------------- 1 | ``cap:pm10`` - read PM10 density (air quality) 2 | ============================================== 3 | 4 | This capability is used to mark sensors that monitor particulate matter (PM) 5 | between 2.5 and 10 micrometers (μm). 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('cap:pm10')) { 10 | console.log('PM10:', await thing.pm10()); 11 | } 12 | 13 | API 14 | --- 15 | 16 | .. js:function:: pm10() 17 | 18 | Get the current PM10 as micrograms per cubic meter (μg/m³). 19 | 20 | :returns: 21 | The current value as micrograms per cubic meter (μg/m³). Value is a 22 | :doc:`number `. 23 | 24 | Example: 25 | 26 | .. sourcecode:: js 27 | 28 | console.log('PM10:', await thing.pm10()); 29 | 30 | Events 31 | ------ 32 | 33 | .. js:data:: pm10Changed 34 | 35 | The PM10 has changed. Payload is a :doc:`number ` with 36 | the new PM10 as micrograms per cubic meter (μg/m³). 37 | 38 | Example: 39 | 40 | .. sourcecode:: js 41 | 42 | thing.on('pm10Changed', v => console.log('Changed to:', v)); 43 | 44 | Protected methods 45 | ----------------- 46 | 47 | .. js:function:: updatePM10(value) 48 | 49 | Update the current PM10 as micrograms per cubic meter (μg/m³). Should be 50 | called whenever a change is detected. 51 | 52 | :param value: 53 | The new PM10 value. Will be converted to a 54 | :doc:`number `. 55 | 56 | Example: 57 | 58 | .. sourcecode:: js 59 | 60 | this.updatePM10(5); 61 | 62 | Implementing capability 63 | ----------------------- 64 | 65 | Implementors of this capability should call ``updatePM10`` whenever the 66 | detected PM10 changes. 67 | 68 | .. sourcecode:: js 69 | 70 | const { Sensor, PM10 } = require('abstract-things/sensors'); 71 | 72 | class Example extends Sensor.with(PM10) { 73 | 74 | constructor() { 75 | super(); 76 | 77 | this.updatePM10(5); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /common/restorable-state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('./state'); 5 | 6 | /** 7 | * Restorable state capability. Restorable state is used for those things 8 | */ 9 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 10 | static availableAPI(builder) { 11 | builder.action('restorableState') 12 | .description('Get the properties that will be captured') 13 | .returns('array') 14 | .done(); 15 | 16 | builder.action('captureState') 17 | .description('Capture parts of the current state that can be restored') 18 | .returns('object') 19 | .done(); 20 | 21 | builder.action('setState') 22 | .description('Restore previously captured state') 23 | .argument('object', false, 'The state to restore') 24 | .returns('object') 25 | .done(); 26 | } 27 | 28 | /** 29 | * Get that this provides the state capability. 30 | */ 31 | static get capability() { 32 | return 'restorable-state'; 33 | } 34 | 35 | constructor(...args) { 36 | super(...args); 37 | } 38 | 39 | /** 40 | * Get the properties that can be captured and restored. 41 | */ 42 | get restorableState() { 43 | return []; 44 | } 45 | 46 | /** 47 | * Capture the current state. 48 | */ 49 | captureState() { 50 | const result = {}; 51 | for(const property of this.restorableState) { 52 | result[property] = this.state[property]; 53 | } 54 | return Promise.resolve(result); 55 | } 56 | 57 | /** 58 | * Restore a previously captured state. 59 | * 60 | * @param {object} state 61 | */ 62 | setState(state) { 63 | try { 64 | return Promise.resolve(this.changeState(state)) 65 | .then(() => this.state); 66 | } catch(ex) { 67 | return Promise.reject(ex); 68 | } 69 | } 70 | 71 | /** 72 | * Actually change to the given state. 73 | * 74 | * @param {object} state 75 | */ 76 | changeState(state) { 77 | return Promise.resolve(); 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /docs/sensors/voltage.rst: -------------------------------------------------------------------------------- 1 | ``cap:voltage`` - read voltage of something 2 | =========================================== 3 | 4 | This capability is used to mark sensors that report the 5 | :doc:`voltage ` of something. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('cap:voltage')) { 10 | const voltage = await thing.voltage(); 11 | console.log('Voltage:', voltage.volts); 12 | } 13 | 14 | API 15 | --- 16 | 17 | .. js:attribute:: voltage 18 | 19 | Get the current :doc:`voltage `. 20 | 21 | :returns: 22 | Promise that resolves to the current :doc:`voltage `. 23 | 24 | Example: 25 | 26 | .. sourcecode:: js 27 | 28 | const voltage = await thing.voltage(); 29 | console.log('Voltage:', voltage.volts); 30 | 31 | Events 32 | ------ 33 | 34 | .. js:data:: voltageChanged 35 | 36 | The voltage has changed. Payload is the new voltage as a 37 | :doc:`voltage `. 38 | 39 | Example: 40 | 41 | .. sourcecode:: js 42 | 43 | thing.on('voltageChanged', v => console.log('Changed to:', v)); 44 | 45 | Protected methods 46 | ----------------- 47 | 48 | .. js:function:: updateVoltage(value) 49 | 50 | Update the :doc:`voltage `. Should be called whenever a 51 | change is detected. 52 | 53 | :param value: 54 | The new voltage. Will be converted to a :doc:`voltage ` 55 | with the default unit being volts. 56 | 57 | Example: 58 | 59 | .. sourcecode:: js 60 | 61 | this.updateVoltage(12); 62 | 63 | Implementing capability 64 | ----------------------- 65 | 66 | Implementors of this capability should call ``updateRelativeHumidity`` whenever 67 | the relative humidity changes. 68 | 69 | .. sourcecode:: js 70 | 71 | const { Sensor, Voltage } = require('abstract-things/sensors'); 72 | 73 | class Example extends Sensor.with(Voltage) { 74 | 75 | constructor() { 76 | super(); 77 | 78 | this.updateVoltage(230); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /media/muteable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Muted = require('./muted'); 5 | const { boolean } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Muted) { 8 | static get capability() { 9 | return 'audio-muteable'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.action('muted') 14 | .description('Get or set if currently muted') 15 | .argument('boolean', true, 'Optional mute status to set') 16 | .returns('boolean', 'Current mute status') 17 | .done(); 18 | 19 | builder.action('setMuted') 20 | .description('Set if audio is muted') 21 | .argument('boolean', false, 'If the audio is muted') 22 | .returns('boolean', 'Current mute status') 23 | .done(); 24 | 25 | builder.action('mute') 26 | .description('Mute the audio') 27 | .returns('boolean', 'The new muted state') 28 | .done(); 29 | 30 | builder.action('unmute') 31 | .description('Unmute the audio') 32 | .returns('boolean', 'The new muted state') 33 | .done(); 34 | 35 | builder.action('toggleMuted') 36 | .description('Toggle the current muted state') 37 | .done(); 38 | } 39 | 40 | muted(muted) { 41 | if(typeof muted === 'undefined') { 42 | return super.muted(); 43 | } 44 | 45 | return this.setMuted(muted); 46 | } 47 | 48 | setMuted(muted) { 49 | try { 50 | muted = boolean(muted, true, 'Boolean for new muted state is required'); 51 | 52 | return Promise.resolve(this.changeMuted(muted)) 53 | .then(() => this.state.muted); 54 | } catch(ex) { 55 | return Promise.reject(ex); 56 | } 57 | } 58 | 59 | mute() { 60 | return this.setMuted(true); 61 | } 62 | 63 | unmute() { 64 | return this.setMuted(false); 65 | } 66 | 67 | toggleMuted() { 68 | return this.muted() 69 | .then(muted => this.setMuted(! muted)); 70 | } 71 | 72 | changeMuted(muted) { 73 | throw new Error('changeMuted has not been implemented'); 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /sensors/motion-detection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { boolean, duration } = require('../values'); 6 | const idleTimer = Symbol('autoIdle'); 7 | 8 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 9 | static get capability() { 10 | return 'motion-detection'; 11 | } 12 | 13 | static availableAPI(builder) { 14 | builder.event('motionDetectedChanged') 15 | .type('boolean') 16 | .description('Change in detected motion') 17 | .done(); 18 | 19 | builder.event('movement') 20 | .description('Movement has been detected') 21 | .done(); 22 | 23 | builder.event('inactivity') 24 | .description('Inactivity has been detected, no motion detected') 25 | .done(); 26 | 27 | builder.action('motionDetected') 28 | .description('Get if motion is currently detected') 29 | .getterForState('motion') 30 | .returns('boolean', 'Current motion detected status') 31 | .done(); 32 | } 33 | 34 | get sensorTypes() { 35 | return [ ...super.sensorTypes, 'motionDetected' ]; 36 | } 37 | 38 | motionDetected() { 39 | return this.value('motionDetected'); 40 | } 41 | 42 | updateMotionDetected(motion, autoIdleTimeout=null) { 43 | motion = boolean(motion); 44 | if(this.updateValue('motionDetected', motion)) { 45 | if(motion) { 46 | // Emit the movement event if movement is detected 47 | this.emitEvent('movement'); 48 | } else { 49 | // Emit the inactivity event if no movement is being detected 50 | this.emitEvent('inactivity'); 51 | } 52 | } 53 | 54 | // Always clear the idle timer 55 | clearTimeout(this[idleTimer]); 56 | 57 | if(motion && autoIdleTimeout) { 58 | /* 59 | * When motion has been detected and automatic idle is requested 60 | * set a timer. 61 | */ 62 | const ms = duration(autoIdleTimeout).ms; 63 | this[idleTimer] = setTimeout(() => this.updateMotion(false), ms); 64 | } 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /docs/lights/brightness.rst: -------------------------------------------------------------------------------- 1 | ``cap:brightness`` - read brightness 2 | ==================================== 3 | 4 | Capability used when a light supports reading the brightness. Usually this is 5 | combined with :doc:`dimmable ` for lights that can actually change 6 | their brightness. 7 | 8 | .. sourcecode:: js 9 | 10 | if(thing.matches('cap:brightness')) { 11 | console.log(await thing.brightness()); 12 | } 13 | 14 | API 15 | --- 16 | 17 | .. js:function:: brightness() 18 | 19 | Get the brightness of the light. 20 | 21 | :returns: 22 | Promise that resolves to a :doc:`percentage ` 23 | between 0 and 100, representing the brightness. 24 | 25 | Example: 26 | 27 | .. sourcecode:: js 28 | 29 | console.log(await thing.brightness()); 30 | 31 | Events 32 | ------ 33 | 34 | .. js:data:: brightnessChanged 35 | 36 | Brightness has changed. The payload of the event will be the brightness as 37 | a :doc:`percentage `. 38 | 39 | .. sourcecode:: js 40 | 41 | thing.on('brightnessChanged', bri => console.log('Brightness is now', bri)); 42 | 43 | Protected functions 44 | ------------------------ 45 | 46 | .. js:function:: updateBrightness(brightness) 47 | 48 | Update the current brightness. Should be called whenever the brightness 49 | has been detected to have changed. 50 | 51 | :param number brightness: The new brightness as a :doc:`percentage `. 52 | 53 | Implementing capability 54 | ----------------------- 55 | 56 | This capability has no functions that need to be implemented. Things using 57 | the capability should call ``updateBrightness`` whenever the brightness 58 | changes. 59 | 60 | .. sourcecode:: js 61 | 62 | const { Light, Brightness } = require('abstract-things/lights'); 63 | 64 | class Example extends Light.with(Brightness) { 65 | 66 | initCallback() { 67 | return super.initCallback() 68 | .then(() => this.updateBrightness(initialBrightnessHere)); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /docs/values/illuminance.rst: -------------------------------------------------------------------------------- 1 | Illuminance 2 | ============ 3 | 4 | Representation of an illuminance level. Returns objects created by `amounts 5 | `_. 6 | 7 | .. sourcecode:: js 8 | 9 | const { illuminance } = require('abstract-things/values'); 10 | 11 | // With no unit - lux are the default unit 12 | const v = illuminance(200); 13 | console.log(v.value); 14 | console.log(v.fc); // convert to foot-candle 15 | console.log(v.lux); // convert to lux 16 | 17 | // With a unit 18 | console.log(illuminance(5, 'lx')); 19 | 20 | // String (with our without unit) 21 | console.log(illuminance('200 lx')); 22 | 23 | Units 24 | ----- 25 | 26 | +-----------------------+----------+-----------------------------------+ 27 | | Unit | SI | Names | 28 | +=======================+==========+===================================+ 29 | | Lux | Yes | ``lx``, ``lux`` | 30 | +-----------------------+----------+-----------------------------------+ 31 | | Phot | No | ``ph``, ``phot`` | 32 | +-----------------------+----------+-----------------------------------+ 33 | | Nox | No | ``nx``, ``nox`` | 34 | +-----------------------+----------+-----------------------------------+ 35 | | Foot-candle | No | ``fc``, ``lm/ft²``, ``ft-c``, | 36 | | | | ``foot-candle``, | 37 | | | | ``foot-candles``, | 38 | | | | ``foot candle``, ``foot candles`` | 39 | +-----------------------+----------+-----------------------------------+ 40 | 41 | String conversion 42 | ----------------- 43 | 44 | Strings are parsed the same as for :doc:`numbers ` with the addition 45 | of units being parsed. The default unit is lux. 46 | 47 | Examples: ``200``, ``200 lx``, ``5 fc``, ``5 phot`` 48 | -------------------------------------------------------------------------------- /sensors/carbon-dioxide-detection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { boolean, duration } = require('../values'); 6 | 7 | const idleTimer = Symbol('autoIdle'); 8 | 9 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 10 | static get capability() { 11 | return 'carbon-dioxide-detection'; 12 | } 13 | 14 | static availableAPI(builder) { 15 | builder.event('carbonDioxideDetectedChanged') 16 | .type('boolean') 17 | .description('Change in carbon dioxide detected') 18 | .done(); 19 | 20 | builder.event('carbonDioxide') 21 | .description('Carbon dioxide has been detected') 22 | .done(); 23 | 24 | builder.event('carbonDioxideCleared') 25 | .description('Carbon dioxide is no longer detected') 26 | .done(); 27 | 28 | builder.action('carbonDioxideDetected') 29 | .description('Get if carbon dioxide is currently detected') 30 | .returns('boolean', 'Current carbonDioxide detected status') 31 | .done(); 32 | } 33 | 34 | get sensorTypes() { 35 | return [ ...super.sensorTypes, 'carbonDioxideDetected' ]; 36 | } 37 | 38 | carbonDioxideDetected() { 39 | return this.value('carbonDioxideDetected'); 40 | } 41 | 42 | updateCarbonDioxideDetected(carbonDioxide, autoIdleTimeout=null) { 43 | carbonDioxide = boolean(carbonDioxide); 44 | if(this.updateValue('carbonDioxideDetected', carbonDioxide)) { 45 | if(carbonDioxide) { 46 | this.emitEvent('carbonDioxide'); 47 | } else { 48 | this.emitEvent('carbonDioxideCleared'); 49 | } 50 | } 51 | 52 | // Always clear the idle timer 53 | clearTimeout(this[idleTimer]); 54 | 55 | if(carbonDioxide && autoIdleTimeout) { 56 | /* 57 | * When carbonDioxide has been detected and automatic idle is requested 58 | * set a timer. 59 | */ 60 | const ms = duration(autoIdleTimeout).ms; 61 | this[idleTimer] = setTimeout(() => this.updateCarbonDioxideDetected(false), ms); 62 | } 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /docs/sensors/power-load.rst: -------------------------------------------------------------------------------- 1 | ``cap:power-load`` - read the current power load 2 | ================================================ 3 | 4 | This capability is used to mark sensors that report power load, that is the 5 | :doc:`power ` currently being used. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('cap:power-load')) { 10 | const powerLoad = await thing.powerLoad(); 11 | console.log('Power load:', powerLoad.watts); 12 | } 13 | 14 | API 15 | --- 16 | 17 | .. js:function:: powerLoad() 18 | 19 | Get the current amount of power being used. 20 | 21 | :returns: 22 | Promise that resolves to the current amount of power used as a 23 | :doc:`power `. 24 | 25 | Example: 26 | 27 | .. sourcecode:: js 28 | 29 | const powerLoad = await thing.powerLoad(); 30 | console.log('Power load:', powerLoad.watts); 31 | 32 | Events 33 | ------ 34 | 35 | .. js:data:: powerLoadChanged 36 | 37 | The amount of power being used has changed. Payload is the power load 38 | as :doc:`power `. 39 | 40 | Example: 41 | 42 | .. sourcecode:: js 43 | 44 | thing.on('powerLoadChanged', v => console.log('Changed to:', v)); 45 | 46 | Protected methods 47 | ----------------- 48 | 49 | .. js:function:: updatePowerLoad(value) 50 | 51 | Update the power load. Should be called whenever a change is detected. 52 | 53 | :param value: 54 | The new amount of power being used, as :doc:`power `. 55 | The default unit is watts. 56 | 57 | Example: 58 | 59 | .. sourcecode:: js 60 | 61 | this.updatePowerLoad(5); 62 | 63 | Implementing capability 64 | ----------------------- 65 | 66 | Implementors of this capability should call ``updatePowerLoad`` whenever 67 | the power load changes. 68 | 69 | .. sourcecode:: js 70 | 71 | const { Sensor, PowerLoad } = require('abstract-things/sensors'); 72 | 73 | class Example extends Sensor.with(PowerLoad) { 74 | 75 | constructor() { 76 | super(); 77 | 78 | this.updatePowerLoad(10); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /docs/sensors/relative-humidity.rst: -------------------------------------------------------------------------------- 1 | ``cap:relative-humidity`` - read humidity of air 2 | ================================================ 3 | 4 | This capability is used to mark sensors that report the relative humidity of 5 | the air. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('cap:relative-humidity')) { 10 | console.log('RH:', await thing.relativeHumidity()); 11 | } 12 | 13 | API 14 | --- 15 | 16 | .. js:function:: relativeHumidity() 17 | 18 | Get the current relative humidity as a :doc:`percentage `. 19 | 20 | :returns: 21 | Promise that resolves to the current relative humidity as a 22 | :doc:`percentage `. 23 | 24 | Example: 25 | 26 | .. sourcecode:: js 27 | 28 | console.log('RH:', await thing.relativeHumidity()); 29 | 30 | Events 31 | ------ 32 | 33 | .. js:data:: relativeHumidityChanged 34 | 35 | The relative humidity has changed. Payload is the new humidity as a 36 | :doc:`percentage `. 37 | 38 | Example: 39 | 40 | .. sourcecode:: js 41 | 42 | thing.on('relativeHumidityChanged', v => console.log('Changed to:', v)); 43 | 44 | Protected methods 45 | ----------------- 46 | 47 | .. js:function:: updateRelativeHumidity(value) 48 | 49 | Update the relative humidity. Should be called whenever a change is detected. 50 | 51 | :param value: 52 | The new relative humidity. Will be converted to a 53 | :doc:`percentage `. 54 | 55 | Example: 56 | 57 | .. sourcecode:: js 58 | 59 | this.updateRelativeHumidity(32); 60 | 61 | Implementing capability 62 | ----------------------- 63 | 64 | Implementors of this capability should call ``updateRelativeHumidity`` whenever 65 | the relative humidity changes. 66 | 67 | .. sourcecode:: js 68 | 69 | const { Sensor, RelativeHumidity } = require('abstract-things/sensors'); 70 | 71 | class Example extends Sensor.with(RelativeHumidity) { 72 | 73 | constructor() { 74 | super(); 75 | 76 | this.updateRelativeHumidity(56); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /sensors/carbon-monoxide-detection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { boolean, duration } = require('../values'); 6 | 7 | const idleTimer = Symbol('autoIdle'); 8 | 9 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 10 | static get capability() { 11 | return 'carbon-monoxide-detection'; 12 | } 13 | 14 | static availableAPI(builder) { 15 | builder.event('carbonMonoxideDetectedChanged') 16 | .type('boolean') 17 | .description('Change in carbon monoxide detected') 18 | .done(); 19 | 20 | builder.event('carbonMonoxide') 21 | .description('Carbon monoxide has been detected') 22 | .done(); 23 | 24 | builder.event('carbonMonoxideCleared') 25 | .description('Carbon monoxide is no longer detected') 26 | .done(); 27 | 28 | builder.action('carbonMonoxideDetected') 29 | .description('Get if carbon monoxide is currently detected') 30 | .returns('boolean', 'Current carbonMonoxide detected status') 31 | .done(); 32 | } 33 | 34 | get sensorTypes() { 35 | return [ ...super.sensorTypes, 'carbonMonoxideDetected' ]; 36 | } 37 | 38 | carbonMonoxideDetected() { 39 | return this.value('carbonMonoxideDetected'); 40 | } 41 | 42 | updateCarbonMonoxideDetected(carbonMonoxide, autoIdleTimeout=null) { 43 | carbonMonoxide = boolean(carbonMonoxide); 44 | if(this.updateValue('carbonMonoxideDetected', carbonMonoxide)) { 45 | if(carbonMonoxide) { 46 | this.emitEvent('carbonMonoxide'); 47 | } else { 48 | this.emitEvent('carbonMonoxideCleared'); 49 | } 50 | } 51 | 52 | // Always clear the idle timer 53 | clearTimeout(this[idleTimer]); 54 | 55 | if(carbonMonoxide && autoIdleTimeout) { 56 | /* 57 | * When carbonMonoxide has been detected and automatic idle is requested 58 | * set a timer. 59 | */ 60 | const ms = duration(autoIdleTimeout).ms; 61 | this[idleTimer] = setTimeout(() => this.updateCarbonMonoxideDetected(false), ms); 62 | } 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /common/switchable-mode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Mode = require('./mode'); 5 | 6 | /** 7 | * Switchable mode capability. 8 | */ 9 | module.exports = Thing.mixin(Parent => class extends Parent.with(Mode) { 10 | /** 11 | * Define the API of appliances that can manage their mode. 12 | */ 13 | static availableAPI(builder) { 14 | builder.action('mode') 15 | .description('Get or set the mode of this appliance') 16 | .argument('string', true, 'Optional mode to change to') 17 | .returns('mode', 'The mode of the appliance') 18 | .getterForState('mode') 19 | .done(); 20 | 21 | builder.action('setMode') 22 | .description('Set the mode of this appliance') 23 | .argument('string', true, 'Mode to change to') 24 | .returns('mode', 'The mode of the appliance') 25 | .done(); 26 | } 27 | 28 | /** 29 | * Get that this provides the switchable capability. 30 | */ 31 | static get capability() { 32 | return 'switchable-mode'; 33 | } 34 | 35 | constructor(...args) { 36 | super(...args); 37 | } 38 | 39 | /** 40 | * Get or switch the mode of the appliance. 41 | * 42 | * @param {string} mode 43 | * optional mode to switch to 44 | * @returns 45 | * string indicating the mode 46 | */ 47 | mode(mode=undefined) { 48 | if(typeof mode !== 'undefined') { 49 | return this.setMode(mode); 50 | } 51 | 52 | return super.mode(); 53 | } 54 | 55 | /** 56 | * Set the mode of this appliance. 57 | * 58 | * @param {string} mode 59 | */ 60 | setMode(mode) { 61 | try { 62 | if(typeof mode === 'undefined') throw new Error('Mode must be specified'); 63 | mode = String(mode); 64 | 65 | return Promise.resolve(this.changeMode(mode)) 66 | .then(() => this.getState('mode')); 67 | } catch(ex) { 68 | return Promise.reject(ex); 69 | } 70 | } 71 | 72 | /** 73 | * Change the current mode of the device. 74 | * 75 | * @param {mode} mode 76 | */ 77 | changeMode(mode) { 78 | throw new Error('changeMode has not been implemented'); 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /docs/building-things/naming.rst: -------------------------------------------------------------------------------- 1 | Naming of identifiers, types and capabilities 2 | ============================================= 3 | 4 | Naming is one of the most important aspects when both building and using things. 5 | Libraries that use ``abstract-things`` are expected to follow a few conventions 6 | to simplify use of the things they expose. 7 | 8 | Namespaces 9 | ---------- 10 | 11 | Libraries are expected to use a short and understandable namespace. Namespaces 12 | are used for things such as identifiers and to mark things with custom types. 13 | 14 | The namespace should be connected to what the library interacts with. This can 15 | be something like ``hue`` for Philips Hue or ``bravia`` for Sony Bravia TVs. 16 | 17 | Identifiers 18 | ----------- 19 | 20 | Every Thing is required to have an identifer. Identifiers should be stable and 21 | globally unique. An identifier needs a prefix, which is usually the namespace 22 | of the library. 23 | 24 | For most implementations an identifier will usually be provided with the thing 25 | being interacted with. In those case it can simply be prefixed with the namespace 26 | to create a suitable identifier. 27 | 28 | Example of identifiers: 29 | 30 | * ``hue:000b57fffe0eee95-01`` 31 | * ``miio:55409498`` 32 | * ``uuid:8125606b-7b57-405b-94d6-e5720c44aa6a`` 33 | * ``space:global`` 34 | 35 | As a convention things that bridge other networks such as Zigbee or Z-wave 36 | include the keyword ``bridge`` in their identifier, such as 37 | ``hue:bridge:000b57fffe0eee95``. 38 | 39 | Types 40 | ----- 41 | 42 | The types defined by ``abstract-things`` try to be short and descriptive. 43 | Libraries may mark things with custom types, but those types are expected to 44 | be namespaced or unique. Those custom types can be used to identify the 45 | specific type of thing. 46 | 47 | Example of custom types: 48 | 49 | * ``hue:light`` 50 | * ``miio:air-purifier`` 51 | * ``zwave`` 52 | 53 | Capabilities 54 | ------------ 55 | 56 | Capabilities follow the same rules as types, see the previous section. 57 | -------------------------------------------------------------------------------- /docs/sensors/illuminance.rst: -------------------------------------------------------------------------------- 1 | ``cap:illuminance`` - read illuminance 2 | ====================================== 3 | 4 | This capability is used to mark sensors that report 5 | :doc:`illuminance `. This is commonly used for sensors 6 | that read light levels. 7 | 8 | .. sourcecode:: js 9 | 10 | if(thing.matches('cap:illuminance')) { 11 | console.log('Light level:', await thing.illuminance()); 12 | } 13 | 14 | API 15 | --- 16 | 17 | .. js:function:: illuminance() 18 | 19 | Get the current :doc:`illuminance `. 20 | 21 | :returns: 22 | Promise that resolves to the current 23 | :doc:`illuminance `. 24 | 25 | Example: 26 | 27 | .. sourcecode:: js 28 | 29 | const lightLevel = await thing.illuminance(); 30 | console.log('Light level:', lightLevel.lux); 31 | 32 | Events 33 | ------ 34 | 35 | .. js:data:: illuminanceChanged 36 | 37 | The illuminance has changed. Payload is the new :doc:`illuminance `. 38 | 39 | Example: 40 | 41 | .. sourcecode:: js 42 | 43 | thing.on('illuminanceChanged', v => console.log('Changed to:', v)); 44 | 45 | Protected methods 46 | ----------------- 47 | 48 | .. js:function:: updateIlluminance(value) 49 | 50 | Update the current illuminance level. Should be called whenever a change in 51 | is detected. 52 | 53 | :param value: 54 | The new illuminance. Will be converted to 55 | :doc:`illuminance `, the default conversion uses 56 | lux. 57 | 58 | Example: 59 | 60 | .. sourcecode:: js 61 | 62 | this.updateIlluminance(20); 63 | 64 | Implementing capability 65 | ----------------------- 66 | 67 | Implementors of this capability should call ``updateIlluminance`` whenever the 68 | detected light level changes. 69 | 70 | .. sourcecode:: js 71 | 72 | const { Sensor, Illuminance } = require('abstract-things/sensors'); 73 | 74 | class Example extends Sensor.with(Illuminance) { 75 | 76 | constructor() { 77 | super(); 78 | 79 | this.updateIlluminance(10); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /controllers/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const State = require('../common/state'); 5 | const { code } = require('../values'); 6 | 7 | /** 8 | * Actions, such as switches and remote controls. 9 | */ 10 | module.exports = Thing.mixin(Parent => class extends Parent.with(State) { 11 | static get capability() { 12 | return 'actions'; 13 | } 14 | 15 | static availableAPI(builder) { 16 | builder.state('actions') 17 | .description('Actions that the controller can emit') 18 | .type('array') 19 | .done(); 20 | 21 | builder.event('actions') 22 | .description('The supported actions have changed') 23 | .type('array') 24 | .done(); 25 | 26 | builder.event('action') 27 | .description('A certain action has been triggered') 28 | .type('object') 29 | .done(); 30 | 31 | builder.event('action:') 32 | .description('Action with the given id has been triggered') 33 | .done(); 34 | 35 | builder.action('actionsChanged') 36 | .description('Get the actions that this controller can emit') 37 | .returns('array') 38 | .done(); 39 | } 40 | 41 | constructor(...args) { 42 | super(...args); 43 | 44 | this.updateState('actions', []); 45 | } 46 | 47 | /** 48 | * Emit the given action for this controller. 49 | * 50 | * @param {string} action 51 | */ 52 | emitAction(action, extra={}) { 53 | this.emitEvent('action', { action: action, data: extra }, { multiple: true }); 54 | this.emitEvent('action:' + action, extra, { multiple: true }); 55 | } 56 | 57 | /** 58 | * Get the available actions for this controller. All of these actions 59 | * can be emitted. 60 | */ 61 | actions() { 62 | return Promise.resolve(this.getState('actions')); 63 | } 64 | 65 | /** 66 | * Update the available actions of the controller. 67 | */ 68 | updateActions(actions) { 69 | let mapped = []; 70 | for(const a of actions) { 71 | mapped.push(code(a)); 72 | } 73 | 74 | if(this.updateState('actions', mapped)) { 75 | this.emitEvent('actionsChanged', mapped); 76 | } 77 | } 78 | }); 79 | -------------------------------------------------------------------------------- /utils/metadata.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const thing = Symbol('thing'); 4 | const name = Symbol('name'); 5 | 6 | /** 7 | * Metadata for a appliance that provides a builder-like API for 8 | * easily updating the metadata. 9 | */ 10 | module.exports = class Metadata { 11 | constructor(instance) { 12 | this[thing] = instance; 13 | 14 | this.types = new Set(); 15 | this.capabilities = new Set(); 16 | } 17 | 18 | get name() { 19 | return this[name]; 20 | } 21 | 22 | set name(n) { 23 | if(this[name] !== n) { 24 | this[name] = n; 25 | this[thing].emitEvent('thing:metadata', this); 26 | } 27 | } 28 | 29 | addTypes(...types) { 30 | for(let type of types) { 31 | this.types.add(type); 32 | } 33 | this[thing].emitEvent('thing:metadata', this); 34 | return this; 35 | } 36 | 37 | addCapabilities(...caps) { 38 | for(let cap of caps) { 39 | this.capabilities.add(cap); 40 | } 41 | this[thing].emitEvent('thing:metadata', this); 42 | return this; 43 | } 44 | 45 | removeCapabilities(...caps) { 46 | for(let cap of caps) { 47 | this.capabilities.delete(cap); 48 | } 49 | this[thing].emitEvent('thing:metadata', this); 50 | return this; 51 | } 52 | 53 | /** 54 | * Get if the appliance is of the given type. 55 | * 56 | * @param {string} type 57 | */ 58 | hasType(type) { 59 | return this.types.has(type); 60 | } 61 | 62 | /** 63 | * Get if the appliance has the given capability. 64 | * 65 | * @param {string} cap 66 | */ 67 | hasCapability(cap) { 68 | return this.capabilities.has(cap); 69 | } 70 | 71 | /** 72 | * Check if this metadata matches the given tags. 73 | * 74 | * @param {string} tags 75 | */ 76 | matches(...tags) { 77 | for(const tag of tags) { 78 | if(tag.indexOf('type:') === 0) { 79 | if(! this.hasType(tag.substring(5))) { 80 | return false; 81 | } 82 | } else if(tag.indexOf('cap:') === 0) { 83 | if(! this.hasCapability(tag.substring(4))) { 84 | return false; 85 | } 86 | } else { 87 | return false; 88 | } 89 | } 90 | 91 | return true; 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /docs/common/power.rst: -------------------------------------------------------------------------------- 1 | ``cap:power`` - monitor power state 2 | ==================================== 3 | 4 | The ``power``-capability is used for any thing that can monitor its power 5 | state. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('cap:power')) { 10 | console.log('Power is', await thing.power()); 11 | 12 | thing.on('powerChanged', power => console.log('Power is now', power)); 13 | } 14 | 15 | Related capabilities: :doc:`switchable-power `, :doc:`state ` 16 | 17 | API 18 | --- 19 | 20 | .. js:function:: power() 21 | 22 | Get the current power state. 23 | 24 | :returns: 25 | Promise that resolves to a :doc:`boolean ` 26 | representing the current power state. 27 | 28 | Example: 29 | 30 | .. sourcecode:: js 31 | 32 | thing.power() 33 | .then(power => ...) 34 | .catch(...); 35 | 36 | const powerIsOn = await thing.power(); 37 | 38 | Events 39 | ------ 40 | 41 | .. describe:: powerChanged 42 | 43 | The current power state has changed. Payload will be current power state 44 | as a :doc:`boolean `. 45 | 46 | .. sourcecode:: js 47 | 48 | thing.on('powerChanged', power => console.log('power is now:', power)); 49 | 50 | Protected methods 51 | ----------------- 52 | 53 | .. js:function:: updatePower(power) 54 | 55 | Update the current power state of the thing. Will change the state key 56 | ``power`` and emit the ``power`` event. 57 | 58 | :param boolean power: The current power state. 59 | 60 | Implementing capability 61 | ----------------------- 62 | 63 | The ``power``-capability has no functions that need to be implemented. Call 64 | ``updatePower`` whenever the monitored power state changes. 65 | 66 | Example: 67 | 68 | .. sourcecode:: js 69 | 70 | const { Thing, Power } = require('abstract-things'); 71 | 72 | class Example extends Thing.with(Power) { 73 | constructor() { 74 | super(); 75 | 76 | // Indicate that power has been switched every second 77 | setInterval(() => { 78 | this.updatePower(! this.getState('power')); 79 | }, 1000); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /docs/sensors/power-consumed.rst: -------------------------------------------------------------------------------- 1 | ``cap:power-consumed`` - read power consumed 2 | ============================================ 3 | 4 | This capability is used to mark sensors that report power consumed by something. 5 | 6 | .. sourcecode:: js 7 | 8 | if(thing.matches('cap:power-consumed')) { 9 | const powerConsumed = await thing.powerConsumed(); 10 | console.log('Power consumed:', powerConsumed.wattHours); 11 | } 12 | 13 | API 14 | --- 15 | 16 | .. js:function:: powerConsumed 17 | 18 | Get the current amount of power consumed. 19 | . 20 | 21 | :returns: 22 | Promise that resolves to the amount of power consumed as 23 | :doc:`energy `. 24 | 25 | Example: 26 | 27 | .. sourcecode:: js 28 | 29 | const powerConsumed = await thing.powerConsumed(); 30 | console.log('Power consumed:', powerConsumed.wattHours); 31 | 32 | Events 33 | ------ 34 | 35 | .. js:data:: powerConsumedChanged 36 | 37 | The amount of power consumed has changed. Payload is the power consumed 38 | as :doc:`energy `. 39 | 40 | Example: 41 | 42 | .. sourcecode:: js 43 | 44 | thing.on('powerConsumedChanged', v => console.log('Changed to:', v)); 45 | 46 | Protected methods 47 | ----------------- 48 | 49 | .. js:function:: updatePowerConsumed(value) 50 | 51 | Update the power consumed. Should be called whenever a change is detected. 52 | 53 | :param value: 54 | The new amount of power consumed, as :doc:`energy `. 55 | The default unit is joules. 56 | 57 | Example: 58 | 59 | .. sourcecode:: js 60 | 61 | const { energy } = require('abstract-things/values'); 62 | this.updatePowerConsumed(energy(0.5, 'wh')); 63 | 64 | Implementing capability 65 | ----------------------- 66 | 67 | Implementors of this capability should call ``updatePowerConsumed`` whenever 68 | the power consumed changes. 69 | 70 | .. sourcecode:: js 71 | 72 | const { Sensor, PowerConsumed } = require('abstract-things/sensors'); 73 | 74 | class Example extends Sensor.with(PowerConsumed) { 75 | 76 | constructor() { 77 | super(); 78 | 79 | this.updatePowerConsumed(10); // Joules 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /docs/common/audio-feedback.rst: -------------------------------------------------------------------------------- 1 | ``cap:audio-feedback`` - if thing emits audio feedback 2 | ====================================================== 3 | 4 | ``audio-feedback`` is used by things that can report if they emit audio 5 | feedback or not. Such feedback can be as simple as beeping when certain buttons 6 | are pressed or when certain actions are performed. It may also be more advanced 7 | audio such as spoken commands. This capability is commonly paired with 8 | :doc:`adjustable-audio-feedback ` if the the thing 9 | support toggling audio feedback on and off. 10 | 11 | .. sourcecode:: js 12 | 13 | if(thing.matches('cap:audio-feedback')) { 14 | console.log('Audio feedback on:', await thing.audioFeedback()); 15 | 16 | thing.on('audioFeedbackChanged', on => console.log('Audio feedback is now:', on)); 17 | } 18 | 19 | API 20 | --- 21 | 22 | .. js:function:: audioFeedback() 23 | 24 | Get if the thing emits audio feedback. 25 | 26 | :returns: 27 | Promise that resolves to a :doc:`boolean ` 28 | representing if audio feedback is enabled. 29 | 30 | Example: 31 | 32 | .. sourcecode:: js 33 | 34 | thing.audioFeedback() 35 | .then(on => ...) 36 | .catch(...); 37 | 38 | const on = await thing.audioFeedback(); 39 | 40 | Events 41 | ------ 42 | 43 | .. describe:: audioFeedbackChanged 44 | 45 | The current audio feedback state has changed. Payload will be if the 46 | feedback is enabled. 47 | 48 | 49 | .. sourcecode:: js 50 | 51 | thing.on('audioFeedbackChanged', on => ...) 52 | 53 | Protected methods 54 | ----------------- 55 | 56 | .. js:function:: updateAudioFeedback(enabled) 57 | 58 | Update if audio feedback is currently enabled. 59 | 60 | :param boolean enabled: If the feedback is enabled. 61 | 62 | Implementing capability 63 | ----------------------- 64 | 65 | This capability requires that the implementors calls ``updateAudioFeedback`` 66 | when changes are detected. 67 | 68 | Example: 69 | 70 | .. sourcecode:: js 71 | 72 | const { Thing, AudioFeedback } = require('abstract-things'); 73 | 74 | class Example extends Thing.with(AudioFeedback) { 75 | 76 | } 77 | -------------------------------------------------------------------------------- /docs/common/nameable.rst: -------------------------------------------------------------------------------- 1 | ``cap:nameable`` - renameable things 2 | ==================================== 3 | 4 | ``nameable`` is used by things that have a name that can be updated. 5 | 6 | .. sourcecode:: js 7 | 8 | if(thing.matches('cap:nameable')) { 9 | thing.setName('New Name') 10 | .then(() => console.log('Name updated')) 11 | .catch(err => console.log('Error occurred during update:', err)); 12 | } 13 | 14 | API 15 | --- 16 | 17 | .. js:function:: setName(name) 18 | 19 | Update the name of this thing. 20 | 21 | :param string name: Name for thing. 22 | :returns: Promise that resolves to the name set. 23 | 24 | Protected methods 25 | ----------------- 26 | 27 | .. js:function:: changeName(name) 28 | 29 | *Abstract.* Change and store the name of the thing. This is called when the 30 | user calls ``setName``. This method should update the ``name`` property of 31 | the metadata when the new name has been stored. 32 | 33 | :param string name: The name to set. 34 | :returns: Promise that resolves after name has been updated. 35 | 36 | Example: 37 | 38 | .. sourcecode:: js 39 | 40 | changeName(name) { 41 | return setNameSomehow(name) 42 | .then(() => this.metadata.name = name); 43 | } 44 | 45 | Implementing capability 46 | ----------------------- 47 | 48 | ``changeName`` needs to be implemented to actually set the name. The name 49 | should be loaded and set either in the constructor or ``initCallback`` of the 50 | thing. 51 | 52 | .. sourcecode:: js 53 | 54 | const { Thing, Nameable } = require('abstract-things'); 55 | 56 | class Example extends Thing.with(Nameable) { 57 | initCallback() { 58 | return super.initCallback() 59 | .then(() => loadNameSomehow()) 60 | .then(name => this.metadata.name = name); 61 | } 62 | 63 | changeName(name) { 64 | return setNameSomehow(name) 65 | .then(() => this.metadata.name = name); 66 | } 67 | } 68 | 69 | For things that just need to be nameable a special capability is provided 70 | that stores the name locally: 71 | 72 | .. sourcecode:: js 73 | 74 | const { Thing, EasyNameable } = require('abstract-things'); 75 | 76 | class Example extends Thing.with(EasyNameable) { 77 | } 78 | -------------------------------------------------------------------------------- /docs/values/color.rst: -------------------------------------------------------------------------------- 1 | Color 2 | ===== 3 | 4 | Colors are available the ``color`` type and supports conversions between many 5 | common color spaces. 6 | 7 | .. sourcecode:: js 8 | 9 | const { color } = require('abstract-things/values'); 10 | 11 | console.log(color('red')); 12 | console.log(color('5500K')); 13 | console.log(color('#ff0000')); 14 | console.log(color('hsl(300, 80%, 100%)')); 15 | 16 | RGB 17 | --- 18 | 19 | RGB colors are supported and are commonly created via either named colors, such 20 | as ``red`` and ``purple`` or via Hex-notation such as ``#ff0000``. 21 | 22 | RGB colors can be created via the ``rgb``-function: 23 | 24 | .. sourcecode:: js 25 | 26 | const colorPicked = color.rgb(255, 0, 0); 27 | 28 | Colors can be converted to RGB via the ``rgb``-accessor and their individual 29 | components accessed: 30 | 31 | .. sourcecode:: js 32 | 33 | const rgb = colorPicked.rgb; 34 | 35 | console.log('Red:', rgb.red); 36 | console.log('Green:', rgb.green); 37 | console.log('Blue:', rgb.blue); 38 | 39 | Temperatures 40 | ------------ 41 | 42 | Color temperatures can be created from a string on the form ``[number]K``, 43 | such as ``4000K`` or ``5500K``: ``color('4000K')``. Temperatures can also be 44 | created via the ``temperature`` function: ``color.temperature(4000)``. 45 | 46 | The following temperatures are available via name: 47 | 48 | * ``overcast`` - 6500 Kelvins 49 | * ``daylight`` - 5500 Kelvins 50 | * ``sunrise`` - 2400 Kelvins 51 | * ``sunset`` - 2400 Kelvins 52 | * ``candle`` - 2000 Kelvins 53 | * ``moonlight`` - 4100 Kelvins 54 | 55 | Example: 56 | 57 | .. sourcecode:: js 58 | 59 | color('4000K'); 60 | color.temperature(5500); 61 | color('overcast'); 62 | 63 | Any color can be converted to its nearest temperature via the getter 64 | ``temperature``: 65 | 66 | .. sourcecode:: js 67 | 68 | console.log(color('red').temperature); 69 | console.log(color('white').temperature); 70 | 71 | The actual Kelvin-value is available via the ``kelvins`` accessor: 72 | 73 | .. sourcecode:: js 74 | 75 | console.log(color.kelvins); 76 | 77 | It's also possible to get a mired-version of the temperature which is used 78 | by Zigbee-lights: ``color('4000K').mired.value`` 79 | -------------------------------------------------------------------------------- /docs/climate/target-humidity.rst: -------------------------------------------------------------------------------- 1 | ``cap:target-humidity`` - read the target humidity 2 | =================================================== 3 | 4 | The ``target-humidity`` capability is used by things such as 5 | :doc:`humidifers ` and :doc:`dehumidifiers ` that 6 | support stopping when a certain target humidity is reached. Some things may 7 | also support setting the target humidity via 8 | :doc:`adjustable-target-humidity `. 9 | 10 | .. sourcecode:: js 11 | 12 | if(thing.matches('cap:target-humidity')) { 13 | const humidity = await thing.targetHumidity(); 14 | console.log('Target humidity:', humidity); 15 | } 16 | 17 | API 18 | --- 19 | 20 | .. js:function:: targetHumidity() 21 | 22 | Get the current target humidity. 23 | 24 | :returns: 25 | Promise that resolves to the current target humidity as a 26 | :doc:`percentage `. 27 | 28 | Example: 29 | 30 | .. sourcecode:: js 31 | 32 | const target = await thing.targetHumidity(); 33 | 34 | Events 35 | ------ 36 | 37 | .. describe:: targetHumidityChanged 38 | 39 | The current target humidity has changed. Payload will be the new target 40 | humidity as a :doc:`percentage `. 41 | 42 | Example: 43 | 44 | .. sourcecode:: js 45 | 46 | thing.on('targetHumidityChanged', th => console.log('Target:', th)); 47 | 48 | Protected methods 49 | ----------------- 50 | 51 | .. js:function:: updateTargetHumidity(target) 52 | 53 | Update the current target humidity. 54 | 55 | :param percentage target: 56 | The new target humidity as a :doc:`percentage `. 57 | 58 | Example: 59 | 60 | .. sourcecode:: js 61 | 62 | this.updateTargetHumidity(40); 63 | this.updateTargetHumidity('55%'); 64 | 65 | Implementing capability 66 | ----------------------- 67 | 68 | When implementing this capability the implementor needs to call 69 | ``updateTargetHumidity`` whenever a change in target humidity is detected. 70 | 71 | .. sourcecode:: js 72 | 73 | const { Thing } = require('abstract-things'); 74 | const { TargetHumidity } = require('abstract-things/climate'); 75 | 76 | class Example extends Thing.with(TargetHumidity) { 77 | 78 | } 79 | -------------------------------------------------------------------------------- /docs/sensors/temperature.rst: -------------------------------------------------------------------------------- 1 | ``cap:temperature`` - read temperature 2 | ====================================== 3 | 4 | This capability is used to mark sensors that report a :doc:`temperature `. 5 | 6 | .. sourcecode:: js 7 | 8 | if(thing.matches('cap:temperature')) { 9 | const temperature = await thing.temperature(); 10 | console.log('Temperature:', temperature.celsius); 11 | } 12 | 13 | API 14 | --- 15 | 16 | .. js:function:: temperature() 17 | 18 | Get the current :doc:`temperature `. 19 | 20 | :returns: 21 | Promise that resolves to the current 22 | :doc:`temperature `. 23 | 24 | Example: 25 | 26 | .. sourcecode:: js 27 | 28 | console.log('Temperature is:', thing.temperature); 29 | 30 | Events 31 | ------ 32 | 33 | .. js:data:: temperatureChanged 34 | 35 | The temperature has changed. Payload is the new :doc:`temperature `. 36 | 37 | Example: 38 | 39 | .. sourcecode:: js 40 | 41 | thing.on('temperatureChanged', temp => console.log('Temp changed to:', temp)); 42 | 43 | Protected methods 44 | ----------------- 45 | 46 | .. js:function:: updateTemperature(value) 47 | 48 | Update the current temperature. Should be called whenever a change in 49 | temperature was detected. 50 | 51 | :param value: 52 | The new temperature. Will be converted to a 53 | :doc:`temperature `, the default conversion uses 54 | degrees Celsius. 55 | 56 | Example: 57 | 58 | .. sourcecode:: js 59 | 60 | // Defaults to Celsius 61 | this.updateTemperature(20); 62 | 63 | // temperature value can be used to use Fahrenheit (or Kelvin) 64 | const { temperature } = require('abstract-things/values'); 65 | this.updateTemperature(temperature(45, 'F')); 66 | 67 | Implementing capability 68 | ----------------------- 69 | 70 | Implementors of this capability should call ``updateTemperature`` whenever the 71 | temperature changes. 72 | 73 | .. sourcecode:: js 74 | 75 | const { Sensor, Temperature } = require('abstract-things/sensors'); 76 | 77 | class Example extends Sensor.with(Temperature) { 78 | 79 | constructor() { 80 | super(); 81 | 82 | this.updateTemperature(22); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /docs/sensors/atmospheric-pressure.rst: -------------------------------------------------------------------------------- 1 | ``cap:atmospheric-pressure`` - read atmospheric pressure 2 | ======================================================= 3 | 4 | This capability is used to mark sensors that report the atmospheric pressure. 5 | 6 | .. sourcecode:: js 7 | 8 | if(thing.matches('cap:atmospheric-pressure')) { 9 | console.log('Atmospheric pressure:', await thing.atmosphericPressure()); 10 | } 11 | 12 | API 13 | --- 14 | 15 | .. js:function:: atmosphericPressure() 16 | 17 | Get the current atmospheric pressure. 18 | 19 | :returns: 20 | Promise that resolves to the atmospheric pressure as a 21 | :doc:`pressure `. 22 | 23 | Example: 24 | 25 | .. sourcecode:: js 26 | 27 | console.log('Atmospheric pressure:', await thing.atmosphericPressure()); 28 | 29 | Events 30 | ------ 31 | 32 | .. js:data:: atmosphericPressureChanged 33 | 34 | The atmospheric pressure has changed. 35 | 36 | Example: 37 | 38 | .. sourcecode:: js 39 | 40 | thing.on('atmosphericPressureChanged', value => console.log('Pressure changed to:', value)); 41 | 42 | Protected methods 43 | ----------------- 44 | 45 | .. js:function:: updateAtmosphericPressure(value) 46 | 47 | Update the current atmospheric pressure. Should be called whenever a change 48 | in atmospheric pressure is detected. 49 | 50 | :param value: The new atmospheric pressure. 51 | 52 | Example: 53 | 54 | .. sourcecode:: js 55 | 56 | // Defaults to pascals 57 | this.updateAtmosphericPressure(101325); 58 | 59 | // pressure value can be used to use hPa (= millibar), bar, psi or mmHg 60 | const { pressure } = require('abstract-things/values'); 61 | this.updateAtmosphericPressure(pressure(1, 'atm')); 62 | this.updateAtmosphericPressure(pressure(1013, 'hPa')); 63 | 64 | Implementing capability 65 | ----------------------- 66 | 67 | Implementors of this capability should call ``updateAtmosphericPressure`` 68 | whenever the atmospheric pressure changes. 69 | 70 | .. sourcecode:: js 71 | 72 | const { Sensor, AtmosphericPressure } = require('abstract-things/sensors'); 73 | 74 | class Example extends Sensor.with(AtmosphericPressure) { 75 | 76 | constructor() { 77 | super(); 78 | 79 | this.updateAtmosphericPressure(101325); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /docs/building-things/index.rst: -------------------------------------------------------------------------------- 1 | Building things 2 | =================== 3 | 4 | Things are built by extending ``Thing`` with a combination of types and 5 | capabilities. The first step is to make sure that the project has acccess to 6 | ``abstract-things``: 7 | 8 | .. sourcecode:: shell 9 | 10 | $ npm install abstract-things 11 | 12 | It is recommended to target at least Node 8 to make use of ``async`` and 13 | ``await``. It will make handling the asynchronous nature of API calls easier. 14 | 15 | The smallest possible thing simply extends ``Thing``: 16 | 17 | .. sourcecode:: js 18 | 19 | const { Thing } = require('abstract-things'); 20 | 21 | class ExampleThing extends Thing { 22 | constructor(id) { 23 | super(); 24 | 25 | // Identifier is required to be set 26 | this.id = 'example:' + id; 27 | } 28 | } 29 | 30 | The following example provides a class named ``Timer`` that declares its type 31 | and available API. It will emit the ``timer`` event when an added timer is 32 | fired. 33 | 34 | .. sourcecode:: js 35 | 36 | const { Thing } = require('abstract-things'); 37 | const { duration } = require('abstract-things/values'); 38 | 39 | /** 40 | * Timer that calls itself `timer:global` and that allows timers to be set 41 | * and listened for in the network. 42 | */ 43 | class Timer extends Thing { 44 | static get type() { 45 | return 'timer'; 46 | } 47 | 48 | static availableAPI(builder) { 49 | builder.event('timer') 50 | .description('A timer has been fired') 51 | .type('string') 52 | .done(); 53 | 54 | builder.action('addTimer') 55 | .description('Add a timer to be fired') 56 | .argument('string', false, 'Name of timer') 57 | .argument('duration', false, 'Amount of time to delay the firing of the timer') 58 | .done(); 59 | } 60 | 61 | constructor() { 62 | super(); 63 | 64 | this.id = 'timer:global'; 65 | } 66 | 67 | addTimer(name, delay) { 68 | if(! name) throw new Error('Timer needs a name'); 69 | if(! delay) throw new Error('Timer needs a delay'); 70 | 71 | delay = duration(delay); 72 | 73 | setTimeout(() => { 74 | this.emitEvent('timer', name); 75 | }, delay.ms) 76 | } 77 | } 78 | 79 | .. toctree:: 80 | :maxdepth: 1 81 | :caption: Topics 82 | 83 | naming 84 | metadata 85 | mixins 86 | init-destroy 87 | events 88 | -------------------------------------------------------------------------------- /utils/collectMetadata.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const DefinitionBuilder = require('./define-api'); 4 | const Metadata = require('./metadata'); 5 | 6 | function collect(metadata, tags, prefix, func, prototype, getter) { 7 | // Check that the getter we are using belongs to the prototype 8 | if(! prototype.hasOwnProperty(getter)) return; 9 | 10 | // Get the value returned 11 | const r = prototype[getter]; 12 | 13 | // The value needs to be something useful 14 | if(typeof r === 'undefined' || r === null) return; 15 | 16 | if(typeof r === 'string') { 17 | // Strings are not expanded when passed to metadata 18 | metadata[func](r); 19 | tags.push(prefix + ':' + r); 20 | } else if(Array.isArray(r)) { 21 | // Arrays are expanded 22 | metadata[func](...r); 23 | for(const i of r) { 24 | tags.push(prefix + ':' + i); 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * Go through the prototype chain of a class looking for information that 31 | * is used to create metadata about an instance. 32 | */ 33 | module.exports = function collectMetadata(Parent, instance) { 34 | const metadata = new Metadata(instance); 35 | const builder = new DefinitionBuilder(); 36 | 37 | let prototype = instance.constructor; 38 | while(prototype !== Parent) { 39 | const tags = []; 40 | 41 | // static get types() { return [ 'typeA', 'typeB ] } 42 | collect(metadata, tags, 'type', 'addTypes', prototype, 'types'); 43 | 44 | // static get type() { return 'type' } 45 | collect(metadata, tags, 'type', 'addTypes', prototype, 'type'); 46 | 47 | // static get capabilities() { return [ 'capA', 'capB ] } 48 | collect(metadata, tags, 'cap', 'addCapabilities', prototype, 'capabilities'); 49 | 50 | // static get capability() { return 'cap' } 51 | collect(metadata, tags, 'cap', 'addCapabilities', prototype, 'capability'); 52 | 53 | // Set the tags to mark the actions with 54 | builder.markWith(tags); 55 | 56 | const api = prototype.availableAPI; 57 | if(typeof api === 'function') { 58 | prototype.availableAPI(builder); 59 | } else if(Array.isArray(api)) { 60 | // If an array treat each entry as a name 61 | for(const action of api) { 62 | builder.action(action).done(); 63 | } 64 | } 65 | 66 | prototype = Object.getPrototypeOf(prototype); 67 | } 68 | 69 | Object.assign(metadata, builder.done()); 70 | return metadata; 71 | }; 72 | -------------------------------------------------------------------------------- /docs/common/switchable-mode.rst: -------------------------------------------------------------------------------- 1 | ``cap:switchable-mode`` - switch mode 2 | ====================================== 3 | 4 | Capability used for things that can switch their mode. 5 | 6 | .. sourcecode:: js 7 | 8 | if(thing.matches('cap:switchable-mode')) { 9 | console.log('Mode is', await thing.mode()); 10 | 11 | // Switch the mode 12 | await thing.mode('new-mode'); 13 | } 14 | 15 | API 16 | --- 17 | 18 | .. js:function:: mode([newMode]) 19 | 20 | Get or set the mode of the thing. Will return the mode as a string if no 21 | mode is specified. Will return a promise if a mode is specified. 22 | 23 | :param string newMode: Optional mode to change to. 24 | :returns: Promise when switching mode, string if getting. 25 | 26 | Example: 27 | 28 | .. sourcecode:: js 29 | 30 | // Getting returns a string 31 | const currentMode = await thing.mode(); 32 | 33 | // Switching returns a promise 34 | thing.mode('new-mode') 35 | .then(result => console.log('Mode is now', result)) 36 | .catch(err => console.log('Error occurred', err); 37 | 38 | .. js:function setMode(newMode) 39 | 40 | Change the current mode. 41 | 42 | :param string newMode: Mode to change to. 43 | :returns: Promise that resolves to the new mode. 44 | 45 | Example: 46 | 47 | .. sourcecode:: js 48 | 49 | thing.setMode('new-mode') 50 | .then(result => console.log('Mode is now', result)) 51 | .catch(err => console.log('Error occurred', err); 52 | 53 | await thing.setMode('new-mode'); 54 | 55 | Protected methods 56 | ----------------- 57 | 58 | .. js:function:: changeMode(newMode) 59 | 60 | *Abstract*. Change to a new mode. Will be called whenever a change to the 61 | mode is requested. Implementations should call ``updateMode(newMode)`` 62 | before resolving to indicate that the mode has changed. 63 | 64 | :param string newMode: The new mode of the thing. 65 | :returns: Promise if asynchronous. 66 | 67 | Implementing capability 68 | ----------------------- 69 | 70 | Implementations require that the method ``changeMode`` is implemented. 71 | 72 | .. sourcecode:: js 73 | 74 | const { Thing, SwitchableMode } = require('abstract-things'); 75 | 76 | class Example extends Thing.with(SwitchableMode) { 77 | 78 | changeMode(newMode) { 79 | return swithcWithPromise(newMode) 80 | .then(() => this.updateMode(newMode)); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /utils/converters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function combine(first, second) { 4 | return function(value) { 5 | return second(first(value)); 6 | }; 7 | } 8 | 9 | class Converters { 10 | constructor() { 11 | this._conversions = []; 12 | this._cached = {}; 13 | } 14 | 15 | add(from, to, converter) { 16 | if(from === to) return; 17 | 18 | const c = { 19 | count: 1, 20 | from: from, 21 | to: to, 22 | converter: converter 23 | }; 24 | this._conversions.push(c); 25 | this._cached[from + '->' + to] = c; 26 | 27 | return this; 28 | } 29 | 30 | _converter(from, to) { 31 | const cached = this._cached[from + '->' + to]; 32 | if(cached) { 33 | return cached.converter; 34 | } 35 | 36 | const checked = {}; 37 | const queue = []; 38 | function insertIntoQueue(c) { 39 | if(c.from === c.to) return; 40 | 41 | const v = checked[c.from + '->' + c.to]; 42 | if(v) return; 43 | checked[c.from + '->' + c.to] = true; 44 | 45 | const count = c.count; 46 | for(let i=0; i count) { 48 | queue.splice(i, 0, c); 49 | return; 50 | } 51 | } 52 | 53 | queue.push(c); 54 | } 55 | 56 | this._conversions.forEach(c => { 57 | if(c.from === from) { 58 | insertIntoQueue(c); 59 | } 60 | }); 61 | 62 | while(queue.length) { 63 | const item = queue.shift(); 64 | 65 | if(item.to === to) { 66 | this._cached[from + '->' + to] = item; 67 | return item.converter; 68 | } else { 69 | this._conversions.forEach(c => { 70 | if(c.from === item.to) { 71 | insertIntoQueue({ 72 | from: item.from, 73 | to: c.to, 74 | count: item.count + 1, 75 | converter: combine(item.converter, c.converter) 76 | }); 77 | } 78 | }); 79 | } 80 | } 81 | 82 | this._cached[from + '->' + to] = { 83 | converter: null 84 | }; 85 | return null; 86 | } 87 | 88 | convert(from, to, value) { 89 | if(from === to) return value; 90 | 91 | const c = this._converter(from, to); 92 | if(! c) { 93 | throw new Error('No suitable conversion between ' + from + ' and ' + to); 94 | } 95 | 96 | return c(value); 97 | } 98 | } 99 | 100 | module.exports = function() { 101 | return new Converters(); 102 | }; 103 | -------------------------------------------------------------------------------- /docs/sensors/pm2.5.rst: -------------------------------------------------------------------------------- 1 | ``cap:pm2.5`` - read PM2.5 density (air quality) 2 | ================================================ 3 | 4 | This capability is used to mark sensors that monitor fine particulate matter 5 | (PM) of up to 2.5 micrometers (μm). 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('cap:pm2.5')) { 10 | console.log('PM2.5:', await thing.pm2_5()); 11 | } 12 | 13 | API 14 | --- 15 | 16 | .. js:function:: pm2_5() 17 | 18 | Get the current PM2.5 as micrograms per cubic meter (μg/m³). Value is a 19 | :doc:`number `. 20 | 21 | :returns: 22 | The current value as micrograms per cubic meter (μg/m³). Value is a 23 | :doc:`number `. 24 | 25 | Example: 26 | 27 | .. sourcecode:: js 28 | 29 | console.log('PM2.5:', await thing.pm2_5()); 30 | 31 | .. js:function:: 'pm2.5'() 32 | 33 | Get the current PM2.5 as micrograms per cubic meter (μg/m³). Value is a 34 | :doc:`number `. 35 | 36 | :returns: 37 | The current value as micrograms per cubic meter (μg/m³). Value is a 38 | :doc:`number `. 39 | 40 | Example: 41 | 42 | .. sourcecode:: js 43 | 44 | console.log('PM2.5:', await thing['pm2.5']()); 45 | 46 | Events 47 | ------ 48 | 49 | .. js:data:: pm2.5Changed 50 | 51 | The PM2.5 has changed. Payload is a :doc:`number ` with 52 | the new PM2.5 as micrograms per cubic meter (μg/m³). 53 | 54 | Example: 55 | 56 | .. sourcecode:: js 57 | 58 | thing.on('pm2.5Changed', v => console.log('Changed to:', v)); 59 | 60 | Protected methods 61 | ----------------- 62 | 63 | .. js:function:: updatePM2_5(value) 64 | 65 | Update the current PM2.5 as micrograms per cubic meter (μg/m³). Should be 66 | called whenever a change is detected. 67 | 68 | :param value: 69 | The new PM2.5 value. Will be converted to a 70 | :doc:`number `. 71 | 72 | Example: 73 | 74 | .. sourcecode:: js 75 | 76 | this.updatePM2_5(10); 77 | 78 | Implementing capability 79 | ----------------------- 80 | 81 | Implementors of this capability should call ``updatePM2_5`` whenever the 82 | detected PM2.5 changes. 83 | 84 | .. sourcecode:: js 85 | 86 | const { Sensor, PM2_5 } = require('abstract-things/sensors'); 87 | 88 | class Example extends Sensor.with(PM2_5) { 89 | 90 | constructor() { 91 | super(); 92 | 93 | this.updatePM2_5(10); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /docs/common/battery-level.rst: -------------------------------------------------------------------------------- 1 | ``cap:battery-level`` - monitor battery level 2 | ============================================= 3 | 4 | The ``battery-level`` capability is used for things that have a battery that 5 | can be monitored. Sometimes this capability is combined with 6 | :doc:`charging-state ` if the thing also can report when it is 7 | being charged. 8 | 9 | .. sourcecode:: js 10 | 11 | if(thing.matches('cap:battery-level')) { 12 | console.log('Current battery level:', await thing.batteryLevel()); 13 | } 14 | 15 | API 16 | --- 17 | 18 | .. js:function:: batteryLevel() 19 | 20 | Get the current battery level as a :doc:`percentage ` 21 | between 0 and 100. 22 | 23 | :returns: Promise that resolves to the battery level in percent. 24 | 25 | Example: 26 | 27 | .. sourcecode:: js 28 | 29 | thing.batteryLevel() 30 | .then(level => ...) 31 | .catch(...); 32 | 33 | const level = await thing.batteryLevel(); 34 | 35 | Events 36 | ------ 37 | 38 | .. describe:: batteryLevelChanged 39 | 40 | The current battery level has changed. Payload will be the new battery 41 | level as a :doc:`percentage `. 42 | 43 | .. sourcecode:: js 44 | 45 | thing.on('batteryLevelChanged', batteryLevel => console.log('Battery level is now:', batteryLevel)); 46 | 47 | Protected methods 48 | ----------------- 49 | 50 | .. js:function:: updateBatteryLevel(batteryLevel) 51 | 52 | Update the current battery level. Should be called whenever a change in 53 | battery level is detected. 54 | 55 | :param percentage batteryLevel: 56 | The new battery level. Will be converted to a 57 | :doc:`percentage `. 58 | 59 | Example: 60 | 61 | .. sourcecode:: js 62 | 63 | this.updateBatteryLevel(20); 64 | this.updateBatteryLevel('10'); 65 | 66 | Implementing capability 67 | ----------------------- 68 | 69 | When implementing this capability the implementor needs to call 70 | ``updateBatteryLevel`` whenever the battery level changes. 71 | 72 | .. sourcecode:: js 73 | 74 | const { Thing, BatteryLevel } = require('abstract-things'); 75 | 76 | class Example extends Thing.with(BatteryLevel) { 77 | 78 | initCallback() { 79 | return super.initCallback() 80 | .then(readBatteryLevelSomehow) 81 | .then(batteryLevel => { 82 | this.updateBatteryLevel(batteryLevel); 83 | }); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /docs/common/state.rst: -------------------------------------------------------------------------------- 1 | ``cap:state`` - state tracking 2 | =================================== 3 | 4 | The ``state``-capability provides a way to get and update the state of a thing. 5 | State is split into several state keys that are updated separately. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('cap:state')) { 10 | console.log('Current state:', this.state); 11 | } 12 | 13 | API 14 | --- 15 | 16 | .. js:function:: state() 17 | 18 | Get the current overall state. 19 | 20 | :returns: 21 | Promise that resolves to an object representing the current state. 22 | Keys represent names of the state key. 23 | 24 | Usage: 25 | 26 | .. sourcecode:: js 27 | 28 | const state = await thing.state(); 29 | console.log('State is', state); 30 | console.log('Value of power is', state.power); 31 | 32 | Events 33 | ------ 34 | 35 | .. js:data:: stateChanged 36 | 37 | State has changed for the thing. 38 | 39 | .. sourcecode:: js 40 | 41 | thing.on('stateChanged', change => 42 | console.log('Key', change.key, 'changed to', change.value) 43 | ); 44 | 45 | Protected methods 46 | ----------------- 47 | 48 | .. js:function:: getState(key[, defaultValue]) 49 | 50 | Get the current value of the given state key. 51 | 52 | :param string power: The state key to get value for. 53 | :param defaultValue: Fallback to return if the state key is not set. 54 | :returns: The value for the state key, the default value or ``null``. 55 | 56 | .. js:function:: updateState(key, value) 57 | 58 | Update the state of the given key. This will update the state key and emit 59 | the event ``stateChanged``. 60 | 61 | :param string key: The state key to update. 62 | :param value: The new value of the state key. 63 | :returns: Boolean indicating if the state value has changed. 64 | 65 | 66 | .. js:function:: removeState(key) 67 | 68 | Remove state stored for the given key. Will emit a ``stateChanged`` event. 69 | 70 | :param string key: The state key to remove. 71 | 72 | Implementing capability 73 | ------------------------ 74 | 75 | The ``state``-capability has no functions that need to be implemented. 76 | ``updateState`` can be called at any time to update a state key. 77 | 78 | .. sourcecode:: js 79 | 80 | const { Thing, State } = require('abstract-things'); 81 | 82 | class Example extends Thing.with(State) { 83 | constructor() { 84 | super(); 85 | 86 | this.updateState('key', true); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /polling.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('./thing'); 4 | const { duration } = require('./values'); 5 | 6 | const pollDuration = Symbol('pollDuration'); 7 | const pollTimer = Symbol('pollTimer'); 8 | const maxPollFailures = Symbol('maxpollFailures'); 9 | const pollFailures = Symbol('pollFailures'); 10 | 11 | module.exports = Thing.mixin(Parent => class extends Parent { 12 | 13 | constructor(...args) { 14 | super(...args); 15 | 16 | this[pollDuration] = 30000; 17 | this.internalPoll = this.internalPoll.bind(this); 18 | this[maxPollFailures] = -1; 19 | this[pollFailures] = 0; 20 | } 21 | 22 | updatePollDuration(time) { 23 | time = this[pollDuration] = duration(time).ms; 24 | 25 | if(this[pollTimer]) { 26 | clearTimeout(this[pollTimer]); 27 | 28 | this[pollTimer] = setTimeout(this.internalPoll, time); 29 | } 30 | } 31 | 32 | updateMaxPollFailures(failures) { 33 | this[maxPollFailures] = failures; 34 | } 35 | 36 | initCallback() { 37 | return super.initCallback() 38 | .then(() => { 39 | // During initalization a single poll is performed 40 | return this.internalPoll(true); 41 | }); 42 | } 43 | 44 | destroyCallback() { 45 | return super.destroyCallback() 46 | .then(() => clearTimeout(this[pollTimer])); 47 | } 48 | 49 | internalPoll(isInitial=false) { 50 | const time = Date.now(); 51 | 52 | // Perform poll async - and schedule new poll after it has resolved 53 | return Promise.resolve(this.poll(isInitial)) 54 | .catch(ex => { 55 | this.debug('Could not poll:', ex); 56 | return new Error('Polling issue'); 57 | }) 58 | .then(r => { 59 | // Check if failure 60 | if(r instanceof Error) { 61 | this[pollFailures]++; 62 | if(this[maxPollFailures] > 0 && this[maxPollFailures] <= this[pollFailures]) { 63 | /* 64 | * The maximum number of failures in a row have been 65 | * reached. Destroy this thing. 66 | */ 67 | this.destroy(); 68 | } 69 | } else { 70 | this[pollFailures] = 0; 71 | } 72 | 73 | // Schedule the next poll 74 | const diff = Date.now() - time; 75 | const d = this[pollDuration]; 76 | 77 | let nextTime = d - diff; 78 | while(nextTime < 0) { 79 | nextTime += d; 80 | } 81 | 82 | this[pollTimer] = setTimeout(this.internalPoll, nextTime); 83 | }); 84 | } 85 | 86 | poll(isInitial) { 87 | throw new Error('Poll has not been implemented'); 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /docs/sensors/carbon-monoxide-level.rst: -------------------------------------------------------------------------------- 1 | ``cap:carbon-monoxide-leve`` - read carbon monoxide level 2 | ========================================================== 3 | 4 | This capability is used to mark sensors that report their carbon monoxide level 5 | as PPM (parts per million). The value is reported as a :doc:`number `. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('cap:carbon-monoxide-level')) { 10 | console.log('Carbon monoxide:', await thing.carbonMonoxideLevel()); 11 | 12 | thing.on('carbonMonoxideLevelChanged', v => console.log('Changed to:', v)); 13 | } 14 | 15 | API 16 | --- 17 | 18 | .. js:function:: carbonMonoxideLevel() 19 | 20 | Get the current carbon monoxide levels as PPM. 21 | 22 | :returns: 23 | Promise that resolves to the current value as a 24 | :doc:`number `. 25 | 26 | .. sourcecode:: js 27 | 28 | console.log('CO is:', await thing.carbonMonoxideLevel()); 29 | 30 | .. js:function:: coLevel() 31 | 32 | Get the current carbon monoxide levels as PPM. 33 | 34 | :returns: 35 | Promise that resolves to the current value as a 36 | :doc:`number `. 37 | 38 | .. sourcecode:: js 39 | 40 | console.log('CO is:', await thing.coLvel()); 41 | 42 | Events 43 | ------ 44 | 45 | .. js:data:: carbonMonoxideChanged 46 | 47 | The carbon monoxide level has changed. Payload is the new PPM as a 48 | :doc:`number `. 49 | 50 | Example: 51 | 52 | .. sourcecode:: js 53 | 54 | thing.on('carbonMonoxideChanged', v => console.log('Changed to:', v)); 55 | 56 | Protected methods 57 | ----------------- 58 | 59 | .. js:function:: updateCarbonMonoxideLevel(value) 60 | 61 | Update the current carbon monoxide level. Should be called whenever a change 62 | in PPM is detected. 63 | 64 | :param value: 65 | The new PPM value. Will be converted to a :doc:`number `. 66 | 67 | Example: 68 | 69 | .. sourcecode:: js 70 | 71 | this.updateCarbonMonoxideLevel(0); 72 | 73 | Implementing capability 74 | ----------------------- 75 | 76 | Implementors of this capability should call ``updateCarbonMonoxideLevel`` 77 | whenever the PPM of carbon monoxide changes. 78 | 79 | .. sourcecode:: js 80 | 81 | const { Sensor, CarbonMonoxideLevel } = require('abstract-things/sensors'); 82 | 83 | class Example extends Sensor.with(CarbonMonoxideLevel) { 84 | 85 | constructor() { 86 | super(); 87 | 88 | this.updateCarbonMonoxideLevel(0); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /sensors/contact-detection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Sensor = require('./sensor'); 5 | const { boolean, duration } = require('../values'); 6 | 7 | const idleTimer = Symbol('autoIdle'); 8 | 9 | module.exports = Thing.mixin(Parent => class extends Parent.with(Sensor) { 10 | static get capability() { 11 | return 'contact-detection'; 12 | } 13 | 14 | static availableAPI(builder) { 15 | builder.event('contactDetectedChanged') 16 | .type('boolean') 17 | .description('Change in detected contact') 18 | .done(); 19 | 20 | builder.event('opened') 21 | .description('Contact sensor is open, contact has been lost') 22 | .done(); 23 | 24 | builder.event('closed') 25 | .description('Contact sensor is closed, contact has been detected') 26 | .done(); 27 | 28 | builder.action('contactDetected') 29 | .description('Get if contact is currently detected') 30 | .returns('boolean', 'Current contact status') 31 | .done(); 32 | 33 | builder.action('isOpen') 34 | .description('Get if the contact indicates an open state') 35 | .returns('boolean', 'If open - no contact detected') 36 | .done(); 37 | 38 | builder.action('isClosed') 39 | .description('Get if the contact indicates a cloded state') 40 | .returns('boolean', 'If closed - contact detected') 41 | .done(); 42 | } 43 | 44 | get sensorTypes() { 45 | return [ ...super.sensorTypes, 'contactDetected' ]; 46 | } 47 | 48 | contactDetected() { 49 | return this.value('contactDetected'); 50 | } 51 | 52 | isOpen() { 53 | return this.contactDetected() 54 | .then(v => ! v); 55 | } 56 | 57 | isClosed() { 58 | return this.contactDetected(); 59 | } 60 | 61 | updateContactDetected(contact, autoIdleTimeout=null) { 62 | contact = boolean(contact); 63 | if(this.updateValue('contactDetected', contact)) { 64 | if(contact) { 65 | // Emit the closed event if contact is true 66 | this.emitEvent('closed'); 67 | } else { 68 | // Emit the opened event if contact is false 69 | this.emitEvent('opened'); 70 | } 71 | } 72 | 73 | // Always clear the idle timer 74 | clearTimeout(this[idleTimer]); 75 | 76 | if(contact && autoIdleTimeout) { 77 | /* 78 | * When contact has been detected and automatic idle is requested 79 | * set a timer. 80 | */ 81 | const ms = duration(autoIdleTimeout).ms; 82 | this[idleTimer] = setTimeout(() => this.updateContactDetected(false), ms); 83 | } 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /media/adjustable-volume.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thing = require('../thing'); 4 | const Volume = require('./volume'); 5 | const { percentage, 'percentage:change': change } = require('../values'); 6 | 7 | module.exports = Thing.mixin(Parent => class extends Parent.with(Volume) { 8 | static get capability() { 9 | return 'adjustable-audio-volume'; 10 | } 11 | 12 | static availableAPI(builder) { 13 | builder.action('volume') 14 | .description('Get or set the volume level as a percentage between 0 and 100') 15 | .argument('percentage:change', true, 'Optional volume to set') 16 | .returns('percentage', 'The volume level') 17 | .done(); 18 | 19 | builder.action('setVolume') 20 | .description('Set the volume') 21 | .argument('percentage', false, 'The volume to set') 22 | .returns('percentage', 'The volume level') 23 | .done(); 24 | 25 | builder.action('increaseVolume') 26 | .description('Increase the volume') 27 | .argument('percentage', false, 'The amount to increase the volume') 28 | .returns('percentage', 'The volume level') 29 | .done(); 30 | 31 | builder.action('decreaseVolume') 32 | .description('Decrease the volume') 33 | .argument('percentage', false, 'The amount to decrease the volume') 34 | .returns('percentage', 'The volume level') 35 | .done(); 36 | } 37 | 38 | volume(volume) { 39 | try { 40 | let currentVolume = this.getState('volume'); 41 | 42 | if(typeof volume !== 'undefined') { 43 | volume = change(volume); 44 | 45 | let toSet; 46 | if(volume.isIncrease) { 47 | toSet = currentVolume + volume.value; 48 | } else if(volume.isDecrease) { 49 | toSet = currentVolume - volume.value; 50 | } else { 51 | toSet = volume.value; 52 | } 53 | 54 | return this.setVolume(toSet); 55 | } 56 | 57 | return Promise.resolve(currentVolume); 58 | } catch(ex) { 59 | return Promise.reject(ex); 60 | } 61 | } 62 | 63 | increaseVolume(amount) { 64 | return this.setVolume(Math.min(100, this.state.volume + amount)); 65 | } 66 | 67 | decreaseVolume(amount) { 68 | return this.setVolume(Math.max(0, this.state.volume - amount)); 69 | } 70 | 71 | setVolume(volume) { 72 | try { 73 | volume = percentage(volume, { min: 0, max: 100 }); 74 | 75 | return Promise.resolve(this.changeVolume(volume)) 76 | .then(() => this.state.volume); 77 | } catch(ex) { 78 | return Promise.reject(ex); 79 | } 80 | } 81 | 82 | changeVolume(volume) { 83 | throw new Error('changeVolume has not been implemented'); 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /docs/common/error-state.rst: -------------------------------------------------------------------------------- 1 | ``cap:error-state`` - error reporting 2 | ============================================= 3 | 4 | The ``error-state`` capability is used when a thing can report an error, such 5 | as a humidifier running out of water or a autonomous vacuum getting stuck. 6 | 7 | .. sourcecode:: js 8 | 9 | if(thing.matches('cap:error-state')) { 10 | if(thing.error) { 11 | console.log('Error is:', thing.error); 12 | } 13 | } 14 | 15 | API 16 | --- 17 | 18 | .. js:function:: error() 19 | 20 | Get the current error or ``null`` if no error. 21 | 22 | :returns: 23 | Promise that resolves to a :doc:`code ` if the thing is 24 | currently in an error state, or ``null`` if no error state. 25 | 26 | Example: 27 | 28 | .. sourcecode:: js 29 | 30 | thing.error() 31 | .then(err => ...) 32 | .catch(...); 33 | 34 | const error = await thing.error(); 35 | 36 | Events 37 | ------ 38 | 39 | .. describe:: errorChanged 40 | 41 | The current error has changed. The payload will be the current error state 42 | as a :doc:`code ` or ``null``. 43 | 44 | Example: 45 | 46 | .. sourcecode:: js 47 | 48 | thing.on('errorChanged', error => console.log('Error state:', error)); 49 | 50 | .. describe:: error 51 | 52 | Emitted when an error occurs. The payload will be the error. 53 | 54 | Example: 55 | 56 | .. sourcecode:: js 57 | 58 | thing.on('error', error => console.log('Error occured:', error)); 59 | 60 | .. describe:: errorCleared 61 | 62 | Emitted when the thing no longer has an error. 63 | 64 | Example: 65 | 66 | .. sourcecode:: js 67 | 68 | thing.on('errorCleared', () => console.log('Thing no longer has an error')); 69 | 70 | Protected methods 71 | ----------------- 72 | 73 | .. js:function:: updateError(batteryLevel) 74 | 75 | Update the current error state. 76 | 77 | :param code error: 78 | The new error state as a :doc:`code ` or ``null`` if 79 | no error. 80 | 81 | Example: 82 | 83 | .. sourcecode:: js 84 | 85 | this.updateError('some-error'); 86 | this.updateError(null); 87 | 88 | Implementing capability 89 | ----------------------- 90 | 91 | When implementing this capability the implementor needs to call 92 | ``updateError`` whenever an error state is entered or left. 93 | 94 | .. sourcecode:: js 95 | 96 | const { Thing, ErrorState } = require('abstract-things'); 97 | 98 | class Example extends Thing.with(ErrorState) { 99 | 100 | } 101 | --------------------------------------------------------------------------------