version 2
This commit is contained in:
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 در پروژه استفاده کنید
|
||||||
|
|
||||||
|
**موفق باشید! 🚀**
|
||||||
|
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
77
src/GreenHome.Api/Controllers/DailyReportController.cs
Normal file
77
src/GreenHome.Api/Controllers/DailyReportController.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
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 = "خطای سرور در پردازش درخواست" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<ProjectReference Include="..\GreenHome.Infrastructure\GreenHome.Infrastructure.csproj" />
|
<ProjectReference Include="..\GreenHome.Infrastructure\GreenHome.Infrastructure.csproj" />
|
||||||
<ProjectReference Include="..\GreenHome.Sms.Ippanel\GreenHome.Sms.Ippanel.csproj" />
|
<ProjectReference Include="..\GreenHome.Sms.Ippanel\GreenHome.Sms.Ippanel.csproj" />
|
||||||
<ProjectReference Include="..\GreenHome.VoiceCall.Avanak\GreenHome.VoiceCall.Avanak.csproj" />
|
<ProjectReference Include="..\GreenHome.VoiceCall.Avanak\GreenHome.VoiceCall.Avanak.csproj" />
|
||||||
|
<ProjectReference Include="..\GreenHome.AI.DeepSeek\GreenHome.AI.DeepSeek.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
using GreenHome.AI.DeepSeek;
|
||||||
using GreenHome.Application;
|
using GreenHome.Application;
|
||||||
using GreenHome.Infrastructure;
|
using GreenHome.Infrastructure;
|
||||||
using GreenHome.Sms.Ippanel;
|
using GreenHome.Sms.Ippanel;
|
||||||
@@ -15,26 +16,35 @@ builder.Services.AddSwaggerGen();
|
|||||||
builder.Services.AddAutoMapper(typeof(GreenHome.Application.MappingProfile));
|
builder.Services.AddAutoMapper(typeof(GreenHome.Application.MappingProfile));
|
||||||
builder.Services.AddValidatorsFromAssemblyContaining<GreenHome.Application.DeviceDtoValidator>();
|
builder.Services.AddValidatorsFromAssemblyContaining<GreenHome.Application.DeviceDtoValidator>();
|
||||||
|
|
||||||
// CORS for Next.js dev (adjust origins as needed)
|
// CORS Configuration
|
||||||
const string CorsPolicy = "DefaultCors";
|
const string CorsPolicy = "DefaultCors";
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy(CorsPolicy, policy =>
|
options.AddPolicy(CorsPolicy, policy =>
|
||||||
|
{
|
||||||
|
if (builder.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
// در محیط Development همه origin ها مجاز هستند
|
||||||
|
policy
|
||||||
|
.AllowAnyOrigin()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// در محیط Production فقط دامنههای مشخص
|
||||||
policy
|
policy
|
||||||
.WithOrigins(
|
.WithOrigins(
|
||||||
"http://green.nabaksoft.ir",
|
"http://green.nabaksoft.ir",
|
||||||
"https://green.nabaksoft.ir",
|
"https://green.nabaksoft.ir",
|
||||||
"http://gh1.nabaksoft.ir",
|
"http://gh1.nabaksoft.ir",
|
||||||
"https://gh1.nabaksoft.ir",
|
"https://gh1.nabaksoft.ir"
|
||||||
"http://localhost:3000",
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://127.0.0.1:3000",
|
|
||||||
"https://localhost:3000"
|
|
||||||
)
|
)
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowAnyMethod()
|
.AllowAnyMethod()
|
||||||
.AllowCredentials()
|
.AllowCredentials();
|
||||||
);
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddDbContext<GreenHome.Infrastructure.GreenHomeDbContext>(options =>
|
builder.Services.AddDbContext<GreenHome.Infrastructure.GreenHomeDbContext>(options =>
|
||||||
@@ -46,6 +56,10 @@ builder.Services.AddScoped<GreenHome.Application.ITelemetryService, GreenHome.In
|
|||||||
builder.Services.AddScoped<GreenHome.Application.IDeviceSettingsService, GreenHome.Infrastructure.DeviceSettingsService>();
|
builder.Services.AddScoped<GreenHome.Application.IDeviceSettingsService, GreenHome.Infrastructure.DeviceSettingsService>();
|
||||||
builder.Services.AddScoped<GreenHome.Application.IAuthService, GreenHome.Infrastructure.AuthService>();
|
builder.Services.AddScoped<GreenHome.Application.IAuthService, GreenHome.Infrastructure.AuthService>();
|
||||||
builder.Services.AddScoped<GreenHome.Application.IAlertService, GreenHome.Infrastructure.AlertService>();
|
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>();
|
||||||
|
|
||||||
// SMS Service Configuration
|
// SMS Service Configuration
|
||||||
builder.Services.AddIppanelSms(builder.Configuration);
|
builder.Services.AddIppanelSms(builder.Configuration);
|
||||||
@@ -53,6 +67,9 @@ builder.Services.AddIppanelSms(builder.Configuration);
|
|||||||
// Voice Call Service Configuration
|
// Voice Call Service Configuration
|
||||||
builder.Services.AddAvanakVoiceCall(builder.Configuration);
|
builder.Services.AddAvanakVoiceCall(builder.Configuration);
|
||||||
|
|
||||||
|
// AI Service Configuration
|
||||||
|
builder.Services.AddDeepSeek(builder.Configuration);
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Apply pending migrations automatically
|
// Apply pending migrations automatically
|
||||||
@@ -78,7 +95,11 @@ using (var scope = app.Services.CreateScope())
|
|||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTPS Redirection فقط در Production
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
}
|
||||||
|
|
||||||
app.UseCors(CorsPolicy);
|
app.UseCors(CorsPolicy);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"applicationUrl": "http://localhost:5064"
|
"applicationUrl": "http://0.0.0.0:5064"
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"applicationUrl": "https://localhost:7274;http://localhost:5064"
|
"applicationUrl": "https://0.0.0.0:7274;http://0.0.0.0:5064"
|
||||||
},
|
},
|
||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
|
|||||||
@@ -4,5 +4,16 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"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=",
|
"AuthorizationToken": "YTA1Zjk3N2EtNzkwOC00ZTg5LWFjZmYtZGEyZDAyNjNlZWQxM2Q2ZDVjYWE0MTA2Yzc1NDYzZDY1Y2VkMjlhMzcwNjA=",
|
||||||
"DefaultSender": "+983000505"
|
"DefaultSender": "+983000505"
|
||||||
},
|
},
|
||||||
|
"DeepSeek": {
|
||||||
|
"BaseUrl": "https://api.deepseek.com",
|
||||||
|
"ApiKey": "sk-4470fc1a003a445e92f357dbe123e5a4",
|
||||||
|
"DefaultModel": "deepseek-chat",
|
||||||
|
"DefaultTemperature": 1.0
|
||||||
|
},
|
||||||
|
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
@@ -118,24 +118,88 @@ public sealed class DeviceSettingsDto
|
|||||||
public int DeviceId { get; set; }
|
public int DeviceId { get; set; }
|
||||||
public string DeviceName { get; set; } = string.Empty;
|
public string DeviceName { get; set; } = string.Empty;
|
||||||
|
|
||||||
// Temperature settings
|
public string Province { get; set; } = string.Empty;
|
||||||
public decimal DangerMaxTemperature { get; set; }
|
public string City { get; set; } = string.Empty;
|
||||||
public decimal DangerMinTemperature { get; set; }
|
public decimal? Latitude { get; set; }
|
||||||
public decimal MaxTemperature { get; set; }
|
public decimal? Longitude { get; set; }
|
||||||
public decimal MinTemperature { get; set; }
|
|
||||||
|
|
||||||
// Gas settings
|
|
||||||
public int MaxGasPPM { get; set; }
|
|
||||||
public int MinGasPPM { 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 DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public DateTime UpdatedAt { 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; }
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
13
src/GreenHome.Application/IDailyReportService.cs
Normal file
13
src/GreenHome.Application/IDailyReportService.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -21,6 +21,16 @@ public sealed class MappingProfile : Profile
|
|||||||
.ReverseMap()
|
.ReverseMap()
|
||||||
.ForMember(dest => dest.Device, opt => opt.Ignore());
|
.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.User, UserDto>().ReverseMap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,9 +7,11 @@ public sealed class AlertNotification
|
|||||||
public Device Device { get; set; } = null!;
|
public Device Device { get; set; } = null!;
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public User User { get; set; } = null!;
|
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; } = null!;
|
||||||
|
public AlertNotificationType NotificationType { get; set; } // Call or SMS
|
||||||
public string Message { get; set; } = string.Empty;
|
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 string? ErrorMessage { get; set; } // Error details if sending failed
|
||||||
public DateTime SentAt { get; set; } = DateTime.UtcNow;
|
public DateTime SentAt { get; set; } = DateTime.UtcNow;
|
||||||
public bool IsSent { get; set; } = true;
|
public bool IsSent { get; set; } = true;
|
||||||
|
|||||||
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; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,23 +6,25 @@ public sealed class DeviceSettings
|
|||||||
public int DeviceId { get; set; }
|
public int DeviceId { get; set; }
|
||||||
public Device Device { get; set; } = null!;
|
public Device Device { get; set; } = null!;
|
||||||
|
|
||||||
// Temperature settings
|
/// <summary>
|
||||||
public decimal DangerMaxTemperature { get; set; } // decimal(18,2)
|
/// استان
|
||||||
public decimal DangerMinTemperature { get; set; } // decimal(18,2)
|
/// </summary>
|
||||||
public decimal MaxTemperature { get; set; } // decimal(18,2)
|
public string Province { get; set; } = string.Empty;
|
||||||
public decimal MinTemperature { get; set; } // decimal(18,2)
|
|
||||||
|
|
||||||
// Gas settings
|
/// <summary>
|
||||||
public int MaxGasPPM { get; set; }
|
/// شهر
|
||||||
public int MinGasPPM { get; set; }
|
/// </summary>
|
||||||
|
public string City { get; set; } = string.Empty;
|
||||||
|
|
||||||
// Light settings
|
/// <summary>
|
||||||
public decimal MaxLux { get; set; } // decimal(18,2)
|
/// عرض جغرافیایی (برای محاسبه طلوع و غروب)
|
||||||
public decimal MinLux { get; set; } // decimal(18,2)
|
/// </summary>
|
||||||
|
public decimal? Latitude { get; set; }
|
||||||
|
|
||||||
// Humidity settings
|
/// <summary>
|
||||||
public decimal MaxHumidityPercent { get; set; } // decimal(18,2)
|
/// طول جغرافیایی (برای محاسبه طلوع و غروب)
|
||||||
public decimal MinHumidityPercent { get; set; } // decimal(18,2)
|
/// </summary>
|
||||||
|
public decimal? Longitude { get; set; }
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; }
|
public DateTime UpdatedAt { 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using GreenHome.Application;
|
using GreenHome.Application;
|
||||||
using GreenHome.Sms.Ippanel;
|
using GreenHome.Sms.Ippanel;
|
||||||
|
using GreenHome.VoiceCall.Avanak;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -12,280 +13,153 @@ public sealed class AlertService : IAlertService
|
|||||||
private readonly GreenHomeDbContext dbContext;
|
private readonly GreenHomeDbContext dbContext;
|
||||||
private readonly IDeviceSettingsService deviceSettingsService;
|
private readonly IDeviceSettingsService deviceSettingsService;
|
||||||
private readonly ISmsService smsService;
|
private readonly ISmsService smsService;
|
||||||
|
private readonly IVoiceCallService voiceCallService;
|
||||||
|
private readonly ISunCalculatorService sunCalculatorService;
|
||||||
private readonly ILogger<AlertService> logger;
|
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(
|
public AlertService(
|
||||||
GreenHomeDbContext dbContext,
|
GreenHomeDbContext dbContext,
|
||||||
IDeviceSettingsService deviceSettingsService,
|
IDeviceSettingsService deviceSettingsService,
|
||||||
ISmsService smsService,
|
ISmsService smsService,
|
||||||
|
IVoiceCallService voiceCallService,
|
||||||
|
ISunCalculatorService sunCalculatorService,
|
||||||
ILogger<AlertService> logger)
|
ILogger<AlertService> logger)
|
||||||
{
|
{
|
||||||
this.dbContext = dbContext;
|
this.dbContext = dbContext;
|
||||||
this.deviceSettingsService = deviceSettingsService;
|
this.deviceSettingsService = deviceSettingsService;
|
||||||
this.smsService = smsService;
|
this.smsService = smsService;
|
||||||
|
this.voiceCallService = voiceCallService;
|
||||||
|
this.sunCalculatorService = sunCalculatorService;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken)
|
public async Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var settings = await deviceSettingsService.GetByDeviceIdAsync(deviceId, cancellationToken);
|
// Get device with settings and user
|
||||||
if (settings == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var device = await dbContext.Devices
|
var device = await dbContext.Devices
|
||||||
.Include(d => d.User)
|
.Include(d => d.User)
|
||||||
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
||||||
|
|
||||||
if (device == null || device.User == null)
|
if (device == null || device.User == null)
|
||||||
{
|
{
|
||||||
|
logger.LogWarning("Device or user not found: DeviceId={DeviceId}", deviceId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var alerts = CollectAlerts(telemetry, settings, device.DeviceName);
|
// Get device settings for location
|
||||||
|
var settings = await deviceSettingsService.GetByDeviceIdAsync(deviceId, cancellationToken);
|
||||||
|
|
||||||
foreach (var alert in alerts)
|
// 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())
|
||||||
{
|
{
|
||||||
await SendAlertIfNeededAsync(deviceId, device.User.Id, device.DeviceName, alert, cancellationToken);
|
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, 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);
|
// Check comparison
|
||||||
CheckHumidityAlert(telemetry, settings, deviceName, alerts);
|
return rule.ComparisonType switch
|
||||||
CheckSoilAlert(telemetry, deviceName, alerts);
|
{
|
||||||
CheckGasAlert(telemetry, settings, deviceName, alerts);
|
Domain.ComparisonType.GreaterThan => sensorValue > rule.Value1,
|
||||||
CheckLuxAlert(telemetry, settings, deviceName, alerts);
|
Domain.ComparisonType.LessThan => sensorValue < rule.Value1,
|
||||||
|
Domain.ComparisonType.Between => rule.Value2 != null && sensorValue >= rule.Value1 && sensorValue <= rule.Value2.Value,
|
||||||
return alerts;
|
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)
|
private async Task SendAlertForConditionAsync(
|
||||||
{
|
Domain.AlertCondition condition,
|
||||||
if (telemetry.TemperatureC > settings.MaxTemperature)
|
Domain.Device device,
|
||||||
{
|
TelemetryDto telemetry,
|
||||||
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,
|
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Check if alert was sent in the last 10 minutes
|
// Determine cooldown based on notification type
|
||||||
var cooldownTime = DateTime.UtcNow.AddMinutes(-AlertCooldownMinutes);
|
var cooldownMinutes = condition.NotificationType == Domain.AlertNotificationType.Call
|
||||||
|
? condition.CallCooldownMinutes
|
||||||
|
: condition.SmsCooldownMinutes;
|
||||||
|
|
||||||
|
// Check if alert was sent recently
|
||||||
|
var cooldownTime = DateTime.UtcNow.AddMinutes(-cooldownMinutes);
|
||||||
var recentAlert = await dbContext.AlertNotifications
|
var recentAlert = await dbContext.AlertNotifications
|
||||||
.Where(a => a.DeviceId == deviceId &&
|
.Where(a => a.DeviceId == device.Id &&
|
||||||
a.UserId == userId &&
|
a.UserId == device.User.Id &&
|
||||||
a.AlertType == alert.Type &&
|
a.AlertConditionId == condition.Id &&
|
||||||
a.SentAt >= cooldownTime)
|
a.SentAt >= cooldownTime)
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
if (recentAlert != null)
|
if (recentAlert != null)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, AlertType={AlertType}", deviceId, alert.Type);
|
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}",
|
||||||
|
device.Id, condition.Id, condition.NotificationType);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user to send SMS
|
// Build alert message
|
||||||
var user = await dbContext.Users
|
var message = BuildAlertMessage(condition, device.DeviceName, telemetry);
|
||||||
.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
|
|
||||||
|
|
||||||
if (user == null || string.IsNullOrWhiteSpace(user.Mobile))
|
// Send notification
|
||||||
{
|
string? messageOutboxIds = null;
|
||||||
logger.LogWarning("User not found or mobile is empty: UserId={UserId}", userId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send SMS and collect response/errors
|
|
||||||
string? messageOutboxIdsJson = null;
|
|
||||||
string? errorMessage = null;
|
string? errorMessage = null;
|
||||||
bool isSent = false;
|
bool isSent = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var smsResponse = await smsService.SendPatternSmsAsync(new PatternSmsRequest
|
if (condition.NotificationType == Domain.AlertNotificationType.SMS)
|
||||||
{
|
{
|
||||||
Recipients = [user.Mobile],
|
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken);
|
||||||
PatternCode = "64di3w9kb0fxvif",
|
|
||||||
Variables = new Dictionary<string, string> {
|
|
||||||
{ "name", deviceName },
|
|
||||||
{ "parameter", alert.ParameterName },
|
|
||||||
{ "value", alert.Value.ToString("F1") },
|
|
||||||
{ "status", alert.Status },
|
|
||||||
}
|
}
|
||||||
}, cancellationToken);
|
else // Call
|
||||||
|
|
||||||
if (smsResponse != null)
|
|
||||||
{
|
{
|
||||||
// Check if SMS was sent successfully
|
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -296,17 +170,19 @@ public sealed class AlertService : IAlertService
|
|||||||
errorMessage += $" | InnerException: {ex.InnerException.Message}";
|
errorMessage += $" | InnerException: {ex.InnerException.Message}";
|
||||||
}
|
}
|
||||||
isSent = false;
|
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 alert: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}",
|
||||||
|
device.Id, condition.Id, condition.NotificationType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save notification to database
|
// Save notification to database
|
||||||
var notification = new Domain.AlertNotification
|
var notification = new Domain.AlertNotification
|
||||||
{
|
{
|
||||||
DeviceId = deviceId,
|
DeviceId = device.Id,
|
||||||
UserId = userId,
|
UserId = device.User.Id,
|
||||||
AlertType = alert.Type,
|
AlertConditionId = condition.Id,
|
||||||
Message = alert.Message,
|
NotificationType = condition.NotificationType,
|
||||||
MessageOutboxIds = messageOutboxIdsJson,
|
Message = message,
|
||||||
|
MessageOutboxIds = messageOutboxIds,
|
||||||
ErrorMessage = errorMessage,
|
ErrorMessage = errorMessage,
|
||||||
SentAt = DateTime.UtcNow,
|
SentAt = DateTime.UtcNow,
|
||||||
IsSent = isSent
|
IsSent = isSent
|
||||||
@@ -315,5 +191,137 @@ public sealed class AlertService : IAlertService
|
|||||||
dbContext.AlertNotifications.Add(notification);
|
dbContext.AlertNotifications.Add(notification);
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
229
src/GreenHome.Infrastructure/DailyReportService.cs
Normal file
229
src/GreenHome.Infrastructure/DailyReportService.cs
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
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} یافت نشد");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 question = $@"این دادههای تلمتری یک روز ({request.PersianDate}) از یک گلخانه هوشمند هستند:
|
||||||
|
|
||||||
|
{dataBuilder}
|
||||||
|
|
||||||
|
لطفاً یک تحلیل خلاصه و کاربردی از این دادهها بده که شامل موارد زیر باشه:
|
||||||
|
1. وضعیت کلی دما، رطوبت، نور و کیفیت هوا
|
||||||
|
2. روندهای مشاهده شده در طول روز
|
||||||
|
3. هر گونه نکته یا هشدار مهم
|
||||||
|
4. پیشنهادات برای بهبود شرایط گلخانه
|
||||||
|
|
||||||
|
خلاصه و مفید باش (حداکثر 300 کلمه).";
|
||||||
|
|
||||||
|
// Send to DeepSeek
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
ChatResponse? aiResponse;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var chatRequest = new ChatRequest
|
||||||
|
{
|
||||||
|
Model = "deepseek-chat",
|
||||||
|
Messages = new List<ChatMessage>
|
||||||
|
{
|
||||||
|
new() { Role = "system", Content = "تو یک متخصص کشاورزی و گلخانه هستی که دادههای تلمتری رو تحلیل میکنی." },
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@
|
|||||||
<ProjectReference Include="..\GreenHome.Application\GreenHome.Application.csproj" />
|
<ProjectReference Include="..\GreenHome.Application\GreenHome.Application.csproj" />
|
||||||
<ProjectReference Include="..\GreenHome.Domain\GreenHome.Domain.csproj" />
|
<ProjectReference Include="..\GreenHome.Domain\GreenHome.Domain.csproj" />
|
||||||
<ProjectReference Include="..\GreenHome.Sms.Ippanel\GreenHome.Sms.Ippanel.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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ public sealed class GreenHomeDbContext : DbContext
|
|||||||
public DbSet<Domain.Device> Devices => Set<Domain.Device>();
|
public DbSet<Domain.Device> Devices => Set<Domain.Device>();
|
||||||
public DbSet<Domain.TelemetryRecord> TelemetryRecords => Set<Domain.TelemetryRecord>();
|
public DbSet<Domain.TelemetryRecord> TelemetryRecords => Set<Domain.TelemetryRecord>();
|
||||||
public DbSet<Domain.DeviceSettings> DeviceSettings => Set<Domain.DeviceSettings>();
|
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.User> Users => Set<Domain.User>();
|
||||||
public DbSet<Domain.VerificationCode> VerificationCodes => Set<Domain.VerificationCode>();
|
public DbSet<Domain.VerificationCode> VerificationCodes => Set<Domain.VerificationCode>();
|
||||||
public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>();
|
public DbSet<Domain.DeviceUser> DeviceUsers => Set<Domain.DeviceUser>();
|
||||||
public DbSet<Domain.AlertNotification> AlertNotifications => Set<Domain.AlertNotification>();
|
public DbSet<Domain.AlertNotification> AlertNotifications => Set<Domain.AlertNotification>();
|
||||||
|
public DbSet<Domain.AIQuery> AIQueries => Set<Domain.AIQuery>();
|
||||||
|
public DbSet<Domain.DailyReport> DailyReports => Set<Domain.DailyReport>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -50,14 +54,10 @@ public sealed class GreenHomeDbContext : DbContext
|
|||||||
{
|
{
|
||||||
b.ToTable("DeviceSettings");
|
b.ToTable("DeviceSettings");
|
||||||
b.HasKey(x => x.Id);
|
b.HasKey(x => x.Id);
|
||||||
b.Property(x => x.DangerMaxTemperature).HasColumnType("decimal(18,2)");
|
b.Property(x => x.Province).HasMaxLength(100);
|
||||||
b.Property(x => x.DangerMinTemperature).HasColumnType("decimal(18,2)");
|
b.Property(x => x.City).HasMaxLength(100);
|
||||||
b.Property(x => x.MaxTemperature).HasColumnType("decimal(18,2)");
|
b.Property(x => x.Latitude).HasColumnType("decimal(9,6)");
|
||||||
b.Property(x => x.MinTemperature).HasColumnType("decimal(18,2)");
|
b.Property(x => x.Longitude).HasColumnType("decimal(9,6)");
|
||||||
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.HasOne(x => x.Device)
|
b.HasOne(x => x.Device)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(x => x.DeviceId)
|
.HasForeignKey(x => x.DeviceId)
|
||||||
@@ -65,6 +65,38 @@ public sealed class GreenHomeDbContext : DbContext
|
|||||||
b.HasIndex(x => x.DeviceId).IsUnique();
|
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 =>
|
modelBuilder.Entity<Domain.User>(b =>
|
||||||
{
|
{
|
||||||
b.ToTable("Users");
|
b.ToTable("Users");
|
||||||
@@ -103,7 +135,7 @@ public sealed class GreenHomeDbContext : DbContext
|
|||||||
{
|
{
|
||||||
b.ToTable("AlertNotifications");
|
b.ToTable("AlertNotifications");
|
||||||
b.HasKey(x => x.Id);
|
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.Message).IsRequired().HasMaxLength(500);
|
||||||
b.Property(x => x.MessageOutboxIds).HasMaxLength(500);
|
b.Property(x => x.MessageOutboxIds).HasMaxLength(500);
|
||||||
b.Property(x => x.ErrorMessage).HasMaxLength(1000);
|
b.Property(x => x.ErrorMessage).HasMaxLength(1000);
|
||||||
@@ -115,7 +147,47 @@ public sealed class GreenHomeDbContext : DbContext
|
|||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(x => x.UserId)
|
.HasForeignKey(x => x.UserId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -22,6 +22,100 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
|
|
||||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
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 =>
|
modelBuilder.Entity("GreenHome.Domain.AlertNotification", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -30,10 +124,8 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
b.Property<string>("AlertType")
|
b.Property<int>("AlertConditionId")
|
||||||
.IsRequired()
|
.HasColumnType("int");
|
||||||
.HasMaxLength(50)
|
|
||||||
.HasColumnType("nvarchar(50)");
|
|
||||||
|
|
||||||
b.Property<int>("DeviceId")
|
b.Property<int>("DeviceId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
@@ -54,6 +146,9 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
.HasMaxLength(500)
|
.HasMaxLength(500)
|
||||||
.HasColumnType("nvarchar(500)");
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<int>("NotificationType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<DateTime>("SentAt")
|
b.Property<DateTime>("SentAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
@@ -62,13 +157,114 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AlertConditionId");
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.HasIndex("DeviceId", "UserId", "AlertType", "SentAt");
|
b.HasIndex("DeviceId", "UserId", "AlertConditionId", "SentAt");
|
||||||
|
|
||||||
b.ToTable("AlertNotifications", (string)null);
|
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 =>
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -110,41 +306,27 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("City")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.Property<decimal>("DangerMaxTemperature")
|
|
||||||
.HasColumnType("decimal(18,2)");
|
|
||||||
|
|
||||||
b.Property<decimal>("DangerMinTemperature")
|
|
||||||
.HasColumnType("decimal(18,2)");
|
|
||||||
|
|
||||||
b.Property<int>("DeviceId")
|
b.Property<int>("DeviceId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int>("MaxGasPPM")
|
b.Property<decimal?>("Latitude")
|
||||||
.HasColumnType("int");
|
.HasColumnType("decimal(9,6)");
|
||||||
|
|
||||||
b.Property<decimal>("MaxHumidityPercent")
|
b.Property<decimal?>("Longitude")
|
||||||
.HasColumnType("decimal(18,2)");
|
.HasColumnType("decimal(9,6)");
|
||||||
|
|
||||||
b.Property<decimal>("MaxLux")
|
b.Property<string>("Province")
|
||||||
.HasColumnType("decimal(18,2)");
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
b.Property<decimal>("MaxTemperature")
|
.HasColumnType("nvarchar(100)");
|
||||||
.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")
|
b.Property<DateTime>("UpdatedAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
@@ -303,8 +485,42 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.ToTable("VerificationCodes", (string)null);
|
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 =>
|
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")
|
b.HasOne("GreenHome.Domain.Device", "Device")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("DeviceId")
|
.HasForeignKey("DeviceId")
|
||||||
@@ -317,11 +533,35 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Restrict)
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AlertCondition");
|
||||||
|
|
||||||
b.Navigation("Device");
|
b.Navigation("Device");
|
||||||
|
|
||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("GreenHome.Domain.User", "User")
|
b.HasOne("GreenHome.Domain.User", "User")
|
||||||
@@ -363,6 +603,11 @@ namespace GreenHome.Infrastructure.Migrations
|
|||||||
b.Navigation("User");
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GreenHome.Domain.AlertCondition", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Rules");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
modelBuilder.Entity("GreenHome.Domain.Device", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("DeviceUsers");
|
b.Navigation("DeviceUsers");
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.Sms.Ippanel", "Gr
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.VoiceCall.Avanak", "GreenHome.VoiceCall.Avanak\GreenHome.VoiceCall.Avanak.csproj", "{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.VoiceCall.Avanak", "GreenHome.VoiceCall.Avanak\GreenHome.VoiceCall.Avanak.csproj", "{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenHome.AI.DeepSeek", "GreenHome.AI.DeepSeek\GreenHome.AI.DeepSeek.csproj", "{63D676E7-B882-4E70-8090-E56CFD516B0A}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{A2C9D8E7-F459-4B1A-9C3D-1E4F5A6B7C8D}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Reference in New Issue
Block a user