From 4d6d4de04eab71381fd12b4cab0884700ca8e07f Mon Sep 17 00:00:00 2001 From: Heiko Rothe Date: Sun, 31 May 2020 11:15:25 +0200 Subject: [PATCH] feat: document API with OpenAPI Each instance now hosts a SwaggerUI under /api that you can use for interacting with the API. --- docs/.vuepress/config.js | 3 +- docs/guide/api.md | 12 ++++ nest-cli.json | 5 +- package-lock.json | 59 +++++++++++++++++++ package.json | 4 ++ src/entities/binary-sensor.ts | 2 +- src/entities/camera.ts | 2 +- src/entities/device-tracker.ts | 2 +- src/entities/entities.controller.spec.ts | 2 +- src/entities/entities.controller.ts | 42 ++++++++++++- src/entities/entities.events.ts | 2 +- src/entities/entities.service.ts | 2 +- src/entities/{entity.ts => entity.dto.ts} | 9 +++ src/entities/entity.proxy.ts | 2 +- src/entities/sensor.ts | 2 +- src/entities/switch.ts | 2 +- src/integrations/grid-eye/grid-eye.service.ts | 2 +- .../home-assistant.service.spec.ts | 2 +- .../home-assistant/home-assistant.service.ts | 2 +- .../omron-d6t/omron-d6t.service.ts | 2 +- src/main.ts | 25 ++++++++ src/status/status.controller.ts | 5 ++ 22 files changed, 174 insertions(+), 16 deletions(-) create mode 100644 docs/guide/api.md rename src/entities/{entity.ts => entity.dto.ts} (64%) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 97eea22..c30b301 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -57,7 +57,8 @@ module.exports = { '/guide/configuration', '/guide/cluster', '/guide/entities', - '/guide/cli' + '/guide/cli', + '/guide/api' ] }, { diff --git a/docs/guide/api.md b/docs/guide/api.md new file mode 100644 index 0000000..9ddce58 --- /dev/null +++ b/docs/guide/api.md @@ -0,0 +1,12 @@ +# API + +::: warning + +The API is still a work in progress and some features are missing. The existing endpoints will stay compatible within the same major version though. + +::: + +Each instance of room-assistant exposes an HTTP API that you can use for debugging or connecting it to different services. The API is accessible under port `6415` (cannot be changed currently). + +The API is documented with an OpenAPI schema that you can retrieve under `/api-json`. For a visual representation navigate to `/api`. You can make all available API calls directly in your browser from this page. + diff --git a/nest-cli.json b/nest-cli.json index 56167b3..2e1000f 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,4 +1,7 @@ { "collection": "@nestjs/schematics", - "sourceRoot": "src" + "sourceRoot": "src", + "compilerOptions": { + "plugins": ["@nestjs/swagger/plugin"] + } } diff --git a/package-lock.json b/package-lock.json index d2255bd..0c5b0bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2479,6 +2479,11 @@ } } }, + "@nestjs/mapped-types": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-0.0.5.tgz", + "integrity": "sha512-QjZCSMHHy8IW4UUTS49QJQ0NrA8MHv6XevNrPLJwh4n3lN7wY9aSRwd1+cBIUDBXEHRKngcYdPtC4oG0fimw+A==" + }, "@nestjs/platform-express": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-7.1.0.tgz", @@ -2621,6 +2626,16 @@ } } }, + "@nestjs/swagger": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-4.5.9.tgz", + "integrity": "sha512-pozVi8ClZdjbxAISlvSq3X2KPTawsgxd46XN2GrtEe1PIImRs0WBYEHxL9IBo+7KjipazJjFI4mt/xwVj/895A==", + "requires": { + "@nestjs/mapped-types": "0.0.5", + "lodash": "4.17.15", + "path-to-regexp": "3.2.0" + } + }, "@nestjs/terminus": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@nestjs/terminus/-/terminus-7.0.1.tgz", @@ -3075,6 +3090,11 @@ } } }, + "@types/validator": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.0.0.tgz", + "integrity": "sha512-WAy5txG7aFX8Vw3sloEKp5p/t/Xt8jD3GRD9DacnFv6Vo8ubudAsRTXgxpQwU0mpzY/H8U4db3roDuCMjShBmw==" + }, "@types/webpack": { "version": "4.41.13", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.13.tgz", @@ -5571,6 +5591,11 @@ "safe-buffer": "^5.0.1" } }, + "class-transformer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.2.3.tgz", + "integrity": "sha512-qsP+0xoavpOlJHuYsQJsN58HXSl8Jvveo+T37rEvCEeRfMWoytAyR0Ua/YsFgpM6AZYZ/og2PJwArwzJl1aXtQ==" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -5594,6 +5619,17 @@ } } }, + "class-validator": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.12.2.tgz", + "integrity": "sha512-TDzPzp8BmpsbPhQpccB3jMUE/3pK0TyqamrK0kcx+ZeFytMA+O6q87JZZGObHHnoo9GM8vl/JppIyKWeEA/EVw==", + "requires": { + "@types/validator": "13.0.0", + "google-libphonenumber": "^3.2.8", + "tslib": ">=1.9.0", + "validator": "13.0.0" + } + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -9450,6 +9486,11 @@ "delegate": "^3.1.2" } }, + "google-libphonenumber": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.10.tgz", + "integrity": "sha512-TsckE9O8QgqaIeaOXPjcJa4/kX3BzFdO1oCbMfmUpRZckml4xJhjJVxaT9Mdt/VrZZkT9lX44eHAEWfJK1tHtw==" + }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -17898,6 +17939,19 @@ } } }, + "swagger-ui-dist": { + "version": "3.25.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.25.5.tgz", + "integrity": "sha512-JZ6dVQS2nPgVYW0+JIIxm84vvFbR/ole6xYJG2DcSdejDLt8ARqIhbZ4InL7RVsLdXpPirUMb7hf2z4Fzqesyw==" + }, + "swagger-ui-express": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.4.tgz", + "integrity": "sha512-Ea96ecpC+Iq9GUqkeD/LFR32xSs8gYqmTW1gXCuKg81c26WV6ZC2FsBSPVExQP6WkyUuz5HEiR0sEv/HCC343g==", + "requires": { + "swagger-ui-dist": "^3.18.1" + } + }, "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", @@ -19097,6 +19151,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.0.0.tgz", + "integrity": "sha512-anYx5fURbgF04lQV18nEQWZ/3wHGnxiKdG4aL8J+jEDsm98n/sU/bey+tYk6tnGJzm7ioh5FoqrAiQ6m03IgaA==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index ba72cca..17bc9d6 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,12 @@ "@nestjs/core": "^7.1.0", "@nestjs/platform-express": "^7.1.0", "@nestjs/schedule": "^0.4.0", + "@nestjs/swagger": "^4.5.9", "@nestjs/terminus": "^7.0.1", "async-mqtt": "^2.6.0", "chalk": "^4.0.0", + "class-transformer": "^0.2.3", + "class-validator": "^0.12.2", "command-line-args": "^5.1.1", "command-line-usage": "^6.1.0", "config": "^3.3.1", @@ -64,6 +67,7 @@ "reflect-metadata": "^0.1.13", "rxjs": "^6.5.5", "slugify": "^1.4.0", + "swagger-ui-express": "^4.1.4", "systeminformation": "^4.26.4", "update-notifier": "^4.1.0", "winston": "^3.2.1" diff --git a/src/entities/binary-sensor.ts b/src/entities/binary-sensor.ts index 91a54ed..0dfe4e9 100644 --- a/src/entities/binary-sensor.ts +++ b/src/entities/binary-sensor.ts @@ -1,4 +1,4 @@ -import { Entity } from './entity'; +import { Entity } from './entity.dto'; export class BinarySensor extends Entity { state: boolean; diff --git a/src/entities/camera.ts b/src/entities/camera.ts index 3f31261..36ef882 100644 --- a/src/entities/camera.ts +++ b/src/entities/camera.ts @@ -1,4 +1,4 @@ -import { Entity } from './entity'; +import { Entity } from './entity.dto'; export class Camera extends Entity { state: Buffer; diff --git a/src/entities/device-tracker.ts b/src/entities/device-tracker.ts index a5b011f..861992e 100644 --- a/src/entities/device-tracker.ts +++ b/src/entities/device-tracker.ts @@ -1,4 +1,4 @@ -import { Entity } from './entity'; +import { Entity } from './entity.dto'; export class DeviceTracker extends Entity { state: boolean; diff --git a/src/entities/entities.controller.spec.ts b/src/entities/entities.controller.spec.ts index 5f8b8e0..8861e97 100644 --- a/src/entities/entities.controller.spec.ts +++ b/src/entities/entities.controller.spec.ts @@ -3,7 +3,7 @@ import { EntitiesController } from './entities.controller'; import { EntitiesService } from './entities.service'; import { Sensor } from './sensor'; import { Switch } from './switch'; -import { Entity } from './entity'; +import { Entity } from './entity.dto'; import { NestEmitterModule } from 'nest-emitter'; import { ClusterModule } from '../cluster/cluster.module'; import { EventEmitter } from 'events'; diff --git a/src/entities/entities.controller.ts b/src/entities/entities.controller.ts index d823a4c..0a3baf7 100644 --- a/src/entities/entities.controller.ts +++ b/src/entities/entities.controller.ts @@ -1,13 +1,53 @@ import { Controller, Get } from '@nestjs/common'; -import { Entity } from './entity'; +import { Entity } from './entity.dto'; import { EntitiesService } from './entities.service'; import { Camera } from './camera'; +import { + ApiExtraModels, + ApiOkResponse, + ApiOperation, + ApiTags, + getSchemaPath, +} from '@nestjs/swagger'; +@ApiTags('entities') +@ApiExtraModels(Entity) @Controller('entities') export class EntitiesController { constructor(private readonly entitiesService: EntitiesService) {} @Get() + @ApiOperation({ + summary: 'Retrieve all entities and their states', + description: + 'The entities will always match the schema, but may also provide some additional information depending on the integration they are from. Note that camera entities are filtered from this list.', + }) + @ApiOkResponse({ + description: 'All registered entities are listed.', + content: { + 'application/json': { + schema: { type: 'array', items: { $ref: getSchemaPath(Entity) } }, + example: [ + { + id: 'example-sensor', + name: 'Example Sensor', + distributed: true, + state: 'instance-name', + attributes: { + distance: 4.2, + }, + }, + { + id: 'example-switch', + name: 'Example Switch', + distributed: false, + state: true, + attributes: {}, + }, + ], + }, + }, + }) getAll(): Entity[] { return this.entitiesService .getAll() diff --git a/src/entities/entities.events.ts b/src/entities/entities.events.ts index ec5cd39..c2fa150 100644 --- a/src/entities/entities.events.ts +++ b/src/entities/entities.events.ts @@ -1,4 +1,4 @@ -import { Entity } from './entity'; +import { Entity } from './entity.dto'; import { StrictEventEmitter } from 'nest-emitter'; import EventEmitter = NodeJS.EventEmitter; import { EntityCustomization } from './entity-customization.interface'; diff --git a/src/entities/entities.service.ts b/src/entities/entities.service.ts index 96b46f4..d3ea12c 100644 --- a/src/entities/entities.service.ts +++ b/src/entities/entities.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; -import { Entity } from './entity'; +import { Entity } from './entity.dto'; import { EntityProxyHandler } from './entity.proxy'; import { InjectEventEmitter } from 'nest-emitter'; import { EntitiesEventEmitter } from './entities.events'; diff --git a/src/entities/entity.ts b/src/entities/entity.dto.ts similarity index 64% rename from src/entities/entity.ts rename to src/entities/entity.dto.ts index a35901f..78501f3 100644 --- a/src/entities/entity.ts +++ b/src/entities/entity.dto.ts @@ -1,3 +1,5 @@ +import { ApiProperty } from '@nestjs/swagger'; + export abstract class Entity { constructor(id: string, name: string, distributed = false) { this.id = id; @@ -7,7 +9,14 @@ export abstract class Entity { readonly id: string; name: string; + + @ApiProperty({ + oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], + }) state: string | number | boolean | Buffer; + + @ApiProperty({ type: 'object' }) attributes: { [key: string]: any } = {}; + readonly distributed: boolean; } diff --git a/src/entities/entity.proxy.ts b/src/entities/entity.proxy.ts index 5731e41..7abcde0 100644 --- a/src/entities/entity.proxy.ts +++ b/src/entities/entity.proxy.ts @@ -1,4 +1,4 @@ -import { Entity } from './entity'; +import { Entity } from './entity.dto'; import { AttributesProxyHandler } from './attributes.proxy'; import { EntitiesEventEmitter } from './entities.events'; import { EntityBehavior } from './entities.config'; diff --git a/src/entities/sensor.ts b/src/entities/sensor.ts index b1474f2..0b014de 100644 --- a/src/entities/sensor.ts +++ b/src/entities/sensor.ts @@ -1,4 +1,4 @@ -import { Entity } from './entity'; +import { Entity } from './entity.dto'; export class Sensor extends Entity { state: number | string; diff --git a/src/entities/switch.ts b/src/entities/switch.ts index fcafa65..9807777 100644 --- a/src/entities/switch.ts +++ b/src/entities/switch.ts @@ -1,4 +1,4 @@ -import { Entity } from './entity'; +import { Entity } from './entity.dto'; export class Switch extends Entity { state: boolean; diff --git a/src/integrations/grid-eye/grid-eye.service.ts b/src/integrations/grid-eye/grid-eye.service.ts index befafc0..fe3e879 100644 --- a/src/integrations/grid-eye/grid-eye.service.ts +++ b/src/integrations/grid-eye/grid-eye.service.ts @@ -5,7 +5,7 @@ import { OnApplicationShutdown, } from '@nestjs/common'; import i2cBus, { PromisifiedBus } from 'i2c-bus'; -import { Entity } from '../../entities/entity'; +import { Entity } from '../../entities/entity.dto'; import { EntitiesService } from '../../entities/entities.service'; import { Sensor } from '../../entities/sensor'; import * as math from 'mathjs'; diff --git a/src/integrations/home-assistant/home-assistant.service.spec.ts b/src/integrations/home-assistant/home-assistant.service.spec.ts index 67992d4..cea43fd 100644 --- a/src/integrations/home-assistant/home-assistant.service.spec.ts +++ b/src/integrations/home-assistant/home-assistant.service.spec.ts @@ -15,7 +15,7 @@ import * as mqtt from 'async-mqtt'; import { system, Systeminformation } from 'systeminformation'; import SystemData = Systeminformation.SystemData; import { SensorConfig } from './sensor-config'; -import { Entity } from '../../entities/entity'; +import { Entity } from '../../entities/entity.dto'; import { Sensor } from '../../entities/sensor'; import { BinarySensor } from '../../entities/binary-sensor'; import { Switch } from '../../entities/switch'; diff --git a/src/integrations/home-assistant/home-assistant.service.ts b/src/integrations/home-assistant/home-assistant.service.ts index 0b3804e..becde84 100644 --- a/src/integrations/home-assistant/home-assistant.service.ts +++ b/src/integrations/home-assistant/home-assistant.service.ts @@ -5,7 +5,7 @@ import { OnApplicationShutdown, OnModuleInit, } from '@nestjs/common'; -import { Entity } from '../../entities/entity'; +import { Entity } from '../../entities/entity.dto'; import { Sensor } from '../../entities/sensor'; import { EntityConfig } from './entity-config'; import { SensorConfig } from './sensor-config'; diff --git a/src/integrations/omron-d6t/omron-d6t.service.ts b/src/integrations/omron-d6t/omron-d6t.service.ts index 030ec13..d2d8a3f 100644 --- a/src/integrations/omron-d6t/omron-d6t.service.ts +++ b/src/integrations/omron-d6t/omron-d6t.service.ts @@ -11,7 +11,7 @@ import i2cBus, { PromisifiedBus } from 'i2c-bus'; import { Interval } from '@nestjs/schedule'; import * as math from 'mathjs'; import { Sensor } from '../../entities/sensor'; -import { Entity } from '../../entities/entity'; +import { Entity } from '../../entities/entity.dto'; import { I2CError } from './i2c.error'; import { SensorConfig } from '../home-assistant/sensor-config'; import { ThermopileOccupancyService } from '../thermopile/thermopile-occupancy.service'; diff --git a/src/main.ts b/src/main.ts index c615cc0..c775508 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,12 +2,37 @@ import './env'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { WINSTON_LOGGER } from './logger'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const pkg = require('../package.json'); async function bootstrap(): Promise { const app = await NestFactory.create(AppModule, { logger: WINSTON_LOGGER, }); app.enableShutdownHooks(); + + const options = new DocumentBuilder() + .setTitle('room-assistant') + .setDescription( + 'Presence tracking and more for automation on the room-level.' + ) + .setVersion(pkg.version) + .setExternalDoc('Website', 'https://room-assistant.io') + .setLicense( + 'MIT License', + 'https://github.com/mKeRix/room-assistant/blob/master/LICENSE' + ) + .addTag('entities', 'Access to the internal entity registry', { + description: 'Configuration', + url: 'https://www.room-assistant.io/guide/entities.html', + }) + .addTag('status', 'Status information about the room-assistant instance') + .build(); + const document = SwaggerModule.createDocument(app, options); + SwaggerModule.setup('api', app, document); + await app.listenAsync(6415); } diff --git a/src/status/status.controller.ts b/src/status/status.controller.ts index a84c9b1..3f949ed 100644 --- a/src/status/status.controller.ts +++ b/src/status/status.controller.ts @@ -5,7 +5,9 @@ import { HealthCheckService, } from '@nestjs/terminus'; import { HealthIndicatorService } from './health-indicator.service'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +@ApiTags('status') @Controller('status') export class StatusController { constructor( @@ -14,6 +16,9 @@ export class StatusController { ) {} @Get() + @ApiOperation({ + summary: 'Check if this instance is healthy', + }) @HealthCheck() check(): Promise { return this.health.check(this.healthIndicatorService.getIndicators());