Compare commits

...

70 Commits

Author SHA1 Message Date
801475654e 0.0.1
Reviewed-on: #9
2024-12-25 23:49:45 +04:00
2dcd5df336 Merge pull request 'front' (#8) from front into dev
Reviewed-on: #8
2024-12-25 23:49:21 +04:00
2febb000ba Отчеты 2024-12-25 23:33:52 +04:00
8715e5949b add: ссылка на статистику 2024-12-24 03:43:41 +04:00
433f6f5926 fix: футер теперь внизу 2024-12-24 03:26:06 +04:00
c1f785eff1 add: добавлена страница теплицы 2024-12-24 03:25:52 +04:00
1c5313a1d2 Merge branch 'front' into front-greenhouse 2024-12-23 20:50:29 +04:00
294bf384b4 add: поддержка swagget-typescript-api. теперь можно генерировать методы API на фронте 2024-12-23 20:48:44 +04:00
c8265a5209 Merge branch 'dev' into front 2024-12-23 19:39:18 +04:00
3393c1be70 демо страницы 2024-12-23 19:36:40 +04:00
2b606be0cc Merge branch 'front' into front-greenhouse 2024-12-23 18:55:22 +04:00
7ed5bc7bc6 Merge pull request 'greenhouseCRUD' (#7) from greenhouseCRUD into dev
Reviewed-on: #7
2024-12-15 20:52:57 +04:00
1c6f0830e3 Merge branch 'devManager' into greenhouseCRUD 2024-12-04 23:25:35 +04:00
aace337052 fix: хз, вроде бы работает, но почему-то не подключается 2024-12-04 23:20:09 +04:00
the
71bc31d0bd containers test 2024-12-04 21:33:02 +04:00
db3cbde0af add: основной круд 2024-12-04 11:41:08 +04:00
7b751b7072 fix: ничего не помогает нормально запросить данные... это конец... 2024-12-04 11:32:34 +04:00
739d434f53 fix: есть контакт! между облаком и кафкой 2024-12-04 06:27:39 +04:00
2e20e9d7cc fix: вынесены важные параметры в переменные окружения 2024-12-04 04:06:56 +04:00
1ddc8d9556 Merge branch 'dev' into greenhouseCRUD 2024-12-04 02:13:27 +04:00
8478bf1a47 add: greenhouse service 2024-12-04 01:57:29 +04:00
9c1720e131 fix: kafka service 2024-12-04 01:57:10 +04:00
m.zargarov
9c0aad605c add Redis service 2024-12-01 19:33:52 +04:00
Аришина)
37abfaf7b2 страница теплиц добавлена 2024-11-20 11:56:13 +04:00
Аришина)
a75a137e35 верстку поменял 2024-11-20 10:32:43 +04:00
fed96d5b86 del: сервисы брокера
они уже есь
2024-11-19 23:42:12 +04:00
9b770d131a Merge branch 'dev' into greenhouseCRUD 2024-11-19 23:35:40 +04:00
c34926e1ec Merge branch 'dev' of https://git.is.ulstu.ru/mfnefd/Cucumber into dev 2024-11-19 23:33:28 +04:00
d03313041c Merge branch 'devManager' into dev 2024-11-19 23:33:16 +04:00
c230d86404 add: сервис брокера, сущности брокера 2024-11-19 23:23:21 +04:00
07c5ea7853 Merge pull request 'brokerService' (#6) from brokerService into dev
Reviewed-on: #6
2024-11-19 19:46:42 +04:00
5e73961ad5 Add Valve Controller 2024-11-19 19:36:05 +04:00
53d93635fc Merge branch 'dev' into greenhouseCRUD 2024-11-19 16:45:19 +04:00
897b6be34a Merge remote-tracking branch 'origin/devManager' into dev 2024-11-19 16:43:03 +04:00
e8a1a8385b Merge branch 'dev' into brokerService 2024-11-13 15:42:26 +04:00
7c310d21f7 Add broker service 2024-11-13 15:24:47 +04:00
5687949f96 todo: в докерфайл добавить инициализацию бд 2024-11-13 03:33:49 +04:00
2a9508f737 add: контроллер теплицы, добавлен сервис брокера в DI 2024-11-13 03:33:03 +04:00
1e2bd05667 add: интерфейсы брокера, сущности топиков брокера 2024-11-13 03:32:30 +04:00
fbfde769b1 add: сущность теплицы 2024-11-13 03:31:43 +04:00
49c7d14925 Merge branch 'FarmCRUD' into dev
mac to ip, dockerfile and docker-compose
2024-11-13 02:08:51 +04:00
3ce4d6baf2 fix: dockerfile и docker-compose 2024-11-13 01:49:13 +04:00
the
5fa9c76b99 test changes 2024-11-13 01:14:32 +04:00
the
c032253699 фикс2 2024-11-13 01:00:16 +04:00
the
83aec339c9 фикс 2024-11-13 00:53:31 +04:00
the
b009ebdd0c АААААААААААА 2024-11-13 00:36:31 +04:00
the
73961934f0 СТОЛЬКО МУЧЕНИЙ РАДИ ОДНОЙ СТРОЧКИ, ЕЩЁ И НЕТВОРК НА САМОМ ДЕЛЕ НЕ РАБОТАЕТ 2024-11-13 00:31:21 +04:00
the
ffef39d409 СТОЛЬКО МУЧЕНИЙ РАДИ ОДНОЙ СТРОЧКИ 2024-11-13 00:30:33 +04:00
the
57e05aba90 kafka network unfix 2024-11-12 22:34:06 +04:00
the
ce3f3a4dc6 kafka network fix 2024-11-12 22:31:57 +04:00
5adec563ac add: dockerfile и изменен чутка docker-compose 2024-11-12 19:57:44 +04:00
488c91d2b1 del: удалены ненужные файлы жесть 2024-11-12 19:48:58 +04:00
03302065ab fix: mac теперь не ипользуется. ip наше все 2024-11-12 19:18:37 +04:00
the
4747f975c5 kafka network 2024-11-12 18:03:45 +04:00
the
f82c8daa92 detector webhooks 2024-11-12 17:04:24 +04:00
the
b7f4aa3f9f webhook fix 2024-11-12 16:19:37 +04:00
the
7f5262575e webhook test 2024-11-12 16:18:14 +04:00
80b002f12a Merge pull request 'add redis' (#3) from add-redis into dev
Reviewed-on: #3
2024-11-10 13:36:59 +04:00
Аришина)
0abb511375 Регистрация и вход работают 2024-10-30 15:31:57 +04:00
3f5bb31646 Менеджер 2024-10-30 11:10:39 +04:00
5be5bb06bb Перенос 2024-10-30 11:06:40 +04:00
1c38c61fbc о 2024-10-29 19:50:35 +04:00
091dcbd3a3 Управление вентилями/нагревателями 2024-10-29 19:49:33 +04:00
the
7f88f87722 fix, no more infinite loops for consumer 2024-10-29 17:51:20 +04:00
the
6291bb483c I MUST JOB 2024-10-29 17:31:47 +04:00
the
08ee12aa8b simple detector + manager 2024-10-29 16:07:02 +04:00
Аришина)
d8aa0bea59 Спидран по фронтенду погнали 2024-10-29 09:21:00 +04:00
the
720cf4bd60 connection test 2024-10-28 19:43:13 +04:00
the
d16968bc98 compose + kafka ui 2024-10-28 14:47:20 +04:00
the
3fa35ba617 init 2024-10-28 13:14:46 +04:00
92 changed files with 35242 additions and 197 deletions

25
.dockerignore Normal file
View 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
View 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
View 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"
}
]
}

41
.vscode/tasks.json vendored Normal file
View 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"
}
]
}

View File

@ -6,6 +6,7 @@ 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)

View File

@ -7,6 +7,7 @@
</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" />

View File

@ -6,123 +6,123 @@ using Microsoft.EntityFrameworkCore;
namespace Cloud.Controllers
{
[Authorize]
[ApiController]
[Route("api/user")]
public class FarmController : ControllerBase
{
private IConfiguration _config;
private ApplicationContext _context;
[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;
}
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");
[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);
}
}
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);
[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");
if (farm == null)
return NotFound("Farm is not found");
return Ok(farm);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
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,
RaspberryMacAddr = farmRequest.RaspberryMacAddr,
};
[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();
Farm? farmCreated = _context.Farms.Add(farm).Entity;
await _context.SaveChangesAsync();
return Ok(farmCreated);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
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);
[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");
if (farm == null)
return NotFound("Farm is not found");
farm.Name = farmRequest.Name;
farm.RaspberryMacAddr = farmRequest.RaspberryMacAddr;
farm.Name = farmRequest.Name;
farm.RaspberryIP = farmRequest.RaspberryIP;
_context.Farms.Update(farm);
await _context.SaveChangesAsync();
_context.Farms.Update(farm);
await _context.SaveChangesAsync();
return Ok(farm);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
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);
[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");
if (farm == null)
return NotFound("Farm is not found");
_context.Farms.Remove(farm);
await _context.SaveChangesAsync();
_context.Farms.Remove(farm);
await _context.SaveChangesAsync();
return Ok("Farm deleted successfully");
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
return Ok("Farm deleted successfully");
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
}

View 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);
}
}
}
}

View 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}");*/
}
}
}

View File

@ -1,32 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Cloud.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}

30
Cloud/Dockerfile Normal file
View 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
View File

@ -0,0 +1,9 @@
namespace Cloud.Enums
{
public enum ValveEnum
{
Open,
Close,
Auto
}
}

View 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);
}
}
}

View File

@ -1,12 +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 RaspberryMacAddr { get; set; }
}
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();
}
}

View 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; }
}
}

View File

@ -0,0 +1,8 @@
namespace Cloud.Models.Support
{
public enum HeatingMode
{
Manual,
Auto
}
}

View File

@ -0,0 +1,8 @@
namespace Cloud.Models.Support
{
public enum WateringMode
{
Manual,
Auto
}
}

View File

@ -7,17 +7,30 @@ 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("localhost:6379");
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>();
@ -37,16 +50,15 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
});
builder.Services.AddDbContext<ApplicationContext>(options =>
options.UseNpgsql("Host=localhost;Port=5438;Database=main_database;Username=postgres;Password=12345"));
// Настройка подключения к БД
builder.Services.AddDbConnectionService();
// Настройка CORS
string frontUrl = Environment.GetEnvironmentVariable("FRONT_URL") ?? "http://localhost:3000";
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontendLocalhost", builder =>
{
builder.WithOrigins("http://localhost:3000") // фронтенд
builder.WithOrigins(frontUrl) // фронтенд
.AllowAnyHeader()
.AllowAnyMethod();
});
@ -58,6 +70,7 @@ 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();
@ -94,6 +107,7 @@ var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
Console.WriteLine("Swagger enabled");
app.UseSwagger();
app.UseSwaggerUI(c =>
{
@ -107,6 +121,9 @@ app.UseHttpsRedirection();
// Включение CORS
app.UseCors("AllowFrontendLocalhost");
// Применение миграций
app.MigrateDb();
app.UseAuthentication();
app.UseAuthorization();

View File

@ -1,8 +1,8 @@
namespace Cloud.Requests
{
public class FarmRequest
{
public string Name { get; set; }
public string RaspberryMacAddr { get; set; }
}
public class FarmRequest
{
public string Name { get; set; }
public string RaspberryIP { get; set; }
}
}

View 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; }
}
}

View File

@ -0,0 +1,7 @@
namespace Cloud.Requests
{
public class ValveRequest
{
public string Action { get; set; }
}
}

View 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;
}
}

View File

@ -0,0 +1,9 @@
using Cloud.Services.Broker.Support;
namespace Cloud.Services.Broker
{
public interface IBrokerProducer
{
Task ProduceAsync(string topic, Command command);
}
}

View File

@ -0,0 +1,9 @@
using Cloud.Services.Broker.Support;
namespace Cloud.Services.Broker
{
public interface IBrokerService : IBrokerProducer, IBrokerConsumer
{
void ChangeBrokerIp(string ip);
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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);
}
}
}

View 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!;
}
}

View 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;
}
}

View 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; }
}
}

View File

@ -0,0 +1,6 @@
namespace Cloud.Services.Broker.Support
{
public interface IBrokerResponse
{
}
}

View 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);
}
}

View 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);
}
}
}

View 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);
}

View 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);
}
}

View 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}");
}
}
}

View File

@ -3,16 +3,16 @@ using FluentValidation;
namespace Cloud.Validation
{
public class FarmValidator : AbstractValidator<FarmRequest>
{
public FarmValidator()
{
RuleFor(request => request.RaspberryMacAddr)
.NotEmpty().WithMessage("MAC address can't be empty")
.Matches("^([0-9A-Fa-f]{2}[:-]?){5}([0-9A-Fa-f]{2})$").WithMessage("MAC address is not valid");
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");
}
}
RuleFor(request => request.Name)
.NotEmpty().WithMessage("Name can't be empty");
}
}
}

View 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");
}
}
}

View File

@ -1,12 +0,0 @@
namespace Cloud;
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}

View File

@ -5,6 +5,10 @@
"Microsoft.AspNetCore": "Warning"
}
},
"Kafka": {
"BootstrapServers": "localhost:9092",
"GroupId": "ValvesHeaters"
},
"AllowedHosts": "*",
"Jwt": {
"Key": "m7TyhE20s0dVtUDAr9EnFdPZnAG8maxgBTaiW5j6kO6RQhWDAGxYmXyu0suDnE0o",

4
Dockerfile Normal file
View 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"]

View 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

View 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"]

View 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)

View 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"]

View 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
View 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*

View 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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View 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"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View 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;
}
};

View 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);
}
}

View 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();
});

View 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;

View File

@ -0,0 +1,6 @@
interface ILoginRequest {
email: string;
password: string;
}
export default ILoginRequest;

View File

@ -0,0 +1,7 @@
interface IRegisterRequest {
name: string;
email: string;
password: string;
}
export default IRegisterRequest;

View File

@ -0,0 +1,8 @@
export function Footer () {
return (
<footer className="footer">
<p>&copy; {new Date().getFullYear()} Cucumber</p>
</footer>
);
};

View 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>
);
};

View 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;

View 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>
);
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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,
});
}

View 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,
}

View 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;
});
};
}

View 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);
}

View 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;
}

View 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>
);

View 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

View 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;

View 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;
}

View 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

View File

@ -0,0 +1,4 @@
export enum HeatingMode {
Manual,
Auto
}

View File

@ -0,0 +1,4 @@
export enum WateringMode {
Manual,
Auto
}

View 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;
}

View 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;

View 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;

View File

@ -0,0 +1,97 @@
import { Link, Navigate } from "react-router-dom";
import { Form, Avatar, Button, Input, Typography } from "antd";
import { useId, useState } from "react";
import React from "react";
export function ProfilePage () {
const token = localStorage.getItem('token');
const [hidden, setHidden] = useState(true);
const [name, setName] = useState('Пользователь');
const [email, setEmail] = useState('user@gmail.com');
const Title = () => <Typography.Title style={{ fontSize: '42px', fontWeight: 'bold', marginBottom: '40px', marginTop: '20px'}}>Ваш профиль</Typography.Title>;
const StyledText = ({ children }: { children: React.ReactNode }) => <Typography.Text strong style={{ fontSize: '24px', marginBottom: '20px' }}>{children}</Typography.Text>;
const StyledValue = ({ children }: { children: React.ReactNode }) => <Typography.Text style={{ fontSize: '24px', marginBottom: '20px' }}>{children}</Typography.Text>;
const User = {
id: 1,
name: name,
email: "user@gmail.com",
password: "12345",
}
function onFinish(name: string, email: string) {
setName(name);
setEmail(email);
}
function onLogout() {
localStorage.removeItem('token');
window.location.href = '/login';
}
return (
<div className="container mx-auto flex" style={{ flexDirection: 'column'}}>
<Title />
<div className="container mx-auto flex" style={{ maxWidth: '40%', marginTop: '100px', flexDirection: 'column'}}>
<div className="container mx-auto flex" style={{ flexDirection: 'column', justifyContent: 'left', alignItems: 'flex-start' }}>
<span style={{ marginLeft: '20px' }}>
<StyledText>Почта:</StyledText>
<StyledValue> {User.email}</StyledValue>
</span>
<span style={{ marginLeft: '20px' }}>
<StyledText>Имя пользователя:</StyledText>
<StyledValue> {User.name}</StyledValue>
</span>
<span style={{ marginLeft: '20px' }}>
<StyledText>Количество теплиц:</StyledText>
<StyledValue> 2</StyledValue>
<Link style={{ marginLeft: '10px', fontSize: '24px', textDecorationLine: 'underline', color: 'blue'}} to="/greenhouses">[Перейти]</Link>
</span>
</div>
<div className="container mx-auto flex" style={{ flexDirection: 'row', justifyContent: 'center', marginTop: '20px'}}>
<Button style={{ marginRight: '10px'}} onClick={() => setHidden(!hidden)}>{hidden ? 'Редактировать' : 'Закончить редактирование'} </Button>
<Button>Выйти</Button>
</div>
<Form style={{ marginTop: '20px', width: '70%' }} layout="vertical" hidden={hidden}>
<Form.Item style={{ width: '100%', justifySelf: 'end', textAlign: 'right' }} name="name" rules={[{ required: false }]}>
Новое имя пользователя: <Input style={{ width: '200px', marginLeft: '10px' }} placeholder={User.name}></Input>
</Form.Item>
<Form.Item style={{ width: '100%', justifySelf: 'end', textAlign: 'right' }} name="email" rules={[{ required: false }]}>
Новая почта: <Input style={{ width: '200px', marginLeft: '10px' }} placeholder={User.email}></Input>
</Form.Item>
<div style={{ textAlign: 'right' }}>
<Button htmlType="submit" onClick={() => onFinish(name, email)}>Сохранить изменения</Button>
</div>
</Form>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '20px'}}>
<Button style={{ width: '160px', backgroundColor: 'red', color: 'white'}} onClick={onLogout}>Выйти из аккаунта</Button>
</div>
</div>
)
}
function async<T>(file: any, File: { new(fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File; prototype: File; }, url: any, string: any, arg4: any) {
throw new Error("Function not implemented.");
}

View File

@ -0,0 +1,72 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Button, Form, Input } from 'antd';
import { } from '../core/api/Api'
import IRegisterRequest from '../Requests/RegisterRequest';
import Text from 'antd/es/typography/Text';
import { createUser } from '../API/api';
export function RegisterPage () {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
const handleFinish = async () => {
// Create the user data object to send to the API
const userData: IRegisterRequest = {
name,
email,
password,
};
try {
if (!navigator.onLine) {
alert('No internet connection');
return;
}
const response = await createUser(userData); // Pass the userData object
if (response) {
navigate('/login'); // Redirect to the login page after successful registration
alert('Пользователь успешно зарегестрирован!');
} else {
console.error('Error registering:');
}
} catch (error) {
console.error('Error registering:', error, userData);
alert('Error registering');
}
};
return (
<div className="container mx-auto" style={{ maxWidth: '20%', marginTop: '200px'}}>
<Text style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '40px', marginTop: '20px'}}>Регистрация</Text>
<Form onFinish={handleFinish}>
<Form.Item label="Имя" name="name" rules={[{ required: true, message: 'Пожалуйста, введите имя!' }]}>
<Input value={name} onChange={e => setName(e.target.value)} />
</Form.Item>
<Form.Item label="Email" name="email" rules={[{ required: true, type: 'email', message: 'Пожалуйста, введите корректный адрес почты!' }]}>
<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>
</Form>
<p>
Уже есть аккаунт? <Link to="/login">Войдите</Link>
</p>
</div>
);
};
export default RegisterPage;

View File

@ -0,0 +1,25 @@
import Title from "antd/es/typography/Title";
export function ReportPage() {
return (
<div style={{ display: 'flex', flexDirection: 'column', marginTop: '200px', marginLeft: '550px' }}>
<Title style={{ marginBottom: '20px', fontSize: '42px', fontWeight: 'bold' }}>Отчеты по теплицам</Title>
<div style={{ marginTop: '20px', display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '100%', maxWidth: '800px' }}>
<div style={{ flex: 1, marginRight: '20px' }}>
<h2>Температура</h2>
<img src="https://i.ibb.co/zhf8s0z/temperature-report.png" alt="temperature-report" />
</div>
<div style={{ flex: 1, marginLeft: '20px' }}>
<h2>Влажность</h2>
<img src="https://i.ibb.co/zhf8s0z/temperature-report.png" alt="humidity-report" />
</div>
<div style={{ flex: 1, marginLeft: '20px' }}>
<h2>Теплицы</h2>
<img src="https://i.ibb.co/zhf8s0z/temperature-report.png" alt="greenhouses-report" />
</div>
</div>
</div>
);
};
export default ReportPage;

View File

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

View File

@ -1,29 +1,141 @@
networks:
vpn:
name: kafkaVPN
driver: bridge
ipam:
config:
- subnet: "192.168.2.0/24"
gateway: "192.168.2.1"
services:
cloud:
networks:
- vpn
build: ./Cloud/
ports:
- "5124:5124"
environment:
ASPNETCORE_ENVIRONMENT: Development
DB_CONNECTION_STRING: ${DB_CONNECTION_STRING}
REDDIS_URL: redis:6379
KAFKA_URL: kafka:29092
# Добавить, когда будет фронт!
# FRONT_URL: front:3000
depends_on:
- postgres
- redis
postgres:
image: postgres:14
container_name: cucumber_database
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: 12345
POSTGRES_DB: main_database
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- "5438:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: 'redis:latest'
ports:
- '6379:6379'
- '6379:6379'
volumes:
- 'cloud-redis:/data'
- 'cloud-redis:/data'
healthcheck:
test:
- CMD
- redis-cli
- ping
retries: 3
timeout: 5s
test:
- CMD
- redis-cli
- ping
retries: 3
timeout: 5s
zookeeper:
networks:
- vpn
image: confluentinc/cp-zookeeper:7.4.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- 2181:2181
kafka:
networks:
vpn:
ipv4_address: 192.168.2.10
image: confluentinc/cp-kafka:7.4.0
ports:
- 9092:9092
- 9997:9997
expose:
- 29092:29092
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENERS: HOST://0.0.0.0:9092,DOCKER://0.0.0.0:29092
KAFKA_ADVERTISED_LISTENERS: HOST://192.168.1.5:9092,DOCKER://kafka:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: DOCKER:PLAINTEXT,HOST:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: DOCKER
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_LOG_FLUSH_INTERVAL_MESSAGES: 10000
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
depends_on:
- zookeeper
init-kafka:
networks:
- vpn
image: confluentinc/cp-kafka:7.4.0
depends_on:
- kafka
entrypoint: [ '/bin/sh', '-c' ]
command: |
"
# blocks until kafka is reachable
kafka-topics --bootstrap-server kafka:29092 --list
echo -e 'Creating kafka topics'
kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic commands --replication-factor 1 --partitions 1
kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic data --replication-factor 1 --partitions 1
kafka-topics --bootstrap-server kafka:29092 --create --if-not-exists --topic response --replication-factor 1 --partitions 1
echo -e 'Successfully created the following topics:'
kafka-topics --bootstrap-server kafka:29092 --list
"
kafka-ui:
networks:
- vpn
container_name: kafka-ui
image: provectuslabs/kafka-ui:latest
ports:
- 8080:8080
depends_on:
- kafka
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092
KAFKA_CLUSTERS_0_METRICS_PORT: 9997
manager:
networks:
- vpn
build:
context: .
dockerfile: ./GreenhouseManager/Dockerfile
environment:
KAFKA_URL: kafka:29092
depends_on:
- kafka
expose:
- 20002
detector:
networks:
- vpn
build:
context: .
dockerfile: ./GreenhouseDetector/Dockerfile
environment:
MANAGER_URL: http://manager:20002
depends_on:
- manager
volumes:
postgres_data:
driver: local

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
kafka-python~=2.0.2
Flask~=3.0.3
requests~=2.31.0