1212
1313from ...microgrid import connection_manager
1414from ...microgrid .component import ComponentCategory , ComponentMetricId , InverterType
15- from ...timeseries import Energy , Percentage , Sample , Temperature
15+ from ...timeseries import Sample
16+ from .._quantities import Energy , Percentage , Power , Temperature
1617from ._component_metrics import ComponentMetricsData
17- from ._result_types import Bound , PowerMetrics
18+ from ._result_types import Bounds , PowerMetrics
1819
1920_logger = logging .getLogger (__name__ )
2021_MIN_TIMESTAMP = datetime .min .replace (tzinfo = timezone .utc )
@@ -479,11 +480,15 @@ def __init__(
479480 super ().__init__ (used_batteries )
480481 self ._battery_metrics = [
481482 ComponentMetricId .POWER_INCLUSION_LOWER_BOUND ,
483+ ComponentMetricId .POWER_EXCLUSION_LOWER_BOUND ,
484+ ComponentMetricId .POWER_EXCLUSION_UPPER_BOUND ,
482485 ComponentMetricId .POWER_INCLUSION_UPPER_BOUND ,
483486 ]
484487
485488 self ._inverter_metrics = [
486489 ComponentMetricId .ACTIVE_POWER_INCLUSION_LOWER_BOUND ,
490+ ComponentMetricId .ACTIVE_POWER_EXCLUSION_LOWER_BOUND ,
491+ ComponentMetricId .ACTIVE_POWER_EXCLUSION_UPPER_BOUND ,
487492 ComponentMetricId .ACTIVE_POWER_INCLUSION_UPPER_BOUND ,
488493 ]
489494
@@ -514,6 +519,84 @@ def inverter_metrics(self) -> Mapping[int, list[ComponentMetricId]]:
514519 """
515520 return {cid : self ._inverter_metrics for cid in set (self ._bat_inv_map .values ())}
516521
522+ def _fetch_inclusion_bounds (
523+ self ,
524+ battery_id : int ,
525+ inverter_id : int ,
526+ metrics_data : dict [int , ComponentMetricsData ],
527+ ) -> tuple [datetime , list [float ], list [float ]]:
528+ timestamp = _MIN_TIMESTAMP
529+ inclusion_lower_bounds : list [float ] = []
530+ inclusion_upper_bounds : list [float ] = []
531+
532+ # Inclusion upper and lower bounds are not related.
533+ # If one is missing, then we can still use the other.
534+ if battery_id in metrics_data :
535+ data = metrics_data [battery_id ]
536+ value = data .get (ComponentMetricId .POWER_INCLUSION_UPPER_BOUND )
537+ if value is not None :
538+ timestamp = max (timestamp , data .timestamp )
539+ inclusion_upper_bounds .append (value )
540+
541+ value = data .get (ComponentMetricId .POWER_INCLUSION_LOWER_BOUND )
542+ if value is not None :
543+ timestamp = max (timestamp , data .timestamp )
544+ inclusion_lower_bounds .append (value )
545+
546+ if inverter_id in metrics_data :
547+ data = metrics_data [inverter_id ]
548+
549+ value = data .get (ComponentMetricId .ACTIVE_POWER_INCLUSION_UPPER_BOUND )
550+ if value is not None :
551+ timestamp = max (data .timestamp , timestamp )
552+ inclusion_upper_bounds .append (value )
553+
554+ value = data .get (ComponentMetricId .ACTIVE_POWER_INCLUSION_LOWER_BOUND )
555+ if value is not None :
556+ timestamp = max (data .timestamp , timestamp )
557+ inclusion_lower_bounds .append (value )
558+
559+ return (timestamp , inclusion_lower_bounds , inclusion_upper_bounds )
560+
561+ def _fetch_exclusion_bounds (
562+ self ,
563+ battery_id : int ,
564+ inverter_id : int ,
565+ metrics_data : dict [int , ComponentMetricsData ],
566+ ) -> tuple [datetime , list [float ], list [float ]]:
567+ timestamp = _MIN_TIMESTAMP
568+ exclusion_lower_bounds : list [float ] = []
569+ exclusion_upper_bounds : list [float ] = []
570+
571+ # Exclusion upper and lower bounds are not related.
572+ # If one is missing, then we can still use the other.
573+ if battery_id in metrics_data :
574+ data = metrics_data [battery_id ]
575+ value = data .get (ComponentMetricId .POWER_EXCLUSION_UPPER_BOUND )
576+ if value is not None :
577+ timestamp = max (timestamp , data .timestamp )
578+ exclusion_upper_bounds .append (value )
579+
580+ value = data .get (ComponentMetricId .POWER_EXCLUSION_LOWER_BOUND )
581+ if value is not None :
582+ timestamp = max (timestamp , data .timestamp )
583+ exclusion_lower_bounds .append (value )
584+
585+ if inverter_id in metrics_data :
586+ data = metrics_data [inverter_id ]
587+
588+ value = data .get (ComponentMetricId .ACTIVE_POWER_EXCLUSION_UPPER_BOUND )
589+ if value is not None :
590+ timestamp = max (data .timestamp , timestamp )
591+ exclusion_upper_bounds .append (value )
592+
593+ value = data .get (ComponentMetricId .ACTIVE_POWER_EXCLUSION_LOWER_BOUND )
594+ if value is not None :
595+ timestamp = max (data .timestamp , timestamp )
596+ exclusion_lower_bounds .append (value )
597+
598+ return (timestamp , exclusion_lower_bounds , exclusion_upper_bounds )
599+
517600 def calculate (
518601 self ,
519602 metrics_data : dict [int , ComponentMetricsData ],
@@ -533,53 +616,45 @@ def calculate(
533616 High level metric calculated from the given metrics.
534617 Return None if there are no component metrics.
535618 """
536- # In the future we will have lower bound, too.
537-
538- result = PowerMetrics (
539- timestamp = _MIN_TIMESTAMP ,
540- supply_bound = Bound (0 , 0 ),
541- consume_bound = Bound (0 , 0 ),
542- )
619+ timestamp = _MIN_TIMESTAMP
620+ inclusion_bounds_lower = 0.0
621+ inclusion_bounds_upper = 0.0
622+ exclusion_bounds_lower = 0.0
623+ exclusion_bounds_upper = 0.0
543624
544625 for battery_id in working_batteries :
545- supply_upper_bounds : list [float ] = []
546- consume_upper_bounds : list [float ] = []
547-
548- if battery_id in metrics_data :
549- data = metrics_data [battery_id ]
550-
551- # Consume and supply bounds are not related.
552- # If one is missing, then we can still use the other.
553- value = data .get (ComponentMetricId .POWER_INCLUSION_UPPER_BOUND )
554- if value is not None :
555- result .timestamp = max (result .timestamp , data .timestamp )
556- consume_upper_bounds .append (value )
557-
558- value = data .get (ComponentMetricId .POWER_INCLUSION_LOWER_BOUND )
559- if value is not None :
560- result .timestamp = max (result .timestamp , data .timestamp )
561- supply_upper_bounds .append (value )
562-
563626 inverter_id = self ._bat_inv_map [battery_id ]
564- if inverter_id in metrics_data :
565- data = metrics_data [inverter_id ]
566-
567- value = data .get (ComponentMetricId .ACTIVE_POWER_INCLUSION_UPPER_BOUND )
568- if value is not None :
569- result .timestamp = max (data .timestamp , result .timestamp )
570- consume_upper_bounds .append (value )
571-
572- value = data .get (ComponentMetricId .ACTIVE_POWER_INCLUSION_LOWER_BOUND )
573- if value is not None :
574- result .timestamp = max (data .timestamp , result .timestamp )
575- supply_upper_bounds .append (value )
576-
577- if len (consume_upper_bounds ) > 0 :
578- result .consume_bound .upper += min (consume_upper_bounds )
579- if len (supply_upper_bounds ) > 0 :
580- result .supply_bound .lower += max (supply_upper_bounds )
627+ (
628+ _ts ,
629+ inclusion_lower_bounds ,
630+ inclusion_upper_bounds ,
631+ ) = self ._fetch_inclusion_bounds (battery_id , inverter_id , metrics_data )
632+ timestamp = max (timestamp , _ts )
633+ (
634+ _ts ,
635+ exclusion_lower_bounds ,
636+ exclusion_upper_bounds ,
637+ ) = self ._fetch_exclusion_bounds (battery_id , inverter_id , metrics_data )
638+ if len (inclusion_upper_bounds ) > 0 :
639+ inclusion_bounds_upper += min (inclusion_upper_bounds )
640+ if len (inclusion_lower_bounds ) > 0 :
641+ inclusion_bounds_lower += max (inclusion_lower_bounds )
642+ if len (exclusion_upper_bounds ) > 0 :
643+ exclusion_bounds_upper += max (exclusion_upper_bounds )
644+ if len (exclusion_lower_bounds ) > 0 :
645+ exclusion_bounds_lower += min (exclusion_lower_bounds )
581646
582- if result . timestamp == _MIN_TIMESTAMP :
647+ if timestamp == _MIN_TIMESTAMP :
583648 return None
584649
585- return result
650+ return PowerMetrics (
651+ timestamp = timestamp ,
652+ inclusion_bounds = Bounds (
653+ Power .from_watts (inclusion_bounds_lower ),
654+ Power .from_watts (inclusion_bounds_upper ),
655+ ),
656+ exclusion_bounds = Bounds (
657+ Power .from_watts (exclusion_bounds_lower ),
658+ Power .from_watts (exclusion_bounds_upper ),
659+ ),
660+ )
0 commit comments