2323
2424from .const import (
2525 CONF_DEFAULT_HEIGHT ,
26+ CONF_MAX_COVERAGE_STEPS ,
27+ CONF_MINIMIZE_MOVEMENTS ,
2628 DEFAULT_DEFAULT_HEIGHT ,
29+ DEFAULT_MAX_COVERAGE_STEPS ,
30+ DEFAULT_MINIMIZE_MOVEMENTS ,
2731 EVENT_FOV_ENTER ,
2832 EVENT_FOV_EXIT ,
2933 EVENT_SUNRISE ,
3034 EVENT_SUNSET ,
3135 FORECAST_STEP_MINUTES ,
3236 SUN_DATA_STEP_SECONDS ,
3337)
38+ from .position_utils import PositionConverter
3439
3540if TYPE_CHECKING :
3641 from .coordinator import AdaptiveDataUpdateCoordinator
@@ -88,6 +93,9 @@ def build_forecast(
8893 default_position : int ,
8994 now : datetime ,
9095 step_minutes : int = FORECAST_STEP_MINUTES ,
96+ minimize_movements : bool = False ,
97+ max_coverage_steps : int = 1 ,
98+ full_coverage_at_zero : bool = True ,
9199) -> Forecast :
92100 """Compute the forecast for one cover.
93101
@@ -110,6 +118,9 @@ def build_forecast(
110118 cover_factory = cover_factory ,
111119 default_position = default_position ,
112120 step_minutes = step_minutes ,
121+ minimize_movements = minimize_movements ,
122+ max_coverage_steps = max_coverage_steps ,
123+ full_coverage_at_zero = full_coverage_at_zero ,
113124 )
114125 events = _build_events (
115126 sun_data = sun_data , cover_factory = cover_factory , samples = samples
@@ -123,6 +134,9 @@ def _build_samples(
123134 cover_factory : Callable [[float , float ], AdaptiveGeneralCover ],
124135 default_position : int ,
125136 step_minutes : int ,
137+ minimize_movements : bool = False ,
138+ max_coverage_steps : int = 1 ,
139+ full_coverage_at_zero : bool = True ,
126140) -> list [ForecastSample ]:
127141 """Walk the sun_data table at *step_minutes* cadence over the full calendar day.
128142
@@ -156,11 +170,14 @@ def _build_samples(
156170 # and every sample collapses to the default position (issue #516).
157171 cover .eval_time = t
158172 if cover .direct_sun_valid :
159- samples .append (
160- ForecastSample (
161- t = t , position = int (cover .calculate_percentage ()), handler = "solar"
173+ pos = int (cover .calculate_percentage ())
174+ if minimize_movements :
175+ # Mirror the live solar branch so the forecast strip matches the
176+ # quantized positions the cover will actually be commanded to.
177+ pos = PositionConverter .quantize_to_coverage_steps (
178+ pos , max_coverage_steps , full_coverage_at_zero
162179 )
163- )
180+ samples . append ( ForecastSample ( t = t , position = pos , handler = "solar" ) )
164181 else :
165182 samples .append (
166183 ForecastSample (t = t , position = int (default_position ), handler = "default" )
@@ -308,9 +325,21 @@ def make_cover(azi: float, ele: float) -> AdaptiveGeneralCover:
308325 options = options ,
309326 )
310327
328+ # Coverage direction comes from the policy's primary axis (single source of
329+ # truth): awning blocks the sun when open (full coverage at 100%), every
330+ # other cover type at 0%.
331+ full_coverage_at_zero = not coord ._policy .axes [0 ].open_blocks_sun # noqa: SLF001
332+
311333 return build_forecast (
312334 sun_data = sun_data ,
313335 cover_factory = make_cover ,
314336 default_position = int (options .get (CONF_DEFAULT_HEIGHT , DEFAULT_DEFAULT_HEIGHT )),
315337 now = dt_util .now (),
338+ minimize_movements = bool (
339+ options .get (CONF_MINIMIZE_MOVEMENTS , DEFAULT_MINIMIZE_MOVEMENTS )
340+ ),
341+ max_coverage_steps = int (
342+ options .get (CONF_MAX_COVERAGE_STEPS , DEFAULT_MAX_COVERAGE_STEPS )
343+ ),
344+ full_coverage_at_zero = full_coverage_at_zero ,
316345 )
0 commit comments