Skip to content

Commit 63ad32e

Browse files
committed
Refactor Solcast to component architecture with standard pattern
- Convert Solcast from mixin to true component accepting base parameter like GECloud - Add autonomous lifecycle with async start/stop methods - Publish standard entities (sensor.pv_forecast_today/tomorrow/d3/d4) without prefixes - Component follows same pattern as other components (GECloud, OctopusAPI, etc.)
1 parent 510544c commit 63ad32e

2 files changed

Lines changed: 405 additions & 340 deletions

File tree

‎apps/predbat/fetch.py‎

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1348,7 +1348,7 @@ def fetch_sensor_data(self):
13481348
self.cost_today_sofar, self.carbon_today_sofar = self.today_cost(self.import_today, self.export_today, self.car_charging_energy, self.load_minutes)
13491349

13501350
# Fetch PV forecast if enabled, today must be enabled, other days are optional
1351-
self.pv_forecast_minute, self.pv_forecast_minute10 = self.fetch_pv_forecast()
1351+
self.pv_forecast_minute, self.pv_forecast_minute10 = self.fetch_pv_forecast_from_entities()
13521352

13531353
# Apply modal filter to historical data
13541354
if self.load_minutes and not self.load_forecast_only:
@@ -2297,3 +2297,70 @@ def load_car_energy(self, now_utc):
22972297
else:
22982298
self.log("Car charging hold {} threshold {}".format(self.car_charging_hold, self.car_charging_threshold * 60.0))
22992299
return self.car_charging_energy
2300+
2301+
def fetch_pv_forecast_from_entities(self):
2302+
"""
2303+
Fetch PV forecast data from configured entities following component architecture
2304+
"""
2305+
pv_forecast_minute = {}
2306+
pv_forecast_minute10 = {}
2307+
found_data = False
2308+
2309+
# Read from configured entities - this is the proper way
2310+
forecast_config_keys = ["pv_forecast_today", "pv_forecast_tomorrow", "pv_forecast_d3", "pv_forecast_d4"]
2311+
2312+
for config_key in forecast_config_keys:
2313+
configured_entity = self.get_arg(config_key, None)
2314+
if configured_entity:
2315+
# Try with forecast attribute first (newer format)
2316+
data = self.get_state_wrapper(entity_id=configured_entity, attribute="forecast")
2317+
if not data:
2318+
# Try with detailedForecast attribute for older Solcast HA integrations
2319+
data = self.get_state_wrapper(entity_id=configured_entity, attribute="detailedForecast")
2320+
2321+
if data:
2322+
found_data = True
2323+
self.log("Found PV forecast data in configured entity {} for {}".format(configured_entity, config_key))
2324+
self._process_forecast_data_to_minutes(data, pv_forecast_minute, pv_forecast_minute10)
2325+
2326+
if not found_data:
2327+
self.log("Warn: No PV forecast data found in configured entities. Check pv_forecast_today, pv_forecast_tomorrow etc. in apps.yaml")
2328+
2329+
return pv_forecast_minute, pv_forecast_minute10
2330+
2331+
def _process_forecast_data_to_minutes(self, forecast_data, pv_forecast_minute, pv_forecast_minute10):
2332+
"""
2333+
Process forecast data from entity into minute-level dictionaries
2334+
"""
2335+
if not forecast_data:
2336+
return
2337+
2338+
for entry in forecast_data:
2339+
if "period_start" not in entry:
2340+
continue
2341+
2342+
try:
2343+
# Parse period start time
2344+
period_start_str = entry.get("period_start", "")
2345+
if isinstance(period_start_str, str):
2346+
period_start = str2time(period_start_str)
2347+
else:
2348+
continue
2349+
2350+
# Calculate minutes from midnight UTC
2351+
minutes = int((period_start - self.midnight_utc).total_seconds() / 60)
2352+
2353+
# Get estimates
2354+
pv_estimate = entry.get("pv_estimate", 0)
2355+
pv_estimate10 = entry.get("pv_estimate10", pv_estimate * 0.1) # Default to 10% if not present
2356+
2357+
# Spread 30-minute forecast over individual minutes
2358+
for minute_offset in range(30):
2359+
minute = minutes + minute_offset
2360+
if minute >= 0: # Only positive minutes
2361+
pv_forecast_minute[minute] = pv_forecast_minute.get(minute, 0) + pv_estimate / 30
2362+
pv_forecast_minute10[minute] = pv_forecast_minute10.get(minute, 0) + pv_estimate10 / 30
2363+
2364+
except (ValueError, TypeError) as e:
2365+
self.log("Warn: Error processing forecast entry {}: {}".format(entry, e))
2366+
continue

0 commit comments

Comments
 (0)