Skip to content

Commit 7efd550

Browse files
springfall2008pre-commit-ci-lite[bot]Copilot
authored
Refactor into Solar API component (#2831)
* Refactor into Solar API component * [pre-commit.ci lite] apply automatic fixes * Update apps/predbat/solcast.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update apps/predbat/web.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Minor fixes * Catch possible crash * Minor fix * Unused import * Wrong sleep * Doc string * More typos * Further typo --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 74b1303 commit 7efd550

7 files changed

Lines changed: 381 additions & 208 deletions

File tree

‎apps/predbat/components.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# pylint: disable=attribute-defined-outside-init
1010

1111

12+
from solcast import SolarAPI
1213
from gecloud import GECloudDirect, GECloudData
1314
from ohme import OhmeAPI
1415
from octopus import OctopusAPI
@@ -61,6 +62,25 @@
6162
"mcp_port": {"required": False, "config": "mcp_port", "default": 8199},
6263
},
6364
},
65+
"solar": {
66+
"class": SolarAPI,
67+
"name": "Solar API",
68+
"args": {
69+
"prefix": {"required": True, "config": "prefix"},
70+
"solcast_host": {"required": False, "config": "solcast_host"},
71+
"solcast_api_key": {"required": False, "config": "solcast_api_key"},
72+
"solcast_sites": {"required": False, "config": "solcast_sites"},
73+
"solcast_poll_hours": {"required": False, "config": "solcast_poll_hours", "default": 8},
74+
"forecast_solar": {"required": False, "config": "forecast_solar", "default": False},
75+
"forecast_solar_max_age": {"required": False, "config": "forecast_solar_max_age", "default": 8},
76+
"pv_forecast_today": {"required": False, "config": "pv_forecast_today"},
77+
"pv_forecast_tomorrow": {"required": False, "config": "pv_forecast_tomorrow"},
78+
"pv_forecast_d3": {"required": False, "config": "pv_forecast_d3"},
79+
"pv_forecast_d4": {"required": False, "config": "pv_forecast_d4"},
80+
"pv_scaling": {"required": False, "config": "pv_scaling", "default": 1.0},
81+
},
82+
"required_or": ["solcast_host", "forecast_solar", "pv_forecast_today"],
83+
},
6484
"gecloud": {
6585
"class": GECloudDirect,
6686
"name": "GivEnergy Cloud Direct",
@@ -217,6 +237,11 @@ def initialize(self, only=None):
217237
have_all_args = False
218238
else:
219239
arg_dict[arg] = self.base.get_arg(arg_info["config"], default, indirect=indirect)
240+
required_or = component_info.get("required_or", [])
241+
# If required_or is set we must have at least one of the listed args
242+
if required_or:
243+
if not any(arg_dict.get(arg, None) for arg in required_or):
244+
have_all_args = False
220245
if have_all_args:
221246
self.log(f"Initializing {component_info['name']} interface")
222247
self.components[component_name] = component_info["class"](*arg_dict.values(), self.base)

‎apps/predbat/fetch.py‎

Lines changed: 45 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from datetime import datetime, timedelta
1313
from utils import minutes_to_time, str2time, dp0, dp1, dp2, dp3, dp4, time_string_to_stamp, minute_data
14-
from config import MINUTE_WATT, PREDICT_STEP, TIME_FORMAT, TIME_FORMAT_SECONDS, PREDBAT_MODE_OPTIONS, PREDBAT_MODE_CONTROL_SOC, PREDBAT_MODE_CONTROL_CHARGEDISCHARGE, PREDBAT_MODE_CONTROL_CHARGE, PREDBAT_MODE_MONITOR, TIME_FORMAT_DAILY
14+
from config import MINUTE_WATT, PREDICT_STEP, TIME_FORMAT, PREDBAT_MODE_OPTIONS, PREDBAT_MODE_CONTROL_SOC, PREDBAT_MODE_CONTROL_CHARGEDISCHARGE, PREDBAT_MODE_CONTROL_CHARGE, PREDBAT_MODE_MONITOR
1515
from futurerate import FutureRate
1616

1717

@@ -430,109 +430,6 @@ def minute_data_load(self, now_utc, entity_name, max_days_previous, load_scaling
430430
age_days = 0
431431
return load_minutes, age_days
432432

433-
def prune_today(self, data, prune=True, group=15, prune_future=False, intermediate=False):
434-
"""
435-
Remove data from before today
436-
"""
437-
results = {}
438-
last_time = None
439-
prev_value = None
440-
for key in data:
441-
# Convert key in format '2024-09-07T15:40:09.799567+00:00' into a datetime
442-
if "." in key:
443-
timekey = datetime.strptime(key, TIME_FORMAT_SECONDS)
444-
else:
445-
timekey = datetime.strptime(key, TIME_FORMAT)
446-
if last_time and (timekey - last_time).seconds < group * 60:
447-
continue
448-
if intermediate and last_time and ((timekey - last_time).seconds > group * 60):
449-
# Large gap, introduce intermediate data point
450-
seconds_gap = int((timekey - last_time).total_seconds())
451-
for i in range(1, seconds_gap // int(group * 60)):
452-
new_time = last_time + timedelta(seconds=i * group * 60)
453-
results[new_time.strftime(TIME_FORMAT)] = prev_value
454-
if not prune or (timekey > self.midnight_utc):
455-
if prune_future and (timekey > self.now_utc):
456-
continue
457-
results[key] = data[key]
458-
last_time = timekey
459-
prev_value = data[key]
460-
return results
461-
462-
def history_attribute(self, history, state_key="state", last_updated_key="last_updated", scale=1.0, attributes=False, daily=False, offset_days=0, first=True, pounds=False):
463-
results = {}
464-
last_updated_time = None
465-
last_day_stamp = None
466-
467-
if not isinstance(history, list):
468-
return results
469-
470-
if history and len(history) >= 1:
471-
history = history[0]
472-
473-
if not isinstance(history, list):
474-
self.log("Warn: history_attribute expects a list of history items, got {}".format(type(history)))
475-
return results
476-
477-
# Process history
478-
for item in history:
479-
if last_updated_key not in item:
480-
continue
481-
482-
if attributes:
483-
if state_key not in item["attributes"]:
484-
continue
485-
state = item["attributes"][state_key]
486-
else:
487-
# Ignore data without correct keys
488-
if state_key not in item:
489-
continue
490-
491-
# Unavailable or bad values
492-
if item[state_key] == "unavailable" or item[state_key] == "unknown":
493-
continue
494-
495-
state = item[state_key]
496-
497-
# Get the numerical key and the timestamp and ignore if in error
498-
try:
499-
state = float(state) * scale
500-
if pounds:
501-
state = dp2(state / 100)
502-
except (ValueError, TypeError):
503-
if isinstance(state, str):
504-
if state.lower() in ["on", "true", "yes"]:
505-
state = 1
506-
elif state.lower() in ["off", "false", "no"]:
507-
state = 0
508-
else:
509-
continue
510-
else:
511-
continue
512-
513-
try:
514-
last_updated_time = item[last_updated_key]
515-
last_updated_stamp = str2time(last_updated_time)
516-
except (ValueError, TypeError):
517-
continue
518-
519-
day_stamp = last_updated_stamp.astimezone().replace(hour=0, minute=0, second=0, microsecond=0)
520-
if offset_days:
521-
day_stamp += timedelta(days=offset_days)
522-
523-
if first and daily and day_stamp == last_day_stamp:
524-
continue
525-
last_day_stamp = day_stamp
526-
527-
# Add the state to the result
528-
if daily:
529-
# Convert day stamp from UTC into localtime
530-
results[day_stamp.strftime(TIME_FORMAT_DAILY)] = state
531-
else:
532-
results[last_updated_time] = state
533-
534-
return results
535-
536433
def fetch_sensor_data(self):
537434
"""
538435
Fetch all the data, e.g. energy rates, load, PV predictions, car plan etc.
@@ -944,6 +841,50 @@ def fetch_sensor_data(self):
944841
else:
945842
self.load_inday_adjustment = 1.0
946843

844+
def fetch_pv_forecast(self):
845+
"""
846+
Fetch PV forecast data from one or more sensors
847+
"""
848+
pv_forecast_minute = {}
849+
pv_forecast_minute10 = {}
850+
851+
# Get data from forecast sensor
852+
entity_id = "sensor." + self.prefix + "_pv_forecast_raw"
853+
pv_forecast_packed_ld = self.get_state_wrapper(entity_id=entity_id, attribute="forecast")
854+
pv_forecast10_packed_ld = self.get_state_wrapper(entity_id=entity_id, attribute="forecast10")
855+
856+
# Convert keys to integers and values to floats
857+
pv_forecast_packed = {}
858+
pv_forecast10_packed = {}
859+
860+
if pv_forecast_packed_ld:
861+
for key, value in pv_forecast_packed_ld.items():
862+
try:
863+
minute = int(key)
864+
pv_forecast_packed[minute] = float(value)
865+
except (ValueError, TypeError):
866+
pass
867+
868+
if pv_forecast10_packed_ld:
869+
for key, value in pv_forecast10_packed_ld.items():
870+
try:
871+
minute = int(key)
872+
pv_forecast10_packed[minute] = float(value)
873+
except (ValueError, TypeError):
874+
pass
875+
876+
# Unpack the forecast data
877+
max_minute = max(pv_forecast_packed.keys()) if pv_forecast_packed else 0
878+
last_value = 0
879+
last_value10 = 0
880+
for minute in range(0, max_minute + 1):
881+
last_value = pv_forecast_packed.get(minute, last_value)
882+
last_value10 = pv_forecast10_packed.get(minute, last_value10)
883+
pv_forecast_minute[minute] = last_value
884+
pv_forecast_minute10[minute] = last_value10
885+
886+
return pv_forecast_minute, pv_forecast_minute10
887+
947888
def predict_battery_temperature(self, battery_temperature_history, step):
948889
"""
949890
Given historical battery temperature data, predict the future temperature

‎apps/predbat/predbat.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
from predheat import PredHeat
6363
from octopus import Octopus
6464
from energydataservice import Energidataservice
65-
from solcast import Solcast
6665
from components import Components
6766
from execute import Execute
6867
from plan import Plan
@@ -73,7 +72,7 @@
7372
from plugin_system import PluginSystem
7473

7574

76-
class PredBat(hass.Hass, Octopus, Energidataservice, Solcast, Fetch, Plan, Execute, Output, UserInterface):
75+
class PredBat(hass.Hass, Octopus, Energidataservice, Fetch, Plan, Execute, Output, UserInterface):
7776
"""
7877
The battery prediction class itself
7978
"""

0 commit comments

Comments
 (0)