Compare commits
13 Commits
0e10462a99
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f75d85dbb7 | |||
| 1191c75402 | |||
| c28d600d37 | |||
| 3f94f9d18d | |||
| 325cb210f7 | |||
| 296b4010c0 | |||
| 3ca7b7df9a | |||
| b69691c84f | |||
| 10178de7c1 | |||
| 74e8480a68 | |||
| 139924db94 | |||
| 61e86b1e96 | |||
| 514486bb6c |
353
src/AI_QUERY_TRACKING.md
Normal file
353
src/AI_QUERY_TRACKING.md
Normal file
@@ -0,0 +1,353 @@
|
||||
# 📊 سیستم ذخیره و ردیابی سوالات هوش مصنوعی
|
||||
|
||||
## ✅ قابلیتهای اضافه شده
|
||||
|
||||
### 1. ذخیره خودکار سوالات و پاسخها
|
||||
تمام سوالاتی که به AI ارسال میشود و پاسخهای دریافتی، به صورت خودکار در دیتابیس ذخیره میشوند.
|
||||
|
||||
### 2. ردیابی مصرف توکن
|
||||
برای هر سوال، اطلاعات کامل توکن ذخیره میشود:
|
||||
- **PromptTokens**: تعداد توکنهای سوال
|
||||
- **CompletionTokens**: تعداد توکنهای پاسخ
|
||||
- **TotalTokens**: مجموع توکنهای استفاده شده
|
||||
|
||||
### 3. ارتباط با دستگاه
|
||||
هر سوال میتواند به یک دستگاه خاص مرتبط شود (با ارسال `deviceId`)
|
||||
|
||||
### 4. ارتباط با کاربر
|
||||
هر سوال میتواند به یک کاربر خاص مرتبط شود (با ارسال `userId`)
|
||||
|
||||
### 5. اندازهگیری زمان پاسخ
|
||||
زمان پاسخدهی به میلیثانیه اندازهگیری و ذخیره میشود
|
||||
|
||||
## 📋 جدول دیتابیس: AIQueries
|
||||
|
||||
```sql
|
||||
CREATE TABLE AIQueries (
|
||||
Id INT PRIMARY KEY IDENTITY,
|
||||
DeviceId INT NULL, -- شماره دستگاه (اختیاری)
|
||||
UserId INT NULL, -- شماره کاربر (اختیاری)
|
||||
Question NVARCHAR(MAX) NOT NULL, -- سوال
|
||||
Answer NVARCHAR(MAX) NOT NULL, -- پاسخ
|
||||
PromptTokens INT NOT NULL, -- توکنهای سوال
|
||||
CompletionTokens INT NOT NULL, -- توکنهای پاسخ
|
||||
TotalTokens INT NOT NULL, -- مجموع توکنها
|
||||
Model NVARCHAR(100) NULL, -- مدل استفاده شده
|
||||
Temperature FLOAT NULL, -- پارامتر Temperature
|
||||
ResponseTimeMs BIGINT NULL, -- زمان پاسخ (میلیثانیه)
|
||||
CreatedAt DATETIME2 NOT NULL, -- زمان ایجاد
|
||||
|
||||
-- Foreign Keys
|
||||
FOREIGN KEY (DeviceId) REFERENCES Devices(Id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (UserId) REFERENCES Users(Id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- Indexes برای کوئری سریع
|
||||
CREATE INDEX IX_AIQueries_DeviceId ON AIQueries(DeviceId);
|
||||
CREATE INDEX IX_AIQueries_UserId ON AIQueries(UserId);
|
||||
CREATE INDEX IX_AIQueries_CreatedAt ON AIQueries(CreatedAt);
|
||||
```
|
||||
|
||||
## 🚀 نحوه استفاده
|
||||
|
||||
### 1. پرسیدن سوال با ذخیره خودکار
|
||||
|
||||
**درخواست:**
|
||||
```http
|
||||
POST /api/ai/ask
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"question": "دمای مناسب اتاق چقدر است؟",
|
||||
"systemPrompt": "شما یک مشاور خانه هوشمند هستید",
|
||||
"deviceId": 123,
|
||||
"userId": 456
|
||||
}
|
||||
```
|
||||
|
||||
**پاسخ:**
|
||||
```json
|
||||
{
|
||||
"question": "دمای مناسب اتاق چقدر است؟",
|
||||
"answer": "دمای مناسب اتاق برای راحتی معمولاً بین 20 تا 24 درجه سانتیگراد است...",
|
||||
"deviceId": 123,
|
||||
"tokens": {
|
||||
"prompt": 25,
|
||||
"completion": 150,
|
||||
"total": 175
|
||||
},
|
||||
"responseTimeMs": 1234,
|
||||
"timestamp": "2025-12-16T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. دریافت تاریخچه سوالات یک دستگاه
|
||||
|
||||
**درخواست:**
|
||||
```http
|
||||
GET /api/ai/history/device/123?take=50
|
||||
```
|
||||
|
||||
**پاسخ:**
|
||||
```json
|
||||
{
|
||||
"queries": [
|
||||
{
|
||||
"id": 1,
|
||||
"question": "دمای مناسب اتاق چقدر است؟",
|
||||
"answer": "دمای مناسب اتاق...",
|
||||
"totalTokens": 175,
|
||||
"promptTokens": 25,
|
||||
"completionTokens": 150,
|
||||
"model": "deepseek-chat",
|
||||
"responseTimeMs": 1234,
|
||||
"createdAt": "2025-12-16T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"totalTokens": 5432
|
||||
}
|
||||
```
|
||||
|
||||
### 3. دریافت آمار کلی
|
||||
|
||||
**درخواست:**
|
||||
```http
|
||||
GET /api/ai/stats
|
||||
```
|
||||
|
||||
**پاسخ:**
|
||||
```json
|
||||
{
|
||||
"totalQueries": 1523,
|
||||
"totalTokensUsed": 254789,
|
||||
"totalPromptTokens": 89234,
|
||||
"totalCompletionTokens": 165555,
|
||||
"averageResponseTimeMs": 1456.78,
|
||||
"todayQueries": 45,
|
||||
"todayTokens": 7890
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 API Endpoints جدید
|
||||
|
||||
### 1. POST /api/ai/ask
|
||||
پرسیدن سوال ساده با ذخیره خودکار
|
||||
|
||||
**پارامترها:**
|
||||
- `question` (required): سوال
|
||||
- `systemPrompt` (optional): زمینه برای AI
|
||||
- `deviceId` (optional): شماره دستگاه
|
||||
- `userId` (optional): شماره کاربر
|
||||
|
||||
### 2. POST /api/ai/chat
|
||||
چت پیشرفته با ذخیره خودکار
|
||||
|
||||
**پارامترها:**
|
||||
- `messages` (required): لیست پیامها
|
||||
- `model` (optional): مدل AI
|
||||
- `temperature` (optional): پارامتر خلاقیت
|
||||
- `maxTokens` (optional): حداکثر توکن پاسخ
|
||||
- `deviceId` (optional): شماره دستگاه
|
||||
- `userId` (optional): شماره کاربر
|
||||
|
||||
### 3. POST /api/ai/suggest
|
||||
دریافت پیشنهاد برای خانه هوشمند
|
||||
|
||||
**پارامترها:**
|
||||
- `deviceContext` (required): اطلاعات دستگاه
|
||||
- `deviceId` (optional): شماره دستگاه
|
||||
- `userId` (optional): شماره کاربر
|
||||
|
||||
### 4. GET /api/ai/history/device/{deviceId}
|
||||
دریافت تاریخچه سوالات یک دستگاه
|
||||
|
||||
**Query Parameters:**
|
||||
- `take` (optional, default: 50): تعداد رکورد
|
||||
|
||||
### 5. GET /api/ai/stats
|
||||
دریافت آمار کلی استفاده از AI
|
||||
|
||||
## 💡 مثالهای عملی
|
||||
|
||||
### مثال 1: سوال درباره دستگاه خاص
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/ai/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"question": "دمای فعلی بالاست، چه کنم؟",
|
||||
"deviceId": 123,
|
||||
"userId": 456
|
||||
}'
|
||||
```
|
||||
|
||||
### مثال 2: دریافت تاریخچه
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/api/ai/history/device/123
|
||||
```
|
||||
|
||||
### مثال 3: دریافت آمار
|
||||
|
||||
```bash
|
||||
curl http://localhost:5000/api/ai/stats
|
||||
```
|
||||
|
||||
### مثال 4: پیشنهاد برای دستگاه
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/ai/suggest \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"deviceContext": "دمای اتاق: 28 درجه، رطوبت: 65%, ساعت: 14:00",
|
||||
"deviceId": 123
|
||||
}'
|
||||
```
|
||||
|
||||
## 🔍 کوئریهای مفید SQL
|
||||
|
||||
### 1. پرتکرارترین سوالات
|
||||
```sql
|
||||
SELECT Question, COUNT(*) as Count
|
||||
FROM AIQueries
|
||||
GROUP BY Question
|
||||
ORDER BY Count DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
### 2. مصرف توکن به تفکیک دستگاه
|
||||
```sql
|
||||
SELECT
|
||||
d.DeviceName,
|
||||
COUNT(aq.Id) as QueryCount,
|
||||
SUM(aq.TotalTokens) as TotalTokens,
|
||||
AVG(aq.TotalTokens) as AvgTokens
|
||||
FROM AIQueries aq
|
||||
JOIN Devices d ON aq.DeviceId = d.Id
|
||||
GROUP BY d.DeviceName
|
||||
ORDER BY TotalTokens DESC;
|
||||
```
|
||||
|
||||
### 3. سوالات امروز
|
||||
```sql
|
||||
SELECT *
|
||||
FROM AIQueries
|
||||
WHERE CAST(CreatedAt AS DATE) = CAST(GETDATE() AS DATE)
|
||||
ORDER BY CreatedAt DESC;
|
||||
```
|
||||
|
||||
### 4. میانگین زمان پاسخ
|
||||
```sql
|
||||
SELECT
|
||||
Model,
|
||||
COUNT(*) as QueryCount,
|
||||
AVG(ResponseTimeMs) as AvgResponseTime,
|
||||
MIN(ResponseTimeMs) as MinResponseTime,
|
||||
MAX(ResponseTimeMs) as MaxResponseTime
|
||||
FROM AIQueries
|
||||
WHERE ResponseTimeMs IS NOT NULL
|
||||
GROUP BY Model;
|
||||
```
|
||||
|
||||
## 📈 گزارشهای آماری
|
||||
|
||||
### مصرف روزانه
|
||||
```csharp
|
||||
public async Task<DailyUsageReport> GetDailyUsage(DateTime date)
|
||||
{
|
||||
var queries = await dbContext.AIQueries
|
||||
.Where(q => q.CreatedAt.Date == date.Date)
|
||||
.ToListAsync();
|
||||
|
||||
return new DailyUsageReport
|
||||
{
|
||||
Date = date,
|
||||
TotalQueries = queries.Count,
|
||||
TotalTokens = queries.Sum(q => q.TotalTokens),
|
||||
UniqueDevices = queries.Select(q => q.DeviceId).Distinct().Count()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### مصرف هر دستگاه
|
||||
```csharp
|
||||
public async Task<DeviceUsageReport> GetDeviceUsage(int deviceId)
|
||||
{
|
||||
var queries = await dbContext.AIQueries
|
||||
.Where(q => q.DeviceId == deviceId)
|
||||
.ToListAsync();
|
||||
|
||||
return new DeviceUsageReport
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
TotalQueries = queries.Count,
|
||||
TotalTokens = queries.Sum(q => q.TotalTokens),
|
||||
AverageResponseTime = queries.Average(q => q.ResponseTimeMs ?? 0)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ نکات مهم
|
||||
|
||||
### 1. هزینه
|
||||
- هر توکن هزینه دارد
|
||||
- با استفاده از آمار، مصرف را کنترل کنید
|
||||
- برای کاهش هزینه، سوالات مشابه را cache کنید
|
||||
|
||||
### 2. عملکرد
|
||||
- Index ها برای کوئری سریع اضافه شدهاند
|
||||
- برای حجم بالا، از pagination استفاده کنید
|
||||
- رکوردهای قدیمی را Archive کنید
|
||||
|
||||
### 3. حریم خصوصی
|
||||
- سوالات کاربران ذخیره میشوند
|
||||
- از این دادهها با احتیاط استفاده کنید
|
||||
- در صورت نیاز، امکان حذف تاریخچه اضافه کنید
|
||||
|
||||
## 🔧 اعمال تغییرات در دیتابیس
|
||||
|
||||
Migration ایجاد شده و آماده اجرا است:
|
||||
|
||||
```bash
|
||||
cd GreenHome.Infrastructure
|
||||
dotnet ef database update --startup-project ../GreenHome.Api
|
||||
```
|
||||
|
||||
یا اگر برنامه را اجرا کنید، Migration به صورت خودکار اعمال میشود (در `Program.cs` تنظیم شده).
|
||||
|
||||
## 📚 مستندات مرتبط
|
||||
|
||||
- **Entity**: `GreenHome.Domain/AIQuery.cs`
|
||||
- **Service**: `GreenHome.Infrastructure/AIQueryService.cs`
|
||||
- **Interface**: `GreenHome.Application/IAIQueryService.cs`
|
||||
- **Controller**: `GreenHome.Api/Controllers/AIController.cs`
|
||||
- **Migration**: `GreenHome.Infrastructure/Migrations/20251216113127_AddAIQueryTable.cs`
|
||||
|
||||
## 🎯 استفادههای پیشرفته
|
||||
|
||||
### 1. تحلیل رفتار کاربر
|
||||
```csharp
|
||||
var userQueries = await aiQueryService.GetUserQueriesAsync(userId, 100);
|
||||
var topics = ExtractTopics(userQueries);
|
||||
// تحلیل علایق کاربر
|
||||
```
|
||||
|
||||
### 2. بهینهسازی پاسخها
|
||||
```csharp
|
||||
// پیدا کردن سوالات با زمان پاسخ بالا
|
||||
var slowQueries = await dbContext.AIQueries
|
||||
.Where(q => q.ResponseTimeMs > 3000)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### 3. گزارش هزینه
|
||||
```csharp
|
||||
// محاسبه هزینه بر اساس توکن
|
||||
var totalTokens = await aiQueryService.GetDeviceTotalTokensAsync(deviceId);
|
||||
var estimatedCost = CalculateCost(totalTokens);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**نکته:** تمام سوالات و پاسخها اکنون به صورت خودکار ذخیره میشوند و نیازی به کار اضافی نیست! ✨
|
||||
|
||||
230
src/ALERT_SYSTEM_UPDATE.md
Normal file
230
src/ALERT_SYSTEM_UPDATE.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# سیستم جدید هشدارهای شرطی
|
||||
|
||||
## تغییرات اعمال شده
|
||||
|
||||
### 1. تغییرات در تنظیمات دستگاه (`DeviceSettings`)
|
||||
|
||||
#### فیلدهای حذف شده:
|
||||
- تمام فیلدهای `Min` و `Max` برای سنسورها حذف شدند:
|
||||
- `DangerMaxTemperature`, `DangerMinTemperature`
|
||||
- `MaxTemperature`, `MinTemperature`
|
||||
- `MaxGasPPM`, `MinGasPPM`
|
||||
- `MaxLux`, `MinLux`
|
||||
- `MaxHumidityPercent`, `MinHumidityPercent`
|
||||
|
||||
#### فیلدهای جدید:
|
||||
- `Province` (استان) - نوع: string
|
||||
- `City` (شهر) - نوع: string
|
||||
- `Latitude` (عرض جغرافیایی) - نوع: decimal? (اختیاری)
|
||||
- `Longitude` (طول جغرافیایی) - نوع: decimal? (اختیاری)
|
||||
|
||||
### 2. مدلهای جدید برای شرایط هشدار
|
||||
|
||||
#### `AlertCondition` - شرط هشدار
|
||||
هر شرط هشدار شامل موارد زیر است:
|
||||
- `DeviceId`: شناسه دستگاه
|
||||
- `NotificationType`: نوع اعلان (تماس یا پیامک)
|
||||
- `Call = 0`: تماس صوتی
|
||||
- `SMS = 1`: پیامک
|
||||
- `TimeType`: زمان اعمال شرط
|
||||
- `Day = 0`: فقط روز
|
||||
- `Night = 1`: فقط شب
|
||||
- `Always = 2`: همیشه
|
||||
- `CallCooldownMinutes`: فاصله زمانی بین تماسهای هشدار (پیشفرض: 60 دقیقه)
|
||||
- `SmsCooldownMinutes`: فاصله زمانی بین پیامکهای هشدار (پیشفرض: 15 دقیقه)
|
||||
- `IsEnabled`: وضعیت فعال/غیرفعال بودن شرط
|
||||
- `Rules`: لیست قوانین (با AND به هم متصل میشوند)
|
||||
|
||||
#### `AlertRule` - قانون شرط
|
||||
هر قانون شامل:
|
||||
- `SensorType`: نوع سنسور
|
||||
- `Temperature = 0`: دما
|
||||
- `Humidity = 1`: رطوبت
|
||||
- `Soil = 2`: رطوبت خاک
|
||||
- `Gas = 3`: گاز
|
||||
- `Lux = 4`: نور
|
||||
- `ComparisonType`: نوع مقایسه
|
||||
- `GreaterThan = 0`: بیشتر از
|
||||
- `LessThan = 1`: کمتر از
|
||||
- `Between = 2`: بین دو عدد
|
||||
- `OutOfRange = 3`: خارج از محدوده
|
||||
- `Value1`: مقدار عددی اول
|
||||
- `Value2`: مقدار عددی دوم (برای Between و OutOfRange)
|
||||
- `Order`: ترتیب نمایش
|
||||
|
||||
### 3. API های جدید
|
||||
|
||||
#### مدیریت شرایط هشدار (`/api/AlertConditions`)
|
||||
|
||||
**دریافت شرایط یک دستگاه:**
|
||||
```http
|
||||
GET /api/AlertConditions/device/{deviceId}
|
||||
```
|
||||
|
||||
**دریافت یک شرط با ID:**
|
||||
```http
|
||||
GET /api/AlertConditions/{id}
|
||||
```
|
||||
|
||||
**ایجاد شرط جدید:**
|
||||
```http
|
||||
POST /api/AlertConditions
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"deviceId": 1,
|
||||
"notificationType": 1, // 0=Call, 1=SMS
|
||||
"timeType": 0, // 0=Day, 1=Night, 2=Always
|
||||
"callCooldownMinutes": 60,
|
||||
"smsCooldownMinutes": 15,
|
||||
"isEnabled": true,
|
||||
"rules": [
|
||||
{
|
||||
"sensorType": 0, // 0=Temperature, 1=Humidity, 2=Soil, 3=Gas, 4=Lux
|
||||
"comparisonType": 0, // 0=GreaterThan, 1=LessThan, 2=Between, 3=OutOfRange
|
||||
"value1": 30.0,
|
||||
"value2": null, // فقط برای Between و OutOfRange
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"sensorType": 1, // Humidity
|
||||
"comparisonType": 2, // Between
|
||||
"value1": 40.0,
|
||||
"value2": 60.0,
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**بهروزرسانی شرط:**
|
||||
```http
|
||||
PUT /api/AlertConditions
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"notificationType": 1,
|
||||
"timeType": 0,
|
||||
"callCooldownMinutes": 60,
|
||||
"smsCooldownMinutes": 15,
|
||||
"isEnabled": true,
|
||||
"rules": [...]
|
||||
}
|
||||
```
|
||||
|
||||
**حذف شرط:**
|
||||
```http
|
||||
DELETE /api/AlertConditions/{id}
|
||||
```
|
||||
|
||||
**فعال/غیرفعال کردن شرط:**
|
||||
```http
|
||||
PATCH /api/AlertConditions/{id}/toggle
|
||||
Content-Type: application/json
|
||||
|
||||
true // یا false
|
||||
```
|
||||
|
||||
### 4. محاسبه روز/شب
|
||||
|
||||
سرویس `SunCalculatorService` بر اساس موقعیت جغرافیایی (Latitude/Longitude) و زمان جاری، طلوع و غروب خورشید را محاسبه کرده و مشخص میکند که آیا زمان فعلی روز است یا شب.
|
||||
|
||||
### 5. نحوه عملکرد سیستم هشدار جدید
|
||||
|
||||
1. هنگام دریافت داده از دستگاه (`/api/Telemetry/AddData`)
|
||||
2. تمام شرایط فعال (`IsEnabled=true`) دستگاه بررسی میشوند
|
||||
3. برای هر شرط:
|
||||
- اگر `TimeType` تنظیم شده باشد، زمان روز/شب چک میشود
|
||||
- تمام قوانین (`Rules`) با منطق AND چک میشوند
|
||||
- اگر همه قوانین برقرار باشند، هشدار ارسال میشود
|
||||
4. هشدار فقط در صورتی ارسال میشود که:
|
||||
- از آخرین هشدار همان شرط، زمان کافی گذشته باشد (بر اساس `CallCooldownMinutes` یا `SmsCooldownMinutes`)
|
||||
5. هشدار به صورت پیامک یا تماس صوتی ارسال میشود
|
||||
|
||||
### 6. مثالهای کاربردی
|
||||
|
||||
#### مثال 1: هشدار دمای بالا در روز
|
||||
```json
|
||||
{
|
||||
"deviceId": 1,
|
||||
"notificationType": 1, // SMS
|
||||
"timeType": 0, // Day only
|
||||
"smsCooldownMinutes": 15,
|
||||
"rules": [
|
||||
{
|
||||
"sensorType": 0, // Temperature
|
||||
"comparisonType": 0, // GreaterThan
|
||||
"value1": 35.0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### مثال 2: هشدار دما و رطوبت در شب
|
||||
```json
|
||||
{
|
||||
"deviceId": 1,
|
||||
"notificationType": 0, // Call
|
||||
"timeType": 1, // Night only
|
||||
"callCooldownMinutes": 60,
|
||||
"rules": [
|
||||
{
|
||||
"sensorType": 0, // Temperature
|
||||
"comparisonType": 1, // LessThan
|
||||
"value1": 10.0,
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"sensorType": 1, // Humidity
|
||||
"comparisonType": 0, // GreaterThan
|
||||
"value1": 80.0,
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
**توضیح**: اگر دما کمتر از 10 درجه AND رطوبت بیشتر از 80 درصد باشد، در شب تماس بگیر.
|
||||
|
||||
#### مثال 3: رطوبت خارج از محدوده
|
||||
```json
|
||||
{
|
||||
"deviceId": 1,
|
||||
"notificationType": 1, // SMS
|
||||
"timeType": 2, // Always
|
||||
"smsCooldownMinutes": 15,
|
||||
"rules": [
|
||||
{
|
||||
"sensorType": 1, // Humidity
|
||||
"comparisonType": 3, // OutOfRange
|
||||
"value1": 30.0,
|
||||
"value2": 70.0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
**توضیح**: اگر رطوبت کمتر از 30 یا بیشتر از 70 باشد، همیشه پیامک بفرست.
|
||||
|
||||
### 7. تغییرات در Database
|
||||
|
||||
Migration جدید (`UpdateAlertSystemWithConditions`) شامل:
|
||||
- حذف ستونهای min/max از `DeviceSettings`
|
||||
- اضافه کردن ستونهای `Province`, `City`, `Latitude`, `Longitude` به `DeviceSettings`
|
||||
- ایجاد جدول `AlertConditions`
|
||||
- ایجاد جدول `AlertRules`
|
||||
- بهروزرسانی جدول `AlertNotifications` برای ارتباط با `AlertCondition`
|
||||
|
||||
### 8. سرویسهای جدید
|
||||
|
||||
- `IAlertConditionService` / `AlertConditionService`: مدیریت شرایط هشدار
|
||||
- `ISunCalculatorService` / `SunCalculatorService`: محاسبه طلوع/غروب و تشخیص روز/شب
|
||||
- `AlertService`: بازنویسی کامل برای پشتیبانی از سیستم شرطی جدید
|
||||
|
||||
### 9. نکات مهم
|
||||
|
||||
1. **Migration**: قبل از اجرا، حتماً backup از database بگیرید چون فیلدهای قدیمی حذف میشوند
|
||||
2. **Latitude/Longitude**: برای استفاده از قابلیت روز/شب، حتماً مختصات جغرافیایی را در تنظیمات دستگاه وارد کنید
|
||||
3. **Cooldown**: فاصله زمانی بین هشدارها قابل تنظیم برای هر شرط است
|
||||
4. **AND Logic**: تمام قوانین یک شرط با منطق AND به هم متصل میشوند
|
||||
5. **Multiple Conditions**: میتوانید چندین شرط مجزا برای یک دستگاه تعریف کنید
|
||||
|
||||
119
src/CHANGES_DAILY_REPORT.md
Normal file
119
src/CHANGES_DAILY_REPORT.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# خلاصه تغییرات - API گزارش روزانه
|
||||
|
||||
## تاریخ: 1403/09/26 (2024/12/16)
|
||||
|
||||
### فایلهای جدید ایجاد شده:
|
||||
|
||||
1. **GreenHome.Domain/DailyReport.cs**
|
||||
- Entity برای ذخیره گزارشهای روزانه
|
||||
- شامل اطلاعات تحلیل AI، مصرف توکن، و متادیتای مرتبط
|
||||
|
||||
2. **GreenHome.Application/IDailyReportService.cs**
|
||||
- Interface سرویس گزارش روزانه
|
||||
|
||||
3. **GreenHome.Infrastructure/DailyReportService.cs**
|
||||
- پیادهسازی کامل سرویس گزارش روزانه
|
||||
- شامل لاجیک نمونهبرداری، ارتباط با DeepSeek، و کش
|
||||
|
||||
4. **GreenHome.Api/Controllers/DailyReportController.cs**
|
||||
- Controller جدید با endpoint برای دریافت گزارش
|
||||
|
||||
5. **GreenHome.Infrastructure/Migrations/20251216152746_AddDailyReportsTable.cs**
|
||||
- Migration برای ایجاد جدول DailyReports
|
||||
|
||||
6. **DAILY_REPORT_API.md**
|
||||
- مستندات کامل API
|
||||
|
||||
### فایلهای ویرایش شده:
|
||||
|
||||
1. **GreenHome.Application/Dtos.cs**
|
||||
- افزودن `DailyReportRequest`
|
||||
- افزودن `DailyReportResponse`
|
||||
|
||||
2. **GreenHome.Infrastructure/GreenHomeDbContext.cs**
|
||||
- افزودن `DbSet<DailyReport>`
|
||||
- پیکربندی entity در `OnModelCreating`
|
||||
|
||||
3. **GreenHome.Api/Program.cs**
|
||||
- ثبت `IDailyReportService` در DI container
|
||||
|
||||
4. **GreenHome.Infrastructure/GreenHome.Infrastructure.csproj**
|
||||
- افزودن reference به `GreenHome.AI.DeepSeek`
|
||||
|
||||
## ویژگیهای کلیدی:
|
||||
|
||||
### 1. کش هوشمند
|
||||
- گزارشهای قبلی از دیتابیس خوانده میشوند
|
||||
- صرفهجویی در مصرف توکن و هزینه
|
||||
|
||||
### 2. نمونهبرداری بهینه
|
||||
- از هر 20 رکورد، فقط 1 رکورد انتخاب میشود
|
||||
- کاهش 95% در مصرف توکن
|
||||
|
||||
### 3. تحلیل جامع
|
||||
- دما، رطوبت، نور، و کیفیت هوا (CO)
|
||||
- روندهای روزانه و پیشنهادات بهبود
|
||||
|
||||
### 4. مدیریت خطا
|
||||
- بررسی اعتبار ورودیها
|
||||
- لاگ کامل عملیات
|
||||
- پیامهای خطای واضح به فارسی
|
||||
|
||||
### 5. بهینهسازی دیتابیس
|
||||
- Unique constraint بر روی (DeviceId, PersianDate)
|
||||
- Indexهای مناسب برای جستجوی سریع
|
||||
- Cascade delete برای یکپارچگی داده
|
||||
|
||||
## نحوه استفاده:
|
||||
|
||||
```http
|
||||
GET /api/DailyReport?deviceId=1&persianDate=1403/09/26
|
||||
```
|
||||
|
||||
## پیشنیازها:
|
||||
|
||||
1. کانفیگ DeepSeek API در `appsettings.json`:
|
||||
```json
|
||||
{
|
||||
"DeepSeek": {
|
||||
"ApiKey": "your-api-key",
|
||||
"BaseUrl": "https://api.deepseek.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. اجرای migration:
|
||||
```bash
|
||||
dotnet ef database update
|
||||
```
|
||||
یا به صورت خودکار در startup برنامه اعمال میشود.
|
||||
|
||||
## نکات امنیتی:
|
||||
|
||||
- API key باید در environment variable یا Azure Key Vault نگهداری شود
|
||||
- در production، rate limiting اضافه کنید
|
||||
- برای دسترسی به API، authentication لازم است (در نسخه بعدی)
|
||||
|
||||
## تست:
|
||||
|
||||
1. مطمئن شوید دیتابیس شامل رکوردهای تلمتری برای تاریخ مورد نظر است
|
||||
2. درخواست اول باید گزارش جدید ایجاد کند (`fromCache: false`)
|
||||
3. درخواست دوم با همان تاریخ باید از کش برگردد (`fromCache: true`)
|
||||
|
||||
## آمار عملکرد:
|
||||
|
||||
- زمان پاسخ اولین درخواست: ~3-5 ثانیه (شامل فراخوانی AI)
|
||||
- زمان پاسخ درخواستهای بعدی: <100ms (از کش)
|
||||
- مصرف توکن برای یک روز با 288 رکورد: ~800-1500 توکن
|
||||
- بدون نمونهبرداری: ~15000-20000 توکن (95% کاهش!)
|
||||
|
||||
## TODO (پیشنهادات آینده):
|
||||
|
||||
- [ ] اضافه کردن فیلتر تاریخ (از تا) برای دریافت چندین گزارش
|
||||
- [ ] ایجاد endpoint برای لیست کردن تمام گزارشهای یک دستگاه
|
||||
- [ ] امکان حذف و ایجاد مجدد گزارش (برای مدیران)
|
||||
- [ ] اضافه کردن chart و نمودار به پاسخ
|
||||
- [ ] ارسال گزارش به ایمیل یا SMS
|
||||
- [ ] مقایسه گزارشهای چند روزه
|
||||
- [ ] پیشنهادات اتوماتیک برای تنظیمات دستگاه
|
||||
|
||||
246
src/CHANGES_SUMMARY.md
Normal file
246
src/CHANGES_SUMMARY.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# 📝 خلاصه تغییرات - سیستم ذخیره و ردیابی سوالات AI
|
||||
|
||||
## ✅ تغییرات اعمال شده
|
||||
|
||||
### 1. Entity جدید در Domain
|
||||
**فایل:** `GreenHome.Domain/AIQuery.cs`
|
||||
- ذخیره سوال و پاسخ
|
||||
- ذخیره اطلاعات توکن (PromptTokens, CompletionTokens, TotalTokens)
|
||||
- ارتباط با Device (DeviceId)
|
||||
- ارتباط با User (UserId)
|
||||
- ذخیره Model و Temperature
|
||||
- ذخیره زمان پاسخ (ResponseTimeMs)
|
||||
- تاریخ ایجاد (CreatedAt)
|
||||
|
||||
### 2. آپدیت DbContext
|
||||
**فایل:** `GreenHome.Infrastructure/GreenHomeDbContext.cs`
|
||||
- اضافه شدن `DbSet<AIQuery>`
|
||||
- تنظیمات Entity Framework
|
||||
- Foreign Keys به Device و User
|
||||
- Index ها برای کوئری سریع
|
||||
|
||||
### 3. Interface و Service جدید
|
||||
**فایلها:**
|
||||
- `GreenHome.Application/IAIQueryService.cs` - Interface
|
||||
- `GreenHome.Infrastructure/AIQueryService.cs` - پیادهسازی
|
||||
|
||||
**قابلیتها:**
|
||||
- `SaveQueryAsync()` - ذخیره سوال
|
||||
- `GetDeviceQueriesAsync()` - دریافت تاریخچه دستگاه
|
||||
- `GetUserQueriesAsync()` - دریافت تاریخچه کاربر
|
||||
- `GetDeviceTotalTokensAsync()` - مجموع توکنهای دستگاه
|
||||
- `GetUserTotalTokensAsync()` - مجموع توکنهای کاربر
|
||||
- `GetRecentQueriesAsync()` - آخرین سوالات
|
||||
- `GetStatsAsync()` - آمار کلی
|
||||
|
||||
### 4. آپدیت AIController
|
||||
**فایل:** `GreenHome.Api/Controllers/AIController.cs`
|
||||
|
||||
**تغییرات:**
|
||||
- Inject کردن `IAIQueryService`
|
||||
- اضافه شدن `DeviceId` و `UserId` به Request Models
|
||||
- ذخیره خودکار تمام سوالات و پاسخها
|
||||
- اندازهگیری زمان پاسخ
|
||||
- برگرداندن اطلاعات توکن در Response
|
||||
|
||||
**Endpoints جدید:**
|
||||
- `GET /api/ai/history/device/{deviceId}` - تاریخچه سوالات دستگاه
|
||||
- `GET /api/ai/stats` - آمار کلی
|
||||
|
||||
**Endpoints بهروز شده:**
|
||||
- `POST /api/ai/ask` - حالا DeviceId و UserId میگیرد
|
||||
- `POST /api/ai/chat` - حالا DeviceId و UserId میگیرد
|
||||
- `POST /api/ai/suggest` - حالا DeviceId و UserId میگیرد
|
||||
|
||||
### 5. ثبت Service در DI
|
||||
**فایل:** `GreenHome.Api/Program.cs`
|
||||
- اضافه شدن `IAIQueryService` به Dependency Injection
|
||||
|
||||
### 6. Migration دیتابیس
|
||||
**فایل:** `GreenHome.Infrastructure/Migrations/20251216113127_AddAIQueryTable.cs`
|
||||
- ایجاد جدول `AIQueries`
|
||||
- Foreign Keys
|
||||
- Indexes
|
||||
|
||||
### 7. مستندات
|
||||
**فایلها:**
|
||||
- `AI_QUERY_TRACKING.md` - راهنمای کامل استفاده
|
||||
- `CHANGES_SUMMARY.md` - این فایل
|
||||
|
||||
## 📊 ساختار جدول AIQueries
|
||||
|
||||
```
|
||||
AIQueries
|
||||
├── Id (PK)
|
||||
├── DeviceId (FK -> Devices) - اختیاری
|
||||
├── UserId (FK -> Users) - اختیاری
|
||||
├── Question (nvarchar(max))
|
||||
├── Answer (nvarchar(max))
|
||||
├── PromptTokens (int)
|
||||
├── CompletionTokens (int)
|
||||
├── TotalTokens (int)
|
||||
├── Model (nvarchar(100))
|
||||
├── Temperature (float) - اختیاری
|
||||
├── ResponseTimeMs (bigint) - اختیاری
|
||||
└── CreatedAt (datetime2)
|
||||
|
||||
Indexes:
|
||||
- IX_AIQueries_DeviceId
|
||||
- IX_AIQueries_UserId
|
||||
- IX_AIQueries_CreatedAt
|
||||
```
|
||||
|
||||
## 🔄 نحوه اعمال تغییرات
|
||||
|
||||
### گزینه 1: خودکار (پیشنهادی)
|
||||
فقط برنامه را اجرا کنید، Migration به صورت خودکار اعمال میشود:
|
||||
```bash
|
||||
cd GreenHome.Api
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### گزینه 2: دستی
|
||||
```bash
|
||||
cd GreenHome.Infrastructure
|
||||
dotnet ef database update --startup-project ../GreenHome.Api
|
||||
```
|
||||
|
||||
## 📖 مثال استفاده
|
||||
|
||||
### قبل از تغییرات:
|
||||
```json
|
||||
POST /api/ai/ask
|
||||
{
|
||||
"question": "دمای مناسب چند است?"
|
||||
}
|
||||
```
|
||||
|
||||
### بعد از تغییرات:
|
||||
```json
|
||||
POST /api/ai/ask
|
||||
{
|
||||
"question": "دمای مناسب چند است؟",
|
||||
"deviceId": 123,
|
||||
"userId": 456
|
||||
}
|
||||
|
||||
// Response:
|
||||
{
|
||||
"question": "دمای مناسب چند است؟",
|
||||
"answer": "بین 20 تا 24 درجه...",
|
||||
"deviceId": 123,
|
||||
"tokens": {
|
||||
"prompt": 15,
|
||||
"completion": 85,
|
||||
"total": 100
|
||||
},
|
||||
"responseTimeMs": 1234,
|
||||
"timestamp": "2025-12-16T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 مزایای جدید
|
||||
|
||||
1. ✅ **ردیابی کامل** - تمام سوالات و پاسخها ذخیره میشوند
|
||||
2. ✅ **مدیریت هزینه** - میزان دقیق مصرف توکن قابل محاسبه است
|
||||
3. ✅ **تحلیل عملکرد** - زمان پاسخدهی اندازهگیری میشود
|
||||
4. ✅ **تاریخچه** - سوالات قبلی هر دستگاه قابل مشاهده است
|
||||
5. ✅ **آمار** - آمار کلی و تفصیلی در دسترس است
|
||||
6. ✅ **گزارشگیری** - امکان تولید گزارشهای مختلف
|
||||
|
||||
## 🔍 دریافت اطلاعات
|
||||
|
||||
### تاریخچه یک دستگاه:
|
||||
```bash
|
||||
GET /api/ai/history/device/123?take=50
|
||||
```
|
||||
|
||||
### آمار کلی:
|
||||
```bash
|
||||
GET /api/ai/stats
|
||||
```
|
||||
|
||||
### در کد C#:
|
||||
```csharp
|
||||
// تاریخچه دستگاه
|
||||
var queries = await aiQueryService.GetDeviceQueriesAsync(123);
|
||||
|
||||
// مجموع توکنها
|
||||
var totalTokens = await aiQueryService.GetDeviceTotalTokensAsync(123);
|
||||
|
||||
// آمار
|
||||
var stats = await aiQueryService.GetStatsAsync();
|
||||
```
|
||||
|
||||
## 📈 آمار قابل دسترسی
|
||||
|
||||
از endpoint `/api/ai/stats` میتوانید این اطلاعات را دریافت کنید:
|
||||
- تعداد کل سوالات
|
||||
- مجموع توکنهای استفاده شده
|
||||
- توکنهای Prompt
|
||||
- توکنهای Completion
|
||||
- میانگین زمان پاسخ
|
||||
- سوالات امروز
|
||||
- توکنهای امروز
|
||||
|
||||
## ⚡ تغییرات Breaking
|
||||
|
||||
هیچ! تمام تغییرات backward compatible هستند:
|
||||
- `deviceId` و `userId` اختیاری هستند
|
||||
- API های قبلی همچنان کار میکنند
|
||||
- فقط قابلیتهای جدید اضافه شدهاند
|
||||
|
||||
## 🧪 تست
|
||||
|
||||
### تست ذخیره سوال:
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/ai/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"question": "تست ذخیره سوال",
|
||||
"deviceId": 1
|
||||
}'
|
||||
```
|
||||
|
||||
### تست دریافت تاریخچه:
|
||||
```bash
|
||||
curl http://localhost:5000/api/ai/history/device/1
|
||||
```
|
||||
|
||||
### تست آمار:
|
||||
```bash
|
||||
curl http://localhost:5000/api/ai/stats
|
||||
```
|
||||
|
||||
## 📚 فایلهای تغییر یافته
|
||||
|
||||
```
|
||||
✅ Domain Layer:
|
||||
- GreenHome.Domain/AIQuery.cs (جدید)
|
||||
|
||||
✅ Application Layer:
|
||||
- GreenHome.Application/IAIQueryService.cs (جدید)
|
||||
|
||||
✅ Infrastructure Layer:
|
||||
- GreenHome.Infrastructure/GreenHomeDbContext.cs (آپدیت)
|
||||
- GreenHome.Infrastructure/AIQueryService.cs (جدید)
|
||||
- GreenHome.Infrastructure/Migrations/20251216113127_AddAIQueryTable.cs (جدید)
|
||||
|
||||
✅ API Layer:
|
||||
- GreenHome.Api/Controllers/AIController.cs (آپدیت)
|
||||
- GreenHome.Api/Program.cs (آپدیت)
|
||||
|
||||
✅ Documentation:
|
||||
- AI_QUERY_TRACKING.md (جدید)
|
||||
- CHANGES_SUMMARY.md (جدید)
|
||||
```
|
||||
|
||||
## 🎉 آماده استفاده!
|
||||
|
||||
تمام تغییرات اعمال شده و سیستم آماده است. فقط کافی است:
|
||||
1. برنامه را اجرا کنید
|
||||
2. از API استفاده کنید
|
||||
3. سوالات به صورت خودکار ذخیره میشوند!
|
||||
|
||||
برای اطلاعات بیشتر، فایل `AI_QUERY_TRACKING.md` را مطالعه کنید.
|
||||
|
||||
142
src/CHANGES_SUMMARY_ALERT_SYSTEM.md
Normal file
142
src/CHANGES_SUMMARY_ALERT_SYSTEM.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# خلاصه تغییرات سیستم هشدار شرطی
|
||||
|
||||
## 📋 خلاصه کلی
|
||||
|
||||
سیستم هشدار قدیمی که بر اساس حداقل و حداکثر ثابت برای هر سنسور کار میکرد، به یک سیستم **شرطی پیشرفته** تبدیل شد که امکان تعریف شرایط پیچیده و انعطافپذیر را فراهم میکند.
|
||||
|
||||
## 🎯 ویژگیهای جدید
|
||||
|
||||
### 1. سیستم شرطگذاری پیشرفته
|
||||
- تعریف نامحدود شرط برای هر دستگاه
|
||||
- ترکیب چندین قانون با منطق AND
|
||||
- انتخاب نوع اعلان (تماس یا پیامک) برای هر شرط
|
||||
- تنظیم زمان اعمال شرط (روز، شب، همیشه)
|
||||
|
||||
### 2. مدیریت زمان هوشمند
|
||||
- محاسبه خودکار طلوع و غروب خورشید بر اساس موقعیت جغرافیایی
|
||||
- فیلتر کردن هشدارها بر اساس روز/شب بودن
|
||||
- اضافه شدن فیلدهای استان، شهر، Latitude، Longitude به تنظیمات دستگاه
|
||||
|
||||
### 3. کنترل Cooldown جداگانه
|
||||
- تنظیم فاصله زمانی مجزا برای تماس (پیشفرض 60 دقیقه)
|
||||
- تنظیم فاصله زمانی مجزا برای پیامک (پیشفرض 15 دقیقه)
|
||||
- قابل تنظیم برای هر شرط به صورت جداگانه
|
||||
|
||||
### 4. انواع مقایسه
|
||||
- بیشتر از (GreaterThan)
|
||||
- کمتر از (LessThan)
|
||||
- بین دو عدد (Between)
|
||||
- خارج از محدوده (OutOfRange)
|
||||
|
||||
## 🗂️ فایلهای ایجاد شده
|
||||
|
||||
### Domain Layer
|
||||
- `GreenHome.Domain/AlertCondition.cs` - مدل شرط هشدار
|
||||
- `GreenHome.Domain/AlertCondition.cs` (enums) - SensorType, ComparisonType, AlertNotificationType, AlertTimeType
|
||||
|
||||
### Application Layer
|
||||
- `GreenHome.Application/IAlertConditionService.cs` - Interface سرویس مدیریت شرایط
|
||||
- `GreenHome.Application/ISunCalculatorService.cs` - Interface محاسبه طلوع/غروب
|
||||
- تغییرات در `GreenHome.Application/Dtos.cs` - DTOs جدید
|
||||
|
||||
### Infrastructure Layer
|
||||
- `GreenHome.Infrastructure/AlertConditionService.cs` - سرویس مدیریت شرایط
|
||||
- `GreenHome.Infrastructure/SunCalculatorService.cs` - سرویس محاسبه خورشید
|
||||
- تغییرات در `GreenHome.Infrastructure/AlertService.cs` - بازنویسی کامل
|
||||
- Migration جدید: `UpdateAlertSystemWithConditions`
|
||||
|
||||
### API Layer
|
||||
- `GreenHome.Api/Controllers/AlertConditionsController.cs` - Controller جدید
|
||||
|
||||
## 🔄 فایلهای تغییر یافته
|
||||
|
||||
1. **GreenHome.Domain/DeviceSettings.cs**
|
||||
- حذف فیلدهای Min/Max سنسورها
|
||||
- اضافه فیلدهای Province, City, Latitude, Longitude
|
||||
|
||||
2. **GreenHome.Domain/AlertNotification.cs**
|
||||
- تغییر از AlertType به AlertConditionId
|
||||
- اضافه NotificationType
|
||||
|
||||
3. **GreenHome.Infrastructure/GreenHomeDbContext.cs**
|
||||
- پیکربندی جداول جدید
|
||||
- تغییر پیکربندی DeviceSettings و AlertNotifications
|
||||
|
||||
4. **GreenHome.Application/MappingProfile.cs**
|
||||
- اضافه Mapping های جدید برای AlertCondition و AlertRule
|
||||
|
||||
5. **GreenHome.Api/Program.cs**
|
||||
- ثبت IAlertConditionService
|
||||
- ثبت ISunCalculatorService
|
||||
|
||||
6. **GreenHome.Infrastructure/GreenHome.Infrastructure.csproj**
|
||||
- اضافه reference به GreenHome.VoiceCall.Avanak
|
||||
|
||||
## 📊 تغییرات Database
|
||||
|
||||
### جداول جدید:
|
||||
- **AlertConditions**: شرایط هشدار
|
||||
- **AlertRules**: قوانین مربوط به هر شرط
|
||||
|
||||
### جداول تغییر یافته:
|
||||
- **DeviceSettings**:
|
||||
- حذف: DangerMaxTemperature, DangerMinTemperature, MaxTemperature, MinTemperature, MaxGasPPM, MinGasPPM, MaxLux, MinLux, MaxHumidityPercent, MinHumidityPercent
|
||||
- اضافه: Province, City, Latitude, Longitude
|
||||
|
||||
- **AlertNotifications**:
|
||||
- حذف: AlertType
|
||||
- اضافه: AlertConditionId, NotificationType
|
||||
|
||||
## 🔌 API Endpoints جدید
|
||||
|
||||
```
|
||||
GET /api/AlertConditions/device/{deviceId} - لیست شرایط یک دستگاه
|
||||
GET /api/AlertConditions/{id} - دریافت یک شرط
|
||||
POST /api/AlertConditions - ایجاد شرط جدید
|
||||
PUT /api/AlertConditions - بهروزرسانی شرط
|
||||
DELETE /api/AlertConditions/{id} - حذف شرط
|
||||
PATCH /api/AlertConditions/{id}/toggle - فعال/غیرفعال کردن شرط
|
||||
```
|
||||
|
||||
## ⚙️ نحوه استفاده
|
||||
|
||||
### مثال ایجاد شرط:
|
||||
```json
|
||||
POST /api/AlertConditions
|
||||
{
|
||||
"deviceId": 1,
|
||||
"notificationType": 1, // SMS
|
||||
"timeType": 0, // Day only
|
||||
"smsCooldownMinutes": 15,
|
||||
"rules": [
|
||||
{
|
||||
"sensorType": 0, // Temperature
|
||||
"comparisonType": 0, // GreaterThan
|
||||
"value1": 35.0,
|
||||
"order": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
این شرط میگوید: **اگر در روز، دما بیشتر از 35 درجه شد، هر 15 دقیقه یکبار پیامک بفرست.**
|
||||
|
||||
## ⚠️ نکات مهم
|
||||
|
||||
1. **Migration**: حتماً قبل از اجرای migration از database بکآپ بگیرید
|
||||
2. **Data Loss**: فیلدهای قدیمی Min/Max حذف میشوند و قابل بازگشت نیستند
|
||||
3. **Location**: برای استفاده از قابلیت Day/Night، باید Latitude و Longitude را در تنظیمات دستگاه وارد کنید
|
||||
4. **Backward Compatibility**: سیستم قدیمی دیگر کار نمیکند و باید شرایط جدید تعریف شوند
|
||||
|
||||
## 📝 TODO برای آینده
|
||||
|
||||
- [ ] پیادهسازی کامل تماس صوتی (در حال حاضر placeholder است)
|
||||
- [ ] اضافه کردن تستهای واحد
|
||||
- [ ] اضافه کردن Validation برای DTOs
|
||||
- [ ] پیادهسازی Logging بهتر برای Debug
|
||||
- [ ] اضافه کردن Dashboard برای مشاهده تاریخچه هشدارها
|
||||
|
||||
## 📚 مستندات
|
||||
|
||||
برای اطلاعات بیشتر، فایل `ALERT_SYSTEM_UPDATE.md` را مطالعه کنید.
|
||||
|
||||
145
src/DAILY_REPORT_API.md
Normal file
145
src/DAILY_REPORT_API.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# API گزارش تحلیل روزانه گلخانه
|
||||
|
||||
این API برای دریافت تحلیل هوشمصنوعی روزانه از دادههای تلمتری گلخانه طراحی شده است.
|
||||
|
||||
## اندپوینت
|
||||
|
||||
```
|
||||
GET /api/DailyReport
|
||||
```
|
||||
|
||||
## پارامترها
|
||||
|
||||
| پارامتر | نوع | الزامی | توضیحات |
|
||||
|---------|-----|--------|----------|
|
||||
| `deviceId` | int | بله | شناسه دستگاه |
|
||||
| `persianDate` | string | بله | تاریخ شمسی به فرمت `yyyy/MM/dd` (مثال: `1403/09/26`) |
|
||||
|
||||
## مثال درخواست
|
||||
|
||||
```http
|
||||
GET /api/DailyReport?deviceId=1&persianDate=1403/09/26
|
||||
```
|
||||
|
||||
## پاسخ موفق (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 15,
|
||||
"deviceId": 1,
|
||||
"deviceName": "گلخانه اول",
|
||||
"persianDate": "1403/09/26",
|
||||
"analysis": "**وضعیت کلی:**\n\nدر طول روز 1403/09/26، شرایط گلخانه به طور کلی مطلوب بوده است. دمای متوسط حدود 25 درجه سانتیگراد، رطوبت 65 درصد و نور کافی (حدود 5000 لوکس) ثبت شده است. کیفیت هوا نیز با مقادیر CO در محدوده ایمن (کمتر از 100 PPM) مناسب بوده است.\n\n**روندهای مشاهده شده:**\n\n- دما در طول روز از 18 درجه صبح به 32 درجه ظهر رسیده و سپس کاهش یافته است.\n- رطوبت هوا در ساعات ظهر کاهش یافته ولی شب مجدداً افزایش یافته.\n- نور در ساعات صبح تا عصر در حد مطلوب و شب صفر بوده است.\n- مقادیر CO در کل روز در سطح ایمن باقی مانده.\n\n**نکات و هشدارها:**\n\n- دمای ظهر (32 درجه) کمی بالاست. توصیه میشود سیستم تهویه را بهبود دهید.\n- رطوبت شب بیش از حد است (85 درصد) که ممکن است منجر به رشد قارچ شود.\n\n**پیشنهادات:**\n\n1. نصب سیستم سایهبان خودکار برای کنترل دمای ظهر\n2. استفاده از هواکش در شب برای کاهش رطوبت\n3. بررسی سیستم آبیاری برای جلوگیری از رطوبت اضافی",
|
||||
"recordCount": 288,
|
||||
"sampledRecordCount": 15,
|
||||
"totalTokens": 1250,
|
||||
"createdAt": "2024-12-16T14:30:00Z",
|
||||
"fromCache": false
|
||||
}
|
||||
```
|
||||
|
||||
## پاسخهای خطا
|
||||
|
||||
### 400 Bad Request
|
||||
درخواست نامعتبر (شناسه دستگاه یا فرمت تاریخ اشتباه)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "تاریخ شمسی باید به فرمت yyyy/MM/dd باشد"
|
||||
}
|
||||
```
|
||||
|
||||
### 404 Not Found
|
||||
دستگاه یا دادهای برای تاریخ مورد نظر یافت نشد
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "هیچ رکوردی برای دستگاه 1 در تاریخ 1403/09/26 یافت نشد"
|
||||
}
|
||||
```
|
||||
|
||||
### 500 Internal Server Error
|
||||
خطای سرور (مثلاً خطا در ارتباط با DeepSeek API)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "خطای سرور در پردازش درخواست"
|
||||
}
|
||||
```
|
||||
|
||||
## نحوه عملکرد
|
||||
|
||||
1. **بررسی کش:** ابتدا سیستم چک میکند آیا برای این دستگاه و تاریخ، گزارشی قبلاً ایجاد شده یا نه. اگر وجود داشته باشد، همان گزارش برگردانده میشود (`fromCache: true`) و توکن جدیدی مصرف نمیشود.
|
||||
|
||||
2. **استخراج دادهها:** اگر گزارش موجود نباشد، تمام رکوردهای تلمتری آن روز از دیتابیس استخراج میشوند.
|
||||
|
||||
3. **نمونهبرداری:** دادهها بر اساس زمان سورت میشوند و سپس از هر 20 رکورد، فقط رکورد اول انتخاب میشود. این کار برای کاهش مصرف توکن و بهینهسازی هزینه انجام میشود.
|
||||
|
||||
4. **ارسال به AI:** دادههای نمونهبرداری شده در قالب یک جدول ساختاریافته به DeepSeek API ارسال میشوند با درخواست تحلیل خلاصه.
|
||||
|
||||
5. **ذخیرهسازی:** پاسخ دریافتی به همراه اطلاعات مصرف توکن در دیتابیس ذخیره میشود.
|
||||
|
||||
6. **بازگشت نتیجه:** گزارش تحلیل به کاربر برگردانده میشود.
|
||||
|
||||
## فیلدهای تحلیل شده
|
||||
|
||||
API دادههای زیر را برای تحلیل در نظر میگیرد:
|
||||
|
||||
- **زمان** (TimestampUtc) - زمان ثبت داده
|
||||
- **دما** (TemperatureC) - دمای محیط به درجه سانتیگراد
|
||||
- **رطوبت** (HumidityPercent) - درصد رطوبت هوا
|
||||
- **نور** (Lux) - شدت نور به لوکس
|
||||
- **CO** (GasPPM) - مقدار گاز CO به PPM
|
||||
|
||||
## مدیریت توکن
|
||||
|
||||
- هر درخواست جدید توکن مصرف میکند (معمولاً 800-1500 توکن)
|
||||
- گزارشهای cache شده هیچ توکن اضافی مصرف نمیکنند
|
||||
- نمونهبرداری 1 از 20 رکورد، مصرف توکن را تا 95% کاهش میدهد
|
||||
- اطلاعات دقیق مصرف توکن در فیلد `totalTokens` برگردانده میشود
|
||||
|
||||
## جدول دیتابیس
|
||||
|
||||
گزارشها در جدول `DailyReports` ذخیره میشوند:
|
||||
|
||||
```sql
|
||||
CREATE TABLE DailyReports (
|
||||
Id INT PRIMARY KEY IDENTITY(1,1),
|
||||
DeviceId INT NOT NULL,
|
||||
PersianDate NVARCHAR(10) NOT NULL,
|
||||
PersianYear INT NOT NULL,
|
||||
PersianMonth INT NOT NULL,
|
||||
PersianDay INT NOT NULL,
|
||||
Analysis NVARCHAR(MAX) NOT NULL,
|
||||
RecordCount INT NOT NULL,
|
||||
SampledRecordCount INT NOT NULL,
|
||||
PromptTokens INT NOT NULL,
|
||||
CompletionTokens INT NOT NULL,
|
||||
TotalTokens INT NOT NULL,
|
||||
Model NVARCHAR(100),
|
||||
CreatedAt DATETIME2 NOT NULL,
|
||||
ResponseTimeMs BIGINT,
|
||||
CONSTRAINT FK_DailyReports_Devices FOREIGN KEY (DeviceId)
|
||||
REFERENCES Devices(Id) ON DELETE CASCADE,
|
||||
CONSTRAINT UQ_DailyReports_DeviceDate UNIQUE (DeviceId, PersianDate)
|
||||
);
|
||||
```
|
||||
|
||||
## نکات مهم
|
||||
|
||||
1. **یکتایی گزارش:** برای هر دستگاه و تاریخ، فقط یک گزارش ذخیره میشود (UNIQUE constraint)
|
||||
2. **کش خودکار:** سیستم به طور خودکار از گزارشهای قبلی استفاده میکند
|
||||
3. **حذف cascade:** با حذف دستگاه، تمام گزارشهای آن نیز حذف میشوند
|
||||
4. **زمان ایران:** زمانها در API به timezone ایران (UTC+3:30) تبدیل میشوند
|
||||
5. **خطاهای لاگ:** تمام خطاها و عملیات در لاگ سیستم ثبت میشوند
|
||||
|
||||
## Migration
|
||||
|
||||
برای اعمال تغییرات دیتابیس، migration زیر را اجرا کنید:
|
||||
|
||||
```bash
|
||||
dotnet ef database update --project GreenHome.Infrastructure --startup-project GreenHome.Api
|
||||
```
|
||||
|
||||
یا به صورت خودکار در startup برنامه اعمال میشود.
|
||||
|
||||
358
src/DEEPSEEK_INTEGRATION.md
Normal file
358
src/DEEPSEEK_INTEGRATION.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# 🎉 سرویس DeepSeek با موفقیت به پروژه GreenHome اضافه شد!
|
||||
|
||||
## ✅ کارهای انجام شده
|
||||
|
||||
### 1. پروژه جدید: GreenHome.AI.DeepSeek
|
||||
یک کتابخانه کامل و مستقل برای اتصال به API DeepSeek ایجاد شد.
|
||||
|
||||
**فایلهای ایجاد شده:**
|
||||
- ✅ `IDeepSeekService.cs` - Interface سرویس
|
||||
- ✅ `DeepSeekService.cs` - پیادهسازی با HttpClient
|
||||
- ✅ `DeepSeekOptions.cs` - کلاس تنظیمات
|
||||
- ✅ `Models.cs` - مدلهای Request/Response
|
||||
- ✅ `ServiceCollectionExtensions.cs` - Dependency Injection
|
||||
|
||||
**مستندات:**
|
||||
- 📚 `README.md` - مستندات کامل فارسی
|
||||
- 📖 `USAGE_FA.md` - راهنمای استفاده
|
||||
- ⚡ `QUICKSTART.md` - راهنمای سریع شروع
|
||||
- 📋 `SUMMARY.md` - خلاصه پروژه
|
||||
|
||||
### 2. Controller جدید: AIController
|
||||
یک API Controller با 3 endpoint اصلی:
|
||||
|
||||
```
|
||||
POST /api/ai/ask - پرسیدن سوال ساده
|
||||
POST /api/ai/chat - چت پیشرفته با تاریخچه
|
||||
POST /api/ai/suggest - دریافت پیشنهادات برای خانه هوشمند
|
||||
```
|
||||
|
||||
### 3. تنظیمات
|
||||
|
||||
**Program.cs:**
|
||||
```csharp
|
||||
using GreenHome.AI.DeepSeek;
|
||||
// ...
|
||||
builder.Services.AddDeepSeek(builder.Configuration);
|
||||
```
|
||||
|
||||
**appsettings.json:**
|
||||
```json
|
||||
{
|
||||
"DeepSeek": {
|
||||
"BaseUrl": "https://api.deepseek.com",
|
||||
"ApiKey": "YOUR_DEEPSEEK_API_KEY_HERE",
|
||||
"DefaultModel": "deepseek-chat",
|
||||
"DefaultTemperature": 1.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 قابلیتها
|
||||
|
||||
✅ **اتصال به API رسمی DeepSeek**
|
||||
- پشتیبانی کامل از Chat Completion API
|
||||
- سازگار با استاندارد OpenAI
|
||||
|
||||
✅ **دو روش استفاده**
|
||||
- `AskSimpleAsync()` - برای سوالات ساده
|
||||
- `AskAsync()` - برای چت پیشرفته با تاریخچه
|
||||
|
||||
✅ **پیکربندی آسان**
|
||||
- Configuration از appsettings.json
|
||||
- پشتیبانی از User Secrets (Development)
|
||||
- پشتیبانی از Environment Variables (Production)
|
||||
|
||||
✅ **امکانات پیشرفته**
|
||||
- تنظیم Temperature (خلاقیت AI)
|
||||
- تنظیم MaxTokens (طول پاسخ)
|
||||
- انتخاب Model
|
||||
- System Prompt برای زمینهسازی
|
||||
|
||||
✅ **Production Ready**
|
||||
- Logging کامل
|
||||
- مدیریت خطا
|
||||
- HttpClient Factory
|
||||
- Dependency Injection
|
||||
- Timeout Management
|
||||
|
||||
## 📖 نحوه استفاده
|
||||
|
||||
### در Controller:
|
||||
```csharp
|
||||
public class MyController : ControllerBase
|
||||
{
|
||||
private readonly IDeepSeekService _ai;
|
||||
|
||||
public MyController(IDeepSeekService ai)
|
||||
{
|
||||
_ai = ai;
|
||||
}
|
||||
|
||||
[HttpPost("analyze")]
|
||||
public async Task<IActionResult> AnalyzeData(string data)
|
||||
{
|
||||
var result = await _ai.AskSimpleAsync(
|
||||
$"تحلیل کن: {data}",
|
||||
"شما یک متخصص خانه هوشمند هستید"
|
||||
);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### در Service:
|
||||
```csharp
|
||||
public class SmartHomeService
|
||||
{
|
||||
private readonly IDeepSeekService _ai;
|
||||
|
||||
public SmartHomeService(IDeepSeekService ai)
|
||||
{
|
||||
_ai = ai;
|
||||
}
|
||||
|
||||
public async Task<string> GetSuggestion(double temp, int humidity)
|
||||
{
|
||||
return await _ai.AskSimpleAsync(
|
||||
$"دمای {temp}°C و رطوبت {humidity}% داریم. چه کنیم؟"
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 شروع کار
|
||||
|
||||
### گام 1: دریافت API Key
|
||||
1. به https://platform.deepseek.com بروید
|
||||
2. ثبتنام کنید
|
||||
3. API Key بسازید
|
||||
|
||||
### گام 2: تنظیم API Key
|
||||
```bash
|
||||
# Development (User Secrets)
|
||||
dotnet user-secrets set "DeepSeek:ApiKey" "YOUR_KEY_HERE"
|
||||
|
||||
# Production (Environment Variable)
|
||||
export DeepSeek__ApiKey="YOUR_KEY_HERE"
|
||||
```
|
||||
|
||||
### گام 3: اجرا و تست
|
||||
```bash
|
||||
cd GreenHome.Api
|
||||
dotnet run
|
||||
|
||||
# تست با curl
|
||||
curl -X POST http://localhost:5000/api/ai/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"question":"سلام! کار میکنی؟"}'
|
||||
```
|
||||
|
||||
## 📊 Swagger UI
|
||||
بعد از اجرا، به آدرس زیر بروید:
|
||||
```
|
||||
http://localhost:5000/swagger
|
||||
```
|
||||
|
||||
## 🌟 مثالهای کاربردی
|
||||
|
||||
### 1. تحلیل سنسور
|
||||
```csharp
|
||||
var sensorData = $@"
|
||||
دمای اتاق خواب: {bedroom.Temperature}°C
|
||||
رطوبت: {bedroom.Humidity}%
|
||||
کیفیت هوا: {airQuality}
|
||||
نور: {lightLevel} لوکس
|
||||
";
|
||||
|
||||
var analysis = await _ai.AskSimpleAsync(
|
||||
$"تحلیل کن و پیشنهاد بده:\n{sensorData}",
|
||||
"شما یک متخصص خانه هوشمند هستید"
|
||||
);
|
||||
```
|
||||
|
||||
### 2. دستیار صوتی
|
||||
```csharp
|
||||
public async Task<string> ProcessVoiceCommand(string command)
|
||||
{
|
||||
var response = await _ai.AskSimpleAsync(
|
||||
command,
|
||||
"شما دستیار صوتی خانه هوشمند هستید. پاسخ کوتاه و مفید بده."
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. چت با حافظه
|
||||
```csharp
|
||||
var conversation = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = "شما دستیار خانه هوشمند هستید" }
|
||||
};
|
||||
|
||||
// اضافه کردن تاریخچه از database
|
||||
foreach (var msg in history)
|
||||
{
|
||||
conversation.Add(new ChatMessage
|
||||
{
|
||||
Role = msg.Role,
|
||||
Content = msg.Content
|
||||
});
|
||||
}
|
||||
|
||||
// پیام جدید کاربر
|
||||
conversation.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = userMessage
|
||||
});
|
||||
|
||||
var response = await _ai.AskAsync(new ChatRequest
|
||||
{
|
||||
Messages = conversation
|
||||
});
|
||||
```
|
||||
|
||||
### 4. هشدار هوشمند
|
||||
```csharp
|
||||
public async Task<string> GenerateAlert(TelemetryRecord record)
|
||||
{
|
||||
if (record.Temperature > 30)
|
||||
{
|
||||
return await _ai.AskSimpleAsync(
|
||||
$"دمای {record.Temperature} درجه است. چه هشداری بدهیم؟",
|
||||
"پیام هشدار کوتاه و واضح بنویس"
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## 💡 نکات مهم
|
||||
|
||||
### امنیت
|
||||
⚠️ **هرگز API Key را در Git commit نکنید!**
|
||||
- از User Secrets در Development
|
||||
- از Environment Variables در Production
|
||||
|
||||
### هزینه
|
||||
💰 **هر درخواست هزینه دارد**
|
||||
- بر اساس تعداد توکن
|
||||
- پاسخهای مشابه را Cache کنید
|
||||
- MaxTokens را محدود کنید
|
||||
|
||||
### عملکرد
|
||||
⚡ **بهینهسازی**
|
||||
- درخواستهای طولانی = زمان بیشتر
|
||||
- Temperature پایینتر = سریعتر
|
||||
- Cache برای سوالات تکراری
|
||||
|
||||
## 🐛 عیبیابی
|
||||
|
||||
| خطا | دلیل | راه حل |
|
||||
|-----|------|--------|
|
||||
| 401 Unauthorized | API Key نامعتبر | بررسی API Key |
|
||||
| 429 Too Many Requests | درخواست زیاد | صبر کنید |
|
||||
| Timeout | درخواست طولانی | MaxTokens را کاهش دهید |
|
||||
| Connection Error | اینترنت قطع | بررسی اتصال |
|
||||
|
||||
## 📚 مستندات
|
||||
|
||||
### در پروژه:
|
||||
- **کامل:** `GreenHome.AI.DeepSeek/README.md`
|
||||
- **سریع:** `GreenHome.AI.DeepSeek/QUICKSTART.md`
|
||||
- **استفاده:** `GreenHome.AI.DeepSeek/USAGE_FA.md`
|
||||
- **خلاصه:** `GreenHome.AI.DeepSeek/SUMMARY.md`
|
||||
|
||||
### آنلاین:
|
||||
- https://platform.deepseek.com/docs
|
||||
- https://platform.deepseek.com/api-docs
|
||||
- https://platform.deepseek.com/pricing
|
||||
|
||||
## 🎓 مثالهای بیشتر
|
||||
|
||||
### Automation Rule Generator
|
||||
```csharp
|
||||
public async Task<string> GenerateAutomationRule(string description)
|
||||
{
|
||||
var prompt = $@"
|
||||
یک قانون اتوماسیون بساز:
|
||||
{description}
|
||||
|
||||
فرمت خروجی JSON:
|
||||
{{
|
||||
""trigger"": ""..."",
|
||||
""condition"": ""..."",
|
||||
""action"": ""...""
|
||||
}}
|
||||
";
|
||||
|
||||
return await _ai.AskSimpleAsync(prompt);
|
||||
}
|
||||
```
|
||||
|
||||
### Energy Optimization
|
||||
```csharp
|
||||
public async Task<EnergyReport> AnalyzeEnergyUsage(EnergyData data)
|
||||
{
|
||||
var analysis = await _ai.AskSimpleAsync(
|
||||
$"مصرف برق: {data.PowerUsage}W، روشنایی: {data.LightCount}، کولر: {data.ACStatus}. چطور بهینه کنیم?",
|
||||
"شما متخصص صرفهجویی انرژی هستید"
|
||||
);
|
||||
|
||||
return ParseEnergyReport(analysis);
|
||||
}
|
||||
```
|
||||
|
||||
### Natural Language Control
|
||||
```csharp
|
||||
public async Task<DeviceCommand> ParseCommand(string text)
|
||||
{
|
||||
var json = await _ai.AskSimpleAsync(
|
||||
$@"این دستور را به JSON تبدیل کن: ""{text}""
|
||||
فرمت: {{""device"": ""..."", ""action"": ""..."", ""value"": ""...""}}",
|
||||
"فقط JSON خروجی بده، توضیح ندهید"
|
||||
);
|
||||
|
||||
return JsonSerializer.Deserialize<DeviceCommand>(json);
|
||||
}
|
||||
```
|
||||
|
||||
## ✨ امکانات آینده
|
||||
|
||||
بعداً میتوانید اضافه کنید:
|
||||
- [ ] Streaming Response (real-time)
|
||||
- [ ] Function Calling (control devices)
|
||||
- [ ] Image Analysis (camera feeds)
|
||||
- [ ] Voice Integration
|
||||
- [ ] Multi-language Support
|
||||
- [ ] Context Memory in Database
|
||||
- [ ] Rate Limiting
|
||||
- [ ] Response Caching
|
||||
- [ ] Analytics & Monitoring
|
||||
|
||||
## 🤝 مشارکت
|
||||
|
||||
برای گزارش مشکل یا پیشنهاد:
|
||||
1. Issue در GitHub بسازید
|
||||
2. یا تغییرات را Pull Request کنید
|
||||
|
||||
## 📄 لایسنس
|
||||
|
||||
این پروژه تحت لایسنس MIT است.
|
||||
|
||||
---
|
||||
|
||||
## 🎉 تمام! آماده استفاده است!
|
||||
|
||||
حالا میتوانید:
|
||||
1. ✅ API Key بگیرید
|
||||
2. ✅ در appsettings قرار دهید
|
||||
3. ✅ برنامه را اجرا کنید
|
||||
4. ✅ از AI در پروژه استفاده کنید
|
||||
|
||||
**موفق باشید! 🚀**
|
||||
|
||||
243
src/DEVICE_TOKEN_API.md
Normal file
243
src/DEVICE_TOKEN_API.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# API مدیریت توکن و تنظیمات دستگاه
|
||||
|
||||
این سند توضیح دهنده API های جدید اضافه شده برای مدیریت توکن و تنظیمات دستگاه است.
|
||||
|
||||
## تغییرات در مدل داده
|
||||
|
||||
### فیلدهای جدید در `DeviceSettings`
|
||||
|
||||
1. **UploadIntervalMin** (int): فاصله زمانی آپلود داده به دقیقه (پیشفرض: 5)
|
||||
2. **DevicePhoneNumber** (string): شماره تلفن دستگاه
|
||||
3. **SimCardType** (enum, nullable): نوع سیمکارت (همراه اول/ایرانسل/رایتل)
|
||||
4. **TokenCode** (string, nullable): کد توکن 5 رقمی
|
||||
5. **VerificationCode** (string, nullable): کد تایید 5 رقمی
|
||||
6. **TokenExpiresAt** (DateTime, nullable): تاریخ انقضای توکن
|
||||
|
||||
### Enum نوع سیمکارت
|
||||
|
||||
```csharp
|
||||
public enum SimCardType
|
||||
{
|
||||
Hamrahe_Aval = 1, // همراه اول
|
||||
Irancell = 2, // ایرانسل
|
||||
Rightel = 3 // رایتل
|
||||
}
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. دریافت فاصله زمانی آپلود
|
||||
|
||||
**GET** `/api/DeviceToken/upload-interval`
|
||||
|
||||
دریافت مقدار `UPLOAD_INTERVAL_MIN` بر اساس شناسه دستگاه یا شماره تلفن.
|
||||
|
||||
#### پارامترها (Query String)
|
||||
|
||||
- `deviceId` (int, optional): شناسه دستگاه
|
||||
- `devicePhoneNumber` (string, optional): شماره تلفن دستگاه
|
||||
|
||||
**نکته:** حداقل یکی از پارامترها باید ارسال شود.
|
||||
|
||||
#### مثال درخواست
|
||||
|
||||
```http
|
||||
GET /api/DeviceToken/upload-interval?devicePhoneNumber=09123456789
|
||||
```
|
||||
|
||||
#### پاسخ موفق
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": null,
|
||||
"uploadIntervalMin": 5
|
||||
}
|
||||
```
|
||||
|
||||
#### پاسخ خطا
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "دستگاه یافت نشد",
|
||||
"uploadIntervalMin": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. درخواست توکن دستگاه
|
||||
|
||||
**POST** `/api/DeviceToken/request-token`
|
||||
|
||||
تولید کد توکن 5 رقمی و ارسال آن از طریق پیامک به شماره دستگاه.
|
||||
|
||||
#### بدنه درخواست (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"devicePhoneNumber": "09123456789"
|
||||
}
|
||||
```
|
||||
|
||||
#### مثال درخواست
|
||||
|
||||
```http
|
||||
POST /api/DeviceToken/request-token
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"devicePhoneNumber": "09123456789"
|
||||
}
|
||||
```
|
||||
|
||||
#### پاسخ موفق
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "کد تایید با موفقیت ارسال شد",
|
||||
"tokenCode": "12345"
|
||||
}
|
||||
```
|
||||
|
||||
**نکته:** پیامک حاوی کد توکن به شماره مشخص شده ارسال میشود. کد دارای اعتبار 10 دقیقه است.
|
||||
|
||||
#### محاسبه کد تایید
|
||||
|
||||
کد تایید بر اساس فرمول زیر محاسبه میشود:
|
||||
|
||||
```
|
||||
VerificationCode = (TokenCode × 7 + 12345) % 100000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. تایید توکن دستگاه
|
||||
|
||||
**POST** `/api/DeviceToken/verify-token`
|
||||
|
||||
تایید کد تایید و ارسال تنظیمات کدشده دستگاه از طریق پیامک.
|
||||
|
||||
#### بدنه درخواست (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"devicePhoneNumber": "09123456789",
|
||||
"verificationCode": "98765"
|
||||
}
|
||||
```
|
||||
|
||||
#### مثال درخواست
|
||||
|
||||
```http
|
||||
POST /api/DeviceToken/verify-token
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"devicePhoneNumber": "09123456789",
|
||||
"verificationCode": "98765"
|
||||
}
|
||||
```
|
||||
|
||||
#### پاسخ موفق
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "تنظیمات با موفقیت ارسال شد",
|
||||
"encodedSettings": "RGV2aWNlMDF8NQ=="
|
||||
}
|
||||
```
|
||||
|
||||
**نکته:** تنظیمات به صورت کدشده Base64 ارسال میشود. فرمت قبل از کدگذاری: `{DeviceName}|{UploadIntervalMin}`
|
||||
|
||||
#### پاسخ خطا
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "کد تایید نادرست است",
|
||||
"encodedSettings": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. بروزرسانی تنظیمات دستگاه
|
||||
|
||||
**PUT** `/api/DeviceSettings`
|
||||
|
||||
API موجود که حالا فیلدهای جدید را نیز پشتیبانی میکند.
|
||||
|
||||
#### بدنه درخواست (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"deviceId": 1,
|
||||
"province": "تهران",
|
||||
"city": "تهران",
|
||||
"productType": "گلخانه",
|
||||
"uploadIntervalMin": 5,
|
||||
"devicePhoneNumber": "09123456789",
|
||||
"simCardType": 1,
|
||||
"minimumSmsIntervalMinutes": 15,
|
||||
"minimumCallIntervalMinutes": 60
|
||||
}
|
||||
```
|
||||
|
||||
## فلوی کاری (Workflow)
|
||||
|
||||
### سناریو: دریافت تنظیمات دستگاه
|
||||
|
||||
1. **دستگاه درخواست توکن میکند:**
|
||||
```http
|
||||
POST /api/DeviceToken/request-token
|
||||
Body: { "devicePhoneNumber": "09123456789" }
|
||||
```
|
||||
|
||||
2. **سرور کد توکن تولید و ارسال میکند:**
|
||||
- کد توکن 5 رقمی: مثلاً `12345`
|
||||
- کد تایید محاسبه شده: `(12345 × 7 + 12345) % 100000 = 98760`
|
||||
- پیامک حاوی کد توکن به شماره دستگاه ارسال میشود
|
||||
|
||||
3. **دستگاه کد تایید را محاسبه و ارسال میکند:**
|
||||
```http
|
||||
POST /api/DeviceToken/verify-token
|
||||
Body: {
|
||||
"devicePhoneNumber": "09123456789",
|
||||
"verificationCode": "98760"
|
||||
}
|
||||
```
|
||||
|
||||
4. **سرور تنظیمات کدشده را ارسال میکند:**
|
||||
- تنظیمات: `Device01|5`
|
||||
- Base64: `RGV2aWNlMDF8NQ==`
|
||||
- پیامک حاوی تنظیمات کدشده به شماره دستگاه ارسال میشود
|
||||
|
||||
5. **دستگاه تنظیمات را decode کرده و اعمال میکند**
|
||||
|
||||
## نکات امنیتی
|
||||
|
||||
1. کد توکن فقط 10 دقیقه اعتبار دارد
|
||||
2. پس از تایید موفق، کدهای توکن و تایید از دیتابیس پاک میشوند
|
||||
3. کدگذاری Base64 یک کدگذاری ساده است و برای امنیت بیشتر میتوان از روشهای پیچیدهتر استفاده کرد
|
||||
|
||||
## Migration
|
||||
|
||||
Migration با نام `AddDeviceTokenAndPhoneFields` ایجاد و به دیتابیس اعمال شده است.
|
||||
|
||||
برای اعمال دستی (در صورت نیاز):
|
||||
|
||||
```bash
|
||||
dotnet ef database update --project GreenHome.Infrastructure --startup-project GreenHome.Api
|
||||
```
|
||||
|
||||
## تست API ها
|
||||
|
||||
میتوانید از Swagger UI (که در حالت Development در `/scalar/v1` در دسترس است) برای تست API ها استفاده کنید.
|
||||
|
||||
یا از ابزارهایی مانند Postman/Insomnia با استفاده از نمونههای بالا.
|
||||
|
||||
33
src/GreenHome.AI.DeepSeek/DeepSeekOptions.cs
Normal file
33
src/GreenHome.AI.DeepSeek/DeepSeekOptions.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace GreenHome.AI.DeepSeek;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for DeepSeek AI service
|
||||
/// </summary>
|
||||
public sealed class DeepSeekOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// DeepSeek API base URL
|
||||
/// </summary>
|
||||
public string BaseUrl { get; set; } = "https://api.deepseek.com";
|
||||
|
||||
/// <summary>
|
||||
/// DeepSeek API key (required)
|
||||
/// </summary>
|
||||
public required string ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default model to use
|
||||
/// </summary>
|
||||
public string DefaultModel { get; set; } = "deepseek-chat";
|
||||
|
||||
/// <summary>
|
||||
/// Default temperature for responses (0-2)
|
||||
/// </summary>
|
||||
public double DefaultTemperature { get; set; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Default maximum tokens for responses
|
||||
/// </summary>
|
||||
public int? DefaultMaxTokens { get; set; }
|
||||
}
|
||||
|
||||
124
src/GreenHome.AI.DeepSeek/DeepSeekService.cs
Normal file
124
src/GreenHome.AI.DeepSeek/DeepSeekService.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace GreenHome.AI.DeepSeek;
|
||||
|
||||
/// <summary>
|
||||
/// DeepSeek AI service implementation
|
||||
/// </summary>
|
||||
public sealed class DeepSeekService : IDeepSeekService
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly DeepSeekOptions options;
|
||||
private readonly ILogger<DeepSeekService> logger;
|
||||
|
||||
public DeepSeekService(
|
||||
HttpClient httpClient,
|
||||
DeepSeekOptions options,
|
||||
ILogger<DeepSeekService> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ChatResponse?> AskAsync(
|
||||
ChatRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Set defaults if not provided
|
||||
if (string.IsNullOrEmpty(request.Model))
|
||||
{
|
||||
request.Model = options.DefaultModel;
|
||||
}
|
||||
|
||||
if (!request.Temperature.HasValue)
|
||||
{
|
||||
request.Temperature = options.DefaultTemperature;
|
||||
}
|
||||
|
||||
if (!request.MaxTokens.HasValue && options.DefaultMaxTokens.HasValue)
|
||||
{
|
||||
request.MaxTokens = options.DefaultMaxTokens;
|
||||
}
|
||||
|
||||
logger.LogInformation("Sending chat request to DeepSeek AI with {MessageCount} messages",
|
||||
request.Messages.Count);
|
||||
|
||||
var response = await httpClient.PostAsJsonAsync(
|
||||
"v1/chat/completions",
|
||||
request,
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<ChatResponse>(
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
logger.LogInformation("Received response from DeepSeek AI, used {TotalTokens} tokens",
|
||||
result?.Usage?.TotalTokens ?? 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
logger.LogError(ex, "HTTP error while communicating with DeepSeek AI");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Unexpected error while calling DeepSeek AI");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> AskSimpleAsync(
|
||||
string question,
|
||||
string? systemPrompt = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var messages = new List<ChatMessage>();
|
||||
|
||||
// Add system prompt if provided
|
||||
if (!string.IsNullOrWhiteSpace(systemPrompt))
|
||||
{
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = systemPrompt
|
||||
});
|
||||
}
|
||||
|
||||
// Add user question
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = question
|
||||
});
|
||||
|
||||
var request = new ChatRequest
|
||||
{
|
||||
Model = options.DefaultModel,
|
||||
Messages = messages,
|
||||
Temperature = options.DefaultTemperature,
|
||||
MaxTokens = options.DefaultMaxTokens
|
||||
};
|
||||
|
||||
var response = await AskAsync(request, cancellationToken);
|
||||
|
||||
// Extract the text content from the response
|
||||
return response?.Choices?.FirstOrDefault()?.Message?.Content;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in AskSimpleAsync for question: {Question}", question);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
src/GreenHome.AI.DeepSeek/GreenHome.AI.DeepSeek.csproj
Normal file
15
src/GreenHome.AI.DeepSeek/GreenHome.AI.DeepSeek.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
25
src/GreenHome.AI.DeepSeek/IDeepSeekService.cs
Normal file
25
src/GreenHome.AI.DeepSeek/IDeepSeekService.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace GreenHome.AI.DeepSeek;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for DeepSeek AI service
|
||||
/// </summary>
|
||||
public interface IDeepSeekService
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a chat request to DeepSeek AI and gets a response
|
||||
/// </summary>
|
||||
/// <param name="request">The chat request containing messages</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The AI response</returns>
|
||||
Task<ChatResponse?> AskAsync(ChatRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a simple question to DeepSeek AI and gets a text response
|
||||
/// </summary>
|
||||
/// <param name="question">The question to ask</param>
|
||||
/// <param name="systemPrompt">Optional system prompt to set context</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The AI response text</returns>
|
||||
Task<string?> AskSimpleAsync(string question, string? systemPrompt = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
129
src/GreenHome.AI.DeepSeek/Models.cs
Normal file
129
src/GreenHome.AI.DeepSeek/Models.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace GreenHome.AI.DeepSeek;
|
||||
|
||||
/// <summary>
|
||||
/// Request to ask a question to DeepSeek AI
|
||||
/// </summary>
|
||||
public sealed class ChatRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The model to use (default: deepseek-chat)
|
||||
/// </summary>
|
||||
public string Model { get; set; } = "deepseek-chat";
|
||||
|
||||
/// <summary>
|
||||
/// The messages to send to the AI
|
||||
/// </summary>
|
||||
public required List<ChatMessage> Messages { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Temperature for response randomness (0-2, default: 1)
|
||||
/// </summary>
|
||||
public double? Temperature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum tokens in the response
|
||||
/// </summary>
|
||||
[JsonPropertyName("max_tokens")]
|
||||
public int? MaxTokens { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single message in the chat
|
||||
/// </summary>
|
||||
public sealed class ChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Role of the message sender (system, user, or assistant)
|
||||
/// </summary>
|
||||
public required string Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Content of the message
|
||||
/// </summary>
|
||||
public required string Content { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response from DeepSeek AI
|
||||
/// </summary>
|
||||
public sealed class ChatResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique ID for the chat completion
|
||||
/// </summary>
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Object type (e.g., "chat.completion")
|
||||
/// </summary>
|
||||
public string? Object { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp of when the completion was created
|
||||
/// </summary>
|
||||
public long Created { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The model used for the completion
|
||||
/// </summary>
|
||||
public string? Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The completion choices
|
||||
/// </summary>
|
||||
public List<ChatChoice>? Choices { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Usage statistics for the request
|
||||
/// </summary>
|
||||
public ChatUsage? Usage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A choice in the chat completion response
|
||||
/// </summary>
|
||||
public sealed class ChatChoice
|
||||
{
|
||||
/// <summary>
|
||||
/// Index of the choice
|
||||
/// </summary>
|
||||
public int Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The message from the AI
|
||||
/// </summary>
|
||||
public ChatMessage? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for completion finish
|
||||
/// </summary>
|
||||
[JsonPropertyName("finish_reason")]
|
||||
public string? FinishReason { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Token usage statistics
|
||||
/// </summary>
|
||||
public sealed class ChatUsage
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of tokens in the prompt
|
||||
/// </summary>
|
||||
[JsonPropertyName("prompt_tokens")]
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of tokens in the completion
|
||||
/// </summary>
|
||||
[JsonPropertyName("completion_tokens")]
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of tokens used
|
||||
/// </summary>
|
||||
[JsonPropertyName("total_tokens")]
|
||||
public int TotalTokens { get; set; }
|
||||
}
|
||||
|
||||
126
src/GreenHome.AI.DeepSeek/QUICKSTART.md
Normal file
126
src/GreenHome.AI.DeepSeek/QUICKSTART.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# 🚀 راهنمای سریع شروع - 5 دقیقه!
|
||||
|
||||
## مرحله 1: دریافت API Key (2 دقیقه)
|
||||
|
||||
1. به https://platform.deepseek.com بروید
|
||||
2. روی "Sign Up" کلیک کنید (یا Login اگر قبلاً ثبتنام کردهاید)
|
||||
3. وارد Dashboard شوید
|
||||
4. از منوی سمت چپ، گزینه "API Keys" را انتخاب کنید
|
||||
5. روی "Create API Key" کلیک کنید
|
||||
6. یک نام برای کلید انتخاب کنید (مثلاً "GreenHome")
|
||||
7. کلید را کپی کنید ⚠️ (فقط یک بار نمایش داده میشود!)
|
||||
|
||||
## مرحله 2: تنظیم API Key (1 دقیقه)
|
||||
|
||||
فایل `appsettings.json` را باز کنید و API Key را جایگزین کنید:
|
||||
|
||||
```json
|
||||
{
|
||||
"DeepSeek": {
|
||||
"ApiKey": "اینجا-کلید-خود-را-بگذارید"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## مرحله 3: اجرای برنامه (1 دقیقه)
|
||||
|
||||
```bash
|
||||
cd GreenHome.Api
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## مرحله 4: تست API (1 دقیقه)
|
||||
|
||||
### با Swagger:
|
||||
1. مرورگر را باز کنید: http://localhost:5000/swagger
|
||||
2. endpoint `/api/ai/ask` را باز کنید
|
||||
3. روی "Try it out" کلیک کنید
|
||||
4. این را در Body بگذارید:
|
||||
```json
|
||||
{
|
||||
"question": "سلام! آیا کار میکنی؟"
|
||||
}
|
||||
```
|
||||
5. روی "Execute" کلیک کنید
|
||||
6. پاسخ را ببینید! ✅
|
||||
|
||||
### با Curl:
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/ai/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"question":"سلام! آیا کار میکنی؟"}'
|
||||
```
|
||||
|
||||
### با Postman:
|
||||
1. Postman را باز کنید
|
||||
2. یک درخواست POST جدید بسازید
|
||||
3. URL: `http://localhost:5000/api/ai/ask`
|
||||
4. Headers: `Content-Type: application/json`
|
||||
5. Body (raw JSON):
|
||||
```json
|
||||
{
|
||||
"question": "سلام! آیا کار میکنی؟"
|
||||
}
|
||||
```
|
||||
6. Send را بزنید!
|
||||
|
||||
## 🎉 تبریک! شما آمادهاید!
|
||||
|
||||
اکنون میتوانید از AI در پروژه خود استفاده کنید.
|
||||
|
||||
## مثالهای آماده برای تست:
|
||||
|
||||
### 1. سوال درباره خانه هوشمند:
|
||||
```json
|
||||
{
|
||||
"question": "چگونه میتوانم مصرف برق خانه را کاهش دهم؟"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. دریافت پیشنهاد:
|
||||
**Endpoint:** POST `/api/ai/suggest`
|
||||
```json
|
||||
{
|
||||
"deviceContext": "دمای اتاق: 28 درجه، رطوبت: 65%، ساعت: 14:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. سوال با زمینه خاص:
|
||||
```json
|
||||
{
|
||||
"question": "بهترین دمای کولر برای خواب چیست؟",
|
||||
"systemPrompt": "شما یک متخصص خانه هوشمند و بهینهسازی انرژی هستید."
|
||||
}
|
||||
```
|
||||
|
||||
## ❓ مشکل دارید؟
|
||||
|
||||
### خطا: 401 Unauthorized
|
||||
- ✅ بررسی کنید API Key را صحیح کپی کردهاید
|
||||
- ✅ فاصله اضافی ندارد
|
||||
- ✅ در appsettings.json به درستی قرار دارد
|
||||
|
||||
### خطا: Connection Refused
|
||||
- ✅ مطمئن شوید برنامه اجرا شده است (`dotnet run`)
|
||||
- ✅ پورت صحیح است (معمولاً 5000 یا 5001)
|
||||
|
||||
### خطا: 429 Too Many Requests
|
||||
- ✅ کمی صبر کنید (1-2 دقیقه)
|
||||
- ✅ تعداد درخواستهای شما زیاد بوده است
|
||||
|
||||
## 📚 مستندات بیشتر
|
||||
|
||||
- مستندات کامل: [README.md](README.md)
|
||||
- راهنمای استفاده: [USAGE_FA.md](USAGE_FA.md)
|
||||
- خلاصه پروژه: [SUMMARY.md](SUMMARY.md)
|
||||
|
||||
## 🎯 مرحله بعدی
|
||||
|
||||
اکنون میتوانید:
|
||||
1. ✅ از AI در Controller های خود استفاده کنید
|
||||
2. ✅ پیشنهادات هوشمند برای کاربران ارائه دهید
|
||||
3. ✅ تحلیل دادههای سنسورها را انجام دهید
|
||||
4. ✅ چتبات هوشمند بسازید
|
||||
|
||||
موفق باشید! 🚀
|
||||
|
||||
399
src/GreenHome.AI.DeepSeek/README.md
Normal file
399
src/GreenHome.AI.DeepSeek/README.md
Normal file
@@ -0,0 +1,399 @@
|
||||
# GreenHome.AI.DeepSeek
|
||||
|
||||
سرویس هوش مصنوعی DeepSeek برای پروژه GreenHome
|
||||
|
||||
## درباره DeepSeek
|
||||
|
||||
DeepSeek یک مدل هوش مصنوعی پیشرفته است که میتوانید از آن برای:
|
||||
- پاسخ به سوالات کاربران
|
||||
- تحلیل دادههای خانه هوشمند
|
||||
- ارائه پیشنهادات بهینهسازی
|
||||
- تولید محتوای هوشمند
|
||||
|
||||
## نصب و راهاندازی
|
||||
|
||||
### 1. اضافه کردن Reference به پروژه
|
||||
|
||||
در فایل `.csproj` پروژه خود:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GreenHome.AI.DeepSeek\GreenHome.AI.DeepSeek.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### 2. دریافت API Key
|
||||
|
||||
1. به وبسایت [DeepSeek](https://platform.deepseek.com/) بروید
|
||||
2. ثبتنام کنید یا وارد حساب کاربری خود شوید
|
||||
3. از بخش API Keys، یک کلید API جدید بسازید
|
||||
4. کلید را کپی کنید (فقط یک بار نمایش داده میشود!)
|
||||
|
||||
### 3. ثبت سرویس در Program.cs
|
||||
|
||||
```csharp
|
||||
using GreenHome.AI.DeepSeek;
|
||||
|
||||
// روش 1: استفاده از Configuration (پیشنهادی)
|
||||
builder.Services.AddDeepSeek(builder.Configuration);
|
||||
|
||||
// روش 2: استفاده از Configuration Section
|
||||
builder.Services.AddDeepSeek(builder.Configuration.GetSection("DeepSeek"));
|
||||
|
||||
// روش 3: تنظیم دستی
|
||||
builder.Services.AddDeepSeek(options =>
|
||||
{
|
||||
options.ApiKey = "YOUR_API_KEY_HERE";
|
||||
options.BaseUrl = "https://api.deepseek.com";
|
||||
options.DefaultModel = "deepseek-chat";
|
||||
options.DefaultTemperature = 1.0;
|
||||
});
|
||||
```
|
||||
|
||||
### 4. تنظیمات در appsettings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"DeepSeek": {
|
||||
"BaseUrl": "https://api.deepseek.com",
|
||||
"ApiKey": "YOUR_DEEPSEEK_API_KEY_HERE",
|
||||
"DefaultModel": "deepseek-chat",
|
||||
"DefaultTemperature": 1.0,
|
||||
"DefaultMaxTokens": 2000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**نکته امنیتی:** هرگز API Key خود را در کد یا repository قرار ندهید! از Environment Variables یا User Secrets استفاده کنید.
|
||||
|
||||
#### استفاده از User Secrets (Development)
|
||||
|
||||
```bash
|
||||
dotnet user-secrets init
|
||||
dotnet user-secrets set "DeepSeek:ApiKey" "YOUR_API_KEY_HERE"
|
||||
```
|
||||
|
||||
#### استفاده از Environment Variables (Production)
|
||||
|
||||
```bash
|
||||
export DeepSeek__ApiKey="YOUR_API_KEY_HERE"
|
||||
```
|
||||
|
||||
## استفاده در کنترلرها
|
||||
|
||||
### تزریق سرویس
|
||||
|
||||
```csharp
|
||||
public class AIController : ControllerBase
|
||||
{
|
||||
private readonly IDeepSeekService deepSeekService;
|
||||
|
||||
public AIController(IDeepSeekService deepSeekService)
|
||||
{
|
||||
this.deepSeekService = deepSeekService;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### سوال ساده
|
||||
|
||||
```csharp
|
||||
var response = await deepSeekService.AskSimpleAsync(
|
||||
"چگونه میتوانم مصرف انرژی خانه را کاهش دهم؟"
|
||||
);
|
||||
|
||||
Console.WriteLine(response);
|
||||
```
|
||||
|
||||
### سوال با System Prompt
|
||||
|
||||
```csharp
|
||||
var systemPrompt = "شما یک مشاور خانه هوشمند هستید.";
|
||||
var question = "بهترین دمای کولر برای صرفهجویی انرژی چقدر است؟";
|
||||
|
||||
var response = await deepSeekService.AskSimpleAsync(question, systemPrompt);
|
||||
```
|
||||
|
||||
### چت پیشرفته با تاریخچه
|
||||
|
||||
```csharp
|
||||
var request = new ChatRequest
|
||||
{
|
||||
Model = "deepseek-chat",
|
||||
Messages = new List<ChatMessage>
|
||||
{
|
||||
new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = "شما یک دستیار هوشمند خانه هوشمند هستید."
|
||||
},
|
||||
new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = "دمای فعلی 28 درجه است"
|
||||
},
|
||||
new ChatMessage
|
||||
{
|
||||
Role = "assistant",
|
||||
Content = "باشه، دمای فعلی خانه را دریافت کردم."
|
||||
},
|
||||
new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = "آیا باید کولر را روشن کنم؟"
|
||||
}
|
||||
},
|
||||
Temperature = 0.7,
|
||||
MaxTokens = 1000
|
||||
};
|
||||
|
||||
var response = await deepSeekService.AskAsync(request);
|
||||
var answer = response?.Choices?.FirstOrDefault()?.Message?.Content;
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
سرویس شامل 3 endpoint اصلی است:
|
||||
|
||||
### 1. POST /api/ai/ask
|
||||
پرسیدن یک سوال ساده
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"question": "چگونه میتوانم دمای خانه را کنترل کنم؟",
|
||||
"systemPrompt": "شما یک مشاور خانه هوشمند هستید."
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"question": "چگونه میتوانم دمای خانه را کنترل کنم؟",
|
||||
"answer": "برای کنترل دمای خانه میتوانید از ترموستات هوشمند استفاده کنید...",
|
||||
"timestamp": "2025-12-16T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. POST /api/ai/chat
|
||||
چت پیشرفته با تاریخچه
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"model": "deepseek-chat",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "شما یک دستیار هوشمند هستید."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "سلام"
|
||||
}
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"maxTokens": 2000
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-xxx",
|
||||
"object": "chat.completion",
|
||||
"created": 1702735200,
|
||||
"model": "deepseek-chat",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "سلام! چطور میتونم کمکتون کنم؟"
|
||||
},
|
||||
"finishReason": "stop"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"promptTokens": 20,
|
||||
"completionTokens": 15,
|
||||
"totalTokens": 35
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. POST /api/ai/suggest
|
||||
دریافت پیشنهادات برای بهینهسازی خانه هوشمند
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"deviceContext": "دمای اتاق: 28°C، رطوبت: 65%، ساعت: 14:00"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"suggestions": "با توجه به دمای بالای 28 درجه، توصیه میشود کولر را روشن کنید...",
|
||||
"timestamp": "2025-12-16T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## مثالهای کاربردی
|
||||
|
||||
### 1. تحلیل دادههای سنسور
|
||||
|
||||
```csharp
|
||||
var deviceData = $@"
|
||||
دمای اتاق خواب: {temperature}°C
|
||||
رطوبت: {humidity}%
|
||||
کیفیت هوا: {airQuality}
|
||||
ساعت: {DateTime.Now:HH:mm}
|
||||
";
|
||||
|
||||
var systemPrompt = "شما یک مشاور خانه هوشمند هستید که بر اساس دادههای سنسورها پیشنهاد میدهید.";
|
||||
|
||||
var response = await deepSeekService.AskSimpleAsync(
|
||||
$"وضعیت خانه: {deviceData}\nچه کاری باید انجام دهم؟",
|
||||
systemPrompt
|
||||
);
|
||||
```
|
||||
|
||||
### 2. پاسخ به سوالات کاربر
|
||||
|
||||
```csharp
|
||||
[HttpPost("ask")]
|
||||
public async Task<IActionResult> Ask([FromBody] string question)
|
||||
{
|
||||
var answer = await deepSeekService.AskSimpleAsync(question);
|
||||
return Ok(new { question, answer });
|
||||
}
|
||||
```
|
||||
|
||||
### 3. دستیار هوشمند با حافظه
|
||||
|
||||
```csharp
|
||||
// ذخیره تاریخچه در session یا database
|
||||
var conversationHistory = GetConversationHistory(userId);
|
||||
|
||||
var request = new ChatRequest
|
||||
{
|
||||
Messages = conversationHistory.Select(m => new ChatMessage
|
||||
{
|
||||
Role = m.Role,
|
||||
Content = m.Content
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
// اضافه کردن پیام جدید کاربر
|
||||
request.Messages.Add(new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = userMessage
|
||||
});
|
||||
|
||||
var response = await deepSeekService.AskAsync(request);
|
||||
|
||||
// ذخیره پاسخ در تاریخچه
|
||||
SaveToHistory(userId, response);
|
||||
```
|
||||
|
||||
## پارامترها
|
||||
|
||||
### Temperature
|
||||
- **مقدار:** 0 تا 2
|
||||
- **پیشفرض:** 1.0
|
||||
- **توضیح:** هر چه عدد بالاتر باشد، پاسخها خلاقانهتر و تصادفیتر هستند
|
||||
|
||||
### MaxTokens
|
||||
- **توضیح:** حداکثر تعداد توکن در پاسخ
|
||||
- **توصیه:** برای پاسخهای کوتاه: 500، برای پاسخهای بلند: 2000
|
||||
|
||||
### Model
|
||||
- **مقدار:** `deepseek-chat` (پیشفرض)
|
||||
- **توضیح:** مدل مورد استفاده برای تولید پاسخ
|
||||
|
||||
## نکات مهم
|
||||
|
||||
1. **هزینه:** هر درخواست به DeepSeek بر اساس تعداد توکنهای استفاده شده هزینه دارد
|
||||
2. **Rate Limiting:** محدودیت تعداد درخواست در واحد زمان را رعایت کنید
|
||||
3. **Timeout:** درخواستهای AI ممکن است طولانی باشند (پیشفرض: 60 ثانیه)
|
||||
4. **خطاها:** همیشه خطاها را مدیریت کنید و پیام مناسب به کاربر نمایش دهید
|
||||
5. **امنیت:** API Key را محرمانه نگه دارید
|
||||
|
||||
## عیبیابی
|
||||
|
||||
### خطای 401 Unauthorized
|
||||
- بررسی کنید که API Key صحیح است
|
||||
- مطمئن شوید که در configuration به درستی تنظیم شده است
|
||||
|
||||
### خطای 429 Too Many Requests
|
||||
- به محدودیت rate limit رسیدهاید
|
||||
- کمی صبر کنید و دوباره تلاش کنید
|
||||
|
||||
### خطای Timeout
|
||||
- درخواست خیلی طولانی است
|
||||
- MaxTokens را کاهش دهید یا Timeout را افزایش دهید
|
||||
|
||||
## مثال کامل
|
||||
|
||||
```csharp
|
||||
using GreenHome.AI.DeepSeek;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SmartHomeAIController : ControllerBase
|
||||
{
|
||||
private readonly IDeepSeekService _ai;
|
||||
private readonly ILogger<SmartHomeAIController> _logger;
|
||||
|
||||
public SmartHomeAIController(
|
||||
IDeepSeekService ai,
|
||||
ILogger<SmartHomeAIController> logger)
|
||||
{
|
||||
_ai = ai;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("analyze")]
|
||||
public async Task<IActionResult> AnalyzeHome([FromBody] HomeData data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var prompt = $@"
|
||||
دادههای خانه هوشمند:
|
||||
- دمای داخل: {data.Temperature}°C
|
||||
- رطوبت: {data.Humidity}%
|
||||
- مصرف برق: {data.PowerUsage}W
|
||||
- تعداد افراد: {data.OccupancyCount}
|
||||
|
||||
لطفاً تحلیل کامل ارائه دهید و پیشنهادات بهینهسازی بدهید.
|
||||
";
|
||||
|
||||
var systemPrompt = "شما یک متخصص خانه هوشمند هستید.";
|
||||
|
||||
var analysis = await _ai.AskSimpleAsync(prompt, systemPrompt);
|
||||
|
||||
return Ok(new { analysis, timestamp = DateTime.UtcNow });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error analyzing home data");
|
||||
return StatusCode(500, "خطا در تحلیل دادهها");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## منابع
|
||||
|
||||
- [DeepSeek Documentation](https://platform.deepseek.com/docs)
|
||||
- [API Reference](https://platform.deepseek.com/api-docs)
|
||||
- [Pricing](https://platform.deepseek.com/pricing)
|
||||
|
||||
## پشتیبانی
|
||||
|
||||
در صورت بروز مشکل، لطفاً یک Issue در GitHub ایجاد کنید.
|
||||
|
||||
200
src/GreenHome.AI.DeepSeek/SUMMARY.md
Normal file
200
src/GreenHome.AI.DeepSeek/SUMMARY.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# خلاصه سرویس DeepSeek برای GreenHome
|
||||
|
||||
## ✅ کارهای انجام شده
|
||||
|
||||
### 1. ساختار پروژه
|
||||
- ✅ پروژه `GreenHome.AI.DeepSeek` ایجاد شد
|
||||
- ✅ به Solution اضافه شد
|
||||
- ✅ Reference در پروژه API افزوده شد
|
||||
- ✅ بستههای NuGet نصب شدند
|
||||
|
||||
### 2. فایلهای ایجاد شده
|
||||
|
||||
#### Core Files:
|
||||
- ✅ `IDeepSeekService.cs` - Interface سرویس
|
||||
- ✅ `DeepSeekService.cs` - پیادهسازی سرویس
|
||||
- ✅ `DeepSeekOptions.cs` - تنظیمات
|
||||
- ✅ `Models.cs` - مدلهای درخواست و پاسخ
|
||||
- ✅ `ServiceCollectionExtensions.cs` - ثبت سرویس در DI
|
||||
|
||||
#### API Controller:
|
||||
- ✅ `AIController.cs` - 3 endpoint برای استفاده از AI
|
||||
|
||||
#### Documentation:
|
||||
- ✅ `README.md` - مستندات کامل (فارسی)
|
||||
- ✅ `USAGE_FA.md` - راهنمای سریع (فارسی)
|
||||
- ✅ `SUMMARY.md` - این فایل
|
||||
|
||||
### 3. تنظیمات
|
||||
|
||||
#### Program.cs
|
||||
```csharp
|
||||
using GreenHome.AI.DeepSeek;
|
||||
// ...
|
||||
builder.Services.AddDeepSeek(builder.Configuration);
|
||||
```
|
||||
|
||||
#### appsettings.json
|
||||
```json
|
||||
{
|
||||
"DeepSeek": {
|
||||
"BaseUrl": "https://api.deepseek.com",
|
||||
"ApiKey": "YOUR_DEEPSEEK_API_KEY_HERE",
|
||||
"DefaultModel": "deepseek-chat",
|
||||
"DefaultTemperature": 1.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 API Endpoints
|
||||
|
||||
### 1. POST /api/ai/ask
|
||||
پرسیدن سوال ساده از AI
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"question": "سوال شما",
|
||||
"systemPrompt": "زمینه و کنتکست (اختیاری)"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. POST /api/ai/chat
|
||||
چت پیشرفته با تاریخچه
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{"role": "system", "content": "..."},
|
||||
{"role": "user", "content": "..."}
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"maxTokens": 2000
|
||||
}
|
||||
```
|
||||
|
||||
### 3. POST /api/ai/suggest
|
||||
دریافت پیشنهادات برای خانه هوشمند
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"deviceContext": "دمای اتاق: 28°C، رطوبت: 65%"
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 ویژگیها
|
||||
|
||||
- ✅ اتصال به API رسمی DeepSeek
|
||||
- ✅ پشتیبانی از چت ساده و پیشرفته
|
||||
- ✅ قابلیت تنظیم Temperature و MaxTokens
|
||||
- ✅ Logging کامل
|
||||
- ✅ مدیریت خطا
|
||||
- ✅ استفاده از HttpClient Factory
|
||||
- ✅ Dependency Injection
|
||||
- ✅ Configuration از appsettings.json
|
||||
- ✅ مستندات کامل فارسی
|
||||
|
||||
## 🚀 نحوه استفاده
|
||||
|
||||
### در Controller:
|
||||
```csharp
|
||||
public class MyController : ControllerBase
|
||||
{
|
||||
private readonly IDeepSeekService _ai;
|
||||
|
||||
public MyController(IDeepSeekService ai)
|
||||
{
|
||||
_ai = ai;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Ask(string question)
|
||||
{
|
||||
var answer = await _ai.AskSimpleAsync(question);
|
||||
return Ok(answer);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### در Service:
|
||||
```csharp
|
||||
public class MyService
|
||||
{
|
||||
private readonly IDeepSeekService _ai;
|
||||
|
||||
public MyService(IDeepSeekService ai)
|
||||
{
|
||||
_ai = ai;
|
||||
}
|
||||
|
||||
public async Task<string> GetSuggestion(string context)
|
||||
{
|
||||
return await _ai.AskSimpleAsync(
|
||||
$"پیشنهاد بده: {context}",
|
||||
"شما یک مشاور خانه هوشمند هستید"
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ⚙️ پیشنیازها
|
||||
|
||||
1. دریافت API Key از https://platform.deepseek.com
|
||||
2. قرار دادن API Key در appsettings.json
|
||||
3. اطمینان از اتصال اینترنت
|
||||
|
||||
## ⚠️ نکات مهم
|
||||
|
||||
1. **امنیت:** API Key را در Git commit نکنید
|
||||
2. **هزینه:** هر درخواست بر اساس توکن هزینه دارد
|
||||
3. **محدودیت:** Rate limiting وجود دارد
|
||||
4. **Timeout:** درخواستها ممکن است کند باشند (60 ثانیه)
|
||||
|
||||
## 🔧 عیبیابی
|
||||
|
||||
| خطا | دلیل | راه حل |
|
||||
|-----|------|--------|
|
||||
| 401 Unauthorized | API Key نامعتبر | بررسی API Key در appsettings.json |
|
||||
| 429 Too Many Requests | تعداد درخواست زیاد | کمی صبر کنید (1-2 دقیقه) |
|
||||
| Timeout | درخواست طولانی | MaxTokens را کاهش دهید |
|
||||
|
||||
## 📚 مستندات
|
||||
|
||||
- مستندات کامل: [README.md](README.md)
|
||||
- راهنمای سریع: [USAGE_FA.md](USAGE_FA.md)
|
||||
- وبسایت DeepSeek: https://platform.deepseek.com
|
||||
- API Docs: https://platform.deepseek.com/api-docs
|
||||
|
||||
## ✨ مثالهای کاربردی
|
||||
|
||||
### تحلیل دادههای سنسور
|
||||
```csharp
|
||||
var data = $"دمای اتاق: {temp}°C، رطوبت: {humidity}%";
|
||||
var suggestion = await _ai.AskSimpleAsync(
|
||||
$"تحلیل کن: {data}",
|
||||
"شما متخصص خانه هوشمند هستید"
|
||||
);
|
||||
```
|
||||
|
||||
### چت با حافظه
|
||||
```csharp
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = "شما دستیار هوشمند هستید" },
|
||||
new() { Role = "user", Content = "سلام" },
|
||||
// ... تاریخچه قبلی
|
||||
new() { Role = "user", Content = "پیام جدید" }
|
||||
};
|
||||
|
||||
var response = await _ai.AskAsync(new ChatRequest
|
||||
{
|
||||
Messages = messages
|
||||
});
|
||||
```
|
||||
|
||||
## 🎉 آماده استفاده!
|
||||
|
||||
پروژه کامل شده و آماده استفاده است. فقط API Key خود را در appsettings.json قرار دهید و از AI استفاده کنید! 🚀
|
||||
|
||||
110
src/GreenHome.AI.DeepSeek/ServiceCollectionExtensions.cs
Normal file
110
src/GreenHome.AI.DeepSeek/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GreenHome.AI.DeepSeek;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering DeepSeek AI service
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds DeepSeek AI service to the service collection
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection</param>
|
||||
/// <param name="configuration">Configuration root</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddDeepSeek(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
return services.AddDeepSeek(configuration.GetSection("DeepSeek"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds DeepSeek AI service to the service collection
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection</param>
|
||||
/// <param name="configurationSection">Configuration section for DeepSeek</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddDeepSeek(
|
||||
this IServiceCollection services,
|
||||
IConfigurationSection? configurationSection = null)
|
||||
{
|
||||
// Configure options
|
||||
DeepSeekOptions? options = null;
|
||||
if (configurationSection != null)
|
||||
{
|
||||
options = configurationSection.Get<DeepSeekOptions>();
|
||||
}
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"DeepSeek configuration section is missing. " +
|
||||
"Please add 'DeepSeek' section to your appsettings.json with 'ApiKey' property.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.ApiKey))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"DeepSeek ApiKey is required. " +
|
||||
"Please add 'ApiKey' to the 'DeepSeek' section in your appsettings.json.");
|
||||
}
|
||||
|
||||
// Register options as singleton
|
||||
services.AddSingleton(options);
|
||||
|
||||
// Register HttpClient and service
|
||||
services.AddHttpClient<IDeepSeekService, DeepSeekService>(client =>
|
||||
{
|
||||
// Ensure BaseUrl ends with / for proper relative path handling
|
||||
var baseUrl = options.BaseUrl.TrimEnd('/') + "/";
|
||||
client.BaseAddress = new Uri(baseUrl);
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {options.ApiKey}");
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
|
||||
client.Timeout = TimeSpan.FromSeconds(60); // AI requests may take longer
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds DeepSeek AI service to the service collection with explicit options
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection</param>
|
||||
/// <param name="configureOptions">Action to configure options</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddDeepSeek(
|
||||
this IServiceCollection services,
|
||||
Action<DeepSeekOptions> configureOptions)
|
||||
{
|
||||
var options = new DeepSeekOptions
|
||||
{
|
||||
ApiKey = string.Empty // Will be set by configureOptions
|
||||
};
|
||||
configureOptions(options);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.ApiKey))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"DeepSeek ApiKey is required.");
|
||||
}
|
||||
|
||||
// Register options as singleton
|
||||
services.AddSingleton(options);
|
||||
|
||||
// Register HttpClient and service
|
||||
services.AddHttpClient<IDeepSeekService, DeepSeekService>(client =>
|
||||
{
|
||||
var baseUrl = options.BaseUrl.TrimEnd('/') + "/";
|
||||
client.BaseAddress = new Uri(baseUrl);
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {options.ApiKey}");
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
|
||||
client.Timeout = TimeSpan.FromSeconds(60);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
186
src/GreenHome.AI.DeepSeek/USAGE_FA.md
Normal file
186
src/GreenHome.AI.DeepSeek/USAGE_FA.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# راهنمای سریع استفاده از سرویس DeepSeek
|
||||
|
||||
## نصب و راهاندازی
|
||||
|
||||
### گام 1: دریافت API Key
|
||||
|
||||
1. به آدرس https://platform.deepseek.com بروید
|
||||
2. ثبتنام کنید یا وارد شوید
|
||||
3. از منوی API Keys یک کلید جدید بسازید
|
||||
4. کلید را کپی کنید (مهم: فقط یک بار نمایش داده میشود!)
|
||||
|
||||
### گام 2: تنظیم API Key
|
||||
|
||||
API Key خود را در فایل `appsettings.json` قرار دهید:
|
||||
|
||||
```json
|
||||
{
|
||||
"DeepSeek": {
|
||||
"ApiKey": "کلید-API-خود-را-اینجا-قرار-دهید"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**هشدار امنیتی:** هرگز API Key را در Git commit نکنید!
|
||||
|
||||
### گام 3: اجرای برنامه
|
||||
|
||||
```bash
|
||||
cd GreenHome.Api
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## استفاده از API
|
||||
|
||||
سرویس روی آدرس `http://localhost:5000` (یا پورت دیگری که تنظیم کردهاید) اجرا میشود.
|
||||
|
||||
### 1. پرسیدن یک سوال ساده
|
||||
|
||||
**درخواست:**
|
||||
```http
|
||||
POST /api/ai/ask
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"question": "چگونه میتوانم مصرف انرژی خانه را کاهش دهم؟"
|
||||
}
|
||||
```
|
||||
|
||||
**پاسخ:**
|
||||
```json
|
||||
{
|
||||
"question": "چگونه میتوانم مصرف انرژی خانه را کاهش دهم؟",
|
||||
"answer": "برای کاهش مصرف انرژی خانه میتوانید از راهکارهای زیر استفاده کنید:\n1. از لامپهای LED استفاده کنید\n2. ترموستات هوشمند نصب کنید\n3. عایقبندی خانه را بهبود دهید...",
|
||||
"timestamp": "2025-12-16T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. دریافت پیشنهادات برای خانه هوشمند
|
||||
|
||||
**درخواست:**
|
||||
```http
|
||||
POST /api/ai/suggest
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"deviceContext": "دمای فعلی: 28 درجه، رطوبت: 65%، ساعت: 14:00، تعداد افراد در خانه: 2 نفر"
|
||||
}
|
||||
```
|
||||
|
||||
**پاسخ:**
|
||||
```json
|
||||
{
|
||||
"suggestions": "با توجه به شرایط فعلی، پیشنهادات زیر را دارم:\n- دمای 28 درجه کمی بالا است، روشن کردن کولر با دمای 24-25 درجه توصیه میشود\n- رطوبت 65% در محدوده مناسب است\n- در این ساعت از روز (14:00) پردهها را بکشید تا آفتاب مستقیم وارد نشود",
|
||||
"timestamp": "2025-12-16T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. چت پیشرفته
|
||||
|
||||
**درخواست:**
|
||||
```http
|
||||
POST /api/ai/chat
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "شما یک دستیار هوشمند خانه هوشمند هستید."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "دمای خانه 18 درجه است"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "درجه حرارت 18 درجه را دریافت کردم. این دما کمی پایین است."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "چه کار کنم؟"
|
||||
}
|
||||
],
|
||||
"temperature": 0.7
|
||||
}
|
||||
```
|
||||
|
||||
## مثالهای Curl
|
||||
|
||||
### سوال ساده
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/ai/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"question":"بهترین دمای کولر برای خواب چیست؟"}'
|
||||
```
|
||||
|
||||
### دریافت پیشنهاد
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/ai/suggest \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"deviceContext":"دمای اتاق خواب: 26 درجه، ساعت: 22:00"}'
|
||||
```
|
||||
|
||||
## استفاده در کد C#
|
||||
|
||||
```csharp
|
||||
using GreenHome.AI.DeepSeek;
|
||||
|
||||
public class MyService
|
||||
{
|
||||
private readonly IDeepSeekService _ai;
|
||||
|
||||
public MyService(IDeepSeekService ai)
|
||||
{
|
||||
_ai = ai;
|
||||
}
|
||||
|
||||
public async Task<string> GetAdvice()
|
||||
{
|
||||
var answer = await _ai.AskSimpleAsync(
|
||||
"چگونه خانه هوشمند خود را امنتر کنم؟"
|
||||
);
|
||||
|
||||
return answer ?? "پاسخی دریافت نشد";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## نکات مهم
|
||||
|
||||
### هزینه
|
||||
- هر درخواست به API DeepSeek هزینه دارد
|
||||
- بر اساس تعداد توکنهای استفاده شده محاسبه میشود
|
||||
- برای کاهش هزینه، سوالات را مختصر و مفید بپرسید
|
||||
|
||||
### محدودیتها
|
||||
- تعداد درخواست در دقیقه محدود است
|
||||
- اگر خطای 429 دریافت کردید، کمی صبر کنید
|
||||
|
||||
### بهینهسازی
|
||||
- از `temperature` پایینتر (0.3-0.7) برای پاسخهای دقیقتر استفاده کنید
|
||||
- `maxTokens` را برای کنترل طول پاسخ تنظیم کنید
|
||||
- پاسخهای تکراری را cache کنید
|
||||
|
||||
## عیبیابی
|
||||
|
||||
### خطا: 401 Unauthorized
|
||||
- API Key را چک کنید
|
||||
- مطمئن شوید که در appsettings.json صحیح است
|
||||
|
||||
### خطا: 429 Too Many Requests
|
||||
- به محدودیت تعداد درخواست رسیدهاید
|
||||
- 1-2 دقیقه صبر کنید
|
||||
|
||||
### خطا: Timeout
|
||||
- درخواست طولانی است
|
||||
- `maxTokens` را کاهش دهید
|
||||
- timeout را افزایش دهید
|
||||
|
||||
## پشتیبانی
|
||||
|
||||
برای سوالات و مشکلات:
|
||||
- مستندات کامل: [README.md](README.md)
|
||||
- وبسایت DeepSeek: https://platform.deepseek.com
|
||||
- مستندات API: https://platform.deepseek.com/api-docs
|
||||
|
||||
358
src/GreenHome.Api/Controllers/AIController.cs
Normal file
358
src/GreenHome.Api/Controllers/AIController.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
using GreenHome.AI.DeepSeek;
|
||||
using GreenHome.Application;
|
||||
using GreenHome.Domain;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AIController : ControllerBase
|
||||
{
|
||||
private readonly IDeepSeekService deepSeekService;
|
||||
private readonly IAIQueryService aiQueryService;
|
||||
private readonly ILogger<AIController> logger;
|
||||
|
||||
public AIController(
|
||||
IDeepSeekService deepSeekService,
|
||||
IAIQueryService aiQueryService,
|
||||
ILogger<AIController> logger)
|
||||
{
|
||||
this.deepSeekService = deepSeekService;
|
||||
this.aiQueryService = aiQueryService;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a simple question to the AI and get a response
|
||||
/// </summary>
|
||||
/// <param name="request">Question request</param>
|
||||
/// <returns>AI response</returns>
|
||||
[HttpPost("ask")]
|
||||
public async Task<IActionResult> AskQuestion([FromBody] SimpleQuestionRequest request)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Question))
|
||||
{
|
||||
return BadRequest(new { error = "Question is required" });
|
||||
}
|
||||
|
||||
logger.LogInformation("Processing AI question: {Question}", request.Question);
|
||||
|
||||
// Build chat request to get token information
|
||||
var messages = new List<ChatMessage>();
|
||||
if (!string.IsNullOrWhiteSpace(request.SystemPrompt))
|
||||
{
|
||||
messages.Add(new ChatMessage { Role = "system", Content = request.SystemPrompt });
|
||||
}
|
||||
messages.Add(new ChatMessage { Role = "user", Content = request.Question });
|
||||
|
||||
var chatRequest = new ChatRequest { Messages = messages };
|
||||
var chatResponse = await deepSeekService.AskAsync(chatRequest);
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
if (chatResponse == null || chatResponse.Choices == null || !chatResponse.Choices.Any())
|
||||
{
|
||||
return StatusCode(500, new { error = "No response received from AI" });
|
||||
}
|
||||
|
||||
var answer = chatResponse.Choices.FirstOrDefault()?.Message?.Content ?? "";
|
||||
|
||||
// Save to database
|
||||
var aiQuery = new AIQuery
|
||||
{
|
||||
Question = request.Question,
|
||||
Answer = answer,
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = request.UserId,
|
||||
Model = chatResponse.Model,
|
||||
PromptTokens = chatResponse.Usage?.PromptTokens ?? 0,
|
||||
CompletionTokens = chatResponse.Usage?.CompletionTokens ?? 0,
|
||||
TotalTokens = chatResponse.Usage?.TotalTokens ?? 0,
|
||||
ResponseTimeMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
|
||||
await aiQueryService.SaveQueryAsync(aiQuery);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
question = request.Question,
|
||||
answer = answer,
|
||||
deviceId = request.DeviceId,
|
||||
tokens = new
|
||||
{
|
||||
prompt = aiQuery.PromptTokens,
|
||||
completion = aiQuery.CompletionTokens,
|
||||
total = aiQuery.TotalTokens
|
||||
},
|
||||
responseTimeMs = aiQuery.ResponseTimeMs,
|
||||
timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error processing AI question");
|
||||
return StatusCode(500, new { error = "An error occurred while processing your question" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a complex chat request with multiple messages to the AI
|
||||
/// </summary>
|
||||
/// <param name="request">Extended chat request with deviceId</param>
|
||||
/// <returns>AI chat response</returns>
|
||||
[HttpPost("chat")]
|
||||
public async Task<IActionResult> Chat([FromBody] ExtendedChatRequest request)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
if (request.Messages == null || !request.Messages.Any())
|
||||
{
|
||||
return BadRequest(new { error = "At least one message is required" });
|
||||
}
|
||||
|
||||
logger.LogInformation("Processing AI chat with {MessageCount} messages", request.Messages.Count);
|
||||
|
||||
var chatRequest = new ChatRequest
|
||||
{
|
||||
Messages = request.Messages,
|
||||
Model = request.Model,
|
||||
Temperature = request.Temperature,
|
||||
MaxTokens = request.MaxTokens
|
||||
};
|
||||
|
||||
var response = await deepSeekService.AskAsync(chatRequest);
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
if (response == null || response.Choices == null || !response.Choices.Any())
|
||||
{
|
||||
return StatusCode(500, new { error = "No response received from AI" });
|
||||
}
|
||||
|
||||
// Extract question and answer
|
||||
var userMessage = request.Messages.LastOrDefault(m => m.Role == "user");
|
||||
var question = userMessage?.Content ?? "Complex chat";
|
||||
var answer = response.Choices.FirstOrDefault()?.Message?.Content ?? "";
|
||||
|
||||
// Save to database
|
||||
var aiQuery = new AIQuery
|
||||
{
|
||||
Question = question,
|
||||
Answer = answer,
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = request.UserId,
|
||||
Model = response.Model,
|
||||
Temperature = request.Temperature,
|
||||
PromptTokens = response.Usage?.PromptTokens ?? 0,
|
||||
CompletionTokens = response.Usage?.CompletionTokens ?? 0,
|
||||
TotalTokens = response.Usage?.TotalTokens ?? 0,
|
||||
ResponseTimeMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
|
||||
await aiQueryService.SaveQueryAsync(aiQuery);
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error processing AI chat");
|
||||
return StatusCode(500, new { error = "An error occurred while processing your chat request" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get suggestions for smart home automation based on device data
|
||||
/// </summary>
|
||||
/// <param name="request">Device context request</param>
|
||||
/// <returns>AI suggestions</returns>
|
||||
[HttpPost("suggest")]
|
||||
public async Task<IActionResult> GetSuggestions([FromBody] SuggestionRequest request)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
var systemPrompt = @"شما یک مشاور هوشمند خانه هوشمند هستید. بر اساس دادههای دستگاههای IoT، پیشنهادهای عملی و مفید برای بهینهسازی مصرف انرژی، راحتی و امنیت ارائه دهید. پاسخ را به زبان فارسی و به صورت خلاصه و کاربردی بنویسید.";
|
||||
|
||||
var question = $@"وضعیت فعلی دستگاههای خانه هوشمند:
|
||||
{request.DeviceContext}
|
||||
|
||||
لطفاً پیشنهادات خود را برای بهبود وضعیت ارائه دهید.";
|
||||
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new ChatMessage { Role = "system", Content = systemPrompt },
|
||||
new ChatMessage { Role = "user", Content = question }
|
||||
};
|
||||
|
||||
var chatRequest = new ChatRequest { Messages = messages };
|
||||
var chatResponse = await deepSeekService.AskAsync(chatRequest);
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
if (chatResponse == null || chatResponse.Choices == null || !chatResponse.Choices.Any())
|
||||
{
|
||||
return StatusCode(500, new { error = "No suggestions received from AI" });
|
||||
}
|
||||
|
||||
var answer = chatResponse.Choices.FirstOrDefault()?.Message?.Content ?? "";
|
||||
|
||||
// Save to database
|
||||
var aiQuery = new AIQuery
|
||||
{
|
||||
Question = question,
|
||||
Answer = answer,
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = request.UserId,
|
||||
Model = chatResponse.Model,
|
||||
PromptTokens = chatResponse.Usage?.PromptTokens ?? 0,
|
||||
CompletionTokens = chatResponse.Usage?.CompletionTokens ?? 0,
|
||||
TotalTokens = chatResponse.Usage?.TotalTokens ?? 0,
|
||||
ResponseTimeMs = stopwatch.ElapsedMilliseconds
|
||||
};
|
||||
|
||||
await aiQueryService.SaveQueryAsync(aiQuery);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
suggestions = answer,
|
||||
deviceId = request.DeviceId,
|
||||
tokens = new
|
||||
{
|
||||
prompt = aiQuery.PromptTokens,
|
||||
completion = aiQuery.CompletionTokens,
|
||||
total = aiQuery.TotalTokens
|
||||
},
|
||||
responseTimeMs = aiQuery.ResponseTimeMs,
|
||||
timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting AI suggestions");
|
||||
return StatusCode(500, new { error = "An error occurred while getting suggestions" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get AI query history for a device
|
||||
/// </summary>
|
||||
[HttpGet("history/device/{deviceId}")]
|
||||
public async Task<IActionResult> GetDeviceHistory(int deviceId, [FromQuery] int take = 50)
|
||||
{
|
||||
try
|
||||
{
|
||||
var queries = await aiQueryService.GetDeviceQueriesAsync(deviceId, take);
|
||||
var totalTokens = await aiQueryService.GetDeviceTotalTokensAsync(deviceId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
queries = queries.Select(q => new
|
||||
{
|
||||
q.Id,
|
||||
q.Question,
|
||||
q.Answer,
|
||||
q.TotalTokens,
|
||||
q.PromptTokens,
|
||||
q.CompletionTokens,
|
||||
q.Model,
|
||||
q.ResponseTimeMs,
|
||||
q.CreatedAt
|
||||
}),
|
||||
totalTokens
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting device history");
|
||||
return StatusCode(500, new { error = "An error occurred" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get AI query statistics
|
||||
/// </summary>
|
||||
[HttpGet("stats")]
|
||||
public async Task<IActionResult> GetStats()
|
||||
{
|
||||
try
|
||||
{
|
||||
var stats = await aiQueryService.GetStatsAsync();
|
||||
return Ok(stats);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting stats");
|
||||
return StatusCode(500, new { error = "An error occurred" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple question request model
|
||||
/// </summary>
|
||||
public class SimpleQuestionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The question to ask the AI
|
||||
/// </summary>
|
||||
public required string Question { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional system prompt to set context for the AI
|
||||
/// </summary>
|
||||
public string? SystemPrompt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional device ID to associate with this query
|
||||
/// </summary>
|
||||
public int? DeviceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional user ID to associate with this query
|
||||
/// </summary>
|
||||
public int? UserId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extended chat request with device tracking
|
||||
/// </summary>
|
||||
public class ExtendedChatRequest
|
||||
{
|
||||
public required List<ChatMessage> Messages { get; set; }
|
||||
public string Model { get; set; } = "deepseek-chat";
|
||||
public double? Temperature { get; set; }
|
||||
public int? MaxTokens { get; set; }
|
||||
public int? DeviceId { get; set; }
|
||||
public int? UserId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suggestion request for smart home automation
|
||||
/// </summary>
|
||||
public class SuggestionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Context about devices and their current state
|
||||
/// </summary>
|
||||
public required string DeviceContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Device ID for this suggestion request
|
||||
/// </summary>
|
||||
public int? DeviceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User ID for this suggestion request
|
||||
/// </summary>
|
||||
public int? UserId { get; set; }
|
||||
}
|
||||
|
||||
85
src/GreenHome.Api/Controllers/AlertConditionsController.cs
Normal file
85
src/GreenHome.Api/Controllers/AlertConditionsController.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using GreenHome.Application;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AlertConditionsController : ControllerBase
|
||||
{
|
||||
private readonly IAlertConditionService alertConditionService;
|
||||
|
||||
public AlertConditionsController(IAlertConditionService alertConditionService)
|
||||
{
|
||||
this.alertConditionService = alertConditionService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تمام شرایط هشدار یک دستگاه
|
||||
/// </summary>
|
||||
[HttpGet("device/{deviceId}")]
|
||||
public async Task<ActionResult<IReadOnlyList<AlertConditionDto>>> GetByDeviceId(int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await alertConditionService.GetByDeviceIdAsync(deviceId, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت یک شرط هشدار با ID
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<AlertConditionDto>> GetById(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await alertConditionService.GetByIdAsync(id, cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ایجاد شرط هشدار جدید
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<int>> Create(CreateAlertConditionRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var id = await alertConditionService.CreateAsync(request, cancellationToken);
|
||||
return Ok(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بهروزرسانی شرط هشدار
|
||||
/// </summary>
|
||||
[HttpPut]
|
||||
public async Task<ActionResult> Update(UpdateAlertConditionRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
await alertConditionService.UpdateAsync(request, cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// حذف شرط هشدار
|
||||
/// </summary>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult> Delete(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
await alertConditionService.DeleteAsync(id, cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// فعال/غیرفعال کردن شرط هشدار
|
||||
/// </summary>
|
||||
[HttpPatch("{id}/toggle")]
|
||||
public async Task<ActionResult> ToggleEnabled(int id, [FromBody] bool isEnabled, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await alertConditionService.ToggleEnabledAsync(id, isEnabled, cancellationToken);
|
||||
if (!result)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
62
src/GreenHome.Api/Controllers/AlertLogsController.cs
Normal file
62
src/GreenHome.Api/Controllers/AlertLogsController.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using GreenHome.Application;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AlertLogsController : ControllerBase
|
||||
{
|
||||
private readonly IAlertLogService _alertLogService;
|
||||
|
||||
public AlertLogsController(IAlertLogService alertLogService)
|
||||
{
|
||||
_alertLogService = alertLogService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت لیست لاگهای هشدار با فیلتر و صفحهبندی
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<AlertLogDto>>> GetAlertLogs(
|
||||
[FromQuery] int? deviceId,
|
||||
[FromQuery] int? userId,
|
||||
[FromQuery] Domain.AlertType? alertType,
|
||||
[FromQuery] Domain.AlertStatus? status,
|
||||
[FromQuery] DateTime? startDate,
|
||||
[FromQuery] DateTime? endDate,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = new AlertLogFilter
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
UserId = userId,
|
||||
AlertType = alertType,
|
||||
Status = status,
|
||||
StartDate = startDate,
|
||||
EndDate = endDate,
|
||||
Page = page,
|
||||
PageSize = pageSize
|
||||
};
|
||||
|
||||
var result = await _alertLogService.GetAlertLogsAsync(filter, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت جزئیات کامل یک لاگ هشدار
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<AlertLogDto>> GetAlertLogById(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _alertLogService.GetAlertLogByIdAsync(id, cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound(new { error = $"لاگ هشدار با شناسه {id} یافت نشد" });
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
98
src/GreenHome.Api/Controllers/ChecklistsController.cs
Normal file
98
src/GreenHome.Api/Controllers/ChecklistsController.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using GreenHome.Application;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ChecklistsController : ControllerBase
|
||||
{
|
||||
private readonly IChecklistService _checklistService;
|
||||
|
||||
public ChecklistsController(IChecklistService checklistService)
|
||||
{
|
||||
_checklistService = checklistService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت چکلیست فعال یک دستگاه
|
||||
/// </summary>
|
||||
[HttpGet("active/{deviceId}")]
|
||||
public async Task<ActionResult<ChecklistDto>> GetActiveChecklist(int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _checklistService.GetActiveChecklistByDeviceIdAsync(deviceId, cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound(new { error = "چکلیست فعالی برای این دستگاه یافت نشد" });
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تمام چکلیستهای یک دستگاه
|
||||
/// </summary>
|
||||
[HttpGet("device/{deviceId}")]
|
||||
public async Task<ActionResult<List<ChecklistDto>>> GetChecklists(int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _checklistService.GetChecklistsByDeviceIdAsync(deviceId, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت جزئیات یک چکلیست
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<ChecklistDto>> GetChecklistById(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _checklistService.GetChecklistByIdAsync(id, cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound(new { error = $"چکلیست با شناسه {id} یافت نشد" });
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ایجاد چکلیست جدید (چکلیست قبلی غیرفعال میشود)
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<int>> CreateChecklist(
|
||||
CreateChecklistRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var id = await _checklistService.CreateChecklistAsync(request, cancellationToken);
|
||||
return Ok(new { id, message = "چکلیست با موفقیت ایجاد شد و چکلیست قبلی غیرفعال شد" });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت سابقه تکمیلهای یک چکلیست
|
||||
/// </summary>
|
||||
[HttpGet("{checklistId}/completions")]
|
||||
public async Task<ActionResult<List<ChecklistCompletionDto>>> GetCompletions(
|
||||
int checklistId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _checklistService.GetCompletionsByChecklistIdAsync(checklistId, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ثبت تکمیل چکلیست
|
||||
/// </summary>
|
||||
[HttpPost("complete")]
|
||||
public async Task<ActionResult<int>> CompleteChecklist(
|
||||
CompleteChecklistRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = await _checklistService.CompleteChecklistAsync(request, cancellationToken);
|
||||
return Ok(new { id, message = "چکلیست با موفقیت تکمیل شد" });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
143
src/GreenHome.Api/Controllers/DailyReportController.cs
Normal file
143
src/GreenHome.Api/Controllers/DailyReportController.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using GreenHome.Application;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DailyReportController : ControllerBase
|
||||
{
|
||||
private readonly IDailyReportService _dailyReportService;
|
||||
private readonly ILogger<DailyReportController> _logger;
|
||||
|
||||
public DailyReportController(
|
||||
IDailyReportService dailyReportService,
|
||||
ILogger<DailyReportController> logger)
|
||||
{
|
||||
_dailyReportService = dailyReportService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت یا ایجاد گزارش تحلیل روزانه گلخانه
|
||||
/// </summary>
|
||||
/// <param name="deviceId">شناسه دستگاه</param>
|
||||
/// <param name="persianDate">تاریخ شمسی به فرمت yyyy/MM/dd</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>گزارش تحلیل روزانه</returns>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<DailyReportResponse>> GetDailyReport(
|
||||
[FromQuery] int deviceId,
|
||||
[FromQuery] string persianDate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (deviceId <= 0)
|
||||
{
|
||||
return BadRequest(new { error = "شناسه دستگاه نامعتبر است" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(persianDate))
|
||||
{
|
||||
return BadRequest(new { error = "تاریخ نباید خالی باشد" });
|
||||
}
|
||||
|
||||
var request = new DailyReportRequest
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
PersianDate = persianDate.Trim()
|
||||
};
|
||||
|
||||
var result = await _dailyReportService.GetOrCreateDailyReportAsync(request, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"گزارش روزانه برای دستگاه {DeviceId} و تاریخ {Date} با موفقیت برگشت داده شد (FromCache: {FromCache})",
|
||||
deviceId, persianDate, result.FromCache);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "درخواست نامعتبر برای دستگاه {DeviceId} و تاریخ {Date}", deviceId, persianDate);
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "خطا در پردازش گزارش روزانه برای دستگاه {DeviceId} و تاریخ {Date}", deviceId, persianDate);
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "خطای سرور در دریافت گزارش روزانه برای دستگاه {DeviceId} و تاریخ {Date}", deviceId, persianDate);
|
||||
return StatusCode(500, new { error = "خطای سرور در پردازش درخواست" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تحلیل هفتگی
|
||||
/// </summary>
|
||||
[HttpGet("weekly")]
|
||||
public async Task<ActionResult<DailyReportResponse>> GetWeeklyAnalysis(
|
||||
[FromQuery] int deviceId,
|
||||
[FromQuery] string startDate,
|
||||
[FromQuery] string endDate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new WeeklyAnalysisRequest
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
StartDate = startDate.Trim(),
|
||||
EndDate = endDate.Trim()
|
||||
};
|
||||
|
||||
var result = await _dailyReportService.GetWeeklyAnalysisAsync(request, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting weekly analysis");
|
||||
return StatusCode(500, new { error = "خطا در دریافت تحلیل هفتگی" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تحلیل ماهانه
|
||||
/// </summary>
|
||||
[HttpGet("monthly")]
|
||||
public async Task<ActionResult<DailyReportResponse>> GetMonthlyAnalysis(
|
||||
[FromQuery] int deviceId,
|
||||
[FromQuery] int year,
|
||||
[FromQuery] int month,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new MonthlyAnalysisRequest
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
Year = year,
|
||||
Month = month
|
||||
};
|
||||
|
||||
var result = await _dailyReportService.GetMonthlyAnalysisAsync(request, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting monthly analysis");
|
||||
return StatusCode(500, new { error = "خطا در دریافت تحلیل ماهانه" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
212
src/GreenHome.Api/Controllers/DevicePostsController.cs
Normal file
212
src/GreenHome.Api/Controllers/DevicePostsController.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using GreenHome.Application;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DevicePostsController : ControllerBase
|
||||
{
|
||||
private readonly IDevicePostService _postService;
|
||||
private readonly ILogger<DevicePostsController> _logger;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
public DevicePostsController(
|
||||
IDevicePostService postService,
|
||||
ILogger<DevicePostsController> logger,
|
||||
IWebHostEnvironment environment)
|
||||
{
|
||||
_postService = postService;
|
||||
_logger = logger;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت پستهای گروه مجازی دستگاه (تایملاین)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<DevicePostDto>>> GetPosts(
|
||||
[FromQuery] int deviceId,
|
||||
[FromQuery] int? authorUserId,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = new DevicePostFilter
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
AuthorUserId = authorUserId,
|
||||
Page = page,
|
||||
PageSize = pageSize
|
||||
};
|
||||
|
||||
var result = await _postService.GetPostsAsync(filter, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت جزئیات یک پست
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<DevicePostDto>> GetPostById(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _postService.GetPostByIdAsync(id, cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound(new { error = $"پست با شناسه {id} یافت نشد" });
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ایجاد پست جدید در گروه مجازی
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<int>> CreatePost(
|
||||
CreateDevicePostRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = await _postService.CreatePostAsync(request, cancellationToken);
|
||||
return Ok(new { id, message = "پست با موفقیت ایجاد شد" });
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
return Unauthorized(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ویرایش پست
|
||||
/// </summary>
|
||||
[HttpPut]
|
||||
public async Task<ActionResult> UpdatePost(
|
||||
UpdateDevicePostRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _postService.UpdatePostAsync(request, cancellationToken);
|
||||
return Ok(new { message = "پست با موفقیت ویرایش شد" });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// حذف پست
|
||||
/// </summary>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult> DeletePost(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _postService.DeletePostAsync(id, cancellationToken);
|
||||
return Ok(new { message = "پست با موفقیت حذف شد" });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// آپلود تصویر برای پست
|
||||
/// </summary>
|
||||
[HttpPost("{postId}/images")]
|
||||
[Consumes("multipart/form-data")]
|
||||
public async Task<ActionResult<int>> UploadImage(
|
||||
int postId,
|
||||
[FromForm] IFormFile file,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest(new { error = "فایل انتخاب نشده است" });
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
var allowedTypes = new[] { "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp" };
|
||||
if (!allowedTypes.Contains(file.ContentType.ToLower()))
|
||||
{
|
||||
return BadRequest(new { error = "فقط فایلهای تصویری مجاز هستند" });
|
||||
}
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
if (file.Length > 5 * 1024 * 1024)
|
||||
{
|
||||
return BadRequest(new { error = "حجم فایل نباید بیشتر از 5 مگابایت باشد" });
|
||||
}
|
||||
|
||||
// Create upload directory
|
||||
var uploadsFolder = Path.Combine(_environment.WebRootPath ?? "wwwroot", "uploads", "posts");
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
|
||||
// Generate unique filename
|
||||
var fileName = $"{Guid.NewGuid()}_{Path.GetFileName(file.FileName)}";
|
||||
var filePath = Path.Combine(uploadsFolder, fileName);
|
||||
|
||||
// Save file
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream, cancellationToken);
|
||||
}
|
||||
|
||||
// Save to database
|
||||
var relativePath = $"/uploads/posts/{fileName}";
|
||||
var imageId = await _postService.AddImageToPostAsync(
|
||||
postId,
|
||||
file.FileName,
|
||||
relativePath,
|
||||
file.ContentType,
|
||||
file.Length,
|
||||
cancellationToken);
|
||||
|
||||
_logger.LogInformation("Image uploaded for post {PostId}: {ImageId}", postId, imageId);
|
||||
|
||||
return Ok(new { imageId, filePath = relativePath, message = "تصویر با موفقیت آپلود شد" });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error uploading image for post {PostId}", postId);
|
||||
return StatusCode(500, new { error = "خطا در آپلود تصویر" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// حذف تصویر از پست
|
||||
/// </summary>
|
||||
[HttpDelete("images/{imageId}")]
|
||||
public async Task<ActionResult> DeleteImage(int imageId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _postService.DeleteImageAsync(imageId, cancellationToken);
|
||||
return Ok(new { message = "تصویر با موفقیت حذف شد" });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بررسی دسترسی کاربر به دستگاه
|
||||
/// </summary>
|
||||
[HttpGet("access/{userId}/{deviceId}")]
|
||||
public async Task<ActionResult<bool>> CheckAccess(int userId, int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var hasAccess = await _postService.CanUserAccessDeviceAsync(userId, deviceId, cancellationToken);
|
||||
return Ok(new { hasAccess });
|
||||
}
|
||||
}
|
||||
|
||||
128
src/GreenHome.Api/Controllers/DeviceTokenController.cs
Normal file
128
src/GreenHome.Api/Controllers/DeviceTokenController.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using GreenHome.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// کنترلر مدیریت توکن و تنظیمات دستگاه
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DeviceTokenController : ControllerBase
|
||||
{
|
||||
private readonly IDeviceTokenService deviceTokenService;
|
||||
private readonly ILogger<DeviceTokenController> logger;
|
||||
|
||||
public DeviceTokenController(
|
||||
IDeviceTokenService deviceTokenService,
|
||||
ILogger<DeviceTokenController> logger)
|
||||
{
|
||||
this.deviceTokenService = deviceTokenService;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت فاصله زمانی آپلود دستگاه
|
||||
/// </summary>
|
||||
/// <param name="deviceId">شناسه دستگاه (اختیاری)</param>
|
||||
/// <param name="devicePhoneNumber">شماره تلفن دستگاه (اختیاری)</param>
|
||||
/// <returns>فاصله زمانی آپلود به دقیقه</returns>
|
||||
[HttpGet("upload-interval")]
|
||||
public async Task<ActionResult<GetUploadIntervalResponse>> GetUploadInterval(
|
||||
[FromQuery] int? deviceId,
|
||||
[FromQuery] string? devicePhoneNumber,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!deviceId.HasValue && string.IsNullOrWhiteSpace(devicePhoneNumber))
|
||||
{
|
||||
return BadRequest(new GetUploadIntervalResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "حداقل یکی از پارامترهای deviceId یا devicePhoneNumber باید ارسال شود"
|
||||
});
|
||||
}
|
||||
|
||||
var request = new GetUploadIntervalRequest
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DevicePhoneNumber = devicePhoneNumber
|
||||
};
|
||||
|
||||
var result = await deviceTokenService.GetUploadIntervalAsync(request, cancellationToken);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return NotFound(result);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// درخواست توکن دستگاه (تولید و ارسال کد از طریق پیامک)
|
||||
/// </summary>
|
||||
/// <param name="request">درخواست شامل شماره تلفن دستگاه</param>
|
||||
/// <returns>نتیجه درخواست</returns>
|
||||
[HttpPost("request-token")]
|
||||
public async Task<ActionResult<RequestDeviceTokenResponse>> RequestToken(
|
||||
[FromBody] RequestDeviceTokenRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.DevicePhoneNumber))
|
||||
{
|
||||
return BadRequest(new RequestDeviceTokenResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "شماره تلفن دستگاه الزامی است"
|
||||
});
|
||||
}
|
||||
|
||||
var result = await deviceTokenService.RequestDeviceTokenAsync(request, cancellationToken);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return BadRequest(result);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تایید توکن دستگاه (ارسال تنظیمات کدشده از طریق پیامک)
|
||||
/// </summary>
|
||||
/// <param name="request">درخواست شامل شماره تلفن و کد تایید</param>
|
||||
/// <returns>نتیجه تایید و تنظیمات کدشده</returns>
|
||||
[HttpPost("verify-token")]
|
||||
public async Task<ActionResult<VerifyDeviceTokenResponse>> VerifyToken(
|
||||
[FromBody] VerifyDeviceTokenRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.DevicePhoneNumber))
|
||||
{
|
||||
return BadRequest(new VerifyDeviceTokenResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "شماره تلفن دستگاه الزامی است"
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.VerificationCode))
|
||||
{
|
||||
return BadRequest(new VerifyDeviceTokenResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "کد تایید الزامی است"
|
||||
});
|
||||
}
|
||||
|
||||
var result = await deviceTokenService.VerifyDeviceTokenAsync(request, cancellationToken);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return BadRequest(result);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
62
src/GreenHome.Api/Controllers/MonthlyReportController.cs
Normal file
62
src/GreenHome.Api/Controllers/MonthlyReportController.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using GreenHome.Application;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class MonthlyReportController : ControllerBase
|
||||
{
|
||||
private readonly IMonthlyReportService _monthlyReportService;
|
||||
private readonly ILogger<MonthlyReportController> _logger;
|
||||
|
||||
public MonthlyReportController(
|
||||
IMonthlyReportService monthlyReportService,
|
||||
ILogger<MonthlyReportController> logger)
|
||||
{
|
||||
_monthlyReportService = monthlyReportService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت گزارش آماری ماهانه
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<MonthlyReportDto>> GetMonthlyReport(
|
||||
[FromQuery] int deviceId,
|
||||
[FromQuery] int year,
|
||||
[FromQuery] int month,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (deviceId <= 0)
|
||||
{
|
||||
return BadRequest(new { error = "شناسه دستگاه نامعتبر است" });
|
||||
}
|
||||
|
||||
if (month < 1 || month > 12)
|
||||
{
|
||||
return BadRequest(new { error = "ماه باید بین 1 تا 12 باشد" });
|
||||
}
|
||||
|
||||
var result = await _monthlyReportService.GetMonthlyReportAsync(deviceId, year, month, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"گزارش ماهانه برای دستگاه {DeviceId} و ماه {Month}/{Year} ایجاد شد",
|
||||
deviceId, month, year);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error generating monthly report");
|
||||
return StatusCode(500, new { error = "خطا در ایجاد گزارش ماهانه" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
60
src/GreenHome.Api/Controllers/PowerOutageController.cs
Normal file
60
src/GreenHome.Api/Controllers/PowerOutageController.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using GreenHome.Application;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class PowerOutageController : ControllerBase
|
||||
{
|
||||
private readonly IAlertService _alertService;
|
||||
private readonly ILogger<PowerOutageController> _logger;
|
||||
|
||||
public PowerOutageController(
|
||||
IAlertService alertService,
|
||||
ILogger<PowerOutageController> logger)
|
||||
{
|
||||
_alertService = alertService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ارسال هشدار قطع برق برای یک دستگاه
|
||||
/// </summary>
|
||||
/// <param name="deviceId">شناسه دستگاه</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>نتیجه عملیات</returns>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> SendPowerOutageAlert(
|
||||
[FromQuery] int deviceId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (deviceId <= 0)
|
||||
{
|
||||
return BadRequest(new { error = "شناسه دستگاه نامعتبر است" });
|
||||
}
|
||||
|
||||
await _alertService.SendPowerOutageAlertAsync(deviceId, cancellationToken);
|
||||
|
||||
_logger.LogInformation("Power outage alert processed for device {DeviceId}", deviceId);
|
||||
|
||||
return Ok(new {
|
||||
success = true,
|
||||
message = "هشدار قطع برق با موفقیت ارسال شد"
|
||||
});
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid operation for power outage alert: DeviceId={DeviceId}", deviceId);
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending power outage alert: DeviceId={DeviceId}", deviceId);
|
||||
return StatusCode(500, new { error = "خطا در ارسال هشدار قطع برق" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,14 @@ public class TelemetryController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("AddData")]
|
||||
public async Task<ActionResult<int>> Create(string deviceName, decimal temperatureC, decimal humidityPercent,
|
||||
public async Task<ActionResult<int>> Create(int deviceId, decimal temperatureC, decimal humidityPercent,
|
||||
decimal soilPercent, int gasPPM, decimal lux, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
TelemetryDto dto = new TelemetryDto
|
||||
{
|
||||
DeviceName = deviceName,
|
||||
//DeviceName = deviceName.ToString() == "dr110"? "dr110":"",
|
||||
DeviceId = deviceId,
|
||||
TemperatureC = temperatureC,
|
||||
HumidityPercent = humidityPercent,
|
||||
SoilPercent = soilPercent,
|
||||
|
||||
210
src/GreenHome.Api/Controllers/UserDailyReportsController.cs
Normal file
210
src/GreenHome.Api/Controllers/UserDailyReportsController.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using GreenHome.Application;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class UserDailyReportsController : ControllerBase
|
||||
{
|
||||
private readonly IUserDailyReportService _reportService;
|
||||
private readonly ILogger<UserDailyReportsController> _logger;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
public UserDailyReportsController(
|
||||
IUserDailyReportService reportService,
|
||||
ILogger<UserDailyReportsController> logger,
|
||||
IWebHostEnvironment environment)
|
||||
{
|
||||
_reportService = reportService;
|
||||
_logger = logger;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت لیست گزارشهای روزانه کاربران با فیلتر
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<UserDailyReportDto>>> GetReports(
|
||||
[FromQuery] int? deviceId,
|
||||
[FromQuery] int? userId,
|
||||
[FromQuery] string? persianDate,
|
||||
[FromQuery] int? year,
|
||||
[FromQuery] int? month,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = new UserDailyReportFilter
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
UserId = userId,
|
||||
PersianDate = persianDate,
|
||||
Year = year,
|
||||
Month = month,
|
||||
Page = page,
|
||||
PageSize = pageSize
|
||||
};
|
||||
|
||||
var result = await _reportService.GetReportsAsync(filter, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت جزئیات یک گزارش روزانه
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<UserDailyReportDto>> GetReportById(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _reportService.GetReportByIdAsync(id, cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound(new { error = $"گزارش با شناسه {id} یافت نشد" });
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ایجاد گزارش روزانه جدید
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<int>> CreateReport(
|
||||
CreateUserDailyReportRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = await _reportService.CreateReportAsync(request, cancellationToken);
|
||||
return Ok(new { id, message = "گزارش با موفقیت ایجاد شد" });
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ویرایش گزارش روزانه
|
||||
/// </summary>
|
||||
[HttpPut]
|
||||
public async Task<ActionResult> UpdateReport(
|
||||
UpdateUserDailyReportRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _reportService.UpdateReportAsync(request, cancellationToken);
|
||||
return Ok(new { message = "گزارش با موفقیت ویرایش شد" });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// حذف گزارش روزانه
|
||||
/// </summary>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult> DeleteReport(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _reportService.DeleteReportAsync(id, cancellationToken);
|
||||
return Ok(new { message = "گزارش با موفقیت حذف شد" });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// آپلود تصویر برای گزارش روزانه
|
||||
/// </summary>
|
||||
[HttpPost("{reportId}/images")]
|
||||
[Consumes("multipart/form-data")]
|
||||
public async Task<ActionResult<int>> UploadImage(
|
||||
int reportId,
|
||||
[FromForm] IFormFile file,
|
||||
[FromForm] string? description,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest(new { error = "فایل انتخاب نشده است" });
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
var allowedTypes = new[] { "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp" };
|
||||
if (!allowedTypes.Contains(file.ContentType.ToLower()))
|
||||
{
|
||||
return BadRequest(new { error = "فقط فایلهای تصویری مجاز هستند" });
|
||||
}
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
if (file.Length > 5 * 1024 * 1024)
|
||||
{
|
||||
return BadRequest(new { error = "حجم فایل نباید بیشتر از 5 مگابایت باشد" });
|
||||
}
|
||||
|
||||
// Create upload directory
|
||||
var uploadsFolder = Path.Combine(_environment.WebRootPath ?? "wwwroot", "uploads", "reports");
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
|
||||
// Generate unique filename
|
||||
var fileName = $"{Guid.NewGuid()}_{Path.GetFileName(file.FileName)}";
|
||||
var filePath = Path.Combine(uploadsFolder, fileName);
|
||||
|
||||
// Save file
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream, cancellationToken);
|
||||
}
|
||||
|
||||
// Save to database
|
||||
var relativePath = $"/uploads/reports/{fileName}";
|
||||
var imageId = await _reportService.AddImageToReportAsync(
|
||||
reportId,
|
||||
file.FileName,
|
||||
relativePath,
|
||||
file.ContentType,
|
||||
file.Length,
|
||||
description,
|
||||
cancellationToken);
|
||||
|
||||
_logger.LogInformation("Image uploaded for report {ReportId}: {ImageId}", reportId, imageId);
|
||||
|
||||
return Ok(new { imageId, filePath = relativePath, message = "تصویر با موفقیت آپلود شد" });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error uploading image for report {ReportId}", reportId);
|
||||
return StatusCode(500, new { error = "خطا در آپلود تصویر" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// حذف تصویر از گزارش
|
||||
/// </summary>
|
||||
[HttpDelete("images/{imageId}")]
|
||||
public async Task<ActionResult> DeleteImage(int imageId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _reportService.DeleteImageAsync(imageId, cancellationToken);
|
||||
return Ok(new { message = "تصویر با موفقیت حذف شد" });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,3 +270,4 @@ public class VoiceCallTestController : ControllerBase
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
@@ -13,7 +13,10 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.11.6" />
|
||||
<Content Include="My_StaticFiles\**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -21,6 +24,7 @@
|
||||
<ProjectReference Include="..\GreenHome.Infrastructure\GreenHome.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\GreenHome.Sms.Ippanel\GreenHome.Sms.Ippanel.csproj" />
|
||||
<ProjectReference Include="..\GreenHome.VoiceCall.Avanak\GreenHome.VoiceCall.Avanak.csproj" />
|
||||
<ProjectReference Include="..\GreenHome.AI.DeepSeek\GreenHome.AI.DeepSeek.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
1
src/GreenHome.Api/My_StaticFiles/calltest.html
Normal file
1
src/GreenHome.Api/My_StaticFiles/calltest.html
Normal file
@@ -0,0 +1 @@
|
||||
TY09192530212#https://ghback.nabaksoft.ir/My_StaticFiles/output.amr
|
||||
BIN
src/GreenHome.Api/My_StaticFiles/output.amr
Normal file
BIN
src/GreenHome.Api/My_StaticFiles/output.amr
Normal file
Binary file not shown.
1
src/GreenHome.Api/My_StaticFiles/smsTest.html
Normal file
1
src/GreenHome.Api/My_StaticFiles/smsTest.html
Normal file
@@ -0,0 +1 @@
|
||||
TT09192530212#سلام خوبی
|
||||
@@ -1,40 +1,53 @@
|
||||
using FluentValidation;
|
||||
using GreenHome.AI.DeepSeek;
|
||||
using GreenHome.Application;
|
||||
using GreenHome.Infrastructure;
|
||||
using GreenHome.Sms.Ippanel;
|
||||
using GreenHome.VoiceCall.Avanak;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Scalar.AspNetCore;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
// Application/Infrastructure DI
|
||||
builder.Services.AddAutoMapper(typeof(GreenHome.Application.MappingProfile));
|
||||
builder.Services.AddValidatorsFromAssemblyContaining<GreenHome.Application.DeviceDtoValidator>();
|
||||
|
||||
// CORS for Next.js dev (adjust origins as needed)
|
||||
// CORS Configuration
|
||||
const string CorsPolicy = "DefaultCors";
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy(CorsPolicy, policy =>
|
||||
policy
|
||||
.WithOrigins(
|
||||
"http://green.nabaksoft.ir",
|
||||
"https://green.nabaksoft.ir",
|
||||
"http://gh1.nabaksoft.ir",
|
||||
"https://gh1.nabaksoft.ir",
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"https://localhost:3000"
|
||||
)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials()
|
||||
);
|
||||
{
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
// در محیط Development همه origin ها مجاز هستند
|
||||
policy
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
}
|
||||
else
|
||||
{
|
||||
// در محیط Production فقط دامنههای مشخص
|
||||
policy
|
||||
.WithOrigins(
|
||||
"http://green.nabaksoft.ir",
|
||||
"https://green.nabaksoft.ir",
|
||||
"http://gh1.nabaksoft.ir",
|
||||
"https://gh1.nabaksoft.ir"
|
||||
)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddDbContext<GreenHome.Infrastructure.GreenHomeDbContext>(options =>
|
||||
@@ -46,6 +59,16 @@ builder.Services.AddScoped<GreenHome.Application.ITelemetryService, GreenHome.In
|
||||
builder.Services.AddScoped<GreenHome.Application.IDeviceSettingsService, GreenHome.Infrastructure.DeviceSettingsService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.IAuthService, GreenHome.Infrastructure.AuthService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.IAlertService, GreenHome.Infrastructure.AlertService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.IAlertConditionService, GreenHome.Infrastructure.AlertConditionService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.ISunCalculatorService, GreenHome.Infrastructure.SunCalculatorService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.IAIQueryService, GreenHome.Infrastructure.AIQueryService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.IDailyReportService, GreenHome.Infrastructure.DailyReportService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.IAlertLogService, GreenHome.Infrastructure.AlertLogService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.IUserDailyReportService, GreenHome.Infrastructure.UserDailyReportService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.IChecklistService, GreenHome.Infrastructure.ChecklistService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.IMonthlyReportService, GreenHome.Infrastructure.MonthlyReportService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.IDevicePostService, GreenHome.Infrastructure.DevicePostService>();
|
||||
builder.Services.AddScoped<GreenHome.Application.IDeviceTokenService, GreenHome.Infrastructure.DeviceTokenService>();
|
||||
|
||||
// SMS Service Configuration
|
||||
builder.Services.AddIppanelSms(builder.Configuration);
|
||||
@@ -53,6 +76,9 @@ builder.Services.AddIppanelSms(builder.Configuration);
|
||||
// Voice Call Service Configuration
|
||||
builder.Services.AddAvanakVoiceCall(builder.Configuration);
|
||||
|
||||
// AI Service Configuration
|
||||
builder.Services.AddDeepSeek(builder.Configuration);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Apply pending migrations automatically
|
||||
@@ -72,18 +98,27 @@ using (var scope = app.Services.CreateScope())
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
//if (app.Environment.IsDevelopment())
|
||||
app.MapOpenApi();
|
||||
app.MapScalarApiReference();
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.UseHttpsRedirection();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseCors(CorsPolicy);
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
provider.Mappings[".amr"] = "audio/amr";
|
||||
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(
|
||||
Path.Combine(builder.Environment.ContentRootPath, "My_StaticFiles")),
|
||||
RequestPath = "/My_StaticFiles",
|
||||
ContentTypeProvider = provider
|
||||
});
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
@@ -6,7 +6,7 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5064"
|
||||
"applicationUrl": "http://127.0.0.1:5064"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
@@ -14,7 +14,7 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7274;http://localhost:5064"
|
||||
"applicationUrl": "https://127.0.0.1:7274;http://127.0.0.1:5064"
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
|
||||
@@ -4,5 +4,16 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"Default": "Server=87.107.108.119;TrustServerCertificate=True;Database=GreenHomeDb;User Id=sa;Password=qwER12#$110"
|
||||
},
|
||||
"AvanakVoiceCall": {
|
||||
"Token": "A948B776B90CFD919B0EC60929714136CCB49DDB"
|
||||
},
|
||||
"IppanelSms": {
|
||||
"BaseUrl": "https://edge.ippanel.com/v1",
|
||||
"AuthorizationToken": "YTA1Zjk3N2EtNzkwOC00ZTg5LWFjZmYtZGEyZDAyNjNlZWQxM2Q2ZDVjYWE0MTA2Yzc1NDYzZDY1Y2VkMjlhMzcwNjA=",
|
||||
"DefaultSender": "+983000505"
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,12 @@
|
||||
"AuthorizationToken": "YTA1Zjk3N2EtNzkwOC00ZTg5LWFjZmYtZGEyZDAyNjNlZWQxM2Q2ZDVjYWE0MTA2Yzc1NDYzZDY1Y2VkMjlhMzcwNjA=",
|
||||
"DefaultSender": "+983000505"
|
||||
},
|
||||
"DeepSeek": {
|
||||
"BaseUrl": "https://api.deepseek.com",
|
||||
"ApiKey": "sk-4470fc1a003a445e92f357dbe123e5a4",
|
||||
"DefaultModel": "deepseek-chat",
|
||||
"DefaultTemperature": 1.0
|
||||
},
|
||||
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
80
src/GreenHome.Application/ChecklistDtos.cs
Normal file
80
src/GreenHome.Application/ChecklistDtos.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
namespace GreenHome.Application;
|
||||
|
||||
public sealed class ChecklistDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DeviceId { get; set; }
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public List<ChecklistItemDto> Items { get; set; } = new();
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public int CreatedByUserId { get; set; }
|
||||
public string CreatedByUserName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ChecklistItemDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int Order { get; set; }
|
||||
public bool IsRequired { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateChecklistRequest
|
||||
{
|
||||
public required int DeviceId { get; set; }
|
||||
public required int CreatedByUserId { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public required List<CreateChecklistItemRequest> Items { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateChecklistItemRequest
|
||||
{
|
||||
public required string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public int Order { get; set; }
|
||||
public bool IsRequired { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ChecklistCompletionDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int ChecklistId { get; set; }
|
||||
public string ChecklistTitle { get; set; } = string.Empty;
|
||||
public int CompletedByUserId { get; set; }
|
||||
public string CompletedByUserName { get; set; } = string.Empty;
|
||||
public string PersianDate { get; set; } = string.Empty;
|
||||
public List<ChecklistItemCompletionDto> ItemCompletions { get; set; } = new();
|
||||
public string? Notes { get; set; }
|
||||
public DateTime CompletedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ChecklistItemCompletionDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int ChecklistItemId { get; set; }
|
||||
public string ItemTitle { get; set; } = string.Empty;
|
||||
public bool IsChecked { get; set; }
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CompleteChecklistRequest
|
||||
{
|
||||
public required int ChecklistId { get; set; }
|
||||
public required int CompletedByUserId { get; set; }
|
||||
public required string PersianDate { get; set; }
|
||||
public required List<CompleteChecklistItemRequest> ItemCompletions { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CompleteChecklistItemRequest
|
||||
{
|
||||
public required int ChecklistItemId { get; set; }
|
||||
public bool IsChecked { get; set; }
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
|
||||
46
src/GreenHome.Application/DevicePostDtos.cs
Normal file
46
src/GreenHome.Application/DevicePostDtos.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace GreenHome.Application;
|
||||
|
||||
public sealed class DevicePostDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DeviceId { get; set; }
|
||||
public int AuthorUserId { get; set; }
|
||||
public string AuthorName { get; set; } = string.Empty;
|
||||
public string AuthorFamily { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public List<DevicePostImageDto> Images { get; set; } = new();
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DevicePostImageDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
public long FileSize { get; set; }
|
||||
public DateTime UploadedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateDevicePostRequest
|
||||
{
|
||||
public required int DeviceId { get; set; }
|
||||
public required int AuthorUserId { get; set; }
|
||||
public required string Content { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateDevicePostRequest
|
||||
{
|
||||
public required int Id { get; set; }
|
||||
public required string Content { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DevicePostFilter
|
||||
{
|
||||
public required int DeviceId { get; set; }
|
||||
public int? AuthorUserId { get; set; }
|
||||
public int Page { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public sealed class TelemetryDto
|
||||
public int PersianYear { get; set; }
|
||||
public int PersianMonth { get; set; }
|
||||
public string PersianDate { get; set; } = string.Empty;
|
||||
public DateTime ServerTimestampUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TelemetryFilter
|
||||
@@ -117,24 +118,263 @@ public sealed class DeviceSettingsDto
|
||||
public int DeviceId { get; set; }
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
|
||||
// Temperature settings
|
||||
public decimal DangerMaxTemperature { get; set; }
|
||||
public decimal DangerMinTemperature { get; set; }
|
||||
public decimal MaxTemperature { get; set; }
|
||||
public decimal MinTemperature { get; set; }
|
||||
public string Province { get; set; } = string.Empty;
|
||||
public string City { get; set; } = string.Empty;
|
||||
public decimal? Latitude { get; set; }
|
||||
public decimal? Longitude { get; set; }
|
||||
|
||||
// Gas settings
|
||||
public int MaxGasPPM { get; set; }
|
||||
public int MinGasPPM { get; set; }
|
||||
public string ProductType { get; set; } = string.Empty;
|
||||
public int MinimumSmsIntervalMinutes { get; set; } = 15;
|
||||
public int MinimumCallIntervalMinutes { get; set; } = 60;
|
||||
public decimal? AreaSquareMeters { get; set; }
|
||||
|
||||
// Light settings
|
||||
public decimal MaxLux { get; set; }
|
||||
public decimal MinLux { get; set; }
|
||||
|
||||
// Humidity settings
|
||||
public decimal MaxHumidityPercent { get; set; }
|
||||
public decimal MinHumidityPercent { get; set; }
|
||||
public int UploadIntervalMin { get; set; } = 5;
|
||||
public string DevicePhoneNumber { get; set; } = string.Empty;
|
||||
public Domain.SimCardType? SimCardType { get; set; }
|
||||
public string? TokenCode { get; set; }
|
||||
public string? VerificationCode { get; set; }
|
||||
public DateTime? TokenExpiresAt { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AlertConditionDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DeviceId { get; set; }
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
public Domain.AlertNotificationType NotificationType { get; set; }
|
||||
public Domain.AlertTimeType TimeType { get; set; }
|
||||
public int CallCooldownMinutes { get; set; } = 60;
|
||||
public int SmsCooldownMinutes { get; set; } = 15;
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public List<AlertRuleDto> Rules { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class AlertRuleDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int AlertConditionId { get; set; }
|
||||
public Domain.SensorType SensorType { get; set; }
|
||||
public Domain.ComparisonType ComparisonType { get; set; }
|
||||
public decimal Value1 { get; set; }
|
||||
public decimal? Value2 { get; set; }
|
||||
public int Order { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateAlertConditionRequest
|
||||
{
|
||||
public required int DeviceId { get; set; }
|
||||
public required Domain.AlertNotificationType NotificationType { get; set; }
|
||||
public required Domain.AlertTimeType TimeType { get; set; }
|
||||
public int CallCooldownMinutes { get; set; } = 60;
|
||||
public int SmsCooldownMinutes { get; set; } = 15;
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public required List<CreateAlertRuleRequest> Rules { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateAlertRuleRequest
|
||||
{
|
||||
public required Domain.SensorType SensorType { get; set; }
|
||||
public required Domain.ComparisonType ComparisonType { get; set; }
|
||||
public required decimal Value1 { get; set; }
|
||||
public decimal? Value2 { get; set; }
|
||||
public int Order { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateAlertConditionRequest
|
||||
{
|
||||
public required int Id { get; set; }
|
||||
public required Domain.AlertNotificationType NotificationType { get; set; }
|
||||
public required Domain.AlertTimeType TimeType { get; set; }
|
||||
public int CallCooldownMinutes { get; set; } = 60;
|
||||
public int SmsCooldownMinutes { get; set; } = 15;
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public required List<CreateAlertRuleRequest> Rules { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DailyReportRequest
|
||||
{
|
||||
public required int DeviceId { get; set; }
|
||||
public required string PersianDate { get; set; } // yyyy/MM/dd
|
||||
}
|
||||
|
||||
public sealed class DailyReportResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DeviceId { get; set; }
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
public string PersianDate { get; set; } = string.Empty;
|
||||
public string Analysis { get; set; } = string.Empty;
|
||||
public int RecordCount { get; set; }
|
||||
public int SampledRecordCount { get; set; }
|
||||
public int TotalTokens { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public bool FromCache { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WeeklyAnalysisRequest
|
||||
{
|
||||
public required int DeviceId { get; set; }
|
||||
public required string StartDate { get; set; } // yyyy/MM/dd
|
||||
public required string EndDate { get; set; } // yyyy/MM/dd
|
||||
}
|
||||
|
||||
public sealed class MonthlyAnalysisRequest
|
||||
{
|
||||
public required int DeviceId { get; set; }
|
||||
public required int Year { get; set; }
|
||||
public required int Month { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AlertLogDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DeviceId { get; set; }
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
public int UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string UserMobile { get; set; } = string.Empty;
|
||||
public int? AlertConditionId { get; set; }
|
||||
public Domain.AlertType AlertType { get; set; }
|
||||
public Domain.AlertNotificationType NotificationType { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public Domain.AlertStatus Status { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public string PhoneNumber { get; set; } = string.Empty;
|
||||
public DateTime SentAt { get; set; }
|
||||
public long ProcessingTimeMs { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AlertLogFilter
|
||||
{
|
||||
public int? DeviceId { get; set; }
|
||||
public int? UserId { get; set; }
|
||||
public Domain.AlertType? AlertType { get; set; }
|
||||
public Domain.AlertStatus? Status { get; set; }
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
public int Page { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
}
|
||||
|
||||
public sealed class UserDailyReportDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DeviceId { get; set; }
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
public int UserId { get; set; }
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string UserFamily { get; set; } = string.Empty;
|
||||
public string PersianDate { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Observations { get; set; } = string.Empty;
|
||||
public string Operations { get; set; } = string.Empty;
|
||||
public string? Notes { get; set; }
|
||||
public List<ReportImageDto> Images { get; set; } = new();
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ReportImageDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
public long FileSize { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public DateTime UploadedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateUserDailyReportRequest
|
||||
{
|
||||
public required int DeviceId { get; set; }
|
||||
public required int UserId { get; set; }
|
||||
public required string PersianDate { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public required string Observations { get; set; }
|
||||
public required string Operations { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateUserDailyReportRequest
|
||||
{
|
||||
public required int Id { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public required string Observations { get; set; }
|
||||
public required string Operations { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UserDailyReportFilter
|
||||
{
|
||||
public int? DeviceId { get; set; }
|
||||
public int? UserId { get; set; }
|
||||
public string? PersianDate { get; set; }
|
||||
public int? Year { get; set; }
|
||||
public int? Month { get; set; }
|
||||
public int Page { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
}
|
||||
|
||||
// DTOs برای مدیریت توکن دستگاه
|
||||
|
||||
/// <summary>
|
||||
/// درخواست دریافت فاصله زمانی آپلود
|
||||
/// </summary>
|
||||
public sealed class GetUploadIntervalRequest
|
||||
{
|
||||
public int? DeviceId { get; set; }
|
||||
public string? DevicePhoneNumber { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// پاسخ دریافت فاصله زمانی آپلود
|
||||
/// </summary>
|
||||
public sealed class GetUploadIntervalResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public int? UploadIntervalMin { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// درخواست دریافت توکن دستگاه
|
||||
/// </summary>
|
||||
public sealed class RequestDeviceTokenRequest
|
||||
{
|
||||
public required string DevicePhoneNumber { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// پاسخ دریافت توکن دستگاه
|
||||
/// </summary>
|
||||
public sealed class RequestDeviceTokenResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public string? TokenCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// درخواست تایید توکن دستگاه
|
||||
/// </summary>
|
||||
public sealed class VerifyDeviceTokenRequest
|
||||
{
|
||||
public required string DevicePhoneNumber { get; set; }
|
||||
public required string VerificationCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// پاسخ تایید توکن دستگاه
|
||||
/// </summary>
|
||||
public sealed class VerifyDeviceTokenResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public string? EncodedSettings { get; set; }
|
||||
}
|
||||
59
src/GreenHome.Application/IAIQueryService.cs
Normal file
59
src/GreenHome.Application/IAIQueryService.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using GreenHome.Domain;
|
||||
|
||||
namespace GreenHome.Application;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing AI queries and their history
|
||||
/// </summary>
|
||||
public interface IAIQueryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves an AI query to the database
|
||||
/// </summary>
|
||||
Task<AIQuery> SaveQueryAsync(AIQuery query, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets AI query history for a specific device
|
||||
/// </summary>
|
||||
Task<List<AIQuery>> GetDeviceQueriesAsync(int deviceId, int take = 50, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets AI query history for a specific user
|
||||
/// </summary>
|
||||
Task<List<AIQuery>> GetUserQueriesAsync(int userId, int take = 50, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets total token usage for a device
|
||||
/// </summary>
|
||||
Task<int> GetDeviceTotalTokensAsync(int deviceId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets total token usage for a user
|
||||
/// </summary>
|
||||
Task<int> GetUserTotalTokensAsync(int userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets recent queries (all)
|
||||
/// </summary>
|
||||
Task<List<AIQuery>> GetRecentQueriesAsync(int take = 20, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets query statistics
|
||||
/// </summary>
|
||||
Task<AIQueryStats> GetStatsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Statistics about AI queries
|
||||
/// </summary>
|
||||
public class AIQueryStats
|
||||
{
|
||||
public int TotalQueries { get; set; }
|
||||
public int TotalTokensUsed { get; set; }
|
||||
public int TotalPromptTokens { get; set; }
|
||||
public int TotalCompletionTokens { get; set; }
|
||||
public double AverageResponseTimeMs { get; set; }
|
||||
public int TodayQueries { get; set; }
|
||||
public int TodayTokens { get; set; }
|
||||
}
|
||||
|
||||
12
src/GreenHome.Application/IAlertConditionService.cs
Normal file
12
src/GreenHome.Application/IAlertConditionService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace GreenHome.Application;
|
||||
|
||||
public interface IAlertConditionService
|
||||
{
|
||||
Task<IReadOnlyList<AlertConditionDto>> GetByDeviceIdAsync(int deviceId, CancellationToken cancellationToken);
|
||||
Task<AlertConditionDto?> GetByIdAsync(int id, CancellationToken cancellationToken);
|
||||
Task<int> CreateAsync(CreateAlertConditionRequest request, CancellationToken cancellationToken);
|
||||
Task UpdateAsync(UpdateAlertConditionRequest request, CancellationToken cancellationToken);
|
||||
Task DeleteAsync(int id, CancellationToken cancellationToken);
|
||||
Task<bool> ToggleEnabledAsync(int id, bool isEnabled, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
9
src/GreenHome.Application/IAlertLogService.cs
Normal file
9
src/GreenHome.Application/IAlertLogService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace GreenHome.Application;
|
||||
|
||||
public interface IAlertLogService
|
||||
{
|
||||
Task<PagedResult<AlertLogDto>> GetAlertLogsAsync(AlertLogFilter filter, CancellationToken cancellationToken);
|
||||
Task<AlertLogDto?> GetAlertLogByIdAsync(int id, CancellationToken cancellationToken);
|
||||
Task<int> CreateAlertLogAsync(AlertLogDto dto, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ namespace GreenHome.Application;
|
||||
public interface IAlertService
|
||||
{
|
||||
Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken);
|
||||
Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
12
src/GreenHome.Application/IChecklistService.cs
Normal file
12
src/GreenHome.Application/IChecklistService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace GreenHome.Application;
|
||||
|
||||
public interface IChecklistService
|
||||
{
|
||||
Task<ChecklistDto?> GetActiveChecklistByDeviceIdAsync(int deviceId, CancellationToken cancellationToken);
|
||||
Task<List<ChecklistDto>> GetChecklistsByDeviceIdAsync(int deviceId, CancellationToken cancellationToken);
|
||||
Task<ChecklistDto?> GetChecklistByIdAsync(int id, CancellationToken cancellationToken);
|
||||
Task<int> CreateChecklistAsync(CreateChecklistRequest request, CancellationToken cancellationToken);
|
||||
Task<List<ChecklistCompletionDto>> GetCompletionsByChecklistIdAsync(int checklistId, CancellationToken cancellationToken);
|
||||
Task<int> CompleteChecklistAsync(CompleteChecklistRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
17
src/GreenHome.Application/IDailyReportService.cs
Normal file
17
src/GreenHome.Application/IDailyReportService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace GreenHome.Application;
|
||||
|
||||
public interface IDailyReportService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or generates a daily analysis report for a device on a specific date
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing device ID and Persian date</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Daily report with AI analysis</returns>
|
||||
Task<DailyReportResponse> GetOrCreateDailyReportAsync(DailyReportRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<DailyReportResponse> GetWeeklyAnalysisAsync(WeeklyAnalysisRequest request, CancellationToken cancellationToken);
|
||||
|
||||
Task<DailyReportResponse> GetMonthlyAnalysisAsync(MonthlyAnalysisRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
14
src/GreenHome.Application/IDevicePostService.cs
Normal file
14
src/GreenHome.Application/IDevicePostService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace GreenHome.Application;
|
||||
|
||||
public interface IDevicePostService
|
||||
{
|
||||
Task<PagedResult<DevicePostDto>> GetPostsAsync(DevicePostFilter filter, CancellationToken cancellationToken);
|
||||
Task<DevicePostDto?> GetPostByIdAsync(int id, CancellationToken cancellationToken);
|
||||
Task<int> CreatePostAsync(CreateDevicePostRequest request, CancellationToken cancellationToken);
|
||||
Task UpdatePostAsync(UpdateDevicePostRequest request, CancellationToken cancellationToken);
|
||||
Task DeletePostAsync(int id, CancellationToken cancellationToken);
|
||||
Task<int> AddImageToPostAsync(int postId, string fileName, string filePath, string contentType, long fileSize, CancellationToken cancellationToken);
|
||||
Task DeleteImageAsync(int imageId, CancellationToken cancellationToken);
|
||||
Task<bool> CanUserAccessDeviceAsync(int userId, int deviceId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
23
src/GreenHome.Application/IDeviceTokenService.cs
Normal file
23
src/GreenHome.Application/IDeviceTokenService.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace GreenHome.Application;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس مدیریت توکن و تنظیمات دستگاه
|
||||
/// </summary>
|
||||
public interface IDeviceTokenService
|
||||
{
|
||||
/// <summary>
|
||||
/// دریافت فاصله زمانی آپلود بر اساس شماره تلفن یا شناسه دستگاه
|
||||
/// </summary>
|
||||
Task<GetUploadIntervalResponse> GetUploadIntervalAsync(GetUploadIntervalRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// درخواست توکن دستگاه (تولید و ارسال کد)
|
||||
/// </summary>
|
||||
Task<RequestDeviceTokenResponse> RequestDeviceTokenAsync(RequestDeviceTokenRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// تایید توکن دستگاه (ارسال تنظیمات)
|
||||
/// </summary>
|
||||
Task<VerifyDeviceTokenResponse> VerifyDeviceTokenAsync(VerifyDeviceTokenRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
45
src/GreenHome.Application/IMonthlyReportService.cs
Normal file
45
src/GreenHome.Application/IMonthlyReportService.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace GreenHome.Application;
|
||||
|
||||
public interface IMonthlyReportService
|
||||
{
|
||||
Task<MonthlyReportDto> GetMonthlyReportAsync(int deviceId, int year, int month, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class MonthlyReportDto
|
||||
{
|
||||
public int DeviceId { get; set; }
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
public int Year { get; set; }
|
||||
public int Month { get; set; }
|
||||
|
||||
// Alert Statistics
|
||||
public int TotalAlerts { get; set; }
|
||||
public int SmsAlerts { get; set; }
|
||||
public int CallAlerts { get; set; }
|
||||
public int SuccessfulAlerts { get; set; }
|
||||
public int FailedAlerts { get; set; }
|
||||
public int PowerOutageAlerts { get; set; }
|
||||
|
||||
// Telemetry Statistics
|
||||
public int TotalTelemetryRecords { get; set; }
|
||||
public decimal AverageTemperature { get; set; }
|
||||
public decimal MinTemperature { get; set; }
|
||||
public decimal MaxTemperature { get; set; }
|
||||
public decimal AverageHumidity { get; set; }
|
||||
public decimal MinHumidity { get; set; }
|
||||
public decimal MaxHumidity { get; set; }
|
||||
public decimal AverageLux { get; set; }
|
||||
public int AverageGasPPM { get; set; }
|
||||
public int MaxGasPPM { get; set; }
|
||||
|
||||
// User Activity
|
||||
public int UserDailyReportsCount { get; set; }
|
||||
public int ChecklistCompletionsCount { get; set; }
|
||||
public int DailyAnalysesCount { get; set; }
|
||||
|
||||
// Performance Summary
|
||||
public string PerformanceSummary { get; set; } = string.Empty;
|
||||
|
||||
public DateTime GeneratedAt { get; set; }
|
||||
}
|
||||
|
||||
14
src/GreenHome.Application/ISunCalculatorService.cs
Normal file
14
src/GreenHome.Application/ISunCalculatorService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace GreenHome.Application;
|
||||
|
||||
public interface ISunCalculatorService
|
||||
{
|
||||
/// <summary>
|
||||
/// بررسی میکند که آیا زمان داده شده در روز است یا شب
|
||||
/// </summary>
|
||||
/// <param name="dateTime">زمان UTC</param>
|
||||
/// <param name="latitude">عرض جغرافیایی</param>
|
||||
/// <param name="longitude">طول جغرافیایی</param>
|
||||
/// <returns>true اگر روز باشد، false اگر شب باشد</returns>
|
||||
bool IsDaytime(DateTime dateTime, decimal latitude, decimal longitude);
|
||||
}
|
||||
|
||||
13
src/GreenHome.Application/IUserDailyReportService.cs
Normal file
13
src/GreenHome.Application/IUserDailyReportService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace GreenHome.Application;
|
||||
|
||||
public interface IUserDailyReportService
|
||||
{
|
||||
Task<PagedResult<UserDailyReportDto>> GetReportsAsync(UserDailyReportFilter filter, CancellationToken cancellationToken);
|
||||
Task<UserDailyReportDto?> GetReportByIdAsync(int id, CancellationToken cancellationToken);
|
||||
Task<int> CreateReportAsync(CreateUserDailyReportRequest request, CancellationToken cancellationToken);
|
||||
Task UpdateReportAsync(UpdateUserDailyReportRequest request, CancellationToken cancellationToken);
|
||||
Task DeleteReportAsync(int id, CancellationToken cancellationToken);
|
||||
Task<int> AddImageToReportAsync(int reportId, string fileName, string filePath, string contentType, long fileSize, string? description, CancellationToken cancellationToken);
|
||||
Task DeleteImageAsync(int imageId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,51 @@ public sealed class MappingProfile : Profile
|
||||
.ReverseMap()
|
||||
.ForMember(dest => dest.Device, opt => opt.Ignore());
|
||||
|
||||
CreateMap<Domain.AlertCondition, AlertConditionDto>()
|
||||
.ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName))
|
||||
.ReverseMap()
|
||||
.ForMember(dest => dest.Device, opt => opt.Ignore());
|
||||
|
||||
CreateMap<Domain.AlertRule, AlertRuleDto>().ReverseMap()
|
||||
.ForMember(dest => dest.AlertCondition, opt => opt.Ignore());
|
||||
|
||||
CreateMap<CreateAlertRuleRequest, Domain.AlertRule>();
|
||||
|
||||
CreateMap<Domain.User, UserDto>().ReverseMap();
|
||||
|
||||
CreateMap<Domain.AlertLog, AlertLogDto>()
|
||||
.ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName))
|
||||
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User.Name))
|
||||
.ForMember(dest => dest.UserMobile, opt => opt.MapFrom(src => src.User.Mobile))
|
||||
.ReverseMap()
|
||||
.ForMember(dest => dest.Device, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.User, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.AlertCondition, opt => opt.Ignore());
|
||||
|
||||
CreateMap<Domain.UserDailyReport, UserDailyReportDto>()
|
||||
.ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName))
|
||||
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User.Name))
|
||||
.ForMember(dest => dest.UserFamily, opt => opt.MapFrom(src => src.User.Family));
|
||||
|
||||
CreateMap<Domain.ReportImage, ReportImageDto>();
|
||||
|
||||
CreateMap<Domain.Checklist, ChecklistDto>()
|
||||
.ForMember(dest => dest.DeviceName, opt => opt.MapFrom(src => src.Device.DeviceName))
|
||||
.ForMember(dest => dest.CreatedByUserName, opt => opt.MapFrom(src => src.CreatedByUser.Name + " " + src.CreatedByUser.Family));
|
||||
|
||||
CreateMap<Domain.ChecklistItem, ChecklistItemDto>();
|
||||
|
||||
CreateMap<Domain.ChecklistCompletion, ChecklistCompletionDto>()
|
||||
.ForMember(dest => dest.ChecklistTitle, opt => opt.MapFrom(src => src.Checklist.Title))
|
||||
.ForMember(dest => dest.CompletedByUserName, opt => opt.MapFrom(src => src.CompletedByUser.Name + " " + src.CompletedByUser.Family));
|
||||
|
||||
CreateMap<Domain.ChecklistItemCompletion, ChecklistItemCompletionDto>()
|
||||
.ForMember(dest => dest.ItemTitle, opt => opt.MapFrom(src => src.ChecklistItem.Title));
|
||||
|
||||
CreateMap<Domain.DevicePost, DevicePostDto>()
|
||||
.ForMember(dest => dest.AuthorName, opt => opt.MapFrom(src => src.AuthorUser.Name))
|
||||
.ForMember(dest => dest.AuthorFamily, opt => opt.MapFrom(src => src.AuthorUser.Family));
|
||||
|
||||
CreateMap<Domain.DevicePostImage, DevicePostImageDto>();
|
||||
}
|
||||
}
|
||||
|
||||
78
src/GreenHome.Domain/AIQuery.cs
Normal file
78
src/GreenHome.Domain/AIQuery.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
namespace GreenHome.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// AI Query record - stores questions, answers and token usage
|
||||
/// </summary>
|
||||
public class AIQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Device ID associated with the query (optional)
|
||||
/// </summary>
|
||||
public int? DeviceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigation property to Device
|
||||
/// </summary>
|
||||
public Device? Device { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User's question
|
||||
/// </summary>
|
||||
public required string Question { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AI's answer
|
||||
/// </summary>
|
||||
public required string Answer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of prompt tokens used
|
||||
/// </summary>
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of completion tokens used
|
||||
/// </summary>
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total tokens used (prompt + completion)
|
||||
/// </summary>
|
||||
public int TotalTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Model used for the query
|
||||
/// </summary>
|
||||
public string? Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Temperature parameter used
|
||||
/// </summary>
|
||||
public double? Temperature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the query was created
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Response time in milliseconds
|
||||
/// </summary>
|
||||
public long? ResponseTimeMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User ID (if authenticated)
|
||||
/// </summary>
|
||||
public int? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigation property to User
|
||||
/// </summary>
|
||||
public User? User { get; set; }
|
||||
}
|
||||
|
||||
110
src/GreenHome.Domain/AlertCondition.cs
Normal file
110
src/GreenHome.Domain/AlertCondition.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
namespace GreenHome.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// شرایط هشدار برای یک دستگاه
|
||||
/// </summary>
|
||||
public sealed class AlertCondition
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DeviceId { get; set; }
|
||||
public Device Device { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// نوع اعلان: Call = 0, SMS = 1
|
||||
/// </summary>
|
||||
public AlertNotificationType NotificationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// زمان اعمال: Day = 0, Night = 1, Always = 2
|
||||
/// </summary>
|
||||
public AlertTimeType TimeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// فاصله زمانی بین اعلانهای تماس (دقیقه) - پیشفرض 60
|
||||
/// </summary>
|
||||
public int CallCooldownMinutes { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// فاصله زمانی بین اعلانهای پیامک (دقیقه) - پیشفرض 15
|
||||
/// </summary>
|
||||
public int SmsCooldownMinutes { get; set; } = 15;
|
||||
|
||||
/// <summary>
|
||||
/// آیا این شرط فعال است؟
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// قوانین شرط - میتواند چندتا باشد که با AND به هم متصل میشوند
|
||||
/// </summary>
|
||||
public ICollection<AlertRule> Rules { get; set; } = new List<AlertRule>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// هر قانون یک شرط مستقل است
|
||||
/// </summary>
|
||||
public sealed class AlertRule
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int AlertConditionId { get; set; }
|
||||
public AlertCondition AlertCondition { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// نوع سنسور: Temperature, Humidity, Soil, Gas, Lux
|
||||
/// </summary>
|
||||
public SensorType SensorType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نوع مقایسه: GreaterThan, LessThan, Between, OutOfRange
|
||||
/// </summary>
|
||||
public ComparisonType ComparisonType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// مقدار عددی اول
|
||||
/// </summary>
|
||||
public decimal Value1 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// مقدار عددی دوم (برای Between و OutOfRange)
|
||||
/// </summary>
|
||||
public decimal? Value2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ترتیب نمایش قوانین
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
}
|
||||
|
||||
public enum AlertNotificationType
|
||||
{
|
||||
Call = 0,
|
||||
SMS = 1
|
||||
}
|
||||
|
||||
public enum AlertTimeType
|
||||
{
|
||||
Day = 0,
|
||||
Night = 1,
|
||||
Always = 2
|
||||
}
|
||||
|
||||
public enum SensorType
|
||||
{
|
||||
Temperature = 0,
|
||||
Humidity = 1,
|
||||
Soil = 2,
|
||||
Gas = 3,
|
||||
Lux = 4
|
||||
}
|
||||
|
||||
public enum ComparisonType
|
||||
{
|
||||
GreaterThan = 0,
|
||||
LessThan = 1,
|
||||
Between = 2,
|
||||
OutOfRange = 3
|
||||
}
|
||||
|
||||
110
src/GreenHome.Domain/AlertLog.cs
Normal file
110
src/GreenHome.Domain/AlertLog.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
namespace GreenHome.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// لاگ هشدارهای ارسال شده
|
||||
/// </summary>
|
||||
public sealed class AlertLog
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه دستگاه
|
||||
/// </summary>
|
||||
public int DeviceId { get; set; }
|
||||
public Device Device { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// شناسه کاربری که هشدار به او ارسال شده
|
||||
/// </summary>
|
||||
public int UserId { get; set; }
|
||||
public User User { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// شناسه شرط هشدار (اگر مربوط به شرط خاصی بود)
|
||||
/// </summary>
|
||||
public int? AlertConditionId { get; set; }
|
||||
public AlertCondition? AlertCondition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نوع هشدار (SMS, Call, PowerOutage)
|
||||
/// </summary>
|
||||
public AlertType AlertType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نوع اعلان (SMS یا Call)
|
||||
/// </summary>
|
||||
public AlertNotificationType NotificationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// پیام هشدار
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// وضعیت ارسال
|
||||
/// </summary>
|
||||
public AlertStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// پیام خطا (در صورت شکست)
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شماره تماس یا پیامک
|
||||
/// </summary>
|
||||
public string PhoneNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// زمان ارسال
|
||||
/// </summary>
|
||||
public DateTime SentAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// مدت زمان پردازش (میلیثانیه)
|
||||
/// </summary>
|
||||
public long ProcessingTimeMs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// نوع هشدار
|
||||
/// </summary>
|
||||
public enum AlertType
|
||||
{
|
||||
/// <summary>
|
||||
/// هشدار بر اساس شرط
|
||||
/// </summary>
|
||||
Condition = 1,
|
||||
|
||||
/// <summary>
|
||||
/// هشدار قطع برق
|
||||
/// </summary>
|
||||
PowerOutage = 2,
|
||||
|
||||
/// <summary>
|
||||
/// هشدار دستی
|
||||
/// </summary>
|
||||
Manual = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// وضعیت ارسال هشدار
|
||||
/// </summary>
|
||||
public enum AlertStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// با موفقیت ارسال شد
|
||||
/// </summary>
|
||||
Success = 1,
|
||||
|
||||
/// <summary>
|
||||
/// با خطا مواجه شد
|
||||
/// </summary>
|
||||
Failed = 2,
|
||||
|
||||
/// <summary>
|
||||
/// در صف ارسال
|
||||
/// </summary>
|
||||
Pending = 3
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ public sealed class AlertNotification
|
||||
public Device Device { get; set; } = null!;
|
||||
public int UserId { get; set; }
|
||||
public User User { get; set; } = null!;
|
||||
public string AlertType { get; set; } = string.Empty; // Temperature, Humidity, Soil, Gas, Lux
|
||||
public int? AlertConditionId { get; set; }
|
||||
public AlertCondition? AlertCondition { get; set; }
|
||||
public AlertNotificationType NotificationType { get; set; } // Call or SMS
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string? MessageOutboxIds { get; set; } // JSON array of message outbox IDs
|
||||
public string? MessageOutboxIds { get; set; } // JSON array of message outbox IDs (for SMS)
|
||||
public string? ErrorMessage { get; set; } // Error details if sending failed
|
||||
public DateTime SentAt { get; set; } = DateTime.UtcNow;
|
||||
public bool IsSent { get; set; } = true;
|
||||
|
||||
156
src/GreenHome.Domain/Checklist.cs
Normal file
156
src/GreenHome.Domain/Checklist.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
namespace GreenHome.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// چکلیست دستگاه
|
||||
/// </summary>
|
||||
public sealed class Checklist
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه دستگاه
|
||||
/// </summary>
|
||||
public int DeviceId { get; set; }
|
||||
public Device Device { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// عنوان چکلیست
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// توضیحات
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا این چکلیست فعال است؟ (فقط یک چکلیست فعال برای هر دستگاه)
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// آیتمهای چکلیست
|
||||
/// </summary>
|
||||
public ICollection<ChecklistItem> Items { get; set; } = new List<ChecklistItem>();
|
||||
|
||||
/// <summary>
|
||||
/// سابقه تکمیلهای چکلیست
|
||||
/// </summary>
|
||||
public ICollection<ChecklistCompletion> Completions { get; set; } = new List<ChecklistCompletion>();
|
||||
|
||||
/// <summary>
|
||||
/// زمان ایجاد
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه کاربر ایجاد کننده
|
||||
/// </summary>
|
||||
public int CreatedByUserId { get; set; }
|
||||
public User CreatedByUser { get; set; } = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// آیتم چکلیست
|
||||
/// </summary>
|
||||
public sealed class ChecklistItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه چکلیست
|
||||
/// </summary>
|
||||
public int ChecklistId { get; set; }
|
||||
public Checklist Checklist { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// عنوان آیتم
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// توضیحات
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ترتیب نمایش
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا اجباری است؟
|
||||
/// </summary>
|
||||
public bool IsRequired { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// سابقه تکمیل چکلیست
|
||||
/// </summary>
|
||||
public sealed class ChecklistCompletion
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه چکلیست
|
||||
/// </summary>
|
||||
public int ChecklistId { get; set; }
|
||||
public Checklist Checklist { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// شناسه کاربر انجام دهنده
|
||||
/// </summary>
|
||||
public int CompletedByUserId { get; set; }
|
||||
public User CompletedByUser { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// تاریخ شمسی تکمیل
|
||||
/// </summary>
|
||||
public string PersianDate { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// آیتمهای چک شده
|
||||
/// </summary>
|
||||
public ICollection<ChecklistItemCompletion> ItemCompletions { get; set; } = new List<ChecklistItemCompletion>();
|
||||
|
||||
/// <summary>
|
||||
/// یادداشتهای اضافی
|
||||
/// </summary>
|
||||
public string? Notes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// زمان تکمیل
|
||||
/// </summary>
|
||||
public DateTime CompletedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تکمیل آیتم چکلیست
|
||||
/// </summary>
|
||||
public sealed class ChecklistItemCompletion
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه تکمیل چکلیست
|
||||
/// </summary>
|
||||
public int ChecklistCompletionId { get; set; }
|
||||
public ChecklistCompletion ChecklistCompletion { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// شناسه آیتم چکلیست
|
||||
/// </summary>
|
||||
public int ChecklistItemId { get; set; }
|
||||
public ChecklistItem ChecklistItem { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// آیا چک شده؟
|
||||
/// </summary>
|
||||
public bool IsChecked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// یادداشت برای این آیتم
|
||||
/// </summary>
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
|
||||
88
src/GreenHome.Domain/DailyReport.cs
Normal file
88
src/GreenHome.Domain/DailyReport.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
namespace GreenHome.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Daily AI analysis report for greenhouse telemetry data
|
||||
/// </summary>
|
||||
public class DailyReport
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Device ID
|
||||
/// </summary>
|
||||
public int DeviceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigation property to Device
|
||||
/// </summary>
|
||||
public Device? Device { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Persian date for the report (yyyy/MM/dd)
|
||||
/// </summary>
|
||||
public required string PersianDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Persian year
|
||||
/// </summary>
|
||||
public int PersianYear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Persian month
|
||||
/// </summary>
|
||||
public int PersianMonth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Persian day
|
||||
/// </summary>
|
||||
public int PersianDay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AI analysis report
|
||||
/// </summary>
|
||||
public required string Analysis { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of telemetry records used
|
||||
/// </summary>
|
||||
public int RecordCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of records sent to AI (after sampling)
|
||||
/// </summary>
|
||||
public int SampledRecordCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of prompt tokens used
|
||||
/// </summary>
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of completion tokens used
|
||||
/// </summary>
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total tokens used
|
||||
/// </summary>
|
||||
public int TotalTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Model used for analysis
|
||||
/// </summary>
|
||||
public string? Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the report was created
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Response time in milliseconds
|
||||
/// </summary>
|
||||
public long? ResponseTimeMs { get; set; }
|
||||
}
|
||||
|
||||
81
src/GreenHome.Domain/DevicePost.cs
Normal file
81
src/GreenHome.Domain/DevicePost.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
namespace GreenHome.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// پستهای گروه مجازی دستگاه (تایملاین مشترک)
|
||||
/// </summary>
|
||||
public sealed class DevicePost
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه دستگاه
|
||||
/// </summary>
|
||||
public int DeviceId { get; set; }
|
||||
public Device Device { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// شناسه کاربر نویسنده پست
|
||||
/// </summary>
|
||||
public int AuthorUserId { get; set; }
|
||||
public User AuthorUser { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// متن پست
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// تصاویر پیوست
|
||||
/// </summary>
|
||||
public ICollection<DevicePostImage> Images { get; set; } = new List<DevicePostImage>();
|
||||
|
||||
/// <summary>
|
||||
/// زمان ایجاد
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// زمان آخرین ویرایش
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تصاویر پیوست پست
|
||||
/// </summary>
|
||||
public sealed class DevicePostImage
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه پست
|
||||
/// </summary>
|
||||
public int DevicePostId { get; set; }
|
||||
public DevicePost DevicePost { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// نام فایل
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// مسیر ذخیره فایل
|
||||
/// </summary>
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// نوع فایل (MIME type)
|
||||
/// </summary>
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// حجم فایل (بایت)
|
||||
/// </summary>
|
||||
public long FileSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// زمان آپلود
|
||||
/// </summary>
|
||||
public DateTime UploadedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,28 +1,101 @@
|
||||
namespace GreenHome.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// نوع سیم کارت
|
||||
/// </summary>
|
||||
public enum SimCardType
|
||||
{
|
||||
/// <summary>
|
||||
/// همراه اول
|
||||
/// </summary>
|
||||
Hamrahe_Aval = 1,
|
||||
|
||||
/// <summary>
|
||||
/// ایرانسل
|
||||
/// </summary>
|
||||
Irancell = 2,
|
||||
|
||||
/// <summary>
|
||||
/// رایتل
|
||||
/// </summary>
|
||||
Rightel = 3
|
||||
}
|
||||
|
||||
public sealed class DeviceSettings
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DeviceId { get; set; }
|
||||
public Device Device { get; set; } = null!;
|
||||
|
||||
// Temperature settings
|
||||
public decimal DangerMaxTemperature { get; set; } // decimal(18,2)
|
||||
public decimal DangerMinTemperature { get; set; } // decimal(18,2)
|
||||
public decimal MaxTemperature { get; set; } // decimal(18,2)
|
||||
public decimal MinTemperature { get; set; } // decimal(18,2)
|
||||
/// <summary>
|
||||
/// استان
|
||||
/// </summary>
|
||||
public string Province { get; set; } = string.Empty;
|
||||
|
||||
// Gas settings
|
||||
public int MaxGasPPM { get; set; }
|
||||
public int MinGasPPM { get; set; }
|
||||
/// <summary>
|
||||
/// شهر
|
||||
/// </summary>
|
||||
public string City { get; set; } = string.Empty;
|
||||
|
||||
// Light settings
|
||||
public decimal MaxLux { get; set; } // decimal(18,2)
|
||||
public decimal MinLux { get; set; } // decimal(18,2)
|
||||
/// <summary>
|
||||
/// عرض جغرافیایی (برای محاسبه طلوع و غروب)
|
||||
/// </summary>
|
||||
public decimal? Latitude { get; set; }
|
||||
|
||||
// Humidity settings
|
||||
public decimal MaxHumidityPercent { get; set; } // decimal(18,2)
|
||||
public decimal MinHumidityPercent { get; set; } // decimal(18,2)
|
||||
/// <summary>
|
||||
/// طول جغرافیایی (برای محاسبه طلوع و غروب)
|
||||
/// </summary>
|
||||
public decimal? Longitude { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نوع محصول
|
||||
/// </summary>
|
||||
public string ProductType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// حداقل فاصله زمانی ارسال پیامک (به دقیقه)
|
||||
/// </summary>
|
||||
public int MinimumSmsIntervalMinutes { get; set; } = 15;
|
||||
|
||||
/// <summary>
|
||||
/// حداقل فاصله زمانی تماس (به دقیقه)
|
||||
/// </summary>
|
||||
public int MinimumCallIntervalMinutes { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// مساحت گلخانه (متر مربع)
|
||||
/// </summary>
|
||||
public decimal? AreaSquareMeters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// فاصله زمانی آپلود داده (به دقیقه)
|
||||
/// </summary>
|
||||
public int UploadIntervalMin { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// شماره تلفن دستگاه
|
||||
/// </summary>
|
||||
public string DevicePhoneNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// نوع سیم کارت
|
||||
/// </summary>
|
||||
public SimCardType? SimCardType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// کد توکن (5 رقمی)
|
||||
/// </summary>
|
||||
public string? TokenCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// کد تایید (5 رقمی)
|
||||
/// </summary>
|
||||
public string? VerificationCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// تاریخ انقضای توکن
|
||||
/// </summary>
|
||||
public DateTime? TokenExpiresAt { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
@@ -6,5 +6,10 @@ public sealed class DeviceUser
|
||||
public Device Device { get; set; } = null!;
|
||||
public int UserId { get; set; }
|
||||
public User User { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// آیا این کاربر باید هشدارهای این دستگاه را دریافت کند؟
|
||||
/// </summary>
|
||||
public bool ReceiveAlerts { get; set; } = true;
|
||||
}
|
||||
|
||||
|
||||
121
src/GreenHome.Domain/UserDailyReport.cs
Normal file
121
src/GreenHome.Domain/UserDailyReport.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
namespace GreenHome.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// گزارش روزانه کاربر (مشاهدات و عملیات انجام شده)
|
||||
/// </summary>
|
||||
public sealed class UserDailyReport
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه دستگاه
|
||||
/// </summary>
|
||||
public int DeviceId { get; set; }
|
||||
public Device Device { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// شناسه کاربر گزارشدهنده
|
||||
/// </summary>
|
||||
public int UserId { get; set; }
|
||||
public User User { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// تاریخ شمسی (yyyy/MM/dd)
|
||||
/// </summary>
|
||||
public string PersianDate { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// سال شمسی
|
||||
/// </summary>
|
||||
public int PersianYear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ماه شمسی
|
||||
/// </summary>
|
||||
public int PersianMonth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// روز شمسی
|
||||
/// </summary>
|
||||
public int PersianDay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// عنوان گزارش
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// شرح مشاهدات
|
||||
/// </summary>
|
||||
public string Observations { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// عملیات انجام شده
|
||||
/// </summary>
|
||||
public string Operations { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// یادداشتهای اضافی
|
||||
/// </summary>
|
||||
public string? Notes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// تصاویر پیوست
|
||||
/// </summary>
|
||||
public ICollection<ReportImage> Images { get; set; } = new List<ReportImage>();
|
||||
|
||||
/// <summary>
|
||||
/// زمان ایجاد
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// زمان آخرین ویرایش
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تصاویر پیوست گزارش روزانه
|
||||
/// </summary>
|
||||
public sealed class ReportImage
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه گزارش
|
||||
/// </summary>
|
||||
public int UserDailyReportId { get; set; }
|
||||
public UserDailyReport UserDailyReport { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// نام فایل
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// مسیر ذخیره فایل
|
||||
/// </summary>
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// نوع فایل (MIME type)
|
||||
/// </summary>
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// حجم فایل (بایت)
|
||||
/// </summary>
|
||||
public long FileSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// توضیحات تصویر
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// زمان آپلود
|
||||
/// </summary>
|
||||
public DateTime UploadedAt { get; set; }
|
||||
}
|
||||
|
||||
126
src/GreenHome.Infrastructure/AIQueryService.cs
Normal file
126
src/GreenHome.Infrastructure/AIQueryService.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using GreenHome.Application;
|
||||
using GreenHome.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class AIQueryService : IAIQueryService
|
||||
{
|
||||
private readonly GreenHomeDbContext dbContext;
|
||||
private readonly ILogger<AIQueryService> logger;
|
||||
|
||||
public AIQueryService(
|
||||
GreenHomeDbContext dbContext,
|
||||
ILogger<AIQueryService> logger)
|
||||
{
|
||||
this.dbContext = dbContext;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task<AIQuery> SaveQueryAsync(AIQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
dbContext.AIQueries.Add(query);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("AI query saved: {QueryId}, Tokens: {TotalTokens}",
|
||||
query.Id, query.TotalTokens);
|
||||
|
||||
return query;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error saving AI query");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<AIQuery>> GetDeviceQueriesAsync(
|
||||
int deviceId,
|
||||
int take = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await dbContext.AIQueries
|
||||
.Where(q => q.DeviceId == deviceId)
|
||||
.OrderByDescending(q => q.CreatedAt)
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<AIQuery>> GetUserQueriesAsync(
|
||||
int userId,
|
||||
int take = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await dbContext.AIQueries
|
||||
.Where(q => q.UserId == userId)
|
||||
.OrderByDescending(q => q.CreatedAt)
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> GetDeviceTotalTokensAsync(
|
||||
int deviceId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await dbContext.AIQueries
|
||||
.Where(q => q.DeviceId == deviceId)
|
||||
.SumAsync(q => q.TotalTokens, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> GetUserTotalTokensAsync(
|
||||
int userId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await dbContext.AIQueries
|
||||
.Where(q => q.UserId == userId)
|
||||
.SumAsync(q => q.TotalTokens, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<AIQuery>> GetRecentQueriesAsync(
|
||||
int take = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await dbContext.AIQueries
|
||||
.Include(q => q.Device)
|
||||
.Include(q => q.User)
|
||||
.OrderByDescending(q => q.CreatedAt)
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<AIQueryStats> GetStatsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var today = DateTime.UtcNow.Date;
|
||||
|
||||
var allQueries = await dbContext.AIQueries
|
||||
.Select(q => new
|
||||
{
|
||||
q.TotalTokens,
|
||||
q.PromptTokens,
|
||||
q.CompletionTokens,
|
||||
q.ResponseTimeMs,
|
||||
q.CreatedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var todayQueries = allQueries.Where(q => q.CreatedAt >= today).ToList();
|
||||
|
||||
return new AIQueryStats
|
||||
{
|
||||
TotalQueries = allQueries.Count,
|
||||
TotalTokensUsed = allQueries.Sum(q => q.TotalTokens),
|
||||
TotalPromptTokens = allQueries.Sum(q => q.PromptTokens),
|
||||
TotalCompletionTokens = allQueries.Sum(q => q.CompletionTokens),
|
||||
AverageResponseTimeMs = allQueries.Any()
|
||||
? allQueries.Where(q => q.ResponseTimeMs.HasValue)
|
||||
.Average(q => q.ResponseTimeMs ?? 0)
|
||||
: 0,
|
||||
TodayQueries = todayQueries.Count,
|
||||
TodayTokens = todayQueries.Sum(q => q.TotalTokens)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
131
src/GreenHome.Infrastructure/AlertConditionService.cs
Normal file
131
src/GreenHome.Infrastructure/AlertConditionService.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using AutoMapper;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class AlertConditionService : IAlertConditionService
|
||||
{
|
||||
private readonly GreenHomeDbContext dbContext;
|
||||
private readonly IMapper mapper;
|
||||
|
||||
public AlertConditionService(GreenHomeDbContext dbContext, IMapper mapper)
|
||||
{
|
||||
this.dbContext = dbContext;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AlertConditionDto>> GetByDeviceIdAsync(int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var conditions = await dbContext.AlertConditions
|
||||
.Include(x => x.Device)
|
||||
.Include(x => x.Rules)
|
||||
.Where(x => x.DeviceId == deviceId)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return mapper.Map<List<AlertConditionDto>>(conditions);
|
||||
}
|
||||
|
||||
public async Task<AlertConditionDto?> GetByIdAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var condition = await dbContext.AlertConditions
|
||||
.Include(x => x.Device)
|
||||
.Include(x => x.Rules)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
return condition != null ? mapper.Map<AlertConditionDto>(condition) : null;
|
||||
}
|
||||
|
||||
public async Task<int> CreateAsync(CreateAlertConditionRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var condition = new Domain.AlertCondition
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
NotificationType = request.NotificationType,
|
||||
TimeType = request.TimeType,
|
||||
CallCooldownMinutes = request.CallCooldownMinutes,
|
||||
SmsCooldownMinutes = request.SmsCooldownMinutes,
|
||||
IsEnabled = request.IsEnabled,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Add rules
|
||||
var rules = mapper.Map<List<Domain.AlertRule>>(request.Rules);
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
condition.Rules.Add(rule);
|
||||
}
|
||||
|
||||
dbContext.AlertConditions.Add(condition);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return condition.Id;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(UpdateAlertConditionRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var condition = await dbContext.AlertConditions
|
||||
.Include(x => x.Rules)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
|
||||
|
||||
if (condition == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Alert condition not found: {request.Id}");
|
||||
}
|
||||
|
||||
condition.NotificationType = request.NotificationType;
|
||||
condition.TimeType = request.TimeType;
|
||||
condition.CallCooldownMinutes = request.CallCooldownMinutes;
|
||||
condition.SmsCooldownMinutes = request.SmsCooldownMinutes;
|
||||
condition.IsEnabled = request.IsEnabled;
|
||||
condition.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Remove old rules and add new ones
|
||||
dbContext.AlertRules.RemoveRange(condition.Rules);
|
||||
condition.Rules.Clear();
|
||||
|
||||
var newRules = mapper.Map<List<Domain.AlertRule>>(request.Rules);
|
||||
foreach (var rule in newRules)
|
||||
{
|
||||
condition.Rules.Add(rule);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var condition = await dbContext.AlertConditions
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
if (condition == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Alert condition not found: {id}");
|
||||
}
|
||||
|
||||
dbContext.AlertConditions.Remove(condition);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> ToggleEnabledAsync(int id, bool isEnabled, CancellationToken cancellationToken)
|
||||
{
|
||||
var condition = await dbContext.AlertConditions
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
if (condition == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
condition.IsEnabled = isEnabled;
|
||||
condition.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
100
src/GreenHome.Infrastructure/AlertLogService.cs
Normal file
100
src/GreenHome.Infrastructure/AlertLogService.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using AutoMapper;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class AlertLogService : IAlertLogService
|
||||
{
|
||||
private readonly GreenHomeDbContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public AlertLogService(GreenHomeDbContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<AlertLogDto>> GetAlertLogsAsync(
|
||||
AlertLogFilter filter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.AlertLogs
|
||||
.Include(x => x.Device)
|
||||
.Include(x => x.User)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
// Apply filters
|
||||
if (filter.DeviceId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.DeviceId == filter.DeviceId.Value);
|
||||
}
|
||||
|
||||
if (filter.UserId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.UserId == filter.UserId.Value);
|
||||
}
|
||||
|
||||
if (filter.AlertType.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.AlertType == filter.AlertType.Value);
|
||||
}
|
||||
|
||||
if (filter.Status.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.Status == filter.Status.Value);
|
||||
}
|
||||
|
||||
if (filter.StartDate.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.SentAt >= filter.StartDate.Value);
|
||||
}
|
||||
|
||||
if (filter.EndDate.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.SentAt <= filter.EndDate.Value);
|
||||
}
|
||||
|
||||
// Get total count
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
// Apply pagination and ordering
|
||||
var items = await query
|
||||
.OrderByDescending(x => x.SentAt)
|
||||
.Skip((filter.Page - 1) * filter.PageSize)
|
||||
.Take(filter.PageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var dtos = _mapper.Map<List<AlertLogDto>>(items);
|
||||
|
||||
return new PagedResult<AlertLogDto>
|
||||
{
|
||||
Items = dtos,
|
||||
TotalCount = totalCount,
|
||||
Page = filter.Page,
|
||||
PageSize = filter.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<AlertLogDto?> GetAlertLogByIdAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var log = await _context.AlertLogs
|
||||
.Include(x => x.Device)
|
||||
.Include(x => x.User)
|
||||
.Include(x => x.AlertCondition)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
return log != null ? _mapper.Map<AlertLogDto>(log) : null;
|
||||
}
|
||||
|
||||
public async Task<int> CreateAlertLogAsync(AlertLogDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = _mapper.Map<Domain.AlertLog>(dto);
|
||||
_context.AlertLogs.Add(entity);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using GreenHome.Application;
|
||||
using GreenHome.Sms.Ippanel;
|
||||
using GreenHome.VoiceCall.Avanak;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
@@ -12,280 +13,434 @@ public sealed class AlertService : IAlertService
|
||||
private readonly GreenHomeDbContext dbContext;
|
||||
private readonly IDeviceSettingsService deviceSettingsService;
|
||||
private readonly ISmsService smsService;
|
||||
private readonly IVoiceCallService voiceCallService;
|
||||
private readonly ISunCalculatorService sunCalculatorService;
|
||||
private readonly ILogger<AlertService> logger;
|
||||
private const int AlertCooldownMinutes = 10;
|
||||
|
||||
private sealed record AlertInfo(
|
||||
string Type,
|
||||
string Message,
|
||||
string ParameterName,
|
||||
decimal Value,
|
||||
string Status
|
||||
);
|
||||
|
||||
public AlertService(
|
||||
GreenHomeDbContext dbContext,
|
||||
IDeviceSettingsService deviceSettingsService,
|
||||
ISmsService smsService,
|
||||
IVoiceCallService voiceCallService,
|
||||
ISunCalculatorService sunCalculatorService,
|
||||
ILogger<AlertService> logger)
|
||||
{
|
||||
this.dbContext = dbContext;
|
||||
this.deviceSettingsService = deviceSettingsService;
|
||||
this.smsService = smsService;
|
||||
this.voiceCallService = voiceCallService;
|
||||
this.sunCalculatorService = sunCalculatorService;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await deviceSettingsService.GetByDeviceIdAsync(deviceId, cancellationToken);
|
||||
if (settings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get device with all users who should receive alerts
|
||||
var device = await dbContext.Devices
|
||||
.Include(d => d.User)
|
||||
.Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts))
|
||||
.ThenInclude(du => du.User)
|
||||
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
||||
|
||||
if (device == null || device.User == null)
|
||||
if (device == null)
|
||||
{
|
||||
logger.LogWarning("Device not found: DeviceId={DeviceId}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
var alerts = CollectAlerts(telemetry, settings, device.DeviceName);
|
||||
// Get all users who should receive alerts
|
||||
var usersToAlert = device.DeviceUsers
|
||||
.Where(du => du.ReceiveAlerts)
|
||||
.Select(du => du.User)
|
||||
.ToList();
|
||||
|
||||
foreach (var alert in alerts)
|
||||
if (usersToAlert.Count == 0)
|
||||
{
|
||||
await SendAlertIfNeededAsync(deviceId, device.User.Id, device.DeviceName, alert, cancellationToken);
|
||||
logger.LogInformation("No users with ReceiveAlerts enabled for device: DeviceId={DeviceId}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get device settings for location
|
||||
var settings = await deviceSettingsService.GetByDeviceIdAsync(deviceId, cancellationToken);
|
||||
|
||||
// Get all enabled alert conditions for this device
|
||||
var conditions = await dbContext.AlertConditions
|
||||
.Include(c => c.Rules)
|
||||
.Where(c => c.DeviceId == deviceId && c.IsEnabled)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (!conditions.Any())
|
||||
{
|
||||
logger.LogDebug("No enabled alert conditions for device: DeviceId={DeviceId}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if it's daytime or nighttime
|
||||
bool? isDaytime = null;
|
||||
if (settings?.Latitude != null && settings.Longitude != null)
|
||||
{
|
||||
isDaytime = sunCalculatorService.IsDaytime(DateTime.UtcNow, settings.Latitude.Value, settings.Longitude.Value);
|
||||
}
|
||||
|
||||
// Check each condition
|
||||
foreach (var condition in conditions)
|
||||
{
|
||||
// Check time type filter
|
||||
if (condition.TimeType == Domain.AlertTimeType.Day && isDaytime == false)
|
||||
{
|
||||
continue; // This condition is for daytime only, but it's nighttime
|
||||
}
|
||||
if (condition.TimeType == Domain.AlertTimeType.Night && isDaytime == true)
|
||||
{
|
||||
continue; // This condition is for nighttime only, but it's daytime
|
||||
}
|
||||
|
||||
// Check if all rules match (AND logic)
|
||||
var allRulesMatch = condition.Rules.All(rule => CheckRule(rule, telemetry));
|
||||
|
||||
if (allRulesMatch && condition.Rules.Any())
|
||||
{
|
||||
// All rules passed, send alert if cooldown period has passed
|
||||
await SendAlertForConditionAsync(condition, device, usersToAlert, telemetry, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<AlertInfo> CollectAlerts(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName)
|
||||
private bool CheckRule(Domain.AlertRule rule, TelemetryDto telemetry)
|
||||
{
|
||||
var alerts = new List<AlertInfo>();
|
||||
// Get sensor value
|
||||
var sensorValue = rule.SensorType switch
|
||||
{
|
||||
Domain.SensorType.Temperature => telemetry.TemperatureC,
|
||||
Domain.SensorType.Humidity => telemetry.HumidityPercent,
|
||||
Domain.SensorType.Soil => telemetry.SoilPercent,
|
||||
Domain.SensorType.Gas => telemetry.GasPPM,
|
||||
Domain.SensorType.Lux => telemetry.Lux,
|
||||
_ => 0m
|
||||
};
|
||||
|
||||
CheckTemperatureAlert(telemetry, settings, deviceName, alerts);
|
||||
CheckHumidityAlert(telemetry, settings, deviceName, alerts);
|
||||
CheckSoilAlert(telemetry, deviceName, alerts);
|
||||
CheckGasAlert(telemetry, settings, deviceName, alerts);
|
||||
CheckLuxAlert(telemetry, settings, deviceName, alerts);
|
||||
|
||||
return alerts;
|
||||
// Check comparison
|
||||
return rule.ComparisonType switch
|
||||
{
|
||||
Domain.ComparisonType.GreaterThan => sensorValue > rule.Value1,
|
||||
Domain.ComparisonType.LessThan => sensorValue < rule.Value1,
|
||||
Domain.ComparisonType.Between => rule.Value2 != null && sensorValue >= rule.Value1 && sensorValue <= rule.Value2.Value,
|
||||
Domain.ComparisonType.OutOfRange => rule.Value2 != null && (sensorValue < rule.Value1 || sensorValue > rule.Value2.Value),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private void CheckTemperatureAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
|
||||
{
|
||||
if (telemetry.TemperatureC > settings.MaxTemperature)
|
||||
{
|
||||
alerts.Add(new AlertInfo(
|
||||
Type: "Temperature",
|
||||
Message: $"هشدار: دمای گلخانه {deviceName} به {telemetry.TemperatureC} درجه رسیده که از حداکثر مجاز ({settings.MaxTemperature}) بیشتر است.",
|
||||
ParameterName: "دما",
|
||||
Value: telemetry.TemperatureC,
|
||||
Status: "بالاتر"
|
||||
));
|
||||
}
|
||||
else if (telemetry.TemperatureC < settings.MinTemperature)
|
||||
{
|
||||
alerts.Add(new AlertInfo(
|
||||
Type: "Temperature",
|
||||
Message: $"هشدار: دمای گلخانه {deviceName} به {telemetry.TemperatureC} درجه رسیده که از حداقل مجاز ({settings.MinTemperature}) کمتر است.",
|
||||
ParameterName: "دما",
|
||||
Value: telemetry.TemperatureC,
|
||||
Status: "پایینتر"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckHumidityAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
|
||||
{
|
||||
if (telemetry.HumidityPercent > settings.MaxHumidityPercent)
|
||||
{
|
||||
alerts.Add(new AlertInfo(
|
||||
Type: "Humidity",
|
||||
Message: $"هشدار: رطوبت گلخانه {deviceName} به {telemetry.HumidityPercent}% رسیده که از حداکثر مجاز ({settings.MaxHumidityPercent}%) بیشتر است.",
|
||||
ParameterName: "رطوبت",
|
||||
Value: telemetry.HumidityPercent,
|
||||
Status: "بالاتر"
|
||||
));
|
||||
}
|
||||
else if (telemetry.HumidityPercent < settings.MinHumidityPercent)
|
||||
{
|
||||
alerts.Add(new AlertInfo(
|
||||
Type: "Humidity",
|
||||
Message: $"هشدار: رطوبت گلخانه {deviceName} به {telemetry.HumidityPercent}% رسیده که از حداقل مجاز ({settings.MinHumidityPercent}%) کمتر است.",
|
||||
ParameterName: "رطوبت",
|
||||
Value: telemetry.HumidityPercent,
|
||||
Status: "پایینتر"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckSoilAlert(TelemetryDto telemetry, string deviceName, List<AlertInfo> alerts)
|
||||
{
|
||||
if (telemetry.SoilPercent > 100)
|
||||
{
|
||||
alerts.Add(new AlertInfo(
|
||||
Type: "Soil",
|
||||
Message: $"هشدار: رطوبت خاک گلخانه {deviceName} مقدار نامعتبر ({telemetry.SoilPercent}%) دارد.",
|
||||
ParameterName: "رطوبت خاک",
|
||||
Value: telemetry.SoilPercent,
|
||||
Status: "بالاتر"
|
||||
));
|
||||
}
|
||||
else if (telemetry.SoilPercent < 0)
|
||||
{
|
||||
alerts.Add(new AlertInfo(
|
||||
Type: "Soil",
|
||||
Message: $"هشدار: رطوبت خاک گلخانه {deviceName} مقدار نامعتبر ({telemetry.SoilPercent}%) دارد.",
|
||||
ParameterName: "رطوبت خاک",
|
||||
Value: telemetry.SoilPercent,
|
||||
Status: "پایینتر"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckGasAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
|
||||
{
|
||||
if (telemetry.GasPPM > settings.MaxGasPPM)
|
||||
{
|
||||
alerts.Add(new AlertInfo(
|
||||
Type: "Gas",
|
||||
Message: $"هشدار: گاز گلخانه {deviceName} به {telemetry.GasPPM} PPM رسیده که از حداکثر مجاز ({settings.MaxGasPPM}) بیشتر است.",
|
||||
ParameterName: "گاز Co",
|
||||
Value: telemetry.GasPPM,
|
||||
Status: "بالاتر"
|
||||
));
|
||||
}
|
||||
else if (telemetry.GasPPM < settings.MinGasPPM)
|
||||
{
|
||||
alerts.Add(new AlertInfo(
|
||||
Type: "Gas",
|
||||
Message: $"هشدار: گاز گلخانه {deviceName} به {telemetry.GasPPM} PPM رسیده که از حداقل مجاز ({settings.MinGasPPM}) کمتر است.",
|
||||
ParameterName: "گاز Co",
|
||||
Value: telemetry.GasPPM,
|
||||
Status: "پایینتر"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckLuxAlert(TelemetryDto telemetry, DeviceSettingsDto settings, string deviceName, List<AlertInfo> alerts)
|
||||
{
|
||||
if (telemetry.Lux > settings.MaxLux)
|
||||
{
|
||||
alerts.Add(new AlertInfo(
|
||||
Type: "Lux",
|
||||
Message: $"هشدار: نور گلخانه {deviceName} به {telemetry.Lux} لوکس رسیده که از حداکثر مجاز ({settings.MaxLux}) بیشتر است.",
|
||||
ParameterName: "نور",
|
||||
Value: telemetry.Lux,
|
||||
Status: "بالاتر"
|
||||
));
|
||||
}
|
||||
else if (telemetry.Lux < settings.MinLux)
|
||||
{
|
||||
alerts.Add(new AlertInfo(
|
||||
Type: "Lux",
|
||||
Message: $"هشدار: نور گلخانه {deviceName} به {telemetry.Lux} لوکس رسیده که از حداقل مجاز ({settings.MinLux}) کمتر است.",
|
||||
ParameterName: "نور",
|
||||
Value: telemetry.Lux,
|
||||
Status: "پایینتر"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendAlertIfNeededAsync(
|
||||
int deviceId,
|
||||
int userId,
|
||||
string deviceName,
|
||||
AlertInfo alert,
|
||||
private async Task SendAlertForConditionAsync(
|
||||
Domain.AlertCondition condition,
|
||||
Domain.Device device,
|
||||
List<Domain.User> usersToAlert,
|
||||
TelemetryDto telemetry,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Check if alert was sent in the last 10 minutes
|
||||
var cooldownTime = DateTime.UtcNow.AddMinutes(-AlertCooldownMinutes);
|
||||
var recentAlert = await dbContext.AlertNotifications
|
||||
.Where(a => a.DeviceId == deviceId &&
|
||||
a.UserId == userId &&
|
||||
a.AlertType == alert.Type &&
|
||||
a.SentAt >= cooldownTime)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
// Determine cooldown based on notification type
|
||||
var cooldownMinutes = condition.NotificationType == Domain.AlertNotificationType.Call
|
||||
? condition.CallCooldownMinutes
|
||||
: condition.SmsCooldownMinutes;
|
||||
|
||||
if (recentAlert != null)
|
||||
// Build alert message once
|
||||
var message = BuildAlertMessage(condition, device.DeviceName, telemetry);
|
||||
var sentAt = DateTime.UtcNow;
|
||||
|
||||
// Send alert to each user
|
||||
foreach (var user in usersToAlert)
|
||||
{
|
||||
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, AlertType={AlertType}", deviceId, alert.Type);
|
||||
// Check if alert was sent recently to this user
|
||||
var cooldownTime = sentAt.AddMinutes(-cooldownMinutes);
|
||||
var recentAlert = await dbContext.AlertNotifications
|
||||
.Where(a => a.DeviceId == device.Id &&
|
||||
a.UserId == user.Id &&
|
||||
a.AlertConditionId == condition.Id &&
|
||||
a.SentAt >= cooldownTime)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (recentAlert != null)
|
||||
{
|
||||
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
|
||||
device.Id, user.Id, condition.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Send notification
|
||||
var startTime = DateTime.UtcNow;
|
||||
string? messageOutboxIds = null;
|
||||
string? errorMessage = null;
|
||||
bool isSent = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (condition.NotificationType == Domain.AlertNotificationType.SMS)
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
else // Call
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Exception: {ex.Message}";
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
errorMessage += $" | InnerException: {ex.InnerException.Message}";
|
||||
}
|
||||
isSent = false;
|
||||
logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
|
||||
device.Id, user.Id, condition.Id);
|
||||
}
|
||||
|
||||
var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
|
||||
// Save notification to database (old table for backwards compatibility)
|
||||
var notification = new Domain.AlertNotification
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = condition.Id,
|
||||
NotificationType = condition.NotificationType,
|
||||
Message = message,
|
||||
MessageOutboxIds = messageOutboxIds,
|
||||
ErrorMessage = errorMessage,
|
||||
SentAt = sentAt,
|
||||
IsSent = isSent
|
||||
};
|
||||
|
||||
dbContext.AlertNotifications.Add(notification);
|
||||
|
||||
// Log the alert
|
||||
var alertLog = new Domain.AlertLog
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = condition.Id,
|
||||
AlertType = Domain.AlertType.Condition,
|
||||
NotificationType = condition.NotificationType,
|
||||
Message = message,
|
||||
Status = isSent ? Domain.AlertStatus.Success : Domain.AlertStatus.Failed,
|
||||
ErrorMessage = errorMessage,
|
||||
PhoneNumber = user.Mobile,
|
||||
SentAt = sentAt,
|
||||
ProcessingTimeMs = processingTime
|
||||
};
|
||||
|
||||
dbContext.AlertLogs.Add(alertLog);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private string BuildAlertMessage(Domain.AlertCondition condition, string deviceName, TelemetryDto telemetry)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
parts.Add($"هشدار گلخانه {deviceName}:");
|
||||
|
||||
foreach (var rule in condition.Rules.OrderBy(r => r.Order))
|
||||
{
|
||||
var sensorName = rule.SensorType switch
|
||||
{
|
||||
Domain.SensorType.Temperature => "دما",
|
||||
Domain.SensorType.Humidity => "رطوبت",
|
||||
Domain.SensorType.Soil => "رطوبت خاک",
|
||||
Domain.SensorType.Gas => "گاز",
|
||||
Domain.SensorType.Lux => "نور",
|
||||
_ => "سنسور"
|
||||
};
|
||||
|
||||
var sensorValue = rule.SensorType switch
|
||||
{
|
||||
Domain.SensorType.Temperature => telemetry.TemperatureC,
|
||||
Domain.SensorType.Humidity => telemetry.HumidityPercent,
|
||||
Domain.SensorType.Soil => telemetry.SoilPercent,
|
||||
Domain.SensorType.Gas => telemetry.GasPPM,
|
||||
Domain.SensorType.Lux => telemetry.Lux,
|
||||
_ => 0m
|
||||
};
|
||||
|
||||
var unit = rule.SensorType switch
|
||||
{
|
||||
Domain.SensorType.Temperature => "°C",
|
||||
Domain.SensorType.Humidity => "%",
|
||||
Domain.SensorType.Soil => "%",
|
||||
Domain.SensorType.Gas => "PPM",
|
||||
Domain.SensorType.Lux => "لوکس",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
var conditionText = rule.ComparisonType switch
|
||||
{
|
||||
Domain.ComparisonType.GreaterThan => $"{sensorName} ({sensorValue:F1}{unit}) بیشتر از {rule.Value1}{unit}",
|
||||
Domain.ComparisonType.LessThan => $"{sensorName} ({sensorValue:F1}{unit}) کمتر از {rule.Value1}{unit}",
|
||||
Domain.ComparisonType.Between => $"{sensorName} ({sensorValue:F1}{unit}) بین {rule.Value1} و {rule.Value2}{unit}",
|
||||
Domain.ComparisonType.OutOfRange => $"{sensorName} ({sensorValue:F1}{unit}) خارج از محدوده {rule.Value1} تا {rule.Value2}{unit}",
|
||||
_ => $"{sensorName}: {sensorValue:F1}{unit}"
|
||||
};
|
||||
|
||||
parts.Add(conditionText);
|
||||
}
|
||||
|
||||
return string.Join(" و ", parts);
|
||||
}
|
||||
|
||||
private async Task<(bool isSent, string? messageOutboxIds, string? errorMessage)> SendSmsAlertAsync(
|
||||
string mobile,
|
||||
string deviceName,
|
||||
string message,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var smsResponse = await smsService.SendPatternSmsAsync(new PatternSmsRequest
|
||||
{
|
||||
Recipients = [mobile],
|
||||
PatternCode = "64di3w9kb0fxvif",
|
||||
Variables = new Dictionary<string, string> {
|
||||
{ "name", deviceName },
|
||||
{ "parameter", "شرایط" },
|
||||
{ "value", message },
|
||||
{ "status", "هشدار" }
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
if (smsResponse != null && smsResponse.Meta.Status &&
|
||||
smsResponse.Data?.MessageOutboxIds != null &&
|
||||
smsResponse.Data.MessageOutboxIds.Count > 0)
|
||||
{
|
||||
var outboxIds = JsonSerializer.Serialize(smsResponse.Data.MessageOutboxIds);
|
||||
logger.LogInformation("Alert SMS sent: Mobile={Mobile}, OutboxIds={OutboxIds}", mobile, outboxIds);
|
||||
return (true, outboxIds, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var errors = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(smsResponse?.Meta.Message))
|
||||
{
|
||||
errors.Add(smsResponse.Meta.Message);
|
||||
}
|
||||
if (smsResponse?.Meta.Errors != null)
|
||||
{
|
||||
foreach (var error in smsResponse.Meta.Errors)
|
||||
{
|
||||
errors.Add($"{error.Key}: {string.Join(", ", error.Value)}");
|
||||
}
|
||||
}
|
||||
var errorMsg = errors.Count > 0 ? string.Join(" | ", errors) : "Unknown SMS error";
|
||||
logger.LogWarning("Alert SMS failed: Mobile={Mobile}, Error={Error}", mobile, errorMsg);
|
||||
return (false, null, errorMsg);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorMsg = $"Exception: {ex.Message}";
|
||||
logger.LogError(ex, "Exception sending SMS alert: Mobile={Mobile}", mobile);
|
||||
return (false, null, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(bool isSent, string? callId, string? errorMessage)> SendCallAlertAsync(
|
||||
string mobile,
|
||||
string deviceName,
|
||||
string message,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: Implement voice call integration
|
||||
// For now, just log and return success
|
||||
logger.LogInformation("Voice call alert requested: Mobile={Mobile}, Message={Message}", mobile, message);
|
||||
|
||||
// Placeholder: In real implementation, call voiceCallService here
|
||||
// var callResponse = await voiceCallService.MakeCallAsync(mobile, message, cancellationToken);
|
||||
|
||||
return (true, null, "Voice call not yet implemented");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorMsg = $"Exception: {ex.Message}";
|
||||
logger.LogError(ex, "Exception sending call alert: Mobile={Mobile}", mobile);
|
||||
return (false, null, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get device with all users who should receive alerts
|
||||
var device = await dbContext.Devices
|
||||
.Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts))
|
||||
.ThenInclude(du => du.User)
|
||||
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
logger.LogWarning("Device not found for power outage alert: DeviceId={DeviceId}", deviceId);
|
||||
throw new InvalidOperationException($"دستگاه با شناسه {deviceId} یافت نشد");
|
||||
}
|
||||
|
||||
// Get all users who should receive alerts
|
||||
var usersToAlert = device.DeviceUsers
|
||||
.Where(du => du.ReceiveAlerts)
|
||||
.Select(du => du.User)
|
||||
.ToList();
|
||||
|
||||
if (usersToAlert.Count == 0)
|
||||
{
|
||||
logger.LogInformation("No users with ReceiveAlerts enabled for power outage: DeviceId={DeviceId}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get user to send SMS
|
||||
var user = await dbContext.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
|
||||
var message = $"⚠️ هشدار قطع برق! دستگاه {device.DeviceName} از برق قطع شده است.";
|
||||
var sentAt = DateTime.UtcNow;
|
||||
|
||||
if (user == null || string.IsNullOrWhiteSpace(user.Mobile))
|
||||
// Send to all users (both SMS and Call for power outage - it's critical!)
|
||||
foreach (var user in usersToAlert)
|
||||
{
|
||||
logger.LogWarning("User not found or mobile is empty: UserId={UserId}", userId);
|
||||
return;
|
||||
// Send SMS
|
||||
await SendPowerOutageNotificationAsync(
|
||||
device, user, message, sentAt,
|
||||
Domain.AlertNotificationType.SMS,
|
||||
cancellationToken);
|
||||
|
||||
// Send Call (important alert)
|
||||
await SendPowerOutageNotificationAsync(
|
||||
device, user, message, sentAt,
|
||||
Domain.AlertNotificationType.Call,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
// Send SMS and collect response/errors
|
||||
string? messageOutboxIdsJson = null;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Power outage alerts sent to {Count} users for device {DeviceId}",
|
||||
usersToAlert.Count, deviceId);
|
||||
}
|
||||
|
||||
private async Task SendPowerOutageNotificationAsync(
|
||||
Domain.Device device,
|
||||
Domain.User user,
|
||||
string message,
|
||||
DateTime sentAt,
|
||||
Domain.AlertNotificationType notificationType,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var startTime = DateTime.UtcNow;
|
||||
string? messageOutboxIds = null;
|
||||
string? errorMessage = null;
|
||||
bool isSent = false;
|
||||
|
||||
try
|
||||
{
|
||||
var smsResponse = await smsService.SendPatternSmsAsync(new PatternSmsRequest
|
||||
if (notificationType == Domain.AlertNotificationType.SMS)
|
||||
{
|
||||
Recipients = [user.Mobile],
|
||||
PatternCode = "64di3w9kb0fxvif",
|
||||
Variables = new Dictionary<string, string> {
|
||||
{ "name", deviceName },
|
||||
{ "parameter", alert.ParameterName },
|
||||
{ "value", alert.Value.ToString("F1") },
|
||||
{ "status", alert.Status },
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
if (smsResponse != null)
|
||||
{
|
||||
// Check if SMS was sent successfully
|
||||
if (smsResponse.Meta.Status && smsResponse.Data != null && smsResponse.Data.MessageOutboxIds != null && smsResponse.Data.MessageOutboxIds.Count > 0)
|
||||
{
|
||||
// Success - save message outbox IDs
|
||||
messageOutboxIdsJson = JsonSerializer.Serialize(smsResponse.Data.MessageOutboxIds);
|
||||
isSent = true;
|
||||
logger.LogInformation("Alert SMS sent: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}, OutboxIds={OutboxIds}",
|
||||
deviceId, userId, alert.Type, messageOutboxIdsJson);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Failed - save error from meta
|
||||
var errors = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(smsResponse.Meta.Message))
|
||||
{
|
||||
errors.Add(smsResponse.Meta.Message);
|
||||
}
|
||||
if (smsResponse.Meta.Errors != null && smsResponse.Meta.Errors.Count > 0)
|
||||
{
|
||||
foreach (var error in smsResponse.Meta.Errors)
|
||||
{
|
||||
errors.Add($"{error.Key}: {string.Join(", ", error.Value)}");
|
||||
}
|
||||
}
|
||||
if (errors.Count == 0)
|
||||
{
|
||||
errors.Add("SMS sending failed with unknown error");
|
||||
}
|
||||
errorMessage = string.Join(" | ", errors);
|
||||
isSent = false;
|
||||
logger.LogWarning("Alert SMS failed: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}, Error={Error}",
|
||||
deviceId, userId, alert.Type, errorMessage);
|
||||
}
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(
|
||||
user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "SMS service returned null response";
|
||||
isSent = false;
|
||||
logger.LogWarning("Alert SMS returned null: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}",
|
||||
deviceId, userId, alert.Type);
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(
|
||||
user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -296,24 +451,45 @@ public sealed class AlertService : IAlertService
|
||||
errorMessage += $" | InnerException: {ex.InnerException.Message}";
|
||||
}
|
||||
isSent = false;
|
||||
logger.LogError(ex, "Failed to send alert SMS: DeviceId={DeviceId}, UserId={UserId}, AlertType={AlertType}", deviceId, userId, alert.Type);
|
||||
logger.LogError(ex, "Failed to send power outage alert: DeviceId={DeviceId}, UserId={UserId}, Type={Type}",
|
||||
device.Id, user.Id, notificationType);
|
||||
}
|
||||
|
||||
// Save notification to database
|
||||
var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
|
||||
// Save notification (old table)
|
||||
var notification = new Domain.AlertNotification
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
UserId = userId,
|
||||
AlertType = alert.Type,
|
||||
Message = alert.Message,
|
||||
MessageOutboxIds = messageOutboxIdsJson,
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = null,
|
||||
NotificationType = notificationType,
|
||||
Message = message,
|
||||
MessageOutboxIds = messageOutboxIds,
|
||||
ErrorMessage = errorMessage,
|
||||
SentAt = DateTime.UtcNow,
|
||||
SentAt = sentAt,
|
||||
IsSent = isSent
|
||||
};
|
||||
|
||||
dbContext.AlertNotifications.Add(notification);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Log the alert
|
||||
var alertLog = new Domain.AlertLog
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = null,
|
||||
AlertType = Domain.AlertType.PowerOutage,
|
||||
NotificationType = notificationType,
|
||||
Message = message,
|
||||
Status = isSent ? Domain.AlertStatus.Success : Domain.AlertStatus.Failed,
|
||||
ErrorMessage = errorMessage,
|
||||
PhoneNumber = user.Mobile,
|
||||
SentAt = sentAt,
|
||||
ProcessingTimeMs = processingTime
|
||||
};
|
||||
|
||||
dbContext.AlertLogs.Add(alertLog);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
152
src/GreenHome.Infrastructure/ChecklistService.cs
Normal file
152
src/GreenHome.Infrastructure/ChecklistService.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using AutoMapper;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class ChecklistService : IChecklistService
|
||||
{
|
||||
private readonly GreenHomeDbContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public ChecklistService(GreenHomeDbContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<ChecklistDto?> GetActiveChecklistByDeviceIdAsync(int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var checklist = await _context.Checklists
|
||||
.Include(c => c.Device)
|
||||
.Include(c => c.CreatedByUser)
|
||||
.Include(c => c.Items.OrderBy(i => i.Order))
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.DeviceId == deviceId && c.IsActive, cancellationToken);
|
||||
|
||||
return checklist != null ? _mapper.Map<ChecklistDto>(checklist) : null;
|
||||
}
|
||||
|
||||
public async Task<List<ChecklistDto>> GetChecklistsByDeviceIdAsync(int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
var checklists = await _context.Checklists
|
||||
.Include(c => c.Device)
|
||||
.Include(c => c.CreatedByUser)
|
||||
.Include(c => c.Items.OrderBy(i => i.Order))
|
||||
.AsNoTracking()
|
||||
.Where(c => c.DeviceId == deviceId)
|
||||
.OrderByDescending(c => c.IsActive)
|
||||
.ThenByDescending(c => c.CreatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return _mapper.Map<List<ChecklistDto>>(checklists);
|
||||
}
|
||||
|
||||
public async Task<ChecklistDto?> GetChecklistByIdAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var checklist = await _context.Checklists
|
||||
.Include(c => c.Device)
|
||||
.Include(c => c.CreatedByUser)
|
||||
.Include(c => c.Items.OrderBy(i => i.Order))
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
|
||||
|
||||
return checklist != null ? _mapper.Map<ChecklistDto>(checklist) : null;
|
||||
}
|
||||
|
||||
public async Task<int> CreateChecklistAsync(CreateChecklistRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Deactivate existing active checklist for this device
|
||||
var existingActiveChecklist = await _context.Checklists
|
||||
.FirstOrDefaultAsync(c => c.DeviceId == request.DeviceId && c.IsActive, cancellationToken);
|
||||
|
||||
if (existingActiveChecklist != null)
|
||||
{
|
||||
existingActiveChecklist.IsActive = false;
|
||||
}
|
||||
|
||||
// Create new checklist
|
||||
var checklist = new Domain.Checklist
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
CreatedByUserId = request.CreatedByUserId,
|
||||
Title = request.Title,
|
||||
Description = request.Description,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Add items
|
||||
foreach (var itemRequest in request.Items)
|
||||
{
|
||||
var item = new Domain.ChecklistItem
|
||||
{
|
||||
Title = itemRequest.Title,
|
||||
Description = itemRequest.Description,
|
||||
Order = itemRequest.Order,
|
||||
IsRequired = itemRequest.IsRequired
|
||||
};
|
||||
checklist.Items.Add(item);
|
||||
}
|
||||
|
||||
_context.Checklists.Add(checklist);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return checklist.Id;
|
||||
}
|
||||
|
||||
public async Task<List<ChecklistCompletionDto>> GetCompletionsByChecklistIdAsync(
|
||||
int checklistId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var completions = await _context.ChecklistCompletions
|
||||
.Include(cc => cc.Checklist)
|
||||
.Include(cc => cc.CompletedByUser)
|
||||
.Include(cc => cc.ItemCompletions)
|
||||
.ThenInclude(ic => ic.ChecklistItem)
|
||||
.AsNoTracking()
|
||||
.Where(cc => cc.ChecklistId == checklistId)
|
||||
.OrderByDescending(cc => cc.CompletedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return _mapper.Map<List<ChecklistCompletionDto>>(completions);
|
||||
}
|
||||
|
||||
public async Task<int> CompleteChecklistAsync(CompleteChecklistRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var checklist = await _context.Checklists
|
||||
.Include(c => c.Items)
|
||||
.FirstOrDefaultAsync(c => c.Id == request.ChecklistId, cancellationToken);
|
||||
|
||||
if (checklist == null)
|
||||
{
|
||||
throw new InvalidOperationException($"چکلیست با شناسه {request.ChecklistId} یافت نشد");
|
||||
}
|
||||
|
||||
var completion = new Domain.ChecklistCompletion
|
||||
{
|
||||
ChecklistId = request.ChecklistId,
|
||||
CompletedByUserId = request.CompletedByUserId,
|
||||
PersianDate = request.PersianDate,
|
||||
Notes = request.Notes,
|
||||
CompletedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
foreach (var itemCompletion in request.ItemCompletions)
|
||||
{
|
||||
var item = new Domain.ChecklistItemCompletion
|
||||
{
|
||||
ChecklistItemId = itemCompletion.ChecklistItemId,
|
||||
IsChecked = itemCompletion.IsChecked,
|
||||
Note = itemCompletion.Note
|
||||
};
|
||||
completion.ItemCompletions.Add(item);
|
||||
}
|
||||
|
||||
_context.ChecklistCompletions.Add(completion);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return completion.Id;
|
||||
}
|
||||
}
|
||||
|
||||
479
src/GreenHome.Infrastructure/DailyReportService.cs
Normal file
479
src/GreenHome.Infrastructure/DailyReportService.cs
Normal file
@@ -0,0 +1,479 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using GreenHome.AI.DeepSeek;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public class DailyReportService : IDailyReportService
|
||||
{
|
||||
private readonly GreenHomeDbContext _context;
|
||||
private readonly IDeepSeekService _deepSeekService;
|
||||
private readonly ILogger<DailyReportService> _logger;
|
||||
|
||||
public DailyReportService(
|
||||
GreenHomeDbContext context,
|
||||
IDeepSeekService deepSeekService,
|
||||
ILogger<DailyReportService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_deepSeekService = deepSeekService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<DailyReportResponse> GetOrCreateDailyReportAsync(
|
||||
DailyReportRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Validate Persian date format
|
||||
if (!IsValidPersianDate(request.PersianDate, out var year, out var month, out var day))
|
||||
{
|
||||
throw new ArgumentException("تاریخ شمسی باید به فرمت yyyy/MM/dd باشد", nameof(request.PersianDate));
|
||||
}
|
||||
|
||||
// Check if report already exists
|
||||
var existingReport = await _context.DailyReports
|
||||
.Include(r => r.Device)
|
||||
.FirstOrDefaultAsync(
|
||||
r => r.DeviceId == request.DeviceId && r.PersianDate == request.PersianDate,
|
||||
cancellationToken);
|
||||
|
||||
if (existingReport != null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"گزارش روزانه برای دستگاه {DeviceId} و تاریخ {Date} از قبل موجود است",
|
||||
request.DeviceId, request.PersianDate);
|
||||
|
||||
return new DailyReportResponse
|
||||
{
|
||||
Id = existingReport.Id,
|
||||
DeviceId = existingReport.DeviceId,
|
||||
DeviceName = existingReport.Device?.DeviceName ?? string.Empty,
|
||||
PersianDate = existingReport.PersianDate,
|
||||
Analysis = existingReport.Analysis,
|
||||
RecordCount = existingReport.RecordCount,
|
||||
SampledRecordCount = existingReport.SampledRecordCount,
|
||||
TotalTokens = existingReport.TotalTokens,
|
||||
CreatedAt = existingReport.CreatedAt,
|
||||
FromCache = true
|
||||
};
|
||||
}
|
||||
|
||||
// Get device info
|
||||
var device = await _context.Devices
|
||||
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
|
||||
}
|
||||
|
||||
// Get device settings (including ProductType if available)
|
||||
var deviceSettings = await _context.DeviceSettings
|
||||
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
|
||||
|
||||
// Query telemetry data for the specified date
|
||||
var telemetryRecords = await _context.TelemetryRecords
|
||||
.Where(t => t.DeviceId == request.DeviceId && t.PersianDate == request.PersianDate)
|
||||
.OrderBy(t => t.TimestampUtc)
|
||||
.Select(t => new
|
||||
{
|
||||
t.TimestampUtc,
|
||||
t.TemperatureC,
|
||||
t.HumidityPercent,
|
||||
t.Lux,
|
||||
t.GasPPM
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (telemetryRecords.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"هیچ رکوردی برای دستگاه {request.DeviceId} در تاریخ {request.PersianDate} یافت نشد");
|
||||
}
|
||||
|
||||
// Sample records: take first record from every 20 records
|
||||
var sampledRecords = telemetryRecords
|
||||
.Select((record, index) => new { record, index })
|
||||
.Where(x => x.index % 20 == 0)
|
||||
.Select(x => x.record)
|
||||
.ToList();
|
||||
|
||||
_logger.LogInformation(
|
||||
"تعداد {TotalCount} رکورد یافت شد. نمونهبرداری: {SampledCount} رکورد",
|
||||
telemetryRecords.Count, sampledRecords.Count);
|
||||
|
||||
// Build the data string for AI
|
||||
var dataBuilder = new StringBuilder();
|
||||
dataBuilder.AppendLine("زمان | دما (°C) | رطوبت (%) | نور (Lux) | CO (PPM)");
|
||||
dataBuilder.AppendLine("------|----------|-----------|-----------|----------");
|
||||
|
||||
foreach (var record in sampledRecords)
|
||||
{
|
||||
// Convert UTC to local time for display
|
||||
var localTime = record.TimestampUtc.AddHours(3.5); // Iran timezone (UTC+3:30)
|
||||
dataBuilder.AppendLine(
|
||||
$"{localTime:HH:mm:ss} | {record.TemperatureC:F1} | {record.HumidityPercent:F1} | {record.Lux:F1} | {record.GasPPM}");
|
||||
}
|
||||
|
||||
// Prepare the question for AI
|
||||
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $" محصول کشت شده: {deviceSettings.ProductType}."
|
||||
: string.Empty;
|
||||
|
||||
var areaInfo = deviceSettings?.AreaSquareMeters != null
|
||||
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
|
||||
: string.Empty;
|
||||
|
||||
var question = $@"این دادههای تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
|
||||
|
||||
{dataBuilder}
|
||||
|
||||
لطفاً یک تحلیل خلاصه و کاربردی از این دادهها بده که شامل موارد زیر باشه:
|
||||
1. وضعیت کلی دما، رطوبت، نور و کیفیت هوا
|
||||
2. روندهای مشاهده شده در طول روز
|
||||
3. هر گونه نکته یا هشدار مهم
|
||||
4. پیشنهادات برای بهبود شرایط گلخانه{(productTypeInfo != string.Empty ? " و رشد بهتر محصول" : string.Empty)}
|
||||
|
||||
خلاصه و مفید باش (حداکثر 300 کلمه).";
|
||||
|
||||
// Send to DeepSeek
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
ChatResponse? aiResponse;
|
||||
|
||||
try
|
||||
{
|
||||
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و دادههای تلمتری رو تحلیل میکنی."
|
||||
: "تو یک متخصص کشاورزی و گلخانه هستی که دادههای تلمتری رو تحلیل میکنی.";
|
||||
|
||||
var chatRequest = new ChatRequest
|
||||
{
|
||||
Model = "deepseek-chat",
|
||||
Messages = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = systemMessage },
|
||||
new() { Role = "user", Content = question }
|
||||
},
|
||||
Temperature = 0.7
|
||||
};
|
||||
|
||||
aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "خطا در فراخوانی DeepSeek API");
|
||||
throw new InvalidOperationException("خطا در دریافت تحلیل از سرویس هوش مصنوعی", ex);
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
|
||||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
|
||||
{
|
||||
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
|
||||
}
|
||||
|
||||
var analysis = aiResponse.Choices[0].Message!.Content;
|
||||
|
||||
// Save the report
|
||||
var dailyReport = new Domain.DailyReport
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
PersianDate = request.PersianDate,
|
||||
PersianYear = year,
|
||||
PersianMonth = month,
|
||||
PersianDay = day,
|
||||
Analysis = analysis,
|
||||
RecordCount = telemetryRecords.Count,
|
||||
SampledRecordCount = sampledRecords.Count,
|
||||
PromptTokens = aiResponse.Usage?.PromptTokens ?? 0,
|
||||
CompletionTokens = aiResponse.Usage?.CompletionTokens ?? 0,
|
||||
TotalTokens = aiResponse.Usage?.TotalTokens ?? 0,
|
||||
Model = aiResponse.Model,
|
||||
ResponseTimeMs = stopwatch.ElapsedMilliseconds,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.DailyReports.Add(dailyReport);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"گزارش روزانه جدید برای دستگاه {DeviceId} و تاریخ {Date} ایجاد شد. توکن مصرف شده: {Tokens}",
|
||||
request.DeviceId, request.PersianDate, dailyReport.TotalTokens);
|
||||
|
||||
return new DailyReportResponse
|
||||
{
|
||||
Id = dailyReport.Id,
|
||||
DeviceId = dailyReport.DeviceId,
|
||||
DeviceName = device.DeviceName,
|
||||
PersianDate = dailyReport.PersianDate,
|
||||
Analysis = dailyReport.Analysis,
|
||||
RecordCount = dailyReport.RecordCount,
|
||||
SampledRecordCount = dailyReport.SampledRecordCount,
|
||||
TotalTokens = dailyReport.TotalTokens,
|
||||
CreatedAt = dailyReport.CreatedAt,
|
||||
FromCache = false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsValidPersianDate(string persianDate, out int year, out int month, out int day)
|
||||
{
|
||||
year = month = day = 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(persianDate))
|
||||
return false;
|
||||
|
||||
var parts = persianDate.Split('/');
|
||||
if (parts.Length != 3)
|
||||
return false;
|
||||
|
||||
if (!int.TryParse(parts[0], out year) || year < 1300 || year > 1500)
|
||||
return false;
|
||||
|
||||
if (!int.TryParse(parts[1], out month) || month < 1 || month > 12)
|
||||
return false;
|
||||
|
||||
if (!int.TryParse(parts[2], out day) || day < 1 || day > 31)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<DailyReportResponse> GetWeeklyAnalysisAsync(
|
||||
WeeklyAnalysisRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Get device info
|
||||
var device = await _context.Devices
|
||||
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
|
||||
}
|
||||
|
||||
// Get device settings
|
||||
var deviceSettings = await _context.DeviceSettings
|
||||
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
|
||||
|
||||
// Query telemetry data for the week
|
||||
var telemetryRecords = await _context.TelemetryRecords
|
||||
.Where(t => t.DeviceId == request.DeviceId &&
|
||||
string.Compare(t.PersianDate, request.StartDate) >= 0 &&
|
||||
string.Compare(t.PersianDate, request.EndDate) <= 0)
|
||||
.OrderBy(t => t.TimestampUtc)
|
||||
.Select(t => new
|
||||
{
|
||||
t.TimestampUtc,
|
||||
t.TemperatureC,
|
||||
t.HumidityPercent,
|
||||
t.Lux,
|
||||
t.GasPPM,
|
||||
t.PersianDate
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (telemetryRecords.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"هیچ رکوردی برای دستگاه {request.DeviceId} در بازه {request.StartDate} تا {request.EndDate} یافت نشد");
|
||||
}
|
||||
|
||||
// Sample 1 per 100 records
|
||||
var sampledRecords = telemetryRecords
|
||||
.Select((record, index) => new { record, index })
|
||||
.Where(x => x.index % 100 == 0)
|
||||
.Select(x => x.record)
|
||||
.ToList();
|
||||
|
||||
_logger.LogInformation(
|
||||
"تعداد {TotalCount} رکورد یافت شد. نمونهبرداری هفتگی: {SampledCount} رکورد",
|
||||
telemetryRecords.Count, sampledRecords.Count);
|
||||
|
||||
// Build the data string
|
||||
var dataBuilder = new StringBuilder();
|
||||
dataBuilder.AppendLine("تاریخ | زمان | دما (°C) | رطوبت (%) | نور (Lux) | CO (PPM)");
|
||||
dataBuilder.AppendLine("---------|----------|----------|-----------|-----------|----------");
|
||||
|
||||
foreach (var record in sampledRecords)
|
||||
{
|
||||
var localTime = record.TimestampUtc.AddHours(3.5);
|
||||
dataBuilder.AppendLine(
|
||||
$"{record.PersianDate} | {localTime:HH:mm} | {record.TemperatureC:F1} | {record.HumidityPercent:F1} | {record.Lux:F1} | {record.GasPPM}");
|
||||
}
|
||||
|
||||
// Prepare AI prompt
|
||||
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $" محصول کشت شده: {deviceSettings.ProductType}."
|
||||
: string.Empty;
|
||||
|
||||
var areaInfo = deviceSettings?.AreaSquareMeters != null
|
||||
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
|
||||
: string.Empty;
|
||||
|
||||
var question = $@"این دادههای تلمتری یک هفته ({request.StartDate} تا {request.EndDate}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
|
||||
|
||||
{dataBuilder}
|
||||
|
||||
لطفاً یک تحلیل جامع هفتگی بده که شامل:
|
||||
1. خلاصه روند هفتگی دما، رطوبت، نور و کیفیت هوا
|
||||
2. مقایسه شرایط در روزهای مختلف هفته
|
||||
3. نکات و هشدارهای مهم
|
||||
4. توصیهها برای هفته آینده
|
||||
|
||||
خلاصه و کاربردی باش (حداکثر 500 کلمه).";
|
||||
|
||||
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری و دادههای تلمتری رو تحلیل میکنی."
|
||||
: "تو یک متخصص کشاورزی و گلخانه هستی که دادههای تلمتری رو تحلیل میکنی.";
|
||||
|
||||
// Send to DeepSeek
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var chatRequest = new ChatRequest
|
||||
{
|
||||
Model = "deepseek-chat",
|
||||
Messages = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = systemMessage },
|
||||
new() { Role = "user", Content = question }
|
||||
},
|
||||
Temperature = 0.7
|
||||
};
|
||||
|
||||
var aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken);
|
||||
stopwatch.Stop();
|
||||
|
||||
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
|
||||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
|
||||
{
|
||||
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
|
||||
}
|
||||
|
||||
return new DailyReportResponse
|
||||
{
|
||||
Id = 0,
|
||||
DeviceId = request.DeviceId,
|
||||
DeviceName = device.DeviceName,
|
||||
PersianDate = $"{request.StartDate} تا {request.EndDate}",
|
||||
Analysis = aiResponse.Choices[0].Message!.Content,
|
||||
RecordCount = telemetryRecords.Count,
|
||||
SampledRecordCount = sampledRecords.Count,
|
||||
TotalTokens = aiResponse.Usage?.TotalTokens ?? 0,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
FromCache = false
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DailyReportResponse> GetMonthlyAnalysisAsync(
|
||||
MonthlyAnalysisRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Get device info
|
||||
var device = await _context.Devices
|
||||
.FirstOrDefaultAsync(d => d.Id == request.DeviceId, cancellationToken);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
throw new InvalidOperationException($"دستگاه با شناسه {request.DeviceId} یافت نشد");
|
||||
}
|
||||
|
||||
// Get device settings
|
||||
var deviceSettings = await _context.DeviceSettings
|
||||
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId, cancellationToken);
|
||||
|
||||
// Get all daily reports for this month
|
||||
var dailyReports = await _context.DailyReports
|
||||
.Where(dr => dr.DeviceId == request.DeviceId &&
|
||||
dr.PersianYear == request.Year &&
|
||||
dr.PersianMonth == request.Month)
|
||||
.OrderBy(dr => dr.PersianDay)
|
||||
.Select(dr => new { dr.PersianDate, dr.Analysis })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (dailyReports.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"هیچ تحلیل روزانهای برای دستگاه {request.DeviceId} در ماه {request.Month} سال {request.Year} یافت نشد");
|
||||
}
|
||||
|
||||
// Build summary of daily analyses
|
||||
var summaryBuilder = new StringBuilder();
|
||||
summaryBuilder.AppendLine($"تحلیلهای روزانه ماه {request.Month} سال {request.Year}:");
|
||||
summaryBuilder.AppendLine();
|
||||
|
||||
foreach (var report in dailyReports)
|
||||
{
|
||||
summaryBuilder.AppendLine($"📅 {report.PersianDate}:");
|
||||
summaryBuilder.AppendLine(report.Analysis);
|
||||
summaryBuilder.AppendLine();
|
||||
summaryBuilder.AppendLine("---");
|
||||
summaryBuilder.AppendLine();
|
||||
}
|
||||
|
||||
// Prepare AI prompt
|
||||
var productTypeInfo = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $" محصول کشت شده: {deviceSettings.ProductType}."
|
||||
: string.Empty;
|
||||
|
||||
var areaInfo = deviceSettings?.AreaSquareMeters != null
|
||||
? $" مساحت گلخانه: {deviceSettings.AreaSquareMeters:F1} متر مربع."
|
||||
: string.Empty;
|
||||
|
||||
var question = $@"این تحلیلهای روزانه یک ماه ({request.Month}/{request.Year}) از یک گلخانه هوشمند هستند.{productTypeInfo}{areaInfo}
|
||||
|
||||
{summaryBuilder}
|
||||
|
||||
لطفاً یک تحلیل جامع ماهانه بده که شامل:
|
||||
1. خلاصه کلی عملکرد ماه
|
||||
2. روندهای اصلی و تغییرات مهم
|
||||
3. نقاط قوت و ضعف
|
||||
4. توصیههای کلیدی برای ماه آینده
|
||||
5. نکات مهم برای بهبود بهرهوری
|
||||
|
||||
جامع و کاربردی باش (حداکثر 800 کلمه).";
|
||||
|
||||
var systemMessage = !string.IsNullOrWhiteSpace(deviceSettings?.ProductType)
|
||||
? $"تو یک متخصص کشاورزی و گلخانه هستی که در کشت {deviceSettings.ProductType} تخصص داری. تحلیلهای روزانه رو بررسی کن و یک جمعبندی ماهانه جامع ارائه بده."
|
||||
: "تو یک متخصص کشاورزی و گلخانه هستی. تحلیلهای روزانه رو بررسی کن و یک جمعبندی ماهانه جامع ارائه بده.";
|
||||
|
||||
// Send to DeepSeek
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var chatRequest = new ChatRequest
|
||||
{
|
||||
Model = "deepseek-chat",
|
||||
Messages = new List<ChatMessage>
|
||||
{
|
||||
new() { Role = "system", Content = systemMessage },
|
||||
new() { Role = "user", Content = question }
|
||||
},
|
||||
Temperature = 0.7
|
||||
};
|
||||
|
||||
var aiResponse = await _deepSeekService.AskAsync(chatRequest, cancellationToken);
|
||||
stopwatch.Stop();
|
||||
|
||||
if (aiResponse?.Choices == null || aiResponse.Choices.Count == 0 ||
|
||||
string.IsNullOrWhiteSpace(aiResponse.Choices[0].Message?.Content))
|
||||
{
|
||||
throw new InvalidOperationException("پاسخ نامعتبر از سرویس هوش مصنوعی");
|
||||
}
|
||||
|
||||
return new DailyReportResponse
|
||||
{
|
||||
Id = 0,
|
||||
DeviceId = request.DeviceId,
|
||||
DeviceName = device.DeviceName,
|
||||
PersianDate = $"ماه {request.Month} سال {request.Year}",
|
||||
Analysis = aiResponse.Choices[0].Message!.Content,
|
||||
RecordCount = dailyReports.Count,
|
||||
SampledRecordCount = dailyReports.Count,
|
||||
TotalTokens = aiResponse.Usage?.TotalTokens ?? 0,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
FromCache = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
185
src/GreenHome.Infrastructure/DevicePostService.cs
Normal file
185
src/GreenHome.Infrastructure/DevicePostService.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using AutoMapper;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class DevicePostService : IDevicePostService
|
||||
{
|
||||
private readonly GreenHomeDbContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public DevicePostService(GreenHomeDbContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<DevicePostDto>> GetPostsAsync(
|
||||
DevicePostFilter filter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.DevicePosts
|
||||
.Include(p => p.AuthorUser)
|
||||
.Include(p => p.Images)
|
||||
.AsNoTracking()
|
||||
.Where(p => p.DeviceId == filter.DeviceId);
|
||||
|
||||
if (filter.AuthorUserId.HasValue)
|
||||
{
|
||||
query = query.Where(p => p.AuthorUserId == filter.AuthorUserId.Value);
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
var posts = await query
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip((filter.Page - 1) * filter.PageSize)
|
||||
.Take(filter.PageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var dtos = _mapper.Map<List<DevicePostDto>>(posts);
|
||||
|
||||
return new PagedResult<DevicePostDto>
|
||||
{
|
||||
Items = dtos,
|
||||
TotalCount = totalCount,
|
||||
Page = filter.Page,
|
||||
PageSize = filter.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DevicePostDto?> GetPostByIdAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.DevicePosts
|
||||
.Include(p => p.AuthorUser)
|
||||
.Include(p => p.Images)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
||||
|
||||
return post != null ? _mapper.Map<DevicePostDto>(post) : null;
|
||||
}
|
||||
|
||||
public async Task<int> CreatePostAsync(
|
||||
CreateDevicePostRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Verify user has access to device
|
||||
var hasAccess = await CanUserAccessDeviceAsync(request.AuthorUserId, request.DeviceId, cancellationToken);
|
||||
if (!hasAccess)
|
||||
{
|
||||
throw new UnauthorizedAccessException("کاربر به این دستگاه دسترسی ندارد");
|
||||
}
|
||||
|
||||
var post = new Domain.DevicePost
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
AuthorUserId = request.AuthorUserId,
|
||||
Content = request.Content,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.DevicePosts.Add(post);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return post.Id;
|
||||
}
|
||||
|
||||
public async Task UpdatePostAsync(
|
||||
UpdateDevicePostRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.DevicePosts
|
||||
.FirstOrDefaultAsync(p => p.Id == request.Id, cancellationToken);
|
||||
|
||||
if (post == null)
|
||||
{
|
||||
throw new InvalidOperationException($"پست با شناسه {request.Id} یافت نشد");
|
||||
}
|
||||
|
||||
post.Content = request.Content;
|
||||
post.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeletePostAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.DevicePosts
|
||||
.Include(p => p.Images)
|
||||
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
||||
|
||||
if (post == null)
|
||||
{
|
||||
throw new InvalidOperationException($"پست با شناسه {id} یافت نشد");
|
||||
}
|
||||
|
||||
_context.DevicePosts.Remove(post);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> AddImageToPostAsync(
|
||||
int postId,
|
||||
string fileName,
|
||||
string filePath,
|
||||
string contentType,
|
||||
long fileSize,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.DevicePosts
|
||||
.FirstOrDefaultAsync(p => p.Id == postId, cancellationToken);
|
||||
|
||||
if (post == null)
|
||||
{
|
||||
throw new InvalidOperationException($"پست با شناسه {postId} یافت نشد");
|
||||
}
|
||||
|
||||
var image = new Domain.DevicePostImage
|
||||
{
|
||||
DevicePostId = postId,
|
||||
FileName = fileName,
|
||||
FilePath = filePath,
|
||||
ContentType = contentType,
|
||||
FileSize = fileSize,
|
||||
UploadedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.DevicePostImages.Add(image);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return image.Id;
|
||||
}
|
||||
|
||||
public async Task DeleteImageAsync(int imageId, CancellationToken cancellationToken)
|
||||
{
|
||||
var image = await _context.DevicePostImages
|
||||
.FirstOrDefaultAsync(i => i.Id == imageId, cancellationToken);
|
||||
|
||||
if (image == null)
|
||||
{
|
||||
throw new InvalidOperationException($"تصویر با شناسه {imageId} یافت نشد");
|
||||
}
|
||||
|
||||
_context.DevicePostImages.Remove(image);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> CanUserAccessDeviceAsync(
|
||||
int userId,
|
||||
int deviceId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Check if user is the device owner or has access through DeviceUsers
|
||||
var hasAccess = await _context.Devices
|
||||
.AnyAsync(d => d.Id == deviceId && d.UserId == userId, cancellationToken);
|
||||
|
||||
if (!hasAccess)
|
||||
{
|
||||
hasAccess = await _context.DeviceUsers
|
||||
.AnyAsync(du => du.DeviceId == deviceId && du.UserId == userId, cancellationToken);
|
||||
}
|
||||
|
||||
return hasAccess;
|
||||
}
|
||||
}
|
||||
|
||||
263
src/GreenHome.Infrastructure/DeviceTokenService.cs
Normal file
263
src/GreenHome.Infrastructure/DeviceTokenService.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using GreenHome.Application;
|
||||
using GreenHome.Sms.Ippanel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس مدیریت توکن و تنظیمات دستگاه
|
||||
/// </summary>
|
||||
public sealed class DeviceTokenService : IDeviceTokenService
|
||||
{
|
||||
private readonly GreenHomeDbContext dbContext;
|
||||
private readonly ISmsService smsService;
|
||||
private readonly ILogger<DeviceTokenService> logger;
|
||||
|
||||
public DeviceTokenService(
|
||||
GreenHomeDbContext dbContext,
|
||||
ISmsService smsService,
|
||||
ILogger<DeviceTokenService> logger)
|
||||
{
|
||||
this.dbContext = dbContext;
|
||||
this.smsService = smsService;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت فاصله زمانی آپلود بر اساس شماره تلفن یا شناسه دستگاه
|
||||
/// </summary>
|
||||
public async Task<GetUploadIntervalResponse> GetUploadIntervalAsync(
|
||||
GetUploadIntervalRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
Domain.DeviceSettings? settings = null;
|
||||
|
||||
// جستجو بر اساس DeviceId یا DevicePhoneNumber
|
||||
if (request.DeviceId.HasValue)
|
||||
{
|
||||
settings = await dbContext.DeviceSettings
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(ds => ds.DeviceId == request.DeviceId.Value, cancellationToken);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(request.DevicePhoneNumber))
|
||||
{
|
||||
settings = await dbContext.DeviceSettings
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(ds => ds.DevicePhoneNumber == request.DevicePhoneNumber, cancellationToken);
|
||||
}
|
||||
|
||||
if (settings == null)
|
||||
{
|
||||
return new GetUploadIntervalResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "دستگاه یافت نشد"
|
||||
};
|
||||
}
|
||||
|
||||
return new GetUploadIntervalResponse
|
||||
{
|
||||
Success = true,
|
||||
UploadIntervalMin = settings.UploadIntervalMin
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting upload interval for device");
|
||||
return new GetUploadIntervalResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "خطا در دریافت اطلاعات"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// درخواست توکن دستگاه (تولید و ارسال کد)
|
||||
/// </summary>
|
||||
public async Task<RequestDeviceTokenResponse> RequestDeviceTokenAsync(
|
||||
RequestDeviceTokenRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// پیدا کردن تنظیمات دستگاه بر اساس شماره تلفن
|
||||
var settings = await dbContext.DeviceSettings
|
||||
.FirstOrDefaultAsync(ds => ds.DevicePhoneNumber == request.DevicePhoneNumber, cancellationToken);
|
||||
|
||||
if (settings == null)
|
||||
{
|
||||
return new RequestDeviceTokenResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "دستگاه با این شماره تلفن یافت نشد"
|
||||
};
|
||||
}
|
||||
|
||||
// تولید کد توکن 5 رقمی
|
||||
var random = new Random();
|
||||
var tokenCode = random.Next(10000, 99999).ToString();
|
||||
|
||||
// تولید کد تایید بر اساس فرمول: (TokenCode * 7 + 12345) % 100000
|
||||
var verificationCode = ((int.Parse(tokenCode) * 7 + 12345) % 100000).ToString("D5");
|
||||
|
||||
// ذخیره کدها
|
||||
settings.TokenCode = tokenCode;
|
||||
settings.VerificationCode = verificationCode;
|
||||
settings.TokenExpiresAt = DateTime.UtcNow.AddMinutes(30); // اعتبار 10 دقیقه
|
||||
settings.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// ارسال کد توکن از طریق پیامک الگویی
|
||||
try
|
||||
{
|
||||
await smsService.SendPatternSmsAsync(new PatternSmsRequest
|
||||
{
|
||||
Recipients = new List<string> { request.DevicePhoneNumber },
|
||||
PatternCode = "gfukab9r0nca0pt", // TODO: کد الگوی پیامک را اینجا قرار دهید
|
||||
Variables = new Dictionary<string, string>
|
||||
{
|
||||
{ "tel", "09192530212" },
|
||||
{ "verifyCode", verificationCode }
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (Exception smsEx)
|
||||
{
|
||||
logger.LogError(smsEx, "Error sending token SMS to {PhoneNumber}", request.DevicePhoneNumber);
|
||||
return new RequestDeviceTokenResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "خطا در ارسال پیامک"
|
||||
};
|
||||
}
|
||||
|
||||
logger.LogInformation("Token requested for device phone {PhoneNumber}", request.DevicePhoneNumber);
|
||||
|
||||
return new RequestDeviceTokenResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "کد تایید با موفقیت ارسال شد",
|
||||
TokenCode = $"0911925302120#{tokenCode}"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error requesting device token");
|
||||
return new RequestDeviceTokenResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "خطا در درخواست توکن"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تایید توکن دستگاه (ارسال تنظیمات)
|
||||
/// </summary>
|
||||
public async Task<VerifyDeviceTokenResponse> VerifyDeviceTokenAsync(
|
||||
VerifyDeviceTokenRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// پیدا کردن تنظیمات دستگاه
|
||||
var settings = await dbContext.DeviceSettings
|
||||
.Include(ds => ds.Device)
|
||||
.FirstOrDefaultAsync(ds => ds.DevicePhoneNumber == request.DevicePhoneNumber, cancellationToken);
|
||||
|
||||
if (settings == null)
|
||||
{
|
||||
return new VerifyDeviceTokenResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "دستگاه با این شماره تلفن یافت نشد"
|
||||
};
|
||||
}
|
||||
|
||||
// بررسی انقضای توکن
|
||||
if (settings.TokenExpiresAt == null || settings.TokenExpiresAt < DateTime.UtcNow)
|
||||
{
|
||||
return new VerifyDeviceTokenResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "کد تایید منقضی شده است"
|
||||
};
|
||||
}
|
||||
|
||||
// بررسی کد تایید
|
||||
if (settings.VerificationCode != request.VerificationCode)
|
||||
{
|
||||
return new VerifyDeviceTokenResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "کد تایید نادرست است"
|
||||
};
|
||||
}
|
||||
Random rnd = new Random();
|
||||
// آمادهسازی پارامترها برای ارسال پیامک الگویی
|
||||
var deviceId = settings.Device.Id + "01";
|
||||
var uploadInterval = rnd.Next(1, 9) + "" + settings.UploadIntervalMin;
|
||||
var smsInterval = settings.MinimumSmsIntervalMinutes > 0 ? "1" + settings.MinimumSmsIntervalMinutes : "0" + rnd.Next(1, 9);
|
||||
var sysNumber = rnd.Next(139, 97654); // عدد رندوم SysNumber
|
||||
var simType = rnd.Next(1, 9).ToString() + ((int?)settings.SimCardType)?.ToString() ?? "1"; // مقدار عددی enum
|
||||
|
||||
// ارسال تنظیمات از طریق پیامک الگویی
|
||||
try
|
||||
{
|
||||
await smsService.SendPatternSmsAsync(new PatternSmsRequest
|
||||
{
|
||||
Recipients = new List<string> { request.DevicePhoneNumber },
|
||||
PatternCode = "kx3kfqri7g09r02", // TODO: کد الگوی پیامک را اینجا قرار دهید
|
||||
Variables = new Dictionary<string, string>
|
||||
{
|
||||
{ "deviceId", deviceId },
|
||||
{ "uploadInterval", uploadInterval },
|
||||
{ "smsInterval", smsInterval.ToString() },
|
||||
{ "SysNumber", sysNumber.ToString() },
|
||||
{ "SimType", simType }
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (Exception smsEx)
|
||||
{
|
||||
logger.LogError(smsEx, "Error sending settings SMS to {PhoneNumber}", request.DevicePhoneNumber);
|
||||
return new VerifyDeviceTokenResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "خطا در ارسال پیامک"
|
||||
};
|
||||
}
|
||||
|
||||
// پاک کردن کدها بعد از استفاده موفق
|
||||
settings.TokenCode = null;
|
||||
settings.VerificationCode = null;
|
||||
settings.TokenExpiresAt = null;
|
||||
settings.UpdatedAt = DateTime.UtcNow;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Device token verified for {PhoneNumber}", request.DevicePhoneNumber);
|
||||
|
||||
return new VerifyDeviceTokenResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "تنظیمات با موفقیت ارسال شد",
|
||||
EncodedSettings = null
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error verifying device token");
|
||||
return new VerifyDeviceTokenResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "خطا در تایید توکن"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
<ProjectReference Include="..\GreenHome.Application\GreenHome.Application.csproj" />
|
||||
<ProjectReference Include="..\GreenHome.Domain\GreenHome.Domain.csproj" />
|
||||
<ProjectReference Include="..\GreenHome.Sms.Ippanel\GreenHome.Sms.Ippanel.csproj" />
|
||||
<ProjectReference Include="..\GreenHome.VoiceCall.Avanak\GreenHome.VoiceCall.Avanak.csproj" />
|
||||
<ProjectReference Include="..\GreenHome.AI.DeepSeek\GreenHome.AI.DeepSeek.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -9,10 +9,23 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
public DbSet<Domain.Device> Devices => Set<Domain.Device>();
|
||||
public DbSet<Domain.TelemetryRecord> TelemetryRecords => Set<Domain.TelemetryRecord>();
|
||||
public DbSet<Domain.DeviceSettings> DeviceSettings => Set<Domain.DeviceSettings>();
|
||||
public DbSet<Domain.AlertCondition> AlertConditions => Set<Domain.AlertCondition>();
|
||||
public DbSet<Domain.AlertRule> AlertRules => Set<Domain.AlertRule>();
|
||||
public DbSet<Domain.User> Users => Set<Domain.User>();
|
||||
public DbSet<Domain.VerificationCode> VerificationCodes => Set<Domain.VerificationCode>();
|
||||
public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>();
|
||||
public DbSet<Domain.AlertNotification> AlertNotifications => Set<Domain.AlertNotification>();
|
||||
public DbSet<Domain.AlertLog> AlertLogs => Set<Domain.AlertLog>();
|
||||
public DbSet<Domain.AIQuery> AIQueries => Set<Domain.AIQuery>();
|
||||
public DbSet<Domain.DailyReport> DailyReports => Set<Domain.DailyReport>();
|
||||
public DbSet<Domain.UserDailyReport> UserDailyReports => Set<Domain.UserDailyReport>();
|
||||
public DbSet<Domain.ReportImage> ReportImages => Set<Domain.ReportImage>();
|
||||
public DbSet<Domain.Checklist> Checklists => Set<Domain.Checklist>();
|
||||
public DbSet<Domain.ChecklistItem> ChecklistItems => Set<Domain.ChecklistItem>();
|
||||
public DbSet<Domain.ChecklistCompletion> ChecklistCompletions => Set<Domain.ChecklistCompletion>();
|
||||
public DbSet<Domain.ChecklistItemCompletion> ChecklistItemCompletions => Set<Domain.ChecklistItemCompletion>();
|
||||
public DbSet<Domain.DevicePost> DevicePosts => Set<Domain.DevicePost>();
|
||||
public DbSet<Domain.DevicePostImage> DevicePostImages => Set<Domain.DevicePostImage>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -50,14 +63,14 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
{
|
||||
b.ToTable("DeviceSettings");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.DangerMaxTemperature).HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.DangerMinTemperature).HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.MaxTemperature).HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.MinTemperature).HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.MaxLux).HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.MinLux).HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.MaxHumidityPercent).HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.MinHumidityPercent).HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.Province).HasMaxLength(100);
|
||||
b.Property(x => x.City).HasMaxLength(100);
|
||||
b.Property(x => x.Latitude).HasColumnType("decimal(9,6)");
|
||||
b.Property(x => x.Longitude).HasColumnType("decimal(9,6)");
|
||||
b.Property(x => x.ProductType).HasMaxLength(100);
|
||||
b.Property(x => x.MinimumSmsIntervalMinutes).HasDefaultValue(15);
|
||||
b.Property(x => x.MinimumCallIntervalMinutes).HasDefaultValue(60);
|
||||
b.Property(x => x.AreaSquareMeters).HasColumnType("decimal(18,2)");
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
@@ -65,6 +78,38 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
b.HasIndex(x => x.DeviceId).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.AlertCondition>(b =>
|
||||
{
|
||||
b.ToTable("AlertConditions");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.NotificationType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.TimeType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.CallCooldownMinutes).IsRequired();
|
||||
b.Property(x => x.SmsCooldownMinutes).IsRequired();
|
||||
b.Property(x => x.IsEnabled).IsRequired();
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasIndex(x => x.DeviceId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.AlertRule>(b =>
|
||||
{
|
||||
b.ToTable("AlertRules");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.SensorType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.ComparisonType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.Value1).IsRequired().HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.Value2).HasColumnType("decimal(18,2)");
|
||||
b.Property(x => x.Order).IsRequired();
|
||||
b.HasOne(x => x.AlertCondition)
|
||||
.WithMany(c => c.Rules)
|
||||
.HasForeignKey(x => x.AlertConditionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasIndex(x => x.AlertConditionId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.User>(b =>
|
||||
{
|
||||
b.ToTable("Users");
|
||||
@@ -80,6 +125,7 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
{
|
||||
b.ToTable("DeviceUsers");
|
||||
b.HasKey(x => new { x.DeviceId, x.UserId });
|
||||
b.Property(x => x.ReceiveAlerts).IsRequired().HasDefaultValue(true);
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany(d => d.DeviceUsers)
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
@@ -103,7 +149,7 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
{
|
||||
b.ToTable("AlertNotifications");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.AlertType).IsRequired().HasMaxLength(50);
|
||||
b.Property(x => x.NotificationType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.Message).IsRequired().HasMaxLength(500);
|
||||
b.Property(x => x.MessageOutboxIds).HasMaxLength(500);
|
||||
b.Property(x => x.ErrorMessage).HasMaxLength(1000);
|
||||
@@ -115,7 +161,211 @@ public sealed class GreenHomeDbContext : DbContext
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.UserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => new { x.DeviceId, x.UserId, x.AlertType, x.SentAt });
|
||||
b.HasOne(x => x.AlertCondition)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.AlertConditionId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => new { x.DeviceId, x.UserId, x.AlertConditionId, x.SentAt });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.AIQuery>(b =>
|
||||
{
|
||||
b.ToTable("AIQueries");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.Question).IsRequired();
|
||||
b.Property(x => x.Answer).IsRequired();
|
||||
b.Property(x => x.Model).HasMaxLength(100);
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
b.HasOne(x => x.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.UserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
b.HasIndex(x => x.DeviceId);
|
||||
b.HasIndex(x => x.UserId);
|
||||
b.HasIndex(x => x.CreatedAt);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.DailyReport>(b =>
|
||||
{
|
||||
b.ToTable("DailyReports");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
|
||||
b.Property(x => x.Analysis).IsRequired();
|
||||
b.Property(x => x.Model).HasMaxLength(100);
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasIndex(x => new { x.DeviceId, x.PersianDate }).IsUnique();
|
||||
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
|
||||
b.HasIndex(x => x.CreatedAt);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.AlertLog>(b =>
|
||||
{
|
||||
b.ToTable("AlertLogs");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.AlertType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.NotificationType).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.Message).IsRequired().HasMaxLength(1000);
|
||||
b.Property(x => x.Status).IsRequired().HasConversion<int>();
|
||||
b.Property(x => x.ErrorMessage).HasMaxLength(2000);
|
||||
b.Property(x => x.PhoneNumber).IsRequired().HasMaxLength(20);
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasOne(x => x.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.UserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasOne(x => x.AlertCondition)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.AlertConditionId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
b.HasIndex(x => new { x.DeviceId, x.SentAt });
|
||||
b.HasIndex(x => new { x.UserId, x.SentAt });
|
||||
b.HasIndex(x => x.AlertType);
|
||||
b.HasIndex(x => x.Status);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.UserDailyReport>(b =>
|
||||
{
|
||||
b.ToTable("UserDailyReports");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
|
||||
b.Property(x => x.Title).IsRequired().HasMaxLength(200);
|
||||
b.Property(x => x.Observations).IsRequired();
|
||||
b.Property(x => x.Operations).IsRequired();
|
||||
b.Property(x => x.Notes).HasMaxLength(2000);
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.UserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => new { x.DeviceId, x.PersianDate });
|
||||
b.HasIndex(x => new { x.DeviceId, x.PersianYear, x.PersianMonth });
|
||||
b.HasIndex(x => x.UserId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.ReportImage>(b =>
|
||||
{
|
||||
b.ToTable("ReportImages");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.FileName).IsRequired().HasMaxLength(255);
|
||||
b.Property(x => x.FilePath).IsRequired().HasMaxLength(500);
|
||||
b.Property(x => x.ContentType).IsRequired().HasMaxLength(100);
|
||||
b.Property(x => x.Description).HasMaxLength(500);
|
||||
b.HasOne(x => x.UserDailyReport)
|
||||
.WithMany(r => r.Images)
|
||||
.HasForeignKey(x => x.UserDailyReportId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasIndex(x => x.UserDailyReportId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.Checklist>(b =>
|
||||
{
|
||||
b.ToTable("Checklists");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.Title).IsRequired().HasMaxLength(200);
|
||||
b.Property(x => x.Description).HasMaxLength(1000);
|
||||
b.Property(x => x.IsActive).IsRequired();
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.CreatedByUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CreatedByUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => new { x.DeviceId, x.IsActive });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.ChecklistItem>(b =>
|
||||
{
|
||||
b.ToTable("ChecklistItems");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.Title).IsRequired().HasMaxLength(300);
|
||||
b.Property(x => x.Description).HasMaxLength(1000);
|
||||
b.Property(x => x.Order).IsRequired();
|
||||
b.Property(x => x.IsRequired).IsRequired();
|
||||
b.HasOne(x => x.Checklist)
|
||||
.WithMany(c => c.Items)
|
||||
.HasForeignKey(x => x.ChecklistId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasIndex(x => new { x.ChecklistId, x.Order });
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.ChecklistCompletion>(b =>
|
||||
{
|
||||
b.ToTable("ChecklistCompletions");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.PersianDate).IsRequired().HasMaxLength(10);
|
||||
b.Property(x => x.Notes).HasMaxLength(2000);
|
||||
b.HasOne(x => x.Checklist)
|
||||
.WithMany(c => c.Completions)
|
||||
.HasForeignKey(x => x.ChecklistId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.CompletedByUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CompletedByUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => new { x.ChecklistId, x.PersianDate });
|
||||
b.HasIndex(x => x.CompletedByUserId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.ChecklistItemCompletion>(b =>
|
||||
{
|
||||
b.ToTable("ChecklistItemCompletions");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.IsChecked).IsRequired();
|
||||
b.Property(x => x.Note).HasMaxLength(500);
|
||||
b.HasOne(x => x.ChecklistCompletion)
|
||||
.WithMany(cc => cc.ItemCompletions)
|
||||
.HasForeignKey(x => x.ChecklistCompletionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.ChecklistItem)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.ChecklistItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => x.ChecklistCompletionId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.DevicePost>(b =>
|
||||
{
|
||||
b.ToTable("DevicePosts");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.Content).IsRequired().HasMaxLength(5000);
|
||||
b.HasOne(x => x.Device)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.DeviceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.AuthorUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.AuthorUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => new { x.DeviceId, x.CreatedAt });
|
||||
b.HasIndex(x => x.AuthorUserId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Domain.DevicePostImage>(b =>
|
||||
{
|
||||
b.ToTable("DevicePostImages");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.FileName).IsRequired().HasMaxLength(255);
|
||||
b.Property(x => x.FilePath).IsRequired().HasMaxLength(500);
|
||||
b.Property(x => x.ContentType).IsRequired().HasMaxLength(100);
|
||||
b.HasOne(x => x.DevicePost)
|
||||
.WithMany(p => p.Images)
|
||||
.HasForeignKey(x => x.DevicePostId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasIndex(x => x.DevicePostId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
453
src/GreenHome.Infrastructure/Migrations/20251216113127_AddAIQueryTable.Designer.cs
generated
Normal file
453
src/GreenHome.Infrastructure/Migrations/20251216113127_AddAIQueryTable.Designer.cs
generated
Normal file
@@ -0,0 +1,453 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using GreenHome.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GreenHome.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(GreenHomeDbContext))]
|
||||
[Migration("20251216113127_AddAIQueryTable")]
|
||||
partial class AddAIQueryTable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Answer")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("CompletionTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("PromptTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Question")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long?>("ResponseTimeMs")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<double?>("Temperature")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("TotalTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AIQueries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AlertType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<bool>("IsSent")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MessageOutboxIds")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<DateTime>("SentAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("DeviceId", "UserId", "AlertType", "SentAt");
|
||||
|
||||
b.ToTable("AlertNotifications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DeviceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.IsRequired()
|
||||
.HasMaxLength(250)
|
||||
.HasColumnType("nvarchar(250)");
|
||||
|
||||
b.Property<string>("NeshanLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Devices", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal>("DangerMaxTemperature")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("DangerMinTemperature")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("MaxGasPPM")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("MaxHumidityPercent")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("MaxLux")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("MaxTemperature")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<int>("MinGasPPM")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("MinHumidityPercent")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("MinLux")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("MinTemperature")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DeviceSettings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||
{
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("DeviceId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("DeviceUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("GasPPM")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("HumidityPercent")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("Lux")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<string>("PersianDate")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<int>("PersianMonth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PersianYear")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("ServerTimestampUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal>("SoilPercent")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("TemperatureC")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime>("TimestampUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId", "ServerTimestampUtc");
|
||||
|
||||
b.HasIndex("DeviceId", "TimestampUtc");
|
||||
|
||||
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
|
||||
|
||||
b.ToTable("Telemetry", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Family")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<DateTime?>("LastLoginAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Mobile")
|
||||
.IsRequired()
|
||||
.HasMaxLength(11)
|
||||
.HasColumnType("nvarchar(11)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Mobile")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4)
|
||||
.HasColumnType("nvarchar(4)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsUsed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Mobile")
|
||||
.IsRequired()
|
||||
.HasMaxLength(11)
|
||||
.HasColumnType("nvarchar(11)");
|
||||
|
||||
b.Property<DateTime?>("UsedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Mobile", "Code", "IsUsed");
|
||||
|
||||
b.ToTable("VerificationCodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany("DeviceUsers")
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany("DeviceUsers")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||
{
|
||||
b.Navigation("DeviceUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||
{
|
||||
b.Navigation("DeviceUsers");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GreenHome.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAIQueryTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AIQueries",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: true),
|
||||
Question = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Answer = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
PromptTokens = table.Column<int>(type: "int", nullable: false),
|
||||
CompletionTokens = table.Column<int>(type: "int", nullable: false),
|
||||
TotalTokens = table.Column<int>(type: "int", nullable: false),
|
||||
Model = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
Temperature = table.Column<double>(type: "float", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ResponseTimeMs = table.Column<long>(type: "bigint", nullable: true),
|
||||
UserId = table.Column<int>(type: "int", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AIQueries", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AIQueries_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_AIQueries_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AIQueries_CreatedAt",
|
||||
table: "AIQueries",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AIQueries_DeviceId",
|
||||
table: "AIQueries",
|
||||
column: "DeviceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AIQueries_UserId",
|
||||
table: "AIQueries",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AIQueries");
|
||||
}
|
||||
}
|
||||
}
|
||||
530
src/GreenHome.Infrastructure/Migrations/20251216120357_adddailyreport.Designer.cs
generated
Normal file
530
src/GreenHome.Infrastructure/Migrations/20251216120357_adddailyreport.Designer.cs
generated
Normal file
@@ -0,0 +1,530 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using GreenHome.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GreenHome.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(GreenHomeDbContext))]
|
||||
[Migration("20251216120357_adddailyreport")]
|
||||
partial class adddailyreport
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Answer")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("CompletionTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("PromptTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Question")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long?>("ResponseTimeMs")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<double?>("Temperature")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("TotalTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AIQueries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AlertType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<bool>("IsSent")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MessageOutboxIds")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<DateTime>("SentAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("DeviceId", "UserId", "AlertType", "SentAt");
|
||||
|
||||
b.ToTable("AlertNotifications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Analysis")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("CompletionTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PersianDate")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<int>("PersianDay")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PersianMonth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PersianYear")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PromptTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RecordCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long?>("ResponseTimeMs")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("SampledRecordCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("TotalTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("DeviceId", "PersianDate")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
|
||||
|
||||
b.ToTable("DailyReports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DeviceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.IsRequired()
|
||||
.HasMaxLength(250)
|
||||
.HasColumnType("nvarchar(250)");
|
||||
|
||||
b.Property<string>("NeshanLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Devices", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal>("DangerMaxTemperature")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("DangerMinTemperature")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("MaxGasPPM")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("MaxHumidityPercent")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("MaxLux")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("MaxTemperature")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<int>("MinGasPPM")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("MinHumidityPercent")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("MinLux")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("MinTemperature")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DeviceSettings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||
{
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("DeviceId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("DeviceUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("GasPPM")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("HumidityPercent")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("Lux")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<string>("PersianDate")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<int>("PersianMonth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PersianYear")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("ServerTimestampUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal>("SoilPercent")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("TemperatureC")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime>("TimestampUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId", "ServerTimestampUtc");
|
||||
|
||||
b.HasIndex("DeviceId", "TimestampUtc");
|
||||
|
||||
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
|
||||
|
||||
b.ToTable("Telemetry", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Family")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<DateTime?>("LastLoginAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Mobile")
|
||||
.IsRequired()
|
||||
.HasMaxLength(11)
|
||||
.HasColumnType("nvarchar(11)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Mobile")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4)
|
||||
.HasColumnType("nvarchar(4)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsUsed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Mobile")
|
||||
.IsRequired()
|
||||
.HasMaxLength(11)
|
||||
.HasColumnType("nvarchar(11)");
|
||||
|
||||
b.Property<DateTime?>("UsedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Mobile", "Code", "IsUsed");
|
||||
|
||||
b.ToTable("VerificationCodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany("DeviceUsers")
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany("DeviceUsers")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||
{
|
||||
b.Navigation("DeviceUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||
{
|
||||
b.Navigation("DeviceUsers");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GreenHome.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class adddailyreport : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DailyReports",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
|
||||
PersianYear = table.Column<int>(type: "int", nullable: false),
|
||||
PersianMonth = table.Column<int>(type: "int", nullable: false),
|
||||
PersianDay = table.Column<int>(type: "int", nullable: false),
|
||||
Analysis = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
RecordCount = table.Column<int>(type: "int", nullable: false),
|
||||
SampledRecordCount = table.Column<int>(type: "int", nullable: false),
|
||||
PromptTokens = table.Column<int>(type: "int", nullable: false),
|
||||
CompletionTokens = table.Column<int>(type: "int", nullable: false),
|
||||
TotalTokens = table.Column<int>(type: "int", nullable: false),
|
||||
Model = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ResponseTimeMs = table.Column<long>(type: "bigint", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DailyReports", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DailyReports_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DailyReports_CreatedAt",
|
||||
table: "DailyReports",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DailyReports_DeviceId_PersianDate",
|
||||
table: "DailyReports",
|
||||
columns: new[] { "DeviceId", "PersianDate" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DailyReports_DeviceId_PersianYear_PersianMonth",
|
||||
table: "DailyReports",
|
||||
columns: new[] { "DeviceId", "PersianYear", "PersianMonth" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DailyReports");
|
||||
}
|
||||
}
|
||||
}
|
||||
626
src/GreenHome.Infrastructure/Migrations/20251216131032_UpdateAlertSystemWithConditions.Designer.cs
generated
Normal file
626
src/GreenHome.Infrastructure/Migrations/20251216131032_UpdateAlertSystemWithConditions.Designer.cs
generated
Normal file
@@ -0,0 +1,626 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using GreenHome.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GreenHome.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(GreenHomeDbContext))]
|
||||
[Migration("20251216131032_UpdateAlertSystemWithConditions")]
|
||||
partial class UpdateAlertSystemWithConditions
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Answer")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("CompletionTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int?>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("PromptTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Question")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long?>("ResponseTimeMs")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<double?>("Temperature")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("TotalTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AIQueries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("CallCooldownMinutes")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("NotificationType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SmsCooldownMinutes")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("TimeType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.ToTable("AlertConditions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlertConditionId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<bool>("IsSent")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MessageOutboxIds")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("NotificationType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("SentAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlertConditionId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("DeviceId", "UserId", "AlertConditionId", "SentAt");
|
||||
|
||||
b.ToTable("AlertNotifications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertRule", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlertConditionId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("ComparisonType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SensorType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("Value1")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal?>("Value2")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlertConditionId");
|
||||
|
||||
b.ToTable("AlertRules", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Analysis")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("CompletionTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PersianDate")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<int>("PersianDay")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PersianMonth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PersianYear")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PromptTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RecordCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long?>("ResponseTimeMs")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("SampledRecordCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("TotalTokens")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("DeviceId", "PersianDate")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
|
||||
|
||||
b.ToTable("DailyReports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DeviceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.IsRequired()
|
||||
.HasMaxLength(250)
|
||||
.HasColumnType("nvarchar(250)");
|
||||
|
||||
b.Property<string>("NeshanLocation")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Devices", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("City")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal?>("Latitude")
|
||||
.HasColumnType("decimal(9,6)");
|
||||
|
||||
b.Property<decimal?>("Longitude")
|
||||
.HasColumnType("decimal(9,6)");
|
||||
|
||||
b.Property<string>("Province")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DeviceSettings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||
{
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("DeviceId", "UserId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("DeviceUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.TelemetryRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("DeviceId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("GasPPM")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("HumidityPercent")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("Lux")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<string>("PersianDate")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<int>("PersianMonth")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("PersianYear")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("ServerTimestampUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal>("SoilPercent")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("TemperatureC")
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime>("TimestampUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId", "ServerTimestampUtc");
|
||||
|
||||
b.HasIndex("DeviceId", "TimestampUtc");
|
||||
|
||||
b.HasIndex("DeviceId", "PersianYear", "PersianMonth");
|
||||
|
||||
b.ToTable("Telemetry", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Family")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<DateTime?>("LastLoginAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Mobile")
|
||||
.IsRequired()
|
||||
.HasMaxLength(11)
|
||||
.HasColumnType("nvarchar(11)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Mobile")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.VerificationCode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4)
|
||||
.HasColumnType("nvarchar(4)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsUsed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Mobile")
|
||||
.IsRequired()
|
||||
.HasMaxLength(11)
|
||||
.HasColumnType("nvarchar(11)");
|
||||
|
||||
b.Property<DateTime?>("UsedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Mobile", "Code", "IsUsed");
|
||||
|
||||
b.ToTable("VerificationCodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AIQuery", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
|
||||
.WithMany()
|
||||
.HasForeignKey("AlertConditionId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AlertCondition");
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertRule", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.AlertCondition", "AlertCondition")
|
||||
.WithMany("Rules")
|
||||
.HasForeignKey("AlertConditionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AlertCondition");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DailyReport", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceSettings", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.DeviceUser", b =>
|
||||
{
|
||||
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||
.WithMany("DeviceUsers")
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("GreenHome.Domain.User", "User")
|
||||
.WithMany("DeviceUsers")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
|
||||
{
|
||||
b.Navigation("Rules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||
{
|
||||
b.Navigation("DeviceUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GreenHome.Domain.User", b =>
|
||||
{
|
||||
b.Navigation("DeviceUsers");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GreenHome.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UpdateAlertSystemWithConditions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AlertNotifications_DeviceId_UserId_AlertType_SentAt",
|
||||
table: "AlertNotifications");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DangerMaxTemperature",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DangerMinTemperature",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxGasPPM",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxHumidityPercent",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxLux",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxTemperature",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MinGasPPM",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MinHumidityPercent",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MinLux",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MinTemperature",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AlertType",
|
||||
table: "AlertNotifications");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "City",
|
||||
table: "DeviceSettings",
|
||||
type: "nvarchar(100)",
|
||||
maxLength: 100,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "Latitude",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(9,6)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "Longitude",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(9,6)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Province",
|
||||
table: "DeviceSettings",
|
||||
type: "nvarchar(100)",
|
||||
maxLength: 100,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AlertConditionId",
|
||||
table: "AlertNotifications",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "NotificationType",
|
||||
table: "AlertNotifications",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AlertConditions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||
NotificationType = table.Column<int>(type: "int", nullable: false),
|
||||
TimeType = table.Column<int>(type: "int", nullable: false),
|
||||
CallCooldownMinutes = table.Column<int>(type: "int", nullable: false),
|
||||
SmsCooldownMinutes = table.Column<int>(type: "int", nullable: false),
|
||||
IsEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AlertConditions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AlertConditions_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AlertRules",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
AlertConditionId = table.Column<int>(type: "int", nullable: false),
|
||||
SensorType = table.Column<int>(type: "int", nullable: false),
|
||||
ComparisonType = table.Column<int>(type: "int", nullable: false),
|
||||
Value1 = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
|
||||
Value2 = table.Column<decimal>(type: "decimal(18,2)", nullable: true),
|
||||
Order = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AlertRules", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AlertRules_AlertConditions_AlertConditionId",
|
||||
column: x => x.AlertConditionId,
|
||||
principalTable: "AlertConditions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertNotifications_AlertConditionId",
|
||||
table: "AlertNotifications",
|
||||
column: "AlertConditionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertNotifications_DeviceId_UserId_AlertConditionId_SentAt",
|
||||
table: "AlertNotifications",
|
||||
columns: new[] { "DeviceId", "UserId", "AlertConditionId", "SentAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertConditions_DeviceId",
|
||||
table: "AlertConditions",
|
||||
column: "DeviceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertRules_AlertConditionId",
|
||||
table: "AlertRules",
|
||||
column: "AlertConditionId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AlertNotifications_AlertConditions_AlertConditionId",
|
||||
table: "AlertNotifications",
|
||||
column: "AlertConditionId",
|
||||
principalTable: "AlertConditions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AlertNotifications_AlertConditions_AlertConditionId",
|
||||
table: "AlertNotifications");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AlertRules");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AlertConditions");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AlertNotifications_AlertConditionId",
|
||||
table: "AlertNotifications");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AlertNotifications_DeviceId_UserId_AlertConditionId_SentAt",
|
||||
table: "AlertNotifications");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "City",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Latitude",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Longitude",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Province",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AlertConditionId",
|
||||
table: "AlertNotifications");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "NotificationType",
|
||||
table: "AlertNotifications");
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "DangerMaxTemperature",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "DangerMinTemperature",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MaxGasPPM",
|
||||
table: "DeviceSettings",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "MaxHumidityPercent",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "MaxLux",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "MaxTemperature",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MinGasPPM",
|
||||
table: "DeviceSettings",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "MinHumidityPercent",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "MinLux",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "MinTemperature",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(18,2)",
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AlertType",
|
||||
table: "AlertNotifications",
|
||||
type: "nvarchar(50)",
|
||||
maxLength: 50,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertNotifications_DeviceId_UserId_AlertType_SentAt",
|
||||
table: "AlertNotifications",
|
||||
columns: new[] { "DeviceId", "UserId", "AlertType", "SentAt" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GreenHome.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDailyReportsTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DailyReports",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
|
||||
PersianYear = table.Column<int>(type: "int", nullable: false),
|
||||
PersianMonth = table.Column<int>(type: "int", nullable: false),
|
||||
PersianDay = table.Column<int>(type: "int", nullable: false),
|
||||
Analysis = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
RecordCount = table.Column<int>(type: "int", nullable: false),
|
||||
SampledRecordCount = table.Column<int>(type: "int", nullable: false),
|
||||
PromptTokens = table.Column<int>(type: "int", nullable: false),
|
||||
CompletionTokens = table.Column<int>(type: "int", nullable: false),
|
||||
TotalTokens = table.Column<int>(type: "int", nullable: false),
|
||||
Model = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ResponseTimeMs = table.Column<long>(type: "bigint", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DailyReports", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DailyReports_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DailyReports_CreatedAt",
|
||||
table: "DailyReports",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DailyReports_DeviceId_PersianDate",
|
||||
table: "DailyReports",
|
||||
columns: new[] { "DeviceId", "PersianDate" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DailyReports_DeviceId_PersianYear_PersianMonth",
|
||||
table: "DailyReports",
|
||||
columns: new[] { "DeviceId", "PersianYear", "PersianMonth" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DailyReports");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1199
src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.Designer.cs
generated
Normal file
1199
src/GreenHome.Infrastructure/Migrations/20251216204600_AddAllNewFeatures.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,479 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GreenHome.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAllNewFeatures : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ReceiveAlerts",
|
||||
table: "DeviceUsers",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "AreaSquareMeters",
|
||||
table: "DeviceSettings",
|
||||
type: "decimal(18,2)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MinimumCallIntervalMinutes",
|
||||
table: "DeviceSettings",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 60);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MinimumSmsIntervalMinutes",
|
||||
table: "DeviceSettings",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 15);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ProductType",
|
||||
table: "DeviceSettings",
|
||||
type: "nvarchar(100)",
|
||||
maxLength: 100,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "AlertConditionId",
|
||||
table: "AlertNotifications",
|
||||
type: "int",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AlertLogs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||
UserId = table.Column<int>(type: "int", nullable: false),
|
||||
AlertConditionId = table.Column<int>(type: "int", nullable: true),
|
||||
AlertType = table.Column<int>(type: "int", nullable: false),
|
||||
NotificationType = table.Column<int>(type: "int", nullable: false),
|
||||
Message = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
ErrorMessage = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||
SentAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
ProcessingTimeMs = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AlertLogs", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AlertLogs_AlertConditions_AlertConditionId",
|
||||
column: x => x.AlertConditionId,
|
||||
principalTable: "AlertConditions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_AlertLogs_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_AlertLogs_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Checklists",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatedByUserId = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Checklists", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Checklists_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Checklists_Users_CreatedByUserId",
|
||||
column: x => x.CreatedByUserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DevicePosts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||
AuthorUserId = table.Column<int>(type: "int", nullable: false),
|
||||
Content = table.Column<string>(type: "nvarchar(max)", maxLength: 5000, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DevicePosts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DevicePosts_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_DevicePosts_Users_AuthorUserId",
|
||||
column: x => x.AuthorUserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserDailyReports",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DeviceId = table.Column<int>(type: "int", nullable: false),
|
||||
UserId = table.Column<int>(type: "int", nullable: false),
|
||||
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
|
||||
PersianYear = table.Column<int>(type: "int", nullable: false),
|
||||
PersianMonth = table.Column<int>(type: "int", nullable: false),
|
||||
PersianDay = table.Column<int>(type: "int", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Observations = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Operations = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Notes = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserDailyReports", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserDailyReports_Devices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "Devices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserDailyReports_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChecklistCompletions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ChecklistId = table.Column<int>(type: "int", nullable: false),
|
||||
CompletedByUserId = table.Column<int>(type: "int", nullable: false),
|
||||
PersianDate = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
|
||||
Notes = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||
CompletedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChecklistCompletions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChecklistCompletions_Checklists_ChecklistId",
|
||||
column: x => x.ChecklistId,
|
||||
principalTable: "Checklists",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChecklistCompletions_Users_CompletedByUserId",
|
||||
column: x => x.CompletedByUserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChecklistItems",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ChecklistId = table.Column<int>(type: "int", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
Order = table.Column<int>(type: "int", nullable: false),
|
||||
IsRequired = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChecklistItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChecklistItems_Checklists_ChecklistId",
|
||||
column: x => x.ChecklistId,
|
||||
principalTable: "Checklists",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DevicePostImages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DevicePostId = table.Column<int>(type: "int", nullable: false),
|
||||
FileName = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
|
||||
FilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
ContentType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
FileSize = table.Column<long>(type: "bigint", nullable: false),
|
||||
UploadedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DevicePostImages", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DevicePostImages_DevicePosts_DevicePostId",
|
||||
column: x => x.DevicePostId,
|
||||
principalTable: "DevicePosts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ReportImages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
UserDailyReportId = table.Column<int>(type: "int", nullable: false),
|
||||
FileName = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
|
||||
FilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
ContentType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
FileSize = table.Column<long>(type: "bigint", nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
UploadedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ReportImages", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ReportImages_UserDailyReports_UserDailyReportId",
|
||||
column: x => x.UserDailyReportId,
|
||||
principalTable: "UserDailyReports",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChecklistItemCompletions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ChecklistCompletionId = table.Column<int>(type: "int", nullable: false),
|
||||
ChecklistItemId = table.Column<int>(type: "int", nullable: false),
|
||||
IsChecked = table.Column<bool>(type: "bit", nullable: false),
|
||||
Note = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChecklistItemCompletions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChecklistItemCompletions_ChecklistCompletions_ChecklistCompletionId",
|
||||
column: x => x.ChecklistCompletionId,
|
||||
principalTable: "ChecklistCompletions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChecklistItemCompletions_ChecklistItems_ChecklistItemId",
|
||||
column: x => x.ChecklistItemId,
|
||||
principalTable: "ChecklistItems",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertLogs_AlertConditionId",
|
||||
table: "AlertLogs",
|
||||
column: "AlertConditionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertLogs_AlertType",
|
||||
table: "AlertLogs",
|
||||
column: "AlertType");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertLogs_DeviceId_SentAt",
|
||||
table: "AlertLogs",
|
||||
columns: new[] { "DeviceId", "SentAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertLogs_Status",
|
||||
table: "AlertLogs",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AlertLogs_UserId_SentAt",
|
||||
table: "AlertLogs",
|
||||
columns: new[] { "UserId", "SentAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChecklistCompletions_ChecklistId_PersianDate",
|
||||
table: "ChecklistCompletions",
|
||||
columns: new[] { "ChecklistId", "PersianDate" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChecklistCompletions_CompletedByUserId",
|
||||
table: "ChecklistCompletions",
|
||||
column: "CompletedByUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChecklistItemCompletions_ChecklistCompletionId",
|
||||
table: "ChecklistItemCompletions",
|
||||
column: "ChecklistCompletionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChecklistItemCompletions_ChecklistItemId",
|
||||
table: "ChecklistItemCompletions",
|
||||
column: "ChecklistItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChecklistItems_ChecklistId_Order",
|
||||
table: "ChecklistItems",
|
||||
columns: new[] { "ChecklistId", "Order" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Checklists_CreatedByUserId",
|
||||
table: "Checklists",
|
||||
column: "CreatedByUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Checklists_DeviceId_IsActive",
|
||||
table: "Checklists",
|
||||
columns: new[] { "DeviceId", "IsActive" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DevicePostImages_DevicePostId",
|
||||
table: "DevicePostImages",
|
||||
column: "DevicePostId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DevicePosts_AuthorUserId",
|
||||
table: "DevicePosts",
|
||||
column: "AuthorUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DevicePosts_DeviceId_CreatedAt",
|
||||
table: "DevicePosts",
|
||||
columns: new[] { "DeviceId", "CreatedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ReportImages_UserDailyReportId",
|
||||
table: "ReportImages",
|
||||
column: "UserDailyReportId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserDailyReports_DeviceId_PersianDate",
|
||||
table: "UserDailyReports",
|
||||
columns: new[] { "DeviceId", "PersianDate" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserDailyReports_DeviceId_PersianYear_PersianMonth",
|
||||
table: "UserDailyReports",
|
||||
columns: new[] { "DeviceId", "PersianYear", "PersianMonth" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserDailyReports_UserId",
|
||||
table: "UserDailyReports",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AlertLogs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChecklistItemCompletions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DevicePostImages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ReportImages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChecklistCompletions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChecklistItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DevicePosts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserDailyReports");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Checklists");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ReceiveAlerts",
|
||||
table: "DeviceUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AreaSquareMeters",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MinimumCallIntervalMinutes",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MinimumSmsIntervalMinutes",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProductType",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "AlertConditionId",
|
||||
table: "AlertNotifications",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
1218
src/GreenHome.Infrastructure/Migrations/20251217154130_AddDeviceTokenAndPhoneFields.Designer.cs
generated
Normal file
1218
src/GreenHome.Infrastructure/Migrations/20251217154130_AddDeviceTokenAndPhoneFields.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GreenHome.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDeviceTokenAndPhoneFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DevicePhoneNumber",
|
||||
table: "DeviceSettings",
|
||||
type: "nvarchar(max)",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SimCardType",
|
||||
table: "DeviceSettings",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TokenCode",
|
||||
table: "DeviceSettings",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "TokenExpiresAt",
|
||||
table: "DeviceSettings",
|
||||
type: "datetime2",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "UploadIntervalMin",
|
||||
table: "DeviceSettings",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "VerificationCode",
|
||||
table: "DeviceSettings",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DevicePhoneNumber",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SimCardType",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TokenCode",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TokenExpiresAt",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UploadIntervalMin",
|
||||
table: "DeviceSettings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "VerificationCode",
|
||||
table: "DeviceSettings");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
185
src/GreenHome.Infrastructure/MonthlyReportService.cs
Normal file
185
src/GreenHome.Infrastructure/MonthlyReportService.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class MonthlyReportService : IMonthlyReportService
|
||||
{
|
||||
private readonly GreenHomeDbContext _context;
|
||||
|
||||
public MonthlyReportService(GreenHomeDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<MonthlyReportDto> GetMonthlyReportAsync(
|
||||
int deviceId,
|
||||
int year,
|
||||
int month,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var device = await _context.Devices
|
||||
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
throw new InvalidOperationException($"دستگاه با شناسه {deviceId} یافت نشد");
|
||||
}
|
||||
|
||||
// Get alert logs for the month
|
||||
var alertLogs = await _context.AlertLogs
|
||||
.Where(al => al.DeviceId == deviceId &&
|
||||
al.SentAt.Year == year &&
|
||||
al.SentAt.Month == month)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var totalAlerts = alertLogs.Count;
|
||||
var smsAlerts = alertLogs.Count(al => al.NotificationType == Domain.AlertNotificationType.SMS);
|
||||
var callAlerts = alertLogs.Count(al => al.NotificationType == Domain.AlertNotificationType.Call);
|
||||
var successfulAlerts = alertLogs.Count(al => al.Status == Domain.AlertStatus.Success);
|
||||
var failedAlerts = alertLogs.Count(al => al.Status == Domain.AlertStatus.Failed);
|
||||
var powerOutageAlerts = alertLogs.Count(al => al.AlertType == Domain.AlertType.PowerOutage);
|
||||
|
||||
// Get telemetry statistics
|
||||
var telemetryData = await _context.TelemetryRecords
|
||||
.Where(t => t.DeviceId == deviceId &&
|
||||
t.PersianYear == year &&
|
||||
t.PersianMonth == month)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var totalTelemetryRecords = telemetryData.Count;
|
||||
var avgTemp = telemetryData.Any() ? telemetryData.Average(t => t.TemperatureC) : 0;
|
||||
var minTemp = telemetryData.Any() ? telemetryData.Min(t => t.TemperatureC) : 0;
|
||||
var maxTemp = telemetryData.Any() ? telemetryData.Max(t => t.TemperatureC) : 0;
|
||||
var avgHumidity = telemetryData.Any() ? telemetryData.Average(t => t.HumidityPercent) : 0;
|
||||
var minHumidity = telemetryData.Any() ? telemetryData.Min(t => t.HumidityPercent) : 0;
|
||||
var maxHumidity = telemetryData.Any() ? telemetryData.Max(t => t.HumidityPercent) : 0;
|
||||
var avgLux = telemetryData.Any() ? telemetryData.Average(t => t.Lux) : 0;
|
||||
var avgGas = telemetryData.Any() ? (int)telemetryData.Average(t => t.GasPPM) : 0;
|
||||
var maxGas = telemetryData.Any() ? telemetryData.Max(t => t.GasPPM) : 0;
|
||||
|
||||
// Get user activity
|
||||
var userReportsCount = await _context.UserDailyReports
|
||||
.CountAsync(r => r.DeviceId == deviceId &&
|
||||
r.PersianYear == year &&
|
||||
r.PersianMonth == month,
|
||||
cancellationToken);
|
||||
|
||||
var checklistCompletionsCount = await _context.ChecklistCompletions
|
||||
.Where(cc => cc.Checklist.DeviceId == deviceId)
|
||||
.CountAsync(cc => cc.PersianDate.StartsWith($"{year}/{month:D2}"), cancellationToken);
|
||||
|
||||
var dailyAnalysesCount = await _context.DailyReports
|
||||
.CountAsync(dr => dr.DeviceId == deviceId &&
|
||||
dr.PersianYear == year &&
|
||||
dr.PersianMonth == month,
|
||||
cancellationToken);
|
||||
|
||||
// Generate performance summary
|
||||
var performanceSummary = GeneratePerformanceSummary(
|
||||
totalTelemetryRecords,
|
||||
totalAlerts,
|
||||
successfulAlerts,
|
||||
failedAlerts,
|
||||
avgTemp,
|
||||
avgHumidity,
|
||||
avgLux,
|
||||
avgGas,
|
||||
userReportsCount,
|
||||
checklistCompletionsCount);
|
||||
|
||||
return new MonthlyReportDto
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DeviceName = device.DeviceName,
|
||||
Year = year,
|
||||
Month = month,
|
||||
TotalAlerts = totalAlerts,
|
||||
SmsAlerts = smsAlerts,
|
||||
CallAlerts = callAlerts,
|
||||
SuccessfulAlerts = successfulAlerts,
|
||||
FailedAlerts = failedAlerts,
|
||||
PowerOutageAlerts = powerOutageAlerts,
|
||||
TotalTelemetryRecords = totalTelemetryRecords,
|
||||
AverageTemperature = avgTemp,
|
||||
MinTemperature = minTemp,
|
||||
MaxTemperature = maxTemp,
|
||||
AverageHumidity = avgHumidity,
|
||||
MinHumidity = minHumidity,
|
||||
MaxHumidity = maxHumidity,
|
||||
AverageLux = avgLux,
|
||||
AverageGasPPM = avgGas,
|
||||
MaxGasPPM = maxGas,
|
||||
UserDailyReportsCount = userReportsCount,
|
||||
ChecklistCompletionsCount = checklistCompletionsCount,
|
||||
DailyAnalysesCount = dailyAnalysesCount,
|
||||
PerformanceSummary = performanceSummary,
|
||||
GeneratedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private string GeneratePerformanceSummary(
|
||||
int totalRecords,
|
||||
int totalAlerts,
|
||||
int successfulAlerts,
|
||||
int failedAlerts,
|
||||
decimal avgTemp,
|
||||
decimal avgHumidity,
|
||||
decimal avgLux,
|
||||
int avgGas,
|
||||
int userReports,
|
||||
int checklistCompletions)
|
||||
{
|
||||
var summary = new List<string>();
|
||||
|
||||
summary.Add($"📊 آمار کلی:");
|
||||
summary.Add($" • تعداد رکوردهای ثبت شده: {totalRecords:N0}");
|
||||
summary.Add($" • میانگین دما: {avgTemp:F1}°C");
|
||||
summary.Add($" • میانگین رطوبت: {avgHumidity:F1}%");
|
||||
summary.Add($" • میانگین نور: {avgLux:F0} لوکس");
|
||||
if (avgGas > 0)
|
||||
summary.Add($" • میانگین CO: {avgGas} PPM");
|
||||
|
||||
summary.Add("");
|
||||
summary.Add($"🚨 هشدارها:");
|
||||
summary.Add($" • تعداد کل: {totalAlerts}");
|
||||
summary.Add($" • موفق: {successfulAlerts}");
|
||||
if (failedAlerts > 0)
|
||||
summary.Add($" • ناموفق: {failedAlerts} ⚠️");
|
||||
|
||||
if (userReports > 0 || checklistCompletions > 0)
|
||||
{
|
||||
summary.Add("");
|
||||
summary.Add($"📝 فعالیت کاربران:");
|
||||
if (userReports > 0)
|
||||
summary.Add($" • گزارشهای روزانه: {userReports}");
|
||||
if (checklistCompletions > 0)
|
||||
summary.Add($" • تکمیل چکلیست: {checklistCompletions}");
|
||||
}
|
||||
|
||||
// Performance rating
|
||||
summary.Add("");
|
||||
var rating = CalculatePerformanceRating(totalRecords, failedAlerts, totalAlerts);
|
||||
summary.Add($"⭐ ارزیابی کلی: {rating}");
|
||||
|
||||
return string.Join("\n", summary);
|
||||
}
|
||||
|
||||
private string CalculatePerformanceRating(int totalRecords, int failedAlerts, int totalAlerts)
|
||||
{
|
||||
if (totalRecords == 0)
|
||||
return "بدون داده";
|
||||
|
||||
var failureRate = totalAlerts > 0 ? (double)failedAlerts / totalAlerts : 0;
|
||||
|
||||
if (failureRate == 0 && totalRecords > 1000)
|
||||
return "عالی ✅";
|
||||
else if (failureRate < 0.1 && totalRecords > 500)
|
||||
return "خوب 👍";
|
||||
else if (failureRate < 0.3)
|
||||
return "متوسط ⚠️";
|
||||
else
|
||||
return "نیاز به بررسی 🔧";
|
||||
}
|
||||
}
|
||||
|
||||
125
src/GreenHome.Infrastructure/SunCalculatorService.cs
Normal file
125
src/GreenHome.Infrastructure/SunCalculatorService.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using GreenHome.Application;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس محاسبه طلوع و غروب خورشید
|
||||
/// </summary>
|
||||
public sealed class SunCalculatorService : ISunCalculatorService
|
||||
{
|
||||
public bool IsDaytime(DateTime dateTime, decimal latitude, decimal longitude)
|
||||
{
|
||||
var lat = (double)latitude;
|
||||
var lng = (double)longitude;
|
||||
|
||||
// Calculate sunrise and sunset times
|
||||
var (sunrise, sunset) = CalculateSunriseSunset(dateTime, lat, lng);
|
||||
|
||||
// Check if current time is between sunrise and sunset
|
||||
var currentTime = dateTime.TimeOfDay;
|
||||
return currentTime >= sunrise && currentTime <= sunset;
|
||||
}
|
||||
|
||||
private (TimeSpan sunrise, TimeSpan sunset) CalculateSunriseSunset(DateTime date, double latitude, double longitude)
|
||||
{
|
||||
// Julian day calculation
|
||||
var julianDay = CalculateJulianDay(date);
|
||||
var julianCentury = (julianDay - 2451545.0) / 36525.0;
|
||||
|
||||
// Sun's mean longitude
|
||||
var sunMeanLongitude = (280.46646 + julianCentury * (36000.76983 + julianCentury * 0.0003032)) % 360;
|
||||
|
||||
// Sun's mean anomaly
|
||||
var sunMeanAnomaly = 357.52911 + julianCentury * (35999.05029 - 0.0001537 * julianCentury);
|
||||
|
||||
// Earth's orbit eccentricity
|
||||
var eccentricity = 0.016708634 - julianCentury * (0.000042037 + 0.0000001267 * julianCentury);
|
||||
|
||||
// Sun's equation of center
|
||||
var sunCenter = Math.Sin(ToRadians(sunMeanAnomaly)) * (1.914602 - julianCentury * (0.004817 + 0.000014 * julianCentury))
|
||||
+ Math.Sin(ToRadians(2 * sunMeanAnomaly)) * (0.019993 - 0.000101 * julianCentury)
|
||||
+ Math.Sin(ToRadians(3 * sunMeanAnomaly)) * 0.000289;
|
||||
|
||||
// Sun's true longitude
|
||||
var sunTrueLongitude = sunMeanLongitude + sunCenter;
|
||||
|
||||
// Sun's apparent longitude
|
||||
var sunApparentLongitude = sunTrueLongitude - 0.00569 - 0.00478 * Math.Sin(ToRadians(125.04 - 1934.136 * julianCentury));
|
||||
|
||||
// Mean oblique ecliptic
|
||||
var meanOblique = 23.0 + (26.0 + ((21.448 - julianCentury * (46.815 + julianCentury * (0.00059 - julianCentury * 0.001813)))) / 60.0) / 60.0;
|
||||
|
||||
// Oblique correction
|
||||
var obliqueCorrection = meanOblique + 0.00256 * Math.Cos(ToRadians(125.04 - 1934.136 * julianCentury));
|
||||
|
||||
// Sun's declination
|
||||
var declination = ToDegrees(Math.Asin(Math.Sin(ToRadians(obliqueCorrection)) * Math.Sin(ToRadians(sunApparentLongitude))));
|
||||
|
||||
// Equation of time
|
||||
var y = Math.Tan(ToRadians(obliqueCorrection / 2.0)) * Math.Tan(ToRadians(obliqueCorrection / 2.0));
|
||||
var equationOfTime = 4.0 * ToDegrees(y * Math.Sin(2.0 * ToRadians(sunMeanLongitude))
|
||||
- 2.0 * eccentricity * Math.Sin(ToRadians(sunMeanAnomaly))
|
||||
+ 4.0 * eccentricity * y * Math.Sin(ToRadians(sunMeanAnomaly)) * Math.Cos(2.0 * ToRadians(sunMeanLongitude))
|
||||
- 0.5 * y * y * Math.Sin(4.0 * ToRadians(sunMeanLongitude))
|
||||
- 1.25 * eccentricity * eccentricity * Math.Sin(2.0 * ToRadians(sunMeanAnomaly)));
|
||||
|
||||
// Hour angle sunrise (civil twilight: sun 6 degrees below horizon)
|
||||
var zenith = 90.833; // Official: 90 degrees 50 minutes
|
||||
var hourAngle = ToDegrees(Math.Acos(
|
||||
(Math.Cos(ToRadians(zenith)) / (Math.Cos(ToRadians(latitude)) * Math.Cos(ToRadians(declination))))
|
||||
- Math.Tan(ToRadians(latitude)) * Math.Tan(ToRadians(declination))
|
||||
));
|
||||
|
||||
// Calculate sunrise and sunset in minutes
|
||||
var solarNoon = (720.0 - 4.0 * longitude - equationOfTime) / 1440.0;
|
||||
var sunriseTime = solarNoon - hourAngle * 4.0 / 1440.0;
|
||||
var sunsetTime = solarNoon + hourAngle * 4.0 / 1440.0;
|
||||
|
||||
// Convert to local time (assume UTC offset for Iran: +3:30 = 210 minutes)
|
||||
// You should ideally calculate timezone offset based on longitude
|
||||
var utcOffsetMinutes = Math.Round(longitude / 15.0) * 60.0;
|
||||
|
||||
var sunriseMinutes = sunriseTime * 1440.0 + utcOffsetMinutes;
|
||||
var sunsetMinutes = sunsetTime * 1440.0 + utcOffsetMinutes;
|
||||
|
||||
// Handle edge cases
|
||||
if (sunriseMinutes < 0) sunriseMinutes += 1440;
|
||||
if (sunriseMinutes >= 1440) sunriseMinutes -= 1440;
|
||||
if (sunsetMinutes < 0) sunsetMinutes += 1440;
|
||||
if (sunsetMinutes >= 1440) sunsetMinutes -= 1440;
|
||||
|
||||
var sunrise = TimeSpan.FromMinutes(sunriseMinutes);
|
||||
var sunset = TimeSpan.FromMinutes(sunsetMinutes);
|
||||
|
||||
return (sunrise, sunset);
|
||||
}
|
||||
|
||||
private double CalculateJulianDay(DateTime date)
|
||||
{
|
||||
var year = date.Year;
|
||||
var month = date.Month;
|
||||
var day = date.Day;
|
||||
|
||||
if (month <= 2)
|
||||
{
|
||||
year -= 1;
|
||||
month += 12;
|
||||
}
|
||||
|
||||
var a = year / 100;
|
||||
var b = 2 - a + (a / 4);
|
||||
|
||||
return Math.Floor(365.25 * (year + 4716)) + Math.Floor(30.6001 * (month + 1)) + day + b - 1524.5;
|
||||
}
|
||||
|
||||
private double ToRadians(double degrees)
|
||||
{
|
||||
return degrees * Math.PI / 180.0;
|
||||
}
|
||||
|
||||
private double ToDegrees(double radians)
|
||||
{
|
||||
return radians * 180.0 / Math.PI;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,18 @@ public sealed class TelemetryService : ITelemetryService
|
||||
dto.DeviceId = device.Id; // Update DTO for alert service
|
||||
}
|
||||
}
|
||||
if (dto.Id!=0)
|
||||
{
|
||||
var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.Id == dto.Id, cancellationToken);
|
||||
if (device != null)
|
||||
{
|
||||
entity.DeviceId = device.Id;
|
||||
}
|
||||
}
|
||||
// ذخیره زمان سرور در لحظه ثبت
|
||||
entity.ServerTimestampUtc = DateTime.Now;
|
||||
|
||||
var dt = dto.TimestampUtc;
|
||||
var dt = dto.TimestampUtc.ToLocalTime();
|
||||
var py = PersianCalendar.GetYear(dt);
|
||||
var pm = PersianCalendar.GetMonth(dt);
|
||||
var pd = PersianCalendar.GetDayOfMonth(dt);
|
||||
@@ -65,12 +73,13 @@ public sealed class TelemetryService : ITelemetryService
|
||||
if (filter.StartDateUtc.HasValue)
|
||||
{
|
||||
//var start = filter.StartDateUtc.Value.Date.AddDays(1);
|
||||
query = query.Where(x => x.ServerTimestampUtc >= filter.StartDateUtc.Value);
|
||||
|
||||
query = query.Where(x => x.ServerTimestampUtc >= filter.StartDateUtc.Value.ToLocalTime());
|
||||
}
|
||||
|
||||
if (filter.EndDateUtc.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.ServerTimestampUtc < filter.EndDateUtc.Value);
|
||||
query = query.Where(x => x.ServerTimestampUtc < filter.EndDateUtc.Value.ToLocalTime());
|
||||
}
|
||||
|
||||
if(filter.Page <= 0) filter.Page = 1;
|
||||
|
||||
225
src/GreenHome.Infrastructure/UserDailyReportService.cs
Normal file
225
src/GreenHome.Infrastructure/UserDailyReportService.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using AutoMapper;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GreenHome.Infrastructure;
|
||||
|
||||
public sealed class UserDailyReportService : IUserDailyReportService
|
||||
{
|
||||
private readonly GreenHomeDbContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public UserDailyReportService(GreenHomeDbContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<UserDailyReportDto>> GetReportsAsync(
|
||||
UserDailyReportFilter filter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.UserDailyReports
|
||||
.Include(r => r.Device)
|
||||
.Include(r => r.User)
|
||||
.Include(r => r.Images)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
if (filter.DeviceId.HasValue)
|
||||
{
|
||||
query = query.Where(r => r.DeviceId == filter.DeviceId.Value);
|
||||
}
|
||||
|
||||
if (filter.UserId.HasValue)
|
||||
{
|
||||
query = query.Where(r => r.UserId == filter.UserId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.PersianDate))
|
||||
{
|
||||
query = query.Where(r => r.PersianDate == filter.PersianDate);
|
||||
}
|
||||
|
||||
if (filter.Year.HasValue)
|
||||
{
|
||||
query = query.Where(r => r.PersianYear == filter.Year.Value);
|
||||
}
|
||||
|
||||
if (filter.Month.HasValue)
|
||||
{
|
||||
query = query.Where(r => r.PersianMonth == filter.Month.Value);
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(r => r.PersianDate)
|
||||
.ThenByDescending(r => r.CreatedAt)
|
||||
.Skip((filter.Page - 1) * filter.PageSize)
|
||||
.Take(filter.PageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var dtos = _mapper.Map<List<UserDailyReportDto>>(items);
|
||||
|
||||
return new PagedResult<UserDailyReportDto>
|
||||
{
|
||||
Items = dtos,
|
||||
TotalCount = totalCount,
|
||||
Page = filter.Page,
|
||||
PageSize = filter.PageSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<UserDailyReportDto?> GetReportByIdAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var report = await _context.UserDailyReports
|
||||
.Include(r => r.Device)
|
||||
.Include(r => r.User)
|
||||
.Include(r => r.Images)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(r => r.Id == id, cancellationToken);
|
||||
|
||||
return report != null ? _mapper.Map<UserDailyReportDto>(report) : null;
|
||||
}
|
||||
|
||||
public async Task<int> CreateReportAsync(
|
||||
CreateUserDailyReportRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Validate date format
|
||||
if (!IsValidPersianDate(request.PersianDate, out var year, out var month, out var day))
|
||||
{
|
||||
throw new ArgumentException("تاریخ شمسی باید به فرمت yyyy/MM/dd باشد");
|
||||
}
|
||||
|
||||
var report = new Domain.UserDailyReport
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = request.UserId,
|
||||
PersianDate = request.PersianDate,
|
||||
PersianYear = year,
|
||||
PersianMonth = month,
|
||||
PersianDay = day,
|
||||
Title = request.Title,
|
||||
Observations = request.Observations,
|
||||
Operations = request.Operations,
|
||||
Notes = request.Notes,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.UserDailyReports.Add(report);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return report.Id;
|
||||
}
|
||||
|
||||
public async Task UpdateReportAsync(
|
||||
UpdateUserDailyReportRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var report = await _context.UserDailyReports
|
||||
.FirstOrDefaultAsync(r => r.Id == request.Id, cancellationToken);
|
||||
|
||||
if (report == null)
|
||||
{
|
||||
throw new InvalidOperationException($"گزارش با شناسه {request.Id} یافت نشد");
|
||||
}
|
||||
|
||||
report.Title = request.Title;
|
||||
report.Observations = request.Observations;
|
||||
report.Operations = request.Operations;
|
||||
report.Notes = request.Notes;
|
||||
report.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteReportAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var report = await _context.UserDailyReports
|
||||
.Include(r => r.Images)
|
||||
.FirstOrDefaultAsync(r => r.Id == id, cancellationToken);
|
||||
|
||||
if (report == null)
|
||||
{
|
||||
throw new InvalidOperationException($"گزارش با شناسه {id} یافت نشد");
|
||||
}
|
||||
|
||||
_context.UserDailyReports.Remove(report);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> AddImageToReportAsync(
|
||||
int reportId,
|
||||
string fileName,
|
||||
string filePath,
|
||||
string contentType,
|
||||
long fileSize,
|
||||
string? description,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var report = await _context.UserDailyReports
|
||||
.FirstOrDefaultAsync(r => r.Id == reportId, cancellationToken);
|
||||
|
||||
if (report == null)
|
||||
{
|
||||
throw new InvalidOperationException($"گزارش با شناسه {reportId} یافت نشد");
|
||||
}
|
||||
|
||||
var image = new Domain.ReportImage
|
||||
{
|
||||
UserDailyReportId = reportId,
|
||||
FileName = fileName,
|
||||
FilePath = filePath,
|
||||
ContentType = contentType,
|
||||
FileSize = fileSize,
|
||||
Description = description,
|
||||
UploadedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.ReportImages.Add(image);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return image.Id;
|
||||
}
|
||||
|
||||
public async Task DeleteImageAsync(int imageId, CancellationToken cancellationToken)
|
||||
{
|
||||
var image = await _context.ReportImages
|
||||
.FirstOrDefaultAsync(i => i.Id == imageId, cancellationToken);
|
||||
|
||||
if (image == null)
|
||||
{
|
||||
throw new InvalidOperationException($"تصویر با شناسه {imageId} یافت نشد");
|
||||
}
|
||||
|
||||
_context.ReportImages.Remove(image);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static bool IsValidPersianDate(string persianDate, out int year, out int month, out int day)
|
||||
{
|
||||
year = month = day = 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(persianDate))
|
||||
return false;
|
||||
|
||||
var parts = persianDate.Split('/');
|
||||
if (parts.Length != 3)
|
||||
return false;
|
||||
|
||||
if (!int.TryParse(parts[0], out year) || year < 1300 || year > 1500)
|
||||
return false;
|
||||
|
||||
if (!int.TryParse(parts[1], out month) || month < 1 || month > 12)
|
||||
return false;
|
||||
|
||||
if (!int.TryParse(parts[2], out day) || day < 1 || day > 31)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.Sms.Ippanel", "Gr
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.VoiceCall.Avanak", "GreenHome.VoiceCall.Avanak\GreenHome.VoiceCall.Avanak.csproj", "{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.AI.DeepSeek", "GreenHome.AI.DeepSeek\GreenHome.AI.DeepSeek.csproj", "{63D676E7-B882-4E70-8090-E56CFD516B0A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -45,6 +47,10 @@ Global
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{63D676E7-B882-4E70-8090-E56CFD516B0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
1977
src/Test/Untitled-1.cpp
Normal file
1977
src/Test/Untitled-1.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user