Compare commits
14 Commits
0df5b350d9
...
master
Author | SHA1 | Date | |
---|---|---|---|
324d2ac7f4 | |||
8a0212a139 | |||
802806b4c2 | |||
1d4bf2d0b6 | |||
79dcb4d75a | |||
7c67fa7de0 | |||
89a416ac38 | |||
e9ffe514dd | |||
6a579772df | |||
20a4b4d349 | |||
0085b95198 | |||
579481ce16 | |||
645f2bb44b | |||
c3bbbd3d13 |
45
.drone.yml
Normal file
45
.drone.yml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: code-analysis
|
||||||
|
image: aosapps/drone-sonar-plugin
|
||||||
|
settings:
|
||||||
|
sonar_host:
|
||||||
|
from_secret: SONAR_HOST
|
||||||
|
sonar_token:
|
||||||
|
from_secret: SONAR_CODE
|
||||||
|
|
||||||
|
- name: kaniko
|
||||||
|
image: banzaicloud/drone-kaniko
|
||||||
|
settings:
|
||||||
|
registry: registry.kmlabz.com
|
||||||
|
repo: birbnetes/${DRONE_REPO_NAME}
|
||||||
|
username:
|
||||||
|
from_secret: DOCKER_USERNAME
|
||||||
|
password:
|
||||||
|
from_secret: DOCKER_PASSWORD
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
- ${DRONE_BUILD_NUMBER}
|
||||||
|
|
||||||
|
- name: dockerhub
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
repo: birbnetes/${DRONE_REPO_NAME}
|
||||||
|
username:
|
||||||
|
from_secret: DOCKERHUB_USER
|
||||||
|
password:
|
||||||
|
from_secret: DOCKERHUB_PASSWORD
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
- ${DRONE_BUILD_NUMBER}
|
||||||
|
|
||||||
|
- name: ms-teams
|
||||||
|
image: kuperiu/drone-teams
|
||||||
|
settings:
|
||||||
|
webhook:
|
||||||
|
from_secret: TEAMS_WEBHOOK
|
||||||
|
when:
|
||||||
|
status: [ failure ]
|
@ -82,7 +82,7 @@ namespace Birdmap.API.Controllers
|
|||||||
Service = new()
|
Service = new()
|
||||||
{
|
{
|
||||||
Id = 0,
|
Id = 0,
|
||||||
Name = "Mqtt Client Service",
|
Name = "Message Queue Service",
|
||||||
Uri = "localhost",
|
Uri = "localhost",
|
||||||
},
|
},
|
||||||
Response = $"IsConnected: {_communicationService.IsConnected}",
|
Response = $"IsConnected: {_communicationService.IsConnected}",
|
||||||
|
@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NSwag.Generation.Processors.Security;
|
using NSwag.Generation.Processors.Security;
|
||||||
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Birdmap.API
|
namespace Birdmap.API
|
||||||
@ -71,7 +72,7 @@ namespace Birdmap.API
|
|||||||
{
|
{
|
||||||
opt.Title = "Birdmap";
|
opt.Title = "Birdmap";
|
||||||
opt.OperationProcessors.Add(new OperationSecurityScopeProcessor("Jwt Token"));
|
opt.OperationProcessors.Add(new OperationSecurityScopeProcessor("Jwt Token"));
|
||||||
opt.AddSecurity("Jwt Token", new string[] { },
|
opt.AddSecurity("Jwt Token", Array.Empty<string>(),
|
||||||
new NSwag.OpenApiSecurityScheme
|
new NSwag.OpenApiSecurityScheme
|
||||||
{
|
{
|
||||||
Type = NSwag.OpenApiSecuritySchemeType.ApiKey,
|
Type = NSwag.OpenApiSecuritySchemeType.ApiKey,
|
||||||
@ -95,11 +96,9 @@ namespace Birdmap.API
|
|||||||
app.UseOpenApi();
|
app.UseOpenApi();
|
||||||
app.UseSwaggerUi3();
|
app.UseSwaggerUi3();
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseSpaStaticFiles();
|
app.UseSpaStaticFiles();
|
||||||
|
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
@ -27,10 +27,25 @@
|
|||||||
"UseRabbitMq": false,
|
"UseRabbitMq": false,
|
||||||
"Mqtt": {
|
"Mqtt": {
|
||||||
"BrokerHostSettings": {
|
"BrokerHostSettings": {
|
||||||
|
"VirtualHost": "",
|
||||||
"Host": "",
|
"Host": "",
|
||||||
"Port": 1883
|
"Port": 1883
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"ExchangeSettings": {
|
||||||
|
"Name": "",
|
||||||
|
"Type": "",
|
||||||
|
"Durable": false,
|
||||||
|
"AutoDelete": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"QueueSettings": {
|
||||||
|
"Name": "",
|
||||||
|
"Durable": false,
|
||||||
|
"Exclusive": false,
|
||||||
|
"AutoDelete": false
|
||||||
|
},
|
||||||
|
|
||||||
"ClientSettings": {
|
"ClientSettings": {
|
||||||
"Id": "ASP.NET Core client",
|
"Id": "ASP.NET Core client",
|
||||||
"Username": "",
|
"Username": "",
|
||||||
|
@ -35,6 +35,8 @@
|
|||||||
|
|
||||||
<!--Skip non-critical Mqtt logs-->
|
<!--Skip non-critical Mqtt logs-->
|
||||||
<logger name="*.*Mqtt*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile" final="true"/>
|
<logger name="*.*Mqtt*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile" final="true"/>
|
||||||
|
<logger name="*.*RabbitMq*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile" final="true"/>
|
||||||
|
<logger name="*.*CommunicationServiceBase*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile" final="true"/>
|
||||||
|
|
||||||
<!--Skip non-critical Hub logs-->
|
<!--Skip non-critical Hub logs-->
|
||||||
<logger name="*.*Hubs*.*" minlevel="Trace" maxlevel="Warning" writeTo="hubsFile" final="true"/>
|
<logger name="*.*Hubs*.*" minlevel="Trace" maxlevel="Warning" writeTo="hubsFile" final="true"/>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
namespace Birdmap.BLL.Interfaces
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace Birdmap.BLL.Interfaces
|
||||||
{
|
{
|
||||||
public interface ICommunicationService
|
public interface ICommunicationService : IHostedService
|
||||||
{
|
{
|
||||||
public bool IsConnected { get; }
|
public bool IsConnected { get; }
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,7 @@ using MQTTnet.Client.Receiving;
|
|||||||
|
|
||||||
namespace Birdmap.BLL.Interfaces
|
namespace Birdmap.BLL.Interfaces
|
||||||
{
|
{
|
||||||
public interface IMqttClientService : IHostedService,
|
public interface IMqttClientService : IMqttClientConnectedHandler,
|
||||||
IMqttClientConnectedHandler,
|
|
||||||
IMqttClientDisconnectedHandler,
|
IMqttClientDisconnectedHandler,
|
||||||
IMqttApplicationMessageReceivedHandler
|
IMqttApplicationMessageReceivedHandler
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
namespace Birdmap.BLL.Options
|
namespace Birdmap.BLL.Options
|
||||||
{
|
{
|
||||||
public record RabbitMqClientOptions(string Hostname, int Port, string Username, string Password, string Topic);
|
public record RabbitMqClientOptions(
|
||||||
|
string Hostname, int Port, string VirtualHost,
|
||||||
|
string Username, string Password,
|
||||||
|
string ExchangeName, string ExchangeType,
|
||||||
|
bool ExchangeDurable, bool ExchangeAutoDelete,
|
||||||
|
string QueueName,
|
||||||
|
bool QueueDurable, bool QueueAutoDelete, bool QueueExclusive,
|
||||||
|
string Topic);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using Newtonsoft.Json;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Timer = System.Timers.Timer;
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
@ -83,5 +84,9 @@ namespace Birdmap.BLL.Services.CommunationServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract Task StartAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
public abstract Task StopAsync(CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ namespace Birdmap.BLL.Services.CommunicationServices.Mqtt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -97,7 +97,7 @@ namespace Birdmap.BLL.Services.CommunicationServices.Mqtt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -7,21 +7,25 @@ using RabbitMQ.Client;
|
|||||||
using RabbitMQ.Client.Events;
|
using RabbitMQ.Client.Events;
|
||||||
using System;
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Birdmap.BLL.Services.CommunationServices.RabbitMq
|
namespace Birdmap.BLL.Services.CommunationServices.RabbitMq
|
||||||
{
|
{
|
||||||
internal class RabbitMqClientService : CommunicationServiceBase, IDisposable
|
internal class RabbitMqClientService : CommunicationServiceBase
|
||||||
{
|
{
|
||||||
private readonly IConnection _connection;
|
private IConnection _connection;
|
||||||
private readonly IModel _channel;
|
private IModel _channel;
|
||||||
|
private readonly IConnectionFactory _factory;
|
||||||
|
private readonly RabbitMqClientOptions _options;
|
||||||
|
|
||||||
public override bool IsConnected => throw new NotImplementedException();
|
public override bool IsConnected => _connection.IsOpen;
|
||||||
|
|
||||||
public RabbitMqClientService(RabbitMqClientOptions options, ILogger<RabbitMqClientService> logger, IInputService inputService, IHubContext<DevicesHub, IDevicesHubClient> hubContext)
|
public RabbitMqClientService(RabbitMqClientOptions options, ILogger<RabbitMqClientService> logger, IInputService inputService, IHubContext<DevicesHub, IDevicesHubClient> hubContext)
|
||||||
: base(logger, inputService, hubContext)
|
: base(logger, inputService, hubContext)
|
||||||
{
|
{
|
||||||
var factory = new ConnectionFactory()
|
_options = options;
|
||||||
|
_factory = new ConnectionFactory()
|
||||||
{
|
{
|
||||||
HostName = options.Hostname,
|
HostName = options.Hostname,
|
||||||
Port = options.Port,
|
Port = options.Port,
|
||||||
@ -30,25 +34,6 @@ namespace Birdmap.BLL.Services.CommunationServices.RabbitMq
|
|||||||
|
|
||||||
AutomaticRecoveryEnabled = true,
|
AutomaticRecoveryEnabled = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
_connection = factory.CreateConnection();
|
|
||||||
_channel = _connection.CreateModel();
|
|
||||||
|
|
||||||
_channel.ExchangeDeclare(exchange: "topic_logs", type: "topic");
|
|
||||||
var queueName = _channel.QueueDeclare().QueueName;
|
|
||||||
|
|
||||||
_channel.QueueBind(queue: queueName,
|
|
||||||
exchange: "topic_logs",
|
|
||||||
routingKey: options.Topic);
|
|
||||||
|
|
||||||
var consumer = new AsyncEventingBasicConsumer(_channel);
|
|
||||||
consumer.Received += OnRecieved;
|
|
||||||
|
|
||||||
_channel.BasicConsume(queue: queueName,
|
|
||||||
autoAck: true,
|
|
||||||
consumer: consumer);
|
|
||||||
|
|
||||||
StartMessageTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task OnRecieved(object sender, BasicDeliverEventArgs eventArgs)
|
private Task OnRecieved(object sender, BasicDeliverEventArgs eventArgs)
|
||||||
@ -62,11 +47,68 @@ namespace Birdmap.BLL.Services.CommunationServices.RabbitMq
|
|||||||
return ProcessJsonMessageAsync(body);
|
return ProcessJsonMessageAsync(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Connect();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Cannot connect. Reconnecting...");
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
await StartAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
StopMessageTimer();
|
StopMessageTimer();
|
||||||
_channel.Close();
|
_channel?.Close();
|
||||||
_connection.Close();
|
_connection?.Close();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Cannot disconnect...");
|
||||||
|
return Task.FromException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Connect()
|
||||||
|
{
|
||||||
|
|
||||||
|
_connection = _factory.CreateConnection();
|
||||||
|
_channel = _connection.CreateModel();
|
||||||
|
|
||||||
|
_channel.ExchangeDeclare(
|
||||||
|
exchange: _options.ExchangeName,
|
||||||
|
type: _options.ExchangeType,
|
||||||
|
durable: _options.ExchangeDurable,
|
||||||
|
autoDelete: _options.ExchangeAutoDelete);
|
||||||
|
|
||||||
|
_channel.QueueDeclare(
|
||||||
|
queue: _options.QueueName,
|
||||||
|
durable: _options.QueueDurable,
|
||||||
|
exclusive: _options.QueueExclusive,
|
||||||
|
autoDelete: _options.QueueAutoDelete);
|
||||||
|
|
||||||
|
_channel.QueueBind(queue: _options.QueueName,
|
||||||
|
exchange: _options.ExchangeName,
|
||||||
|
routingKey: _options.Topic);
|
||||||
|
|
||||||
|
var consumer = new AsyncEventingBasicConsumer(_channel);
|
||||||
|
consumer.Received += OnRecieved;
|
||||||
|
|
||||||
|
_channel.BasicConsume(queue: _options.QueueName,
|
||||||
|
autoAck: true,
|
||||||
|
consumer: consumer);
|
||||||
|
|
||||||
|
StartMessageTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,25 @@ namespace Birdmap.BLL
|
|||||||
{
|
{
|
||||||
Host = brokerHost.GetValue<string>("Host"),
|
Host = brokerHost.GetValue<string>("Host"),
|
||||||
Port = brokerHost.GetValue<int>("Port"),
|
Port = brokerHost.GetValue<int>("Port"),
|
||||||
|
VirtualHost = brokerHost.GetValue<string>("VirtualHost"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var exchange = mqtt.GetSection("ExchangeSettings");
|
||||||
|
var exchangeSettings = new
|
||||||
|
{
|
||||||
|
Name = exchange.GetValue<string>("Name"),
|
||||||
|
Type = exchange.GetValue<string>("Type"),
|
||||||
|
Durable = exchange.GetValue<bool>("Durable"),
|
||||||
|
AutoDelete = exchange.GetValue<bool>("AutoDelete"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var queue = mqtt.GetSection("QueueSettings");
|
||||||
|
var queueSettings = new
|
||||||
|
{
|
||||||
|
Name = queue.GetValue<string>("Name"),
|
||||||
|
Durable = exchange.GetValue<bool>("Durable"),
|
||||||
|
Exclusive = exchange.GetValue<bool>("Exclusive"),
|
||||||
|
AutoDelete = exchange.GetValue<bool>("AutoDelete"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (configuration.GetValue<bool>("UseRabbitMq"))
|
if (configuration.GetValue<bool>("UseRabbitMq"))
|
||||||
@ -68,8 +87,17 @@ namespace Birdmap.BLL
|
|||||||
services.AddRabbitMqClientServiceWithConfig(new RabbitMqClientOptions(
|
services.AddRabbitMqClientServiceWithConfig(new RabbitMqClientOptions(
|
||||||
Hostname: brokerHostSettings.Host,
|
Hostname: brokerHostSettings.Host,
|
||||||
Port: brokerHostSettings.Port,
|
Port: brokerHostSettings.Port,
|
||||||
|
VirtualHost: brokerHostSettings.VirtualHost,
|
||||||
Username: clientSettings.Username,
|
Username: clientSettings.Username,
|
||||||
Password: clientSettings.Password,
|
Password: clientSettings.Password,
|
||||||
|
ExchangeName: exchangeSettings.Name,
|
||||||
|
ExchangeType: exchangeSettings.Type,
|
||||||
|
ExchangeDurable: exchangeSettings.Durable,
|
||||||
|
ExchangeAutoDelete: exchangeSettings.AutoDelete,
|
||||||
|
QueueName: queueSettings.Name,
|
||||||
|
QueueDurable: queueSettings.Durable,
|
||||||
|
QueueExclusive: queueSettings.Exclusive,
|
||||||
|
QueueAutoDelete: queueSettings.AutoDelete,
|
||||||
Topic: clientSettings.Topic));
|
Topic: clientSettings.Topic));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -111,9 +139,9 @@ namespace Birdmap.BLL
|
|||||||
private static IServiceCollection AddClientServiceWithProvider<T>(this IServiceCollection services) where T : class, ICommunicationService
|
private static IServiceCollection AddClientServiceWithProvider<T>(this IServiceCollection services) where T : class, ICommunicationService
|
||||||
{
|
{
|
||||||
services.AddSingleton<T>();
|
services.AddSingleton<T>();
|
||||||
services.AddSingleton(serviceProvider =>
|
services.AddSingleton<IHostedService>(serviceProvider =>
|
||||||
{
|
{
|
||||||
return (IHostedService)serviceProvider.GetService<T>();
|
return serviceProvider.GetService<T>();
|
||||||
});
|
});
|
||||||
services.AddSingleton<ICommunicationServiceProvider>(serviceProvider =>
|
services.AddSingleton<ICommunicationServiceProvider>(serviceProvider =>
|
||||||
{
|
{
|
||||||
|
@ -17,7 +17,7 @@ services:
|
|||||||
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
|
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Birdmap.API/Dockerfile
|
dockerfile: Dockerfile
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
environment:
|
environment:
|
||||||
@ -38,9 +38,18 @@ services:
|
|||||||
- Birdmap_Defaults__Services__KMLabz-Service=https://birb.k8s.kmlabz.com/devices
|
- Birdmap_Defaults__Services__KMLabz-Service=https://birb.k8s.kmlabz.com/devices
|
||||||
- Birdmap_UseDummyServices=true
|
- Birdmap_UseDummyServices=true
|
||||||
- Birdmap_ServicesBaseUrl=https://birb.k8s.kmlabz.com/
|
- Birdmap_ServicesBaseUrl=https://birb.k8s.kmlabz.com/
|
||||||
- Birdmap_UseRabbitMq=true
|
- Birdmap_UseRabbitMq=false
|
||||||
- Birdmap_Mqtt__BrokerHostSettings__Host=localhost
|
- Birdmap_Mqtt__BrokerHostSettings__Host=localhost
|
||||||
- Birdmap_Mqtt__BrokerHostSettings__Port=1883
|
- Birdmap_Mqtt__BrokerHostSettings__Port=1883
|
||||||
|
- Birdmap_Mqtt__BrokerHostSettings__VirtualHost=/
|
||||||
|
- Birdmap_Mqtt__ExchangeSettings__Name=birbmapexchange
|
||||||
|
- Birdmap_Mqtt__ExchangeSettings__Type=fanout
|
||||||
|
- Birdmap_Mqtt__ExchangeSettings__Durable=true
|
||||||
|
- Birdmap_Mqtt__ExchangeSettings__AutoDelete=true
|
||||||
|
- Birdmap_Mqtt__QueueSettings__Name=birbmapqueue
|
||||||
|
- Birdmap_Mqtt__QueueSettings__Durable=true
|
||||||
|
- Birdmap_Mqtt__QueueSettings__Exclusive=true
|
||||||
|
- Birdmap_Mqtt__QueueSettings__AutoDelete=true
|
||||||
- Birdmap_Mqtt__ClientSettings__Id=ASP.NET Core client
|
- Birdmap_Mqtt__ClientSettings__Id=ASP.NET Core client
|
||||||
- Birdmap_Mqtt__ClientSettings__Username=username
|
- Birdmap_Mqtt__ClientSettings__Username=username
|
||||||
- Birdmap_Mqtt__ClientSettings__Password=password
|
- Birdmap_Mqtt__ClientSettings__Password=password
|
||||||
|
Reference in New Issue
Block a user