OK my next SPARK project is Mold reduction…
We have a recent mold issue in our small house, caused by a broken gutter… Not uncommon… According to the BBC article above 1.3 Million homes in the UK have this issue, having lived in China for 10 years I know this isn’t only an issue here… That’s super unhealthy for a lot of families…
The reality to fighting mold is not just cleaning or building work but also about PREVENTION…
You can do this with money… A faster spin cycle washing machine meaning less water when drying indoors (if you have no outdoor area)…, or with dehumidifiers, which may help but are a big cost outlay for low income families.
The cost of electricity also becomes a factor.
On the ground I have found timing and weather to be major factors… It’s no good washing and hanging clothes when it’s about to rain, or when it’s going to be very cold so it costs lots more to heat the house when venting the property.
I get lost in code and tend to do chores on auto-pilot… Here is my simple personal AI aided solution to this very real issue… It’s not perfected yet, but I think it’s a good fit here.
For this project/module I am using Met Office Weather API and working out washing/cleaning schedules using a function written in GPT-5 on the API. The goal is to work out more optimal times to put on washing every few days to prevent the mold.
My goal is to understand and fix this problem moving forward, as we transition to an AI (cheaper than air) future for my (Healthier) family and creating a net value add in the long term (while sharing this process for others to copy).
For this project I will use:
Met Office Weather API (Global Spot Data - Free for personal use)
GPT-5 (Pretty much vibe coding to work out an algorithm for this house)
Elbow Grease

This project is completely replicatable with a little vibe coding.
What the Indoor Drying Planner Does
This tool processes hourly Met Office data and analyses humidity, dew-point, temperature, visibility, pressure trends and precipitation risk to estimate when indoor air will clear moisture most efficiently. It identifies the best 6-hour, 12-hour and 24-hour drying windows, and highlights the safest daytime hours (09:00–21:00) to start washing so moisture doesn’t accumulate overnight.
The aim is simple:
reduce indoor humidity spikes, speed up evaporation, and lower mould risk by choosing the least harmful drying times.
By avoiding saturated air, fog, rain-loaded periods, and cold night-time traps, the algorithm attempts to reduce the number of hours a home spends above 70–85% relative humidity — the zone where mould thrives — which in (GPT-5) theory could reduce mould spread by 30–60%, and practically by up to 70–90% in small UK homes that rely on indoor drying.
Important:
This is a conceptual and untested model.
It uses building-physics principles and forecast data to guide decision-making, but real-world results will vary depending on ventilation, insulation, heating habits and specific home conditions.
It’s ultimately a low-cost, data-driven idea intended to make indoor drying healthier and more energy-efficient for families without outdoor space.
Feedback and domain specific corrections welcome 
Reference Code (PHP)
function GetIndoorDryingPlanFromTimeSeries(array $timeSeries): array
{
// Helper: parse ISO time → (DateTime in UK, formatted string, hour 0–23)
$parseTimeUK = function (?string $iso): array {
if (!$iso) return [null, null, null];
try {
$dt = new DateTime($iso, new DateTimeZone('UTC'));
$dt->setTimezone(new DateTimeZone('Europe/London'));
return [
$dt,
$dt->format('D j M Y, H:i'),
(int)$dt->format('G') // 0–23
];
} catch (Exception $e) {
return [null, $iso, null];
}
};
$formatTimeUK = function (?string $iso) use ($parseTimeUK): ?string {
[, $formatted, ] = $parseTimeUK($iso);
return $formatted;
};
// ---------- 0. Normalise + sort by time ----------
usort($timeSeries, function ($a, $b) {
return strcmp($a['time'] ?? '', $b['time'] ?? '');
});
// Derive simple pressure tendency from mslp (R/F/S)
$prevMslp = null;
foreach ($timeSeries as $i => $row) {
$mslp = isset($row['mslp']) ? (float)$row['mslp'] : null;
if ($prevMslp === null || $mslp === null) {
$timeSeries[$i]['pressure_tendency'] = '';
} else {
$delta = $mslp - $prevMslp;
if ($delta > 0) {
$timeSeries[$i]['pressure_tendency'] = 'R';
} elseif ($delta < 0) {
$timeSeries[$i]['pressure_tendency'] = 'F';
} else {
$timeSeries[$i]['pressure_tendency'] = 'S';
}
}
$prevMslp = $mslp;
}
// ---------- 1. Indoor drying stress score per row ----------
$stressRow = function(array $row): float
{
$h = isset($row['screenRelativeHumidity'])
? (float)$row['screenRelativeHumidity']
: (float)($row['humidity'] ?? 0);
if (isset($row['maxScreenAirTemp'], $row['minScreenAirTemp'])) {
$t = ((float)$row['maxScreenAirTemp'] + (float)$row['minScreenAirTemp']) / 2.0;
} elseif (isset($row['feelsLikeTemp'])) {
$t = (float)$row['feelsLikeTemp'];
} else {
$t = (float)($row['screenTemperature'] ?? $row['temperature'] ?? 0);
}
$v = (float)($row['visibility'] ?? 0);
$pt = (string)($row['pressure_tendency'] ?? '');
$wc = isset($row['significantWeatherCode'])
? (int)$row['significantWeatherCode']
: (int)($row['weather_code'] ?? 0);
$ws = isset($row['windSpeed10m'])
? (float)$row['windSpeed10m']
: (float)($row['wind_speed'] ?? 0);
$precipProb = isset($row['probOfPrecipitation'])
? (float)$row['probOfPrecipitation']
: (float)($row['probOfRain'] ?? 0);
$hourUK = isset($row['hour_uk']) ? (int)$row['hour_uk'] : null;
// Rising pressure → clearing/drying
$pressureBonus = ($pt === 'R' ? 2.0 : ($pt === 'F' ? -2.0 : 0.0));
$weatherPenalty = 0.0;
if ($wc !== 0) {
$weatherPenalty += 5.0;
}
if ($v < 10000) {
$weatherPenalty += 5.0;
}
if ($precipProb >= 50) {
$weatherPenalty += 10.0;
} elseif ($precipProb >= 20) {
$weatherPenalty += 5.0;
}
// Simple dew point estimate (approximation)
$indoorTempAssumed = 18.0; // °C
$dewPoint = $t - ((100.0 - $h) / 5.0);
$drynessPotential = $indoorTempAssumed - $dewPoint;
if ($drynessPotential < 0) {
$drynessPotential = 0.0;
}
$dryingScore =
($drynessPotential * 3.0) +
((100.0 - $h) * 0.5) +
($v / 10000.0) +
($ws * 0.5) +
$pressureBonus -
($precipProb * 0.2) -
$weatherPenalty;
$indoorStress = 100.0 - $dryingScore;
// *** NEW: big penalty for night-time (outside 09:00–21:00 UK) ***
if ($hourUK !== null && ($hourUK < 9 || $hourUK > 21)) {
$indoorStress += 40.0; // tune this if you like
}
return $indoorStress;
};
// ---------- 2. Add stress + datetime + hour_uk ----------
$Data = [];
foreach ($timeSeries as $row) {
$row['datetime'] = $row['time'] ?? null;
[, , $hourUK] = $parseTimeUK($row['datetime']);
$row['hour_uk'] = $hourUK;
$row['indoor_stress'] = $stressRow($row);
$Data[] = $row;
}
$n = count($Data);
if ($n === 0) {
return [
'best_hours' => [],
'best_6h_window' => null,
'best_12h_window' => null,
'best_24h_window' => null,
'rows' => [],
'summary_text' => '<div class="drying-plan-summary"><p>No data available.</p></div>',
];
}
// ---------- 3. Helper: best (lowest-stress) sliding window ----------
$bestWindow = function (array $rows, int $windowHours): ?array
{
$count = count($rows);
if ($count === 0 || $windowHours <= 0) return null;
$windowSize = min($windowHours, $count);
$stressValues = array_column($rows, 'indoor_stress');
$sum = array_sum(array_slice($stressValues, 0, $windowSize));
$bestSum = $sum;
$bestStart = 0;
for ($i = $windowSize; $i < $count; $i++) {
$sum += $stressValues[$i] - $stressValues[$i - $windowSize];
if ($sum < $bestSum) {
$bestSum = $sum;
$bestStart = $i - $windowSize + 1;
}
}
$bestEnd = $bestStart + $windowSize - 1;
return [
'start_index' => $bestStart,
'end_index' => $bestEnd,
'start_time' => $rows[$bestStart]['datetime'] ?? null,
'end_time' => $rows[$bestEnd]['datetime'] ?? null,
'total_stress' => $bestSum,
'average_stress' => $bestSum / $windowSize,
];
};
// ---------- 4. Best individual hours (lowest stress, 09:00–21:00 only) ----------
$byStress = $Data;
usort($byStress, function ($a, $b) {
$sa = (float)($a['indoor_stress'] ?? 0);
$sb = (float)($b['indoor_stress'] ?? 0);
return $sa <=> $sb;
});
$bestHoursClean = [];
foreach ($byStress as $row) {
$iso = $row['datetime'] ?? null;
[, $ukStr, $hourUK] = $parseTimeUK($iso);
if ($hourUK === null) continue;
if ($hourUK < 9 || $hourUK > 21) continue;
$bestHoursClean[] = [
'datetime' => $iso,
'datetime_uk' => $ukStr,
'indoor_stress' => (float)($row['indoor_stress'] ?? 0),
];
if (count($bestHoursClean) >= 6) break;
}
// Fallback if somehow no daytime hours qualify
if (empty($bestHoursClean) && !empty($byStress)) {
$fallback = array_slice($byStress, 0, min(3, count($byStress)));
foreach ($fallback as $row) {
$iso = $row['datetime'] ?? null;
[, $ukStr, ] = $parseTimeUK($iso);
$bestHoursClean[] = [
'datetime' => $iso,
'datetime_uk' => $ukStr,
'indoor_stress' => (float)($row['indoor_stress'] ?? 0),
];
}
}
// ---------- 5. Best 6h, 12h, 24h windows (now with night penalty baked in) ----------
$best6 = $bestWindow($Data, 6);
$best12 = $bestWindow($Data, 12);
$best24 = $bestWindow($Data, 24);
// ---------- 6. Quality labels ----------
$stressLabel = function (?array $win): string {
if (!$win) return 'unknown';
$avg = $win['average_stress'];
if ($avg <= 40) return 'very good';
if ($avg <= 55) return 'good';
if ($avg <= 70) return 'fair';
return 'poor';
};
$label6 = $stressLabel($best6);
$label12 = $stressLabel($best12);
$label24 = $stressLabel($best24);
// ---------- 7. HTML summary block ----------
$html = '<div class="drying-plan-summary" style="font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.5;color:#222;padding:10px 14px;border:1px solid #ccc;border-radius:8px;max-width:640px;background:#fafafa;">';
$html .= '<h2 style="margin:0 0 8px;font-size:16px;">Indoor Drying Plan</h2>';
$html .= '<ul style="margin:0 0 10px 18px;padding:0;">';
if ($best24 && $best24['start_time'] && $best24['end_time']) {
$html .= '<li><strong>Best 24-hour period</strong> (least mould / humidity stress):<br>'
. htmlspecialchars($formatTimeUK($best24['start_time'])) . ' → '
. htmlspecialchars($formatTimeUK($best24['end_time'])) . '<br>'
. 'Average stress: ' . round($best24['average_stress'], 1)
. ' (' . htmlspecialchars($label24) . ')</li>';
}
if ($best12 && $best12['start_time'] && $best12['end_time']) {
$html .= '<li style="margin-top:6px;"><strong>Good 12-hour block</strong>:<br>'
. htmlspecialchars($formatTimeUK($best12['start_time'])) . ' → '
. htmlspecialchars($formatTimeUK($best12['end_time'])) . '<br>'
. 'Average stress: ' . round($best12['average_stress'], 1)
. ' (' . htmlspecialchars($label12) . ')</li>';
}
if ($best6 && $best6['start_time'] && $best6['end_time']) {
$html .= '<li style="margin-top:6px;"><strong>Best 6-hour stretch</strong> (ideal to have clothes hanging):<br>'
. htmlspecialchars($formatTimeUK($best6['start_time'])) . ' → '
. htmlspecialchars($formatTimeUK($best6['end_time'])) . '<br>'
. 'Average stress: ' . round($best6['average_stress'], 1)
. ' (' . htmlspecialchars($label6) . ')</li>';
}
$html .= '</ul>';
if (!empty($bestHoursClean)) {
$html .= '<p style="margin:8px 0 4px;"><strong>Good hours to start a wash (between 09:00 and 21:00 UK):</strong></p>';
$html .= '<ul style="margin:0 0 4px 18px;padding:0;">';
foreach ($bestHoursClean as $h) {
$html .= '<li>'
. htmlspecialchars($h['datetime_uk'] ?? $h['datetime'])
. ' — stress ' . round($h['indoor_stress'], 1)
. '</li>';
}
$html .= '</ul>';
}
$html .= '<p style="margin:8px 0 0;font-size:12px;color:#555;">'
. 'Lower stress means less time with very high humidity, '
. 'so less condensation and mould risk when drying clothes indoors. '
. 'Night-time hours are penalised so suggestions favour daytime (09:00–21:00).'
. '</p>';
$html .= '</div>';
// Convenience: add UK times to windows
$addUkTimes = function (?array $win) use ($formatTimeUK) {
if (!$win) return null;
$win['start_time_uk'] = $formatTimeUK($win['start_time'] ?? null);
$win['end_time_uk'] = $formatTimeUK($win['end_time'] ?? null);
return $win;
};
$best6 = $addUkTimes($best6);
$best12 = $addUkTimes($best12);
$best24 = $addUkTimes($best24);
return [
'best_hours' => $bestHoursClean,
'best_6h_window' => $best6,
'best_12h_window' => $best12,
'best_24h_window' => $best24,
'rows' => $Data,
'summary_text' => $html,
];
}