From d11b765ddcb7eea273ab4ac6d4b707bb0f4349a6 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Sat, 22 Jul 2023 14:54:58 +0200 Subject: [PATCH] Add DeviceModel, DeviceManufacturer, and DeviceSpecification + Cleanup --- Common/BusinessRules/UIStringValidator.cs | 2 +- Common/DTOs/DeviceDTO.cs | 2 +- Common/DTOs/DeviceManufacturerDTO.cs | 14 + Common/DTOs/DeviceModelDTO.cs | 30 ++ Common/DTOs/DeviceSpecificationDTO.cs | 20 ++ ...30707120742_Add-Device-And-ControlGrant.cs | 3 +- ...709122559_Remove-Duplicate-Device-Table.cs | 3 +- Common/Database/Models/Device.cs | 4 +- Common/Database/Models/DeviceModel.cs | 2 +- Common/Services/DiscordBotService.cs | 2 + Common/Utils/StringUtils.cs | 2 - Common/Websocket/WebSocketHandler.cs | 2 +- RestAPI/Attributes/DisplaynameAttribute.cs | 87 +++++ RestAPI/Controllers/Api/V1/Device/Register.cs | 52 +++ .../Api/V1/Device/RegisterManufacturer.cs | 42 +++ .../Api/V1/Device/RegisterModel.cs | 84 +++++ .../Api/V1/Device/RegisterSpecification.cs | 45 +++ spec/openapi.yaml | 339 ++++++++++++++++++ 18 files changed, 723 insertions(+), 12 deletions(-) create mode 100644 Common/DTOs/DeviceManufacturerDTO.cs create mode 100644 Common/DTOs/DeviceModelDTO.cs create mode 100644 Common/DTOs/DeviceSpecificationDTO.cs create mode 100644 RestAPI/Attributes/DisplaynameAttribute.cs create mode 100644 RestAPI/Controllers/Api/V1/Device/Register.cs create mode 100644 RestAPI/Controllers/Api/V1/Device/RegisterManufacturer.cs create mode 100644 RestAPI/Controllers/Api/V1/Device/RegisterModel.cs create mode 100644 RestAPI/Controllers/Api/V1/Device/RegisterSpecification.cs diff --git a/Common/BusinessRules/UIStringValidator.cs b/Common/BusinessRules/UIStringValidator.cs index 3167c02..1c9f709 100644 --- a/Common/BusinessRules/UIStringValidator.cs +++ b/Common/BusinessRules/UIStringValidator.cs @@ -19,7 +19,7 @@ public static bool IsBadUiString(ReadOnlySpan str) str = str.Trim(); - // String was only whitespace + // String had whitespace on one or both ends if (str.Length != len) return true; // Check if string contains any unwanted characters diff --git a/Common/DTOs/DeviceDTO.cs b/Common/DTOs/DeviceDTO.cs index 1420b01..f72d7a4 100644 --- a/Common/DTOs/DeviceDTO.cs +++ b/Common/DTOs/DeviceDTO.cs @@ -20,7 +20,7 @@ public readonly struct DeviceDto public RfProtocol Protocol { get; init; } - public Guid ManufacturerId { get; init; } + public Guid? ManufacturerId { get; init; } public string ManufacturerName { get; init; } diff --git a/Common/DTOs/DeviceManufacturerDTO.cs b/Common/DTOs/DeviceManufacturerDTO.cs new file mode 100644 index 0000000..58362b1 --- /dev/null +++ b/Common/DTOs/DeviceManufacturerDTO.cs @@ -0,0 +1,14 @@ +using ZapMe.Enums.Devices; + +namespace ZapMe.DTOs; + +public readonly struct DeviceManufacturerDto +{ + public Guid Id { get; init; } + + public string Name { get; init; } + + public string ModelWebsiteUrl { get; init; } + + public Uri IconUrl { get; init; } +} \ No newline at end of file diff --git a/Common/DTOs/DeviceModelDTO.cs b/Common/DTOs/DeviceModelDTO.cs new file mode 100644 index 0000000..e67b577 --- /dev/null +++ b/Common/DTOs/DeviceModelDTO.cs @@ -0,0 +1,30 @@ +using ZapMe.Enums.Devices; + +namespace ZapMe.DTOs; + +public readonly struct DeviceModelDto +{ + public Guid Id { get; init; } + + public string Name { get; init; } + + public Guid ModelId { get; init; } + + public string ModelName { get; init; } + + public string ModelNumber { get; init; } + + public string ModelWebsiteUrl { get; init; } + + public Uri IconUrl { get; init; } + + public RfProtocol Protocol { get; init; } + + public Guid ManufacturerId { get; init; } + + public string ManufacturerName { get; init; } + + public string ManufacturerWebsiteUrl { get; init; } + + public DateTime RegisteredAt { get; init; } +} \ No newline at end of file diff --git a/Common/DTOs/DeviceSpecificationDTO.cs b/Common/DTOs/DeviceSpecificationDTO.cs new file mode 100644 index 0000000..aa9f0cd --- /dev/null +++ b/Common/DTOs/DeviceSpecificationDTO.cs @@ -0,0 +1,20 @@ +using ZapMe.Database.Documents; + +namespace ZapMe.DTOs; + +public readonly struct DeviceSpecificationDto +{ + public Guid Id { get; init; } + + public ulong Frequency { get; init; } + + public DeviceProprietarySpecification.ModulationType Modulation { get; init; } + + public ulong DataRate { get; init; } + + public ulong PacketSize { get; init; } + + public DeviceProprietarySpecification.EncodingType Encoding { get; init; } + + public bool InvertedBits { get; init; } +} \ No newline at end of file diff --git a/Common/Database/Migrations/20230707120742_Add-Device-And-ControlGrant.cs b/Common/Database/Migrations/20230707120742_Add-Device-And-ControlGrant.cs index 075db0c..cf017f3 100644 --- a/Common/Database/Migrations/20230707120742_Add-Device-And-ControlGrant.cs +++ b/Common/Database/Migrations/20230707120742_Add-Device-And-ControlGrant.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/Common/Database/Migrations/20230709122559_Remove-Duplicate-Device-Table.cs b/Common/Database/Migrations/20230709122559_Remove-Duplicate-Device-Table.cs index fcea009..683c4cf 100644 --- a/Common/Database/Migrations/20230709122559_Remove-Duplicate-Device-Table.cs +++ b/Common/Database/Migrations/20230709122559_Remove-Duplicate-Device-Table.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/Common/Database/Models/Device.cs b/Common/Database/Models/Device.cs index 0815bab..dae3418 100644 --- a/Common/Database/Models/Device.cs +++ b/Common/Database/Models/Device.cs @@ -8,9 +8,9 @@ public sealed class DeviceEntity { public Guid Id { get; private set; } - public Guid ModelId { get; private set; } + public Guid ModelId { get; init; } - public Guid OwnerId { get; private set; } + public Guid OwnerId { get; init; } public required string Name { get; set; } diff --git a/Common/Database/Models/DeviceModel.cs b/Common/Database/Models/DeviceModel.cs index 0ea32e8..a7d02c1 100644 --- a/Common/Database/Models/DeviceModel.cs +++ b/Common/Database/Models/DeviceModel.cs @@ -26,7 +26,7 @@ public sealed class DeviceModelEntity public Guid IconId { get; init; } - public Guid ManufacturerId { get; init; } + public Guid? ManufacturerId { get; init; } /// /// The FCC ID of this device model, if applicable. diff --git a/Common/Services/DiscordBotService.cs b/Common/Services/DiscordBotService.cs index 3ad5891..d3a05aa 100644 --- a/Common/Services/DiscordBotService.cs +++ b/Common/Services/DiscordBotService.cs @@ -35,8 +35,10 @@ public async Task SetActivityAsync(DiscordActivity activity, UserStatus? status, } else { +#if !DEBUG await _client.ConnectAsync(activity, status, idleSince); IsConnected = true; +#endif } } } diff --git a/Common/Utils/StringUtils.cs b/Common/Utils/StringUtils.cs index aeaeb9e..e09c6ab 100644 --- a/Common/Utils/StringUtils.cs +++ b/Common/Utils/StringUtils.cs @@ -1,6 +1,4 @@ using System.Buffers; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Security.Cryptography; namespace ZapMe.Utils; diff --git a/Common/Websocket/WebSocketHandler.cs b/Common/Websocket/WebSocketHandler.cs index 5432488..4c74c68 100644 --- a/Common/Websocket/WebSocketHandler.cs +++ b/Common/Websocket/WebSocketHandler.cs @@ -137,7 +137,7 @@ private async Task RunDeviceAsync(WebSocket webSocket, CancellationToken cancell .FirstOrDefaultAsync(cancellationToken); if (authenticationResult is null) { - _logger.LogError("Failed to authenticate websocket connection, provided AccessToken was invalid"); + _logger.LogError("Failed to authenticate websocket connection, provided AccessToken was invalid"); return; } diff --git a/RestAPI/Attributes/DisplaynameAttribute.cs b/RestAPI/Attributes/DisplaynameAttribute.cs new file mode 100644 index 0000000..ce59ea6 --- /dev/null +++ b/RestAPI/Attributes/DisplaynameAttribute.cs @@ -0,0 +1,87 @@ +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using System.ComponentModel.DataAnnotations; +using ZapMe.BusinessRules; + +namespace ZapMe.Attributes; + +/// +/// An attribute used to validate whether a display name is valid. +/// +/// +/// Inherits from . +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] +public class DisplaynameAttribute : ValidationAttribute, IParameterAttribute +{ + /// + /// Regular expression for username validation. + /// + public const string DisplaynameRegex = /* lang=regex */ @"^[^\s].*[^\s]$"; + + /// + /// Example username used to generate OpenApi documentation. + /// + public const string ExampleDisplayname = "String"; + + private const string _ErrMsgMustBeString = "Must be a string"; + + /// + /// Indicates whether validation should be performed. + /// + public bool ShouldValidate { get; } + + /// + /// Initializes a new instance of the class with the specified validation behavior. + /// + /// True if validation should be performed; otherwise, false. + public DisplaynameAttribute(bool shouldValidate) + { + ShouldValidate = shouldValidate; + } + + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (!ShouldValidate) return ValidationResult.Success; + + if (value is null) + { + return ValidationResult.Success; + } + + if (value is not string displayname) + { + return new ValidationResult(_ErrMsgMustBeString); + } + + if (Char.IsWhiteSpace(displayname[0]) || Char.IsWhiteSpace(displayname[^1])) + { + return new ValidationResult("Displayname cannot start or end with whitespace."); + } + + if (UIStringValidator.IsBadUiString(displayname)) + { + return new ValidationResult("Displayname must not contain obnoxious characters."); + } + + return ValidationResult.Success; + } + + /// + public void Apply(OpenApiSchema schema) + { + if (ShouldValidate) + { + schema.Pattern = DisplaynameRegex; + } + + schema.Example = new OpenApiString(ExampleDisplayname); + } + + /// + public void Apply(OpenApiParameter parameter) + { + Apply(parameter.Schema); + } +} diff --git a/RestAPI/Controllers/Api/V1/Device/Register.cs b/RestAPI/Controllers/Api/V1/Device/Register.cs new file mode 100644 index 0000000..a82ae4b --- /dev/null +++ b/RestAPI/Controllers/Api/V1/Device/Register.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Security.Claims; +using ZapMe.Attributes; +using ZapMe.Database.Models; +using ZapMe.DTOs; +using ZapMe.Utils; + +namespace ZapMe.Controllers.Api.V1; + +public partial class DeviceController +{ + public readonly record struct RegisterDeviceRequest + { + public Guid DeviceModelId { get; init; } + + [Displayname(true)] + public string Name { get; init; } + } + + /// + /// Register a device. + /// + /// + /// Device + [HttpPost(Name = "RegisterDevice")] + [ProducesResponseType(typeof(DeviceDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task Register(RegisterDeviceRequest data, CancellationToken cancellationToken) + { + var deviceModel = await _dbContext.DeviceModels + .AsNoTracking() + .Where(dm => dm.Id == data.DeviceModelId) + .FirstOrDefaultAsync(cancellationToken); + + if (deviceModel is null) + return NotFound(); + + var device = new DeviceEntity + { + ModelId = deviceModel.Id, + OwnerId = User.GetUserId(), + Name = data.Name, + AccessToken = StringUtils.GenerateUrlSafeRandomString(64) + }; + + _dbContext.Devices.Add(device); + await _dbContext.SaveChangesAsync(cancellationToken); + + return Ok(); + } +} diff --git a/RestAPI/Controllers/Api/V1/Device/RegisterManufacturer.cs b/RestAPI/Controllers/Api/V1/Device/RegisterManufacturer.cs new file mode 100644 index 0000000..a40a4d0 --- /dev/null +++ b/RestAPI/Controllers/Api/V1/Device/RegisterManufacturer.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; +using ZapMe.Attributes; +using ZapMe.Database.Models; +using ZapMe.DTOs; + +namespace ZapMe.Controllers.Api.V1; + +public partial class DeviceController +{ + public readonly struct RegisterDeviceManufacturerRequest + { + [Displayname(true)] + public required string Name { get; init; } + + public required string WebsiteUrl { get; init; } + + public Guid IconId { get; init; } + } + + /// + /// Register a device manufacturer. + /// + /// + /// Device + [HttpPost("manufacturer", Name = "RegisterDeviceManufacturer")] + [ProducesResponseType(typeof(DeviceManufacturerDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task RegisterManufacturer(RegisterDeviceManufacturerRequest data, CancellationToken cancellationToken) + { + var deviceManufacturer = new DeviceManufacturerEntity + { + Name = data.Name, + WebsiteUrl = data.WebsiteUrl, + IconId = data.IconId, + }; + + _dbContext.DeviceManufacturers.Add(deviceManufacturer); + await _dbContext.SaveChangesAsync(cancellationToken); + + return Ok(); + } +} diff --git a/RestAPI/Controllers/Api/V1/Device/RegisterModel.cs b/RestAPI/Controllers/Api/V1/Device/RegisterModel.cs new file mode 100644 index 0000000..8384062 --- /dev/null +++ b/RestAPI/Controllers/Api/V1/Device/RegisterModel.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using ZapMe.Attributes; +using ZapMe.Database.Documents; +using ZapMe.Database.Models; +using ZapMe.DTOs; +using ZapMe.Enums.Devices; + +namespace ZapMe.Controllers.Api.V1; + +public partial class DeviceController +{ + public readonly struct RegisterDeviceModelRequest + { + [Displayname(true)] + public string Name { get; init; } + + public string ModelNumber { get; init; } + + public string WebsiteUrl { get; init; } + + public Guid IconId { get; init; } + + public Guid? ManufacturerId { get; init; } + + public string? FccId { get; init; } + + public string DocumentationUrl { get; init; } + + public RfProtocol Protocol { get; init; } + + public Guid? SpecificationId { get; init; } + } + + /// + /// Register a device. + /// + /// + /// Device + [HttpPost("model", Name = "RegisterDeviceModel")] + [ProducesResponseType(typeof(DeviceModelDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task RegisterModel(RegisterDeviceModelRequest data, [FromServices] Marten.IQuerySession querySession, CancellationToken cancellationToken) + { + DeviceManufacturerEntity? manufacturer = null; + if (data.ManufacturerId.HasValue) + { + manufacturer = await _dbContext.DeviceManufacturers + .AsNoTracking() + .Where(dm => dm.Id == data.ManufacturerId.Value) + .FirstOrDefaultAsync(cancellationToken); + + if (manufacturer is null) + return NotFound(); + } + + DeviceProprietarySpecification? specification = null; + if (data.SpecificationId.HasValue) + { + specification = await querySession.LoadAsync(data.SpecificationId.Value, cancellationToken); + + if (specification is null) + return NotFound(); + } + + var deviceModel = new DeviceModelEntity + { + Name = data.Name, + ModelNumber = data.ModelNumber, + WebsiteUrl = data.WebsiteUrl, + IconId = data.IconId, + ManufacturerId = manufacturer?.Id, + FccId = data.FccId, + DocumentationUrl = data.DocumentationUrl, + Protocol = data.Protocol, + SpecificationId = specification?.Id + }; + + _dbContext.DeviceModels.Add(deviceModel); + await _dbContext.SaveChangesAsync(cancellationToken); + + return Ok(); + } +} diff --git a/RestAPI/Controllers/Api/V1/Device/RegisterSpecification.cs b/RestAPI/Controllers/Api/V1/Device/RegisterSpecification.cs new file mode 100644 index 0000000..b75ceb0 --- /dev/null +++ b/RestAPI/Controllers/Api/V1/Device/RegisterSpecification.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using ZapMe.Database.Documents; +using ZapMe.DTOs; + +namespace ZapMe.Controllers.Api.V1; + +public partial class DeviceController +{ + public readonly struct RegisterDeviceSpecificationRequest + { + public ulong Frequency { get; init; } + public DeviceProprietarySpecification.ModulationType Modulation { get; init; } + public ulong DataRate { get; init; } + public ulong PacketSize { get; init; } + public DeviceProprietarySpecification.EncodingType Encoding { get; init; } + public bool InvertedBits { get; init; } + } + + /// + /// Register a device specification. + /// + /// + /// Device + [HttpPost("specification", Name = "RegisterDeviceSpecification")] + [ProducesResponseType(typeof(DeviceSpecificationDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task RegisterSpecification(RegisterDeviceSpecificationRequest data, [FromServices] Marten.IDocumentStore documentStore, CancellationToken cancellationToken) + { + var device = new DeviceProprietarySpecification + { + Frequency = data.Frequency, + Modulation = data.Modulation, + DataRate = data.DataRate, + PacketSize = data.PacketSize, + Encoding = data.Encoding, + InvertedBits = data.InvertedBits, + }; + + using var session = documentStore.LightweightSession(); + session.Store(device); + await session.SaveChangesAsync(cancellationToken); + + return Ok(); + } +} diff --git a/spec/openapi.yaml b/spec/openapi.yaml index 9519592..a18d729 100644 --- a/spec/openapi.yaml +++ b/spec/openapi.yaml @@ -448,6 +448,106 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorDetails' + /api/v1/device: + post: + tags: + - Device + summary: Register a device. + operationId: RegisterDevice + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/RegisterDeviceRequest' + responses: + '200': + description: Device + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceDto' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDetails' + /api/v1/device/manufacturer: + post: + tags: + - Device + summary: Register a device manufacturer. + operationId: RegisterDeviceManufacturer + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/RegisterDeviceManufacturerRequest' + responses: + '200': + description: Device + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceManufacturerDto' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDetails' + /api/v1/device/model: + post: + tags: + - Device + summary: Register a device. + operationId: RegisterDeviceModel + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/RegisterDeviceModelRequest' + responses: + '200': + description: Device + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceModelDto' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDetails' + /api/v1/device/specification: + post: + tags: + - Device + summary: Register a device specification. + operationId: RegisterDeviceSpecification + requestBody: + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/RegisterDeviceSpecificationRequest' + responses: + '200': + description: Device + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceSpecificationDto' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorDetails' /api/v1/health: get: tags: @@ -1003,6 +1103,53 @@ components: format: uri additionalProperties: false DeviceDto: + required: + - iconUrl + - id + - manufacturerName + - manufacturerWebsiteUrl + - modelId + - modelName + - modelNumber + - modelWebsiteUrl + - name + - protocol + - registeredAt + type: object + properties: + id: + type: string + format: uuid + name: + type: string + modelId: + type: string + format: uuid + modelName: + type: string + modelNumber: + type: string + modelWebsiteUrl: + type: string + iconUrl: + type: string + format: uri + protocol: + allOf: + - $ref: '#/components/schemas/RfProtocol' + manufacturerId: + type: string + format: uuid + nullable: true + manufacturerName: + type: string + manufacturerWebsiteUrl: + type: string + registeredAt: + type: string + format: date-time + additionalProperties: false + DeviceManufacturerDto: required: - iconUrl - id @@ -1049,6 +1196,91 @@ components: type: string format: date-time additionalProperties: false + DeviceModelDto: + required: + - iconUrl + - id + - manufacturerId + - manufacturerName + - manufacturerWebsiteUrl + - modelId + - modelName + - modelNumber + - modelWebsiteUrl + - name + - protocol + - registeredAt + type: object + properties: + id: + type: string + format: uuid + name: + type: string + modelId: + type: string + format: uuid + modelName: + type: string + modelNumber: + type: string + modelWebsiteUrl: + type: string + iconUrl: + type: string + format: uri + protocol: + allOf: + - $ref: '#/components/schemas/RfProtocol' + manufacturerId: + type: string + format: uuid + manufacturerName: + type: string + manufacturerWebsiteUrl: + type: string + registeredAt: + type: string + format: date-time + additionalProperties: false + DeviceSpecificationDto: + required: + - dataRate + - encoding + - frequency + - id + - invertedBits + - modulation + - packetSize + type: object + properties: + id: + type: string + format: uuid + frequency: + type: integer + format: int64 + modulation: + allOf: + - $ref: '#/components/schemas/ModulationType' + dataRate: + type: integer + format: int64 + packetSize: + type: integer + format: int64 + encoding: + allOf: + - $ref: '#/components/schemas/EncodingType' + invertedBits: + type: boolean + additionalProperties: false + EncodingType: + enum: + - none + - proprietary + - manchester + type: string ErrorDetails: required: - code @@ -1107,6 +1339,16 @@ components: description: UserId's of users that this user has sent friend requests to additionalProperties: false description: List of incoming and outgoing friendrequests + ModulationType: + enum: + - ask + - ook + - gfsk + - fsk + - fsK2 + - fsK4 + - msk + type: string NotificationSeverityLevel: enum: - info @@ -1169,6 +1411,103 @@ components: description: Response from cloudflare turnstile request additionalProperties: false description: Request sent to server to request a password reset token + RegisterDeviceManufacturerRequest: + required: + - iconId + - name + - websiteUrl + type: object + properties: + name: + pattern: '^[^\s].*[^\s]$' + type: string + example: String + websiteUrl: + type: string + iconId: + type: string + format: uuid + additionalProperties: false + RegisterDeviceModelRequest: + required: + - documentationUrl + - iconId + - modelNumber + - name + - protocol + - websiteUrl + type: object + properties: + name: + pattern: '^[^\s].*[^\s]$' + type: string + example: String + modelNumber: + type: string + websiteUrl: + type: string + iconId: + type: string + format: uuid + manufacturerId: + type: string + format: uuid + nullable: true + fccId: + type: string + nullable: true + documentationUrl: + type: string + protocol: + allOf: + - $ref: '#/components/schemas/RfProtocol' + specificationId: + type: string + format: uuid + nullable: true + additionalProperties: false + RegisterDeviceRequest: + required: + - deviceModelId + - name + type: object + properties: + deviceModelId: + type: string + format: uuid + name: + pattern: '^[^\s].*[^\s]$' + type: string + example: String + additionalProperties: false + RegisterDeviceSpecificationRequest: + required: + - dataRate + - encoding + - frequency + - invertedBits + - modulation + - packetSize + type: object + properties: + frequency: + type: integer + format: int64 + modulation: + allOf: + - $ref: '#/components/schemas/ModulationType' + dataRate: + type: integer + format: int64 + packetSize: + type: integer + format: int64 + encoding: + allOf: + - $ref: '#/components/schemas/EncodingType' + invertedBits: + type: boolean + additionalProperties: false RfProtocol: enum: - proprietary