Alarm Calculation Base Class¶
The AlarmCalc class is the base class for all alarm calculation classes. It determines the interface that all alarm calculation classes must implement. The AlarmCalc class is defined in the alarm_calc_core.py file and defines these top level methods:
- A default
__init__method that receives the object name and alarm id to calculate. It will do some initial checks to make sure the alarm definition is correct and will create the necessary data structures to store the results. - The
savemethod that will save the results of the calculation to the database. This method should be called after the calculation is done and the results are ready to be stored. - A series of properties that can be used to retrieve the main configurations related to the calculation, like the name of the alarm calculator, the object name and the alarm id, etc.
The subclasses of AlarmCalc must implement the following attributes and methods:
-
_nameclass attribute: A string with the name of the alarm calculator. This is used to identify the alarm calculator type in the database. For example, in case of alarms based on feature thresholds, the class definition starts like this:class AlarmCalcThreshold(AlarmCalc): _name = "threshold" -
A
__init__method that that will call the parent__init__method and will also check for the required settings of this alarm calculator. - A
calculatemethod that will perform the actual calculation of the alarm. This method should receive the period of the calculation and return a DataFrame with the calculated alarms. At the end of the calculation, the resulting DataFrame must be saved in the_resultattribute of the class.
Class Definition¶
AlarmCalculator(object_name, alarm_id)
¶
Base abstract class for alarm calculators. Already defines the methods and attributes that are common to all alarm calculators.
This should be overloaded in child classes of AlarmCalculator to define the name, requirements and logic of the alarm calculator.
It's extremely important to define the name of the alarm calculator in the child class. This name must be equal to the "trigger_name" value of the "trigger" column of the alarm in performance_db and will be used to check if the alarm calculator is the correct one for the alarm. Set the name of the alarm calculator in the child class as a class attribute "_name".
Also, the alarm must be defined in the database with the data source type "server_alarm".
Below there is a simple example on how to define a child class of AlarmCalculator:
class AlarmCalculatorExample(AlarmCalculator):
# name of the alarm calculator
_name = "name"
def __init__(self, object_name: str, alarm_id: int) -> None:
# initialize parent class
super().__init__(object_name, alarm_id)
Parameters:
-
(object_name¶str) –Name of the object for which the alarm is calculated. It must exist in performance_db.
-
(alarm_id¶int) –ID of the alarm (manufacturer id) for which the alarm calculator is being created. It must exist in performance_db for the model of the object.
Source code in echo_energycalc/alarm_calc_core.py
def __init__(
self,
object_name: str,
alarm_id: int,
) -> None:
"""
Constructor of the AlarmCalculator class.
This should be overloaded in child classes of AlarmCalculator to define the name, requirements and logic of the alarm calculator.
It's extremely important to define the name of the alarm calculator in the child class. This name must be equal to the "trigger_name" value of the "trigger" column of the alarm in performance_db and will be used to check if the alarm calculator is the correct one for the alarm. Set the name of the alarm calculator in the child class as a class attribute "_name".
Also, the alarm must be defined in the database with the data source type "server_alarm".
Below there is a simple example on how to define a child class of AlarmCalculator:
```python
class AlarmCalculatorExample(AlarmCalculator):
# name of the alarm calculator
_name = "name"
def __init__(self, object_name: str, alarm_id: int) -> None:
# initialize parent class
super().__init__(object_name, alarm_id)
```
Parameters
----------
object_name : str
Name of the object for which the alarm is calculated. It must exist in performance_db.
alarm_id : int
ID of the alarm (manufacturer id) for which the alarm calculator is being created. It must exist in performance_db for the model of the object.
"""
# checking arguments
if not isinstance(object_name, str):
raise TypeError(f"object_name must be a string, not {type(object_name)}")
if not isinstance(alarm_id, int):
raise TypeError(f"alarm_id must be an integer, not {type(alarm_id)}")
# check if self._name is defined in child class
if not hasattr(self, "_name"):
raise ValueError(f"AlarmCalculator name is not defined in {self.__class__.__name__}.")
# creating structure that will be used to connect to performance_db
self._perfdb = PerfDB(application_name=self.__class__.__name__)
# check if object exists in performance_db
obj_def = self._perfdb.objects.instances.get(object_names=[object_name], output_type="dict")
if len(obj_def) == 0:
raise ValueError(f"Object {object_name} does not exist in performance_db.")
self._object = object_name
# get object model
self._object_model = obj_def[object_name]["object_model_name"]
# check if alarm exists in performance_db
alarm_def = self._perfdb.alarms.definitions.get(
object_models=[self._object_model],
data_source_types=["server_alarm"],
alarm_ids=[alarm_id],
match_alarm_id_on="manufacturer_id",
output_type="dict",
)
if len(alarm_def) == 0:
raise ValueError(
f"Alarm {alarm_id} does not exist in performance_db for object {object_name} with data_source_type = 'server_alarm'.",
)
self._alarm_id = alarm_id
# getting the type of the alarm
self._alarm_type = next(iter(alarm_def[self._object_model][alarm_id].keys()))
# getting id of the alarm in the database
self._alarm_db_id = alarm_def[self._object_model][alarm_id][self._alarm_type]["id"]
# getting other relevant information about the alarm
self._alarm_settings = alarm_def[self._object_model][alarm_id][self._alarm_type]
# popping the id from the settings
self._alarm_settings.pop("id")
# validating if trigger_type is equal to the name of the alarm calculator
if "trigger_type" not in self.alarm_settings["trigger"]:
raise ValueError(f"'trigger_type' not found in alarm {alarm_id} settings.")
if self.alarm_settings["trigger"]["trigger_type"] != self.name:
raise ValueError(f"'trigger_type' in alarm {alarm_id} settings is different from the alarm calculator name.")
# getting maximum duration of alarm from database settings
settings = self._perfdb.settings.get()
if "alarms" not in settings and "default_truncate_seconds" not in settings["alarms"]:
logger.warning("Default truncate seconds not found in settings. Using 604800 seconds as default.")
self._max_duration_seconds = 604800
else:
self._max_duration_seconds: int = settings["alarms"]["default_truncate_seconds"]["value"]["max_seconds"]
# results of the calculation. It will be filled by the method "calculate".
self._result: DataFrame | None = None
# period evaluated in the calculation
self._evaluated_period: DateTimeRange | None = None
alarm_db_id
property
¶
ID of the alarm in the database. This is used to get the alarm settings.
Returns:
-
int–ID of the alarm in the database.
alarm_id
property
¶
ID of the alarm that is calculated (manufacturer_id). This will be defined in the constructor and cannot be changed.
Returns:
-
int–ID of the alarm that is calculated (manufacturer_id).
alarm_settings
property
¶
Settings of the alarm. This is a dictionary with the settings of the alarm that is being calculated.
Returns:
-
dict[str, Any]–Settings of the alarm.
alarm_type
property
¶
Type of the alarm that is calculated. This will be defined in the constructor and cannot be changed.
Returns:
-
str–Type of the alarm that is calculated.
name
property
¶
Name of the alarm calculator. Is defined in child classes of AlarmCalculator.
This must be equal to the "server_calc_type" attribute of the alarm in performance_db.
Returns:
-
str–Name of the alarm calculator.
object
property
¶
Object for which the alarm is calculated. This will be defined in the constructor and cannot be changed.
Returns:
-
str–Object name for which the alarm is calculated.
result
property
¶
Result of the calculation. This is None until the method "calculate" is called.
Returns:
-
DataFrame | None:–Result of the calculation if the method "calculate" was called. None otherwise.
calculate(period, cached_data=None, save=True)
abstractmethod
¶
Abstract method that should be implemented in child classes. Should calculate the alarm for the given object and period.
The method also should call the method "save" to save the calculated alarm in performance_db.
At the end of the method, the attribute "_result" should be filled with the result of the calculation.
Parameters:
-
(period¶DateTimeRange) –Period for which the alarm will be calculated.
-
(cached_data¶dict[str, DataFrame] | None, default:None) –Dict with DataFrame containing the cached data for each object. This is useful to avoid querying the database multiple times for the same data. The default is None.
-
(save¶bool, default:True) –If True, the result of the alarms will be saved in the database. The default is True.
Returns:
-
DataFrame–Pandas DataFrame with the calculated alarm.
Source code in echo_energycalc/alarm_calc_core.py
@abstractmethod
def calculate(
self,
period: DateTimeRange,
cached_data: dict[str, DataFrame] | None = None,
save: bool = True,
) -> DataFrame:
"""
Abstract method that should be implemented in child classes. Should calculate the alarm for the given object and period.
The method also should call the method "save" to save the calculated alarm in performance_db.
At the end of the method, the attribute "_result" should be filled with the result of the calculation.
Parameters
----------
period : DateTimeRange
Period for which the alarm will be calculated.
cached_data : dict[str, DataFrame] | None, optional
Dict with DataFrame containing the cached data for each object. This is useful to avoid querying the database multiple times for the same data. The default is None.
save : bool, optional
If True, the result of the alarms will be saved in the database. The default is True.
Returns
-------
DataFrame
Pandas DataFrame with the calculated alarm.
"""
raise NotImplementedError("The method 'calculate' must be implemented in the child class.")
save()
¶
Save the result of the calculation in the database.
If the method "calculate" was not called before, this method will raise an error.
Source code in echo_energycalc/alarm_calc_core.py
def save(self) -> None:
"""
Save the result of the calculation in the database.
If the method "calculate" was not called before, this method will raise an error.
"""
# checking if the result was calculated
if self._result is None:
raise ValueError("The method 'calculate' must be called before saving the result.")
# checking if the period was evaluated
if self._evaluated_period is None:
raise ValueError("Evaluated period was not set during calculation.")
# getting result
result = self._result
# checking if there are rows where start and end are equal
if not result.empty:
equal_count = result[result["start"] == result["end"]].shape[0]
if equal_count > 0:
logger.warning(f"Found {equal_count} rows with start equal to end in the result. These rows will be removed.")
result = result[result["start"] != result["end"]]
end_before_start_count = result[result["end"] < result["start"]].shape[0]
if end_before_start_count > 0:
logger.warning(f"Found {end_before_start_count} rows with end before start in the result. These rows will be removed.")
result = result[result["end"] >= result["start"]]
# renaming columns
result = result.rename(columns={"alarm_id": "manufacturer_id"})
# dropping existing alarms within the period
logger.info(
f"Deleting existing alarms for object {self.object} and alarm {self.alarm_id} within the period {self._evaluated_period}.",
)
self._perfdb.alarms.history.delete(
object_names=[self.object],
period=self._evaluated_period,
alarm_ids=[self.alarm_db_id],
)
if not result.empty:
# saving the result in the database
logger.info(f"Saving alarms for object {self.object} and alarm {self.alarm_id} within the period {self._evaluated_period}.")
self._perfdb.alarms.history.insert(
df=result,
on_conflict="update",
)
else:
logger.info(f"No alarms to save for object {self.object} and alarm {self.alarm_id} within the period {self._evaluated_period}.")