Implement LED trigger policy & LED Indicator

PiperOrigin-RevId: 975754032
Change-Id: I9aaa4e75a63aa1e5300ef0cdff87fc208613e862
diff --git a/tlbmc/collector/led_collector.cc b/tlbmc/collector/led_collector.cc
index 76ac82f..079c8da 100644
--- a/tlbmc/collector/led_collector.cc
+++ b/tlbmc/collector/led_collector.cc
@@ -82,6 +82,8 @@
   }
 
   std::vector<std::unique_ptr<ConditionPolicy>> condition_policies;
+  absl::flat_hash_map<std::string, std::unique_ptr<TriggerPolicy>>
+      trigger_policies;
   condition_policies.reserve(params.led_configs.policies_size());
   for (const LedPolicy& policy : params.led_configs.policies()) {
     if (policy.preset_name().empty()) {
@@ -100,8 +102,25 @@
             policy.condition().expression(), std::move(expr)));
         break;
       }
-      case LedPolicy::EventCase::kTrigger:
-        return absl::UnimplementedError("LED triggers are not yet supported");
+      case LedPolicy::EventCase::kTrigger: {
+        absl::string_view event_name = policy.trigger().event();
+        if (event_name.empty()) {
+          return absl::InvalidArgumentError("Trigger event cannot be empty");
+        }
+        auto compiled_policy = std::make_unique<TriggerPolicy>(
+            policy.name(), policy.preset_name(), event_name);
+
+        // TODO(b/546330351): Implement GPIO triggers.
+
+        auto [_, inserted] = trigger_policies.try_emplace(
+            event_name, std::move(compiled_policy));
+        if (!inserted) {
+          return absl::InvalidArgumentError(
+              absl::StrCat("Duplicate trigger event: ", event_name));
+        }
+        break;
+      }
+      case LedPolicy::EventCase::EVENT_NOT_SET:
       default:
         return absl::InvalidArgumentError("LED policy event must be set");
     }
@@ -174,6 +193,30 @@
   }
 
   bool has_conditional_policies = !condition_policies.empty();
+  absl::flat_hash_map<std::string, LedIndicator> indicators;
+  for (const LedIndicator& indicator : params.led_configs.indicators()) {
+    if (indicator.led_name().empty()) {
+      return absl::InvalidArgumentError("Indicator led_name cannot be empty");
+    }
+    if (!leds.contains(indicator.led_name())) {
+      return absl::NotFoundError(
+          absl::StrCat("Indicator LED not found: ", indicator.led_name()));
+    }
+    if (indicator.event_on().empty()) {
+      return absl::InvalidArgumentError("Indicator event_on cannot be empty");
+    }
+    if (indicator.event_off().empty()) {
+      return absl::InvalidArgumentError("Indicator event_off cannot be empty");
+    }
+    if (indicator.event_blink().empty()) {
+      return absl::InvalidArgumentError(
+          "Indicator event_blink cannot be empty");
+    }
+    if (!indicators.try_emplace(indicator.system_id(), indicator).second) {
+      return absl::InvalidArgumentError(absl::StrCat(
+          "Duplicate indicator system_id: ", indicator.system_id()));
+    }
+  }
 
   if (!has_conditional_policies) {
     for (const auto& [_, led] : leds) {
@@ -184,6 +227,7 @@
   auto collector = absl::WrapUnique(new LedCollector(
       std::move(task_scheduler), std::move(leds), std::move(presets),
       std::move(compiled_condition_variables), std::move(condition_policies),
+      std::move(trigger_policies), std::move(indicators),
       params.power_control_collector, params.clock));
 
   if (has_conditional_policies) {
@@ -285,6 +329,89 @@
   return ApplyPolicy(policy, clock_->Now());
 }
 
+absl::Status LedCollector::Trigger(absl::string_view event) {
+  auto it = trigger_policies_.find(event);
+  if (it == trigger_policies_.end()) {
+    return absl::NotFoundError(
+        absl::StrCat("Trigger event not found: ", event));
+  }
+  return ApplyPolicy(*it->second);
+}
+
+absl::StatusOr<const LedIndicator*> LedCollector::GetIndicator(
+    absl::string_view system_id) const {
+  if (indicators_.empty()) {
+    return absl::FailedPreconditionError("Indicator LED is not configured");
+  }
+  auto it = indicators_.find(system_id);
+  if (it != indicators_.end()) {
+    return &it->second;
+  }
+  it = indicators_.find("");
+  if (it != indicators_.end()) {
+    return &it->second;
+  }
+  return absl::NotFoundError(
+      absl::StrCat("Indicator LED not found for system: ", system_id));
+}
+
+bool LedCollector::HasIndicatorConfig() const { return !indicators_.empty(); }
+
+bool LedCollector::HasIndicatorConfig(absl::string_view system_id) const {
+  return indicators_.contains(system_id) || indicators_.contains("");
+}
+
+absl::StatusOr<std::string> LedCollector::GetIndicatorLedName(
+    absl::string_view system_id) const {
+  ECCLESIA_ASSIGN_OR_RETURN(const LedIndicator* indicator,
+                            GetIndicator(system_id));
+  return indicator->led_name();
+}
+
+absl::StatusOr<std::string> LedCollector::GetIndicatorLedName() const {
+  return GetIndicatorLedName("");
+}
+
+absl::StatusOr<LedState> LedCollector::GetIndicatorState(
+    absl::string_view system_id) const {
+  ECCLESIA_ASSIGN_OR_RETURN(const LedIndicator* indicator,
+                            GetIndicator(system_id));
+  ECCLESIA_ASSIGN_OR_RETURN(LedBehavior behavior,
+                            GetLedBehavior(indicator->led_name()));
+  return behavior.state();
+}
+
+absl::StatusOr<LedState> LedCollector::GetIndicatorState() const {
+  return GetIndicatorState("");
+}
+
+absl::Status LedCollector::SetIndicatorState(absl::string_view system_id,
+                                             LedState state) {
+  ECCLESIA_ASSIGN_OR_RETURN(const LedIndicator* indicator,
+                            GetIndicator(system_id));
+  switch (state) {
+    case LED_STATE_ON:
+      return Trigger(indicator->event_on());
+    case LED_STATE_BLINK: {
+      absl::Status status = Trigger(indicator->event_blink());
+      if (absl::IsNotFound(status)) {
+        return Trigger(indicator->event_on());
+      }
+      return status;
+    }
+    case LED_STATE_OFF:
+      return Trigger(indicator->event_off());
+    case LED_STATE_UNSPECIFIED:
+    default:
+      return absl::InvalidArgumentError(
+          absl::StrCat("Invalid LED state: ", LedState_Name(state)));
+  }
+}
+
+absl::Status LedCollector::SetIndicatorState(LedState state) {
+  return SetIndicatorState("", state);
+}
+
 absl::Status LedCollector::EvaluatePolicies() {
   absl::flat_hash_map<std::string, double> variable_map;
   ECCLESIA_ASSIGN_OR_RETURN(variable_map, ResolveConditionVariables());
@@ -383,6 +510,20 @@
   }
   response["condition_policies"] = condition_policies;
 
+  nlohmann::json trigger_policies = nlohmann::json::object();
+  for (const auto& [event, policy] : trigger_policies_) {
+    nlohmann::json policy_json;
+    policy_json["name"] = policy->name;
+    policy_json["preset_name"] = policy->preset_name;
+    policy_json["event"] = policy->event_name;
+    const absl::Time last_applied_time = policy->last_applied_time.load();
+    if (last_applied_time != absl::InfinitePast()) {
+      policy_json["last_applied_time"] = absl::FormatTime(last_applied_time);
+    }
+    trigger_policies[event] = policy_json;
+  }
+  response["trigger_policies"] = trigger_policies;
+
   return response;
 }
 
diff --git a/tlbmc/collector/led_collector.h b/tlbmc/collector/led_collector.h
index dccd2b7..b642a2c 100644
--- a/tlbmc/collector/led_collector.h
+++ b/tlbmc/collector/led_collector.h
@@ -85,6 +85,14 @@
         std::numeric_limits<double>::quiet_NaN();
   };
 
+  struct TriggerPolicy : CompiledPolicy {
+    TriggerPolicy(absl::string_view name_, absl::string_view preset_name_,
+                  absl::string_view event_name)
+        : CompiledPolicy(name_, preset_name_), event_name(event_name) {}
+
+    std::string event_name;
+  };
+
   static absl::StatusOr<std::unique_ptr<LedCollector>> Create(
       const Params& params);
 
@@ -103,12 +111,48 @@
   virtual absl::StatusOr<LedBehavior> GetLedBehavior(
       absl::string_view led_name) const;
 
+  // Triggers an event-driven LED policy at the current time.
+  virtual absl::Status Trigger(absl::string_view event);
+
   // Returns all LED names managed by this collector.
   virtual std::vector<std::string> GetLedNames() const;
 
   // Returns all preset names managed by this collector.
   virtual std::vector<std::string> GetPresetNames() const;
 
+  // Returns all indicator configurations mapped by system_id.
+  const absl::flat_hash_map<std::string, LedIndicator>& GetIndicators() const {
+    return indicators_;
+  }
+
+  // Returns true if at least one indicator LED configuration exists.
+  virtual bool HasIndicatorConfig() const;
+
+  // Returns true if an indicator LED configuration exists for `system_id`
+  // or if a default/fallback indicator (system_id == "") is configured.
+  virtual bool HasIndicatorConfig(absl::string_view system_id) const;
+
+  // Returns the name of the LED assigned to indicate the given `system_id`.
+  // Falls back to the default indicator ("") if `system_id` is not found.
+  // Returns FailedPreconditionError if no indicators are configured, or
+  // NotFoundError if neither `system_id` nor the default indicator exists.
+  virtual absl::StatusOr<std::string> GetIndicatorLedName(
+      absl::string_view system_id) const;
+  virtual absl::StatusOr<std::string> GetIndicatorLedName() const;
+
+  // Returns the current state of the indicator LED for `system_id`
+  // (falling back to the default indicator if not found).
+  virtual absl::StatusOr<LedState> GetIndicatorState(
+      absl::string_view system_id) const;
+  virtual absl::StatusOr<LedState> GetIndicatorState() const;
+
+  // Applies `state` to the configured indicator for `system_id`.
+  // If `state` is LED_STATE_BLINK and no blink event policy is configured,
+  // it automatically falls back to the event_on policy.
+  virtual absl::Status SetIndicatorState(absl::string_view system_id,
+                                         LedState state);
+  virtual absl::Status SetIndicatorState(LedState state);
+
  protected:
   LedCollector() = default;
 
@@ -133,16 +177,26 @@
       std::vector<std::unique_ptr<CompiledConditionVariable>>
           condition_variables,
       std::vector<std::unique_ptr<ConditionPolicy>> condition_policies,
+      absl::flat_hash_map<std::string, std::unique_ptr<TriggerPolicy>>
+          trigger_policies,
+      absl::flat_hash_map<std::string, LedIndicator> indicators,
       const PowerControlCollector* power_control_collector,
       ecclesia::Clock* clock = ecclesia::Clock::RealClock())
       : leds_(std::move(leds)),
         presets_(std::move(presets)),
         condition_variables_(std::move(condition_variables)),
         condition_policies_(std::move(condition_policies)),
+        trigger_policies_(std::move(trigger_policies)),
+        indicators_(std::move(indicators)),
         power_control_collector_(power_control_collector),
         clock_(clock),
         task_scheduler_(std::move(task_scheduler)) {}
 
+  // Looks up the indicator for `system_id`, falling back to the default
+  // indicator ("") if present.
+  absl::StatusOr<const LedIndicator*> GetIndicator(
+      absl::string_view system_id) const;
+
   // Applies a policy and updates its last applied time to `timestamp`.
   //
   // An explicit `timestamp` is used during `EvaluatePolicies()` so that all
@@ -166,6 +220,9 @@
       presets_;
   std::vector<std::unique_ptr<CompiledConditionVariable>> condition_variables_;
   const std::vector<std::unique_ptr<ConditionPolicy>> condition_policies_;
+  const absl::flat_hash_map<std::string, std::unique_ptr<TriggerPolicy>>
+      trigger_policies_;
+  const absl::flat_hash_map<std::string, LedIndicator> indicators_;
   const PowerControlCollector* power_control_collector_ = nullptr;
   ecclesia::Clock* clock_ = ecclesia::Clock::RealClock();
 
diff --git a/tlbmc/led_config.proto b/tlbmc/led_config.proto
index cc4e12b..458c807 100644
--- a/tlbmc/led_config.proto
+++ b/tlbmc/led_config.proto
@@ -89,9 +89,9 @@
   string expression = 1;
 }
 
-enum LedTrigger {
-  LED_TRIGGER_UNSPECIFIED = 0;
-  // TODO: b/546330351 - impl GPIO trigger
+message LedTrigger {
+  // Name of the event that activates this policy (e.g., "indicator:on").
+  string event = 1;
 }
 
 message LedPolicy {
@@ -103,9 +103,26 @@
   }
 }
 
+// Configuration mapping Redfish indicator LED states to trigger events.
+message LedIndicator {
+  // System ID of the LED (e.g., "system", "chassis").
+  // If not set, the LED is applied to unmatched systems as a fallback.
+  string system_id = 1;
+  // Name of the LED to control, matching a defined LedConfig.name.
+  string led_name = 2;
+  // Event name emitted when the indicator is set to ON.
+  string event_on = 3 [default = "indicator:on"];
+  // Event name emitted when the indicator is set to OFF.
+  string event_off = 4 [default = "indicator:off"];
+  // Event name emitted when the indicator is set to BLINK.
+  string event_blink = 5 [default = "indicator:blink"];
+}
+
 message LedConfigs {
   repeated LedConfig leds = 1;
   repeated LedPreset presets = 2;
   repeated LedConditionVariable variables = 3;
   repeated LedPolicy policies = 4;
+  // Indicator configurations mapping system IDs to LED trigger events.
+  repeated LedIndicator indicators = 5;
 }