Repository: mozilla-iot/webthing-node
Branch: master
Commit: 4a6d1e237301
Files: 44
Total size: 127.8 KB
Directory structure:
gitextract_p46l49r7/
├── .dockerignore
├── .eslintignore
├── .eslintrc.js
├── .github/
│ └── workflows/
│ ├── build.yml
│ ├── projects.yml
│ └── release.yml
├── .gitignore
├── .npmignore
├── .prettierrc.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── docker-compose.yml
├── example/
│ ├── multiple-things.js
│ ├── package.json
│ ├── platform/
│ │ ├── Makefile
│ │ ├── adc/
│ │ │ └── adc-property.js
│ │ ├── board/
│ │ │ ├── artik1020.js
│ │ │ ├── artik530.js
│ │ │ ├── edison.js
│ │ │ ├── flex-phat.js
│ │ │ ├── play-phat.js
│ │ │ └── traffic-phat.js
│ │ ├── gpio/
│ │ │ └── gpio-property.js
│ │ ├── package.json
│ │ └── pwm/
│ │ └── pwm-property.js
│ ├── simplest-thing.js
│ └── single-thing.js
├── package.json
├── src/
│ ├── action.ts
│ ├── event.ts
│ ├── index.ts
│ ├── property.ts
│ ├── server.ts
│ ├── thing.ts
│ ├── types.ts
│ ├── utils.ts
│ ├── value.ts
│ └── webthing.ts
├── test.sh
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
Dockerfile
.git/
#.gitignore contents:
*~
*.swp
*.tgz
node_modules/
================================================
FILE: .eslintignore
================================================
**/node_modules/
/.eslintrc.js
tmp/
lib/*.js
lib/*.d.ts
index.js
index.d.ts
webthing.js
webthing.d.ts
*.eslintrc.js
================================================
FILE: .eslintrc.js
================================================
module.exports = {
'env': {
'browser': true,
'commonjs': true,
'es6': true,
'jasmine': true,
'jest': true,
'mocha': true,
'node': true
},
'extends': [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/@typescript-eslint'
],
'parser': '@typescript-eslint/parser',
'parserOptions': {
'sourceType': 'module'
},
'plugins': [
'@typescript-eslint'
],
'rules': {
'arrow-parens': [
'error',
'always'
],
'arrow-spacing': 'error',
'block-scoped-var': 'error',
'block-spacing': [
'error',
'always'
],
'@typescript-eslint/brace-style': [
'error',
'1tbs'
],
'@typescript-eslint/comma-dangle': [
'error',
'always-multiline'
],
'@typescript-eslint/comma-spacing': 'error',
'comma-style': [
'error',
'last'
],
'computed-property-spacing': [
'error',
'never'
],
'curly': 'error',
'@typescript-eslint/default-param-last': 'error',
'dot-notation': 'error',
'eol-last': 'error',
'@typescript-eslint/explicit-module-boundary-types': [
'warn',
{
'allowArgumentsExplicitlyTypedAsAny': true
}
],
'@typescript-eslint/explicit-function-return-type': [
'error',
{
'allowExpressions': true
}
],
'@typescript-eslint/func-call-spacing': [
'error',
'never'
],
'@typescript-eslint/indent': [
'error',
2,
{
'ArrayExpression': 'first',
'CallExpression': {
'arguments': 'first'
},
'FunctionDeclaration': {
'parameters': 'first'
},
'FunctionExpression': {
'parameters': 'first'
},
'ObjectExpression': 'first',
'SwitchCase': 1
}
],
'key-spacing': [
'error',
{
'afterColon': true,
'beforeColon': false,
'mode': 'strict'
}
],
'@typescript-eslint/keyword-spacing': 'off',
'linebreak-style': [
'error',
'unix'
],
'@typescript-eslint/lines-between-class-members': [
'error',
'always'
],
'max-len': [
'error',
100
],
'@typescript-eslint/member-delimiter-style': [
'error',
{
'singleline': {
'delimiter': 'semi',
'requireLast': false
},
'multiline': {
'delimiter': 'semi',
'requireLast': true
}
}
],
'multiline-ternary': [
'error',
'always-multiline'
],
'no-console': 0,
'@typescript-eslint/no-duplicate-imports': 'error',
'no-eval': 'error',
'@typescript-eslint/no-explicit-any': [
'error',
{
'ignoreRestArgs': true
}
],
'no-floating-decimal': 'error',
'no-implicit-globals': 'error',
'no-implied-eval': 'error',
'no-lonely-if': 'error',
'no-multi-spaces': [
'error',
{
'ignoreEOLComments': true
}
],
'no-multiple-empty-lines': 'error',
'@typescript-eslint/no-namespace': [
'error',
{
'allowDeclarations': true
}
],
'@typescript-eslint/no-non-null-assertion': 'off',
'no-prototype-builtins': 'off',
'no-return-assign': 'error',
'no-script-url': 'error',
'no-self-compare': 'error',
'no-sequences': 'error',
'no-shadow-restricted-names': 'error',
'no-tabs': 'error',
'no-throw-literal': 'error',
'no-trailing-spaces': 'error',
'no-undefined': 'error',
'no-unmodified-loop-condition': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
'argsIgnorePattern': '^_',
'varsIgnorePattern': '^_'
}
],
'no-useless-computed-key': 'error',
'no-useless-concat': 'error',
'@typescript-eslint/no-useless-constructor': 'error',
'no-useless-return': 'error',
'no-var': 'error',
'no-void': 'error',
'no-whitespace-before-property': 'error',
'object-curly-newline': [
'error',
{
'consistent': true
}
],
'object-curly-spacing': [
'error',
'always'
],
'object-property-newline': [
'error',
{
'allowMultiplePropertiesPerLine': true
}
],
'operator-linebreak': [
'error',
'after',
{
'overrides': {
'?': 'before',
':': 'before'
}
}
],
'padded-blocks': [
'error',
{
'blocks': 'never'
}
],
'prefer-const': 'error',
'@typescript-eslint/prefer-for-of': 'error',
'prefer-template': 'error',
'quote-props': [
'error',
'as-needed'
],
'@typescript-eslint/quotes': [
'error',
'single',
{
'allowTemplateLiterals': true
}
],
'@typescript-eslint/semi': [
'error',
'always'
],
'semi-spacing': [
'error',
{
'after': true,
'before': false
}
],
'semi-style': [
'error',
'last'
],
'space-before-blocks': [
'error',
'always'
],
'@typescript-eslint/space-before-function-paren': [
'error',
{
'anonymous': 'always',
'asyncArrow': 'always',
'named': 'never'
}
],
'space-in-parens': [
'error',
'never'
],
'@typescript-eslint/space-infix-ops': 'error',
'space-unary-ops': [
'error',
{
'nonwords': false,
'words': true
}
],
'spaced-comment': [
'error',
'always',
{
'block': {
'balanced': true,
'exceptions': [
'*'
]
}
}
],
'switch-colon-spacing': [
'error',
{
'after': true,
'before': false
}
],
'template-curly-spacing': [
'error',
'never'
],
'@typescript-eslint/type-annotation-spacing': 'error',
'yoda': 'error'
},
'overrides': [
{
'files': [
'example/**/*.js'
],
'rules': {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-var-requires': 'off'
}
}
]
};
================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on:
pull_request:
branches:
- master
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [
10,
12,
14,
]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: |
npm ci
- name: Check formatting
run: |
npx prettier -c 'src/*.ts' 'example/**/*.{js,ts}'
- name: Lint with eslint
run: |
npm run lint
- name: Transpile ts files
run: |
npm run build
- name: Run integration tests
run: |
./test.sh
================================================
FILE: .github/workflows/projects.yml
================================================
name: Add new issues to the specified project column
on:
issues:
types: [opened]
jobs:
add-new-issues-to-project-column:
runs-on: ubuntu-latest
steps:
- name: add-new-issues-to-organization-based-project-column
uses: docker://takanabe/github-actions-automate-projects:v0.0.1
env:
GITHUB_TOKEN: ${{ secrets.CI_TOKEN }}
GITHUB_PROJECT_URL: https://github.com/orgs/WebThingsIO/projects/4
GITHUB_PROJECT_COLUMN_NAME: To do
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: 'https://registry.npmjs.org'
- name: Set release version
run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV
- name: Create Release
id: create_release
uses: actions/create-release@v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ env.RELEASE_VERSION }}
draft: false
prerelease: false
- name: Build project
run: |
npm ci
npm run lint
npm run build
env:
CI: true
- name: Publish to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
**/iotjs_modules
**/node_modules
*.swp
*.tgz
*~
.#*
index.d.ts
index.d.ts.map
index.js
index.js.map
lib/*.js
lib/*.js.map
lib/*.d.ts
lib/*.d.ts.map
npm-debug.log
tmp/
webthing.d.ts
webthing.d.ts.map
webthing.js
webthing.js.map
example/package-lock.json
================================================
FILE: .npmignore
================================================
.eslintrc.js
test-server.js
*.ts
!*.d.ts
================================================
FILE: .prettierrc.json
================================================
{
"printWidth": 100,
"singleQuote": true
}
================================================
FILE: CHANGELOG.md
================================================
# webthing Changelog
## [Unreleased]
## [0.15.0] - 2021-01-05
### Added
- Parameter to disable host validation in server.
## [0.14.0] - 2020-12-14
### Changed
- Converted project to TypeScript.
## [0.13.1] - 2020-11-28
### Fixed
- Test fixes.
## [0.13.0] - 2020-09-23
### Changed
- Update author and URLs to indicate new project home.
## [0.12.3] - 2020-06-18
### Changed
- mDNS record now indicates TLS support.
## [0.12.2] - 2020-05-04
### Changed
- Invalid POST requests to action resources now generate an error status.
## [0.12.1] - 2020-03-27
### Changed
- Updated dependencies.
## [0.12.0] - 2019-07-12
### Changed
- Things now use `title` rather than `name`.
- Things now require a unique ID in the form of a URI.
### Added
- Support for `id`, `base`, `security`, and `securityDefinitions` keys in thing description.
## [0.11.1] - 2019-06-05
### Added
- Ability to set a base URL path on server.
## [0.11.0] - 2019-01-16
### Changed
- WebThingServer constructor can now take a list of additional API routes.
### Fixed
- Properties could not include a custom `links` array at initialization.
## [0.10.0] - 2018-11-30
### Changed
- Property, Action, and Event description now use `links` rather than `href`. - [Spec PR](https://github.com/WebThingsIO/wot/pull/119)
[Unreleased]: https://github.com/WebThingsIO/webthing-node/compare/v0.15.0...HEAD
[0.15.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.14.0...v0.15.0
[0.14.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.13.1...v0.14.0
[0.13.1]: https://github.com/WebThingsIO/webthing-node/compare/v0.13.0...v0.13.1
[0.13.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.12.3...v0.13.0
[0.12.3]: https://github.com/WebThingsIO/webthing-node/compare/v0.12.2...v0.12.3
[0.12.2]: https://github.com/WebThingsIO/webthing-node/compare/v0.12.1...v0.12.2
[0.12.1]: https://github.com/WebThingsIO/webthing-node/compare/v0.12.0...v0.12.1
[0.12.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.11.1...v0.12.0
[0.11.1]: https://github.com/WebThingsIO/webthing-node/compare/v0.11.0...v0.11.1
[0.11.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.9.1...v0.10.0
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Community Participation Guidelines
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
For more details, please read the
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
## How to Report
For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.
<!--
## Project Specific Etiquette
In some cases, there will be additional project etiquette i.e.: (https://bugzilla.mozilla.org/page.cgi?id=etiquette.html).
Please update for your project.
-->
================================================
FILE: Dockerfile
================================================
#!/bin/echo docker build . -f
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MPL-2.0
#{
# Copyright: 2018-present Samsung Electronics France SAS, and other contributors
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/ .
#}
FROM node:10-buster
LABEL maintainer="Philippe Coval (p.coval@samsung.com)"
ENV DEBIAN_FRONTEND noninteractive
ENV LC_ALL en_US.UTF-8
ENV LANG ${LC_ALL}
ENV project webthing-node
COPY . /usr/local/${project}/${project}
WORKDIR /usr/local/${project}/${project}
RUN echo "#log: ${project}: Preparing sources" \
&& set -x \
&& which npm \
&& npm --version \
&& npm install \
&& npm run test || echo "TODO: check package.json" \
&& sync
EXPOSE 8888
WORKDIR /usr/local/${project}/${project}
ENTRYPOINT [ "/usr/local/bin/npm", "run" ]
CMD [ "start" ]
================================================
FILE: LICENSE
================================================
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
================================================
FILE: Makefile
================================================
#!/bin/make -f
# -*- makefile -*-
# SPDX-License-Identifier: MPL-2.0
#{
# Copyright 2018-present Samsung Electronics France SAS, and other contributors
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.*
#}
default: help all
tmp_dir ?= tmp
runtime ?= node
export runtime
eslint ?= node_modules/eslint/bin/eslint.js
tsc ?= node_modules/typescript/bin/tsc
srcs ?= $(wildcard *.ts lib/*.ts | sort | uniq)
run_args ?=
run_timeout ?= 10
main_src ?= example/multiple-things.js
NODE_PATH := .:${NODE_PATH}
export NODE_PATH
port?=8888
url?=http://localhost:${port}
help:
@echo "## Usage: "
@echo "# make start # To start default application"
@echo "# make test # To test default application"
all: build
setup/%:
${@F}
node_modules: package.json
npm install
modules: ${runtime}_modules
ls $<
package-lock.json: package.json
rm -fv "$@"
npm install
ls "$@"
setup/node: node_modules
@echo "NODE_PATH=$${NODE_PATH}"
node --version
npm --version
setup: setup/${runtime}
build/%: setup
@echo "log: $@: $^"
build/node: setup node_modules eslint
build: build/${runtime}
run/%: ${main_src} build
${@F} $< ${run_args}
run/npm: ${main_src} setup
npm start
run: run/${runtime}
clean:
rm -rf ${tmp_dir}
cleanall: clean
rm -f *~
distclean: cleanall
rm -rf node_modules
${tmp_dir}/rule/test/pid/%: ${main_src} build modules
@mkdir -p "${@D}"
${@F} $< & echo $$! > "$@"
sleep ${run_timeout}
cat $@
test/%: ${tmp_dir}/rule/test/pid/%
cat $<
curl ${url} || curl -I ${url}
@echo ""
curl --fail ${url}/0/properties
@echo ""
curl --fail ${url}/1/properties
@echo ""
kill $$(cat $<) ||:
kill -9 $$(cat $<) ||:
test/npm: package.json
npm test
test: test/${runtime}
start: run
start/board/%: example/platform/Makefile example/platform/board/%.js
${MAKE} -C ${<D} board/${@F}
check/%: ${srcs}
${MAKE} setup
@echo "log: SHELL=$${SHELL}"
status=0 ; \
for src in $^; do \
echo "log: check: $${src}: ($@)" ; \
${@F} $${src} \
&& echo "log: check: $${src}: OK" \
|| status=1 ; \
done ; \
exit $${status}
check/npm:
npm run lint
check: check/${runtime}
eslint: .eslintrc.js ${eslint}
@rm -rf tmp/dist
${eslint} --no-color --fix . ||:
${eslint} --no-color .
${tsc}
git diff --exit-code
eslint/setup: node_modules
ls ${eslint} || npm install eslint-plugin-node eslint
${eslint} --version
${eslint}:
ls $@ || make eslint/setup
touch $@
.eslintrc.js: ${eslint}
ls $@ || $< --init
lint/%: eslint
sync
lint: lint/${runtime}
sync
================================================
FILE: README.md
================================================
# webthing
[](https://github.com/WebThingsIO/webthing-node/workflows/Node.js%20package)
[](https://www.npmjs.com/package/webthing)
[](LICENSE)
Implementation of an HTTP [Web Thing](https://iot.mozilla.org/wot/).
# Installation
`webthing` can be installed via `npm`, as such:
```shell
$ npm install webthing
```
# Example
In this example we will set up a dimmable light and a humidity sensor (both using fake data, of course). Both working examples can be found in [here](https://github.com/WebThingsIO/webthing-node/tree/master/example).
## Dimmable Light
Imagine you have a dimmable light that you want to expose via the web of things API. The light can be turned on/off and the brightness can be set from 0% to 100%. Besides the name, description, and type, a [`Light`](https://iot.mozilla.org/schemas/#Light) is required to expose two properties:
* `on`: the state of the light, whether it is turned on or off
* Setting this property via a `PUT {"on": true/false}` call to the REST API toggles the light.
* `brightness`: the brightness level of the light from 0-100%
* Setting this property via a PUT call to the REST API sets the brightness level of this light.
First we create a new Thing:
```javascript
const light = new Thing('urn:dev:ops:my-lamp-1234',
'My Lamp',
['OnOffSwitch', 'Light'],
'A web connected lamp');
```
Now we can add the required properties.
The **`on`** property reports and sets the on/off state of the light. For this, we need to have a `Value` object which holds the actual state and also a method to turn the light on/off. For our purposes, we just want to log the new state if the light is switched on/off.
```javascript
light.addProperty(
new Property(
light,
'on',
new Value(true, (v) => console.log('On-State is now', v)),
{
'@type': 'OnOffProperty',
title: 'On/Off',
type: 'boolean',
description: 'Whether the lamp is turned on',
}));
```
The **`brightness`** property reports the brightness level of the light and sets the level. Like before, instead of actually setting the level of a light, we just log the level.
```javascript
light.addProperty(
new Property(
light,
'brightness',
new Value(50, v => console.log('Brightness is now', v)),
{
'@type': 'BrightnessProperty',
title: 'Brightness',
type: 'number',
description: 'The level of light from 0-100',
minimum: 0,
maximum: 100,
unit: 'percent',
}));
```
Now we can add our newly created thing to the server and start it:
```javascript
// If adding more than one thing, use MultipleThings() with a name.
// In the single thing case, the thing's name will be broadcast.
const server = new WebThingServer(SingleThing(light), 8888);
process.on('SIGINT', () => {
server.stop().then(() => process.exit()).catch(() => process.exit());
});
server.start().catch(console.error);
```
This will start the server, making the light available via the WoT REST API and announcing it as a discoverable resource on your local network via mDNS.
## Sensor
Let's now also connect a humidity sensor to the server we set up for our light.
A [`MultiLevelSensor`](https://iot.mozilla.org/schemas/#MultiLevelSensor) (a sensor that returns a level instead of just on/off) has one required property (besides the name, type, and optional description): **`level`**. We want to monitor this property and get notified if the value changes.
First we create a new Thing:
```javascript
const sensor = new Thing('urn:dev:ops:my-humidity-sensor-1234',
'My Humidity Sensor',
['MultiLevelSensor'],
'A web connected humidity sensor');
```
Then we create and add the appropriate property:
* `level`: tells us what the sensor is actually reading
* Contrary to the light, the value cannot be set via an API call, as it wouldn't make much sense, to SET what a sensor is reading. Therefore, we are creating a *readOnly* property.
```javascript
const level = new Value(0.0);
sensor.addProperty(
new Property(
sensor,
'level',
level,
{
'@type': 'LevelProperty',
title: 'Humidity',
type: 'number',
description: 'The current humidity in %',
minimum: 0,
maximum: 100,
unit: 'percent',
readOnly: true,
}));
```
Now we have a sensor that constantly reports 0%. To make it usable, we need a thread or some kind of input when the sensor has a new reading available. For this purpose we start a thread that queries the physical sensor every few seconds. For our purposes, it just calls a fake method.
```javascript
// Poll the sensor reading every 3 seconds
setInterval(() => {
// Update the underlying value, which in turn notifies all listeners
level.notifyOfExternalUpdate(readFromGPIO());
}, 3000);
```
This will update our `Value` object with the sensor readings via the `this.level.notifyOfExternalUpdate(readFromGPIO());` call. The `Value` object now notifies the property and the thing that the value has changed, which in turn notifies all websocket listeners.
# Adding to Gateway
To add your web thing to the WebThings Gateway, install the "Web Thing" add-on and follow the instructions [here](https://github.com/WebThingsIO/thing-url-adapter#readme).
# Resources
* https://iot.mozilla.org/things/
* https://hacks.mozilla.org/2018/05/creating-web-things-with-python-node-js-and-java/
* https://nodejs.org/en/
* https://github.com/rzr/webthing-iotjs/wiki
* https://youtu.be/Z-oiFl6gwGw
================================================
FILE: docker-compose.yml
================================================
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MPL-2.0
#{
# Copyright: 2018-present Samsung Electronics France SAS, and other contributors
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/ .
#}
version: "2"
services:
web:
build: .
command: start
volumes:
- /sys:/sys
ports:
- "8888:8888"
network_mode: "host"
================================================
FILE: example/multiple-things.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
const {
Action,
Event,
MultipleThings,
Property,
Thing,
Value,
WebThingServer,
} = require('webthing');
const { v4: uuidv4 } = require('uuid');
class OverheatedEvent extends Event {
constructor(thing, data) {
super(thing, 'overheated', data);
}
}
class FadeAction extends Action {
constructor(thing, input) {
super(uuidv4(), thing, 'fade', input);
}
performAction() {
return new Promise((resolve) => {
setTimeout(() => {
this.thing.setProperty('brightness', this.input.brightness);
this.thing.addEvent(new OverheatedEvent(this.thing, 102));
resolve();
}, this.input.duration);
});
}
}
/**
* A dimmable light that logs received commands to stdout.
*/
class ExampleDimmableLight extends Thing {
constructor() {
super('urn:dev:ops:my-lamp-1234', 'My Lamp', ['OnOffSwitch', 'Light'], 'A web connected lamp');
this.addProperty(
new Property(this, 'on', new Value(true, (v) => console.log('On-State is now', v)), {
'@type': 'OnOffProperty',
title: 'On/Off',
type: 'boolean',
description: 'Whether the lamp is turned on',
})
);
this.addProperty(
new Property(this, 'brightness', new Value(50, (v) => console.log('Brightness is now', v)), {
'@type': 'BrightnessProperty',
title: 'Brightness',
type: 'integer',
description: 'The level of light from 0-100',
minimum: 0,
maximum: 100,
unit: 'percent',
})
);
this.addAvailableAction(
'fade',
{
title: 'Fade',
description: 'Fade the lamp to a given level',
input: {
type: 'object',
required: ['brightness', 'duration'],
properties: {
brightness: {
type: 'integer',
minimum: 0,
maximum: 100,
unit: 'percent',
},
duration: {
type: 'integer',
minimum: 1,
unit: 'milliseconds',
},
},
},
},
FadeAction
);
this.addAvailableEvent('overheated', {
description: 'The lamp has exceeded its safe operating temperature',
type: 'number',
unit: 'degree celsius',
});
}
}
/**
* A humidity sensor which updates its measurement every few seconds.
*/
class FakeGpioHumiditySensor extends Thing {
constructor() {
super(
'urn:dev:ops:my-humidity-sensor-1234',
'My Humidity Sensor',
['MultiLevelSensor'],
'A web connected humidity sensor'
);
this.level = new Value(0.0);
this.addProperty(
new Property(this, 'level', this.level, {
'@type': 'LevelProperty',
title: 'Humidity',
type: 'number',
description: 'The current humidity in %',
minimum: 0,
maximum: 100,
unit: 'percent',
readOnly: true,
})
);
// Poll the sensor reading every 3 seconds
setInterval(() => {
// Update the underlying value, which in turn notifies all listeners
const newLevel = this.readFromGPIO();
console.log('setting new humidity level:', newLevel);
this.level.notifyOfExternalUpdate(newLevel);
}, 3000);
}
/**
* Mimic an actual sensor updating its reading every couple seconds.
*/
readFromGPIO() {
return Math.abs(70.0 * Math.random() * (-0.5 + Math.random()));
}
}
function runServer() {
// Create a thing that represents a dimmable light
const light = new ExampleDimmableLight();
// Create a thing that represents a humidity sensor
const sensor = new FakeGpioHumiditySensor();
// If adding more than one thing, use MultipleThings() with a name.
// In the single thing case, the thing's name will be broadcast.
const server = new WebThingServer(
new MultipleThings([light, sensor], 'LightAndTempDevice'),
8888
);
process.on('SIGINT', () => {
server
.stop()
.then(() => process.exit())
.catch(() => process.exit());
});
server.start().catch(console.error);
}
runServer();
================================================
FILE: example/package.json
================================================
{
"name": "example",
"version": "1.0.0",
"author": "WebThingsIO",
"license": "MPL-2.0",
"dependencies": {
"webthing": "file:.."
}
}
================================================
FILE: example/platform/Makefile
================================================
#!/bin/make -f
# -*- makefile -*-
# SPDX-License-Identifier: MPL-2.0
#{
# Copyright 2018-present Samsung Electronics France SAS, and other contributors
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.*
#}
default: help all
main_src ?= index.js
lib_srcs ?= $(wildcard */*.js | sort)
runtime ?= node
topreldir ?= ../..
topdir ?= ${CURDIR}/${topreldir}
run_args ?=
npm_args ?= start
sudo ?= sudo
gpio ?= gpio
tmp_dir ?= tmp
export PATH
NODE_PATH := ${topreldir}:${NODE_PATH}
export NODE_PATH
rpi_gpio ?= 11
rpi_gpio_list ?= 13 19 26
edison_gpio ?= 12
help:
@echo "Usage:"
@echo '# make start'
@echo '# make $${board}'
@echo '# make artik1020'
@echo '# make edison'
@echo '# make flex-phat'
@echo '# make play-phat'
@echo '# make traffic-phat'
%: %/${runtime}
echo "# $@: $^"
all: check
setup/node: ${topreldir}/node_modules node_modules
@echo "NODE_PATH=$${NODE_PATH}"
@echo "$@: $^"
run/%: /sys/class/gpio/export ${main_src} setup
ls -l $<
-which ${@F}
${@F} ${main_src} ${run_args}
sudo/run/%: /sys/class/gpio/export ${main_src} setup
ls -l $<
-which ${@F}
${sudo} env "PATH=${PATH}" ${@F} ${main_src} ${run_args}
run/npm: /sys/class/gpio/export package.json setup
ls -l $<
npm run ${npm_args} ${run_args}
run: run/${runtime}
sync
start: run
sync
force:
/sys/kernel/debug/gpio:
${sudo} ls -l $@
/sys/class/gpio/export: /sys/kernel/debug/gpio force
${sudo} cat $<
node_modules: package.json
-which npm
npm --version
npm install
@mkdir -p "$@"
ln -fs ../../.. ${@}/webthing
package.json:
npm init
${topreldir}/node_modules: ${topreldir}/package.json
cd ${@D} && npm install
check/%: ${lib_srcs}
${MAKE} setup
status=0 ; \
for src in $^; do \
echo "log: check: $${src}: ($@)" ; \
${@F} $${src} \
&& echo "log: check: $${src}: OK" \
|| status=1 ; \
done ; \
exit $${status}
check: check/${runtime}
board/%: ${main_src} board/%.js /sys/class/gpio/export setup
${runtime} $< ${@F}
/sys/class/gpio/gpio${edison_gpio}: /sys/class/gpio/export
echo ${edison_gpio} | ${sudo} tee $<
ls -l $@
artik1020/%: ${main_src}
${MAKE} sudo/run/${@F} run_args="${@D}"
edison/%: /sys/class/gpio/gpio${edison_gpio} ${main_src}
echo out | ${sudo} tee ${<}/direction
echo 0 | ${sudo} tee ${<}/value
${sudo} cat /sys/kernel/debug/gpio_debug/gpio${edison_gpio}/current_pinmux # mode0
echo mode1 | ${sudo} tee /sys/kernel/debug/gpio_debug/gpio${edison_gpio}/current_pinmux
${MAKE} sudo/run/${@F} run_args="${@D}"
gpio: /sys/class/gpio/export
-${gpio} -v || ${sudo} apt-get install gpio || echo "TODO: install BCM tool"
-${gpio} -v
flex-phat/%: ${main_src} gpio
${gpio} -g mode ${rpi_gpio} up
${MAKE} run/${@F} run_args="${@D}"
play-phat/%: ${main_src} /sys/class/gpio/export
-lsmod | grep gpio_keys \
&& ${sudo} modprobe -rv gpio_keys \
|| echo "log: will use /sys/class/gpio/"
${MAKE} run/${@F} run_args="${@D}"
traffic-phat/%: ${main_src} gpio
for num in ${rpi_gpio_list} ; do \
${sudo} ${gpio} export $${num} in ; \
${sudo} ${gpio} -g mode $${num} up ; \
${sudo} ${gpio} unexport $${num} ; \
done
${MAKE} run/${@F} run_args="${@D}"
================================================
FILE: example/platform/adc/adc-property.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
/**
*
* Copyright 2018-present Samsung Electronics France SAS, and other contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.*
*/
const console = require('console');
// Disable logs here by editing to '!console.log'
const log = console.log || function () {};
const verbose = !console.log || function () {};
const { Property, Value } = require('webthing');
const adc = require('../adc');
class AdcInProperty extends Property {
constructor(thing, name, value, metadata, config) {
super(thing, name, new Value(Number(value)), {
'@type': 'LevelProperty',
title: (metadata && metadata.title) || `Level: ${name}`,
type: 'number',
readOnly: true,
description: (metadata && metadata.description) || `ADC Sensor on pin=${config.pin}`,
});
const self = this;
config.frequency = config.frequency || 1;
config.range = config.range || 0xfff;
this.period = 1000.0 / config.frequency;
this.config = config;
this.port = adc.open(config, (err) => {
log(`log: ADC: ${self.getName()}: open: ${err} (null expected)`);
if (err) {
console.error(`error: ADC: ${self.getName()}: Fail to open:\
${config.pin}`);
return null;
}
self.inverval = setInterval(() => {
let value = Number(self.port.readSync());
verbose(`log: ADC:\
${self.getName()}: update: 0x${value.toString(0xf)}`);
value = Number(Math.floor((100.0 * value) / self.config.range));
if (value !== self.lastValue) {
log(`log: ADC: ${self.getName()}: change: ${value}%`);
self.value.notifyOfExternalUpdate(value);
self.lastValue = value;
}
}, self.period);
});
}
close() {
try {
this.inverval && clearInterval(this.inverval);
this.port && this.port.closeSync();
} catch (err) {
console.error(`error: ADC: ${this.getName()} close:${err}`);
return err;
}
log(`log: ADC: ${this.getName()}: close:`);
}
}
function AdcProperty(thing, name, value, metadata, config) {
if (config.direction === 'in') {
return new AdcInProperty(thing, name, value, metadata, config);
}
throw 'error: Invalid param';
}
module.exports = AdcProperty;
================================================
FILE: example/platform/board/artik1020.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
/**
*
* Copyright 2018-present Samsung Electronics France SAS, and other contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/
*/
const { Thing } = require('webthing');
const AdcProperty = require('../adc/adc-property');
const PwmProperty = require('../pwm/pwm-property');
class ARTIK1020Thing extends Thing {
constructor(name, type, description) {
super(
'urn:dev:ops:my-artik1020-1234',
name || 'ARTIK1020',
type || [],
description || 'A web connected ARTIK1020'
);
const self = this;
this.pinProperties = [
new AdcProperty(
this,
'ADC0',
0,
{ description: 'A0 on J24 of board' },
{ direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\
/in_voltage0_raw' }
),
new AdcProperty(
this,
'ADC1',
0,
{ description: 'A1 on J24 of board' },
{ direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\
/in_voltage1_raw' }
),
new AdcProperty(
this,
'ADC2',
0,
{ description: 'A2 on J24 of board' },
{ direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\
/in_voltage2_raw' }
),
new AdcProperty(
this,
'ADC3',
0,
{ description: 'A3 on J24 of board' },
{ direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\
/in_voltage5_raw' }
),
new AdcProperty(
this,
'ADC4',
0,
{ description: 'A4 on J24 of board' },
{ direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\
/in_voltage6_raw' }
),
new AdcProperty(
this,
'ADC5',
0,
{ description: 'A5 on J24 of board' },
{ direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\
/in_voltage7_raw' }
),
new PwmProperty(this, 'PWM0', 50, { description: 'XPWMO1 on J26[6] of board (pwm0)' }),
new PwmProperty(
this,
'PWM1',
50,
{ description: 'XPWMO0 on J26[5] of board (pwm1)' },
{ pwm: { pin: 1 } }
),
];
this.pinProperties.forEach((property) => {
self.addProperty(property);
});
}
close() {
this.pinProperties.forEach((property) => {
property.close && property.close();
});
}
}
module.exports = function () {
if (!module.exports.instance) {
module.exports.instance = new ARTIK1020Thing();
}
return module.exports.instance;
};
================================================
FILE: example/platform/board/artik530.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
/**
*
* Copyright 2018-present Samsung Electronics France SAS, and other contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.*
*/
const { Thing } = require('webthing');
const AdcProperty = require('../adc/adc-property');
const GpioProperty = require('../gpio/gpio-property');
class ARTIK530Thing extends Thing {
constructor(name, type, description) {
super(
'urn:dev:ops:my-artik530-1234',
name || 'ARTIK530',
type || [],
description || 'A web connected ARTIK530 or ARTIK720'
);
const self = this;
this.pinProperties = [
new GpioProperty(
this,
'RedLED',
false,
{ description: 'Red LED on interposer board (on GPIO28)' },
{ direction: 'out', pin: 28 }
),
new GpioProperty(
this,
'BlueLED',
false,
{ description: 'Blue LED on interposer board (on GPIO38)' },
{ direction: 'out', pin: 38 }
),
new GpioProperty(
this,
'Up',
true,
{ description: 'SW403 Button: Nearest board edge,\
next to red LED (on GPIO30)' },
{ direction: 'in', pin: 30 }
),
new GpioProperty(
this,
'Down',
true,
{ description: 'SW404 Button: Next to blue LED (on GPIO32)' },
{ direction: 'in', pin: 32 }
),
new AdcProperty(
this,
'ADC0',
0,
{ description: 'Analog port of ARTIK05x' },
{
direction: 'in',
device: '/sys/bus/platform/devices\
/c0053000.adc/iio:device0/in_voltage0_raw',
}
),
new AdcProperty(
this,
'ADC1',
0,
{ description: 'Analog port of ARTIK05x' },
{
direction: 'in',
device: '/sys/bus/platform/devices/\
c0053000.adc/iio:device0/in_voltage1_raw',
}
),
];
this.pinProperties.forEach((property) => {
self.addProperty(property);
});
}
close() {
this.pinProperties.forEach((property) => {
property.close && property.close();
});
}
}
module.exports = function () {
if (!module.exports.instance) {
module.exports.instance = new ARTIK530Thing();
}
return module.exports.instance;
};
================================================
FILE: example/platform/board/edison.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
/**
*
* Copyright 2018-present Samsung Electronics France SAS, and other contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.*
*/
const { Thing } = require('webthing');
const PwmProperty = require('../pwm/pwm-property');
class EdisonThing extends Thing {
constructor(name, type, description) {
super(
'urn:dev:ops:my-edison-1234',
name || 'Edison',
type || [],
description || 'A web connected Edison'
);
const self = this;
this.pinProperties = [
new PwmProperty(this, 'PWM0', 50, {
description: 'Analog port of Edison',
}),
];
this.pinProperties.forEach((property) => {
self.addProperty(property);
});
}
close() {
this.pinProperties.forEach((property) => {
property.close && property.close();
});
}
}
module.exports = function () {
if (!module.exports.instance) {
module.exports.instance = new EdisonThing();
}
return module.exports.instance;
};
================================================
FILE: example/platform/board/flex-phat.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
/**
*
* Copyright 2018-present Samsung Electronics France SAS, and other contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.*
*/
const { Thing } = require('webthing');
const GpioProperty = require('../gpio/gpio-property');
class FlexPHatThing extends Thing {
constructor(name, type, description) {
super(
'urn:dev:ops:my-flex-phat-1234',
name || 'FlexPHat',
type || [],
description || 'A web connected Flex RaspberryPi Hat'
);
const self = this;
this.gpioProperties = [
new GpioProperty(
this,
'Relay',
false,
{ description: 'Actuator (on GPIO5)' },
{ direction: 'out', pin: 5 }
),
new GpioProperty(
this,
'BlueLED',
false,
{ description: 'Actuator (on GPIO13)' },
{ direction: 'out', pin: 13 }
),
new GpioProperty(
this,
'GreenLED',
false,
{ description: 'Actuator (on GPIO19)' },
{ direction: 'out', pin: 19 }
),
new GpioProperty(
this,
'RedLED',
false,
{ description: 'Actuator (on GPIO26)' },
{ direction: 'out', pin: 26 }
),
new GpioProperty(
this,
'Button',
false,
{ description: 'Push Button (on GPIO11)' },
{ direction: 'in', pin: 11 }
),
new GpioProperty(
this,
'GPIO23',
false,
{ description: 'Input on GPIO 23 (unwired but modable)' },
{ direction: 'in', pin: 23 }
),
];
this.gpioProperties.forEach((property) => {
self.addProperty(property);
});
}
close() {
this.gpioProperties.forEach((property) => {
property.close && property.close();
});
}
}
module.exports = function () {
if (!module.exports.instance) {
module.exports.instance = new FlexPHatThing();
}
return module.exports.instance;
};
================================================
FILE: example/platform/board/play-phat.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
/**
*
* Copyright 2018-present Samsung Electronics France SAS, and other contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.*
*/
const { Thing } = require('webthing');
const GpioProperty = require('../gpio/gpio-property');
class PlayPHatThing extends Thing {
constructor(name, type, description) {
super(
'urn:dev:ops:my-play-phat-1234',
name || 'PlayPHat',
type || [],
description || 'A web connected Play RaspberryPi Hat'
);
const self = this;
this.gpioProperties = [
new GpioProperty(
this,
'Left',
false,
{ description: 'SW1 Sensor Button on GPIO4 (Pin7)' },
{ direction: 'in', pin: 4 }
),
new GpioProperty(
this,
'Right',
false,
{ description: 'SW2 Sensor button on GPIO17 (Pin11)' },
{ direction: 'in', pin: 17 }
),
new GpioProperty(
this,
'Up',
false,
{ description: 'SW3 Sensor button on GPIO22 (Pin15)' },
{ direction: 'in', pin: 22 }
),
new GpioProperty(
this,
'Down',
false,
{ description: 'SW4 Sensor button on GPIO27 (Pin13)' },
{ direction: 'in', pin: 27 }
),
new GpioProperty(
this,
'A',
false,
{ description: 'SW5 Sensor button on GPIO19 (Pin35)' },
{ direction: 'in', pin: 19 }
),
new GpioProperty(
this,
'B',
false,
{ description: 'SW6 Sensor button on GPIO26 (Pin37)' },
{ direction: 'in', pin: 26 }
),
new GpioProperty(
this,
'Start',
false,
{ description: 'SW7 Sensor button on GPIO5 (Pin29)' },
{ direction: 'in', pin: 5 }
),
new GpioProperty(
this,
'Select',
false,
{ description: 'SW8 Sensor button on GPIO6 (Pin31)' },
{ direction: 'in', pin: 6 }
),
];
this.gpioProperties.forEach((property) => {
self.addProperty(property);
});
}
close() {
this.gpioProperties.forEach((property) => {
property.close && property.close();
});
}
}
module.exports = function () {
if (!module.exports.instance) {
module.exports.instance = new PlayPHatThing();
}
return module.exports.instance;
};
================================================
FILE: example/platform/board/traffic-phat.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
/**
*
* Copyright 2018-present Samsung Electronics France SAS, and other contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.*
*/
const { Thing } = require('webthing');
const GpioProperty = require('../gpio/gpio-property');
class TrafficPHatThing extends Thing {
constructor(name, type, description) {
super(
'urn:dev:ops:my-traffic-phat-1234',
name || 'TrafficPHat',
type || [],
description || 'A web connected Traffic RaspberryPi Hat'
);
const self = this;
this.pinProperties = [
new GpioProperty(
this,
'Red',
false,
{
description: 'LED on GPIO2 (Pin2)',
},
{
direction: 'out',
pin: 2,
}
),
new GpioProperty(
this,
'Orange',
false,
{
description: 'LED on GPIO3 (Pin5)',
},
{
direction: 'out',
pin: 3,
}
),
new GpioProperty(
this,
'Green',
false,
{
description: 'LED on GPIO4 (Pin7)',
},
{
direction: 'out',
pin: 4,
}
),
new GpioProperty(
this,
'B1',
true,
{
description: 'SW1 Sensor Button on GPIO3 (Pin33)',
},
{
direction: 'in',
pin: 13,
}
),
new GpioProperty(
this,
'B2',
true,
{
description: 'SW2 Sensor button on GPIO19 (Pin35)',
},
{
direction: 'in',
pin: 19,
}
),
new GpioProperty(
this,
'B3',
true,
{
description: 'SW3 Sensor button on GPIO26 (Pin37)',
},
{
direction: 'in',
pin: 26,
}
),
];
this.pinProperties.forEach((property) => {
self.addProperty(property);
});
}
close() {
this.pinProperties.forEach((property) => {
property.close && property.close();
});
}
}
module.exports = function () {
if (!module.exports.instance) {
module.exports.instance = new TrafficPHatThing();
}
return module.exports.instance;
};
================================================
FILE: example/platform/gpio/gpio-property.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
/**
*
* Copyright 2018-present Samsung Electronics France SAS, and other contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.*
*/
const console = require('console');
// Disable logs here by editing to '!console.log'
const log = console.log || function () {};
const { Property, Value } = require('webthing');
const gpio = require('gpio');
class GpioOutProperty extends Property {
constructor(thing, name, value, metadata, config) {
super(thing, name, new Value(Boolean(value)), {
'@type': 'OnOffProperty',
title: (metadata && metadata.title) || `On/Off: ${name}`,
type: 'boolean',
description: (metadata && metadata.description) || `GPIO Actuator on pin=${config.pin}`,
});
const self = this;
this.config = config;
this.port = gpio.export(config.pin, {
direction: 'out',
ready: () => {
log(`log: GPIO: ${self.getName()}: open:`);
self.value.valueForwarder = (value) => {
try {
log(`log: GPIO: ${self.getName()}: \
writing: ${value}`);
self.port.set(value);
} catch (err) {
console.error(`error: GPIO:
${self.getName()}: Fail to write: ${err}`);
return err;
}
};
},
});
}
close() {
try {
this.port && this.port.unexport(this.config.pin);
} catch (err) {
console.error(`error: GPIO: ${this.getName()}: Fail to close: ${err}`);
return err;
}
log(`log: GPIO: ${this.getName()}: close:`);
}
}
class GpioInProperty extends Property {
constructor(thing, name, value, metadata, config) {
super(thing, name, new Value(Boolean(value)), {
'@type': 'BooleanProperty',
title: (metadata && metadata.title) || `On/Off: ${name}`,
type: 'boolean',
readOnly: true,
description: (metadata && metadata.description) || `GPIO Sensor on pin=${config.pin}`,
});
const self = this;
this.config = config;
const callback = () => {
log(`log: GPIO: ${self.getName()}: open:`);
self.port.on('change', (value) => {
value = Boolean(value);
log(`log: GPIO: ${self.getName()}: change: ${value}`);
self.value.notifyOfExternalUpdate(value);
});
};
this.port = gpio.export(config.pin, { direction: 'in', ready: callback });
}
close() {
try {
this.port && this.port.unexport(this.config.pin);
} catch (err) {
console.error(`error: GPIO: ${this.getName()} close:${err}`);
return err;
}
log(`log: GPIO: ${this.getName()}: close:`);
}
}
function GpioProperty(thing, name, value, metadata, config) {
if (config.direction === 'out') {
return new GpioOutProperty(thing, name, value, metadata, config);
} else if (config.direction === 'in') {
return new GpioInProperty(thing, name, value, metadata, config);
}
throw 'error: Invalid param';
}
module.exports = GpioProperty;
================================================
FILE: example/platform/package.json
================================================
{
"name": "board-webthings",
"version": "0.0.0",
"description": "Various Single Board computers's I/O implemented as webthings",
"main": "index.js",
"scripts": {
"start": "NODE_PATH=.:../.. node index",
"flex-phat": "NODE_PATH=.:../.. node index flex-phat",
"play-phat": "NODE_PATH=.:../.. node index play-phat"
},
"author": "Philippe Coval <p.coval@samsung.com>",
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/rzr/webthing-iotjs.git"
},
"keywords": [
"webthing",
"wot",
"raspberry-pi",
"ARTIK"
],
"bugs": {
"url": "https://github.com/rzr/webthing-iotjs/issues"
},
"bin": {
"board-webthings": "./index.js"
},
"homepage": "https://github.com/rzr/webthing-iotjs",
"dependencies": {},
"optionalDependencies": {
"gpio": "^0.2.10",
"pwm": "0.0.3"
}
}
================================================
FILE: example/platform/pwm/pwm-property.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
/**
*
* Copyright 2018-present Samsung Electronics France SAS, and other contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/
*/
const console = require('console');
// Disable logs here by editing to '!console.log'
const log = console.log || function () {};
const verbose = !console.log || function () {};
const { Property, Value } = require('webthing');
const pwm = require('pwm');
class PwmOutProperty extends Property {
constructor(thing, name, value, metadata, config) {
if (typeof config === 'undefined') {
config = {};
}
super(thing, name, new Value(Number(value)), {
'@type': 'LevelProperty',
title: (metadata && metadata.title) || `PWM: ${name} (dutyCycle)`,
type: 'integer',
minimum: config.minimum || 0,
maximum: config.maximum || 100,
readOnly: false,
unit: 'percent',
description: (metadata && metadata.description) || `PWM DutyCycle`,
});
const self = this;
this.config = config;
if (typeof this.config.pwm == 'undefined') {
this.config.pwm = {};
}
if (typeof this.config.pwm.pin == 'undefined') {
this.config.pwm.pin = 0;
}
if (typeof this.config.pwm.chip == 'undefined') {
this.config.pwm.chip = 0;
}
// secs (eg: 50Hz = 20 ms = 0.02 sec)
if (typeof this.config.pwm.period == 'undefined') {
this.config.pwm.period = 0.02;
}
// [0..1]
if (typeof this.config.pwm.dutyCycle == 'undefined') {
this.config.pwm.dutyCycle = 0.5;
}
verbose(`log: opening: ${this.getName()}`);
this.port = pwm.export(this.config.pwm.chip, this.config.pwm.pin, (err) => {
verbose(`log: PWM: ${self.getName()}: open: ${err}`);
if (err) {
console.error(`error: PWM: ${self.getName()}: open: ${err}`);
throw err;
}
self.port.freq = 1 / self.config.pwm.period;
// Linux sysfs uses usecs units
self.port.setPeriod(self.config.pwm.period * 1e9, () => {
self.port.setDutyCycle(self.config.pwm.dutyCycle * (self.config.pwm.period * 1e9), () => {
self.port.setEnable(1, () => {
verbose(`log: ${self.getName()}: Enabled`);
});
});
});
self.value.valueForwarder = (value) => {
const usec = Math.floor(self.config.pwm.period * 1e9 * (Number(value) / 100.0));
self.port.setDutyCycle(usec, () => {
verbose(`log: setDutyCycle: usec=${usec}`);
});
};
});
}
close() {
verbose(`log: PWM: ${this.getName()}: close:`);
try {
this.port && this.port.unexport();
} catch (err) {
console.error(`error: PWM: ${this.getName()} close:${err}`);
return err;
}
log(`log: PWM: ${this.getName()}: close:`);
}
}
module.exports = PwmOutProperty;
if (module.parent === null) {
new PwmOutProperty();
}
================================================
FILE: example/simplest-thing.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
/**
*
* Copyright 2018-present Samsung Electronics France SAS, and other contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.*
*/
const { Property, SingleThing, Thing, Value, WebThingServer } = require('webthing');
function makeThing() {
const thing = new Thing(
'urn:dev:ops:my-actuator-1234',
'ActuatorExample',
['OnOffSwitch'],
'An actuator example that just log'
);
thing.addProperty(
new Property(thing, 'on', new Value(true, (update) => console.log(`change: ${update}`)), {
'@type': 'OnOffProperty',
title: 'On/Off',
type: 'boolean',
description: 'Whether the output is changed',
})
);
return thing;
}
function runServer() {
const port = process.argv[2] ? Number(process.argv[2]) : 8888;
const url = `http://localhost:${port}/properties/on`;
console.log(`Usage:\n
${process.argv[0]} ${process.argv[1]} [port]
Try:
curl -X PUT -H 'Content-Type: application/json' --data '{"on": true }' ${url}
`);
const thing = makeThing();
const server = new WebThingServer(new SingleThing(thing), port);
process.on('SIGINT', () => {
server
.stop()
.then(() => process.exit())
.catch(() => process.exit());
});
server.start().catch(console.error);
}
runServer();
================================================
FILE: example/single-thing.js
================================================
// -*- mode: js; js-indent-level:2; -*-
// SPDX-License-Identifier: MPL-2.0
const { Action, Event, Property, SingleThing, Thing, Value, WebThingServer } = require('webthing');
const { v4: uuidv4 } = require('uuid');
class OverheatedEvent extends Event {
constructor(thing, data) {
super(thing, 'overheated', data);
}
}
class FadeAction extends Action {
constructor(thing, input) {
super(uuidv4(), thing, 'fade', input);
}
performAction() {
return new Promise((resolve) => {
setTimeout(() => {
this.thing.setProperty('brightness', this.input.brightness);
this.thing.addEvent(new OverheatedEvent(this.thing, 102));
resolve();
}, this.input.duration);
});
}
}
function makeThing() {
const thing = new Thing(
'urn:dev:ops:my-lamp-1234',
'My Lamp',
['OnOffSwitch', 'Light'],
'A web connected lamp'
);
thing.addProperty(
new Property(thing, 'on', new Value(true), {
'@type': 'OnOffProperty',
title: 'On/Off',
type: 'boolean',
description: 'Whether the lamp is turned on',
})
);
thing.addProperty(
new Property(thing, 'brightness', new Value(50), {
'@type': 'BrightnessProperty',
title: 'Brightness',
type: 'integer',
description: 'The level of light from 0-100',
minimum: 0,
maximum: 100,
unit: 'percent',
})
);
thing.addAvailableAction(
'fade',
{
title: 'Fade',
description: 'Fade the lamp to a given level',
input: {
type: 'object',
required: ['brightness', 'duration'],
properties: {
brightness: {
type: 'integer',
minimum: 0,
maximum: 100,
unit: 'percent',
},
duration: {
type: 'integer',
minimum: 1,
unit: 'milliseconds',
},
},
},
},
FadeAction
);
thing.addAvailableEvent('overheated', {
description: 'The lamp has exceeded its safe operating temperature',
type: 'number',
unit: 'degree celsius',
});
return thing;
}
function runServer() {
const thing = makeThing();
// If adding more than one thing, use MultipleThings() with a name.
// In the single thing case, the thing's name will be broadcast.
const server = new WebThingServer(new SingleThing(thing), 8888);
process.on('SIGINT', () => {
server
.stop()
.then(() => process.exit())
.catch(() => process.exit());
});
server.start().catch(console.error);
}
runServer();
================================================
FILE: package.json
================================================
{
"name": "webthing",
"version": "0.15.0",
"description": "HTTP Web Thing implementation",
"main": "lib/webthing.js",
"scripts": {
"lint": "tsc --noEmit && eslint . --ext .ts",
"node": "NODE_PATH=. node",
"start": "NODE_PATH=. node example/multiple-things",
"test": "make test",
"simplest": "NODE_PATH=. node example/simplest-thing",
"prettier": "npx prettier -w 'src/*.ts' 'example/**/*.{js,ts}'",
"build": "tsc -p ."
},
"repository": {
"type": "git",
"url": "git+https://github.com/WebThingsIO/webthing-node.git"
},
"keywords": [
"iot",
"web",
"thing",
"webthing"
],
"author": "WebThingsIO",
"license": "MPL-2.0",
"bugs": {
"url": "https://github.com/WebThingsIO/webthing-node/issues"
},
"homepage": "https://github.com/WebThingsIO/webthing-node#readme",
"types": "lib/index.d.ts",
"dependencies": {
"ajv": "^7.0.4",
"body-parser": "^1.19.0",
"dnssd": "^0.4.1",
"express": "^4.17.1",
"express-ws": "^4.0.0",
"prettier": "^2.2.1"
},
"devDependencies": {
"@types/body-parser": "^1.19.0",
"@types/dnssd": "^0.4.1",
"@types/express": "^4.17.11",
"@types/express-ws": "^3.0.0",
"@types/node": "^14.14.25",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.14.2",
"babel-eslint": "^10.1.0",
"eslint": "^7.19.0",
"eslint-config-prettier": "^7.2.0",
"typescript": "^4.1.3",
"uuid": "^8.3.2"
}
}
================================================
FILE: src/action.ts
================================================
/**
* High-level Action base class implementation.
*/
import * as utils from './utils';
import { AnyType, Link, PrimitiveJsonType } from './types';
import Thing from './thing';
/**
* An Action represents an individual action on a thing.
*/
class Action<InputType = AnyType> {
private id: string;
private thing: Thing;
private name: string;
private input: InputType;
private hrefPrefix: string;
private href: string;
private status: string;
private timeRequested: string;
private timeCompleted: string | null;
constructor(id: string, thing: Thing, name: string, input: InputType) {
/**
* Initialize the object.
*
* @param {String} id ID of this action
* @param {Object} thing Thing this action belongs to
* @param {String} name Name of the action
* @param {Object} input Any action inputs
*/
this.id = id;
this.thing = thing;
this.name = name;
this.input = input;
this.hrefPrefix = '';
this.href = `/actions/${this.name}/${this.id}`;
this.status = 'created';
this.timeRequested = utils.timestamp();
this.timeCompleted = null;
}
/**
* Get the action description.
*
* @returns {Object} Description of the action as an object.
*/
asActionDescription(): Action.ActionDescription {
const description: Action.ActionDescription = {
[this.name]: {
href: this.hrefPrefix + this.href,
timeRequested: this.timeRequested,
status: this.status,
},
};
if (this.input !== null) {
description[this.name].input = <AnyType>(<unknown>this.input);
}
if (this.timeCompleted !== null) {
description[this.name].timeCompleted = this.timeCompleted;
}
return description;
}
/**
* Set the prefix of any hrefs associated with this action.
*
* @param {String} prefix The prefix
*/
setHrefPrefix(prefix: string): void {
this.hrefPrefix = prefix;
}
/**
* Get this action's ID.
*
* @returns {String} The ID.
*/
getId(): string {
return this.id;
}
/**
* Get this action's name.
*
* @returns {String} The name.
*/
getName(): string {
return this.name;
}
/**
* Get this action's href.
*
* @returns {String} The href.
*/
getHref(): string {
return this.hrefPrefix + this.href;
}
/**
* Get this action's status.
*
* @returns {String} The status.
*/
getStatus(): string {
return this.status;
}
/**
* Get the thing associated with this action.
*
* @returns {Object} The thing.
*/
getThing(): Thing {
return this.thing;
}
/**
* Get the time the action was requested.
*
* @returns {String} The time.
*/
getTimeRequested(): string {
return this.timeRequested;
}
/**
* Get the time the action was completed.
*
* @returns {String} The time.
*/
getTimeCompleted(): string | null {
return this.timeCompleted;
}
/**
* Get the inputs for this action.
*
* @returns {Object} The inputs.
*/
getInput(): InputType {
return this.input;
}
/**
* Start performing the action.
*/
start(): void {
this.status = 'pending';
this.thing.actionNotify(<Action<AnyType>>(<unknown>this));
this.performAction().then(
() => this.finish(),
() => this.finish()
);
}
/**
* Override this with the code necessary to perform the action.
*
* @returns {Object} Promise that resolves when the action is finished.
*/
performAction(): Promise<void> {
return Promise.resolve();
}
/**
* Override this with the code necessary to cancel the action.
*
* @returns {Object} Promise that resolves when the action is cancelled.
*/
cancel(): Promise<void> {
return Promise.resolve();
}
/**
* Finish performing the action.
*/
finish(): void {
this.status = 'completed';
this.timeCompleted = utils.timestamp();
this.thing.actionNotify(<Action<AnyType>>(<unknown>this));
}
}
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Action {
interface ActionMetadata {
title?: string;
description?: string;
links?: Link[];
input?: {
type?: PrimitiveJsonType;
minimum?: number;
maximum?: number;
multipleOf?: number;
enum?: readonly string[] | readonly number[];
};
}
interface ActionDescription<InputType = AnyType> {
[name: string]: {
href: string;
timeRequested: string;
status: string;
input?: InputType;
timeCompleted?: string;
};
}
export interface ActionTypeClass<InputType = AnyType> {
new (thing: Thing, input: InputType): Action<InputType>;
}
}
export = Action;
================================================
FILE: src/event.ts
================================================
/**
* High-level Event base class implementation.
*/
import Thing from './thing';
import * as utils from './utils';
import { AnyType, PrimitiveJsonType, Link } from './types';
/**
* An Event represents an individual event from a thing.
*/
class Event<Data = AnyType> {
private thing: Thing;
private name: string;
private data: Data | null;
private time: string;
/**
* Initialize the object.
*
* @param {Object} thing Thing this event belongs to
* @param {String} name Name of the event
* @param {*} data (Optional) Data associated with the event
*/
constructor(thing: Thing, name: string, data?: Data) {
this.thing = thing;
this.name = name;
this.data = typeof data !== 'undefined' ? data : null;
this.time = utils.timestamp();
}
/**
* Get the event description.
*
* @returns {Object} Description of the event as an object.
*/
asEventDescription(): Event.EventDescription {
const description: Event.EventDescription = {
[this.name]: {
timestamp: this.time,
},
};
if (this.data !== null) {
description[this.name].data = <AnyType>(<unknown>this.data);
}
return description;
}
/**
* Get the thing associated with this event.
*
* @returns {Object} The thing.
*/
getThing(): Thing {
return this.thing;
}
/**
* Get the event's name.
*
* @returns {String} The name.
*/
getName(): string {
return this.name;
}
/**
* Get the event's data.
*
* @returns {*} The data.
*/
getData(): Data | null {
return this.data;
}
/**
* Get the event's timestamp.
*
* @returns {String} The time.
*/
getTime(): string {
return this.time;
}
}
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Event {
interface EventDescription {
[name: string]: {
timestamp: string;
data?: AnyType;
};
}
interface EventMetadata {
type?: PrimitiveJsonType;
'@type'?: string;
unit?: string;
title?: string;
description?: string;
links?: Link[];
minimum?: number;
maximum?: number;
multipleOf?: number;
enum?: readonly string[] | readonly number[];
}
}
export = Event;
================================================
FILE: src/index.ts
================================================
export * from './webthing';
================================================
FILE: src/property.ts
================================================
/**
* High-level Property base class implementation.
*/
import Ajv, { ValidateFunction } from 'ajv';
import Thing from './thing';
import Value from './value';
import { AnyType, PrimitiveJsonType, Link } from './types';
const ajv = new Ajv();
/**
* A Property represents an individual state value of a thing.
*/
class Property<ValueType = AnyType> {
private thing: Thing;
private name: string;
private value: Value<ValueType>;
private metadata: Property.PropertyMetadata;
private href: string;
private hrefPrefix: string;
private validate: ValidateFunction;
/**
* Initialize the object.
*
* @param {Thing} thing Thing this property belongs to
* @param {String} name Name of the property
* @param {Value} value Value object to hold the property value
* @param {Object} metadata Property metadata, i.e. type, description, unit,
* etc., as an object.
*/
constructor(
thing: Thing,
name: string,
value: Value<ValueType>,
metadata: Property.PropertyMetadata
) {
this.thing = thing;
this.name = name;
this.value = value;
this.hrefPrefix = '';
this.href = `/properties/${this.name}`;
this.metadata = JSON.parse(JSON.stringify(metadata || {}));
delete metadata.title;
delete metadata.unit;
delete metadata['@type'];
this.validate = ajv.compile(metadata);
// Add the property change observer to notify the Thing about a property
// change.
this.value.on('update', () => this.thing.propertyNotify(<Property<AnyType>>(<unknown>this)));
}
/**
* Validate new property value before setting it.
*
* @param {*} value - New value
* @throws Error if the property is readonly or is invalid
*/
validateValue(value: ValueType): void {
if (this.metadata.hasOwnProperty('readOnly') && this.metadata.readOnly) {
throw new Error('Read-only property');
}
const valid = this.validate(value);
if (!valid) {
throw new Error('Invalid property value');
}
}
/**
* Get the property description.
*
* @returns {Object} Description of the property as an object.
*/
asPropertyDescription(): Property.PropertyDescription {
const description = JSON.parse(JSON.stringify(this.metadata));
if (!description.hasOwnProperty('links')) {
description.links = [];
}
description.links.push({
rel: 'property',
href: this.hrefPrefix + this.href,
});
return description;
}
/**
* Set the prefix of any hrefs associated with this property.
*
* @param {String} prefix The prefix
*/
setHrefPrefix(prefix: string): void {
this.hrefPrefix = prefix;
}
/**
* Get the href of this property.
*
* @returns {String} The href
*/
getHref(): string {
return `${this.hrefPrefix}${this.href}`;
}
/**
* Get the current property value.
*
* @returns {*} The current value
*/
getValue(): ValueType {
return this.value.get();
}
/**
* Set the current value of the property.
*
* @param {*} value The value to set
*/
setValue(value: ValueType): void {
this.validateValue(value);
this.value.set(value);
}
/**
* Get the name of this property.
*
* @returns {String} The property name.
*/
getName(): string {
return this.name;
}
/**
* Get the thing associated with this property.
*
* @returns {Object} The thing.
*/
getThing(): Thing {
return this.thing;
}
/**
* Get the metadata associated with this property
*
* @returns {Object} The metadata
*/
getMetadata(): Property.PropertyMetadata {
return this.metadata;
}
}
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Property {
// could we use .type to strongly type the enum, minimum and maximum?
interface PropertyMetadata {
type?: PrimitiveJsonType;
'@type'?: string;
unit?: string;
title?: string;
description?: string;
links?: Link[];
enum?: AnyType[];
readOnly?: boolean;
minimum?: number;
maximum?: number;
multipleOf?: number;
}
interface PropertyDescription extends PropertyMetadata {
links: Link[];
}
}
export = Property;
================================================
FILE: src/server.ts
================================================
/**
* Node Web Thing server implementation.
*/
import bodyParser from 'body-parser';
import * as dnssd from 'dnssd';
import express from 'express';
import expressWs from 'express-ws';
import * as http from 'http';
import * as https from 'https';
import * as os from 'os';
import * as utils from './utils';
import Thing from './thing';
import { AnyType } from './types';
/**
* A container for a single thing.
*/
export class SingleThing {
private thing: Thing;
/**
* Initialize the container.
*
* @param {Object} thing The thing to store
*/
constructor(thing: Thing) {
this.thing = thing;
}
/**
* Get the thing at the given index.
*/
getThing(): Thing {
return this.thing;
}
/**
* Get the list of things.
*/
getThings(): Thing[] {
return [this.thing];
}
/**
* Get the mDNS server name.
*/
getName(): string {
return this.thing.getTitle();
}
}
/**
* A container for multiple things.
*/
export class MultipleThings {
private things: Thing[];
private name: string;
/**
* Initialize the container.
*
* @param {Object} things The things to store
* @param {String} name The mDNS server name
*/
constructor(things: Thing[], name: string) {
this.things = things;
this.name = name;
}
/**
* Get the thing at the given index.
*
* @param {Number|String} idx The index
*/
getThing(idx?: number | string): Thing | null {
idx = parseInt(idx as string);
if (isNaN(idx) || idx < 0 || idx >= this.things.length) {
return null;
}
return this.things[idx];
}
/**
* Get the list of things.
*/
getThings(): Thing[] {
return this.things;
}
/**
* Get the mDNS server name.
*/
getName(): string {
return this.name;
}
}
/**
* Base handler that is initialized with a list of things.
*/
abstract class BaseHandler {
protected things: SingleThing | MultipleThings;
/**
* Initialize the handler.
*
* @param {Object} things List of Things managed by the server
*/
constructor(things: SingleThing | MultipleThings) {
this.things = things;
}
abstract get(req: express.Request, res: express.Response): void;
/**
* Get the thing this request is for.
*
* @param {Object} req The request object
* @returns {Object} The thing, or null if not found.
*/
getThing(req: express.Request): Thing | null {
return this.things.getThing(req.params.thingId);
}
}
/**
* Handle a request to / when the server manages multiple things.
*/
class ThingsHandler extends BaseHandler {
/**
* Handle a GET request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
get(req: express.Request, res: express.Response): void {
const wsHref = `${req.secure ? 'wss' : 'ws'}://${req.headers.host}`;
res.json(
this.things.getThings().map((thing) => {
const description = thing.asThingDescription();
description.href = thing.getHref();
description.links.push({
rel: 'alternate',
href: `${wsHref}${thing.getHref()}`,
});
description.base = `${req.protocol}://${req.headers.host}${thing.getHref()}`;
description.securityDefinitions = {
nosec_sc: {
scheme: 'nosec',
},
};
description.security = 'nosec_sc';
return description;
})
);
}
}
/**
* Handle a request to /.
*/
class ThingHandler extends BaseHandler {
/**
* Handle a GET request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
get(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
const wsHref = `${req.secure ? 'wss' : 'ws'}://${req.headers.host}`;
const description = thing.asThingDescription();
description.links.push({
rel: 'alternate',
href: `${wsHref}${thing.getHref()}`,
});
description.base = `${req.protocol}://${req.headers.host}${thing.getHref()}`;
description.securityDefinitions = {
nosec_sc: {
scheme: 'nosec',
},
};
description.security = 'nosec_sc';
res.json(description);
}
/**
* Handle a websocket request.
*
* @param {Object} ws The websocket object
* @param {Object} req The request object
*/
ws(ws: import('ws'), req: express.Request): void {
const thing = this.getThing(req);
if (thing === null) {
ws.send(
JSON.stringify({
messageType: 'error',
data: {
status: '404 Not Found',
message: 'The requested thing was not found',
},
})
);
return;
}
thing.addSubscriber(ws);
ws.on('error', () => thing.removeSubscriber(ws));
ws.on('close', () => thing.removeSubscriber(ws));
ws.on('message', (msg) => {
let message: {
messageType: string;
data: Record<string, unknown>;
};
try {
message = JSON.parse(msg as string);
} catch (e1) {
try {
ws.send(
JSON.stringify({
messageType: 'error',
data: {
status: '400 Bad Request',
message: 'Parsing request failed',
},
})
);
} catch (e2) {
// do nothing
}
return;
}
if (!message.hasOwnProperty('messageType') || !message.hasOwnProperty('data')) {
try {
ws.send(
JSON.stringify({
messageType: 'error',
data: {
status: '400 Bad Request',
message: 'Invalid message',
},
})
);
} catch (e) {
// do nothing
}
return;
}
const messageType = message.messageType;
switch (messageType) {
case 'setProperty': {
for (const propertyName in message.data) {
try {
thing.setProperty(propertyName, <AnyType>message.data[propertyName]);
} catch (e) {
ws.send(
JSON.stringify({
messageType: 'error',
data: {
status: '400 Bad Request',
message: e.message,
},
})
);
}
}
break;
}
case 'requestAction': {
for (const actionName in message.data) {
let input = null;
const actionData = <Record<string, unknown>>message.data[actionName];
if (actionData.hasOwnProperty('input')) {
input = actionData.input;
}
const action = thing.performAction(actionName, input);
if (action) {
action.start();
} else {
ws.send(
JSON.stringify({
messageType: 'error',
data: {
status: '400 Bad Request',
message: 'Invalid action request',
request: message,
},
})
);
}
}
break;
}
case 'addEventSubscription': {
for (const eventName in message.data) {
thing.addEventSubscriber(eventName, ws);
}
break;
}
default: {
try {
ws.send(
JSON.stringify({
messageType: 'error',
data: {
status: '400 Bad Request',
message: `Unknown messageType: ${messageType}`,
request: message,
},
})
);
} catch (e) {
// do nothing
}
}
}
});
}
}
/**
* Handle a request to /properties.
*/
class PropertiesHandler extends BaseHandler {
/**
* Handle a GET request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
get(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
res.json(thing.getProperties());
}
}
/**
* Handle a request to /properties/<property>.
*/
class PropertyHandler extends BaseHandler {
/**
* Handle a GET request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
get(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
const propertyName = req.params.propertyName;
if (thing.hasProperty(propertyName)) {
res.json({ [propertyName]: thing.getProperty(propertyName) });
} else {
res.status(404).end();
}
}
/**
* Handle a PUT request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
put(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
const propertyName = req.params.propertyName;
if (!req.body.hasOwnProperty(propertyName)) {
res.status(400).end();
return;
}
if (thing.hasProperty(propertyName)) {
try {
thing.setProperty(propertyName, req.body[propertyName]);
} catch (e) {
res.status(400).end();
return;
}
res.json({ [propertyName]: thing.getProperty(propertyName) });
} else {
res.status(404).end();
}
}
}
/**
* Handle a request to /actions.
*/
class ActionsHandler extends BaseHandler {
/**
* Handle a GET request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
get(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
res.json(thing.getActionDescriptions());
}
/**
* Handle a POST request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
post(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
const keys = Object.keys(req.body);
if (keys.length !== 1) {
res.status(400).end();
return;
}
const actionName = keys[0];
let input = null;
if (req.body[actionName].hasOwnProperty('input')) {
input = req.body[actionName].input;
}
const action = thing.performAction(actionName, input);
if (action) {
const response = action.asActionDescription();
action.start();
res.status(201);
res.json(response);
} else {
res.status(400).end();
}
}
}
/**
* Handle a request to /actions/<action_name>.
*/
class ActionHandler extends BaseHandler {
/**
* Handle a GET request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
get(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
const actionName = req.params.actionName;
res.json(thing.getActionDescriptions(actionName));
}
/**
* Handle a POST request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
post(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
const actionName = req.params.actionName;
const keys = Object.keys(req.body);
if (keys.length !== 1) {
res.status(400).end();
return;
}
if (keys[0] !== actionName) {
res.status(400).end();
return;
}
let input = null;
if (req.body[actionName].hasOwnProperty('input')) {
input = req.body[actionName].input;
}
const action = thing.performAction(actionName, input);
if (action) {
const response = action.asActionDescription();
action.start();
res.status(201);
res.json(response);
} else {
res.status(400).end();
}
}
}
/**
* Handle a request to /actions/<action_name>/<action_id>.
*/
class ActionIDHandler extends BaseHandler {
/**
* Handle a GET request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
get(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
const actionName = req.params.actionName;
const actionId = req.params.actionId;
const action = thing.getAction(actionName, actionId);
if (action === null) {
res.status(404).end();
return;
}
res.json(action.asActionDescription());
}
/**
* Handle a PUT request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
put(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
// TODO: this is not yet defined in the spec
res.status(200).end();
}
/**
* Handle a DELETE request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
delete(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
const actionName = req.params.actionName;
const actionId = req.params.actionId;
if (thing.removeAction(actionName, actionId)) {
res.status(204).end();
} else {
res.status(404).end();
}
}
}
/**
* Handle a request to /events.
*/
class EventsHandler extends BaseHandler {
/**
* Handle a GET request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
get(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
res.json(thing.getEventDescriptions());
}
}
/**
* Handle a request to /events/<event_name>.
*/
class EventHandler extends BaseHandler {
/**
* Handle a GET request.
*
* @param {Object} req The request object
* @param {Object} res The response object
*/
get(req: express.Request, res: express.Response): void {
const thing = this.getThing(req);
if (thing === null) {
res.status(404).end();
return;
}
const eventName = req.params.eventName;
res.json(thing.getEventDescriptions(eventName));
}
}
/**
* Server to represent a Web Thing over HTTP.
*/
export class WebThingServer {
things: SingleThing | MultipleThings;
name: string;
port: number;
hostname: string | null;
basePath: string;
disableHostValidation: boolean;
hosts: string[];
app: express.Express & { isTls?: boolean };
// HACK because the express types are weird
server: http.Server | https.Server;
router: expressWs.Router;
mdns!: dnssd.Advertisement;
/**
* Initialize the WebThingServer.
*
* For documentation on the additional route handlers, see:
* http://expressjs.com/en/4x/api.html#app.use
*
* @param {Object} things Things managed by this server -- should be of type
* SingleThing or MultipleThings
* @param {Number} port Port to listen on (defaults to 80)
* @param {String} hostname Optional host name, i.e. mything.com
* @param {Object} sslOptions SSL options to pass to the express server
* @param {Object[]} additionalRoutes List of additional routes to add to
* server, i.e. [{path: '..', handler: ..}]
* @param {String} basePath Base URL path to use, rather than '/'
* @param {Boolean} disableHostValidation Whether or not to disable host
* validation -- note that this can
* lead to DNS rebinding attacks
*/
constructor(
things: SingleThing | MultipleThings,
port: number | null = null,
hostname: string | null = null,
sslOptions: https.ServerOptions | null = null,
additionalRoutes: Record<string, express.RequestHandler>[] | null = null,
basePath = '/',
disableHostValidation = false
) {
this.things = things;
this.name = things.getName();
this.port = Number(port) || (sslOptions ? 443 : 80);
this.hostname = hostname;
this.basePath = basePath.replace(/\/$/, '');
this.disableHostValidation = !!disableHostValidation;
const systemHostname = os.hostname().toLowerCase();
this.hosts = [
'localhost',
`localhost:${port}`,
`${systemHostname}.local`,
`${systemHostname}.local:${port}`,
];
utils.getAddresses().forEach((address) => {
this.hosts.push(address, `${address}:${port}`);
});
if (hostname) {
hostname = hostname.toLowerCase();
this.hosts.push(hostname, `${hostname}:${port}`);
}
if (things instanceof MultipleThings) {
const list = things.getThings();
for (let i = 0; i < list.length; i++) {
const thing = list[i];
thing.setHrefPrefix(`${this.basePath}/${i}`);
}
} else {
things.getThing().setHrefPrefix(this.basePath);
}
this.app = express();
this.app.use(bodyParser.json());
// Validate Host header
this.app.use((request, response, next: () => unknown) => {
const host = request.headers.host;
if (this.disableHostValidation || (host && this.hosts.includes(host.toLowerCase()))) {
next();
} else {
response.status(403).send('Forbidden');
}
});
// Set CORS headers
this.app.use((_request, response, next) => {
response.setHeader('Access-Control-Allow-Origin', '*');
response.setHeader(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
response.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, PUT, POST, DELETE');
next();
});
if (sslOptions) {
this.server = https.createServer(sslOptions);
this.app.isTls = true;
} else {
this.server = http.createServer();
this.app.isTls = false;
}
expressWs(this.app, this.server);
const thingsHandler = new ThingsHandler(this.things);
const thingHandler = new ThingHandler(this.things);
const propertiesHandler = new PropertiesHandler(this.things);
const propertyHandler = new PropertyHandler(this.things);
const actionsHandler = new ActionsHandler(this.things);
const actionHandler = new ActionHandler(this.things);
const actionIdHandler = new ActionIDHandler(this.things);
const eventsHandler = new EventsHandler(this.things);
const eventHandler = new EventHandler(this.things);
this.router = express.Router();
if (Array.isArray(additionalRoutes)) {
for (const route of additionalRoutes) {
this.router.use(route.path, route.handler);
}
}
if (this.things instanceof MultipleThings) {
this.router.get('/', (req, res) => thingsHandler.get(req, res));
this.router.get('/:thingId', (req, res) => thingHandler.get(req, res));
this.router.ws('/:thingId', (ws, req) => thingHandler.ws(ws, req));
this.router.get('/:thingId/properties', (req, res) => propertiesHandler.get(req, res));
this.router.get('/:thingId/properties/:propertyName', (req, res) =>
propertyHandler.get(req, res)
);
this.router.put('/:thingId/properties/:propertyName', (req, res) =>
propertyHandler.put(req, res)
);
this.router.get('/:thingId/actions', (req, res) => actionsHandler.get(req, res));
this.router.post('/:thingId/actions', (req, res) => actionsHandler.post(req, res));
this.router.get('/:thingId/actions/:actionName', (req, res) => actionHandler.get(req, res));
this.router.post('/:thingId/actions/:actionName', (req, res) => actionHandler.post(req, res));
this.router.get('/:thingId/actions/:actionName/:actionId', (req, res) =>
actionIdHandler.get(req, res)
);
this.router.put('/:thingId/actions/:actionName/:actionId', (req, res) =>
actionIdHandler.put(req, res)
);
this.router.delete('/:thingId/actions/:actionName/:actionId', (req, res) =>
actionIdHandler.delete(req, res)
);
this.router.get('/:thingId/events', (req, res) => eventsHandler.get(req, res));
this.router.get('/:thingId/events/:eventName', (req, res) => eventHandler.get(req, res));
} else {
this.router.get('/', (req, res) => thingHandler.get(req, res));
this.router.ws('/', (ws, req) => thingHandler.ws(ws, req));
this.router.get('/properties', (req, res) => propertiesHandler.get(req, res));
this.router.get('/properties/:propertyName', (req, res) => propertyHandler.get(req, res));
this.router.put('/properties/:propertyName', (req, res) => propertyHandler.put(req, res));
this.router.get('/actions', (req, res) => actionsHandler.get(req, res));
this.router.post('/actions', (req, res) => actionsHandler.post(req, res));
this.router.get('/actions/:actionName', (req, res) => actionHandler.get(req, res));
this.router.post('/actions/:actionName', (req, res) => actionHandler.post(req, res));
this.router.get('/actions/:actionName/:actionId', (req, res) =>
actionIdHandler.get(req, res)
);
this.router.put('/actions/:actionName/:actionId', (req, res) =>
actionIdHandler.put(req, res)
);
this.router.delete('/actions/:actionName/:actionId', (req, res) =>
actionIdHandler.delete(req, res)
);
this.router.get('/events', (req, res) => eventsHandler.get(req, res));
this.router.get('/events/:eventName', (req, res) => eventHandler.get(req, res));
}
this.app.use(this.basePath || '/', this.router);
this.server.on('request', this.app);
}
/**
* Start listening for incoming connections.
*
* @returns {Promise} Promise which resolves once the server is started.
*/
start(): Promise<void> {
const opts: dnssd.Options = {
name: this.name,
txt: {
path: '/',
},
};
if (this.app.isTls) {
opts.txt.tls = '1';
}
this.mdns = new dnssd.Advertisement(new dnssd.ServiceType('_webthing._tcp'), this.port!, opts);
this.mdns.on('error', (e) => {
console.debug(`mDNS error: ${e}`);
setTimeout(() => {
this.mdns.start();
}, 10000);
});
this.mdns.start();
return new Promise((resolve) => {
this.server.listen({ port: this.port }, resolve);
});
}
/**
* Stop listening.
*
* @param {boolean?} force - Whether or not to force shutdown immediately.
* @returns {Promise} Promise which resolves once the server is stopped.
*/
stop(force = false): Promise<unknown> {
const promises: Promise<void>[] = [];
if (this.mdns) {
promises.push(
new Promise((resolve, reject) => {
this.mdns.stop(force, (error?: unknown) => {
if (error) {
reject(error);
} else {
resolve();
}
});
})
);
}
promises.push(
new Promise((resolve, reject) => {
this.server.close((error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
})
);
return Promise.all(promises);
}
}
================================================
FILE: src/thing.ts
================================================
/**
* High-level Thing base class implementation.
*/
import Ajv from 'ajv';
import Property from './property';
import Event from './event';
import Action from './action';
import { AnyType, Link, Subscriber } from './types';
const ajv = new Ajv();
/**
* A Web Thing.
*/
class Thing {
private id: string;
private title: string;
private type: string[];
private context: string;
private description: string;
private properties: { [name: string]: Property };
private availableActions: {
[actionName: string]: {
metadata: Action.ActionMetadata;
class: Action.ActionTypeClass;
};
};
private availableEvents: {
[name: string]: {
metadata: Event.EventMetadata;
subscribers: Set<Subscriber>;
};
};
private actions: { [name: string]: Action[] };
private events: Event[];
private subscribers = new Set<Subscriber>();
private hrefPrefix: string;
private uiHref: string | null;
/**
* Initialize the object.
*
* @param {String} id The thing's unique ID - must be a URI
* @param {String} title The thing's title
* @param {String} type (Optional) The thing's type(s)
* @param {String} description (Optional) Description of the thing
*/
constructor(id: string, title: string, type: string | string[], description: string) {
if (!Array.isArray(type)) {
type = [type];
}
this.id = id;
this.title = title;
this.context = 'https://webthings.io/schemas';
this.type = type || [];
this.description = description || '';
this.properties = {};
this.availableActions = {};
this.availableEvents = {};
this.actions = {};
this.events = [];
this.subscribers = new Set();
this.hrefPrefix = '';
this.uiHref = null;
}
/**
* Return the thing state as a Thing Description.
*
* @returns {Object} Current thing state
*/
asThingDescription(): Thing.ThingDescription {
const thing: Omit<Thing.ThingDescription, 'name' | 'href'> = {
id: this.id,
title: this.title,
'@context': this.context,
'@type': this.type,
properties: this.getPropertyDescriptions(),
actions: {},
events: {},
links: [
{
rel: 'properties',
href: `${this.hrefPrefix}/properties`,
},
{
rel: 'actions',
href: `${this.hrefPrefix}/actions`,
},
{
rel: 'events',
href: `${this.hrefPrefix}/events`,
},
],
};
for (const name in this.availableActions) {
thing.actions[name] = this.availableActions[name].metadata;
thing.actions[name].links = [
{
rel: 'action',
href: `${this.hrefPrefix}/actions/${name}`,
},
];
}
for (const name in this.availableEvents) {
thing.events[name] = this.availableEvents[name].metadata;
thing.events[name].links = [
{
rel: 'event',
href: `${this.hrefPrefix}/events/${name}`,
},
];
}
if (this.uiHref) {
thing.links.push({
rel: 'alternate',
mediaType: 'text/html',
href: this.uiHref,
});
}
if (this.description) {
thing.description = this.description;
}
return thing as Thing.ThingDescription;
}
/**
* Get this thing's href.
*
* @returns {String} The href.
*/
getHref(): string {
if (this.hrefPrefix) {
return this.hrefPrefix;
}
return '/';
}
/**
* Get this thing's UI href.
*
* @returns {String|null} The href.
*/
getUiHref(): string | null {
return this.uiHref;
}
/**
* Set the prefix of any hrefs associated with this thing.
*
* @param {String} prefix The prefix
*/
setHrefPrefix(prefix: string): void {
this.hrefPrefix = prefix;
for (const property of Object.values(this.properties)) {
property.setHrefPrefix(prefix);
}
for (const actionName in this.actions) {
for (const action of this.actions[actionName]) {
action.setHrefPrefix(prefix);
}
}
}
/**
* Set the href of this thing's custom UI.
*
* @param {String} href The href
*/
setUiHref(href: string): void {
this.uiHref = href;
}
/**
* Get the ID of the thing.
*
* @returns {String} The ID.
*/
getId(): string {
return this.id;
}
/**
* Get the title of the thing.
*
* @returns {String} The title.
*/
getTitle(): string {
return this.title;
}
/**
* Get the type context of the thing.
*
* @returns {String} The context.
*/
getContext(): string {
return this.context;
}
/**
* Get the type(s) of the thing.
*
* @returns {String[]} The type(s).
*/
getType(): string[] {
return this.type;
}
/**
* Get the description of the thing.
*
* @returns {String} The description.
*/
getDescription(): string {
return this.description;
}
/**
* Get the thing's properties as an object.
*
* @returns {Object} Properties, i.e. name -> description
*/
getPropertyDescriptions(): { [name: string]: Property.PropertyDescription } {
const descriptions: { [name: string]: Property.PropertyDescription } = {};
for (const name in this.properties) {
descriptions[name] = this.properties[name].asPropertyDescription();
}
return descriptions;
}
/**
* Get the thing's actions as an array.
*
* @param {String?} actionName Optional action name to get descriptions for
*
* @returns {Object} Action descriptions.
*/
getActionDescriptions(actionName?: string | null): Action.ActionDescription[] {
const descriptions: Action.ActionDescription[] = [];
if (!actionName) {
for (const name in this.actions) {
for (const action of this.actions[name]) {
descriptions.push(action.asActionDescription());
}
}
} else if (this.actions.hasOwnProperty(actionName)) {
for (const action of this.actions[actionName]) {
descriptions.push(action.asActionDescription());
}
}
return descriptions;
}
/**
* Get the thing's events as an array.
*
* @param {String?} eventName Optional event name to get descriptions for
*
* @returns {Object} Event descriptions.
*/
getEventDescriptions(eventName?: string | null): Event.EventDescription[] {
if (!eventName) {
return this.events.map((e) => e.asEventDescription());
} else {
return this.events
.filter((e) => e.getName() === eventName)
.map((e) => e.asEventDescription());
}
}
/**
* Add a property to this thing.
*
* @param {Object} property Property to add
*/
addProperty(property: Property): void {
property.setHrefPrefix(this.hrefPrefix);
this.properties[property.getName()] = property;
}
/**
* Remove a property from this thing.
*
* @param {Object} property Property to remove
*/
removeProperty(property: Property): void {
if (this.properties.hasOwnProperty(property.getName())) {
delete this.properties[property.getName()];
}
}
/**
* Find a property by name.
*
* @param {String} propertyName Name of the property to find
*
* @returns {(Object|null)} Property if found, else null
*/
findProperty(propertyName: string): Property | null {
if (this.properties.hasOwnProperty(propertyName)) {
return this.properties[propertyName];
}
return null;
}
/**
* Get a property's value.
*
* @param {String} propertyName Name of the property to get the value of
*
* @returns {*} Current property value if found, else null
*/
getProperty(propertyName: string): unknown | null {
const prop = this.findProperty(propertyName);
if (prop) {
return prop.getValue();
}
return null;
}
/**
* Get a mapping of all properties and their values.
*
* Returns an object of propertyName -> value.
*/
getProperties(): Record<string, unknown> {
const props: Record<string, unknown> = {};
for (const name in this.properties) {
props[name] = this.properties[name].getValue();
}
return props;
}
/**
* Determine whether or not this thing has a given property.
*
* @param {String} propertyName The property to look for
*
* @returns {Boolean} Indication of property presence
*/
hasProperty(propertyName: string): boolean {
return this.properties.hasOwnProperty(propertyName);
}
/**
* Set a property value.
*
* @param {String} propertyName Name of the property to set
* @param {*} value Value to set
*/
setProperty(propertyName: string, value: AnyType): void {
const prop = this.findProperty(propertyName);
if (!prop) {
return;
}
prop.setValue(value);
}
/**
* Get an action.
*
* @param {String} actionName Name of the action
* @param {String} actionId ID of the action
* @returns {Object} The requested action if found, else null
*/
getAction(actionName: string, actionId: string): Action | null {
if (!this.actions.hasOwnProperty(actionName)) {
return null;
}
for (const action of this.actions[actionName]) {
if (action.getId() === actionId) {
return action;
}
}
return null;
}
/**
* Add a new event and notify subscribers.
*
* @param {Object} event The event that occurred
*/
addEvent(event: Event): void {
this.events.push(event);
this.eventNotify(event);
}
/**
* Add an available event.
*
* @param {String} name Name of the event
* @param {Object} metadata Event metadata, i.e. type, description, etc., as
* an object.
*/
addAvailableEvent(name: string, metadata: Event.EventMetadata): void {
if (!metadata) {
metadata = {};
}
this.availableEvents[name] = {
metadata: metadata,
subscribers: new Set(),
};
}
/**
* Perform an action on the thing.
*
* @param {String} actionName Name of the action
* @param {Object} input Any action inputs
* @returns {Object} The action that was created.
*/
performAction<InputType = AnyType>(
actionName: string,
input: InputType | null
): Action<InputType> | undefined {
input = input || null;
if (!this.availableActions.hasOwnProperty(actionName)) {
return;
}
const actionType = this.availableActions[actionName];
if (actionType.metadata.hasOwnProperty('input')) {
const schema = JSON.parse(JSON.stringify(actionType.metadata.input));
if (schema.hasOwnProperty('properties')) {
const props: Record<string, unknown>[] = Object.values(schema.properties);
for (const prop of props) {
delete prop.title;
delete prop.unit;
delete prop['@type'];
}
}
const valid = ajv.validate(schema, input);
if (!valid) {
return;
}
}
const action: Action<InputType> = <Action<InputType>>(
new actionType.class(this, <AnyType>(<unknown>input))
);
action.setHrefPrefix(this.hrefPrefix);
this.actionNotify(<Action<AnyType>>(<unknown>action));
this.actions[actionName].push(<Action<AnyType>>(<unknown>action));
return action;
}
/**
* Remove an existing action.
*
* @param {String} actionName Name of the action
* @param {String} actionId ID of the action
* @returns boolean indicating the presence of the action.
*/
removeAction(actionName: string, actionId: string): boolean {
const action = this.getAction(actionName, actionId);
if (action === null) {
return false;
}
action.cancel();
for (let i = 0; i < this.actions[actionName].length; ++i) {
if (this.actions[actionName][i].getId() === actionId) {
this.actions[actionName].splice(i, 1);
break;
}
}
return true;
}
/**
* Add an available action.
*
* @param {String} name Name of the action
* @param {Object} metadata Action metadata, i.e. type, description, etc., as
* an object.
* @param {Object} cls Class to instantiate for this action
*/
addAvailableAction(
name: string,
metadata: Action.ActionMetadata | null,
cls: Action.ActionTypeClass
): void {
if (!metadata) {
metadata = {};
}
this.availableActions[name] = {
metadata: metadata,
class: cls,
};
this.actions[name] = [];
}
/**
* Add a new websocket subscriber.
*
* @param {Object} ws The websocket
*/
addSubscriber(ws: Subscriber): void {
this.subscribers.add(ws);
}
/**
* Remove a websocket subscriber.
*
*/
removeSubscriber(ws: Subscriber): void {
if (this.subscribers.has(ws)) {
this.subscribers.delete(ws);
}
for (const name in this.availableEvents) {
this.removeEventSubscriber(name, ws);
}
}
/**
* Add a new websocket subscriber to an event.
*
* @param {String} name Name of the event
* @param {Subscriber} ws The websocket
*/
addEventSubscriber(name: string, ws: Subscriber): void {
if (this.availableEvents.hasOwnProperty(name)) {
this.availableEvents[name].subscribers.add(ws);
}
}
/**
* Remove a websocket subscriber from an event.
*
* @param {String} name Name of the event
* @param {Object} ws The websocket
*/
removeEventSubscriber(name: string, ws: Subscriber): void {
if (
this.availableEvents.hasOwnProperty(name) &&
this.availableEvents[name].subscribers.has(ws)
) {
this.availableEvents[name].subscribers.delete(ws);
}
}
/**
* Notify all subscribers of a property change.
*
* @param {Object} property The property that changed
*/
propertyNotify(property: Property<AnyType>): void {
const message = JSON.stringify({
messageType: 'propertyStatus',
data: {
[property.getName()]: property.getValue(),
},
});
for (const subscriber of this.subscribers) {
try {
subscriber.send(message);
} catch (e) {
// do nothing
}
}
}
/**
* Notify all subscribers of an action status change.
*
* @param {Object} action The action whose status changed
*/
actionNotify(action: Action): void {
const message = JSON.stringify({
messageType: 'actionStatus',
data: action.asActionDescription(),
});
for (const subscriber of this.subscribers) {
try {
subscriber.send(message);
} catch (e) {
// do nothing
}
}
}
/**
* Notify all subscribers of an event.
*
* @param {Object} event The event that occurred
*/
eventNotify(event: Event): void {
if (!this.availableEvents.hasOwnProperty(event.getName())) {
return;
}
const message = JSON.stringify({
messageType: 'event',
data: event.asEventDescription(),
});
for (const subscriber of this.availableEvents[event.getName()].subscribers) {
try {
subscriber.send(message);
} catch (e) {
// do nothing
}
}
}
}
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Thing {
export interface SecurityScheme {
'@type'?: string | string[];
scheme: string;
description?: string;
descriptions?: { [lang: string]: string };
proxy?: string;
}
export interface ThingDescription {
id: string;
title: string;
name: string;
href: string;
'@context': string;
'@type': string[];
properties: { [name: string]: Property.PropertyDescription };
links: Link[];
actions: { [name: string]: Action.ActionMetadata };
events: { [name: string]: Event.EventMetadata };
description?: string;
base?: string;
securityDefinitions?: { [security: string]: SecurityScheme };
security?: string;
}
}
export = Thing;
================================================
FILE: src/types.ts
================================================
export type PrimitiveJsonType =
| 'null'
| 'boolean'
| 'object'
| 'array'
| 'number'
| 'integer'
| 'string';
export type AnyType = null | boolean | number | string | Record<string, unknown> | unknown[];
export interface Link {
rel: string;
href: string;
mediaType?: string;
}
export interface Subscriber {
send(message: string): void;
}
================================================
FILE: src/utils.ts
================================================
/**
* Utility functions.
*/
import * as os from 'os';
/**
* Get the current time.
*
* @returns {String} The current time in the form YYYY-mm-ddTHH:MM:SS+00:00
*/
export function timestamp(): string {
const date = new Date().toISOString();
return date.replace(/\.\d{3}Z/, '+00:00');
}
/**
* Get all IP addresses.
*
* @returns {string[]} Array of addresses.
*/
export function getAddresses(): string[] {
const addresses = new Set<string>();
const ifaces = os.networkInterfaces();
Object.keys(ifaces).forEach((iface) => {
ifaces[iface]!.forEach((addr) => {
const address = addr.address.toLowerCase();
// Filter out link-local addresses.
if (addr.family === 'IPv6' && !address.startsWith('fe80:')) {
addresses.add(`[${address}]`);
} else if (addr.family === 'IPv4' && !address.startsWith('169.254.')) {
addresses.add(address);
}
});
});
return Array.from(addresses).sort();
}
================================================
FILE: src/value.ts
================================================
/**
* An observable, settable value interface.
*/
import { EventEmitter } from 'events';
import { AnyType } from './types';
/**
* A property value.
*
* This is used for communicating between the Thing representation and the
* actual physical thing implementation.
*
* Notifies all observers when the underlying value changes through an external
* update (command to turn the light off) or if the underlying sensor reports a
* new value.
*/
class Value<ValueType = AnyType> extends EventEmitter {
private lastValue: ValueType;
private valueForwarder: Value.Forwarder<ValueType> | null;
/**
* Initialize the object.
*
* @param {*} initialValue The initial value
* @param {function?} valueForwarder The method that updates the actual value
* on the thing
*/
constructor(initialValue: ValueType, valueForwarder: Value.Forwarder<ValueType> | null = null) {
super();
this.lastValue = initialValue;
this.valueForwarder = valueForwarder;
}
/**
* Set a new value for this thing.
*
* @param {*} value Value to set
*/
set(value: ValueType): void {
if (this.valueForwarder) {
this.valueForwarder(value);
}
this.notifyOfExternalUpdate(value);
}
/**
* Return the last known value from the underlying thing.
*
* @returns the value.
*/
get(): ValueType {
return this.lastValue;
}
/**
* Notify observers of a new value.
*
* @param {*} value New value
*/
notifyOfExternalUpdate(value: ValueType): void {
if (typeof value !== 'undefined' && value !== null && value !== this.lastValue) {
this.lastValue = value;
this.emit('update', value);
}
}
}
declare namespace Value {
export type Forwarder<T> = (value: T) => void;
}
export = Value;
================================================
FILE: src/webthing.ts
================================================
import Action from './action';
import Event from './event';
import Property from './property';
import Thing from './thing';
import Value from './value';
export { Action, Event, Property, Thing, Value };
export * from './server';
================================================
FILE: test.sh
================================================
#!/bin/bash -e
pushd example
npm install
popd
# clone the webthing-tester
if [ ! -d webthing-tester ]; then
git clone https://github.com/WebThingsIO/webthing-tester
fi
pip3 install --user -r webthing-tester/requirements.txt
export NODE_PATH=.
# build and test the single-thing example
node example/single-thing.js &
EXAMPLE_PID=$!
sleep 5
./webthing-tester/test-client.py
kill -15 $EXAMPLE_PID
# build and test the multiple-things example
node example/multiple-things.js &
EXAMPLE_PID=$!
sleep 5
./webthing-tester/test-client.py --path-prefix "/0"
kill -15 $EXAMPLE_PID
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"moduleResolution": "node",
"lib": [
"es2018",
"dom"
],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"rootDir": "src",
"outDir": "lib",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
}
}
gitextract_p46l49r7/ ├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .github/ │ └── workflows/ │ ├── build.yml │ ├── projects.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── example/ │ ├── multiple-things.js │ ├── package.json │ ├── platform/ │ │ ├── Makefile │ │ ├── adc/ │ │ │ └── adc-property.js │ │ ├── board/ │ │ │ ├── artik1020.js │ │ │ ├── artik530.js │ │ │ ├── edison.js │ │ │ ├── flex-phat.js │ │ │ ├── play-phat.js │ │ │ └── traffic-phat.js │ │ ├── gpio/ │ │ │ └── gpio-property.js │ │ ├── package.json │ │ └── pwm/ │ │ └── pwm-property.js │ ├── simplest-thing.js │ └── single-thing.js ├── package.json ├── src/ │ ├── action.ts │ ├── event.ts │ ├── index.ts │ ├── property.ts │ ├── server.ts │ ├── thing.ts │ ├── types.ts │ ├── utils.ts │ ├── value.ts │ └── webthing.ts ├── test.sh └── tsconfig.json
SYMBOL INDEX (182 symbols across 20 files)
FILE: example/multiple-things.js
class OverheatedEvent (line 15) | class OverheatedEvent extends Event {
method constructor (line 16) | constructor(thing, data) {
class FadeAction (line 21) | class FadeAction extends Action {
method constructor (line 22) | constructor(thing, input) {
method performAction (line 26) | performAction() {
class ExampleDimmableLight (line 40) | class ExampleDimmableLight extends Thing {
method constructor (line 41) | constructor() {
class FakeGpioHumiditySensor (line 102) | class FakeGpioHumiditySensor extends Thing {
method constructor (line 103) | constructor() {
method readFromGPIO (line 137) | readFromGPIO() {
function runServer (line 142) | function runServer() {
FILE: example/platform/adc/adc-property.js
class AdcInProperty (line 23) | class AdcInProperty extends Property {
method constructor (line 24) | constructor(thing, name, value, metadata, config) {
method close (line 58) | close() {
function AdcProperty (line 70) | function AdcProperty(thing, name, value, metadata, config) {
FILE: example/platform/board/artik1020.js
class ARTIK1020Thing (line 18) | class ARTIK1020Thing extends Thing {
method constructor (line 19) | constructor(name, type, description) {
method close (line 91) | close() {
FILE: example/platform/board/artik530.js
class ARTIK530Thing (line 18) | class ARTIK530Thing extends Thing {
method constructor (line 19) | constructor(name, type, description) {
method close (line 85) | close() {
FILE: example/platform/board/edison.js
class EdisonThing (line 17) | class EdisonThing extends Thing {
method constructor (line 18) | constructor(name, type, description) {
method close (line 36) | close() {
FILE: example/platform/board/flex-phat.js
class FlexPHatThing (line 17) | class FlexPHatThing extends Thing {
method constructor (line 18) | constructor(name, type, description) {
method close (line 75) | close() {
FILE: example/platform/board/play-phat.js
class PlayPHatThing (line 17) | class PlayPHatThing extends Thing {
method constructor (line 18) | constructor(name, type, description) {
method close (line 89) | close() {
FILE: example/platform/board/traffic-phat.js
class TrafficPHatThing (line 17) | class TrafficPHatThing extends Thing {
method constructor (line 18) | constructor(name, type, description) {
method close (line 105) | close() {
FILE: example/platform/gpio/gpio-property.js
class GpioOutProperty (line 22) | class GpioOutProperty extends Property {
method constructor (line 23) | constructor(thing, name, value, metadata, config) {
method close (line 51) | close() {
class GpioInProperty (line 62) | class GpioInProperty extends Property {
method constructor (line 63) | constructor(thing, name, value, metadata, config) {
method close (line 84) | close() {
function GpioProperty (line 95) | function GpioProperty(thing, name, value, metadata, config) {
FILE: example/platform/pwm/pwm-property.js
class PwmOutProperty (line 22) | class PwmOutProperty extends Property {
method constructor (line 23) | constructor(thing, name, value, metadata, config) {
method close (line 83) | close() {
FILE: example/simplest-thing.js
function makeThing (line 13) | function makeThing() {
function runServer (line 32) | function runServer() {
FILE: example/single-thing.js
class OverheatedEvent (line 7) | class OverheatedEvent extends Event {
method constructor (line 8) | constructor(thing, data) {
class FadeAction (line 13) | class FadeAction extends Action {
method constructor (line 14) | constructor(thing, input) {
method performAction (line 18) | performAction() {
function makeThing (line 29) | function makeThing() {
function runServer (line 92) | function runServer() {
FILE: src/action.ts
class Action (line 12) | class Action<InputType = AnyType> {
method constructor (line 31) | constructor(id: string, thing: Thing, name: string, input: InputType) {
method asActionDescription (line 56) | asActionDescription(): Action.ActionDescription {
method setHrefPrefix (line 81) | setHrefPrefix(prefix: string): void {
method getId (line 90) | getId(): string {
method getName (line 99) | getName(): string {
method getHref (line 108) | getHref(): string {
method getStatus (line 117) | getStatus(): string {
method getThing (line 126) | getThing(): Thing {
method getTimeRequested (line 135) | getTimeRequested(): string {
method getTimeCompleted (line 144) | getTimeCompleted(): string | null {
method getInput (line 153) | getInput(): InputType {
method start (line 160) | start(): void {
method performAction (line 174) | performAction(): Promise<void> {
method cancel (line 183) | cancel(): Promise<void> {
method finish (line 190) | finish(): void {
type ActionMetadata (line 199) | interface ActionMetadata {
type ActionDescription (line 212) | interface ActionDescription<InputType = AnyType> {
type ActionTypeClass (line 222) | interface ActionTypeClass<InputType = AnyType> {
FILE: src/event.ts
class Event (line 12) | class Event<Data = AnyType> {
method constructor (line 28) | constructor(thing: Thing, name: string, data?: Data) {
method asEventDescription (line 40) | asEventDescription(): Event.EventDescription {
method getThing (line 59) | getThing(): Thing {
method getName (line 68) | getName(): string {
method getData (line 77) | getData(): Data | null {
method getTime (line 86) | getTime(): string {
type EventDescription (line 93) | interface EventDescription {
type EventMetadata (line 100) | interface EventMetadata {
FILE: src/property.ts
class Property (line 15) | class Property<ValueType = AnyType> {
method constructor (line 39) | constructor(
method validateValue (line 68) | validateValue(value: ValueType): void {
method asPropertyDescription (line 84) | asPropertyDescription(): Property.PropertyDescription {
method setHrefPrefix (line 103) | setHrefPrefix(prefix: string): void {
method getHref (line 112) | getHref(): string {
method getValue (line 121) | getValue(): ValueType {
method setValue (line 130) | setValue(value: ValueType): void {
method getName (line 140) | getName(): string {
method getThing (line 149) | getThing(): Thing {
method getMetadata (line 158) | getMetadata(): Property.PropertyMetadata {
type PropertyMetadata (line 166) | interface PropertyMetadata {
type PropertyDescription (line 180) | interface PropertyDescription extends PropertyMetadata {
FILE: src/server.ts
class SingleThing (line 19) | class SingleThing {
method constructor (line 27) | constructor(thing: Thing) {
method getThing (line 34) | getThing(): Thing {
method getThings (line 41) | getThings(): Thing[] {
method getName (line 48) | getName(): string {
class MultipleThings (line 56) | class MultipleThings {
method constructor (line 67) | constructor(things: Thing[], name: string) {
method getThing (line 77) | getThing(idx?: number | string): Thing | null {
method getThings (line 89) | getThings(): Thing[] {
method getName (line 96) | getName(): string {
method constructor (line 112) | constructor(things: SingleThing | MultipleThings) {
method getThing (line 124) | getThing(req: express.Request): Thing | null {
class ThingsHandler (line 132) | class ThingsHandler extends BaseHandler {
method get (line 139) | get(req: express.Request, res: express.Response): void {
class ThingHandler (line 165) | class ThingHandler extends BaseHandler {
method get (line 172) | get(req: express.Request, res: express.Response): void {
method ws (line 202) | ws(ws: import('ws'), req: express.Request): void {
class PropertiesHandler (line 344) | class PropertiesHandler extends BaseHandler {
method get (line 351) | get(req: express.Request, res: express.Response): void {
class PropertyHandler (line 365) | class PropertyHandler extends BaseHandler {
method get (line 372) | get(req: express.Request, res: express.Response): void {
method put (line 393) | put(req: express.Request, res: express.Response): void {
class ActionsHandler (line 424) | class ActionsHandler extends BaseHandler {
method get (line 431) | get(req: express.Request, res: express.Response): void {
method post (line 447) | post(req: express.Request, res: express.Response): void {
class ActionHandler (line 482) | class ActionHandler extends BaseHandler {
method get (line 489) | get(req: express.Request, res: express.Response): void {
method post (line 507) | post(req: express.Request, res: express.Response): void {
class ActionIDHandler (line 548) | class ActionIDHandler extends BaseHandler {
method get (line 555) | get(req: express.Request, res: express.Response): void {
method put (line 580) | put(req: express.Request, res: express.Response): void {
method delete (line 597) | delete(req: express.Request, res: express.Response): void {
class EventsHandler (line 618) | class EventsHandler extends BaseHandler {
method get (line 625) | get(req: express.Request, res: express.Response): void {
class EventHandler (line 639) | class EventHandler extends BaseHandler {
method get (line 646) | get(req: express.Request, res: express.Response): void {
class WebThingServer (line 662) | class WebThingServer {
method constructor (line 704) | constructor(
method start (line 856) | start(): Promise<void> {
method stop (line 888) | stop(force = false): Promise<unknown> {
FILE: src/thing.ts
class Thing (line 16) | class Thing {
method constructor (line 61) | constructor(id: string, title: string, type: string | string[], descri...
method asThingDescription (line 86) | asThingDescription(): Thing.ThingDescription {
method getHref (line 151) | getHref(): string {
method getUiHref (line 164) | getUiHref(): string | null {
method setHrefPrefix (line 173) | setHrefPrefix(prefix: string): void {
method setUiHref (line 192) | setUiHref(href: string): void {
method getId (line 201) | getId(): string {
method getTitle (line 210) | getTitle(): string {
method getContext (line 219) | getContext(): string {
method getType (line 228) | getType(): string[] {
method getDescription (line 237) | getDescription(): string {
method getPropertyDescriptions (line 246) | getPropertyDescriptions(): { [name: string]: Property.PropertyDescript...
method getActionDescriptions (line 262) | getActionDescriptions(actionName?: string | null): Action.ActionDescri...
method getEventDescriptions (line 287) | getEventDescriptions(eventName?: string | null): Event.EventDescriptio...
method addProperty (line 302) | addProperty(property: Property): void {
method removeProperty (line 312) | removeProperty(property: Property): void {
method findProperty (line 325) | findProperty(propertyName: string): Property | null {
method getProperty (line 340) | getProperty(propertyName: string): unknown | null {
method getProperties (line 354) | getProperties(): Record<string, unknown> {
method hasProperty (line 370) | hasProperty(propertyName: string): boolean {
method setProperty (line 380) | setProperty(propertyName: string, value: AnyType): void {
method getAction (line 396) | getAction(actionName: string, actionId: string): Action | null {
method addEvent (line 415) | addEvent(event: Event): void {
method addAvailableEvent (line 427) | addAvailableEvent(name: string, metadata: Event.EventMetadata): void {
method performAction (line 445) | performAction<InputType = AnyType>(
method removeAction (line 492) | removeAction(actionName: string, actionId: string): boolean {
method addAvailableAction (line 517) | addAvailableAction(
method addSubscriber (line 538) | addSubscriber(ws: Subscriber): void {
method removeSubscriber (line 546) | removeSubscriber(ws: Subscriber): void {
method addEventSubscriber (line 562) | addEventSubscriber(name: string, ws: Subscriber): void {
method removeEventSubscriber (line 574) | removeEventSubscriber(name: string, ws: Subscriber): void {
method propertyNotify (line 588) | propertyNotify(property: Property<AnyType>): void {
method actionNotify (line 610) | actionNotify(action: Action): void {
method eventNotify (line 630) | eventNotify(event: Event): void {
type SecurityScheme (line 652) | interface SecurityScheme {
type ThingDescription (line 660) | interface ThingDescription {
FILE: src/types.ts
type PrimitiveJsonType (line 1) | type PrimitiveJsonType =
type AnyType (line 10) | type AnyType = null | boolean | number | string | Record<string, unknown...
type Link (line 12) | interface Link {
type Subscriber (line 18) | interface Subscriber {
FILE: src/utils.ts
function timestamp (line 12) | function timestamp(): string {
function getAddresses (line 22) | function getAddresses(): string[] {
FILE: src/value.ts
class Value (line 18) | class Value<ValueType = AnyType> extends EventEmitter {
method constructor (line 30) | constructor(initialValue: ValueType, valueForwarder: Value.Forwarder<V...
method set (line 41) | set(value: ValueType): void {
method get (line 54) | get(): ValueType {
method notifyOfExternalUpdate (line 63) | notifyOfExternalUpdate(value: ValueType): void {
type Forwarder (line 72) | type Forwarder<T> = (value: T) => void;
Condensed preview — 44 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (139K chars).
[
{
"path": ".dockerignore",
"chars": 68,
"preview": "Dockerfile\n.git/\n#.gitignore contents:\n*~\n*.swp\n*.tgz\nnode_modules/\n"
},
{
"path": ".eslintignore",
"chars": 116,
"preview": "**/node_modules/\n/.eslintrc.js\ntmp/\nlib/*.js\nlib/*.d.ts\nindex.js\nindex.d.ts\nwebthing.js\nwebthing.d.ts\n*.eslintrc.js\n"
},
{
"path": ".eslintrc.js",
"chars": 6321,
"preview": "module.exports = {\n 'env': {\n 'browser': true,\n 'commonjs': true,\n 'es6': true,\n 'jasmine': true,\n 'jest"
},
{
"path": ".github/workflows/build.yml",
"chars": 861,
"preview": "name: Build\n\non:\n pull_request:\n branches:\n - master\n push:\n branches:\n - master\n\njobs:\n build:\n r"
},
{
"path": ".github/workflows/projects.yml",
"chars": 477,
"preview": "name: Add new issues to the specified project column\n\non:\n issues:\n types: [opened]\n\njobs:\n add-new-issues-to-proje"
},
{
"path": ".github/workflows/release.yml",
"chars": 977,
"preview": "name: Release\n\non:\n push:\n tags:\n - v[0-9]+.[0-9]+.[0-9]+\n\njobs:\n build:\n runs-on: ubuntu-latest\n steps:"
},
{
"path": ".gitignore",
"chars": 253,
"preview": "**/iotjs_modules\n**/node_modules\n*.swp\n*.tgz\n*~\n.#*\nindex.d.ts\nindex.d.ts.map\nindex.js\nindex.js.map\nlib/*.js\nlib/*.js.ma"
},
{
"path": ".npmignore",
"chars": 41,
"preview": ".eslintrc.js\ntest-server.js\n*.ts\n!*.d.ts\n"
},
{
"path": ".prettierrc.json",
"chars": 47,
"preview": "{\n \"printWidth\": 100,\n \"singleQuote\": true\n}\n"
},
{
"path": "CHANGELOG.md",
"chars": 2256,
"preview": "# webthing Changelog\n\n## [Unreleased]\n\n## [0.15.0] - 2021-01-05\n### Added\n- Parameter to disable host validation in serv"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 691,
"preview": "# Community Participation Guidelines\n\nThis repository is governed by Mozilla's code of conduct and etiquette guidelines."
},
{
"path": "Dockerfile",
"chars": 939,
"preview": "#!/bin/echo docker build . -f\n# -*- coding: utf-8 -*-\n# SPDX-License-Identifier: MPL-2.0\n#{\n# Copyright: 2018-present Sa"
},
{
"path": "LICENSE",
"chars": 16725,
"preview": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\""
},
{
"path": "Makefile",
"chars": 2627,
"preview": "#!/bin/make -f\n# -*- makefile -*-\n# SPDX-License-Identifier: MPL-2.0\n#{\n# Copyright 2018-present Samsung Electronics Fra"
},
{
"path": "README.md",
"chars": 5898,
"preview": "# webthing\n\n[](https:"
},
{
"path": "docker-compose.yml",
"chars": 499,
"preview": "# -*- coding: utf-8 -*-\n# SPDX-License-Identifier: MPL-2.0\n#{\n# Copyright: 2018-present Samsung Electronics France SAS, "
},
{
"path": "example/multiple-things.js",
"chars": 4179,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n\nconst {\n Action,\n Event,\n MultipleThing"
},
{
"path": "example/package.json",
"chars": 148,
"preview": "{\n \"name\": \"example\",\n \"version\": \"1.0.0\",\n \"author\": \"WebThingsIO\",\n \"license\": \"MPL-2.0\",\n \"dependencies\": {\n "
},
{
"path": "example/platform/Makefile",
"chars": 3250,
"preview": "#!/bin/make -f\n# -*- makefile -*-\n# SPDX-License-Identifier: MPL-2.0\n#{\n# Copyright 2018-present Samsung Electronics Fra"
},
{
"path": "example/platform/adc/adc-property.js",
"chars": 2454,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung E"
},
{
"path": "example/platform/board/artik1020.js",
"chars": 2722,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung E"
},
{
"path": "example/platform/board/artik530.js",
"chars": 2473,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung E"
},
{
"path": "example/platform/board/edison.js",
"chars": 1207,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung E"
},
{
"path": "example/platform/board/flex-phat.js",
"chars": 2159,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung E"
},
{
"path": "example/platform/board/play-phat.js",
"chars": 2559,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung E"
},
{
"path": "example/platform/board/traffic-phat.js",
"chars": 2456,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung E"
},
{
"path": "example/platform/gpio/gpio-property.js",
"chars": 3152,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung E"
},
{
"path": "example/platform/package.json",
"chars": 876,
"preview": "{\n \"name\": \"board-webthings\",\n \"version\": \"0.0.0\",\n \"description\": \"Various Single Board computers's I/O implemented "
},
{
"path": "example/platform/pwm/pwm-property.js",
"chars": 3076,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung E"
},
{
"path": "example/simplest-thing.js",
"chars": 1500,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n/**\n *\n * Copyright 2018-present Samsung El"
},
{
"path": "example/single-thing.js",
"chars": 2565,
"preview": "// -*- mode: js; js-indent-level:2; -*-\n// SPDX-License-Identifier: MPL-2.0\n\nconst { Action, Event, Property, SingleThi"
},
{
"path": "package.json",
"chars": 1487,
"preview": "{\n \"name\": \"webthing\",\n \"version\": \"0.15.0\",\n \"description\": \"HTTP Web Thing implementation\",\n \"main\": \"lib/webthing"
},
{
"path": "src/action.ts",
"chars": 4734,
"preview": "/**\n * High-level Action base class implementation.\n */\n\nimport * as utils from './utils';\nimport { AnyType, Link, Primi"
},
{
"path": "src/event.ts",
"chars": 2238,
"preview": "/**\n * High-level Event base class implementation.\n */\n\nimport Thing from './thing';\nimport * as utils from './utils';\ni"
},
{
"path": "src/index.ts",
"chars": 28,
"preview": "export * from './webthing';\n"
},
{
"path": "src/property.ts",
"chars": 4227,
"preview": "/**\n * High-level Property base class implementation.\n */\n\nimport Ajv, { ValidateFunction } from 'ajv';\nimport Thing fro"
},
{
"path": "src/server.ts",
"chars": 24026,
"preview": "/**\n * Node Web Thing server implementation.\n */\n\nimport bodyParser from 'body-parser';\nimport * as dnssd from 'dnssd';\n"
},
{
"path": "src/thing.ts",
"chars": 15918,
"preview": "/**\n * High-level Thing base class implementation.\n */\n\nimport Ajv from 'ajv';\nimport Property from './property';\nimport"
},
{
"path": "src/types.ts",
"chars": 362,
"preview": "export type PrimitiveJsonType =\n | 'null'\n | 'boolean'\n | 'object'\n | 'array'\n | 'number'\n | 'integer'\n | 'string"
},
{
"path": "src/utils.ts",
"chars": 956,
"preview": "/**\n * Utility functions.\n */\n\nimport * as os from 'os';\n\n/**\n * Get the current time.\n *\n * @returns {String} The curre"
},
{
"path": "src/value.ts",
"chars": 1810,
"preview": "/**\n * An observable, settable value interface.\n */\n\nimport { EventEmitter } from 'events';\nimport { AnyType } from './t"
},
{
"path": "src/webthing.ts",
"chars": 231,
"preview": "import Action from './action';\nimport Event from './event';\nimport Property from './property';\nimport Thing from './thin"
},
{
"path": "test.sh",
"chars": 578,
"preview": "#!/bin/bash -e\n\npushd example\nnpm install\npopd\n\n# clone the webthing-tester\nif [ ! -d webthing-tester ]; then\n git cl"
},
{
"path": "tsconfig.json",
"chars": 678,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es2018\",\n \"module\": \"commonjs\",\n \"moduleResolution\": \"node\",\n \"lib\": [\n"
}
]
About this extraction
This page contains the full source code of the mozilla-iot/webthing-node GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 44 files (127.8 KB), approximately 33.7k tokens, and a symbol index with 182 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.