Implements detailed tracking of soil composition, nutrients, climate, lighting, and surrounding environment to help understand plant success and optimize growing conditions. Environmental Data Types (lib/environment/types.ts): - Soil composition (type, pH, texture, drainage, organic matter) - Soil amendments (compost, perlite, amendments tracking) - Nutrient profiles (NPK, secondary nutrients, micronutrients, EC, TDS) - Fertilizer applications (type, schedule, NPK values) - Lighting conditions (natural/artificial, hours, spectrum, PPFD/DLI) - Climate tracking (temperature, humidity, airflow, CO2, USDA zones) - Growing location (indoor/outdoor/greenhouse with details) - Container information (type, material, size, drainage) - Watering schedule (method, frequency, water quality/pH) - Surrounding environment (companion plants, pests, diseases, ecosystem) - Growth metrics (measurements, health scores, vigor tracking) Environmental Analysis (lib/environment/analysis.ts): - Compare environments with similarity scoring (0-100) - Generate personalized growing recommendations - Calculate environmental health scores - Find plants with similar conditions - Analyze growth correlations across network - Identify optimal growing conditions API Endpoints: - /api/environment/recommendations - Get plant-specific advice - /api/environment/similar - Find plants with similar conditions - /api/environment/compare - Compare two plants environments - /api/environment/analysis - Network-wide growth correlation Features: - Comprehensive soil tracking (pH, texture, drainage, amendments) - Full NPK and micronutrient monitoring - Sunlight exposure and artificial light tracking - Temperature and humidity ranges - Water quality monitoring (pH, TDS, chlorine) - Pest and disease tracking - Companion planting recommendations - Environmental health scoring with priority-based recommendations - Growth success analysis across similar environments Integration: - Updated PlantData type to include environment and growthMetrics - Compatible with existing blockchain structure - Optional environmental data (backward compatible) Use Cases: - Track what works for your plants - Learn from successful growers with similar conditions - Get personalized recommendations based on your setup - Compare your environment with thriving plants - Optimize conditions for better yield/health - Share growing knowledge with the community - Research optimal conditions by species This enables growers to: - Understand why plants thrive or struggle - Replicate successful growing conditions - Make data-driven decisions - Learn from the collective experience - Improve propagation success rates
432 lines
14 KiB
TypeScript
432 lines
14 KiB
TypeScript
/**
|
|
* Environmental Analysis Service
|
|
* Compares growing conditions, generates recommendations, and analyzes correlations
|
|
*/
|
|
|
|
import {
|
|
GrowingEnvironment,
|
|
EnvironmentalComparison,
|
|
EnvironmentalRecommendation,
|
|
SoilComposition,
|
|
ClimateConditions,
|
|
LightingConditions,
|
|
NutrientProfile,
|
|
} from './types';
|
|
import { PlantData } from '../blockchain/types';
|
|
|
|
/**
|
|
* Compare two growing environments and return similarity score
|
|
*/
|
|
export function compareEnvironments(
|
|
env1: GrowingEnvironment,
|
|
env2: GrowingEnvironment
|
|
): EnvironmentalComparison {
|
|
const similarities: string[] = [];
|
|
const differences: string[] = [];
|
|
let score = 0;
|
|
let maxScore = 0;
|
|
|
|
// Compare location type (10 points)
|
|
maxScore += 10;
|
|
if (env1.location.type === env2.location.type) {
|
|
score += 10;
|
|
similarities.push(`Both ${env1.location.type} growing`);
|
|
} else {
|
|
differences.push(`Location: ${env1.location.type} vs ${env2.location.type}`);
|
|
}
|
|
|
|
// Compare soil type (15 points)
|
|
maxScore += 15;
|
|
if (env1.soil.type === env2.soil.type) {
|
|
score += 15;
|
|
similarities.push(`Same soil type: ${env1.soil.type}`);
|
|
} else {
|
|
differences.push(`Soil: ${env1.soil.type} vs ${env2.soil.type}`);
|
|
}
|
|
|
|
// Compare soil pH (10 points, within 0.5 range)
|
|
maxScore += 10;
|
|
const pHDiff = Math.abs(env1.soil.pH - env2.soil.pH);
|
|
if (pHDiff <= 0.5) {
|
|
score += 10;
|
|
similarities.push(`Similar soil pH: ${env1.soil.pH.toFixed(1)} ≈ ${env2.soil.pH.toFixed(1)}`);
|
|
} else if (pHDiff <= 1.0) {
|
|
score += 5;
|
|
differences.push(`Soil pH: ${env1.soil.pH.toFixed(1)} vs ${env2.soil.pH.toFixed(1)}`);
|
|
} else {
|
|
differences.push(`Soil pH differs significantly: ${env1.soil.pH.toFixed(1)} vs ${env2.soil.pH.toFixed(1)}`);
|
|
}
|
|
|
|
// Compare temperature (15 points)
|
|
maxScore += 15;
|
|
const tempDiff = Math.abs(env1.climate.temperatureDay - env2.climate.temperatureDay);
|
|
if (tempDiff <= 3) {
|
|
score += 15;
|
|
similarities.push(`Similar temperatures: ${env1.climate.temperatureDay}°C ≈ ${env2.climate.temperatureDay}°C`);
|
|
} else if (tempDiff <= 7) {
|
|
score += 8;
|
|
differences.push(`Temperature: ${env1.climate.temperatureDay}°C vs ${env2.climate.temperatureDay}°C`);
|
|
} else {
|
|
differences.push(`Temperature differs significantly: ${env1.climate.temperatureDay}°C vs ${env2.climate.temperatureDay}°C`);
|
|
}
|
|
|
|
// Compare humidity (10 points)
|
|
maxScore += 10;
|
|
const humidityDiff = Math.abs(env1.climate.humidityAverage - env2.climate.humidityAverage);
|
|
if (humidityDiff <= 10) {
|
|
score += 10;
|
|
similarities.push(`Similar humidity: ${env1.climate.humidityAverage}% ≈ ${env2.climate.humidityAverage}%`);
|
|
} else if (humidityDiff <= 20) {
|
|
score += 5;
|
|
} else {
|
|
differences.push(`Humidity: ${env1.climate.humidityAverage}% vs ${env2.climate.humidityAverage}%`);
|
|
}
|
|
|
|
// Compare lighting type (15 points)
|
|
maxScore += 15;
|
|
if (env1.lighting.type === env2.lighting.type) {
|
|
score += 15;
|
|
similarities.push(`Same lighting: ${env1.lighting.type}`);
|
|
} else {
|
|
differences.push(`Lighting: ${env1.lighting.type} vs ${env2.lighting.type}`);
|
|
}
|
|
|
|
// Compare natural light exposure if both use natural light (10 points)
|
|
if (env1.lighting.naturalLight && env2.lighting.naturalLight) {
|
|
maxScore += 10;
|
|
if (env1.lighting.naturalLight.exposure === env2.lighting.naturalLight.exposure) {
|
|
score += 10;
|
|
similarities.push(`Same sun exposure: ${env1.lighting.naturalLight.exposure}`);
|
|
} else {
|
|
differences.push(`Sun exposure: ${env1.lighting.naturalLight.exposure} vs ${env2.lighting.naturalLight.exposure}`);
|
|
}
|
|
}
|
|
|
|
// Compare water source (5 points)
|
|
maxScore += 5;
|
|
if (env1.watering.waterSource === env2.watering.waterSource) {
|
|
score += 5;
|
|
similarities.push(`Same water source: ${env1.watering.waterSource}`);
|
|
} else {
|
|
differences.push(`Water: ${env1.watering.waterSource} vs ${env2.watering.waterSource}`);
|
|
}
|
|
|
|
// Compare container type (10 points)
|
|
if (env1.container && env2.container) {
|
|
maxScore += 10;
|
|
if (env1.container.type === env2.container.type) {
|
|
score += 10;
|
|
similarities.push(`Same container: ${env1.container.type}`);
|
|
} else {
|
|
differences.push(`Container: ${env1.container.type} vs ${env2.container.type}`);
|
|
}
|
|
}
|
|
|
|
// Calculate final score as percentage
|
|
const finalScore = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;
|
|
|
|
return {
|
|
plant1: '', // Will be filled by caller
|
|
plant2: '', // Will be filled by caller
|
|
similarities,
|
|
differences,
|
|
score: finalScore,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate environmental recommendations based on plant data
|
|
*/
|
|
export function generateRecommendations(
|
|
plant: PlantData
|
|
): EnvironmentalRecommendation[] {
|
|
const recommendations: EnvironmentalRecommendation[] = [];
|
|
|
|
if (!plant.environment) {
|
|
recommendations.push({
|
|
category: 'general',
|
|
priority: 'high',
|
|
issue: 'No environmental data',
|
|
recommendation: 'Add environmental information to track growing conditions',
|
|
impact: 'Environmental tracking helps optimize growing conditions and share knowledge',
|
|
});
|
|
return recommendations;
|
|
}
|
|
|
|
const env = plant.environment;
|
|
|
|
// Soil pH recommendations
|
|
if (env.soil.pH < 5.5) {
|
|
recommendations.push({
|
|
category: 'soil',
|
|
priority: 'high',
|
|
issue: `Low soil pH (${env.soil.pH})`,
|
|
recommendation: 'Add lime to raise pH. Most plants prefer 6.0-7.0',
|
|
impact: 'Acidic soil can lock out nutrients and harm roots',
|
|
});
|
|
} else if (env.soil.pH > 7.5) {
|
|
recommendations.push({
|
|
category: 'soil',
|
|
priority: 'high',
|
|
issue: `High soil pH (${env.soil.pH})`,
|
|
recommendation: 'Add sulfur to lower pH. Most plants prefer 6.0-7.0',
|
|
impact: 'Alkaline soil can prevent nutrient uptake',
|
|
});
|
|
}
|
|
|
|
// Organic matter recommendations
|
|
if (env.soil.organicMatter < 3) {
|
|
recommendations.push({
|
|
category: 'soil',
|
|
priority: 'medium',
|
|
issue: `Low organic matter (${env.soil.organicMatter}%)`,
|
|
recommendation: 'Add compost or aged manure to improve soil structure',
|
|
impact: 'Increases water retention and nutrient availability',
|
|
});
|
|
}
|
|
|
|
// Drainage recommendations
|
|
if (env.soil.drainage === 'poor') {
|
|
recommendations.push({
|
|
category: 'soil',
|
|
priority: 'high',
|
|
issue: 'Poor soil drainage',
|
|
recommendation: 'Add perlite, sand, or create raised beds',
|
|
impact: 'Prevents root rot and improves oxygen availability',
|
|
});
|
|
}
|
|
|
|
// Temperature recommendations
|
|
if (env.climate.temperatureDay > 35) {
|
|
recommendations.push({
|
|
category: 'climate',
|
|
priority: 'high',
|
|
issue: `High daytime temperature (${env.climate.temperatureDay}°C)`,
|
|
recommendation: 'Provide shade during hottest hours, increase watering',
|
|
impact: 'Extreme heat can cause stress and reduce growth',
|
|
});
|
|
} else if (env.climate.temperatureDay < 10) {
|
|
recommendations.push({
|
|
category: 'climate',
|
|
priority: 'high',
|
|
issue: `Low daytime temperature (${env.climate.temperatureDay}°C)`,
|
|
recommendation: 'Provide frost protection or move indoors',
|
|
impact: 'Cold temperatures can slow growth or damage plants',
|
|
});
|
|
}
|
|
|
|
// Humidity recommendations
|
|
if (env.climate.humidityAverage < 30) {
|
|
recommendations.push({
|
|
category: 'climate',
|
|
priority: 'medium',
|
|
issue: `Low humidity (${env.climate.humidityAverage}%)`,
|
|
recommendation: 'Mist plants, use humidity tray, or group plants together',
|
|
impact: 'Low humidity can cause leaf tip browning and stress',
|
|
});
|
|
} else if (env.climate.humidityAverage > 80) {
|
|
recommendations.push({
|
|
category: 'climate',
|
|
priority: 'medium',
|
|
issue: `High humidity (${env.climate.humidityAverage}%)`,
|
|
recommendation: 'Improve air circulation to prevent fungal issues',
|
|
impact: 'High humidity can promote mold and disease',
|
|
});
|
|
}
|
|
|
|
// Lighting recommendations
|
|
if (env.lighting.type === 'natural' && env.lighting.naturalLight) {
|
|
const hoursPerDay = env.lighting.naturalLight.hoursPerDay;
|
|
if (hoursPerDay < 4 && plant.status === 'growing') {
|
|
recommendations.push({
|
|
category: 'light',
|
|
priority: 'high',
|
|
issue: `Insufficient light (${hoursPerDay} hours/day)`,
|
|
recommendation: 'Move to brighter location or supplement with grow lights',
|
|
impact: 'Inadequate light causes leggy growth and poor health',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Nutrient recommendations
|
|
if (env.nutrients) {
|
|
const npk = [env.nutrients.nitrogen, env.nutrients.phosphorus, env.nutrients.potassium];
|
|
if (npk.some(n => n < 1)) {
|
|
recommendations.push({
|
|
category: 'nutrients',
|
|
priority: 'medium',
|
|
issue: `Low NPK levels (${npk.join('-')})`,
|
|
recommendation: 'Apply balanced fertilizer according to plant needs',
|
|
impact: 'Nutrient deficiency reduces growth and yield',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Water quality recommendations
|
|
if (env.watering.waterQuality) {
|
|
if (env.watering.waterQuality.pH && env.watering.waterQuality.pH > 7.5) {
|
|
recommendations.push({
|
|
category: 'water',
|
|
priority: 'low',
|
|
issue: `Alkaline water pH (${env.watering.waterQuality.pH})`,
|
|
recommendation: 'Consider pH adjustment or rainwater collection',
|
|
impact: 'High water pH can gradually raise soil pH',
|
|
});
|
|
}
|
|
if (env.watering.waterQuality.chlorine === 'yes') {
|
|
recommendations.push({
|
|
category: 'water',
|
|
priority: 'low',
|
|
issue: 'Chlorinated water',
|
|
recommendation: 'Let water sit 24h before use or use filter',
|
|
impact: 'Chlorine can harm beneficial soil microbes',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Container recommendations
|
|
if (env.container && env.container.drainage === 'no') {
|
|
recommendations.push({
|
|
category: 'general',
|
|
priority: 'critical',
|
|
issue: 'Container has no drainage',
|
|
recommendation: 'Add drainage holes immediately to prevent root rot',
|
|
impact: 'Critical: Standing water will kill most plants',
|
|
});
|
|
}
|
|
|
|
return recommendations.sort((a, b) => {
|
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Find plants with similar growing conditions
|
|
*/
|
|
export function findSimilarEnvironments(
|
|
targetPlant: PlantData,
|
|
allPlants: PlantData[],
|
|
minScore: number = 70
|
|
): Array<{ plant: PlantData; comparison: EnvironmentalComparison }> {
|
|
if (!targetPlant.environment) {
|
|
return [];
|
|
}
|
|
|
|
const similar: Array<{ plant: PlantData; comparison: EnvironmentalComparison }> = [];
|
|
|
|
for (const plant of allPlants) {
|
|
// Skip self and plants without environment data
|
|
if (plant.id === targetPlant.id || !plant.environment) {
|
|
continue;
|
|
}
|
|
|
|
const comparison = compareEnvironments(targetPlant.environment, plant.environment);
|
|
comparison.plant1 = targetPlant.id;
|
|
comparison.plant2 = plant.id;
|
|
|
|
if (comparison.score >= minScore) {
|
|
similar.push({ plant, comparison });
|
|
}
|
|
}
|
|
|
|
// Sort by similarity score (highest first)
|
|
return similar.sort((a, b) => b.comparison.score - a.comparison.score);
|
|
}
|
|
|
|
/**
|
|
* Analyze growth success based on environmental factors
|
|
*/
|
|
export function analyzeGrowthCorrelation(plants: PlantData[]): {
|
|
bestConditions: Partial<GrowingEnvironment>;
|
|
insights: string[];
|
|
} {
|
|
const successfulPlants = plants.filter(
|
|
p => p.environment && (p.status === 'mature' || p.status === 'flowering' || p.status === 'fruiting')
|
|
);
|
|
|
|
if (successfulPlants.length === 0) {
|
|
return {
|
|
bestConditions: {},
|
|
insights: ['Not enough data to determine optimal conditions'],
|
|
};
|
|
}
|
|
|
|
const insights: string[] = [];
|
|
|
|
// Analyze soil pH
|
|
const pHValues = successfulPlants
|
|
.filter(p => p.environment?.soil.pH)
|
|
.map(p => p.environment!.soil.pH);
|
|
|
|
if (pHValues.length > 0) {
|
|
const avgPH = pHValues.reduce((a, b) => a + b, 0) / pHValues.length;
|
|
insights.push(`Successful plants average soil pH: ${avgPH.toFixed(1)}`);
|
|
}
|
|
|
|
// Analyze temperature
|
|
const temps = successfulPlants
|
|
.filter(p => p.environment?.climate.temperatureDay)
|
|
.map(p => p.environment!.climate.temperatureDay);
|
|
|
|
if (temps.length > 0) {
|
|
const avgTemp = temps.reduce((a, b) => a + b, 0) / temps.length;
|
|
const minTemp = Math.min(...temps);
|
|
const maxTemp = Math.max(...temps);
|
|
insights.push(`Successful temperature range: ${minTemp}-${maxTemp}°C (avg: ${avgTemp.toFixed(1)}°C)`);
|
|
}
|
|
|
|
// Analyze lighting
|
|
const lightingTypes = successfulPlants
|
|
.filter(p => p.environment?.lighting.type)
|
|
.map(p => p.environment!.lighting.type);
|
|
|
|
const lightCount = lightingTypes.reduce((acc, type) => {
|
|
acc[type] = (acc[type] || 0) + 1;
|
|
return acc;
|
|
}, {} as Record<string, number>);
|
|
|
|
const mostCommonLight = Object.entries(lightCount)
|
|
.sort((a, b) => b[1] - a[1])[0];
|
|
|
|
if (mostCommonLight) {
|
|
insights.push(`Most successful lighting: ${mostCommonLight[0]} (${mostCommonLight[1]} plants)`);
|
|
}
|
|
|
|
return {
|
|
bestConditions: {
|
|
// Would be filled with actual aggregate data
|
|
},
|
|
insights,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate environmental health score (0-100)
|
|
*/
|
|
export function calculateEnvironmentalHealth(env: GrowingEnvironment): number {
|
|
let score = 100;
|
|
let deductions = 0;
|
|
|
|
// Soil health (up to -20 points)
|
|
if (env.soil.pH < 5.5 || env.soil.pH > 7.5) deductions += 10;
|
|
if (env.soil.drainage === 'poor') deductions += 10;
|
|
if (env.soil.organicMatter < 3) deductions += 5;
|
|
|
|
// Climate (up to -20 points)
|
|
if (env.climate.temperatureDay > 35 || env.climate.temperatureDay < 10) deductions += 10;
|
|
if (env.climate.humidityAverage < 30 || env.climate.humidityAverage > 80) deductions += 5;
|
|
if (env.climate.airflow === 'none') deductions += 5;
|
|
|
|
// Lighting (up to -15 points)
|
|
if (env.lighting.naturalLight && env.lighting.naturalLight.hoursPerDay < 4) deductions += 15;
|
|
|
|
// Container (up to -15 points)
|
|
if (env.container && env.container.drainage === 'no') deductions += 15;
|
|
|
|
// Water (up to -10 points)
|
|
if (env.watering.waterQuality?.chlorine === 'yes') deductions += 5;
|
|
if (env.watering.waterQuality?.pH && env.watering.waterQuality.pH > 7.5) deductions += 5;
|
|
|
|
return Math.max(0, score - deductions);
|
|
}
|