diff --git a/aws_lambda_powertools/utilities/data_classes/__init__.py b/aws_lambda_powertools/utilities/data_classes/__init__.py index 38274f0bab4..64416e3cdd9 100644 --- a/aws_lambda_powertools/utilities/data_classes/__init__.py +++ b/aws_lambda_powertools/utilities/data_classes/__init__.py @@ -7,6 +7,14 @@ from .appsync_resolver_event import AppSyncResolverEvent from .aws_config_rule_event import AWSConfigRuleEvent from .bedrock_agent_event import BedrockAgentEvent +from .cloud_watch_alarm_event import ( + CloudWatchAlarmConfiguration, + CloudWatchAlarmData, + CloudWatchAlarmEvent, + CloudWatchAlarmMetric, + CloudWatchAlarmMetricStat, + CloudWatchAlarmState, +) from .cloud_watch_custom_widget_event import CloudWatchDashboardCustomWidgetEvent from .cloud_watch_logs_event import CloudWatchLogsEvent from .code_pipeline_job_event import CodePipelineJobEvent @@ -42,6 +50,12 @@ "AppSyncResolverEvent", "ALBEvent", "BedrockAgentEvent", + "CloudWatchAlarmData", + "CloudWatchAlarmEvent", + "CloudWatchAlarmMetric", + "CloudWatchAlarmState", + "CloudWatchAlarmConfiguration", + "CloudWatchAlarmMetricStat", "CloudWatchDashboardCustomWidgetEvent", "CloudWatchLogsEvent", "CodePipelineJobEvent", diff --git a/aws_lambda_powertools/utilities/data_classes/cloud_watch_alarm_event.py b/aws_lambda_powertools/utilities/data_classes/cloud_watch_alarm_event.py new file mode 100644 index 00000000000..d085228cb37 --- /dev/null +++ b/aws_lambda_powertools/utilities/data_classes/cloud_watch_alarm_event.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +from functools import cached_property +from typing import Any, Dict, List, Literal, Optional + +from aws_lambda_powertools.utilities.data_classes.common import DictWrapper + + +class CloudWatchAlarmState(DictWrapper): + @property + def value(self) -> Literal["OK", "ALARM", "INSUFFICIENT_DATA"]: + """ + Overall state of the alarm. + """ + return self["value"] + + @property + def reason(self) -> str: + """ + Reason why alarm was changed to this state. + """ + return self["reason"] + + @property + def reason_data(self) -> str: + """ + Additional data to back up the reason, usually contains the evaluated data points, + the calculated threshold and timestamps. + """ + return self["reasonData"] + + @cached_property + def reason_data_decoded(self) -> Optional[Any]: + """ + Deserialized version of reason_data. + """ + + return self._json_deserializer(self.reason_data) if self.reason_data else None + + @property + def actions_suppressed_by(self) -> Optional[Literal["Alarm", "ExtensionPeriod", "WaitPeriod"]]: + """ + Describes why the actions when the value is `ALARM` are suppressed in a composite + alarm. + """ + return self.get("actionsSuppressedBy", None) + + @property + def actions_suppressed_reason(self) -> Optional[str]: + """ + Captures the reason for action suppression. + """ + return self.get("actionsSuppressedReason", None) + + @property + def timestamp(self) -> str: + """ + Timestamp of this state change in ISO-8601 format. + """ + return self["timestamp"] + + +class CloudWatchAlarmMetric(DictWrapper): + @property + def metric_id(self) -> str: + """ + Unique ID of the alarm metric. + """ + return self["id"] + + @property + def expression(self) -> Optional[str]: + """ + Optional expression of the alarm metric. + """ + return self.get("expression", None) + + @property + def label(self) -> Optional[str]: + """ + Optional label of the alarm metric. + """ + return self.get("label", None) + + @property + def return_data(self) -> bool: + """ + Whether this metric data is used to determine the state of the alarm or not. + """ + return self["returnData"] + + @property + def metric_stat(self) -> CloudWatchAlarmMetricStat: + return CloudWatchAlarmMetricStat(self["metricStat"]) + + +class CloudWatchAlarmMetricStat(DictWrapper): + @property + def period(self) -> Optional[int]: + """ + Metric evaluation period, in seconds. + """ + return self.get("period", None) + + @property + def stat(self) -> Optional[str]: + """ + Statistical aggregation of metric points, e.g. Average, SampleCount, etc. + """ + return self.get("stat", None) + + @property + def unit(self) -> Optional[str]: + """ + Unit for metric. + """ + return self.get("unit", None) + + @property + def metric(self) -> Optional[Dict]: + """ + Metric details + """ + return self.get("metric", {}) + + +class CloudWatchAlarmData(DictWrapper): + @property + def alarm_name(self) -> str: + """ + Alarm name. + """ + return self["alarmName"] + + @property + def state(self) -> CloudWatchAlarmState: + """ + The current state of the Alarm. + """ + return CloudWatchAlarmState(self["state"]) + + @property + def previous_state(self) -> CloudWatchAlarmState: + """ + The previous state of the Alarm. + """ + return CloudWatchAlarmState(self["previousState"]) + + @property + def configuration(self) -> CloudWatchAlarmConfiguration: + """ + The configuration of the Alarm. + """ + return CloudWatchAlarmConfiguration(self["configuration"]) + + +class CloudWatchAlarmConfiguration(DictWrapper): + @property + def description(self) -> Optional[str]: + """ + Optional description for the Alarm. + """ + return self.get("description", None) + + @property + def alarm_rule(self) -> Optional[str]: + """ + Optional description for the Alarm rule in case of composite alarm. + """ + return self.get("alarmRule", None) + + @property + def alarm_actions_suppressor(self) -> Optional[str]: + """ + Optional action suppression for the Alarm rule in case of composite alarm. + """ + return self.get("actionsSuppressor", None) + + @property + def alarm_actions_suppressor_wait_period(self) -> Optional[str]: + """ + Optional action suppression wait period for the Alarm rule in case of composite alarm. + """ + return self.get("actionsSuppressorWaitPeriod", None) + + @property + def alarm_actions_suppressor_extension_period(self) -> Optional[str]: + """ + Optional action suppression extension period for the Alarm rule in case of composite alarm. + """ + return self.get("actionsSuppressorExtensionPeriod", None) + + @property + def metrics(self) -> Optional[List[CloudWatchAlarmMetric]]: + """ + The metrics evaluated for the Alarm. + """ + metrics = self.get("metrics") + return [CloudWatchAlarmMetric(i) for i in metrics] if metrics else None + + +class CloudWatchAlarmEvent(DictWrapper): + @property + def source(self) -> Literal["aws.cloudwatch"]: + """ + Source of the triggered event. + """ + return self["source"] + + @property + def alarm_arn(self) -> str: + """ + The ARN of the CloudWatch Alarm. + """ + return self["alarmArn"] + + @property + def region(self) -> str: + """ + The AWS region in which the Alarm is active. + """ + return self["region"] + + @property + def source_account_id(self) -> str: + """ + The AWS Account ID that the Alarm is deployed to. + """ + return self["accountId"] + + @property + def timestamp(self) -> str: + """ + Alarm state change event timestamp in ISO-8601 format. + """ + return self["time"] + + @property + def alarm_data(self) -> CloudWatchAlarmData: + """ + Contains basic data about the Alarm and its current and previous states. + """ + return CloudWatchAlarmData(self["alarmData"]) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 97b7a5dfda2..45c9ccd9869 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -74,39 +74,40 @@ Log Data Event for Troubleshooting ## Supported event sources -| Event Source | Data_class | -|---------------------------------------------------------------------------|----------------------------------------------------| -| [Active MQ](#active-mq) | `ActiveMQEvent` | -| [API Gateway Authorizer](#api-gateway-authorizer) | `APIGatewayAuthorizerRequestEvent` | -| [API Gateway Authorizer V2](#api-gateway-authorizer-v2) | `APIGatewayAuthorizerEventV2` | -| [API Gateway Proxy](#api-gateway-proxy) | `APIGatewayProxyEvent` | -| [API Gateway Proxy V2](#api-gateway-proxy-v2) | `APIGatewayProxyEventV2` | -| [Application Load Balancer](#application-load-balancer) | `ALBEvent` | -| [AppSync Authorizer](#appsync-authorizer) | `AppSyncAuthorizerEvent` | -| [AppSync Resolver](#appsync-resolver) | `AppSyncResolverEvent` | -| [AWS Config Rule](#aws-config-rule) | `AWSConfigRuleEvent` | -| [Bedrock Agent](#bedrock-agent) | `BedrockAgent` | -| [CloudWatch Dashboard Custom Widget](#cloudwatch-dashboard-custom-widget) | `CloudWatchDashboardCustomWidgetEvent` | -| [CloudWatch Logs](#cloudwatch-logs) | `CloudWatchLogsEvent` | -| [CodePipeline Job Event](#codepipeline-job) | `CodePipelineJobEvent` | -| [Cognito User Pool](#cognito-user-pool) | Multiple available under `cognito_user_pool_event` | -| [Connect Contact Flow](#connect-contact-flow) | `ConnectContactFlowEvent` | -| [DynamoDB streams](#dynamodb-streams) | `DynamoDBStreamEvent`, `DynamoDBRecordEventName` | -| [EventBridge](#eventbridge) | `EventBridgeEvent` | -| [Kafka](#kafka) | `KafkaEvent` | -| [Kinesis Data Stream](#kinesis-streams) | `KinesisStreamEvent` | -| [Kinesis Firehose Delivery Stream](#kinesis-firehose-delivery-stream) | `KinesisFirehoseEvent` | -| [Lambda Function URL](#lambda-function-url) | `LambdaFunctionUrlEvent` | -| [Rabbit MQ](#rabbit-mq) | `RabbitMQEvent` | -| [S3](#s3) | `S3Event` | -| [S3 Batch Operations](#s3-batch-operations) | `S3BatchOperationEvent` | -| [S3 Object Lambda](#s3-object-lambda) | `S3ObjectLambdaEvent` | -| [S3 EventBridge Notification](#s3-eventbridge-notification) | `S3EventBridgeNotificationEvent` | -| [SES](#ses) | `SESEvent` | -| [SNS](#sns) | `SNSEvent` | -| [SQS](#sqs) | `SQSEvent` | -| [VPC Lattice V2](#vpc-lattice-v2) | `VPCLatticeV2Event` | -| [VPC Lattice V1](#vpc-lattice-v1) | `VPCLatticeEvent` | +| Event Source | Data_class | +|-------------------------------------------------------------------------------|----------------------------------------------------| +| [Active MQ](#active-mq) | `ActiveMQEvent` | +| [API Gateway Authorizer](#api-gateway-authorizer) | `APIGatewayAuthorizerRequestEvent` | +| [API Gateway Authorizer V2](#api-gateway-authorizer-v2) | `APIGatewayAuthorizerEventV2` | +| [API Gateway Proxy](#api-gateway-proxy) | `APIGatewayProxyEvent` | +| [API Gateway Proxy V2](#api-gateway-proxy-v2) | `APIGatewayProxyEventV2` | +| [Application Load Balancer](#application-load-balancer) | `ALBEvent` | +| [AppSync Authorizer](#appsync-authorizer) | `AppSyncAuthorizerEvent` | +| [AppSync Resolver](#appsync-resolver) | `AppSyncResolverEvent` | +| [AWS Config Rule](#aws-config-rule) | `AWSConfigRuleEvent` | +| [Bedrock Agent](#bedrock-agent) | `BedrockAgent` | +| [CloudWatch Alarm State Change Action](#cloudwatch-alarm-state-change-action) | `CloudWatchAlarmEvent` | +| [CloudWatch Dashboard Custom Widget](#cloudwatch-dashboard-custom-widget) | `CloudWatchDashboardCustomWidgetEvent` | +| [CloudWatch Logs](#cloudwatch-logs) | `CloudWatchLogsEvent` | +| [CodePipeline Job Event](#codepipeline-job) | `CodePipelineJobEvent` | +| [Cognito User Pool](#cognito-user-pool) | Multiple available under `cognito_user_pool_event` | +| [Connect Contact Flow](#connect-contact-flow) | `ConnectContactFlowEvent` | +| [DynamoDB streams](#dynamodb-streams) | `DynamoDBStreamEvent`, `DynamoDBRecordEventName` | +| [EventBridge](#eventbridge) | `EventBridgeEvent` | +| [Kafka](#kafka) | `KafkaEvent` | +| [Kinesis Data Stream](#kinesis-streams) | `KinesisStreamEvent` | +| [Kinesis Firehose Delivery Stream](#kinesis-firehose-delivery-stream) | `KinesisFirehoseEvent` | +| [Lambda Function URL](#lambda-function-url) | `LambdaFunctionUrlEvent` | +| [Rabbit MQ](#rabbit-mq) | `RabbitMQEvent` | +| [S3](#s3) | `S3Event` | +| [S3 Batch Operations](#s3-batch-operations) | `S3BatchOperationEvent` | +| [S3 Object Lambda](#s3-object-lambda) | `S3ObjectLambdaEvent` | +| [S3 EventBridge Notification](#s3-eventbridge-notification) | `S3EventBridgeNotificationEvent` | +| [SES](#ses) | `SESEvent` | +| [SNS](#sns) | `SNSEvent` | +| [SQS](#sqs) | `SQSEvent` | +| [VPC Lattice V2](#vpc-lattice-v2) | `VPCLatticeV2Event` | +| [VPC Lattice V1](#vpc-lattice-v1) | `VPCLatticeEvent` | ???+ info The examples provided below are far from exhaustive - the data classes themselves are designed to provide a form of @@ -528,6 +529,17 @@ In this example, we also use the new Logger `correlation_id` and built-in `corre return { "markdown": f"# {echo}" } ``` +### CloudWatch Alarm State Change Action + +[CloudWatch supports Lambda as an alarm state change action](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarms-and-actions){target="_blank"}. +You can use the `CloudWathAlarmEvent` data class to access the fields containing such data as alarm information, current state, and previous state. + +=== "app.py" + + ```python hl_lines="2 8" + --8<-- "examples/event_sources/src/cloudwatch_alarm_event.py" + ``` + ### CloudWatch Logs CloudWatch Logs events by default are compressed and base64 encoded. You can use the helper function provided to decode, diff --git a/examples/event_sources/src/cloudwatch_alarm_event.py b/examples/event_sources/src/cloudwatch_alarm_event.py new file mode 100644 index 00000000000..503c25ef0b0 --- /dev/null +++ b/examples/event_sources/src/cloudwatch_alarm_event.py @@ -0,0 +1,19 @@ +from aws_lambda_powertools import Logger +from aws_lambda_powertools.utilities.data_classes import CloudWatchAlarmEvent, event_source +from aws_lambda_powertools.utilities.typing import LambdaContext + +logger = Logger() + + +@event_source(data_class=CloudWatchAlarmEvent) +def lambda_handler(event: CloudWatchAlarmEvent, context: LambdaContext) -> dict: + logger.info(f"Alarm {event.alarm_data.alarm_name} state is {event.alarm_data.state.value}") + + # You can now work with event. For example, you can enrich the received data, and + # decide on how you want to route the alarm. + + return { + "name": event.alarm_data.alarm_name, + "arn": event.alarm_arn, + "urgent": "Priority: P1" in (event.alarm_data.configuration.description or ""), + } diff --git a/tests/events/cloudWatchAlarmEventCompositeMetric.json b/tests/events/cloudWatchAlarmEventCompositeMetric.json new file mode 100644 index 00000000000..67200c10edb --- /dev/null +++ b/tests/events/cloudWatchAlarmEventCompositeMetric.json @@ -0,0 +1,30 @@ +{ + "source":"aws.cloudwatch", + "alarmArn":"arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main", + "accountId":"111122223333", + "time":"2023-08-04T12:56:46.138+0000", + "region":"us-east-1", + "alarmData":{ + "alarmName":"CompositeDemo.Main", + "state":{ + "value":"ALARM", + "reason":"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", + "reasonData":"{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", + "timestamp":"2023-08-04T12:56:46.138+0000" + }, + "previousState":{ + "value":"ALARM", + "reason":"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", + "reasonData":"{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", + "timestamp":"2023-08-04T12:54:46.138+0000", + "actionsSuppressedBy":"WaitPeriod", + "actionsSuppressedReason":"Actions suppressed by WaitPeriod" + }, + "configuration":{ + "alarmRule":"ALARM(CompositeDemo.FirstChild) OR ALARM(CompositeDemo.SecondChild)", + "actionsSuppressor":"CompositeDemo.ActionsSuppressor", + "actionsSuppressorWaitPeriod":120, + "actionsSuppressorExtensionPeriod":180 + } + } +} diff --git a/tests/events/cloudWatchAlarmEventSingleMetric.json b/tests/events/cloudWatchAlarmEventSingleMetric.json new file mode 100644 index 00000000000..fa5089cd6b5 --- /dev/null +++ b/tests/events/cloudWatchAlarmEventSingleMetric.json @@ -0,0 +1,59 @@ +{ + "source": "aws.cloudwatch", + "alarmArn": "arn:aws:cloudwatch:eu-west-1:912397435824:alarm:test_alarm", + "accountId": "123456789012", + "time": "2024-02-17T11:53:08.431+0000", + "region": "eu-west-1", + "alarmData": { + "alarmName": "Test alert", + "state": { + "value": "ALARM", + "reason": "Threshold Crossed: 1 out of the last 1 datapoints [1.0 (17/02/24 11:51:00)] was less than the threshold (10.0) (minimum 1 datapoint for OK -> ALARM transition).", + "reasonData": "{\"version\":\"1.0\",\"queryDate\":\"2024-02-17T11:53:08.423+0000\",\"startDate\":\"2024-02-17T11:51:00.000+0000\",\"statistic\":\"SampleCount\",\"period\":60,\"recentDatapoints\":[1.0],\"threshold\":10.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2024-02-17T11:51:00.000+0000\",\"sampleCount\":1.0,\"value\":1.0}]}", + "timestamp": "2024-02-17T11:53:08.431+0000" + }, + "previousState": { + "value": "OK", + "reason": "Threshold Crossed: 1 out of the last 1 datapoints [1.0 (17/02/24 11:50:00)] was not greater than the threshold (10.0) (minimum 1 datapoint for ALARM -> OK transition).", + "reasonData": "{\"version\":\"1.0\",\"queryDate\":\"2024-02-17T11:51:31.460+0000\",\"startDate\":\"2024-02-17T11:50:00.000+0000\",\"statistic\":\"SampleCount\",\"period\":60,\"recentDatapoints\":[1.0],\"threshold\":10.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2024-02-17T11:50:00.000+0000\",\"sampleCount\":1.0,\"value\":1.0}]}", + "timestamp": "2024-02-17T11:51:31.462+0000" + }, + "configuration": { + "description": "This is description **here**", + "metrics": [ + { + "id": "e1", + "expression": "m1/m2", + "label": "Expression1", + "returnData": true + }, + { + "id": "m1", + "metricStat": { + "metric": { + "namespace": "AWS/Lambda", + "name": "Invocations", + "dimensions": {} + }, + "period": 60, + "stat": "SampleCount" + }, + "returnData": false + }, + { + "id": "m2", + "metricStat": { + "metric": { + "namespace": "AWS/Lambda", + "name": "Duration", + "dimensions": {} + }, + "period": 60, + "stat": "SampleCount" + }, + "returnData": false + } + ] + } + } +} diff --git a/tests/unit/data_classes/test_cloud_watch_alarm_event.py b/tests/unit/data_classes/test_cloud_watch_alarm_event.py new file mode 100644 index 00000000000..56933a1505d --- /dev/null +++ b/tests/unit/data_classes/test_cloud_watch_alarm_event.py @@ -0,0 +1,104 @@ +import json +from typing import Dict, List + +from aws_lambda_powertools.utilities.data_classes import CloudWatchAlarmEvent +from tests.functional.utils import load_event + + +def test_cloud_watch_alarm_event_single_metric(): + raw_event = load_event("cloudWatchAlarmEventSingleMetric.json") + parsed_event = CloudWatchAlarmEvent(raw_event) + + assert parsed_event.source == raw_event["source"] + assert parsed_event.region == raw_event["region"] + assert parsed_event.alarm_arn == raw_event["alarmArn"] + assert parsed_event.alarm_data.alarm_name == raw_event["alarmData"]["alarmName"] + + assert parsed_event.alarm_data.state.value == raw_event["alarmData"]["state"]["value"] + assert parsed_event.alarm_data.state.reason == raw_event["alarmData"]["state"]["reason"] + assert parsed_event.alarm_data.state.reason_data == raw_event["alarmData"]["state"]["reasonData"] + assert parsed_event.alarm_data.state.reason_data_decoded == json.loads( + raw_event["alarmData"]["state"]["reasonData"], + ) + assert parsed_event.alarm_data.state.timestamp == raw_event["alarmData"]["state"]["timestamp"] + + assert parsed_event.alarm_data.previous_state.value == raw_event["alarmData"]["previousState"]["value"] + assert parsed_event.alarm_data.previous_state.reason == raw_event["alarmData"]["previousState"]["reason"] + assert parsed_event.alarm_data.previous_state.reason_data == raw_event["alarmData"]["previousState"]["reasonData"] + assert parsed_event.alarm_data.previous_state.reason_data_decoded == json.loads( + raw_event["alarmData"]["previousState"]["reasonData"], + ) + assert parsed_event.alarm_data.previous_state.timestamp == raw_event["alarmData"]["previousState"]["timestamp"] + + assert parsed_event.alarm_data.configuration.description == raw_event["alarmData"]["configuration"]["description"] + assert parsed_event.alarm_data.configuration.alarm_rule is None + assert parsed_event.alarm_data.configuration.alarm_actions_suppressor is None + assert parsed_event.alarm_data.configuration.alarm_actions_suppressor_extension_period is None + assert parsed_event.alarm_data.configuration.alarm_actions_suppressor_wait_period is None + + assert isinstance(parsed_event.alarm_data.configuration.metrics, List) + # metric position 0 + metric_0 = parsed_event.alarm_data.configuration.metrics[0] + raw_metric_0 = raw_event["alarmData"]["configuration"]["metrics"][0] + assert metric_0.metric_id == raw_metric_0["id"] + assert metric_0.expression == raw_metric_0["expression"] + assert metric_0.label == raw_metric_0["label"] + assert metric_0.return_data == raw_metric_0["returnData"] + + # metric position 1 + metric_1 = parsed_event.alarm_data.configuration.metrics[1] + raw_metric_1 = raw_event["alarmData"]["configuration"]["metrics"][1] + assert metric_1.metric_id == raw_metric_1["id"] + assert metric_1.return_data == raw_metric_1["returnData"] + assert metric_1.metric_stat.stat == raw_metric_1["metricStat"]["stat"] + assert metric_1.metric_stat.period == raw_metric_1["metricStat"]["period"] + assert metric_1.metric_stat.unit is None + assert isinstance(metric_1.metric_stat.metric, Dict) + + +def test_cloud_watch_alarm_event_composite_metric(): + raw_event = load_event("cloudWatchAlarmEventCompositeMetric.json") + parsed_event = CloudWatchAlarmEvent(raw_event) + + assert parsed_event.source == raw_event["source"] + assert parsed_event.region == raw_event["region"] + assert parsed_event.alarm_arn == raw_event["alarmArn"] + assert parsed_event.alarm_data.alarm_name == raw_event["alarmData"]["alarmName"] + + assert parsed_event.alarm_data.state.value == raw_event["alarmData"]["state"]["value"] + assert parsed_event.alarm_data.state.reason == raw_event["alarmData"]["state"]["reason"] + assert parsed_event.alarm_data.state.reason_data == raw_event["alarmData"]["state"]["reasonData"] + assert parsed_event.alarm_data.state.reason_data_decoded == json.loads( + raw_event["alarmData"]["state"]["reasonData"], + ) + assert parsed_event.alarm_data.state.timestamp == raw_event["alarmData"]["state"]["timestamp"] + + assert parsed_event.alarm_data.previous_state.value == raw_event["alarmData"]["previousState"]["value"] + assert parsed_event.alarm_data.previous_state.reason == raw_event["alarmData"]["previousState"]["reason"] + assert parsed_event.alarm_data.previous_state.reason_data == raw_event["alarmData"]["previousState"]["reasonData"] + assert parsed_event.alarm_data.previous_state.reason_data_decoded == json.loads( + raw_event["alarmData"]["previousState"]["reasonData"], + ) + assert parsed_event.alarm_data.previous_state.timestamp == raw_event["alarmData"]["previousState"]["timestamp"] + assert ( + parsed_event.alarm_data.previous_state.actions_suppressed_by + == raw_event["alarmData"]["previousState"]["actionsSuppressedBy"] + ) + assert ( + parsed_event.alarm_data.previous_state.actions_suppressed_reason + == raw_event["alarmData"]["previousState"]["actionsSuppressedReason"] + ) + + assert parsed_event.alarm_data.configuration.alarm_rule == raw_event["alarmData"]["configuration"]["alarmRule"] + assert ( + parsed_event.alarm_data.configuration.alarm_actions_suppressor_wait_period + == raw_event["alarmData"]["configuration"]["actionsSuppressorWaitPeriod"] + ) + assert ( + parsed_event.alarm_data.configuration.alarm_actions_suppressor_extension_period + == raw_event["alarmData"]["configuration"]["actionsSuppressorExtensionPeriod"] + ) + assert ( + parsed_event.alarm_data.configuration.alarm_actions_suppressor + == raw_event["alarmData"]["configuration"]["actionsSuppressor"] + )