[
  {
    "path": ".dockerignore",
    "content": "Dockerfile\n.git/\n#.gitignore contents:\n*~\n*.swp\n*.tgz\nnode_modules/\n"
  },
  {
    "path": ".eslintignore",
    "content": "**/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",
    "content": "module.exports = {\n  'env': {\n    'browser': true,\n    'commonjs': true,\n    'es6': true,\n    'jasmine': true,\n    'jest': true,\n    'mocha': true,\n    'node': true\n  },\n  'extends': [\n    'eslint:recommended',\n    'plugin:@typescript-eslint/eslint-recommended',\n    'plugin:@typescript-eslint/recommended',\n    'prettier',\n    'prettier/@typescript-eslint'\n  ],\n  'parser': '@typescript-eslint/parser',\n  'parserOptions': {\n    'sourceType': 'module'\n  },\n  'plugins': [\n    '@typescript-eslint'\n  ],\n  'rules': {\n    'arrow-parens': [\n      'error',\n      'always'\n    ],\n    'arrow-spacing': 'error',\n    'block-scoped-var': 'error',\n    'block-spacing': [\n      'error',\n      'always'\n    ],\n    '@typescript-eslint/brace-style': [\n      'error',\n      '1tbs'\n    ],\n    '@typescript-eslint/comma-dangle': [\n      'error',\n      'always-multiline'\n    ],\n    '@typescript-eslint/comma-spacing': 'error',\n    'comma-style': [\n      'error',\n      'last'\n    ],\n    'computed-property-spacing': [\n      'error',\n      'never'\n    ],\n    'curly': 'error',\n    '@typescript-eslint/default-param-last': 'error',\n    'dot-notation': 'error',\n    'eol-last': 'error',\n    '@typescript-eslint/explicit-module-boundary-types': [\n      'warn',\n      {\n        'allowArgumentsExplicitlyTypedAsAny': true\n      }\n    ],\n    '@typescript-eslint/explicit-function-return-type': [\n      'error',\n      {\n        'allowExpressions': true\n      }\n    ],\n    '@typescript-eslint/func-call-spacing': [\n      'error',\n      'never'\n    ],\n    '@typescript-eslint/indent': [\n      'error',\n      2,\n      {\n        'ArrayExpression': 'first',\n        'CallExpression': {\n          'arguments': 'first'\n        },\n        'FunctionDeclaration': {\n          'parameters': 'first'\n        },\n        'FunctionExpression': {\n          'parameters': 'first'\n        },\n        'ObjectExpression': 'first',\n        'SwitchCase': 1\n      }\n    ],\n    'key-spacing': [\n      'error',\n      {\n        'afterColon': true,\n        'beforeColon': false,\n        'mode': 'strict'\n      }\n    ],\n    '@typescript-eslint/keyword-spacing': 'off',\n    'linebreak-style': [\n      'error',\n      'unix'\n    ],\n    '@typescript-eslint/lines-between-class-members': [\n      'error',\n      'always'\n    ],\n    'max-len': [\n      'error',\n      100\n    ],\n    '@typescript-eslint/member-delimiter-style': [\n      'error',\n      {\n        'singleline': {\n          'delimiter': 'semi',\n          'requireLast': false\n        },\n        'multiline': {\n          'delimiter': 'semi',\n          'requireLast': true\n        }\n      }\n    ],\n    'multiline-ternary': [\n      'error',\n      'always-multiline'\n    ],\n    'no-console': 0,\n    '@typescript-eslint/no-duplicate-imports': 'error',\n    'no-eval': 'error',\n    '@typescript-eslint/no-explicit-any': [\n      'error',\n      {\n        'ignoreRestArgs': true\n      }\n    ],\n    'no-floating-decimal': 'error',\n    'no-implicit-globals': 'error',\n    'no-implied-eval': 'error',\n    'no-lonely-if': 'error',\n    'no-multi-spaces': [\n      'error',\n      {\n        'ignoreEOLComments': true\n      }\n    ],\n    'no-multiple-empty-lines': 'error',\n    '@typescript-eslint/no-namespace': [\n      'error',\n      {\n        'allowDeclarations': true\n      }\n    ],\n    '@typescript-eslint/no-non-null-assertion': 'off',\n    'no-prototype-builtins': 'off',\n    'no-return-assign': 'error',\n    'no-script-url': 'error',\n    'no-self-compare': 'error',\n    'no-sequences': 'error',\n    'no-shadow-restricted-names': 'error',\n    'no-tabs': 'error',\n    'no-throw-literal': 'error',\n    'no-trailing-spaces': 'error',\n    'no-undefined': 'error',\n    'no-unmodified-loop-condition': 'error',\n    '@typescript-eslint/no-unused-vars': [\n      'error',\n      {\n        'argsIgnorePattern': '^_',\n        'varsIgnorePattern': '^_'\n      }\n    ],\n    'no-useless-computed-key': 'error',\n    'no-useless-concat': 'error',\n    '@typescript-eslint/no-useless-constructor': 'error',\n    'no-useless-return': 'error',\n    'no-var': 'error',\n    'no-void': 'error',\n    'no-whitespace-before-property': 'error',\n    'object-curly-newline': [\n      'error',\n      {\n        'consistent': true\n      }\n    ],\n    'object-curly-spacing': [\n      'error',\n      'always'\n    ],\n    'object-property-newline': [\n      'error',\n      {\n        'allowMultiplePropertiesPerLine': true\n      }\n    ],\n    'operator-linebreak': [\n      'error',\n      'after',\n      {\n        'overrides': {\n          '?': 'before',\n          ':': 'before'\n        }\n      }\n    ],\n    'padded-blocks': [\n      'error',\n      {\n        'blocks': 'never'\n      }\n    ],\n    'prefer-const': 'error',\n    '@typescript-eslint/prefer-for-of': 'error',\n    'prefer-template': 'error',\n    'quote-props': [\n      'error',\n      'as-needed'\n    ],\n    '@typescript-eslint/quotes': [\n      'error',\n      'single',\n      {\n        'allowTemplateLiterals': true\n      }\n    ],\n    '@typescript-eslint/semi': [\n      'error',\n      'always'\n    ],\n    'semi-spacing': [\n      'error',\n      {\n        'after': true,\n        'before': false\n      }\n    ],\n    'semi-style': [\n      'error',\n      'last'\n    ],\n    'space-before-blocks': [\n      'error',\n      'always'\n    ],\n    '@typescript-eslint/space-before-function-paren': [\n      'error',\n      {\n        'anonymous': 'always',\n        'asyncArrow': 'always',\n        'named': 'never'\n      }\n    ],\n    'space-in-parens': [\n      'error',\n      'never'\n    ],\n    '@typescript-eslint/space-infix-ops': 'error',\n    'space-unary-ops': [\n      'error',\n      {\n        'nonwords': false,\n        'words': true\n      }\n    ],\n    'spaced-comment': [\n      'error',\n      'always',\n      {\n        'block': {\n          'balanced': true,\n          'exceptions': [\n            '*'\n          ]\n        }\n      }\n    ],\n    'switch-colon-spacing': [\n      'error',\n      {\n        'after': true,\n        'before': false\n      }\n    ],\n    'template-curly-spacing': [\n      'error',\n      'never'\n    ],\n    '@typescript-eslint/type-annotation-spacing': 'error',\n    'yoda': 'error'\n  },\n  'overrides': [\n    {\n      'files': [\n        'example/**/*.js'\n      ],\n      'rules': {\n        '@typescript-eslint/explicit-function-return-type': 'off',\n        '@typescript-eslint/no-var-requires': 'off'\n      }\n    }\n  ]\n};\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  pull_request:\n    branches:\n      - master\n  push:\n    branches:\n      - master\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version: [\n          10,\n          12,\n          14,\n        ]\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-python@v2\n        with:\n          python-version: '3.9'\n      - uses: actions/setup-node@v1\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Install dependencies\n        run: |\n          npm ci\n      - name: Check formatting\n        run: |\n          npx prettier -c 'src/*.ts' 'example/**/*.{js,ts}'\n      - name: Lint with eslint\n        run: |\n          npm run lint\n      - name: Transpile ts files\n        run: |\n          npm run build\n      - name: Run integration tests\n        run: |\n          ./test.sh\n"
  },
  {
    "path": ".github/workflows/projects.yml",
    "content": "name: Add new issues to the specified project column\n\non:\n  issues:\n    types: [opened]\n\njobs:\n  add-new-issues-to-project-column:\n    runs-on: ubuntu-latest\n    steps:\n    - name: add-new-issues-to-organization-based-project-column\n      uses: docker://takanabe/github-actions-automate-projects:v0.0.1\n      env:\n        GITHUB_TOKEN: ${{ secrets.CI_TOKEN }}\n        GITHUB_PROJECT_URL: https://github.com/orgs/WebThingsIO/projects/4\n        GITHUB_PROJECT_COLUMN_NAME: To do\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "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:\n      - uses: actions/checkout@v2\n\n      - uses: actions/setup-node@v1\n        with:\n          node-version: 12\n          registry-url: 'https://registry.npmjs.org'\n\n      - name: Set release version\n        run: echo \"RELEASE_VERSION=${GITHUB_REF:11}\" >> $GITHUB_ENV\n\n      - name: Create Release\n        id: create_release\n        uses: actions/create-release@v1.0.0\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: Release ${{ env.RELEASE_VERSION }}\n          draft: false\n          prerelease: false\n\n      - name: Build project\n        run: |\n          npm ci\n          npm run lint\n          npm run build\n        env:\n          CI: true\n\n      - name: Publish to npm\n        run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "**/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.map\nlib/*.d.ts\nlib/*.d.ts.map\nnpm-debug.log\ntmp/\nwebthing.d.ts\nwebthing.d.ts.map\nwebthing.js\nwebthing.js.map\nexample/package-lock.json\n"
  },
  {
    "path": ".npmignore",
    "content": ".eslintrc.js\ntest-server.js\n*.ts\n!*.d.ts\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"printWidth\": 100,\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# webthing Changelog\n\n## [Unreleased]\n\n## [0.15.0] - 2021-01-05\n### Added\n- Parameter to disable host validation in server.\n\n## [0.14.0] - 2020-12-14\n### Changed\n- Converted project to TypeScript.\n\n## [0.13.1] - 2020-11-28\n### Fixed\n- Test fixes.\n\n## [0.13.0] - 2020-09-23\n### Changed\n- Update author and URLs to indicate new project home.\n\n## [0.12.3] - 2020-06-18\n### Changed\n- mDNS record now indicates TLS support.\n\n## [0.12.2] - 2020-05-04\n### Changed\n- Invalid POST requests to action resources now generate an error status.\n\n## [0.12.1] - 2020-03-27\n### Changed\n- Updated dependencies.\n\n## [0.12.0] - 2019-07-12\n### Changed\n- Things now use `title` rather than `name`.\n- Things now require a unique ID in the form of a URI.\n### Added\n- Support for `id`, `base`, `security`, and `securityDefinitions` keys in thing description.\n\n## [0.11.1] - 2019-06-05\n### Added\n- Ability to set a base URL path on server.\n\n## [0.11.0] - 2019-01-16\n### Changed\n- WebThingServer constructor can now take a list of additional API routes.\n### Fixed\n- Properties could not include a custom `links` array at initialization.\n\n## [0.10.0] - 2018-11-30\n### Changed\n- Property, Action, and Event description now use `links` rather than `href`. - [Spec PR](https://github.com/WebThingsIO/wot/pull/119)\n\n[Unreleased]: https://github.com/WebThingsIO/webthing-node/compare/v0.15.0...HEAD\n[0.15.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.14.0...v0.15.0\n[0.14.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.13.1...v0.14.0\n[0.13.1]: https://github.com/WebThingsIO/webthing-node/compare/v0.13.0...v0.13.1\n[0.13.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.12.3...v0.13.0\n[0.12.3]: https://github.com/WebThingsIO/webthing-node/compare/v0.12.2...v0.12.3\n[0.12.2]: https://github.com/WebThingsIO/webthing-node/compare/v0.12.1...v0.12.2\n[0.12.1]: https://github.com/WebThingsIO/webthing-node/compare/v0.12.0...v0.12.1\n[0.12.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.11.1...v0.12.0\n[0.11.1]: https://github.com/WebThingsIO/webthing-node/compare/v0.11.0...v0.11.1\n[0.11.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.10.0...v0.11.0\n[0.10.0]: https://github.com/WebThingsIO/webthing-node/compare/v0.9.1...v0.10.0\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Community Participation Guidelines\n\nThis repository is governed by Mozilla's code of conduct and etiquette guidelines. \nFor more details, please read the\n[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). \n\n## How to Report\nFor 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.\n\n<!--\n## Project Specific Etiquette\n\nIn some cases, there will be additional project etiquette i.e.: (https://bugzilla.mozilla.org/page.cgi?id=etiquette.html).\nPlease update for your project.\n-->\n"
  },
  {
    "path": "Dockerfile",
    "content": "#!/bin/echo docker build . -f\n# -*- coding: utf-8 -*-\n# SPDX-License-Identifier: MPL-2.0\n#{\n# Copyright: 2018-present Samsung Electronics France SAS, and other contributors\n#\n# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/ .\n#}\n\nFROM node:10-buster\nLABEL maintainer=\"Philippe Coval (p.coval@samsung.com)\"\n\nENV DEBIAN_FRONTEND noninteractive\nENV LC_ALL en_US.UTF-8\nENV LANG ${LC_ALL}\n\nENV project webthing-node\nCOPY . /usr/local/${project}/${project}\nWORKDIR /usr/local/${project}/${project}\nRUN echo \"#log: ${project}: Preparing sources\" \\\n  && set -x \\\n  && which npm \\\n  && npm --version \\\n  && npm install \\\n  && npm run test || echo \"TODO: check package.json\" \\\n  && sync\n\nEXPOSE 8888\nWORKDIR /usr/local/${project}/${project}\nENTRYPOINT [ \"/usr/local/bin/npm\", \"run\" ]\nCMD [ \"start\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "Makefile",
    "content": "#!/bin/make -f\n# -*- makefile -*-\n# SPDX-License-Identifier: MPL-2.0\n#{\n# Copyright 2018-present Samsung Electronics France SAS, and other contributors\n#\n# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/.*\n#}\n\ndefault: help all\n\ntmp_dir ?= tmp\nruntime ?= node\nexport runtime\neslint ?= node_modules/eslint/bin/eslint.js\ntsc ?= node_modules/typescript/bin/tsc\nsrcs ?= $(wildcard *.ts lib/*.ts | sort | uniq)\nrun_args ?=\nrun_timeout ?= 10\n\nmain_src ?= example/multiple-things.js\nNODE_PATH := .:${NODE_PATH}\nexport NODE_PATH\n\n\nport?=8888\nurl?=http://localhost:${port}\n\nhelp:\n\t@echo \"## Usage: \"\n\t@echo \"# make start # To start default application\"\n\t@echo \"# make test # To test default application\"\n\nall: build\n\nsetup/%:\n\t${@F}\n\nnode_modules: package.json\n\tnpm install\n\nmodules: ${runtime}_modules\n\tls $<\n\npackage-lock.json: package.json\n\trm -fv \"$@\"\n\tnpm install\n\tls \"$@\"\n\nsetup/node: node_modules\n\t@echo \"NODE_PATH=$${NODE_PATH}\"\n\tnode --version\n\tnpm --version\n\nsetup: setup/${runtime}\n\nbuild/%: setup\n\t@echo \"log: $@: $^\"\n\nbuild/node: setup node_modules eslint\n\nbuild: build/${runtime}\n\nrun/%: ${main_src} build\n\t${@F} $< ${run_args}\n\nrun/npm: ${main_src} setup\n\tnpm start\n\nrun: run/${runtime}\n\nclean:\n\trm -rf ${tmp_dir}\n\ncleanall: clean\n\trm -f *~\n\ndistclean: cleanall\n\trm -rf node_modules\n\n${tmp_dir}/rule/test/pid/%: ${main_src} build modules\n\t@mkdir -p \"${@D}\"\n\t${@F} $< & echo $$! > \"$@\"\n\tsleep ${run_timeout}\n\tcat $@\n\ntest/%: ${tmp_dir}/rule/test/pid/%\n\tcat $<\n\tcurl ${url} || curl -I ${url}\n\t@echo \"\"\n\tcurl --fail ${url}/0/properties\n\t@echo \"\"\n\tcurl --fail ${url}/1/properties\n\t@echo \"\"\n\tkill $$(cat $<) ||:\n\tkill -9 $$(cat $<) ||:\n\ntest/npm: package.json\n\tnpm test\n\ntest: test/${runtime}\n\nstart: run\n\nstart/board/%: example/platform/Makefile example/platform/board/%.js\n\t${MAKE} -C ${<D} board/${@F}\n\ncheck/%: ${srcs}\n\t${MAKE} setup\n\t@echo \"log: SHELL=$${SHELL}\"\n\tstatus=0 ; \\\n for src in $^; do \\\n echo \"log: check: $${src}: ($@)\" ; \\\n ${@F} $${src} \\\n && echo \"log: check: $${src}: OK\" \\\n || status=1 ; \\\n done ; \\\n\texit $${status}\n\ncheck/npm:\n\tnpm run lint\n\ncheck: check/${runtime}\n\neslint: .eslintrc.js ${eslint}\n\t@rm -rf tmp/dist\n\t${eslint} --no-color --fix . ||:\n\t${eslint} --no-color .\n\t${tsc}\n\tgit diff --exit-code\n\neslint/setup: node_modules\n\tls ${eslint} || npm install eslint-plugin-node eslint\n\t${eslint} --version\n\n${eslint}:\n\tls $@ || make eslint/setup\n\ttouch $@\n\n.eslintrc.js: ${eslint}\n\tls $@ || $< --init\n\nlint/%: eslint\n\tsync\n\nlint: lint/${runtime}\n\tsync\n"
  },
  {
    "path": "README.md",
    "content": "# webthing\n\n[![Build Status](https://github.com/WebThingsIO/webthing-node/workflows/Node.js%20package/badge.svg)](https://github.com/WebThingsIO/webthing-node/workflows/Node.js%20package)\n[![NPM](https://img.shields.io/npm/v/webthing.svg)](https://www.npmjs.com/package/webthing)\n[![license](https://img.shields.io/badge/license-MPL--2.0-blue.svg)](LICENSE)\n\nImplementation of an HTTP [Web Thing](https://iot.mozilla.org/wot/).\n\n# Installation\n\n`webthing` can be installed via `npm`, as such:\n\n```shell\n$ npm install webthing\n```\n\n# Example\n\nIn 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).\n\n## Dimmable Light\n\nImagine 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:\n* `on`: the state of the light, whether it is turned on or off\n    * Setting this property via a `PUT {\"on\": true/false}` call to the REST API toggles the light.\n* `brightness`: the brightness level of the light from 0-100%\n    * Setting this property via a PUT call to the REST API sets the brightness level of this light.\n\nFirst we create a new Thing:\n\n```javascript\nconst light = new Thing('urn:dev:ops:my-lamp-1234',\n                        'My Lamp',\n                        ['OnOffSwitch', 'Light'],\n                        'A web connected lamp');\n```\n\nNow we can add the required properties.\n\nThe **`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.\n\n```javascript\nlight.addProperty(\n  new Property(\n    light,\n    'on',\n    new Value(true, (v) => console.log('On-State is now', v)),\n    {\n      '@type': 'OnOffProperty',\n      title: 'On/Off',\n      type: 'boolean',\n      description: 'Whether the lamp is turned on',\n    }));\n```\n\nThe **`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.\n\n```javascript\nlight.addProperty(\n  new Property(\n    light,\n    'brightness',\n    new Value(50, v => console.log('Brightness is now', v)),\n    {\n      '@type': 'BrightnessProperty',\n      title: 'Brightness',\n      type: 'number',\n      description: 'The level of light from 0-100',\n      minimum: 0,\n      maximum: 100,\n      unit: 'percent',\n    }));\n```\n\nNow we can add our newly created thing to the server and start it:\n\n```javascript\n// If adding more than one thing, use MultipleThings() with a name.\n// In the single thing case, the thing's name will be broadcast.\nconst server = new WebThingServer(SingleThing(light), 8888);\n\nprocess.on('SIGINT', () => {\n  server.stop().then(() => process.exit()).catch(() => process.exit());\n});\n\nserver.start().catch(console.error);\n```\n\nThis 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.\n\n## Sensor\n\nLet's now also connect a humidity sensor to the server we set up for our light.\n\nA [`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.\n\nFirst we create a new Thing:\n\n```javascript\nconst sensor = new Thing('urn:dev:ops:my-humidity-sensor-1234',\n                         'My Humidity Sensor',\n                         ['MultiLevelSensor'],\n                         'A web connected humidity sensor');\n```\n\nThen we create and add the appropriate property:\n* `level`: tells us what the sensor is actually reading\n    * 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.\n\n    ```javascript\n    const level = new Value(0.0);\n\n    sensor.addProperty(\n      new Property(\n        sensor,\n        'level',\n        level,\n        {\n          '@type': 'LevelProperty',\n          title: 'Humidity',\n          type: 'number',\n          description: 'The current humidity in %',\n          minimum: 0,\n          maximum: 100,\n          unit: 'percent',\n          readOnly: true,\n        }));\n    ```\n\nNow 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.\n\n```javascript\n// Poll the sensor reading every 3 seconds\nsetInterval(() => {\n  // Update the underlying value, which in turn notifies all listeners\n  level.notifyOfExternalUpdate(readFromGPIO());\n}, 3000);\n```\n\nThis 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.\n\n# Adding to Gateway\n\nTo 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).\n\n# Resources\n\n* https://iot.mozilla.org/things/\n* https://hacks.mozilla.org/2018/05/creating-web-things-with-python-node-js-and-java/\n* https://nodejs.org/en/\n* https://github.com/rzr/webthing-iotjs/wiki\n* https://youtu.be/Z-oiFl6gwGw\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "# -*- coding: utf-8 -*-\n# SPDX-License-Identifier: MPL-2.0\n#{\n# Copyright: 2018-present Samsung Electronics France SAS, and other contributors\n#\n# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/ .\n#}\n\nversion: \"2\"\n\nservices:\n  web:\n    build: .\n    command: start\n    volumes:\n      - /sys:/sys\n    ports:\n      - \"8888:8888\"\n    network_mode: \"host\"\n"
  },
  {
    "path": "example/multiple-things.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n\nconst {\n  Action,\n  Event,\n  MultipleThings,\n  Property,\n  Thing,\n  Value,\n  WebThingServer,\n} = require('webthing');\nconst { v4: uuidv4 } = require('uuid');\n\nclass OverheatedEvent extends Event {\n  constructor(thing, data) {\n    super(thing, 'overheated', data);\n  }\n}\n\nclass FadeAction extends Action {\n  constructor(thing, input) {\n    super(uuidv4(), thing, 'fade', input);\n  }\n\n  performAction() {\n    return new Promise((resolve) => {\n      setTimeout(() => {\n        this.thing.setProperty('brightness', this.input.brightness);\n        this.thing.addEvent(new OverheatedEvent(this.thing, 102));\n        resolve();\n      }, this.input.duration);\n    });\n  }\n}\n\n/**\n * A dimmable light that logs received commands to stdout.\n */\nclass ExampleDimmableLight extends Thing {\n  constructor() {\n    super('urn:dev:ops:my-lamp-1234', 'My Lamp', ['OnOffSwitch', 'Light'], 'A web connected lamp');\n\n    this.addProperty(\n      new Property(this, 'on', new Value(true, (v) => console.log('On-State is now', v)), {\n        '@type': 'OnOffProperty',\n        title: 'On/Off',\n        type: 'boolean',\n        description: 'Whether the lamp is turned on',\n      })\n    );\n\n    this.addProperty(\n      new Property(this, 'brightness', new Value(50, (v) => console.log('Brightness is now', v)), {\n        '@type': 'BrightnessProperty',\n        title: 'Brightness',\n        type: 'integer',\n        description: 'The level of light from 0-100',\n        minimum: 0,\n        maximum: 100,\n        unit: 'percent',\n      })\n    );\n\n    this.addAvailableAction(\n      'fade',\n      {\n        title: 'Fade',\n        description: 'Fade the lamp to a given level',\n        input: {\n          type: 'object',\n          required: ['brightness', 'duration'],\n          properties: {\n            brightness: {\n              type: 'integer',\n              minimum: 0,\n              maximum: 100,\n              unit: 'percent',\n            },\n            duration: {\n              type: 'integer',\n              minimum: 1,\n              unit: 'milliseconds',\n            },\n          },\n        },\n      },\n      FadeAction\n    );\n\n    this.addAvailableEvent('overheated', {\n      description: 'The lamp has exceeded its safe operating temperature',\n      type: 'number',\n      unit: 'degree celsius',\n    });\n  }\n}\n\n/**\n * A humidity sensor which updates its measurement every few seconds.\n */\nclass FakeGpioHumiditySensor extends Thing {\n  constructor() {\n    super(\n      'urn:dev:ops:my-humidity-sensor-1234',\n      'My Humidity Sensor',\n      ['MultiLevelSensor'],\n      'A web connected humidity sensor'\n    );\n\n    this.level = new Value(0.0);\n    this.addProperty(\n      new Property(this, 'level', this.level, {\n        '@type': 'LevelProperty',\n        title: 'Humidity',\n        type: 'number',\n        description: 'The current humidity in %',\n        minimum: 0,\n        maximum: 100,\n        unit: 'percent',\n        readOnly: true,\n      })\n    );\n\n    // Poll the sensor reading every 3 seconds\n    setInterval(() => {\n      // Update the underlying value, which in turn notifies all listeners\n      const newLevel = this.readFromGPIO();\n      console.log('setting new humidity level:', newLevel);\n      this.level.notifyOfExternalUpdate(newLevel);\n    }, 3000);\n  }\n\n  /**\n   * Mimic an actual sensor updating its reading every couple seconds.\n   */\n  readFromGPIO() {\n    return Math.abs(70.0 * Math.random() * (-0.5 + Math.random()));\n  }\n}\n\nfunction runServer() {\n  // Create a thing that represents a dimmable light\n  const light = new ExampleDimmableLight();\n\n  // Create a thing that represents a humidity sensor\n  const sensor = new FakeGpioHumiditySensor();\n\n  // If adding more than one thing, use MultipleThings() with a name.\n  // In the single thing case, the thing's name will be broadcast.\n  const server = new WebThingServer(\n    new MultipleThings([light, sensor], 'LightAndTempDevice'),\n    8888\n  );\n\n  process.on('SIGINT', () => {\n    server\n      .stop()\n      .then(() => process.exit())\n      .catch(() => process.exit());\n  });\n\n  server.start().catch(console.error);\n}\n\nrunServer();\n"
  },
  {
    "path": "example/package.json",
    "content": "{\n  \"name\": \"example\",\n  \"version\": \"1.0.0\",\n  \"author\": \"WebThingsIO\",\n  \"license\": \"MPL-2.0\",\n  \"dependencies\": {\n    \"webthing\": \"file:..\"\n  }\n}\n"
  },
  {
    "path": "example/platform/Makefile",
    "content": "#!/bin/make -f\n# -*- makefile -*-\n# SPDX-License-Identifier: MPL-2.0\n#{\n# Copyright 2018-present Samsung Electronics France SAS, and other contributors\n#\n# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/.*\n#}\n\ndefault: help all\n\nmain_src ?= index.js\nlib_srcs ?= $(wildcard */*.js | sort)\nruntime ?= node\ntopreldir ?= ../..\ntopdir ?= ${CURDIR}/${topreldir}\nrun_args ?=\nnpm_args ?= start\nsudo ?= sudo\ngpio ?= gpio\ntmp_dir ?= tmp\n\nexport PATH\nNODE_PATH := ${topreldir}:${NODE_PATH}\nexport NODE_PATH\n\nrpi_gpio ?= 11\nrpi_gpio_list ?= 13 19 26\nedison_gpio ?= 12\n\nhelp:\n\t@echo \"Usage:\"\n\t@echo '# make start'\n\t@echo '# make $${board}'\n\t@echo '# make artik1020'\n\t@echo '# make edison'\n\t@echo '# make flex-phat'\n\t@echo '# make play-phat'\n\t@echo '# make traffic-phat'\n\n%: %/${runtime}\n\techo \"# $@: $^\"\n\nall: check\n\nsetup/node: ${topreldir}/node_modules node_modules\n\t@echo \"NODE_PATH=$${NODE_PATH}\"\n\t@echo \"$@: $^\"\n\nrun/%: /sys/class/gpio/export ${main_src} setup\n\tls -l $<\n\t-which ${@F}\n\t${@F} ${main_src} ${run_args}\n\nsudo/run/%: /sys/class/gpio/export ${main_src} setup\n\tls -l $<\n\t-which ${@F}\n\t${sudo} env \"PATH=${PATH}\" ${@F} ${main_src} ${run_args}\n\nrun/npm: /sys/class/gpio/export package.json setup\n\tls -l $<\n\tnpm run ${npm_args} ${run_args}\n\nrun: run/${runtime}\n\tsync\n\nstart: run\n\tsync\n\nforce:\n\n/sys/kernel/debug/gpio:\n\t${sudo} ls -l $@\n\n/sys/class/gpio/export: /sys/kernel/debug/gpio force\n\t${sudo} cat $<\n\nnode_modules: package.json\n\t-which npm\n\tnpm --version\n\tnpm install\n\t@mkdir -p \"$@\"\n\tln -fs ../../.. ${@}/webthing\n\npackage.json:\n\tnpm init\n\n${topreldir}/node_modules: ${topreldir}/package.json\n\tcd ${@D} && npm install\n\n\ncheck/%: ${lib_srcs}\n\t${MAKE} setup\n\tstatus=0 ; \\\n\tfor src in $^; do \\\n echo \"log: check: $${src}: ($@)\" ; \\\n ${@F} $${src} \\\n && echo \"log: check: $${src}: OK\" \\\n || status=1 ; \\\n done ; \\\n\texit $${status}\n\ncheck: check/${runtime}\n\nboard/%: ${main_src} board/%.js /sys/class/gpio/export setup\n\t${runtime} $< ${@F}\n\n/sys/class/gpio/gpio${edison_gpio}: /sys/class/gpio/export\n\techo ${edison_gpio} | ${sudo} tee $<\n\tls -l $@\n\nartik1020/%: ${main_src}\n\t${MAKE} sudo/run/${@F} run_args=\"${@D}\"\n\nedison/%: /sys/class/gpio/gpio${edison_gpio} ${main_src}\n\techo out | ${sudo} tee ${<}/direction\n\techo 0 | ${sudo} tee ${<}/value\n\t${sudo} cat /sys/kernel/debug/gpio_debug/gpio${edison_gpio}/current_pinmux # mode0\n\techo mode1 | ${sudo} tee /sys/kernel/debug/gpio_debug/gpio${edison_gpio}/current_pinmux\n\t${MAKE} sudo/run/${@F} run_args=\"${@D}\"\n\ngpio: /sys/class/gpio/export \n\t-${gpio} -v || ${sudo} apt-get install gpio || echo \"TODO: install BCM tool\"\n\t-${gpio} -v\n\nflex-phat/%: ${main_src} gpio\n\t${gpio} -g mode ${rpi_gpio} up\n\t${MAKE} run/${@F} run_args=\"${@D}\"\n\nplay-phat/%: ${main_src} /sys/class/gpio/export\n\t-lsmod | grep gpio_keys \\\n && ${sudo} modprobe -rv gpio_keys \\\n || echo \"log: will use /sys/class/gpio/\"\n\t${MAKE} run/${@F} run_args=\"${@D}\"\n\ntraffic-phat/%: ${main_src} gpio\n\t for num in ${rpi_gpio_list} ; do \\\n ${sudo} ${gpio} export $${num} in ; \\\n ${sudo} ${gpio} -g mode $${num} up ; \\\n ${sudo} ${gpio} unexport $${num} ; \\\n done\n\t${MAKE} run/${@F} run_args=\"${@D}\"\n"
  },
  {
    "path": "example/platform/adc/adc-property.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung Electronics France SAS, and other contributors\n *\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/.*\n */\n\nconst console = require('console');\n\n// Disable logs here by editing to '!console.log'\nconst log = console.log || function () {};\nconst verbose = !console.log || function () {};\n\nconst { Property, Value } = require('webthing');\n\nconst adc = require('../adc');\n\nclass AdcInProperty extends Property {\n  constructor(thing, name, value, metadata, config) {\n    super(thing, name, new Value(Number(value)), {\n      '@type': 'LevelProperty',\n      title: (metadata && metadata.title) || `Level: ${name}`,\n      type: 'number',\n      readOnly: true,\n      description: (metadata && metadata.description) || `ADC Sensor on pin=${config.pin}`,\n    });\n    const self = this;\n    config.frequency = config.frequency || 1;\n    config.range = config.range || 0xfff;\n    this.period = 1000.0 / config.frequency;\n    this.config = config;\n    this.port = adc.open(config, (err) => {\n      log(`log: ADC: ${self.getName()}: open: ${err} (null expected)`);\n      if (err) {\n        console.error(`error: ADC: ${self.getName()}: Fail to open:\\\n ${config.pin}`);\n        return null;\n      }\n      self.inverval = setInterval(() => {\n        let value = Number(self.port.readSync());\n        verbose(`log: ADC:\\\n ${self.getName()}: update: 0x${value.toString(0xf)}`);\n        value = Number(Math.floor((100.0 * value) / self.config.range));\n        if (value !== self.lastValue) {\n          log(`log: ADC: ${self.getName()}: change: ${value}%`);\n          self.value.notifyOfExternalUpdate(value);\n          self.lastValue = value;\n        }\n      }, self.period);\n    });\n  }\n\n  close() {\n    try {\n      this.inverval && clearInterval(this.inverval);\n      this.port && this.port.closeSync();\n    } catch (err) {\n      console.error(`error: ADC: ${this.getName()} close:${err}`);\n      return err;\n    }\n    log(`log: ADC: ${this.getName()}: close:`);\n  }\n}\n\nfunction AdcProperty(thing, name, value, metadata, config) {\n  if (config.direction === 'in') {\n    return new AdcInProperty(thing, name, value, metadata, config);\n  }\n  throw 'error: Invalid param';\n}\n\nmodule.exports = AdcProperty;\n"
  },
  {
    "path": "example/platform/board/artik1020.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung Electronics France SAS, and other contributors\n *\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/\n */\n\nconst { Thing } = require('webthing');\n\nconst AdcProperty = require('../adc/adc-property');\nconst PwmProperty = require('../pwm/pwm-property');\n\nclass ARTIK1020Thing extends Thing {\n  constructor(name, type, description) {\n    super(\n      'urn:dev:ops:my-artik1020-1234',\n      name || 'ARTIK1020',\n      type || [],\n      description || 'A web connected ARTIK1020'\n    );\n    const self = this;\n    this.pinProperties = [\n      new AdcProperty(\n        this,\n        'ADC0',\n        0,\n        { description: 'A0 on J24 of board' },\n        { direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\\\n/in_voltage0_raw' }\n      ),\n      new AdcProperty(\n        this,\n        'ADC1',\n        0,\n        { description: 'A1 on J24 of board' },\n        { direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\\\n/in_voltage1_raw' }\n      ),\n      new AdcProperty(\n        this,\n        'ADC2',\n        0,\n        { description: 'A2 on J24 of board' },\n        { direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\\\n/in_voltage2_raw' }\n      ),\n      new AdcProperty(\n        this,\n        'ADC3',\n        0,\n        { description: 'A3 on J24 of board' },\n        { direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\\\n/in_voltage5_raw' }\n      ),\n      new AdcProperty(\n        this,\n        'ADC4',\n        0,\n        { description: 'A4 on J24 of board' },\n        { direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\\\n/in_voltage6_raw' }\n      ),\n      new AdcProperty(\n        this,\n        'ADC5',\n        0,\n        { description: 'A5 on J24 of board' },\n        { direction: 'in', device: '/sys/devices/12d10000.adc/iio:device0\\\n/in_voltage7_raw' }\n      ),\n      new PwmProperty(this, 'PWM0', 50, { description: 'XPWMO1 on J26[6] of board (pwm0)' }),\n\n      new PwmProperty(\n        this,\n        'PWM1',\n        50,\n        { description: 'XPWMO0 on J26[5] of board (pwm1)' },\n        { pwm: { pin: 1 } }\n      ),\n    ];\n    this.pinProperties.forEach((property) => {\n      self.addProperty(property);\n    });\n  }\n\n  close() {\n    this.pinProperties.forEach((property) => {\n      property.close && property.close();\n    });\n  }\n}\n\nmodule.exports = function () {\n  if (!module.exports.instance) {\n    module.exports.instance = new ARTIK1020Thing();\n  }\n  return module.exports.instance;\n};\n"
  },
  {
    "path": "example/platform/board/artik530.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung Electronics France SAS, and other contributors\n *\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/.*\n */\n\nconst { Thing } = require('webthing');\n\nconst AdcProperty = require('../adc/adc-property');\nconst GpioProperty = require('../gpio/gpio-property');\n\nclass ARTIK530Thing extends Thing {\n  constructor(name, type, description) {\n    super(\n      'urn:dev:ops:my-artik530-1234',\n      name || 'ARTIK530',\n      type || [],\n      description || 'A web connected ARTIK530 or ARTIK720'\n    );\n    const self = this;\n    this.pinProperties = [\n      new GpioProperty(\n        this,\n        'RedLED',\n        false,\n        { description: 'Red LED on interposer board (on GPIO28)' },\n        { direction: 'out', pin: 28 }\n      ),\n      new GpioProperty(\n        this,\n        'BlueLED',\n        false,\n        { description: 'Blue LED on interposer board (on GPIO38)' },\n        { direction: 'out', pin: 38 }\n      ),\n      new GpioProperty(\n        this,\n        'Up',\n        true,\n        { description: 'SW403 Button: Nearest board edge,\\\n next to red LED (on GPIO30)' },\n        { direction: 'in', pin: 30 }\n      ),\n      new GpioProperty(\n        this,\n        'Down',\n        true,\n        { description: 'SW404 Button: Next to blue LED (on GPIO32)' },\n        { direction: 'in', pin: 32 }\n      ),\n      new AdcProperty(\n        this,\n        'ADC0',\n        0,\n        { description: 'Analog port of ARTIK05x' },\n        {\n          direction: 'in',\n          device: '/sys/bus/platform/devices\\\n/c0053000.adc/iio:device0/in_voltage0_raw',\n        }\n      ),\n      new AdcProperty(\n        this,\n        'ADC1',\n        0,\n        { description: 'Analog port of ARTIK05x' },\n        {\n          direction: 'in',\n          device: '/sys/bus/platform/devices/\\\nc0053000.adc/iio:device0/in_voltage1_raw',\n        }\n      ),\n    ];\n    this.pinProperties.forEach((property) => {\n      self.addProperty(property);\n    });\n  }\n\n  close() {\n    this.pinProperties.forEach((property) => {\n      property.close && property.close();\n    });\n  }\n}\n\nmodule.exports = function () {\n  if (!module.exports.instance) {\n    module.exports.instance = new ARTIK530Thing();\n  }\n  return module.exports.instance;\n};\n"
  },
  {
    "path": "example/platform/board/edison.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung Electronics France SAS, and other contributors\n *\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/.*\n */\n\nconst { Thing } = require('webthing');\n\nconst PwmProperty = require('../pwm/pwm-property');\n\nclass EdisonThing extends Thing {\n  constructor(name, type, description) {\n    super(\n      'urn:dev:ops:my-edison-1234',\n      name || 'Edison',\n      type || [],\n      description || 'A web connected Edison'\n    );\n    const self = this;\n    this.pinProperties = [\n      new PwmProperty(this, 'PWM0', 50, {\n        description: 'Analog port of Edison',\n      }),\n    ];\n    this.pinProperties.forEach((property) => {\n      self.addProperty(property);\n    });\n  }\n\n  close() {\n    this.pinProperties.forEach((property) => {\n      property.close && property.close();\n    });\n  }\n}\n\nmodule.exports = function () {\n  if (!module.exports.instance) {\n    module.exports.instance = new EdisonThing();\n  }\n  return module.exports.instance;\n};\n"
  },
  {
    "path": "example/platform/board/flex-phat.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung Electronics France SAS, and other contributors\n *\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/.*\n */\n\nconst { Thing } = require('webthing');\n\nconst GpioProperty = require('../gpio/gpio-property');\n\nclass FlexPHatThing extends Thing {\n  constructor(name, type, description) {\n    super(\n      'urn:dev:ops:my-flex-phat-1234',\n      name || 'FlexPHat',\n      type || [],\n      description || 'A web connected Flex RaspberryPi Hat'\n    );\n    const self = this;\n    this.gpioProperties = [\n      new GpioProperty(\n        this,\n        'Relay',\n        false,\n        { description: 'Actuator (on GPIO5)' },\n        { direction: 'out', pin: 5 }\n      ),\n      new GpioProperty(\n        this,\n        'BlueLED',\n        false,\n        { description: 'Actuator (on GPIO13)' },\n        { direction: 'out', pin: 13 }\n      ),\n      new GpioProperty(\n        this,\n        'GreenLED',\n        false,\n        { description: 'Actuator (on GPIO19)' },\n        { direction: 'out', pin: 19 }\n      ),\n      new GpioProperty(\n        this,\n        'RedLED',\n        false,\n        { description: 'Actuator (on GPIO26)' },\n        { direction: 'out', pin: 26 }\n      ),\n      new GpioProperty(\n        this,\n        'Button',\n        false,\n        { description: 'Push Button (on GPIO11)' },\n        { direction: 'in', pin: 11 }\n      ),\n      new GpioProperty(\n        this,\n        'GPIO23',\n        false,\n        { description: 'Input on GPIO 23 (unwired but modable)' },\n        { direction: 'in', pin: 23 }\n      ),\n    ];\n    this.gpioProperties.forEach((property) => {\n      self.addProperty(property);\n    });\n  }\n\n  close() {\n    this.gpioProperties.forEach((property) => {\n      property.close && property.close();\n    });\n  }\n}\n\nmodule.exports = function () {\n  if (!module.exports.instance) {\n    module.exports.instance = new FlexPHatThing();\n  }\n  return module.exports.instance;\n};\n"
  },
  {
    "path": "example/platform/board/play-phat.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung Electronics France SAS, and other contributors\n *\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/.*\n */\n\nconst { Thing } = require('webthing');\n\nconst GpioProperty = require('../gpio/gpio-property');\n\nclass PlayPHatThing extends Thing {\n  constructor(name, type, description) {\n    super(\n      'urn:dev:ops:my-play-phat-1234',\n      name || 'PlayPHat',\n      type || [],\n      description || 'A web connected Play RaspberryPi Hat'\n    );\n    const self = this;\n    this.gpioProperties = [\n      new GpioProperty(\n        this,\n        'Left',\n        false,\n        { description: 'SW1 Sensor Button on GPIO4 (Pin7)' },\n        { direction: 'in', pin: 4 }\n      ),\n      new GpioProperty(\n        this,\n        'Right',\n        false,\n        { description: 'SW2 Sensor button on GPIO17 (Pin11)' },\n        { direction: 'in', pin: 17 }\n      ),\n      new GpioProperty(\n        this,\n        'Up',\n        false,\n        { description: 'SW3 Sensor button on GPIO22 (Pin15)' },\n        { direction: 'in', pin: 22 }\n      ),\n      new GpioProperty(\n        this,\n        'Down',\n        false,\n        { description: 'SW4 Sensor button on GPIO27 (Pin13)' },\n        { direction: 'in', pin: 27 }\n      ),\n      new GpioProperty(\n        this,\n        'A',\n        false,\n        { description: 'SW5 Sensor button on GPIO19 (Pin35)' },\n        { direction: 'in', pin: 19 }\n      ),\n      new GpioProperty(\n        this,\n        'B',\n        false,\n        { description: 'SW6 Sensor button on GPIO26 (Pin37)' },\n        { direction: 'in', pin: 26 }\n      ),\n      new GpioProperty(\n        this,\n        'Start',\n        false,\n        { description: 'SW7 Sensor button on GPIO5 (Pin29)' },\n        { direction: 'in', pin: 5 }\n      ),\n      new GpioProperty(\n        this,\n        'Select',\n        false,\n        { description: 'SW8 Sensor button on GPIO6 (Pin31)' },\n        { direction: 'in', pin: 6 }\n      ),\n    ];\n    this.gpioProperties.forEach((property) => {\n      self.addProperty(property);\n    });\n  }\n\n  close() {\n    this.gpioProperties.forEach((property) => {\n      property.close && property.close();\n    });\n  }\n}\n\nmodule.exports = function () {\n  if (!module.exports.instance) {\n    module.exports.instance = new PlayPHatThing();\n  }\n  return module.exports.instance;\n};\n"
  },
  {
    "path": "example/platform/board/traffic-phat.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung Electronics France SAS, and other contributors\n *\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/.*\n */\n\nconst { Thing } = require('webthing');\n\nconst GpioProperty = require('../gpio/gpio-property');\n\nclass TrafficPHatThing extends Thing {\n  constructor(name, type, description) {\n    super(\n      'urn:dev:ops:my-traffic-phat-1234',\n      name || 'TrafficPHat',\n      type || [],\n      description || 'A web connected Traffic RaspberryPi Hat'\n    );\n    const self = this;\n    this.pinProperties = [\n      new GpioProperty(\n        this,\n        'Red',\n        false,\n        {\n          description: 'LED on GPIO2 (Pin2)',\n        },\n        {\n          direction: 'out',\n          pin: 2,\n        }\n      ),\n      new GpioProperty(\n        this,\n        'Orange',\n        false,\n        {\n          description: 'LED on GPIO3 (Pin5)',\n        },\n        {\n          direction: 'out',\n          pin: 3,\n        }\n      ),\n      new GpioProperty(\n        this,\n        'Green',\n        false,\n        {\n          description: 'LED on GPIO4 (Pin7)',\n        },\n        {\n          direction: 'out',\n          pin: 4,\n        }\n      ),\n      new GpioProperty(\n        this,\n        'B1',\n        true,\n        {\n          description: 'SW1 Sensor Button on GPIO3 (Pin33)',\n        },\n        {\n          direction: 'in',\n          pin: 13,\n        }\n      ),\n      new GpioProperty(\n        this,\n        'B2',\n        true,\n        {\n          description: 'SW2 Sensor button on GPIO19 (Pin35)',\n        },\n        {\n          direction: 'in',\n          pin: 19,\n        }\n      ),\n      new GpioProperty(\n        this,\n        'B3',\n        true,\n        {\n          description: 'SW3 Sensor button on GPIO26 (Pin37)',\n        },\n        {\n          direction: 'in',\n          pin: 26,\n        }\n      ),\n    ];\n    this.pinProperties.forEach((property) => {\n      self.addProperty(property);\n    });\n  }\n\n  close() {\n    this.pinProperties.forEach((property) => {\n      property.close && property.close();\n    });\n  }\n}\n\nmodule.exports = function () {\n  if (!module.exports.instance) {\n    module.exports.instance = new TrafficPHatThing();\n  }\n  return module.exports.instance;\n};\n"
  },
  {
    "path": "example/platform/gpio/gpio-property.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung Electronics France SAS, and other contributors\n *\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/.*\n */\n\nconst console = require('console');\n\n// Disable logs here by editing to '!console.log'\nconst log = console.log || function () {};\n\nconst { Property, Value } = require('webthing');\n\nconst gpio = require('gpio');\n\nclass GpioOutProperty extends Property {\n  constructor(thing, name, value, metadata, config) {\n    super(thing, name, new Value(Boolean(value)), {\n      '@type': 'OnOffProperty',\n      title: (metadata && metadata.title) || `On/Off: ${name}`,\n      type: 'boolean',\n      description: (metadata && metadata.description) || `GPIO Actuator on pin=${config.pin}`,\n    });\n    const self = this;\n    this.config = config;\n    this.port = gpio.export(config.pin, {\n      direction: 'out',\n      ready: () => {\n        log(`log: GPIO: ${self.getName()}: open:`);\n        self.value.valueForwarder = (value) => {\n          try {\n            log(`log: GPIO: ${self.getName()}: \\\nwriting: ${value}`);\n            self.port.set(value);\n          } catch (err) {\n            console.error(`error: GPIO: \n${self.getName()}: Fail to write: ${err}`);\n            return err;\n          }\n        };\n      },\n    });\n  }\n\n  close() {\n    try {\n      this.port && this.port.unexport(this.config.pin);\n    } catch (err) {\n      console.error(`error: GPIO: ${this.getName()}: Fail to close: ${err}`);\n      return err;\n    }\n    log(`log: GPIO: ${this.getName()}: close:`);\n  }\n}\n\nclass GpioInProperty extends Property {\n  constructor(thing, name, value, metadata, config) {\n    super(thing, name, new Value(Boolean(value)), {\n      '@type': 'BooleanProperty',\n      title: (metadata && metadata.title) || `On/Off: ${name}`,\n      type: 'boolean',\n      readOnly: true,\n      description: (metadata && metadata.description) || `GPIO Sensor on pin=${config.pin}`,\n    });\n    const self = this;\n    this.config = config;\n    const callback = () => {\n      log(`log: GPIO: ${self.getName()}: open:`);\n      self.port.on('change', (value) => {\n        value = Boolean(value);\n        log(`log: GPIO: ${self.getName()}: change: ${value}`);\n        self.value.notifyOfExternalUpdate(value);\n      });\n    };\n    this.port = gpio.export(config.pin, { direction: 'in', ready: callback });\n  }\n\n  close() {\n    try {\n      this.port && this.port.unexport(this.config.pin);\n    } catch (err) {\n      console.error(`error: GPIO: ${this.getName()} close:${err}`);\n      return err;\n    }\n    log(`log: GPIO: ${this.getName()}: close:`);\n  }\n}\n\nfunction GpioProperty(thing, name, value, metadata, config) {\n  if (config.direction === 'out') {\n    return new GpioOutProperty(thing, name, value, metadata, config);\n  } else if (config.direction === 'in') {\n    return new GpioInProperty(thing, name, value, metadata, config);\n  }\n  throw 'error: Invalid param';\n}\n\nmodule.exports = GpioProperty;\n"
  },
  {
    "path": "example/platform/package.json",
    "content": "{\n  \"name\": \"board-webthings\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Various Single Board computers's I/O implemented as webthings\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"NODE_PATH=.:../.. node index\",\n    \"flex-phat\": \"NODE_PATH=.:../.. node index flex-phat\",\n    \"play-phat\": \"NODE_PATH=.:../.. node index play-phat\"\n  },\n  \"author\": \"Philippe Coval <p.coval@samsung.com>\",\n  \"license\": \"MPL-2.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/rzr/webthing-iotjs.git\"\n  },\n  \"keywords\": [\n    \"webthing\",\n    \"wot\",\n    \"raspberry-pi\",\n    \"ARTIK\"\n  ],\n  \"bugs\": {\n    \"url\": \"https://github.com/rzr/webthing-iotjs/issues\"\n  },\n  \"bin\": {\n    \"board-webthings\": \"./index.js\"\n  },\n  \"homepage\": \"https://github.com/rzr/webthing-iotjs\",\n  \"dependencies\": {},\n  \"optionalDependencies\": {\n    \"gpio\": \"^0.2.10\",\n    \"pwm\": \"0.0.3\"\n  }\n}\n"
  },
  {
    "path": "example/platform/pwm/pwm-property.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n\n/**\n *\n * Copyright 2018-present Samsung Electronics France SAS, and other contributors\n *\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/\n */\nconst console = require('console');\n\n// Disable logs here by editing to '!console.log'\nconst log = console.log || function () {};\nconst verbose = !console.log || function () {};\n\nconst { Property, Value } = require('webthing');\n\nconst pwm = require('pwm');\n\nclass PwmOutProperty extends Property {\n  constructor(thing, name, value, metadata, config) {\n    if (typeof config === 'undefined') {\n      config = {};\n    }\n    super(thing, name, new Value(Number(value)), {\n      '@type': 'LevelProperty',\n      title: (metadata && metadata.title) || `PWM: ${name} (dutyCycle)`,\n      type: 'integer',\n      minimum: config.minimum || 0,\n      maximum: config.maximum || 100,\n      readOnly: false,\n      unit: 'percent',\n      description: (metadata && metadata.description) || `PWM DutyCycle`,\n    });\n    const self = this;\n    this.config = config;\n    if (typeof this.config.pwm == 'undefined') {\n      this.config.pwm = {};\n    }\n    if (typeof this.config.pwm.pin == 'undefined') {\n      this.config.pwm.pin = 0;\n    }\n\n    if (typeof this.config.pwm.chip == 'undefined') {\n      this.config.pwm.chip = 0;\n    }\n    // secs (eg: 50Hz = 20 ms = 0.02 sec)\n    if (typeof this.config.pwm.period == 'undefined') {\n      this.config.pwm.period = 0.02;\n    }\n    // [0..1]\n    if (typeof this.config.pwm.dutyCycle == 'undefined') {\n      this.config.pwm.dutyCycle = 0.5;\n    }\n    verbose(`log: opening: ${this.getName()}`);\n    this.port = pwm.export(this.config.pwm.chip, this.config.pwm.pin, (err) => {\n      verbose(`log: PWM: ${self.getName()}: open: ${err}`);\n      if (err) {\n        console.error(`error: PWM: ${self.getName()}: open: ${err}`);\n        throw err;\n      }\n      self.port.freq = 1 / self.config.pwm.period;\n      // Linux sysfs uses usecs units\n      self.port.setPeriod(self.config.pwm.period * 1e9, () => {\n        self.port.setDutyCycle(self.config.pwm.dutyCycle * (self.config.pwm.period * 1e9), () => {\n          self.port.setEnable(1, () => {\n            verbose(`log: ${self.getName()}: Enabled`);\n          });\n        });\n      });\n\n      self.value.valueForwarder = (value) => {\n        const usec = Math.floor(self.config.pwm.period * 1e9 * (Number(value) / 100.0));\n        self.port.setDutyCycle(usec, () => {\n          verbose(`log: setDutyCycle: usec=${usec}`);\n        });\n      };\n    });\n  }\n\n  close() {\n    verbose(`log: PWM: ${this.getName()}: close:`);\n    try {\n      this.port && this.port.unexport();\n    } catch (err) {\n      console.error(`error: PWM: ${this.getName()} close:${err}`);\n      return err;\n    }\n    log(`log: PWM: ${this.getName()}: close:`);\n  }\n}\n\nmodule.exports = PwmOutProperty;\n\nif (module.parent === null) {\n  new PwmOutProperty();\n}\n"
  },
  {
    "path": "example/simplest-thing.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n/**\n *\n * Copyright 2018-present Samsung Electronics France SAS, and other contributors\n *\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/.*\n */\nconst { Property, SingleThing, Thing, Value, WebThingServer } = require('webthing');\n\nfunction makeThing() {\n  const thing = new Thing(\n    'urn:dev:ops:my-actuator-1234',\n    'ActuatorExample',\n    ['OnOffSwitch'],\n    'An actuator example that just log'\n  );\n\n  thing.addProperty(\n    new Property(thing, 'on', new Value(true, (update) => console.log(`change: ${update}`)), {\n      '@type': 'OnOffProperty',\n      title: 'On/Off',\n      type: 'boolean',\n      description: 'Whether the output is changed',\n    })\n  );\n  return thing;\n}\n\nfunction runServer() {\n  const port = process.argv[2] ? Number(process.argv[2]) : 8888;\n  const url = `http://localhost:${port}/properties/on`;\n\n  console.log(`Usage:\\n\n${process.argv[0]} ${process.argv[1]} [port]\n\nTry:\ncurl -X PUT -H 'Content-Type: application/json' --data '{\"on\": true }' ${url}\n`);\n\n  const thing = makeThing();\n  const server = new WebThingServer(new SingleThing(thing), port);\n  process.on('SIGINT', () => {\n    server\n      .stop()\n      .then(() => process.exit())\n      .catch(() => process.exit());\n  });\n  server.start().catch(console.error);\n}\n\nrunServer();\n"
  },
  {
    "path": "example/single-thing.js",
    "content": "// -*- mode: js; js-indent-level:2;  -*-\n// SPDX-License-Identifier: MPL-2.0\n\nconst { Action, Event, Property, SingleThing, Thing, Value, WebThingServer } = require('webthing');\nconst { v4: uuidv4 } = require('uuid');\n\nclass OverheatedEvent extends Event {\n  constructor(thing, data) {\n    super(thing, 'overheated', data);\n  }\n}\n\nclass FadeAction extends Action {\n  constructor(thing, input) {\n    super(uuidv4(), thing, 'fade', input);\n  }\n\n  performAction() {\n    return new Promise((resolve) => {\n      setTimeout(() => {\n        this.thing.setProperty('brightness', this.input.brightness);\n        this.thing.addEvent(new OverheatedEvent(this.thing, 102));\n        resolve();\n      }, this.input.duration);\n    });\n  }\n}\n\nfunction makeThing() {\n  const thing = new Thing(\n    'urn:dev:ops:my-lamp-1234',\n    'My Lamp',\n    ['OnOffSwitch', 'Light'],\n    'A web connected lamp'\n  );\n\n  thing.addProperty(\n    new Property(thing, 'on', new Value(true), {\n      '@type': 'OnOffProperty',\n      title: 'On/Off',\n      type: 'boolean',\n      description: 'Whether the lamp is turned on',\n    })\n  );\n  thing.addProperty(\n    new Property(thing, 'brightness', new Value(50), {\n      '@type': 'BrightnessProperty',\n      title: 'Brightness',\n      type: 'integer',\n      description: 'The level of light from 0-100',\n      minimum: 0,\n      maximum: 100,\n      unit: 'percent',\n    })\n  );\n\n  thing.addAvailableAction(\n    'fade',\n    {\n      title: 'Fade',\n      description: 'Fade the lamp to a given level',\n      input: {\n        type: 'object',\n        required: ['brightness', 'duration'],\n        properties: {\n          brightness: {\n            type: 'integer',\n            minimum: 0,\n            maximum: 100,\n            unit: 'percent',\n          },\n          duration: {\n            type: 'integer',\n            minimum: 1,\n            unit: 'milliseconds',\n          },\n        },\n      },\n    },\n    FadeAction\n  );\n\n  thing.addAvailableEvent('overheated', {\n    description: 'The lamp has exceeded its safe operating temperature',\n    type: 'number',\n    unit: 'degree celsius',\n  });\n\n  return thing;\n}\n\nfunction runServer() {\n  const thing = makeThing();\n\n  // If adding more than one thing, use MultipleThings() with a name.\n  // In the single thing case, the thing's name will be broadcast.\n  const server = new WebThingServer(new SingleThing(thing), 8888);\n\n  process.on('SIGINT', () => {\n    server\n      .stop()\n      .then(() => process.exit())\n      .catch(() => process.exit());\n  });\n\n  server.start().catch(console.error);\n}\n\nrunServer();\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"webthing\",\n  \"version\": \"0.15.0\",\n  \"description\": \"HTTP Web Thing implementation\",\n  \"main\": \"lib/webthing.js\",\n  \"scripts\": {\n    \"lint\": \"tsc --noEmit && eslint . --ext .ts\",\n    \"node\": \"NODE_PATH=. node\",\n    \"start\": \"NODE_PATH=. node example/multiple-things\",\n    \"test\": \"make test\",\n    \"simplest\": \"NODE_PATH=. node example/simplest-thing\",\n    \"prettier\": \"npx prettier -w 'src/*.ts' 'example/**/*.{js,ts}'\",\n    \"build\": \"tsc -p .\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/WebThingsIO/webthing-node.git\"\n  },\n  \"keywords\": [\n    \"iot\",\n    \"web\",\n    \"thing\",\n    \"webthing\"\n  ],\n  \"author\": \"WebThingsIO\",\n  \"license\": \"MPL-2.0\",\n  \"bugs\": {\n    \"url\": \"https://github.com/WebThingsIO/webthing-node/issues\"\n  },\n  \"homepage\": \"https://github.com/WebThingsIO/webthing-node#readme\",\n  \"types\": \"lib/index.d.ts\",\n  \"dependencies\": {\n    \"ajv\": \"^7.0.4\",\n    \"body-parser\": \"^1.19.0\",\n    \"dnssd\": \"^0.4.1\",\n    \"express\": \"^4.17.1\",\n    \"express-ws\": \"^4.0.0\",\n    \"prettier\": \"^2.2.1\"\n  },\n  \"devDependencies\": {\n    \"@types/body-parser\": \"^1.19.0\",\n    \"@types/dnssd\": \"^0.4.1\",\n    \"@types/express\": \"^4.17.11\",\n    \"@types/express-ws\": \"^3.0.0\",\n    \"@types/node\": \"^14.14.25\",\n    \"@typescript-eslint/eslint-plugin\": \"^4.14.2\",\n    \"@typescript-eslint/parser\": \"^4.14.2\",\n    \"babel-eslint\": \"^10.1.0\",\n    \"eslint\": \"^7.19.0\",\n    \"eslint-config-prettier\": \"^7.2.0\",\n    \"typescript\": \"^4.1.3\",\n    \"uuid\": \"^8.3.2\"\n  }\n}\n"
  },
  {
    "path": "src/action.ts",
    "content": "/**\n * High-level Action base class implementation.\n */\n\nimport * as utils from './utils';\nimport { AnyType, Link, PrimitiveJsonType } from './types';\nimport Thing from './thing';\n\n/**\n * An Action represents an individual action on a thing.\n */\nclass Action<InputType = AnyType> {\n  private id: string;\n\n  private thing: Thing;\n\n  private name: string;\n\n  private input: InputType;\n\n  private hrefPrefix: string;\n\n  private href: string;\n\n  private status: string;\n\n  private timeRequested: string;\n\n  private timeCompleted: string | null;\n\n  constructor(id: string, thing: Thing, name: string, input: InputType) {\n    /**\n     * Initialize the object.\n     *\n     * @param {String} id ID of this action\n     * @param {Object} thing Thing this action belongs to\n     * @param {String} name Name of the action\n     * @param {Object} input Any action inputs\n     */\n    this.id = id;\n    this.thing = thing;\n    this.name = name;\n    this.input = input;\n    this.hrefPrefix = '';\n    this.href = `/actions/${this.name}/${this.id}`;\n    this.status = 'created';\n    this.timeRequested = utils.timestamp();\n    this.timeCompleted = null;\n  }\n\n  /**\n   * Get the action description.\n   *\n   * @returns {Object} Description of the action as an object.\n   */\n  asActionDescription(): Action.ActionDescription {\n    const description: Action.ActionDescription = {\n      [this.name]: {\n        href: this.hrefPrefix + this.href,\n        timeRequested: this.timeRequested,\n        status: this.status,\n      },\n    };\n\n    if (this.input !== null) {\n      description[this.name].input = <AnyType>(<unknown>this.input);\n    }\n\n    if (this.timeCompleted !== null) {\n      description[this.name].timeCompleted = this.timeCompleted;\n    }\n\n    return description;\n  }\n\n  /**\n   * Set the prefix of any hrefs associated with this action.\n   *\n   * @param {String} prefix The prefix\n   */\n  setHrefPrefix(prefix: string): void {\n    this.hrefPrefix = prefix;\n  }\n\n  /**\n   * Get this action's ID.\n   *\n   * @returns {String} The ID.\n   */\n  getId(): string {\n    return this.id;\n  }\n\n  /**\n   * Get this action's name.\n   *\n   * @returns {String} The name.\n   */\n  getName(): string {\n    return this.name;\n  }\n\n  /**\n   * Get this action's href.\n   *\n   * @returns {String} The href.\n   */\n  getHref(): string {\n    return this.hrefPrefix + this.href;\n  }\n\n  /**\n   * Get this action's status.\n   *\n   * @returns {String} The status.\n   */\n  getStatus(): string {\n    return this.status;\n  }\n\n  /**\n   * Get the thing associated with this action.\n   *\n   * @returns {Object} The thing.\n   */\n  getThing(): Thing {\n    return this.thing;\n  }\n\n  /**\n   * Get the time the action was requested.\n   *\n   * @returns {String} The time.\n   */\n  getTimeRequested(): string {\n    return this.timeRequested;\n  }\n\n  /**\n   * Get the time the action was completed.\n   *\n   * @returns {String} The time.\n   */\n  getTimeCompleted(): string | null {\n    return this.timeCompleted;\n  }\n\n  /**\n   * Get the inputs for this action.\n   *\n   * @returns {Object} The inputs.\n   */\n  getInput(): InputType {\n    return this.input;\n  }\n\n  /**\n   * Start performing the action.\n   */\n  start(): void {\n    this.status = 'pending';\n    this.thing.actionNotify(<Action<AnyType>>(<unknown>this));\n    this.performAction().then(\n      () => this.finish(),\n      () => this.finish()\n    );\n  }\n\n  /**\n   * Override this with the code necessary to perform the action.\n   *\n   * @returns {Object} Promise that resolves when the action is finished.\n   */\n  performAction(): Promise<void> {\n    return Promise.resolve();\n  }\n\n  /**\n   * Override this with the code necessary to cancel the action.\n   *\n   * @returns {Object} Promise that resolves when the action is cancelled.\n   */\n  cancel(): Promise<void> {\n    return Promise.resolve();\n  }\n\n  /**\n   * Finish performing the action.\n   */\n  finish(): void {\n    this.status = 'completed';\n    this.timeCompleted = utils.timestamp();\n    this.thing.actionNotify(<Action<AnyType>>(<unknown>this));\n  }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\ndeclare namespace Action {\n  interface ActionMetadata {\n    title?: string;\n    description?: string;\n    links?: Link[];\n    input?: {\n      type?: PrimitiveJsonType;\n      minimum?: number;\n      maximum?: number;\n      multipleOf?: number;\n      enum?: readonly string[] | readonly number[];\n    };\n  }\n\n  interface ActionDescription<InputType = AnyType> {\n    [name: string]: {\n      href: string;\n      timeRequested: string;\n      status: string;\n      input?: InputType;\n      timeCompleted?: string;\n    };\n  }\n\n  export interface ActionTypeClass<InputType = AnyType> {\n    new (thing: Thing, input: InputType): Action<InputType>;\n  }\n}\n\nexport = Action;\n"
  },
  {
    "path": "src/event.ts",
    "content": "/**\n * High-level Event base class implementation.\n */\n\nimport Thing from './thing';\nimport * as utils from './utils';\nimport { AnyType, PrimitiveJsonType, Link } from './types';\n\n/**\n * An Event represents an individual event from a thing.\n */\nclass Event<Data = AnyType> {\n  private thing: Thing;\n\n  private name: string;\n\n  private data: Data | null;\n\n  private time: string;\n\n  /**\n   * Initialize the object.\n   *\n   * @param {Object} thing Thing this event belongs to\n   * @param {String} name Name of the event\n   * @param {*} data (Optional) Data associated with the event\n   */\n  constructor(thing: Thing, name: string, data?: Data) {\n    this.thing = thing;\n    this.name = name;\n    this.data = typeof data !== 'undefined' ? data : null;\n    this.time = utils.timestamp();\n  }\n\n  /**\n   * Get the event description.\n   *\n   * @returns {Object} Description of the event as an object.\n   */\n  asEventDescription(): Event.EventDescription {\n    const description: Event.EventDescription = {\n      [this.name]: {\n        timestamp: this.time,\n      },\n    };\n\n    if (this.data !== null) {\n      description[this.name].data = <AnyType>(<unknown>this.data);\n    }\n\n    return description;\n  }\n\n  /**\n   * Get the thing associated with this event.\n   *\n   * @returns {Object} The thing.\n   */\n  getThing(): Thing {\n    return this.thing;\n  }\n\n  /**\n   * Get the event's name.\n   *\n   * @returns {String} The name.\n   */\n  getName(): string {\n    return this.name;\n  }\n\n  /**\n   * Get the event's data.\n   *\n   * @returns {*} The data.\n   */\n  getData(): Data | null {\n    return this.data;\n  }\n\n  /**\n   * Get the event's timestamp.\n   *\n   * @returns {String} The time.\n   */\n  getTime(): string {\n    return this.time;\n  }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\ndeclare namespace Event {\n  interface EventDescription {\n    [name: string]: {\n      timestamp: string;\n      data?: AnyType;\n    };\n  }\n\n  interface EventMetadata {\n    type?: PrimitiveJsonType;\n    '@type'?: string;\n    unit?: string;\n    title?: string;\n    description?: string;\n    links?: Link[];\n    minimum?: number;\n    maximum?: number;\n    multipleOf?: number;\n    enum?: readonly string[] | readonly number[];\n  }\n}\n\nexport = Event;\n"
  },
  {
    "path": "src/index.ts",
    "content": "export * from './webthing';\n"
  },
  {
    "path": "src/property.ts",
    "content": "/**\n * High-level Property base class implementation.\n */\n\nimport Ajv, { ValidateFunction } from 'ajv';\nimport Thing from './thing';\nimport Value from './value';\nimport { AnyType, PrimitiveJsonType, Link } from './types';\n\nconst ajv = new Ajv();\n\n/**\n * A Property represents an individual state value of a thing.\n */\nclass Property<ValueType = AnyType> {\n  private thing: Thing;\n\n  private name: string;\n\n  private value: Value<ValueType>;\n\n  private metadata: Property.PropertyMetadata;\n\n  private href: string;\n\n  private hrefPrefix: string;\n\n  private validate: ValidateFunction;\n\n  /**\n   * Initialize the object.\n   *\n   * @param {Thing} thing Thing this property belongs to\n   * @param {String} name Name of the property\n   * @param {Value} value Value object to hold the property value\n   * @param {Object} metadata Property metadata, i.e. type, description, unit,\n   *                          etc., as an object.\n   */\n  constructor(\n    thing: Thing,\n    name: string,\n    value: Value<ValueType>,\n    metadata: Property.PropertyMetadata\n  ) {\n    this.thing = thing;\n    this.name = name;\n    this.value = value;\n    this.hrefPrefix = '';\n    this.href = `/properties/${this.name}`;\n    this.metadata = JSON.parse(JSON.stringify(metadata || {}));\n\n    delete metadata.title;\n    delete metadata.unit;\n    delete metadata['@type'];\n    this.validate = ajv.compile(metadata);\n\n    // Add the property change observer to notify the Thing about a property\n    // change.\n    this.value.on('update', () => this.thing.propertyNotify(<Property<AnyType>>(<unknown>this)));\n  }\n\n  /**\n   * Validate new property value before setting it.\n   *\n   * @param {*} value - New value\n   * @throws Error if the property is readonly or is invalid\n   */\n  validateValue(value: ValueType): void {\n    if (this.metadata.hasOwnProperty('readOnly') && this.metadata.readOnly) {\n      throw new Error('Read-only property');\n    }\n\n    const valid = this.validate(value);\n    if (!valid) {\n      throw new Error('Invalid property value');\n    }\n  }\n\n  /**\n   * Get the property description.\n   *\n   * @returns {Object} Description of the property as an object.\n   */\n  asPropertyDescription(): Property.PropertyDescription {\n    const description = JSON.parse(JSON.stringify(this.metadata));\n\n    if (!description.hasOwnProperty('links')) {\n      description.links = [];\n    }\n\n    description.links.push({\n      rel: 'property',\n      href: this.hrefPrefix + this.href,\n    });\n    return description;\n  }\n\n  /**\n   * Set the prefix of any hrefs associated with this property.\n   *\n   * @param {String} prefix The prefix\n   */\n  setHrefPrefix(prefix: string): void {\n    this.hrefPrefix = prefix;\n  }\n\n  /**\n   * Get the href of this property.\n   *\n   * @returns {String} The href\n   */\n  getHref(): string {\n    return `${this.hrefPrefix}${this.href}`;\n  }\n\n  /**\n   * Get the current property value.\n   *\n   * @returns {*} The current value\n   */\n  getValue(): ValueType {\n    return this.value.get();\n  }\n\n  /**\n   * Set the current value of the property.\n   *\n   * @param {*} value The value to set\n   */\n  setValue(value: ValueType): void {\n    this.validateValue(value);\n    this.value.set(value);\n  }\n\n  /**\n   * Get the name of this property.\n   *\n   * @returns {String} The property name.\n   */\n  getName(): string {\n    return this.name;\n  }\n\n  /**\n   * Get the thing associated with this property.\n   *\n   * @returns {Object} The thing.\n   */\n  getThing(): Thing {\n    return this.thing;\n  }\n\n  /**\n   * Get the metadata associated with this property\n   *\n   * @returns {Object} The metadata\n   */\n  getMetadata(): Property.PropertyMetadata {\n    return this.metadata;\n  }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\ndeclare namespace Property {\n  // could we use .type to strongly type the enum, minimum and maximum?\n  interface PropertyMetadata {\n    type?: PrimitiveJsonType;\n    '@type'?: string;\n    unit?: string;\n    title?: string;\n    description?: string;\n    links?: Link[];\n    enum?: AnyType[];\n    readOnly?: boolean;\n    minimum?: number;\n    maximum?: number;\n    multipleOf?: number;\n  }\n\n  interface PropertyDescription extends PropertyMetadata {\n    links: Link[];\n  }\n}\n\nexport = Property;\n"
  },
  {
    "path": "src/server.ts",
    "content": "/**\n * Node Web Thing server implementation.\n */\n\nimport bodyParser from 'body-parser';\nimport * as dnssd from 'dnssd';\nimport express from 'express';\nimport expressWs from 'express-ws';\nimport * as http from 'http';\nimport * as https from 'https';\nimport * as os from 'os';\nimport * as utils from './utils';\nimport Thing from './thing';\nimport { AnyType } from './types';\n\n/**\n * A container for a single thing.\n */\nexport class SingleThing {\n  private thing: Thing;\n\n  /**\n   * Initialize the container.\n   *\n   * @param {Object} thing The thing to store\n   */\n  constructor(thing: Thing) {\n    this.thing = thing;\n  }\n\n  /**\n   * Get the thing at the given index.\n   */\n  getThing(): Thing {\n    return this.thing;\n  }\n\n  /**\n   * Get the list of things.\n   */\n  getThings(): Thing[] {\n    return [this.thing];\n  }\n\n  /**\n   * Get the mDNS server name.\n   */\n  getName(): string {\n    return this.thing.getTitle();\n  }\n}\n\n/**\n * A container for multiple things.\n */\nexport class MultipleThings {\n  private things: Thing[];\n\n  private name: string;\n\n  /**\n   * Initialize the container.\n   *\n   * @param {Object} things The things to store\n   * @param {String} name The mDNS server name\n   */\n  constructor(things: Thing[], name: string) {\n    this.things = things;\n    this.name = name;\n  }\n\n  /**\n   * Get the thing at the given index.\n   *\n   * @param {Number|String} idx The index\n   */\n  getThing(idx?: number | string): Thing | null {\n    idx = parseInt(idx as string);\n    if (isNaN(idx) || idx < 0 || idx >= this.things.length) {\n      return null;\n    }\n\n    return this.things[idx];\n  }\n\n  /**\n   * Get the list of things.\n   */\n  getThings(): Thing[] {\n    return this.things;\n  }\n\n  /**\n   * Get the mDNS server name.\n   */\n  getName(): string {\n    return this.name;\n  }\n}\n\n/**\n * Base handler that is initialized with a list of things.\n */\nabstract class BaseHandler {\n  protected things: SingleThing | MultipleThings;\n\n  /**\n   * Initialize the handler.\n   *\n   * @param {Object} things List of Things managed by the server\n   */\n  constructor(things: SingleThing | MultipleThings) {\n    this.things = things;\n  }\n\n  abstract get(req: express.Request, res: express.Response): void;\n\n  /**\n   * Get the thing this request is for.\n   *\n   * @param {Object} req The request object\n   * @returns {Object} The thing, or null if not found.\n   */\n  getThing(req: express.Request): Thing | null {\n    return this.things.getThing(req.params.thingId);\n  }\n}\n\n/**\n * Handle a request to / when the server manages multiple things.\n */\nclass ThingsHandler extends BaseHandler {\n  /**\n   * Handle a GET request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  get(req: express.Request, res: express.Response): void {\n    const wsHref = `${req.secure ? 'wss' : 'ws'}://${req.headers.host}`;\n    res.json(\n      this.things.getThings().map((thing) => {\n        const description = thing.asThingDescription();\n        description.href = thing.getHref();\n        description.links.push({\n          rel: 'alternate',\n          href: `${wsHref}${thing.getHref()}`,\n        });\n        description.base = `${req.protocol}://${req.headers.host}${thing.getHref()}`;\n        description.securityDefinitions = {\n          nosec_sc: {\n            scheme: 'nosec',\n          },\n        };\n        description.security = 'nosec_sc';\n        return description;\n      })\n    );\n  }\n}\n\n/**\n * Handle a request to /.\n */\nclass ThingHandler extends BaseHandler {\n  /**\n   * Handle a GET request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  get(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    const wsHref = `${req.secure ? 'wss' : 'ws'}://${req.headers.host}`;\n    const description = thing.asThingDescription();\n    description.links.push({\n      rel: 'alternate',\n      href: `${wsHref}${thing.getHref()}`,\n    });\n    description.base = `${req.protocol}://${req.headers.host}${thing.getHref()}`;\n    description.securityDefinitions = {\n      nosec_sc: {\n        scheme: 'nosec',\n      },\n    };\n    description.security = 'nosec_sc';\n\n    res.json(description);\n  }\n\n  /**\n   * Handle a websocket request.\n   *\n   * @param {Object} ws The websocket object\n   * @param {Object} req The request object\n   */\n  ws(ws: import('ws'), req: express.Request): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      ws.send(\n        JSON.stringify({\n          messageType: 'error',\n          data: {\n            status: '404 Not Found',\n            message: 'The requested thing was not found',\n          },\n        })\n      );\n      return;\n    }\n\n    thing.addSubscriber(ws);\n\n    ws.on('error', () => thing.removeSubscriber(ws));\n    ws.on('close', () => thing.removeSubscriber(ws));\n\n    ws.on('message', (msg) => {\n      let message: {\n        messageType: string;\n        data: Record<string, unknown>;\n      };\n      try {\n        message = JSON.parse(msg as string);\n      } catch (e1) {\n        try {\n          ws.send(\n            JSON.stringify({\n              messageType: 'error',\n              data: {\n                status: '400 Bad Request',\n                message: 'Parsing request failed',\n              },\n            })\n          );\n        } catch (e2) {\n          // do nothing\n        }\n\n        return;\n      }\n\n      if (!message.hasOwnProperty('messageType') || !message.hasOwnProperty('data')) {\n        try {\n          ws.send(\n            JSON.stringify({\n              messageType: 'error',\n              data: {\n                status: '400 Bad Request',\n                message: 'Invalid message',\n              },\n            })\n          );\n        } catch (e) {\n          // do nothing\n        }\n\n        return;\n      }\n\n      const messageType = message.messageType;\n      switch (messageType) {\n        case 'setProperty': {\n          for (const propertyName in message.data) {\n            try {\n              thing.setProperty(propertyName, <AnyType>message.data[propertyName]);\n            } catch (e) {\n              ws.send(\n                JSON.stringify({\n                  messageType: 'error',\n                  data: {\n                    status: '400 Bad Request',\n                    message: e.message,\n                  },\n                })\n              );\n            }\n          }\n\n          break;\n        }\n        case 'requestAction': {\n          for (const actionName in message.data) {\n            let input = null;\n            const actionData = <Record<string, unknown>>message.data[actionName];\n            if (actionData.hasOwnProperty('input')) {\n              input = actionData.input;\n            }\n\n            const action = thing.performAction(actionName, input);\n            if (action) {\n              action.start();\n            } else {\n              ws.send(\n                JSON.stringify({\n                  messageType: 'error',\n                  data: {\n                    status: '400 Bad Request',\n                    message: 'Invalid action request',\n                    request: message,\n                  },\n                })\n              );\n            }\n          }\n\n          break;\n        }\n        case 'addEventSubscription': {\n          for (const eventName in message.data) {\n            thing.addEventSubscriber(eventName, ws);\n          }\n\n          break;\n        }\n        default: {\n          try {\n            ws.send(\n              JSON.stringify({\n                messageType: 'error',\n                data: {\n                  status: '400 Bad Request',\n                  message: `Unknown messageType: ${messageType}`,\n                  request: message,\n                },\n              })\n            );\n          } catch (e) {\n            // do nothing\n          }\n        }\n      }\n    });\n  }\n}\n\n/**\n * Handle a request to /properties.\n */\nclass PropertiesHandler extends BaseHandler {\n  /**\n   * Handle a GET request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  get(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    res.json(thing.getProperties());\n  }\n}\n\n/**\n * Handle a request to /properties/<property>.\n */\nclass PropertyHandler extends BaseHandler {\n  /**\n   * Handle a GET request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  get(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    const propertyName = req.params.propertyName;\n    if (thing.hasProperty(propertyName)) {\n      res.json({ [propertyName]: thing.getProperty(propertyName) });\n    } else {\n      res.status(404).end();\n    }\n  }\n\n  /**\n   * Handle a PUT request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  put(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    const propertyName = req.params.propertyName;\n    if (!req.body.hasOwnProperty(propertyName)) {\n      res.status(400).end();\n      return;\n    }\n\n    if (thing.hasProperty(propertyName)) {\n      try {\n        thing.setProperty(propertyName, req.body[propertyName]);\n      } catch (e) {\n        res.status(400).end();\n        return;\n      }\n\n      res.json({ [propertyName]: thing.getProperty(propertyName) });\n    } else {\n      res.status(404).end();\n    }\n  }\n}\n\n/**\n * Handle a request to /actions.\n */\nclass ActionsHandler extends BaseHandler {\n  /**\n   * Handle a GET request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  get(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    res.json(thing.getActionDescriptions());\n  }\n\n  /**\n   * Handle a POST request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  post(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    const keys = Object.keys(req.body);\n    if (keys.length !== 1) {\n      res.status(400).end();\n      return;\n    }\n\n    const actionName = keys[0];\n    let input = null;\n    if (req.body[actionName].hasOwnProperty('input')) {\n      input = req.body[actionName].input;\n    }\n\n    const action = thing.performAction(actionName, input);\n    if (action) {\n      const response = action.asActionDescription();\n      action.start();\n\n      res.status(201);\n      res.json(response);\n    } else {\n      res.status(400).end();\n    }\n  }\n}\n\n/**\n * Handle a request to /actions/<action_name>.\n */\nclass ActionHandler extends BaseHandler {\n  /**\n   * Handle a GET request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  get(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    const actionName = req.params.actionName;\n\n    res.json(thing.getActionDescriptions(actionName));\n  }\n\n  /**\n   * Handle a POST request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  post(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    const actionName = req.params.actionName;\n\n    const keys = Object.keys(req.body);\n    if (keys.length !== 1) {\n      res.status(400).end();\n      return;\n    }\n\n    if (keys[0] !== actionName) {\n      res.status(400).end();\n      return;\n    }\n\n    let input = null;\n    if (req.body[actionName].hasOwnProperty('input')) {\n      input = req.body[actionName].input;\n    }\n\n    const action = thing.performAction(actionName, input);\n    if (action) {\n      const response = action.asActionDescription();\n      action.start();\n\n      res.status(201);\n      res.json(response);\n    } else {\n      res.status(400).end();\n    }\n  }\n}\n\n/**\n * Handle a request to /actions/<action_name>/<action_id>.\n */\nclass ActionIDHandler extends BaseHandler {\n  /**\n   * Handle a GET request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  get(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    const actionName = req.params.actionName;\n    const actionId = req.params.actionId;\n\n    const action = thing.getAction(actionName, actionId);\n    if (action === null) {\n      res.status(404).end();\n      return;\n    }\n\n    res.json(action.asActionDescription());\n  }\n\n  /**\n   * Handle a PUT request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  put(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    // TODO: this is not yet defined in the spec\n    res.status(200).end();\n  }\n\n  /**\n   * Handle a DELETE request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  delete(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    const actionName = req.params.actionName;\n    const actionId = req.params.actionId;\n\n    if (thing.removeAction(actionName, actionId)) {\n      res.status(204).end();\n    } else {\n      res.status(404).end();\n    }\n  }\n}\n\n/**\n * Handle a request to /events.\n */\nclass EventsHandler extends BaseHandler {\n  /**\n   * Handle a GET request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  get(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    res.json(thing.getEventDescriptions());\n  }\n}\n\n/**\n * Handle a request to /events/<event_name>.\n */\nclass EventHandler extends BaseHandler {\n  /**\n   * Handle a GET request.\n   *\n   * @param {Object} req The request object\n   * @param {Object} res The response object\n   */\n  get(req: express.Request, res: express.Response): void {\n    const thing = this.getThing(req);\n    if (thing === null) {\n      res.status(404).end();\n      return;\n    }\n\n    const eventName = req.params.eventName;\n\n    res.json(thing.getEventDescriptions(eventName));\n  }\n}\n\n/**\n * Server to represent a Web Thing over HTTP.\n */\nexport class WebThingServer {\n  things: SingleThing | MultipleThings;\n\n  name: string;\n\n  port: number;\n\n  hostname: string | null;\n\n  basePath: string;\n\n  disableHostValidation: boolean;\n\n  hosts: string[];\n\n  app: express.Express & { isTls?: boolean };\n\n  // HACK because the express types are weird\n  server: http.Server | https.Server;\n\n  router: expressWs.Router;\n\n  mdns!: dnssd.Advertisement;\n\n  /**\n   * Initialize the WebThingServer.\n   *\n   * For documentation on the additional route handlers, see:\n   * http://expressjs.com/en/4x/api.html#app.use\n   *\n   * @param {Object} things Things managed by this server -- should be of type\n   *                        SingleThing or MultipleThings\n   * @param {Number} port Port to listen on (defaults to 80)\n   * @param {String} hostname Optional host name, i.e. mything.com\n   * @param {Object} sslOptions SSL options to pass to the express server\n   * @param {Object[]} additionalRoutes List of additional routes to add to\n   *                                    server, i.e. [{path: '..', handler: ..}]\n   * @param {String} basePath Base URL path to use, rather than '/'\n   * @param {Boolean} disableHostValidation Whether or not to disable host\n   *                                        validation -- note that this can\n   *                                        lead to DNS rebinding attacks\n   */\n  constructor(\n    things: SingleThing | MultipleThings,\n    port: number | null = null,\n    hostname: string | null = null,\n    sslOptions: https.ServerOptions | null = null,\n    additionalRoutes: Record<string, express.RequestHandler>[] | null = null,\n    basePath = '/',\n    disableHostValidation = false\n  ) {\n    this.things = things;\n    this.name = things.getName();\n    this.port = Number(port) || (sslOptions ? 443 : 80);\n    this.hostname = hostname;\n    this.basePath = basePath.replace(/\\/$/, '');\n    this.disableHostValidation = !!disableHostValidation;\n\n    const systemHostname = os.hostname().toLowerCase();\n    this.hosts = [\n      'localhost',\n      `localhost:${port}`,\n      `${systemHostname}.local`,\n      `${systemHostname}.local:${port}`,\n    ];\n\n    utils.getAddresses().forEach((address) => {\n      this.hosts.push(address, `${address}:${port}`);\n    });\n\n    if (hostname) {\n      hostname = hostname.toLowerCase();\n      this.hosts.push(hostname, `${hostname}:${port}`);\n    }\n\n    if (things instanceof MultipleThings) {\n      const list = things.getThings();\n      for (let i = 0; i < list.length; i++) {\n        const thing = list[i];\n        thing.setHrefPrefix(`${this.basePath}/${i}`);\n      }\n    } else {\n      things.getThing().setHrefPrefix(this.basePath);\n    }\n\n    this.app = express();\n    this.app.use(bodyParser.json());\n\n    // Validate Host header\n    this.app.use((request, response, next: () => unknown) => {\n      const host = request.headers.host;\n      if (this.disableHostValidation || (host && this.hosts.includes(host.toLowerCase()))) {\n        next();\n      } else {\n        response.status(403).send('Forbidden');\n      }\n    });\n\n    // Set CORS headers\n    this.app.use((_request, response, next) => {\n      response.setHeader('Access-Control-Allow-Origin', '*');\n      response.setHeader(\n        'Access-Control-Allow-Headers',\n        'Origin, X-Requested-With, Content-Type, Accept'\n      );\n      response.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, PUT, POST, DELETE');\n      next();\n    });\n\n    if (sslOptions) {\n      this.server = https.createServer(sslOptions);\n      this.app.isTls = true;\n    } else {\n      this.server = http.createServer();\n      this.app.isTls = false;\n    }\n\n    expressWs(this.app, this.server);\n\n    const thingsHandler = new ThingsHandler(this.things);\n    const thingHandler = new ThingHandler(this.things);\n    const propertiesHandler = new PropertiesHandler(this.things);\n    const propertyHandler = new PropertyHandler(this.things);\n    const actionsHandler = new ActionsHandler(this.things);\n    const actionHandler = new ActionHandler(this.things);\n    const actionIdHandler = new ActionIDHandler(this.things);\n    const eventsHandler = new EventsHandler(this.things);\n    const eventHandler = new EventHandler(this.things);\n\n    this.router = express.Router();\n\n    if (Array.isArray(additionalRoutes)) {\n      for (const route of additionalRoutes) {\n        this.router.use(route.path, route.handler);\n      }\n    }\n    if (this.things instanceof MultipleThings) {\n      this.router.get('/', (req, res) => thingsHandler.get(req, res));\n      this.router.get('/:thingId', (req, res) => thingHandler.get(req, res));\n      this.router.ws('/:thingId', (ws, req) => thingHandler.ws(ws, req));\n      this.router.get('/:thingId/properties', (req, res) => propertiesHandler.get(req, res));\n      this.router.get('/:thingId/properties/:propertyName', (req, res) =>\n        propertyHandler.get(req, res)\n      );\n      this.router.put('/:thingId/properties/:propertyName', (req, res) =>\n        propertyHandler.put(req, res)\n      );\n      this.router.get('/:thingId/actions', (req, res) => actionsHandler.get(req, res));\n      this.router.post('/:thingId/actions', (req, res) => actionsHandler.post(req, res));\n      this.router.get('/:thingId/actions/:actionName', (req, res) => actionHandler.get(req, res));\n      this.router.post('/:thingId/actions/:actionName', (req, res) => actionHandler.post(req, res));\n      this.router.get('/:thingId/actions/:actionName/:actionId', (req, res) =>\n        actionIdHandler.get(req, res)\n      );\n      this.router.put('/:thingId/actions/:actionName/:actionId', (req, res) =>\n        actionIdHandler.put(req, res)\n      );\n      this.router.delete('/:thingId/actions/:actionName/:actionId', (req, res) =>\n        actionIdHandler.delete(req, res)\n      );\n      this.router.get('/:thingId/events', (req, res) => eventsHandler.get(req, res));\n      this.router.get('/:thingId/events/:eventName', (req, res) => eventHandler.get(req, res));\n    } else {\n      this.router.get('/', (req, res) => thingHandler.get(req, res));\n      this.router.ws('/', (ws, req) => thingHandler.ws(ws, req));\n      this.router.get('/properties', (req, res) => propertiesHandler.get(req, res));\n      this.router.get('/properties/:propertyName', (req, res) => propertyHandler.get(req, res));\n      this.router.put('/properties/:propertyName', (req, res) => propertyHandler.put(req, res));\n      this.router.get('/actions', (req, res) => actionsHandler.get(req, res));\n      this.router.post('/actions', (req, res) => actionsHandler.post(req, res));\n      this.router.get('/actions/:actionName', (req, res) => actionHandler.get(req, res));\n      this.router.post('/actions/:actionName', (req, res) => actionHandler.post(req, res));\n      this.router.get('/actions/:actionName/:actionId', (req, res) =>\n        actionIdHandler.get(req, res)\n      );\n      this.router.put('/actions/:actionName/:actionId', (req, res) =>\n        actionIdHandler.put(req, res)\n      );\n      this.router.delete('/actions/:actionName/:actionId', (req, res) =>\n        actionIdHandler.delete(req, res)\n      );\n      this.router.get('/events', (req, res) => eventsHandler.get(req, res));\n      this.router.get('/events/:eventName', (req, res) => eventHandler.get(req, res));\n    }\n\n    this.app.use(this.basePath || '/', this.router);\n    this.server.on('request', this.app);\n  }\n\n  /**\n   * Start listening for incoming connections.\n   *\n   * @returns {Promise} Promise which resolves once the server is started.\n   */\n  start(): Promise<void> {\n    const opts: dnssd.Options = {\n      name: this.name,\n      txt: {\n        path: '/',\n      },\n    };\n\n    if (this.app.isTls) {\n      opts.txt.tls = '1';\n    }\n\n    this.mdns = new dnssd.Advertisement(new dnssd.ServiceType('_webthing._tcp'), this.port!, opts);\n    this.mdns.on('error', (e) => {\n      console.debug(`mDNS error: ${e}`);\n      setTimeout(() => {\n        this.mdns.start();\n      }, 10000);\n    });\n    this.mdns.start();\n\n    return new Promise((resolve) => {\n      this.server.listen({ port: this.port }, resolve);\n    });\n  }\n\n  /**\n   * Stop listening.\n   *\n   * @param {boolean?} force - Whether or not to force shutdown immediately.\n   * @returns {Promise} Promise which resolves once the server is stopped.\n   */\n  stop(force = false): Promise<unknown> {\n    const promises: Promise<void>[] = [];\n\n    if (this.mdns) {\n      promises.push(\n        new Promise((resolve, reject) => {\n          this.mdns.stop(force, (error?: unknown) => {\n            if (error) {\n              reject(error);\n            } else {\n              resolve();\n            }\n          });\n        })\n      );\n    }\n\n    promises.push(\n      new Promise((resolve, reject) => {\n        this.server.close((error) => {\n          if (error) {\n            reject(error);\n          } else {\n            resolve();\n          }\n        });\n      })\n    );\n\n    return Promise.all(promises);\n  }\n}\n"
  },
  {
    "path": "src/thing.ts",
    "content": "/**\n * High-level Thing base class implementation.\n */\n\nimport Ajv from 'ajv';\nimport Property from './property';\nimport Event from './event';\nimport Action from './action';\nimport { AnyType, Link, Subscriber } from './types';\n\nconst ajv = new Ajv();\n\n/**\n * A Web Thing.\n */\nclass Thing {\n  private id: string;\n\n  private title: string;\n\n  private type: string[];\n\n  private context: string;\n\n  private description: string;\n\n  private properties: { [name: string]: Property };\n\n  private availableActions: {\n    [actionName: string]: {\n      metadata: Action.ActionMetadata;\n      class: Action.ActionTypeClass;\n    };\n  };\n\n  private availableEvents: {\n    [name: string]: {\n      metadata: Event.EventMetadata;\n      subscribers: Set<Subscriber>;\n    };\n  };\n\n  private actions: { [name: string]: Action[] };\n\n  private events: Event[];\n\n  private subscribers = new Set<Subscriber>();\n\n  private hrefPrefix: string;\n\n  private uiHref: string | null;\n\n  /**\n   * Initialize the object.\n   *\n   * @param {String} id The thing's unique ID - must be a URI\n   * @param {String} title The thing's title\n   * @param {String} type (Optional) The thing's type(s)\n   * @param {String} description (Optional) Description of the thing\n   */\n  constructor(id: string, title: string, type: string | string[], description: string) {\n    if (!Array.isArray(type)) {\n      type = [type];\n    }\n\n    this.id = id;\n    this.title = title;\n    this.context = 'https://webthings.io/schemas';\n    this.type = type || [];\n    this.description = description || '';\n    this.properties = {};\n    this.availableActions = {};\n    this.availableEvents = {};\n    this.actions = {};\n    this.events = [];\n    this.subscribers = new Set();\n    this.hrefPrefix = '';\n    this.uiHref = null;\n  }\n\n  /**\n   * Return the thing state as a Thing Description.\n   *\n   * @returns {Object} Current thing state\n   */\n  asThingDescription(): Thing.ThingDescription {\n    const thing: Omit<Thing.ThingDescription, 'name' | 'href'> = {\n      id: this.id,\n      title: this.title,\n      '@context': this.context,\n      '@type': this.type,\n      properties: this.getPropertyDescriptions(),\n      actions: {},\n      events: {},\n      links: [\n        {\n          rel: 'properties',\n          href: `${this.hrefPrefix}/properties`,\n        },\n        {\n          rel: 'actions',\n          href: `${this.hrefPrefix}/actions`,\n        },\n        {\n          rel: 'events',\n          href: `${this.hrefPrefix}/events`,\n        },\n      ],\n    };\n\n    for (const name in this.availableActions) {\n      thing.actions[name] = this.availableActions[name].metadata;\n      thing.actions[name].links = [\n        {\n          rel: 'action',\n          href: `${this.hrefPrefix}/actions/${name}`,\n        },\n      ];\n    }\n\n    for (const name in this.availableEvents) {\n      thing.events[name] = this.availableEvents[name].metadata;\n      thing.events[name].links = [\n        {\n          rel: 'event',\n          href: `${this.hrefPrefix}/events/${name}`,\n        },\n      ];\n    }\n\n    if (this.uiHref) {\n      thing.links.push({\n        rel: 'alternate',\n        mediaType: 'text/html',\n        href: this.uiHref,\n      });\n    }\n\n    if (this.description) {\n      thing.description = this.description;\n    }\n\n    return thing as Thing.ThingDescription;\n  }\n\n  /**\n   * Get this thing's href.\n   *\n   * @returns {String} The href.\n   */\n  getHref(): string {\n    if (this.hrefPrefix) {\n      return this.hrefPrefix;\n    }\n\n    return '/';\n  }\n\n  /**\n   * Get this thing's UI href.\n   *\n   * @returns {String|null} The href.\n   */\n  getUiHref(): string | null {\n    return this.uiHref;\n  }\n\n  /**\n   * Set the prefix of any hrefs associated with this thing.\n   *\n   * @param {String} prefix The prefix\n   */\n  setHrefPrefix(prefix: string): void {\n    this.hrefPrefix = prefix;\n\n    for (const property of Object.values(this.properties)) {\n      property.setHrefPrefix(prefix);\n    }\n\n    for (const actionName in this.actions) {\n      for (const action of this.actions[actionName]) {\n        action.setHrefPrefix(prefix);\n      }\n    }\n  }\n\n  /**\n   * Set the href of this thing's custom UI.\n   *\n   * @param {String} href The href\n   */\n  setUiHref(href: string): void {\n    this.uiHref = href;\n  }\n\n  /**\n   * Get the ID of the thing.\n   *\n   * @returns {String} The ID.\n   */\n  getId(): string {\n    return this.id;\n  }\n\n  /**\n   * Get the title of the thing.\n   *\n   * @returns {String} The title.\n   */\n  getTitle(): string {\n    return this.title;\n  }\n\n  /**\n   * Get the type context of the thing.\n   *\n   * @returns {String} The context.\n   */\n  getContext(): string {\n    return this.context;\n  }\n\n  /**\n   * Get the type(s) of the thing.\n   *\n   * @returns {String[]} The type(s).\n   */\n  getType(): string[] {\n    return this.type;\n  }\n\n  /**\n   * Get the description of the thing.\n   *\n   * @returns {String} The description.\n   */\n  getDescription(): string {\n    return this.description;\n  }\n\n  /**\n   * Get the thing's properties as an object.\n   *\n   * @returns {Object} Properties, i.e. name -> description\n   */\n  getPropertyDescriptions(): { [name: string]: Property.PropertyDescription } {\n    const descriptions: { [name: string]: Property.PropertyDescription } = {};\n    for (const name in this.properties) {\n      descriptions[name] = this.properties[name].asPropertyDescription();\n    }\n\n    return descriptions;\n  }\n\n  /**\n   * Get the thing's actions as an array.\n   *\n   * @param {String?} actionName Optional action name to get descriptions for\n   *\n   * @returns {Object} Action descriptions.\n   */\n  getActionDescriptions(actionName?: string | null): Action.ActionDescription[] {\n    const descriptions: Action.ActionDescription[] = [];\n\n    if (!actionName) {\n      for (const name in this.actions) {\n        for (const action of this.actions[name]) {\n          descriptions.push(action.asActionDescription());\n        }\n      }\n    } else if (this.actions.hasOwnProperty(actionName)) {\n      for (const action of this.actions[actionName]) {\n        descriptions.push(action.asActionDescription());\n      }\n    }\n\n    return descriptions;\n  }\n\n  /**\n   * Get the thing's events as an array.\n   *\n   * @param {String?} eventName Optional event name to get descriptions for\n   *\n   * @returns {Object} Event descriptions.\n   */\n  getEventDescriptions(eventName?: string | null): Event.EventDescription[] {\n    if (!eventName) {\n      return this.events.map((e) => e.asEventDescription());\n    } else {\n      return this.events\n        .filter((e) => e.getName() === eventName)\n        .map((e) => e.asEventDescription());\n    }\n  }\n\n  /**\n   * Add a property to this thing.\n   *\n   * @param {Object} property Property to add\n   */\n  addProperty(property: Property): void {\n    property.setHrefPrefix(this.hrefPrefix);\n    this.properties[property.getName()] = property;\n  }\n\n  /**\n   * Remove a property from this thing.\n   *\n   * @param {Object} property Property to remove\n   */\n  removeProperty(property: Property): void {\n    if (this.properties.hasOwnProperty(property.getName())) {\n      delete this.properties[property.getName()];\n    }\n  }\n\n  /**\n   * Find a property by name.\n   *\n   * @param {String} propertyName Name of the property to find\n   *\n   * @returns {(Object|null)} Property if found, else null\n   */\n  findProperty(propertyName: string): Property | null {\n    if (this.properties.hasOwnProperty(propertyName)) {\n      return this.properties[propertyName];\n    }\n\n    return null;\n  }\n\n  /**\n   * Get a property's value.\n   *\n   * @param {String} propertyName Name of the property to get the value of\n   *\n   * @returns {*} Current property value if found, else null\n   */\n  getProperty(propertyName: string): unknown | null {\n    const prop = this.findProperty(propertyName);\n    if (prop) {\n      return prop.getValue();\n    }\n\n    return null;\n  }\n\n  /**\n   * Get a mapping of all properties and their values.\n   *\n   * Returns an object of propertyName -> value.\n   */\n  getProperties(): Record<string, unknown> {\n    const props: Record<string, unknown> = {};\n    for (const name in this.properties) {\n      props[name] = this.properties[name].getValue();\n    }\n\n    return props;\n  }\n\n  /**\n   * Determine whether or not this thing has a given property.\n   *\n   * @param {String} propertyName The property to look for\n   *\n   * @returns {Boolean} Indication of property presence\n   */\n  hasProperty(propertyName: string): boolean {\n    return this.properties.hasOwnProperty(propertyName);\n  }\n\n  /**\n   * Set a property value.\n   *\n   * @param {String} propertyName Name of the property to set\n   * @param {*} value Value to set\n   */\n  setProperty(propertyName: string, value: AnyType): void {\n    const prop = this.findProperty(propertyName);\n    if (!prop) {\n      return;\n    }\n\n    prop.setValue(value);\n  }\n\n  /**\n   * Get an action.\n   *\n   * @param {String} actionName Name of the action\n   * @param {String} actionId ID of the action\n   * @returns {Object} The requested action if found, else null\n   */\n  getAction(actionName: string, actionId: string): Action | null {\n    if (!this.actions.hasOwnProperty(actionName)) {\n      return null;\n    }\n\n    for (const action of this.actions[actionName]) {\n      if (action.getId() === actionId) {\n        return action;\n      }\n    }\n\n    return null;\n  }\n\n  /**\n   * Add a new event and notify subscribers.\n   *\n   * @param {Object} event The event that occurred\n   */\n  addEvent(event: Event): void {\n    this.events.push(event);\n    this.eventNotify(event);\n  }\n\n  /**\n   * Add an available event.\n   *\n   * @param {String} name Name of the event\n   * @param {Object} metadata Event metadata, i.e. type, description, etc., as\n   *                          an object.\n   */\n  addAvailableEvent(name: string, metadata: Event.EventMetadata): void {\n    if (!metadata) {\n      metadata = {};\n    }\n\n    this.availableEvents[name] = {\n      metadata: metadata,\n      subscribers: new Set(),\n    };\n  }\n\n  /**\n   * Perform an action on the thing.\n   *\n   * @param {String} actionName Name of the action\n   * @param {Object} input Any action inputs\n   * @returns {Object} The action that was created.\n   */\n  performAction<InputType = AnyType>(\n    actionName: string,\n    input: InputType | null\n  ): Action<InputType> | undefined {\n    input = input || null;\n\n    if (!this.availableActions.hasOwnProperty(actionName)) {\n      return;\n    }\n\n    const actionType = this.availableActions[actionName];\n\n    if (actionType.metadata.hasOwnProperty('input')) {\n      const schema = JSON.parse(JSON.stringify(actionType.metadata.input));\n\n      if (schema.hasOwnProperty('properties')) {\n        const props: Record<string, unknown>[] = Object.values(schema.properties);\n\n        for (const prop of props) {\n          delete prop.title;\n          delete prop.unit;\n          delete prop['@type'];\n        }\n      }\n\n      const valid = ajv.validate(schema, input);\n      if (!valid) {\n        return;\n      }\n    }\n\n    const action: Action<InputType> = <Action<InputType>>(\n      new actionType.class(this, <AnyType>(<unknown>input))\n    );\n    action.setHrefPrefix(this.hrefPrefix);\n    this.actionNotify(<Action<AnyType>>(<unknown>action));\n    this.actions[actionName].push(<Action<AnyType>>(<unknown>action));\n    return action;\n  }\n\n  /**\n   * Remove an existing action.\n   *\n   * @param {String} actionName Name of the action\n   * @param {String} actionId ID of the action\n   * @returns boolean indicating the presence of the action.\n   */\n  removeAction(actionName: string, actionId: string): boolean {\n    const action = this.getAction(actionName, actionId);\n    if (action === null) {\n      return false;\n    }\n\n    action.cancel();\n    for (let i = 0; i < this.actions[actionName].length; ++i) {\n      if (this.actions[actionName][i].getId() === actionId) {\n        this.actions[actionName].splice(i, 1);\n        break;\n      }\n    }\n\n    return true;\n  }\n\n  /**\n   * Add an available action.\n   *\n   * @param {String} name Name of the action\n   * @param {Object} metadata Action metadata, i.e. type, description, etc., as\n   *                          an object.\n   * @param {Object} cls Class to instantiate for this action\n   */\n  addAvailableAction(\n    name: string,\n    metadata: Action.ActionMetadata | null,\n    cls: Action.ActionTypeClass\n  ): void {\n    if (!metadata) {\n      metadata = {};\n    }\n\n    this.availableActions[name] = {\n      metadata: metadata,\n      class: cls,\n    };\n    this.actions[name] = [];\n  }\n\n  /**\n   * Add a new websocket subscriber.\n   *\n   * @param {Object} ws The websocket\n   */\n  addSubscriber(ws: Subscriber): void {\n    this.subscribers.add(ws);\n  }\n\n  /**\n   * Remove a websocket subscriber.\n   *\n   */\n  removeSubscriber(ws: Subscriber): void {\n    if (this.subscribers.has(ws)) {\n      this.subscribers.delete(ws);\n    }\n\n    for (const name in this.availableEvents) {\n      this.removeEventSubscriber(name, ws);\n    }\n  }\n\n  /**\n   * Add a new websocket subscriber to an event.\n   *\n   * @param {String} name Name of the event\n   * @param {Subscriber} ws The websocket\n   */\n  addEventSubscriber(name: string, ws: Subscriber): void {\n    if (this.availableEvents.hasOwnProperty(name)) {\n      this.availableEvents[name].subscribers.add(ws);\n    }\n  }\n\n  /**\n   * Remove a websocket subscriber from an event.\n   *\n   * @param {String} name Name of the event\n   * @param {Object} ws The websocket\n   */\n  removeEventSubscriber(name: string, ws: Subscriber): void {\n    if (\n      this.availableEvents.hasOwnProperty(name) &&\n      this.availableEvents[name].subscribers.has(ws)\n    ) {\n      this.availableEvents[name].subscribers.delete(ws);\n    }\n  }\n\n  /**\n   * Notify all subscribers of a property change.\n   *\n   * @param {Object} property The property that changed\n   */\n  propertyNotify(property: Property<AnyType>): void {\n    const message = JSON.stringify({\n      messageType: 'propertyStatus',\n      data: {\n        [property.getName()]: property.getValue(),\n      },\n    });\n\n    for (const subscriber of this.subscribers) {\n      try {\n        subscriber.send(message);\n      } catch (e) {\n        // do nothing\n      }\n    }\n  }\n\n  /**\n   * Notify all subscribers of an action status change.\n   *\n   * @param {Object} action The action whose status changed\n   */\n  actionNotify(action: Action): void {\n    const message = JSON.stringify({\n      messageType: 'actionStatus',\n      data: action.asActionDescription(),\n    });\n\n    for (const subscriber of this.subscribers) {\n      try {\n        subscriber.send(message);\n      } catch (e) {\n        // do nothing\n      }\n    }\n  }\n\n  /**\n   * Notify all subscribers of an event.\n   *\n   * @param {Object} event The event that occurred\n   */\n  eventNotify(event: Event): void {\n    if (!this.availableEvents.hasOwnProperty(event.getName())) {\n      return;\n    }\n\n    const message = JSON.stringify({\n      messageType: 'event',\n      data: event.asEventDescription(),\n    });\n\n    for (const subscriber of this.availableEvents[event.getName()].subscribers) {\n      try {\n        subscriber.send(message);\n      } catch (e) {\n        // do nothing\n      }\n    }\n  }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\ndeclare namespace Thing {\n  export interface SecurityScheme {\n    '@type'?: string | string[];\n    scheme: string;\n    description?: string;\n    descriptions?: { [lang: string]: string };\n    proxy?: string;\n  }\n\n  export interface ThingDescription {\n    id: string;\n    title: string;\n    name: string;\n    href: string;\n    '@context': string;\n    '@type': string[];\n    properties: { [name: string]: Property.PropertyDescription };\n    links: Link[];\n    actions: { [name: string]: Action.ActionMetadata };\n    events: { [name: string]: Event.EventMetadata };\n    description?: string;\n    base?: string;\n    securityDefinitions?: { [security: string]: SecurityScheme };\n    security?: string;\n  }\n}\n\nexport = Thing;\n"
  },
  {
    "path": "src/types.ts",
    "content": "export type PrimitiveJsonType =\n  | 'null'\n  | 'boolean'\n  | 'object'\n  | 'array'\n  | 'number'\n  | 'integer'\n  | 'string';\n\nexport type AnyType = null | boolean | number | string | Record<string, unknown> | unknown[];\n\nexport interface Link {\n  rel: string;\n  href: string;\n  mediaType?: string;\n}\n\nexport interface Subscriber {\n  send(message: string): void;\n}\n"
  },
  {
    "path": "src/utils.ts",
    "content": "/**\n * Utility functions.\n */\n\nimport * as os from 'os';\n\n/**\n * Get the current time.\n *\n * @returns {String} The current time in the form YYYY-mm-ddTHH:MM:SS+00:00\n */\nexport function timestamp(): string {\n  const date = new Date().toISOString();\n  return date.replace(/\\.\\d{3}Z/, '+00:00');\n}\n\n/**\n * Get all IP addresses.\n *\n * @returns {string[]} Array of addresses.\n */\nexport function getAddresses(): string[] {\n  const addresses = new Set<string>();\n\n  const ifaces = os.networkInterfaces();\n  Object.keys(ifaces).forEach((iface) => {\n    ifaces[iface]!.forEach((addr) => {\n      const address = addr.address.toLowerCase();\n\n      // Filter out link-local addresses.\n      if (addr.family === 'IPv6' && !address.startsWith('fe80:')) {\n        addresses.add(`[${address}]`);\n      } else if (addr.family === 'IPv4' && !address.startsWith('169.254.')) {\n        addresses.add(address);\n      }\n    });\n  });\n\n  return Array.from(addresses).sort();\n}\n"
  },
  {
    "path": "src/value.ts",
    "content": "/**\n * An observable, settable value interface.\n */\n\nimport { EventEmitter } from 'events';\nimport { AnyType } from './types';\n\n/**\n * A property value.\n *\n * This is used for communicating between the Thing representation and the\n * actual physical thing implementation.\n *\n * Notifies all observers when the underlying value changes through an external\n * update (command to turn the light off) or if the underlying sensor reports a\n * new value.\n */\nclass Value<ValueType = AnyType> extends EventEmitter {\n  private lastValue: ValueType;\n\n  private valueForwarder: Value.Forwarder<ValueType> | null;\n\n  /**\n   * Initialize the object.\n   *\n   * @param {*} initialValue The initial value\n   * @param {function?} valueForwarder The method that updates the actual value\n   *                                   on the thing\n   */\n  constructor(initialValue: ValueType, valueForwarder: Value.Forwarder<ValueType> | null = null) {\n    super();\n    this.lastValue = initialValue;\n    this.valueForwarder = valueForwarder;\n  }\n\n  /**\n   * Set a new value for this thing.\n   *\n   * @param {*} value Value to set\n   */\n  set(value: ValueType): void {\n    if (this.valueForwarder) {\n      this.valueForwarder(value);\n    }\n\n    this.notifyOfExternalUpdate(value);\n  }\n\n  /**\n   * Return the last known value from the underlying thing.\n   *\n   * @returns the value.\n   */\n  get(): ValueType {\n    return this.lastValue;\n  }\n\n  /**\n   * Notify observers of a new value.\n   *\n   * @param {*} value New value\n   */\n  notifyOfExternalUpdate(value: ValueType): void {\n    if (typeof value !== 'undefined' && value !== null && value !== this.lastValue) {\n      this.lastValue = value;\n      this.emit('update', value);\n    }\n  }\n}\n\ndeclare namespace Value {\n  export type Forwarder<T> = (value: T) => void;\n}\n\nexport = Value;\n"
  },
  {
    "path": "src/webthing.ts",
    "content": "import Action from './action';\nimport Event from './event';\nimport Property from './property';\nimport Thing from './thing';\nimport Value from './value';\n\nexport { Action, Event, Property, Thing, Value };\n\nexport * from './server';\n"
  },
  {
    "path": "test.sh",
    "content": "#!/bin/bash -e\n\npushd example\nnpm install\npopd\n\n# clone the webthing-tester\nif [ ! -d webthing-tester ]; then\n    git clone https://github.com/WebThingsIO/webthing-tester\nfi\npip3 install --user -r webthing-tester/requirements.txt\n\nexport NODE_PATH=.\n# build and test the single-thing example\nnode example/single-thing.js &\nEXAMPLE_PID=$!\nsleep 5\n./webthing-tester/test-client.py\nkill -15 $EXAMPLE_PID\n\n# build and test the multiple-things example\nnode example/multiple-things.js &\nEXAMPLE_PID=$!\nsleep 5\n./webthing-tester/test-client.py --path-prefix \"/0\"\nkill -15 $EXAMPLE_PID\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2018\",\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"node\",\n    \"lib\": [\n      \"es2018\",\n      \"dom\"\n    ],\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"sourceMap\": true,\n    \"rootDir\": \"src\",\n    \"outDir\": \"lib\",\n    \"strict\": true,\n    \"noImplicitAny\": true,\n    \"strictNullChecks\": true,\n    \"strictFunctionTypes\": true,\n    \"strictBindCallApply\": true,\n    \"strictPropertyInitialization\": true,\n    \"noImplicitThis\": true,\n    \"alwaysStrict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"esModuleInterop\": true\n  }\n}\n"
  }
]