dev #9
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
**/.classpath
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
4
.env
Normal file
4
.env
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
POSTGRES_USER="postgres"
|
||||||
|
POSTGRES_PASSWORD="12345"
|
||||||
|
POSTGRES_DB="main_database"
|
||||||
|
DB_CONNECTION_STRING="Host=postgres:5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
|
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md.
|
||||||
|
"name": ".NET Core Launch (web)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/Cloud/bin/Debug/net6.0/Cloud.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/Cloud",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||||
|
"serverReadyAction": {
|
||||||
|
"action": "openExternally",
|
||||||
|
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"dotnet.defaultSolution": "Cloud.sln"
|
||||||
|
}
|
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/Cloud.sln",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/Cloud.sln",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/Cloud.sln"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
22
Cloud.sln
Normal file
22
Cloud.sln
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cloud", "Cloud\Cloud.csproj", "{D279AAA9-D4F8-4C7B-B34D-44A7429C87AA}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{D279AAA9-D4F8-4C7B-B34D-44A7429C87AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D279AAA9-D4F8-4C7B-B34D-44A7429C87AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D279AAA9-D4F8-4C7B-B34D-44A7429C87AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D279AAA9-D4F8-4C7B-B34D-44A7429C87AA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
31
Cloud/ApplicationContext.cs
Normal file
31
Cloud/ApplicationContext.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using Cloud.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Cloud;
|
||||||
|
public class ApplicationContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<User> Users { get; set; } = null!;
|
||||||
|
public DbSet<Farm> Farms { get; set; } = null!;
|
||||||
|
public DbSet<Greenhouse> Greenhouses { get; set; } = null!;
|
||||||
|
|
||||||
|
public ApplicationContext(DbContextOptions<ApplicationContext> options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApplicationContext()
|
||||||
|
: base(new DbContextOptionsBuilder<ApplicationContext>()
|
||||||
|
.UseNpgsql("Host=localhost;Port=5438;Database=main_database;Username=postgres;Password=12345")
|
||||||
|
.Options)
|
||||||
|
{
|
||||||
|
Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
if (!optionsBuilder.IsConfigured)
|
||||||
|
{
|
||||||
|
optionsBuilder.UseNpgsql("Host=localhost;Port=5438;Database=main_database;Username=postgres;Password=12345");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
Cloud/Cloud.csproj
Normal file
24
Cloud/Cloud.csproj
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Confluent.Kafka" Version="2.6.1" />
|
||||||
|
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.14" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.6">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
|
||||||
|
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.35.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
104
Cloud/Controllers/AuthController.cs
Normal file
104
Cloud/Controllers/AuthController.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Cloud.Models;
|
||||||
|
using Cloud.Requests;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace Cloud.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AuthController : ControllerBase
|
||||||
|
{
|
||||||
|
private PasswordHasher<User> _passwordHasher;
|
||||||
|
private IConfiguration _config;
|
||||||
|
private ApplicationContext _context;
|
||||||
|
|
||||||
|
public AuthController(IConfiguration config, ApplicationContext context)
|
||||||
|
{
|
||||||
|
_passwordHasher = new PasswordHasher<User>();
|
||||||
|
_config = config;
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("register")]
|
||||||
|
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
|
||||||
|
{
|
||||||
|
var existUser = await _context.Users.SingleOrDefaultAsync(u => u.Email == request.Email);
|
||||||
|
|
||||||
|
if (existUser != null) {
|
||||||
|
return BadRequest("Пользователь с такой эл. почтой уже существует");
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Name = request.Name,
|
||||||
|
Email = request.Email,
|
||||||
|
Password = _passwordHasher.HashPassword(null, request.Password)
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Users.Add(user);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok("Пользователь успешно зарегистрирован");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<IActionResult> Login([FromBody] LoginRequest request)
|
||||||
|
{
|
||||||
|
var user = await _context.Users.SingleOrDefaultAsync(u => u.Email == request.Email);
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return Unauthorized("Пользователя с такой эл. почтой не существует");
|
||||||
|
}
|
||||||
|
|
||||||
|
var verificationResult = _passwordHasher.VerifyHashedPassword(null, user.Password, request.Password);
|
||||||
|
|
||||||
|
if (verificationResult == PasswordVerificationResult.Failed) {
|
||||||
|
return Unauthorized("Неверный пароль");
|
||||||
|
}
|
||||||
|
|
||||||
|
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
|
||||||
|
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.Name, user.Email),
|
||||||
|
};
|
||||||
|
|
||||||
|
var Sectoken = new JwtSecurityToken(_config["Jwt:Issuer"],
|
||||||
|
_config["Jwt:Issuer"],
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.Now.AddMinutes(120),
|
||||||
|
signingCredentials: credentials);
|
||||||
|
|
||||||
|
var token = new JwtSecurityTokenHandler().WriteToken(Sectoken);
|
||||||
|
|
||||||
|
return Ok(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("user")]
|
||||||
|
public async Task<IActionResult> GetAuthUser()
|
||||||
|
{
|
||||||
|
var userEmail = User.Identity.Name;
|
||||||
|
|
||||||
|
var user = await _context.Users.SingleOrDefaultAsync(u => u.Email == userEmail);
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return NotFound("Пользователь не найден");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
user.Id,
|
||||||
|
user.Name,
|
||||||
|
user.Email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
128
Cloud/Controllers/FarmController.cs
Normal file
128
Cloud/Controllers/FarmController.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
using Cloud.Models;
|
||||||
|
using Cloud.Requests;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Cloud.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/user")]
|
||||||
|
public class FarmController : ControllerBase
|
||||||
|
{
|
||||||
|
private IConfiguration _config;
|
||||||
|
private ApplicationContext _context;
|
||||||
|
|
||||||
|
public FarmController(IConfiguration config, ApplicationContext context)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{userId}/farm")]
|
||||||
|
public async Task<ActionResult<List<Farm>>> Index(int userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<Farm> farms = await
|
||||||
|
_context.Farms.Where(x => x.UserId == userId).AsNoTracking().ToListAsync();
|
||||||
|
if (!farms.Any())
|
||||||
|
return NotFound("Farms is not found");
|
||||||
|
|
||||||
|
return Ok(farms);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{userId}/farm/{farmId}")]
|
||||||
|
public async Task<ActionResult<Farm>> Show(int userId, int farmId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Farm? farm = await
|
||||||
|
_context.Farms.FirstOrDefaultAsync(x => x.UserId == userId && x.Id == farmId);
|
||||||
|
|
||||||
|
if (farm == null)
|
||||||
|
return NotFound("Farm is not found");
|
||||||
|
|
||||||
|
return Ok(farm);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{userId}/farm")]
|
||||||
|
public async Task<ActionResult<Farm>> Create([FromBody] FarmRequest farmRequest, int userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var farm = new Farm
|
||||||
|
{
|
||||||
|
Name = farmRequest.Name,
|
||||||
|
UserId = userId,
|
||||||
|
RaspberryIP = farmRequest.RaspberryIP,
|
||||||
|
};
|
||||||
|
|
||||||
|
Farm? farmCreated = _context.Farms.Add(farm).Entity;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(farmCreated);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{userId}/farm/{farmId}")]
|
||||||
|
public async Task<ActionResult<Farm>> Update([FromBody] FarmRequest farmRequest, int userId, int farmId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Farm? farm = await _context.Farms.FirstOrDefaultAsync(x => x.Id == farmId && x.UserId == userId);
|
||||||
|
|
||||||
|
if (farm == null)
|
||||||
|
return NotFound("Farm is not found");
|
||||||
|
|
||||||
|
farm.Name = farmRequest.Name;
|
||||||
|
farm.RaspberryIP = farmRequest.RaspberryIP;
|
||||||
|
|
||||||
|
_context.Farms.Update(farm);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(farm);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{userId}/farm/{farmId}")]
|
||||||
|
public async Task<ActionResult> Delete(int userId, int farmId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Farm? farm = await _context.Farms.FirstOrDefaultAsync(x => x.Id == farmId && x.UserId == userId);
|
||||||
|
|
||||||
|
if (farm == null)
|
||||||
|
return NotFound("Farm is not found");
|
||||||
|
|
||||||
|
_context.Farms.Remove(farm);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok("Farm deleted successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
160
Cloud/Controllers/GreengouseController.cs
Normal file
160
Cloud/Controllers/GreengouseController.cs
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
using Cloud.Models;
|
||||||
|
using Cloud.Requests;
|
||||||
|
using Cloud.Services.Broker;
|
||||||
|
using Cloud.Services.Broker.Support;
|
||||||
|
using Cloud.Services.Domain;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Cloud.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/farm/{farmId}/greenhouse")]
|
||||||
|
public class GreenhouseController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IGreenhouseService _greenhouseService;
|
||||||
|
public GreenhouseController(IGreenhouseService greenhouseService)
|
||||||
|
{
|
||||||
|
_greenhouseService = greenhouseService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает текущую информацию о всех теплицах пользователя
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="farmId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<List<GreenhouseInfo>>> GetAll(int farmId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var greenhouses = _greenhouseService.GetAll(farmId);
|
||||||
|
if (greenhouses == null) return NotFound("Greenhouses is not found");
|
||||||
|
return Ok(greenhouses);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает текущую информацию о конкретной теплице
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="farmId"></param>
|
||||||
|
/// <param name="greenhouseId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("{greenhouseId}")]
|
||||||
|
public async Task<ActionResult<GreenhouseInfo>> Get(int farmId, int greenhouseId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var greenhouses = _greenhouseService.GetGreenhouseInfo(greenhouseId, farmId);
|
||||||
|
return Ok(greenhouses);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает сохраненные данные для автоматизации теплицы
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="farmId"></param>
|
||||||
|
/// <param name="greenhouseId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("{greenhouseId}/settings")]
|
||||||
|
public async Task<ActionResult<Greenhouse>> GetGreenhouse(int farmId, int greenhouseId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var greenhouse = await _greenhouseService.GetGreenhouse(greenhouseId);
|
||||||
|
if (greenhouse == null) return NotFound("Greenhouses is not found");
|
||||||
|
return Ok(greenhouse);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сохраняет в базе данных API данные для автоматизации теплицы
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="farmId"></param>
|
||||||
|
/// <param name="greenhouse"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<Greenhouse>> SaveToDatabase(int farmId, GreenhouseRequest greenhouse)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var greenhouseEntity = new Greenhouse()
|
||||||
|
{
|
||||||
|
RecomendedTemperature = greenhouse.RecomendedTemperature,
|
||||||
|
WateringMode = greenhouse.WateringMode,
|
||||||
|
HeatingMode = greenhouse.HeatingMode
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _greenhouseService.Create(greenhouseEntity);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Обновляет в базе данных API данные для автоматизации теплицы
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="farmId">ID фермы</param>
|
||||||
|
/// <param name="greenhouseId">ID теплицы</param>
|
||||||
|
/// <param name="greenhouse">Данные для обновления</param>
|
||||||
|
/// <returns>Обновленный объект Greenhouse</returns>
|
||||||
|
[HttpPut("{greenhouseId}/settings")]
|
||||||
|
public async Task<ActionResult<Greenhouse>> Update(int farmId, int greenhouseId, GreenhouseRequest greenhouse)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var greenhouseEntity = new Greenhouse()
|
||||||
|
{
|
||||||
|
Id = greenhouseId,
|
||||||
|
FarmId = farmId,
|
||||||
|
WateringMode = greenhouse.WateringMode,
|
||||||
|
HeatingMode = greenhouse.HeatingMode,
|
||||||
|
RecomendedTemperature = greenhouse.RecomendedTemperature
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _greenhouseService.Update(greenhouseEntity);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет из базы данных API запись настроек автоматизации теплицы
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="farmId"></param>
|
||||||
|
/// <param name="greenhouseId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpDelete("{greenhouseId}")]
|
||||||
|
public async Task<ActionResult> Delete(int farmId, int greenhouseId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = await _greenhouseService.Delete(greenhouseId);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
Cloud/Controllers/ValveController.cs
Normal file
43
Cloud/Controllers/ValveController.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using Cloud.Requests;
|
||||||
|
using Cloud.Services;
|
||||||
|
using Cloud.Services.Broker;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Cloud.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
[Route("api")]
|
||||||
|
public class ValveController : ControllerBase
|
||||||
|
{
|
||||||
|
//Контроллер вентиля
|
||||||
|
|
||||||
|
private readonly IBrokerService _kafkaService;
|
||||||
|
|
||||||
|
public ValveController(IBrokerService kafkaService)
|
||||||
|
{
|
||||||
|
_kafkaService = kafkaService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("farm/{farmId}/greenhouse/{ghId}/watering")]
|
||||||
|
public async Task<IActionResult> interactValve([FromBody] ValveRequest request, int farmId, int ghId)
|
||||||
|
{
|
||||||
|
var kafkaRequest = new
|
||||||
|
{
|
||||||
|
FarmId = farmId,
|
||||||
|
GreenHouseId = ghId,
|
||||||
|
SomeAction = request.Action,
|
||||||
|
};
|
||||||
|
|
||||||
|
var message = JsonSerializer.Serialize(kafkaRequest);
|
||||||
|
return Ok(kafkaRequest);
|
||||||
|
|
||||||
|
/*await _kafkaService.ProduceAsync("ValvesHeatersRequest", message);
|
||||||
|
|
||||||
|
return Ok($"Valve status is {request.Action}");*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
Cloud/Dockerfile
Normal file
30
Cloud/Dockerfile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 5124
|
||||||
|
|
||||||
|
ENV ASPNETCORE_URLS=http://+:5124
|
||||||
|
|
||||||
|
# Creates a non-root user with an explicit UID and adds permission to access the /app folder
|
||||||
|
# For more info, please refer to https://aka.ms/vscode-docker-dotnet-configure-containers
|
||||||
|
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
||||||
|
ARG configuration=Development
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["Cloud.csproj", "."]
|
||||||
|
RUN dotnet restore "./Cloud.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/."
|
||||||
|
RUN dotnet build "./Cloud.csproj" -c $configuration -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
ARG configuration=Development
|
||||||
|
RUN dotnet publish "./Cloud.csproj" -c $configuration -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet", "Cloud.dll"]
|
||||||
|
|
9
Cloud/Enums/ValveEnum.cs
Normal file
9
Cloud/Enums/ValveEnum.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Cloud.Enums
|
||||||
|
{
|
||||||
|
public enum ValveEnum
|
||||||
|
{
|
||||||
|
Open,
|
||||||
|
Close,
|
||||||
|
Auto
|
||||||
|
}
|
||||||
|
}
|
30
Cloud/Middlewares/DatabaseMiddleware.cs
Normal file
30
Cloud/Middlewares/DatabaseMiddleware.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Cloud.Middlewares;
|
||||||
|
|
||||||
|
public static class DatabaseMiddleware
|
||||||
|
{
|
||||||
|
public static void AddDbConnectionService(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
string connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING")
|
||||||
|
?? "Host=localhost;Port=5438;Database=main_database;Username=postgres;Password=12345";
|
||||||
|
|
||||||
|
services.AddDbContext<ApplicationContext>(options =>
|
||||||
|
options.UseNpgsql(connectionString));
|
||||||
|
|
||||||
|
}
|
||||||
|
public static void MigrateDb(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = app.ApplicationServices.CreateScope();
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<ApplicationContext>();
|
||||||
|
|
||||||
|
context.Database.Migrate();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
Cloud/Migrations/20241027220558_CreateUsersTable.Designer.cs
generated
Normal file
53
Cloud/Migrations/20241027220558_CreateUsersTable.Designer.cs
generated
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using Cloud;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Cloud.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationContext))]
|
||||||
|
[Migration("20241027220558_CreateUsersTable")]
|
||||||
|
partial class CreateUsersTable
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "6.0.14")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
Cloud/Migrations/20241027220558_CreateUsersTable.cs
Normal file
34
Cloud/Migrations/20241027220558_CreateUsersTable.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Cloud.Migrations
|
||||||
|
{
|
||||||
|
public partial class CreateUsersTable : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Email = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Password = table.Column<string>(type: "text", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
95
Cloud/Migrations/20241028192806_CreateFarmsTable.Designer.cs
generated
Normal file
95
Cloud/Migrations/20241028192806_CreateFarmsTable.Designer.cs
generated
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using Cloud;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Cloud.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationContext))]
|
||||||
|
[Migration("20241028192806_CreateFarmsTable")]
|
||||||
|
partial class CreateFarmsTable
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "6.0.14")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.Farm", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RaspberryMacAddr")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Farms");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.Farm", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Cloud.Models.User", "User")
|
||||||
|
.WithMany("Farms")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Farms");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
Cloud/Migrations/20241028192806_CreateFarmsTable.cs
Normal file
45
Cloud/Migrations/20241028192806_CreateFarmsTable.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Cloud.Migrations
|
||||||
|
{
|
||||||
|
public partial class CreateFarmsTable : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Farms",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
UserId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RaspberryMacAddr = table.Column<string>(type: "text", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Farms", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Farms_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Farms_UserId",
|
||||||
|
table: "Farms",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Farms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
95
Cloud/Migrations/20241030111034_front.Designer.cs
generated
Normal file
95
Cloud/Migrations/20241030111034_front.Designer.cs
generated
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using Cloud;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Cloud.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationContext))]
|
||||||
|
[Migration("20241030111034_front")]
|
||||||
|
partial class front
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "6.0.14")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.Farm", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RaspberryMacAddr")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Farms");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.Farm", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Cloud.Models.User", "User")
|
||||||
|
.WithMany("Farms")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Farms");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
Cloud/Migrations/20241030111034_front.cs
Normal file
19
Cloud/Migrations/20241030111034_front.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Cloud.Migrations
|
||||||
|
{
|
||||||
|
public partial class front : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
Cloud/Migrations/ApplicationContextModelSnapshot.cs
Normal file
93
Cloud/Migrations/ApplicationContextModelSnapshot.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using Cloud;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Cloud.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationContext))]
|
||||||
|
partial class ApplicationContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "6.0.14")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.Farm", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RaspberryMacAddr")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Farms");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.Farm", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Cloud.Models.User", "User")
|
||||||
|
.WithMany("Farms")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Cloud.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Farms");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
Cloud/Models/Farm.cs
Normal file
12
Cloud/Models/Farm.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace Cloud.Models
|
||||||
|
{
|
||||||
|
public class Farm
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public User? User { get; set; }
|
||||||
|
public string RaspberryIP { get; set; }
|
||||||
|
List<Greenhouse> Greenhouses { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
14
Cloud/Models/Greenhouse.cs
Normal file
14
Cloud/Models/Greenhouse.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using Cloud.Models.Support;
|
||||||
|
|
||||||
|
namespace Cloud.Models
|
||||||
|
{
|
||||||
|
public class Greenhouse
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int RecomendedTemperature { get; set; }
|
||||||
|
public WateringMode WateringMode { get; set; }
|
||||||
|
public HeatingMode HeatingMode { get; set; }
|
||||||
|
public int FarmId { get; set; }
|
||||||
|
public Farm? Farm { get; set; }
|
||||||
|
}
|
||||||
|
}
|
8
Cloud/Models/Support/HeatingMode.cs
Normal file
8
Cloud/Models/Support/HeatingMode.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Cloud.Models.Support
|
||||||
|
{
|
||||||
|
public enum HeatingMode
|
||||||
|
{
|
||||||
|
Manual,
|
||||||
|
Auto
|
||||||
|
}
|
||||||
|
}
|
8
Cloud/Models/Support/WateringMode.cs
Normal file
8
Cloud/Models/Support/WateringMode.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Cloud.Models.Support
|
||||||
|
{
|
||||||
|
public enum WateringMode
|
||||||
|
{
|
||||||
|
Manual,
|
||||||
|
Auto
|
||||||
|
}
|
||||||
|
}
|
13
Cloud/Models/User.cs
Normal file
13
Cloud/Models/User.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace Cloud.Models;
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
public List<Farm> Farms { get; set; } = new();
|
||||||
|
}
|
133
Cloud/Program.cs
Normal file
133
Cloud/Program.cs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
using Cloud;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.Text;
|
||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.AspNetCore;
|
||||||
|
using Cloud.Validation;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using Cloud.Services.Broker.Implement.Kafka;
|
||||||
|
using Cloud.Services.Broker;
|
||||||
|
using Cloud.Services;
|
||||||
|
using Cloud.Services.Domain.Implement;
|
||||||
|
using Cloud.Services.Domain;
|
||||||
|
using Cloud.Services.Cache;
|
||||||
|
using Cloud.Support;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Cloud.Middlewares;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
builder.Services.AddSingleton<IBrokerService, KafkaService>();
|
||||||
|
builder.Services.AddTransient<IGreenhouseService, GreenhouseService>();
|
||||||
|
|
||||||
|
//Redis configuration
|
||||||
|
string redisUrl = Environment.GetEnvironmentVariable("REDIS_URL") ?? "localhost:6379";
|
||||||
|
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
|
||||||
|
{
|
||||||
|
var configuration = ConfigurationOptions.Parse(redisUrl);
|
||||||
|
return ConnectionMultiplexer.Connect(configuration);
|
||||||
|
});
|
||||||
|
builder.Services.AddSingleton<IRedisCacheService, RedisCacheService>();
|
||||||
|
|
||||||
|
//Jwt configuration
|
||||||
|
var jwtIssuer = builder.Configuration.GetSection("Jwt:Issuer").Get<string>();
|
||||||
|
var jwtKey = builder.Configuration.GetSection("Jwt:Key").Get<string>();
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = jwtIssuer,
|
||||||
|
ValidAudience = jwtIssuer,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Настройка подключения к БД
|
||||||
|
builder.Services.AddDbConnectionService();
|
||||||
|
// Настройка CORS
|
||||||
|
string frontUrl = Environment.GetEnvironmentVariable("FRONT_URL") ?? "http://localhost:3000";
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowFrontendLocalhost", builder =>
|
||||||
|
{
|
||||||
|
builder.WithOrigins(frontUrl) // фронтенд
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddFluentValidationAutoValidation();
|
||||||
|
builder.Services.AddFluentValidationClientsideAdapters();
|
||||||
|
builder.Services.AddValidatorsFromAssemblyContaining<LoginValidator>();
|
||||||
|
builder.Services.AddValidatorsFromAssemblyContaining<RegisterValidator>();
|
||||||
|
builder.Services.AddValidatorsFromAssemblyContaining<FarmValidator>();
|
||||||
|
builder.Services.AddValidatorsFromAssemblyContaining<ValveValidator>();
|
||||||
|
|
||||||
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen(c =>
|
||||||
|
{
|
||||||
|
c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "Cloud API", Version = "v1" });
|
||||||
|
|
||||||
|
c.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Description = "Введите ваш Bearer токен",
|
||||||
|
Name = "Authorization",
|
||||||
|
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
|
||||||
|
Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey
|
||||||
|
});
|
||||||
|
|
||||||
|
c.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new Microsoft.OpenApi.Models.OpenApiReference
|
||||||
|
{
|
||||||
|
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new string[] {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
Console.WriteLine("Swagger enabled");
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI(c =>
|
||||||
|
{
|
||||||
|
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Cloud API V1");
|
||||||
|
c.RoutePrefix = string.Empty;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
// Включение CORS
|
||||||
|
app.UseCors("AllowFrontendLocalhost");
|
||||||
|
|
||||||
|
// Применение миграций
|
||||||
|
app.MigrateDb();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
31
Cloud/Properties/launchSettings.json
Normal file
31
Cloud/Properties/launchSettings.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:43967",
|
||||||
|
"sslPort": 44304
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"Cloud": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "https://localhost:7113;http://localhost:5124",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
Cloud/Requests/FarmRequest.cs
Normal file
8
Cloud/Requests/FarmRequest.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Cloud.Requests
|
||||||
|
{
|
||||||
|
public class FarmRequest
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string RaspberryIP { get; set; }
|
||||||
|
}
|
||||||
|
}
|
11
Cloud/Requests/GreenhouseRequest.cs
Normal file
11
Cloud/Requests/GreenhouseRequest.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using Cloud.Models.Support;
|
||||||
|
|
||||||
|
namespace Cloud.Requests
|
||||||
|
{
|
||||||
|
public class GreenhouseRequest
|
||||||
|
{
|
||||||
|
public int RecomendedTemperature { get; set; }
|
||||||
|
public WateringMode WateringMode { get; set; }
|
||||||
|
public HeatingMode HeatingMode { get; set; }
|
||||||
|
}
|
||||||
|
}
|
13
Cloud/Requests/LoginRequest.cs
Normal file
13
Cloud/Requests/LoginRequest.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Cloud.Requests;
|
||||||
|
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
15
Cloud/Requests/RegisterRequest.cs
Normal file
15
Cloud/Requests/RegisterRequest.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Cloud.Requests;
|
||||||
|
|
||||||
|
public class RegisterRequest
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
7
Cloud/Requests/ValveRequest.cs
Normal file
7
Cloud/Requests/ValveRequest.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Cloud.Requests
|
||||||
|
{
|
||||||
|
public class ValveRequest
|
||||||
|
{
|
||||||
|
public string Action { get; set; }
|
||||||
|
}
|
||||||
|
}
|
9
Cloud/Services/Broker/IBrokerConsumer.cs
Normal file
9
Cloud/Services/Broker/IBrokerConsumer.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Cloud.Services.Broker.Support;
|
||||||
|
|
||||||
|
namespace Cloud.Services.Broker
|
||||||
|
{
|
||||||
|
public interface IBrokerConsumer
|
||||||
|
{
|
||||||
|
IEnumerable<T>? WaitMessages<T>(string topic) where T : IBrokerResponse;
|
||||||
|
}
|
||||||
|
}
|
9
Cloud/Services/Broker/IBrokerProducer.cs
Normal file
9
Cloud/Services/Broker/IBrokerProducer.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Cloud.Services.Broker.Support;
|
||||||
|
|
||||||
|
namespace Cloud.Services.Broker
|
||||||
|
{
|
||||||
|
public interface IBrokerProducer
|
||||||
|
{
|
||||||
|
Task ProduceAsync(string topic, Command command);
|
||||||
|
}
|
||||||
|
}
|
9
Cloud/Services/Broker/IBrokerService.cs
Normal file
9
Cloud/Services/Broker/IBrokerService.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Cloud.Services.Broker.Support;
|
||||||
|
|
||||||
|
namespace Cloud.Services.Broker
|
||||||
|
{
|
||||||
|
public interface IBrokerService : IBrokerProducer, IBrokerConsumer
|
||||||
|
{
|
||||||
|
void ChangeBrokerIp(string ip);
|
||||||
|
}
|
||||||
|
}
|
93
Cloud/Services/Broker/Implement/Kafka/KafkaConsumer.cs
Normal file
93
Cloud/Services/Broker/Implement/Kafka/KafkaConsumer.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
|
||||||
|
using Cloud.Services.Broker.Support;
|
||||||
|
using Confluent.Kafka;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Cloud.Services.Broker.Implement.Kafka
|
||||||
|
{
|
||||||
|
public class KafkaConsumer : IBrokerConsumer
|
||||||
|
{
|
||||||
|
private IConsumer<string, string> _consumer;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
|
||||||
|
public KafkaConsumer(IConfiguration config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
Console.WriteLine($"KafkaConsumer created. IP:" + _config["KAFKA_URL"]);
|
||||||
|
ChangeBrokerIp(_config["KAFKA_URL"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<T>? WaitMessages<T>(string topic)
|
||||||
|
where T : IBrokerResponse
|
||||||
|
{
|
||||||
|
List<T> res = new();
|
||||||
|
List<PartitionMetadata> partitions;
|
||||||
|
|
||||||
|
using var adminClient = new AdminClientBuilder(new AdminClientConfig { BootstrapServers = _config["KAFKA_URL"] }).Build();
|
||||||
|
var meta = adminClient.GetMetadata(TimeSpan.FromSeconds(20));
|
||||||
|
var currentTopic = meta.Topics.SingleOrDefault(t => t.Topic == topic)
|
||||||
|
?? throw new Exception($"Topic {topic} not found");
|
||||||
|
partitions = currentTopic.Partitions;
|
||||||
|
|
||||||
|
_consumer.Subscribe(topic);
|
||||||
|
foreach (var partition in partitions)
|
||||||
|
{
|
||||||
|
var topicPartition = new TopicPartition(topic, partition.PartitionId);
|
||||||
|
_consumer.Assign(topicPartition);
|
||||||
|
|
||||||
|
T? message = _consume<T>();
|
||||||
|
if (message == null) return null;
|
||||||
|
res.Add(message);
|
||||||
|
|
||||||
|
}
|
||||||
|
_consumer.Unassign();
|
||||||
|
_consumer.Unsubscribe();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private T? _consume<T>() where T : IBrokerResponse
|
||||||
|
{
|
||||||
|
var sw = new Stopwatch();
|
||||||
|
sw.Start();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var consumeResult = _consumer.Consume(TimeSpan.FromMinutes(1));
|
||||||
|
|
||||||
|
if (consumeResult?.Message?.Value == null)
|
||||||
|
{
|
||||||
|
// Предел по времени
|
||||||
|
if (sw.Elapsed > TimeSpan.FromMinutes(1))
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string jsonObj = consumeResult.Message.Value;
|
||||||
|
return JsonSerializer.Deserialize<T>(jsonObj);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_consumer.Close();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeBrokerIp(string ip)
|
||||||
|
{
|
||||||
|
var consumerConfig = new ConsumerConfig()
|
||||||
|
{
|
||||||
|
BootstrapServers = ip,
|
||||||
|
GroupId = _config["Kafka:GroupId"],
|
||||||
|
AutoOffsetReset = AutoOffsetReset.Earliest,
|
||||||
|
};
|
||||||
|
_consumer?.Close();
|
||||||
|
_consumer = new ConsumerBuilder<string, string>(consumerConfig).Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
Cloud/Services/Broker/Implement/Kafka/KafkaProducer.cs
Normal file
38
Cloud/Services/Broker/Implement/Kafka/KafkaProducer.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Cloud.Services.Broker.Support;
|
||||||
|
using Confluent.Kafka;
|
||||||
|
|
||||||
|
namespace Cloud.Services.Broker.Implement.Kafka
|
||||||
|
{
|
||||||
|
public class KafkaProducer : IBrokerProducer
|
||||||
|
{
|
||||||
|
private IProducer<string, string> _producer;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
|
||||||
|
public KafkaProducer(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_config = configuration;
|
||||||
|
Console.WriteLine($"KafkaProducer created. IP:" + _config["KAFKA_URL"]);
|
||||||
|
ChangeBrokerIp(_config["KAFKA_URL"]);
|
||||||
|
}
|
||||||
|
public async Task ProduceAsync(string topic, Command command)
|
||||||
|
{
|
||||||
|
var commandSerialized = JsonSerializer.Serialize(command);
|
||||||
|
var message = new Message<string, string> { Key = Guid.NewGuid().ToString(), Value = commandSerialized };
|
||||||
|
|
||||||
|
//Produce the Message
|
||||||
|
await _producer.ProduceAsync(topic, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeBrokerIp(string ip)
|
||||||
|
{
|
||||||
|
var producerConfig = new ProducerConfig
|
||||||
|
{
|
||||||
|
BootstrapServers = ip
|
||||||
|
};
|
||||||
|
|
||||||
|
//Build the Producer
|
||||||
|
_producer = new ProducerBuilder<string, string>(producerConfig).Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
Cloud/Services/Broker/Implement/Kafka/KafkaService.cs
Normal file
30
Cloud/Services/Broker/Implement/Kafka/KafkaService.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using Cloud.Services.Broker.Support;
|
||||||
|
|
||||||
|
namespace Cloud.Services.Broker.Implement.Kafka
|
||||||
|
{
|
||||||
|
public class KafkaService : IBrokerService
|
||||||
|
{
|
||||||
|
private readonly KafkaProducer _producer;
|
||||||
|
private readonly KafkaConsumer _consumer;
|
||||||
|
|
||||||
|
public KafkaService(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_producer = new KafkaProducer(configuration);
|
||||||
|
_consumer = new KafkaConsumer(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<T>? WaitMessages<T>(string topic)
|
||||||
|
where T : IBrokerResponse
|
||||||
|
=> _consumer.WaitMessages<T>(topic);
|
||||||
|
|
||||||
|
|
||||||
|
public async Task ProduceAsync(string topic, Command command)
|
||||||
|
=> await _producer.ProduceAsync("commands", command);
|
||||||
|
|
||||||
|
public void ChangeBrokerIp(string ip)
|
||||||
|
{
|
||||||
|
_consumer.ChangeBrokerIp(ip);
|
||||||
|
_producer.ChangeBrokerIp(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
Cloud/Services/Broker/Support/Command.cs
Normal file
10
Cloud/Services/Broker/Support/Command.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Cloud.Services.Broker.Support
|
||||||
|
{
|
||||||
|
public class Command
|
||||||
|
{
|
||||||
|
public Guid GreenhouseId { get; set; }
|
||||||
|
public string CommandName { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
9
Cloud/Services/Broker/Support/CommandResult.cs
Normal file
9
Cloud/Services/Broker/Support/CommandResult.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Cloud.Services.Broker.Support
|
||||||
|
{
|
||||||
|
public class CommandResult : IBrokerResponse
|
||||||
|
{
|
||||||
|
public int CommandId { get; set; }
|
||||||
|
public int GreenhouseId { get; set; }
|
||||||
|
public string ResultMessage { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
12
Cloud/Services/Broker/Support/GreenhouseInfo.cs
Normal file
12
Cloud/Services/Broker/Support/GreenhouseInfo.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace Cloud.Services.Broker.Support
|
||||||
|
{
|
||||||
|
public class GreenhouseInfo : IBrokerResponse
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int PercentWater { get; set; }
|
||||||
|
public int SoilTemperature { get; set; }
|
||||||
|
public bool PumpStatus { get; set; }
|
||||||
|
public bool HeatingStatus { get; set; }
|
||||||
|
public bool AutoWateringStatus { get; set; }
|
||||||
|
}
|
||||||
|
}
|
6
Cloud/Services/Broker/Support/IBrokerResponse.cs
Normal file
6
Cloud/Services/Broker/Support/IBrokerResponse.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace Cloud.Services.Broker.Support
|
||||||
|
{
|
||||||
|
public interface IBrokerResponse
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
13
Cloud/Services/Cache/IRedisCacheService.cs
Normal file
13
Cloud/Services/Cache/IRedisCacheService.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Cloud.Services.Cache
|
||||||
|
{
|
||||||
|
public interface IRedisCacheService
|
||||||
|
{
|
||||||
|
Task SetCacheAsync<T>(string key, T value, TimeSpan? expiry = null);
|
||||||
|
Task<T?> GetCacheAsync<T>(string key);
|
||||||
|
}
|
||||||
|
}
|
37
Cloud/Services/Cache/RedisCacheService.cs
Normal file
37
Cloud/Services/Cache/RedisCacheService.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Cloud.Services.Cache
|
||||||
|
{
|
||||||
|
public class RedisCacheService : IRedisCacheService
|
||||||
|
{
|
||||||
|
private readonly IConnectionMultiplexer _connectionMultiplexer;
|
||||||
|
|
||||||
|
public RedisCacheService(IConnectionMultiplexer connectionMultiplexer)
|
||||||
|
{
|
||||||
|
_connectionMultiplexer = connectionMultiplexer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetCacheAsync<T>(string key, T value, TimeSpan? expiry = null)
|
||||||
|
{
|
||||||
|
var database = _connectionMultiplexer.GetDatabase();
|
||||||
|
var serializedValue = JsonSerializer.Serialize(value);
|
||||||
|
await database.StringSetAsync(key, serializedValue, expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T?> GetCacheAsync<T>(string key)
|
||||||
|
{
|
||||||
|
var database = _connectionMultiplexer.GetDatabase();
|
||||||
|
var value = await database.StringGetAsync(key);
|
||||||
|
|
||||||
|
if (value.IsNullOrEmpty)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<T>(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
Cloud/Services/Domain/IGreenhouseService.cs
Normal file
45
Cloud/Services/Domain/IGreenhouseService.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using Cloud.Models;
|
||||||
|
using Cloud.Services.Broker.Support;
|
||||||
|
|
||||||
|
namespace Cloud.Services.Domain;
|
||||||
|
|
||||||
|
public interface IGreenhouseService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает текущую информацию о конкретной теплице из брокера
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID теплицы</param>
|
||||||
|
/// <param name="farmId">ID фермы, то есть брокера</param>
|
||||||
|
/// <returns>Текущие данные о теплице от менеджера теплицы</returns>
|
||||||
|
public Task<GreenhouseInfo?> GetGreenhouseInfo(int id, int farmId);
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает сохраненные данные для автоматизации теплицы из базы данных
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID теплицы</param>
|
||||||
|
/// <returns>Данные для автоматизации теплицы</returns>
|
||||||
|
public Task<Greenhouse?> GetGreenhouse(int id);
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает список данных о всех теплицах пользователя из брокера
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="farmId">ID фермы</param>
|
||||||
|
/// <returns>Список текущих данных о теплицах</returns>
|
||||||
|
public Task<IEnumerable<GreenhouseInfo>?> GetAll(int farmId);
|
||||||
|
/// <summary>
|
||||||
|
/// Сохраняет данные об автоматизации теплицы в базу данных
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="greenhouse">Данные автоматизации теплицы</param>
|
||||||
|
/// <returns>Созданную сущность из базы данных</returns>
|
||||||
|
public Task<Greenhouse> Create(Greenhouse greenhouse);
|
||||||
|
/// <summary>
|
||||||
|
/// Обновляет данные автоматизации теплицы в базе данных
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="greenhouse">Новая информация об автоматизации теплицы</param>
|
||||||
|
/// <returns>Обновленную сущность из базы данных</returns>
|
||||||
|
public Task<Greenhouse> Update(Greenhouse greenhouse);
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет данные об автоматизации теплицы из базы данных
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID данных автоматизации теплицы</param>
|
||||||
|
/// <returns>Возвращает удаленную сущность</returns>
|
||||||
|
public Task<Greenhouse> Delete(int id);
|
||||||
|
}
|
67
Cloud/Services/Domain/Implement/GreenhouseService.cs
Normal file
67
Cloud/Services/Domain/Implement/GreenhouseService.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using Cloud.Models;
|
||||||
|
using Cloud.Services.Broker;
|
||||||
|
using Cloud.Services.Broker.Support;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Cloud.Services.Domain.Implement;
|
||||||
|
|
||||||
|
public class GreenhouseService : IGreenhouseService
|
||||||
|
{
|
||||||
|
private readonly IBrokerService _brokerService;
|
||||||
|
private readonly ApplicationContext _context;
|
||||||
|
|
||||||
|
public GreenhouseService(IBrokerService brokerService, ApplicationContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_brokerService = brokerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Greenhouse> Create(Greenhouse greenhouse)
|
||||||
|
{
|
||||||
|
var res = await _context.Greenhouses.AddAsync(greenhouse);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return res.Entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Greenhouse> Delete(int id)
|
||||||
|
{
|
||||||
|
var greenhouse = await _context.Greenhouses.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
_context.Greenhouses.Remove(greenhouse);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return greenhouse;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<Greenhouse?> GetGreenhouse(int id)
|
||||||
|
{
|
||||||
|
return await _context.Greenhouses.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Greenhouse> Update(Greenhouse greenhouse)
|
||||||
|
{
|
||||||
|
var res = _context.Greenhouses.Update(greenhouse);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return res.Entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<GreenhouseInfo>?> GetAll(int farmId)
|
||||||
|
{
|
||||||
|
// await _changeBrokerIp(farmId);
|
||||||
|
return _brokerService.WaitMessages<GreenhouseInfo>("data");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GreenhouseInfo?> GetGreenhouseInfo(int id, int farmId)
|
||||||
|
{
|
||||||
|
// await _changeBrokerIp(farmId);
|
||||||
|
var infos = _brokerService.WaitMessages<GreenhouseInfo>("data");
|
||||||
|
return infos?.FirstOrDefault(x => x.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task _changeBrokerIp(int farmId)
|
||||||
|
{
|
||||||
|
var farm = await _context.Farms.FirstOrDefaultAsync(x => x.Id == farmId);
|
||||||
|
_brokerService.ChangeBrokerIp(farm.RaspberryIP);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
26
Cloud/Support/NetworkSupport.cs
Normal file
26
Cloud/Support/NetworkSupport.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
namespace Cloud.Support;
|
||||||
|
|
||||||
|
public static class NetworkSupport
|
||||||
|
{
|
||||||
|
public static async Task CheckConnectionAsync(string address)
|
||||||
|
{
|
||||||
|
using var client = new HttpClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await client.GetAsync(address);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Соединение успешно проверено. Статус-код: {response.StatusCode}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Соединение не удалось проверить. Статус-код: {response.StatusCode}. URL: {address}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Ошибка при проверке соединения: {ex.Message}. URL: {address}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
Cloud/Validation/FarmValidator.cs
Normal file
18
Cloud/Validation/FarmValidator.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using Cloud.Requests;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace Cloud.Validation
|
||||||
|
{
|
||||||
|
public class FarmValidator : AbstractValidator<FarmRequest>
|
||||||
|
{
|
||||||
|
public FarmValidator()
|
||||||
|
{
|
||||||
|
RuleFor(request => request.RaspberryIP)
|
||||||
|
.NotEmpty().WithMessage("IP address can't be empty")
|
||||||
|
.Matches(@"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$").WithMessage("IP address is not valid");
|
||||||
|
|
||||||
|
RuleFor(request => request.Name)
|
||||||
|
.NotEmpty().WithMessage("Name can't be empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
Cloud/Validation/LoginValidator.cs
Normal file
18
Cloud/Validation/LoginValidator.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using Cloud.Requests;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace Cloud.Validation;
|
||||||
|
|
||||||
|
public class LoginValidator : AbstractValidator<LoginRequest>
|
||||||
|
{
|
||||||
|
public LoginValidator()
|
||||||
|
{
|
||||||
|
RuleFor(request => request.Email)
|
||||||
|
.NotEmpty().WithMessage("Email обязателен для заполнения")
|
||||||
|
.EmailAddress().WithMessage("Некорректный формат Email");
|
||||||
|
|
||||||
|
RuleFor(request => request.Password)
|
||||||
|
.NotEmpty().WithMessage("Пароль обязателен для заполнения")
|
||||||
|
.MinimumLength(8).WithMessage("Пароль должен быть не менее 8 символов");
|
||||||
|
}
|
||||||
|
}
|
22
Cloud/Validation/RegisterValidator.cs
Normal file
22
Cloud/Validation/RegisterValidator.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using Cloud.Requests;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace Cloud.Validation;
|
||||||
|
|
||||||
|
public class RegisterValidator : AbstractValidator<RegisterRequest>
|
||||||
|
{
|
||||||
|
public RegisterValidator()
|
||||||
|
{
|
||||||
|
RuleFor(user => user.Name)
|
||||||
|
.NotEmpty().WithMessage("Имя обязательно для заполнения")
|
||||||
|
.MaximumLength(50).WithMessage("Имя должно быть не более 50 символов");
|
||||||
|
|
||||||
|
RuleFor(user => user.Email)
|
||||||
|
.NotEmpty().WithMessage("Email обязателен для заполнения")
|
||||||
|
.EmailAddress().WithMessage("Некорректный формат Email");
|
||||||
|
|
||||||
|
RuleFor(user => user.Password)
|
||||||
|
.NotEmpty().WithMessage("Пароль обязателен для заполнения")
|
||||||
|
.MinimumLength(8).WithMessage("Пароль должен быть не менее 8 символов");
|
||||||
|
}
|
||||||
|
}
|
16
Cloud/Validation/ValveValidator.cs
Normal file
16
Cloud/Validation/ValveValidator.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using Cloud.Enums;
|
||||||
|
using Cloud.Requests;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace Cloud.Validation
|
||||||
|
{
|
||||||
|
public class ValveValidator : AbstractValidator<ValveRequest>
|
||||||
|
{
|
||||||
|
public ValveValidator() {
|
||||||
|
|
||||||
|
RuleFor(request => request.Action)
|
||||||
|
.NotEmpty().WithMessage("Action can't be empty").
|
||||||
|
IsEnumName(typeof (ValveEnum)).WithMessage("Action is not correct");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
Cloud/appsettings.Development.json
Normal file
8
Cloud/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
Cloud/appsettings.json
Normal file
17
Cloud/appsettings.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Kafka": {
|
||||||
|
"BootstrapServers": "localhost:9092",
|
||||||
|
"GroupId": "ValvesHeaters"
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Jwt": {
|
||||||
|
"Key": "m7TyhE20s0dVtUDAr9EnFdPZnAG8maxgBTaiW5j6kO6RQhWDAGxYmXyu0suDnE0o",
|
||||||
|
"Issuer": "localhost"
|
||||||
|
}
|
||||||
|
}
|
4
Dockerfile
Normal file
4
Dockerfile
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
FROM docker/whalesay:latest
|
||||||
|
LABEL Name=cucumber Version=0.0.1
|
||||||
|
RUN apt-get -y update && apt-get install -y fortunes
|
||||||
|
CMD ["sh", "-c", "/usr/games/fortune -a | cowsay"]
|
48
GreenhouseController/ManageController.py
Normal file
48
GreenhouseController/ManageController.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from json import dumps
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from json import dumps
|
||||||
|
|
||||||
|
class ManageController:
|
||||||
|
def __init__(self, producer, topic='commands'):
|
||||||
|
self.valve_state = "closed"
|
||||||
|
self.heater_state = "off"
|
||||||
|
self.producer = producer
|
||||||
|
self.topic = topic
|
||||||
|
|
||||||
|
def toggle_device(self, device, request_id, greenhouse_id):
|
||||||
|
|
||||||
|
|
||||||
|
if device == 'valve':
|
||||||
|
|
||||||
|
if self.valve_state == 'closed':
|
||||||
|
self.valve_state = 'open'
|
||||||
|
print("Valve opened")
|
||||||
|
else:
|
||||||
|
self.valve_state = 'closed'
|
||||||
|
print("Valve closed")
|
||||||
|
|
||||||
|
elif device == 'heater':
|
||||||
|
|
||||||
|
if self.heater_state == 'off':
|
||||||
|
self.heater_state = 'on'
|
||||||
|
print("Heater turned on")
|
||||||
|
else:
|
||||||
|
self.heater_state = 'off'
|
||||||
|
print("Heater turned off")
|
||||||
|
|
||||||
|
|
||||||
|
self.send_status(request_id, greenhouse_id)
|
||||||
|
|
||||||
|
def send_status(self, request_id, greenhouse_id):
|
||||||
|
|
||||||
|
status = {
|
||||||
|
'request_id': request_id,
|
||||||
|
'greenhouse_id': greenhouse_id,
|
||||||
|
'valve_state': self.valve_state,
|
||||||
|
'heater_state': self.heater_state
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"Sent device status: {status}")
|
||||||
|
return status
|
11
GreenhouseDetector/Dockerfile
Normal file
11
GreenhouseDetector/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY GreenhouseDetector/detector.py .
|
||||||
|
|
||||||
|
CMD ["python", "detector.py"]
|
43
GreenhouseDetector/detector.py
Normal file
43
GreenhouseDetector/detector.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import random as rnd
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
class Detector:
|
||||||
|
def __init__(self, id, moistureThresholdUpper, moistureThresholdLower, tempThresholdUpper, tempThresholdLower):
|
||||||
|
self.MANAGER_URL = os.environ.get('MANAGER_URL')
|
||||||
|
print("MANAGER_URL=", self.MANAGER_URL)
|
||||||
|
self.id = id
|
||||||
|
self.moistureThresholdUpper = moistureThresholdUpper
|
||||||
|
self.moistureThresholdLower = moistureThresholdLower
|
||||||
|
self.tempThresholdUpper = tempThresholdUpper
|
||||||
|
self.tempThresholdLower = tempThresholdLower
|
||||||
|
self.moisture = 0
|
||||||
|
self.temp = 0
|
||||||
|
|
||||||
|
def cycle(self):
|
||||||
|
self.moisture += rnd.random() / 100
|
||||||
|
self.temp += (rnd.random() - 0.5) / 100
|
||||||
|
|
||||||
|
def sendData(self):
|
||||||
|
data = {"moisture": self.moisture,
|
||||||
|
"temp": self.temp}
|
||||||
|
requests.post(f"{self.MANAGER_URL}/webhook?id={self.id}", json=data)
|
||||||
|
|
||||||
|
detector1 = Detector(1, 0.6, 0.2, 40, 20)
|
||||||
|
|
||||||
|
detectors = [detector1]
|
||||||
|
|
||||||
|
if __name__ =="__main__":
|
||||||
|
while True:
|
||||||
|
for detector in detectors:
|
||||||
|
detector.cycle()
|
||||||
|
detector.sendData()
|
||||||
|
time.sleep(1)
|
||||||
|
|
11
GreenhouseManager/Dockerfile
Normal file
11
GreenhouseManager/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY GreenhouseManager/manager.py .
|
||||||
|
|
||||||
|
CMD ["python", "manager.py"]
|
140
GreenhouseManager/manager.py
Normal file
140
GreenhouseManager/manager.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import os
|
||||||
|
from kafka import KafkaProducer, KafkaConsumer
|
||||||
|
import kafka
|
||||||
|
import socket
|
||||||
|
from json import dumps, loads
|
||||||
|
from flask import Flask, request
|
||||||
|
import time
|
||||||
|
from enum import Enum
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
def start_manager():
|
||||||
|
return
|
||||||
|
|
||||||
|
class Manager:
|
||||||
|
def __init__(self, _id: int, moisture: float = 0, temp: float = 20, isAutoOn: bool = False, valve_state: str = "closed",
|
||||||
|
heater_state: str = "off"):
|
||||||
|
KAFKA_URL = os.environ.get('KAFKA_URL')
|
||||||
|
print("KAFKA_URL=", KAFKA_URL)
|
||||||
|
self._id = _id
|
||||||
|
self.moisture = moisture
|
||||||
|
self.temp = temp
|
||||||
|
self.isAutoOn = isAutoOn
|
||||||
|
self.valve_state = valve_state
|
||||||
|
self.heater_state = heater_state
|
||||||
|
|
||||||
|
self.dataPublisher = KafkaProducer(
|
||||||
|
bootstrap_servers=[KAFKA_URL],
|
||||||
|
client_id=f'manager{self._id}_producer',
|
||||||
|
value_serializer=lambda v: dumps(v).encode('utf-8')
|
||||||
|
)
|
||||||
|
|
||||||
|
self.controllerConsumer = KafkaConsumer(
|
||||||
|
'commands',
|
||||||
|
bootstrap_servers=[KAFKA_URL],
|
||||||
|
auto_offset_reset='earliest',
|
||||||
|
enable_auto_commit=True,
|
||||||
|
consumer_timeout_ms=2000,
|
||||||
|
group_id=f'manager{self._id}',
|
||||||
|
value_deserializer=lambda x: loads(x.decode('utf-8'))
|
||||||
|
)
|
||||||
|
self.controllerConsumerResponse = KafkaProducer(
|
||||||
|
bootstrap_servers=[KAFKA_URL],
|
||||||
|
client_id=f'manager{self._id}_producer',
|
||||||
|
value_serializer=lambda v: dumps(v).encode('utf-8')
|
||||||
|
)
|
||||||
|
|
||||||
|
def sendData(self):
|
||||||
|
print("sending data...")
|
||||||
|
message = {
|
||||||
|
'id': self._id,
|
||||||
|
'moisture': self.moisture,
|
||||||
|
'temp': self.temp,
|
||||||
|
'valveStatus': str(self.valve_state),
|
||||||
|
'heaterStatus': str(self.heater_state),
|
||||||
|
'isAutoOn': self.isAutoOn
|
||||||
|
}
|
||||||
|
|
||||||
|
print(message)
|
||||||
|
self.dataPublisher.send('data', message)
|
||||||
|
self.dataPublisher.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_device(self, device, request_id, greenhouse_id):
|
||||||
|
|
||||||
|
if device == 'valve':
|
||||||
|
|
||||||
|
if self.valve_state == 'closed':
|
||||||
|
self.valve_state = 'open'
|
||||||
|
print("Valve opened")
|
||||||
|
else:
|
||||||
|
self.valve_state = 'closed'
|
||||||
|
print("Valve closed")
|
||||||
|
|
||||||
|
elif device == 'heater':
|
||||||
|
|
||||||
|
if self.heater_state == 'off':
|
||||||
|
self.heater_state = 'on'
|
||||||
|
print("Heater turned on")
|
||||||
|
else:
|
||||||
|
self.heater_state = 'off'
|
||||||
|
print("Heater turned off")
|
||||||
|
|
||||||
|
self.send_status(request_id, greenhouse_id)
|
||||||
|
|
||||||
|
def send_status(self, request_id, greenhouse_id):
|
||||||
|
|
||||||
|
status = {
|
||||||
|
'request_id': request_id,
|
||||||
|
'greenhouse_id': greenhouse_id,
|
||||||
|
'valve_state': self.valve_state,
|
||||||
|
'heater_state': self.heater_state
|
||||||
|
}
|
||||||
|
self.sendDataCommand(status)
|
||||||
|
print("Updating info...\n")
|
||||||
|
|
||||||
|
def sendDataCommand(self, message):
|
||||||
|
print("sending data...")
|
||||||
|
|
||||||
|
self.dataPublisher.send('response', message)
|
||||||
|
|
||||||
|
def getCommand(self):
|
||||||
|
messages = self.controllerConsumer.poll(timeout_ms=1000)
|
||||||
|
|
||||||
|
# Проверяем, есть ли сообщения
|
||||||
|
|
||||||
|
for tp, msgs in messages.items():
|
||||||
|
for message in msgs:
|
||||||
|
print(f"Manager {self._id} received message: ")
|
||||||
|
print(message.value)
|
||||||
|
self.request_id = message.value['request_id']
|
||||||
|
self.greenhouse_id = message.value['greenhouse_id']
|
||||||
|
self.command = message.value['command']
|
||||||
|
self.toggle_device(self.command, self.request_id, self.greenhouse_id)
|
||||||
|
|
||||||
|
@app.route(f'/webhook', methods=['POST'])
|
||||||
|
def webhook():
|
||||||
|
print("received webhook", request.args.get('id'))
|
||||||
|
for manager in managers:
|
||||||
|
print()
|
||||||
|
if int(request.args.get('id')) == manager._id and request.method == 'POST':
|
||||||
|
print("Data received from Webhook is", request.json)
|
||||||
|
|
||||||
|
body = request.json
|
||||||
|
for key, value in body.items():
|
||||||
|
setattr(manager, key, value)
|
||||||
|
|
||||||
|
manager.sendData()
|
||||||
|
|
||||||
|
return f"Webhook received for manager {manager._id}"
|
||||||
|
return "Webhook ignored"
|
||||||
|
|
||||||
|
t1 = threading.Thread(target=start_manager)
|
||||||
|
manager1 = Manager(_id=1)
|
||||||
|
managers = [manager1]
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
threading.Thread(target=lambda: app.run(host="0.0.0.0", port=20002, debug=True, use_reloader=False)).start()
|
24
cucumber-frontend/.gitignore
vendored
Normal file
24
cucumber-frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
46
cucumber-frontend/README.md
Normal file
46
cucumber-frontend/README.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.\
|
||||||
|
You will also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||||
|
|
||||||
|
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||||
|
|
||||||
|
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
31730
cucumber-frontend/package-lock.json
generated
Normal file
31730
cucumber-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
cucumber-frontend/package.json
Normal file
55
cucumber-frontend/package.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "cucumber-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.5.1",
|
||||||
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"@types/jest": "^27.5.2",
|
||||||
|
"@types/node": "^16.18.115",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"antd": "^5.21.6",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"chart.js": "^4.4.7",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-chartjs-2": "^5.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-dropzone": "^14.3.5",
|
||||||
|
"react-router-dom": "^6.27.0",
|
||||||
|
"react-scripts": "^5.0.1",
|
||||||
|
"swagger-typescript-api": "^13.0.23",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
|
"gen-api": "swagger-typescript-api -r -o ./src/core/api/ --modular -p "
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"tailwindcss": "^3.4.14"
|
||||||
|
}
|
||||||
|
}
|
BIN
cucumber-frontend/public/favicon.ico
Normal file
BIN
cucumber-frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
43
cucumber-frontend/public/index.html
Normal file
43
cucumber-frontend/public/index.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
cucumber-frontend/public/logo192.png
Normal file
BIN
cucumber-frontend/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
cucumber-frontend/public/logo512.png
Normal file
BIN
cucumber-frontend/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
cucumber-frontend/public/manifest.json
Normal file
25
cucumber-frontend/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
3
cucumber-frontend/public/robots.txt
Normal file
3
cucumber-frontend/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
62
cucumber-frontend/src/API/api.ts
Normal file
62
cucumber-frontend/src/API/api.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import axios, { AxiosError, AxiosRequestHeaders } from 'axios';
|
||||||
|
import IUser from '../models/IUser';
|
||||||
|
import IFarm from '../models/IFarm';
|
||||||
|
import LoginRequest from '../Requests/LoginRequest';
|
||||||
|
import IRegisterRequest from '../Requests/RegisterRequest';
|
||||||
|
|
||||||
|
const API_BASE_URL = 'http://localhost:5124/api';
|
||||||
|
|
||||||
|
const getHeaders = (): { [key: string]: string } => {
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const getFarms = async (): Promise<IFarm[]> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE_URL}/farms`, { headers: getHeaders() });
|
||||||
|
return response.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
console.error('Error fetching farms:', axiosError.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUser = async (userId: number): Promise<IUser> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE_URL}/Auth/user/${userId}`, { headers: getHeaders() });
|
||||||
|
return response.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
console.error('Error fetching user:', axiosError.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createUser = async (userData: IRegisterRequest): Promise<IUser> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${API_BASE_URL}/Auth/register`, userData, { headers: getHeaders() });
|
||||||
|
return response.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
console.error('Error creating user:', axiosError.message);
|
||||||
|
if (axiosError.response) {
|
||||||
|
// Логируем дополнительные данные об ошибке
|
||||||
|
console.error('Response data:', axiosError.response.data);
|
||||||
|
console.error('Response status:', axiosError.response.status);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const loginUser = async (userData: LoginRequest): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${API_BASE_URL}/Auth/login`, userData, { headers: getHeaders() });
|
||||||
|
return response.data; // JWT токен
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
console.error('Error logging in:', axiosError.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
44
cucumber-frontend/src/App.css
Normal file
44
cucumber-frontend/src/App.css
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
.App {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-logo {
|
||||||
|
height: 40vmin;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.App-logo {
|
||||||
|
animation: App-logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header {
|
||||||
|
background-color: #282c34;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-link {
|
||||||
|
color: #61dafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes App-logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
9
cucumber-frontend/src/App.test.tsx
Normal file
9
cucumber-frontend/src/App.test.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
33
cucumber-frontend/src/App.tsx
Normal file
33
cucumber-frontend/src/App.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './App.css';
|
||||||
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
|
import {LoginPage} from './pages/Login';
|
||||||
|
import {RegisterPage} from './pages/Register';
|
||||||
|
import {AppLayout} from './components/Layout';
|
||||||
|
import { ProfilePage } from './pages/Profile';
|
||||||
|
import { GreenHouseListPage } from './pages/GreenHouseListPage';
|
||||||
|
import { ReportPage } from './pages/ReportPage';
|
||||||
|
import GreenHousePage from './pages/GreenHousePage';
|
||||||
|
import {GreenHouseStats} from './components/greenhouse/GreenHouseStats';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<AppLayout />}>
|
||||||
|
<Route index element={<Navigate to="/report" />} />
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
|
<Route path="/greenhouses" element={<GreenHouseListPage />} />
|
||||||
|
<Route path="/greenhouses/:id" element={<GreenHousePage />} />
|
||||||
|
<Route path="/greenhouses/:id/statistics" element={<GreenHouseStats />} />
|
||||||
|
<Route path="/report" element={<ReportPage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
6
cucumber-frontend/src/Requests/LoginRequest.ts
Normal file
6
cucumber-frontend/src/Requests/LoginRequest.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
interface ILoginRequest {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ILoginRequest;
|
7
cucumber-frontend/src/Requests/RegisterRequest.ts
Normal file
7
cucumber-frontend/src/Requests/RegisterRequest.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
interface IRegisterRequest {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IRegisterRequest;
|
8
cucumber-frontend/src/components/Footer.tsx
Normal file
8
cucumber-frontend/src/components/Footer.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export function Footer () {
|
||||||
|
return (
|
||||||
|
<footer className="footer">
|
||||||
|
<p>© {new Date().getFullYear()} Cucumber</p>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
27
cucumber-frontend/src/components/Header.tsx
Normal file
27
cucumber-frontend/src/components/Header.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import Title from 'antd/es/typography/Title';
|
||||||
|
import { HomeOutlined, LoginOutlined, ProfileOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export function Header () {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between bg-slate-700" style={{ height: '100px'}}>
|
||||||
|
<div className="logo">
|
||||||
|
<Title level={1} className='text-white' style={{ marginLeft: '20px' }}><Link to="/" style={{ textDecoration: 'none', color: 'white'}}>Cucumber</Link></Title>
|
||||||
|
</div>
|
||||||
|
<nav className="nav">
|
||||||
|
<ul className="flex">
|
||||||
|
<li className="mr-4">
|
||||||
|
<Link to="/login"><LoginOutlined style={{ fontSize: '24px' }} className="text-white"/></Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to="/profile"><ProfileOutlined style={{ fontSize: '24px', marginRight: '20px'}} className="text-white"/></Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to="/greenhouses"><HomeOutlined style={{ fontSize: '24px', marginRight: '20px'}} className="text-white"/> </Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
19
cucumber-frontend/src/components/Layout.tsx
Normal file
19
cucumber-frontend/src/components/Layout.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Layout } from 'antd';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import {Header} from './Header';
|
||||||
|
import {Footer} from './Footer';
|
||||||
|
|
||||||
|
export function AppLayout () {
|
||||||
|
return (
|
||||||
|
<div id="app-layout" className="bg-slate-100">
|
||||||
|
<Header />
|
||||||
|
<div id="main-content">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
187
cucumber-frontend/src/components/greenhouse/GreenHouseStats.tsx
Normal file
187
cucumber-frontend/src/components/greenhouse/GreenHouseStats.tsx
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { DatePicker, Select, Button, Form } from 'antd';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { Line } from 'react-chartjs-2';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
BarElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ArcElement,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
} from 'chart.js';
|
||||||
|
|
||||||
|
import GreenhouseService from '../../core/services/greenhouse-service';
|
||||||
|
import { setLabels } from 'react-chartjs-2/dist/utils';
|
||||||
|
|
||||||
|
|
||||||
|
export function GreenHouseStats() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const [from, setFrom] = useState<Date | null>(null);
|
||||||
|
const [to, setTo] = useState<Date | null>(null);
|
||||||
|
const [type, setType] = useState<'humidity' | 'temperature'>('humidity');
|
||||||
|
|
||||||
|
const service = new GreenhouseService();
|
||||||
|
|
||||||
|
const temperatureHistory = service.getTemperatureHistory(1);
|
||||||
|
const humidityHistory = service.getHumidityHistory(1);
|
||||||
|
|
||||||
|
const isButtonDisabled = !from || !to || (to.valueOf() === undefined || from.valueOf() === undefined)
|
||||||
|
|
||||||
|
//const isButtonDisabled = !from || !to || (to.getTime() - from.getTime()) / (1000 * 60 * 60 * 24) > 30;
|
||||||
|
ChartJS.register(ArcElement, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||||
|
|
||||||
|
const [chartData, setChartData] = useState({
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
data: [],
|
||||||
|
borderColor: '',
|
||||||
|
fill: false,
|
||||||
|
tension: 0.4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const [data, setData] = useState({
|
||||||
|
labels: ["Jan", "Feb", "Mar", "Apr", "May"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Up Line",
|
||||||
|
data: [10, 20, 15, 40, 30],
|
||||||
|
borderColor: "rgb(28, 44, 44)", // Customize line color
|
||||||
|
fill: false,
|
||||||
|
tension: 0.4, // Adjust line tension for smoother curves
|
||||||
|
},
|
||||||
|
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
}, [from, to, type]);
|
||||||
|
|
||||||
|
const createDateRange = (startDate: string, endDate: string) => {
|
||||||
|
const start = new Date(startDate);
|
||||||
|
const end = new Date(endDate);
|
||||||
|
const dates = [];
|
||||||
|
let currentDate = start;
|
||||||
|
while (currentDate <= end) {
|
||||||
|
dates.push(new Date(currentDate));
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1);
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerateReport = () => {
|
||||||
|
console.log(from, to, type);
|
||||||
|
|
||||||
|
if (from && to) {
|
||||||
|
const startDate = '2024-12-14';
|
||||||
|
const endDate = '2024-12-24';
|
||||||
|
|
||||||
|
const itemdata = type === 'humidity' ? humidityHistory : temperatureHistory;
|
||||||
|
const startDateObj = new Date(Date.parse(startDate));
|
||||||
|
const endDateObj = new Date(Date.parse(endDate));
|
||||||
|
|
||||||
|
const filteredData = itemdata.filter(item => {
|
||||||
|
const itemDate = new Date(Date.parse(item.date));
|
||||||
|
return itemDate >= startDateObj && itemDate <= endDateObj;
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredData.forEach(element => {
|
||||||
|
console.log('wff');
|
||||||
|
console.log(element.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dateRange = createDateRange(startDate, endDate);
|
||||||
|
const labels: string[] = dateRange.map(date => date.getDate() % 5 === 0 ? date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long' }) : '');
|
||||||
|
|
||||||
|
setChartData({
|
||||||
|
labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: type === 'humidity' ? 'Влажность' : 'Температура',
|
||||||
|
data: itemdata.map(item => item.value),
|
||||||
|
backgroundColor: [
|
||||||
|
'rgb(153, 102, 255)'
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgb(153, 102, 255)'
|
||||||
|
],
|
||||||
|
borderWidth: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
// setData({
|
||||||
|
// labels,
|
||||||
|
// datasets: [
|
||||||
|
// {
|
||||||
|
// label: 'Expenses by Month',
|
||||||
|
// data: filteredData.map(item => item.value),
|
||||||
|
// backgroundColor: [
|
||||||
|
// 'rgb(153, 102, 255)'
|
||||||
|
// ],
|
||||||
|
// borderColor: [
|
||||||
|
// 'rgb(153, 102, 255)'
|
||||||
|
// ],
|
||||||
|
// borderWidth: 1
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// } as any);
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Дата",
|
||||||
|
},
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Значение",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Form onFinish={handleGenerateReport} className="p-8" style={{ marginTop: '100px', marginLeft: '650px', height: '800px', width: '600px' }}>
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Статистика по выбранной теплице</h1>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<DatePicker value={from} onChange={(date) => setFrom(date)} format="DD.MM.YYYY" />
|
||||||
|
<DatePicker value={to} onChange={(date) => setTo(date)} format="DD.MM.YYYY" />
|
||||||
|
</div>
|
||||||
|
<div className="m-4">
|
||||||
|
<Select value={type} onChange={(value) => setType(value)}>
|
||||||
|
<Select.Option value="humidity">Влажность</Select.Option>
|
||||||
|
<Select.Option value="temperature">Температура</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Button type="primary" htmlType="submit" disabled={isButtonDisabled}>
|
||||||
|
Сформировать отчёт
|
||||||
|
</Button>
|
||||||
|
<div className="m-4">
|
||||||
|
<Line data={chartData} options={options} />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Table } from "antd";
|
||||||
|
import { HistoryData } from "../../core/services/greenhouse-service";
|
||||||
|
|
||||||
|
export interface GreenhouseCommandTableProps {
|
||||||
|
history: HistoryData[],
|
||||||
|
className: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const GreenhouseCommandHistoryTable = (props: GreenhouseCommandTableProps) => {
|
||||||
|
const { history, className } = props;
|
||||||
|
return (
|
||||||
|
<Table columns={[
|
||||||
|
{ title: 'Действие', dataIndex: 'action' },
|
||||||
|
{ title: 'Время начала', dataIndex: 'startAt' },
|
||||||
|
{ title: 'Время конца', dataIndex: 'endAt'}
|
||||||
|
]} dataSource={history} className={className} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GreenhouseCommandHistoryTable;
|
@ -0,0 +1,46 @@
|
|||||||
|
import { GreenhouseInfo } from "../../core/api/data-contracts";
|
||||||
|
|
||||||
|
export interface GreehouseDataDisplayProps {
|
||||||
|
info: GreenhouseInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
const GreenhouseDataDisplay = (props: GreehouseDataDisplayProps) => {
|
||||||
|
const { info } = props;
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">Данные теплицы</h1>
|
||||||
|
<div className="bg-white shadow-md rounded-lg p-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Процент влажности почвы:</h2>
|
||||||
|
<p className="text-lg">{info.percentWater}%</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Температура почвы:</h2>
|
||||||
|
<p className="text-lg">{info.soilTemperature}°C</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Статус вентиля:</h2>
|
||||||
|
<p className="text-lg">
|
||||||
|
{info.pumpStatus ? 'Открыт' : 'Закрыт'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Статус нагревателя:</h2>
|
||||||
|
<p className="text-lg">
|
||||||
|
{info.heatingStatus ? 'Включен' : 'Выключен'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Статус автополива:</h2>
|
||||||
|
<p className="text-lg">
|
||||||
|
{info.autoWateringStatus ? 'Активирован' : 'Деактивирован'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GreenhouseDataDisplay;
|
@ -0,0 +1,26 @@
|
|||||||
|
import { Switch } from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export interface GreenhouseManagerProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const GreenhouseManager = (props: GreenhouseManagerProps) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Управление</h1>
|
||||||
|
<div className="m-4">
|
||||||
|
<label htmlFor="watering" className="text-xl me-2">Полив</label>
|
||||||
|
<Switch id="watering" checkedChildren="Вкл" unCheckedChildren="Выкл" />
|
||||||
|
</div>
|
||||||
|
<div className="m-4">
|
||||||
|
<label htmlFor="heating" className="text-xl me-2">Нагрев</label>
|
||||||
|
<Switch id="heating" checkedChildren="Вкл" unCheckedChildren="Выкл" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GreenhouseManager;
|
@ -0,0 +1,97 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Greenhouse, HeatingMode, WateringMode } from "../../core/api/data-contracts";
|
||||||
|
import { Select, Button, Form, Input } from "antd";
|
||||||
|
|
||||||
|
export interface GreenhouseSettingsFormProps {
|
||||||
|
updateSettings: (settings: Greenhouse) => void,
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const GreenhouseSettingsForm = (props: GreenhouseSettingsFormProps) => {
|
||||||
|
const { updateSettings, id } = props;
|
||||||
|
const [settings, setSettings] = useState<Greenhouse>({
|
||||||
|
id: Number(id!),
|
||||||
|
wateringMode: WateringMode.Value0,
|
||||||
|
heatingMode: HeatingMode.Value0,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Настройки</h1>
|
||||||
|
<Form
|
||||||
|
name="basic"
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={{ remember: true }}
|
||||||
|
onFinish={(values: any) => updateSettings({ ...settings, ...values })}
|
||||||
|
onFinishFailed={(error: any) => console.log(error)}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Режим полива"
|
||||||
|
name="wateringMode"
|
||||||
|
rules={[{ required: true, message: 'Выберите режим полива' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={settings.wateringMode}
|
||||||
|
onChange={(value) => setSettings({ ...settings, wateringMode: value })}
|
||||||
|
>
|
||||||
|
<Select.Option value={WateringMode.Value0}>Ручной</Select.Option>
|
||||||
|
<Select.Option value={WateringMode.Value1}>Автоматический</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Режим нагрева"
|
||||||
|
name="heatingMode"
|
||||||
|
rules={[{ required: true, message: 'Выберите режим нагрева' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={settings.heatingMode}
|
||||||
|
onChange={(value) => setSettings({ ...settings, heatingMode: value })}
|
||||||
|
>
|
||||||
|
<Select.Option value={HeatingMode.Value0}>Ручной</Select.Option>
|
||||||
|
<Select.Option value={HeatingMode.Value1}>Автоматический</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Минимальная температура"
|
||||||
|
name="minTemperature"
|
||||||
|
rules={[{ required: true, message: 'Введите минимальную температуру' }]}
|
||||||
|
>
|
||||||
|
<Input type="number" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Максимальная температура"
|
||||||
|
name="maxTemperature"
|
||||||
|
rules={[{ required: true, message: 'Введите максимальную температуру' }]}
|
||||||
|
>
|
||||||
|
<Input type="number" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Минимальная влажность почвы"
|
||||||
|
name="minHumidity"
|
||||||
|
rules={[{ required: true, message: 'Введите минимальную влажность почвы' }]}
|
||||||
|
>
|
||||||
|
<Input type="number" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Максимальная влажность почвы"
|
||||||
|
name="maxHumidity"
|
||||||
|
rules={[{ required: true, message: 'Введите максимальную влажность почвы' }]}
|
||||||
|
>
|
||||||
|
<Input type="number" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
Обновить настройки
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GreenhouseSettingsForm;
|
297
cucumber-frontend/src/core/api/Api.ts
Normal file
297
cucumber-frontend/src/core/api/Api.ts
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||||
|
* ## ##
|
||||||
|
* ## AUTHOR: acacode ##
|
||||||
|
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Farm,
|
||||||
|
FarmRequest,
|
||||||
|
Greenhouse,
|
||||||
|
GreenhouseInfo,
|
||||||
|
GreenhouseRequest,
|
||||||
|
LoginRequest,
|
||||||
|
RegisterRequest,
|
||||||
|
ValveRequest,
|
||||||
|
} from "./data-contracts";
|
||||||
|
import { ContentType, HttpClient, RequestParams } from "./http-client";
|
||||||
|
|
||||||
|
export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Auth
|
||||||
|
* @name AuthRegisterCreate
|
||||||
|
* @request POST:/api/Auth/register
|
||||||
|
* @secure
|
||||||
|
* @response `200` `void` Success
|
||||||
|
*/
|
||||||
|
authRegisterCreate = (data: RegisterRequest, params: RequestParams = {}) =>
|
||||||
|
this.request<void, any>({
|
||||||
|
path: `/api/Auth/register`,
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
secure: true,
|
||||||
|
type: ContentType.Json,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Auth
|
||||||
|
* @name AuthLoginCreate
|
||||||
|
* @request POST:/api/Auth/login
|
||||||
|
* @secure
|
||||||
|
* @response `200` `void` Success
|
||||||
|
*/
|
||||||
|
authLoginCreate = (data: LoginRequest, params: RequestParams = {}) =>
|
||||||
|
this.request<void, any>({
|
||||||
|
path: `/api/Auth/login`,
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
secure: true,
|
||||||
|
type: ContentType.Json,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Auth
|
||||||
|
* @name AuthUserList
|
||||||
|
* @request GET:/api/Auth/user
|
||||||
|
* @secure
|
||||||
|
* @response `200` `void` Success
|
||||||
|
*/
|
||||||
|
authUserList = (params: RequestParams = {}) =>
|
||||||
|
this.request<void, any>({
|
||||||
|
path: `/api/Auth/user`,
|
||||||
|
method: "GET",
|
||||||
|
secure: true,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Farm
|
||||||
|
* @name UserFarmDetail
|
||||||
|
* @request GET:/api/user/{userId}/farm
|
||||||
|
* @secure
|
||||||
|
* @response `200` `(Farm)[]` Success
|
||||||
|
*/
|
||||||
|
userFarmDetail = (userId: number, params: RequestParams = {}) =>
|
||||||
|
this.request<Farm[], any>({
|
||||||
|
path: `/api/user/${userId}/farm`,
|
||||||
|
method: "GET",
|
||||||
|
secure: true,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Farm
|
||||||
|
* @name UserFarmCreate
|
||||||
|
* @request POST:/api/user/{userId}/farm
|
||||||
|
* @secure
|
||||||
|
* @response `200` `Farm` Success
|
||||||
|
*/
|
||||||
|
userFarmCreate = (userId: number, data: FarmRequest, params: RequestParams = {}) =>
|
||||||
|
this.request<Farm, any>({
|
||||||
|
path: `/api/user/${userId}/farm`,
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
secure: true,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Farm
|
||||||
|
* @name UserFarmDetail2
|
||||||
|
* @request GET:/api/user/{userId}/farm/{farmId}
|
||||||
|
* @originalName userFarmDetail
|
||||||
|
* @duplicate
|
||||||
|
* @secure
|
||||||
|
* @response `200` `Farm` Success
|
||||||
|
*/
|
||||||
|
userFarmDetail2 = (userId: number, farmId: number, params: RequestParams = {}) =>
|
||||||
|
this.request<Farm, any>({
|
||||||
|
path: `/api/user/${userId}/farm/${farmId}`,
|
||||||
|
method: "GET",
|
||||||
|
secure: true,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Farm
|
||||||
|
* @name UserFarmUpdate
|
||||||
|
* @request PUT:/api/user/{userId}/farm/{farmId}
|
||||||
|
* @secure
|
||||||
|
* @response `200` `Farm` Success
|
||||||
|
*/
|
||||||
|
userFarmUpdate = (userId: number, farmId: number, data: FarmRequest, params: RequestParams = {}) =>
|
||||||
|
this.request<Farm, any>({
|
||||||
|
path: `/api/user/${userId}/farm/${farmId}`,
|
||||||
|
method: "PUT",
|
||||||
|
body: data,
|
||||||
|
secure: true,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Farm
|
||||||
|
* @name UserFarmDelete
|
||||||
|
* @request DELETE:/api/user/{userId}/farm/{farmId}
|
||||||
|
* @secure
|
||||||
|
* @response `200` `void` Success
|
||||||
|
*/
|
||||||
|
userFarmDelete = (userId: number, farmId: number, params: RequestParams = {}) =>
|
||||||
|
this.request<void, any>({
|
||||||
|
path: `/api/user/${userId}/farm/${farmId}`,
|
||||||
|
method: "DELETE",
|
||||||
|
secure: true,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Greenhouse
|
||||||
|
* @name FarmGreenhouseDetail
|
||||||
|
* @request GET:/api/farm/{farmId}/greenhouse
|
||||||
|
* @secure
|
||||||
|
* @response `200` `(GreenhouseInfo)[]` Success
|
||||||
|
*/
|
||||||
|
farmGreenhouseDetail = (farmId: number, params: RequestParams = {}) =>
|
||||||
|
this.request<GreenhouseInfo[], any>({
|
||||||
|
path: `/api/farm/${farmId}/greenhouse`,
|
||||||
|
method: "GET",
|
||||||
|
secure: true,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Greenhouse
|
||||||
|
* @name FarmGreenhouseCreate
|
||||||
|
* @request POST:/api/farm/{farmId}/greenhouse
|
||||||
|
* @secure
|
||||||
|
* @response `200` `Greenhouse` Success
|
||||||
|
*/
|
||||||
|
farmGreenhouseCreate = (farmId: number, data: GreenhouseRequest, params: RequestParams = {}) =>
|
||||||
|
this.request<Greenhouse, any>({
|
||||||
|
path: `/api/farm/${farmId}/greenhouse`,
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
secure: true,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Greenhouse
|
||||||
|
* @name FarmGreenhouseDetail2
|
||||||
|
* @request GET:/api/farm/{farmId}/greenhouse/{greenhouseId}
|
||||||
|
* @originalName farmGreenhouseDetail
|
||||||
|
* @duplicate
|
||||||
|
* @secure
|
||||||
|
* @response `200` `GreenhouseInfo` Success
|
||||||
|
*/
|
||||||
|
farmGreenhouseDetail2 = (farmId: number, greenhouseId: number, params: RequestParams = {}) =>
|
||||||
|
this.request<GreenhouseInfo, any>({
|
||||||
|
path: `/api/farm/${farmId}/greenhouse/${greenhouseId}`,
|
||||||
|
method: "GET",
|
||||||
|
secure: true,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Greenhouse
|
||||||
|
* @name FarmGreenhouseDelete
|
||||||
|
* @request DELETE:/api/farm/{farmId}/greenhouse/{greenhouseId}
|
||||||
|
* @secure
|
||||||
|
* @response `200` `void` Success
|
||||||
|
*/
|
||||||
|
farmGreenhouseDelete = (farmId: number, greenhouseId: number, params: RequestParams = {}) =>
|
||||||
|
this.request<void, any>({
|
||||||
|
path: `/api/farm/${farmId}/greenhouse/${greenhouseId}`,
|
||||||
|
method: "DELETE",
|
||||||
|
secure: true,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Greenhouse
|
||||||
|
* @name FarmGreenhouseSettingsDetail
|
||||||
|
* @request GET:/api/farm/{farmId}/greenhouse/{greenhouseId}/settings
|
||||||
|
* @secure
|
||||||
|
* @response `200` `Greenhouse` Success
|
||||||
|
*/
|
||||||
|
farmGreenhouseSettingsDetail = (farmId: number, greenhouseId: number, params: RequestParams = {}) =>
|
||||||
|
this.request<Greenhouse, any>({
|
||||||
|
path: `/api/farm/${farmId}/greenhouse/${greenhouseId}/settings`,
|
||||||
|
method: "GET",
|
||||||
|
secure: true,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Greenhouse
|
||||||
|
* @name FarmGreenhouseSettingsUpdate
|
||||||
|
* @request PUT:/api/farm/{farmId}/greenhouse/{greenhouseId}/settings
|
||||||
|
* @secure
|
||||||
|
* @response `200` `Greenhouse` Success
|
||||||
|
*/
|
||||||
|
farmGreenhouseSettingsUpdate = (
|
||||||
|
farmId: number,
|
||||||
|
greenhouseId: number,
|
||||||
|
data: GreenhouseRequest,
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
this.request<Greenhouse, any>({
|
||||||
|
path: `/api/farm/${farmId}/greenhouse/${greenhouseId}/settings`,
|
||||||
|
method: "PUT",
|
||||||
|
body: data,
|
||||||
|
secure: true,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* No description
|
||||||
|
*
|
||||||
|
* @tags Valve
|
||||||
|
* @name FarmGreenhouseWateringCreate
|
||||||
|
* @request POST:/api/farm/{farmId}/greenhouse/{ghId}/watering
|
||||||
|
* @secure
|
||||||
|
* @response `200` `void` Success
|
||||||
|
*/
|
||||||
|
farmGreenhouseWateringCreate = (farmId: number, ghId: number, data: ValveRequest, params: RequestParams = {}) =>
|
||||||
|
this.request<void, any>({
|
||||||
|
path: `/api/farm/${farmId}/greenhouse/${ghId}/watering`,
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
secure: true,
|
||||||
|
type: ContentType.Json,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
}
|
92
cucumber-frontend/src/core/api/data-contracts.ts
Normal file
92
cucumber-frontend/src/core/api/data-contracts.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||||
|
* ## ##
|
||||||
|
* ## AUTHOR: acacode ##
|
||||||
|
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Farm {
|
||||||
|
/** @format int32 */
|
||||||
|
id?: number;
|
||||||
|
name?: string | null;
|
||||||
|
/** @format int32 */
|
||||||
|
userId?: number;
|
||||||
|
user?: User;
|
||||||
|
raspberryIP?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FarmRequest {
|
||||||
|
name?: string | null;
|
||||||
|
raspberryIP?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Greenhouse {
|
||||||
|
/** @format int32 */
|
||||||
|
id?: number;
|
||||||
|
/** @format int32 */
|
||||||
|
recomendedTemperature?: number;
|
||||||
|
wateringMode?: WateringMode;
|
||||||
|
heatingMode?: HeatingMode;
|
||||||
|
/** @format int32 */
|
||||||
|
farmId?: number;
|
||||||
|
farm?: Farm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GreenhouseInfo {
|
||||||
|
/** @format int32 */
|
||||||
|
id?: number;
|
||||||
|
/** @format int32 */
|
||||||
|
percentWater?: number;
|
||||||
|
/** @format int32 */
|
||||||
|
soilTemperature?: number;
|
||||||
|
pumpStatus?: boolean;
|
||||||
|
heatingStatus?: boolean;
|
||||||
|
autoWateringStatus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GreenhouseRequest {
|
||||||
|
/** @format int32 */
|
||||||
|
recomendedTemperature?: number;
|
||||||
|
wateringMode?: WateringMode;
|
||||||
|
heatingMode?: HeatingMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @format int32 */
|
||||||
|
export enum HeatingMode {
|
||||||
|
Value0 = 0,
|
||||||
|
Value1 = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
email?: string | null;
|
||||||
|
password?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
name?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
password?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
/** @format int32 */
|
||||||
|
id?: number;
|
||||||
|
name?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
password?: string | null;
|
||||||
|
farms?: Farm[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValveRequest {
|
||||||
|
action?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @format int32 */
|
||||||
|
export enum WateringMode {
|
||||||
|
Value0 = 0,
|
||||||
|
Value1 = 1,
|
||||||
|
}
|
220
cucumber-frontend/src/core/api/http-client.ts
Normal file
220
cucumber-frontend/src/core/api/http-client.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||||
|
* ## ##
|
||||||
|
* ## AUTHOR: acacode ##
|
||||||
|
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type QueryParamsType = Record<string | number, any>;
|
||||||
|
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
|
||||||
|
|
||||||
|
export interface FullRequestParams extends Omit<RequestInit, "body"> {
|
||||||
|
/** set parameter to `true` for call `securityWorker` for this request */
|
||||||
|
secure?: boolean;
|
||||||
|
/** request path */
|
||||||
|
path: string;
|
||||||
|
/** content type of request body */
|
||||||
|
type?: ContentType;
|
||||||
|
/** query params */
|
||||||
|
query?: QueryParamsType;
|
||||||
|
/** format of response (i.e. response.json() -> format: "json") */
|
||||||
|
format?: ResponseFormat;
|
||||||
|
/** request body */
|
||||||
|
body?: unknown;
|
||||||
|
/** base url */
|
||||||
|
baseUrl?: string;
|
||||||
|
/** request cancellation token */
|
||||||
|
cancelToken?: CancelToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
|
||||||
|
|
||||||
|
export interface ApiConfig<SecurityDataType = unknown> {
|
||||||
|
baseUrl?: string;
|
||||||
|
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
|
||||||
|
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
|
||||||
|
customFetch?: typeof fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
|
||||||
|
data: D;
|
||||||
|
error: E;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CancelToken = Symbol | string | number;
|
||||||
|
|
||||||
|
export enum ContentType {
|
||||||
|
Json = "application/json",
|
||||||
|
FormData = "multipart/form-data",
|
||||||
|
UrlEncoded = "application/x-www-form-urlencoded",
|
||||||
|
Text = "text/plain",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpClient<SecurityDataType = unknown> {
|
||||||
|
public baseUrl: string = "http://172.18.167.3:5124";
|
||||||
|
private securityData: SecurityDataType | null = null;
|
||||||
|
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
||||||
|
private abortControllers = new Map<CancelToken, AbortController>();
|
||||||
|
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
|
||||||
|
|
||||||
|
private baseApiParams: RequestParams = {
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {},
|
||||||
|
redirect: "follow",
|
||||||
|
referrerPolicy: "no-referrer",
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
|
||||||
|
Object.assign(this, apiConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSecurityData = (data: SecurityDataType | null) => {
|
||||||
|
this.securityData = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected encodeQueryParam(key: string, value: any) {
|
||||||
|
const encodedKey = encodeURIComponent(key);
|
||||||
|
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addQueryParam(query: QueryParamsType, key: string) {
|
||||||
|
return this.encodeQueryParam(key, query[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addArrayQueryParam(query: QueryParamsType, key: string) {
|
||||||
|
const value = query[key];
|
||||||
|
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toQueryString(rawQuery?: QueryParamsType): string {
|
||||||
|
const query = rawQuery || {};
|
||||||
|
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
|
||||||
|
return keys
|
||||||
|
.map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key)))
|
||||||
|
.join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
||||||
|
const queryString = this.toQueryString(rawQuery);
|
||||||
|
return queryString ? `?${queryString}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
||||||
|
[ContentType.Json]: (input: any) =>
|
||||||
|
input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
|
||||||
|
[ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input),
|
||||||
|
[ContentType.FormData]: (input: any) =>
|
||||||
|
Object.keys(input || {}).reduce((formData, key) => {
|
||||||
|
const property = input[key];
|
||||||
|
formData.append(
|
||||||
|
key,
|
||||||
|
property instanceof Blob
|
||||||
|
? property
|
||||||
|
: typeof property === "object" && property !== null
|
||||||
|
? JSON.stringify(property)
|
||||||
|
: `${property}`,
|
||||||
|
);
|
||||||
|
return formData;
|
||||||
|
}, new FormData()),
|
||||||
|
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
||||||
|
};
|
||||||
|
|
||||||
|
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
|
||||||
|
return {
|
||||||
|
...this.baseApiParams,
|
||||||
|
...params1,
|
||||||
|
...(params2 || {}),
|
||||||
|
headers: {
|
||||||
|
...(this.baseApiParams.headers || {}),
|
||||||
|
...(params1.headers || {}),
|
||||||
|
...((params2 && params2.headers) || {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
|
||||||
|
if (this.abortControllers.has(cancelToken)) {
|
||||||
|
const abortController = this.abortControllers.get(cancelToken);
|
||||||
|
if (abortController) {
|
||||||
|
return abortController.signal;
|
||||||
|
}
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
this.abortControllers.set(cancelToken, abortController);
|
||||||
|
return abortController.signal;
|
||||||
|
};
|
||||||
|
|
||||||
|
public abortRequest = (cancelToken: CancelToken) => {
|
||||||
|
const abortController = this.abortControllers.get(cancelToken);
|
||||||
|
|
||||||
|
if (abortController) {
|
||||||
|
abortController.abort();
|
||||||
|
this.abortControllers.delete(cancelToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public request = async <T = any, E = any>({
|
||||||
|
body,
|
||||||
|
secure,
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
query,
|
||||||
|
format,
|
||||||
|
baseUrl,
|
||||||
|
cancelToken,
|
||||||
|
...params
|
||||||
|
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
|
||||||
|
const secureParams =
|
||||||
|
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
|
||||||
|
this.securityWorker &&
|
||||||
|
(await this.securityWorker(this.securityData))) ||
|
||||||
|
{};
|
||||||
|
const requestParams = this.mergeRequestParams(params, secureParams);
|
||||||
|
const queryString = query && this.toQueryString(query);
|
||||||
|
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
|
||||||
|
const responseFormat = format || requestParams.format;
|
||||||
|
|
||||||
|
return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, {
|
||||||
|
...requestParams,
|
||||||
|
headers: {
|
||||||
|
...(requestParams.headers || {}),
|
||||||
|
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
|
||||||
|
},
|
||||||
|
signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null,
|
||||||
|
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
|
||||||
|
}).then(async (response) => {
|
||||||
|
const r = response.clone() as HttpResponse<T, E>;
|
||||||
|
r.data = null as unknown as T;
|
||||||
|
r.error = null as unknown as E;
|
||||||
|
|
||||||
|
const data = !responseFormat
|
||||||
|
? r
|
||||||
|
: await response[responseFormat]()
|
||||||
|
.then((data) => {
|
||||||
|
if (r.ok) {
|
||||||
|
r.data = data;
|
||||||
|
} else {
|
||||||
|
r.error = data;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
r.error = e;
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cancelToken) {
|
||||||
|
this.abortControllers.delete(cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) throw data;
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
94
cucumber-frontend/src/core/services/greenhouse-service.ts
Normal file
94
cucumber-frontend/src/core/services/greenhouse-service.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { Greenhouse, GreenhouseInfo } from "../api/data-contracts";
|
||||||
|
|
||||||
|
export type HistoryData = {
|
||||||
|
id: number,
|
||||||
|
action: string,
|
||||||
|
startAt: string,
|
||||||
|
endAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TemperatureHistoryData = {
|
||||||
|
id: number,
|
||||||
|
value: number,
|
||||||
|
date: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HumidityHistoryData = {
|
||||||
|
id: number,
|
||||||
|
value: number,
|
||||||
|
date: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class GreenhouseService {
|
||||||
|
private static _info: GreenhouseInfo = {} as GreenhouseInfo;
|
||||||
|
public getData(id: number): GreenhouseInfo {
|
||||||
|
if (GreenhouseService._info.id) return GreenhouseService._info;
|
||||||
|
|
||||||
|
let info = {
|
||||||
|
id: id,
|
||||||
|
percentWater: randomInt(50, 60),
|
||||||
|
soilTemperature: randomInt(17, 28),
|
||||||
|
pumpStatus: randomInt(0, 1) == 1,
|
||||||
|
heatingStatus: randomInt(0, 1) == 1,
|
||||||
|
autoWateringStatus: randomInt(0, 1) == 1
|
||||||
|
};
|
||||||
|
GreenhouseService._info = info;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSettings(settings: Greenhouse): boolean {
|
||||||
|
let newInfo = {
|
||||||
|
id: settings.id,
|
||||||
|
percentWater: randomInt(50, 60),
|
||||||
|
soilTemperature: randomInt(17, 28),
|
||||||
|
pumpStatus: randomInt(0, 1) == 1,
|
||||||
|
heatingStatus: settings.heatingMode,
|
||||||
|
autoWateringStatus: settings.wateringMode
|
||||||
|
} as GreenhouseInfo;
|
||||||
|
GreenhouseService._info = newInfo;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCommandsHistory(id: number): HistoryData[] {
|
||||||
|
return [
|
||||||
|
{ id: 1, action: "Открыт вентиль", startAt: "10.12.2024", endAt: "11.12.2024" },
|
||||||
|
{ id: 2, action: "Закрыт вентиль", startAt: "11.12.2024", endAt: "14.12.2024" },
|
||||||
|
{ id: 3, action: "Открыт вентиль", startAt: "14.12.2024", endAt: "15.12.2024" },
|
||||||
|
{ id: 4, action: "Закрыт вентиль", startAt: "15.12.2024", endAt: "17.12.2024" },
|
||||||
|
{ id: 5, action: "Открыт вентиль", startAt: "17.12.2024", endAt: "18.12.2024" },
|
||||||
|
|
||||||
|
{ id: 6, action: "Включен нагреватель", startAt: "18.12.2024", endAt: "22.12.2024"},
|
||||||
|
{ id: 7, action: "Выключен нагреватель", startAt: "22.12.2024", endAt: "24.12.2024"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTemperatureHistory(id: number): TemperatureHistoryData[] {
|
||||||
|
return [
|
||||||
|
{ id: 1, value: 30, date: "10.12.2024" },
|
||||||
|
{ id: 2, value: 28, date: "12.12.2024" },
|
||||||
|
{ id: 3, value: 29, date: "14.12.2024" },
|
||||||
|
{ id: 4, value: 31, date: "16.12.2024" },
|
||||||
|
{ id: 5, value: 27, date: "18.12.2024" },
|
||||||
|
{ id: 6, value: 26, date: "20.12.2024" },
|
||||||
|
{ id: 7, value: 32, date: "22.12.2024" },
|
||||||
|
{ id: 8, value: 33, date: "24.12.2024" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHumidityHistory(id: number): HumidityHistoryData[] {
|
||||||
|
return [
|
||||||
|
{ id: 1, value: 78, date: "10.12.2024" },
|
||||||
|
{ id: 2, value: 80, date: "12.12.2024" },
|
||||||
|
{ id: 3, value: 81, date: "14.12.2024" },
|
||||||
|
{ id: 4, value: 82, date: "16.12.2024" },
|
||||||
|
{ id: 5, value: 83, date: "18.12.2024" },
|
||||||
|
{ id: 6, value: 79, date: "20.12.2024" },
|
||||||
|
{ id: 7, value: 84, date: "22.12.2024" },
|
||||||
|
{ id: 8, value: 85, date: "24.12.2024" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomInt(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
|
}
|
51
cucumber-frontend/src/index.css
Normal file
51
cucumber-frontend/src/index.css
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
background-color: #282c34;
|
||||||
|
min-height: 10vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
background-color: #282c34;
|
||||||
|
min-height: 10vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
position:relative;
|
||||||
|
left:0;
|
||||||
|
bottom:0;
|
||||||
|
right:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
17
cucumber-frontend/src/index.tsx
Normal file
17
cucumber-frontend/src/index.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(
|
||||||
|
document.getElementById('root') as HTMLElement
|
||||||
|
);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
1
cucumber-frontend/src/logo.svg
Normal file
1
cucumber-frontend/src/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
11
cucumber-frontend/src/models/IFarm.tsx
Normal file
11
cucumber-frontend/src/models/IFarm.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type IUser from "./IUser";
|
||||||
|
|
||||||
|
interface IFarm {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
userId: number;
|
||||||
|
user: IUser | null;
|
||||||
|
rasberryMacAddr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IFarm;
|
13
cucumber-frontend/src/models/IGreenhouse.tsx
Normal file
13
cucumber-frontend/src/models/IGreenhouse.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import IFarm from "./IFarm";
|
||||||
|
import { HeatingMode } from "./Support/HeatingMode";
|
||||||
|
import { WateringMode } from "./Support/WateringMode";
|
||||||
|
|
||||||
|
export interface IGreenhouse {
|
||||||
|
id: number;
|
||||||
|
recommendedTemperature: number;
|
||||||
|
wateringMode: WateringMode;
|
||||||
|
heatingMode: HeatingMode;
|
||||||
|
farmId: number;
|
||||||
|
farm?: IFarm;
|
||||||
|
}
|
||||||
|
|
11
cucumber-frontend/src/models/IUser.tsx
Normal file
11
cucumber-frontend/src/models/IUser.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type IFarm from "./IFarm";
|
||||||
|
|
||||||
|
interface IUser {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
farms: IFarm[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IUser
|
4
cucumber-frontend/src/models/Support/HeatingMode.ts
Normal file
4
cucumber-frontend/src/models/Support/HeatingMode.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum HeatingMode {
|
||||||
|
Manual,
|
||||||
|
Auto
|
||||||
|
}
|
4
cucumber-frontend/src/models/Support/WateringMode.ts
Normal file
4
cucumber-frontend/src/models/Support/WateringMode.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum WateringMode {
|
||||||
|
Manual,
|
||||||
|
Auto
|
||||||
|
}
|
167
cucumber-frontend/src/pages/GreenHouseListPage.tsx
Normal file
167
cucumber-frontend/src/pages/GreenHouseListPage.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Modal, Button, Table, Form, Input, Select, Space } from 'antd';
|
||||||
|
import { Greenhouse, GreenhouseInfo, HeatingMode, WateringMode } from '../core/api/data-contracts';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export function GreenHouseListPage() {
|
||||||
|
const [greenhouses, setGreenhouses] = useState<GreenhouseInfo[]>([]);
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const [editingGreenhouse, setEditingGreenhouse] = useState<GreenhouseInfo | null>(null);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchGreenhouses();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchGreenhouses = async () => {
|
||||||
|
setGreenhouses([
|
||||||
|
{
|
||||||
|
id: 1735006258520,
|
||||||
|
soilTemperature: 32,
|
||||||
|
percentWater: 80,
|
||||||
|
autoWateringStatus: Boolean(WateringMode.Value1),
|
||||||
|
pumpStatus: Boolean(WateringMode.Value1),
|
||||||
|
heatingStatus: Boolean(HeatingMode.Value1)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1735006258521,
|
||||||
|
soilTemperature: 30,
|
||||||
|
percentWater: 82,
|
||||||
|
autoWateringStatus: Boolean(WateringMode.Value1),
|
||||||
|
pumpStatus: Boolean(WateringMode.Value1),
|
||||||
|
heatingStatus: Boolean(HeatingMode.Value1)
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddOrEditSubmit = () => {
|
||||||
|
form.validateFields().then((values) => {
|
||||||
|
if (editingGreenhouse) {
|
||||||
|
setGreenhouses(greenhouses.map(g => g.id === editingGreenhouse.id ? { ...g, ...values } : g));
|
||||||
|
} else {
|
||||||
|
setGreenhouses([...greenhouses, { id: Date.now(), ...values }]);
|
||||||
|
}
|
||||||
|
setIsModalVisible(false);
|
||||||
|
setEditingGreenhouse(null);
|
||||||
|
form.resetFields();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id?: number) => {
|
||||||
|
setGreenhouses(greenhouses.filter(g => g.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalOpen = (greenhouse?: GreenhouseInfo) => {
|
||||||
|
setEditingGreenhouse(greenhouse || null);
|
||||||
|
setIsModalVisible(true);
|
||||||
|
if (greenhouse) {
|
||||||
|
form.setFieldsValue(greenhouse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Температура',
|
||||||
|
dataIndex: 'soilTemperature',
|
||||||
|
key: 'soilTemperature',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Процент влажности',
|
||||||
|
dataIndex: 'percentWater',
|
||||||
|
key: 'percentWater',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Режим полива',
|
||||||
|
dataIndex: 'autoWateringStatus',
|
||||||
|
key: 'autoWateringStatus',
|
||||||
|
render: (status: boolean) => status ? 'Автоматически' : 'Вручную',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Статус вентиля',
|
||||||
|
dataIndex: 'pumpStatus',
|
||||||
|
key: 'pumpStatus',
|
||||||
|
render: (status: boolean) => status ? 'Открыт' : 'Закрыт',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Статус нагревателя',
|
||||||
|
dataIndex: 'heatingStatus',
|
||||||
|
key: 'heatingStatus',
|
||||||
|
render: (status: boolean) => status ? 'Включен' : 'Выключен',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Действия',
|
||||||
|
key: 'action',
|
||||||
|
render: (_: any, record: GreenhouseInfo) => (
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => startWatering(record.id)}>Начать полив</Button>
|
||||||
|
<Button onClick={() => startHeating(record.id)}>Начать нагрев</Button>
|
||||||
|
<Button onClick={() => handleDelete(record.id)}>Удалить</Button>
|
||||||
|
<Button onClick={() => handleModalOpen(record)}>Редактировать</Button>
|
||||||
|
<Link to={`/greenhouses/${record.id}`}>Перейти</Link>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-3xl font-bold mb-4">Список теплиц</h1>
|
||||||
|
|
||||||
|
<Button onClick={() => handleModalOpen()} className="mb-4">
|
||||||
|
Добавить теплицу
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Table columns={columns} dataSource={greenhouses} rowKey="id" />
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={editingGreenhouse ? "Редактирование теплицы" : "Добавление теплицы"}
|
||||||
|
visible={isModalVisible}
|
||||||
|
onOk={handleAddOrEditSubmit}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
setEditingGreenhouse(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item name="soilTemperature" label="Температура">
|
||||||
|
<Input type="number" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="percentWater" label="Процент влажности">
|
||||||
|
<Input type="number" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="autoWateringStatus" label="Режим автополива">
|
||||||
|
<Select>
|
||||||
|
<Select.Option value={WateringMode.Value0}>Отключен</Select.Option>
|
||||||
|
<Select.Option value={WateringMode.Value1}>Включен</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="pumpStatus" label="Статус вентиля">
|
||||||
|
<Select>
|
||||||
|
<Select.Option value={false}>Закрыт</Select.Option>
|
||||||
|
<Select.Option value={true}>Открыт</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="heatingStatus" label="Статус нагревателя">
|
||||||
|
<Select>
|
||||||
|
<Select.Option value={false}>Отключен</Select.Option>
|
||||||
|
<Select.Option value={true}>Включен</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
function startWatering(id: number | undefined): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startHeating(id: number | undefined): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
43
cucumber-frontend/src/pages/GreenHousePage.tsx
Normal file
43
cucumber-frontend/src/pages/GreenHousePage.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import GreenhouseService, { HistoryData } from '../core/services/greenhouse-service';
|
||||||
|
import { Greenhouse, GreenhouseInfo, HeatingMode, WateringMode } from '../core/api/data-contracts';
|
||||||
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import GreenhouseDataDisplay from '../components/greenhouse/GreenhouseDataDisplay';
|
||||||
|
import GreenhouseSettingsForm from '../components/greenhouse/GreenhouseSettingsForm';
|
||||||
|
import GreenhouseManager from '../components/greenhouse/GreenhouseManager';
|
||||||
|
import GreenhouseCommandHistoryTable from '../components/greenhouse/GreenhouseCommandTable';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
|
||||||
|
const GreenHousePage = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
let service = new GreenhouseService();
|
||||||
|
|
||||||
|
const [info, setInfo] = useState<GreenhouseInfo>(service.getData(Number(id!)));
|
||||||
|
const [history, setHistory] = useState<HistoryData[]>(service.getCommandsHistory(Number(id!)));
|
||||||
|
|
||||||
|
|
||||||
|
const updateSettings = (settings: Greenhouse) => {
|
||||||
|
const success = service.setSettings(settings);
|
||||||
|
if (success) {
|
||||||
|
alert('Настройки успешно обновлены');
|
||||||
|
} else {
|
||||||
|
alert('Ошибка обновления настроек');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column'}}>
|
||||||
|
<Button type='primary' className='m-2'>
|
||||||
|
<Link to={'/greenhouses/' + id + '/statistics'}>Получить статистику по данной теплице</Link>
|
||||||
|
</Button>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'row'}}>
|
||||||
|
<GreenhouseDataDisplay info={info} />
|
||||||
|
<GreenhouseManager />
|
||||||
|
<GreenhouseSettingsForm updateSettings={updateSettings} id={info.id!} />
|
||||||
|
</div>
|
||||||
|
<GreenhouseCommandHistoryTable history={history} className="m-3" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GreenHousePage;
|
67
cucumber-frontend/src/pages/Login.tsx
Normal file
67
cucumber-frontend/src/pages/Login.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import { Button, Form, Input } from 'antd';
|
||||||
|
import { loginUser } from '../API/api';
|
||||||
|
import ILoginRequest from '../Requests/LoginRequest';
|
||||||
|
import Text from 'antd/es/typography/Text';
|
||||||
|
|
||||||
|
export function LoginPage () {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onFinish = async () => {
|
||||||
|
// Создаём объект с данными для отправки на сервер
|
||||||
|
const userData: ILoginRequest = {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await loginUser(userData);
|
||||||
|
// Сохраняем токен
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
alert('Вы успешно вошли в систему!');
|
||||||
|
navigate('/'); // Перенаправляем на главную страницу
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error logging in:', error);
|
||||||
|
alert('Ошибка при входе в систему. Проверьте введенные данные.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto" style={{ maxWidth: '20%', marginTop: '200px'}}>
|
||||||
|
<Text style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '40px', marginTop: '20px'}}>Вход в систему</Text>
|
||||||
|
<Form onFinish={onFinish}>
|
||||||
|
<Form.Item
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
rules={[{ required: true, type: 'email', message: 'Пожалуйста, введите корректный email!' }]}
|
||||||
|
>
|
||||||
|
<Input value={email} onChange={e => setEmail(e.target.value)} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Пароль"
|
||||||
|
name="password"
|
||||||
|
rules={[{ required: true, min: 8, message: 'Пароль должен содержать не менее 8 символов!' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
Войти
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
<p>
|
||||||
|
Нет аккаунта? <Link to="/register">Зарегистрируйтесь</Link>
|
||||||
|
</p>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user