# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify,
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
#
# The purpose of this template is to create two IoT Rules in your AWS Account:
# - <stack name>_UpdateShadowWithLoRaWANPayload_<binary decoder name>
# - <stack name>_UpdateShadowWithLoRaWANPayload_MapThingName_<binary decoder name>

# Both rules will update a shadow of an AWS IoT Thing with a decoded payload from a LoRaWAN device. The
# difference between both rules is how they detect the name of the AWS IoT Thing for the shadow update:
#
#  - The rule "<stack name>_UpdateShadowWithLoRaWANPayload_<binary decoder name>" will update a
# shadow of AWS IoT Thing with the name derived from the value of the attribute "WirelessDeviceId"
#
#  - The rule "<stack name>_UpdateShadowWithLoRaWANPayload_MapThingName_<binary decoder name>" allows
# customization of the logic do derive a Thing name throught AWS Lambda function. An example Lambda function
# in this sample will perform a lookup in the IoT Registry based on the attribute name of the thing.

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  Sample for publishing the telemetry and transmission metadata from a LoRaWAN device to the shadow of the AWS IoT Thing                                                                         

#  ██████   █████  ██████   █████  ███    ███ ███████ ████████ ███████ ██████  ███████ 
#  ██   ██ ██   ██ ██   ██ ██   ██ ████  ████ ██         ██    ██      ██   ██ ██      
#  ██████  ███████ ██████  ███████ ██ ████ ██ █████      ██    █████   ██████  ███████ 
#  ██      ██   ██ ██   ██ ██   ██ ██  ██  ██ ██         ██    ██      ██   ██      ██ 
#  ██      ██   ██ ██   ██ ██   ██ ██      ██ ███████    ██    ███████ ██   ██ ███████    
#                                                                                      
Parameters:
  # Name of binary decode to transform incoming binary LoRaWAN Payloads to JSON.
  # For the demonstration purposes you can use the included sample decoder which will generate
  # the payload like this:
  # {
  #    temperature": 26.55 -> random number
  #    "humidity": 42.44, -> random number
  #    "input_length": 11 -> length of payload received from a LoRaWAN device
  #    "input_hex": "CBC4091501700109027FFF" -> payload received from a LoRaWAN device
  # }
  # Please review src-iotrule-transformation/app.py for instructions on how to add
  # a binary decoder for your LoRaWAN device.
  ParamBinaryDecoderName:
    Type: String
    Default: sample_device
    Description: Name of binary decoder as configured in src-iotrule-transformation/app.py

  # The IoT Rule will publish error messages on the topic configured below.
  TopicOutgoingErrors:
    Type: String
    Default: iotthingshadowsample_error
    Description: Topic for errors

  # The IoT Rule will publish debug messages on the topic configured below.
  TopicOutgoingDebug:
    Type: String
    Default: iotthingshadowsample_debug
    Description: Name of topic to publish debug messages to

  # The parameter below specifies a name of the IoT Thing shadow to update
  ParamShadowName:
    Type: String
    Default: LoRaWANTelemetry

  # The attribute according to the JSON path specified below will be used to
  # detect the name of the IoT Thing for shadow update.
  # Example: if you specify "WirelessDeviceId" and the incoming payload is
  # {"WirelessDeviceId": # "8b00de4a-0fac-407b-93e6-8c59fd411f16",..."}
  # , the shadow of the AWS IoT Thing with the name "8b00de4a-0fac-407b-93e6-8c59fd411f16" will be updated.
  ParamShadowThingNamePath:
    Type: String
    Default: WirelessDeviceId
    AllowedValues:
      - WirelessDeviceId
      - WirelessMetadata.LoRaWAN.DevEui
    Description: JSON path to retrieve the name of the shadow IoT Thing

#                                                                                      
#  ██████  ███████ ███████  ██████  ██    ██ ██████   ██████ ███████ ███████ 
#  ██   ██ ██      ██      ██    ██ ██    ██ ██   ██ ██      ██      ██      
#  ██████  █████   ███████ ██    ██ ██    ██ ██████  ██      █████   ███████ 
#  ██   ██ ██           ██ ██    ██ ██    ██ ██   ██ ██      ██           ██ 
#  ██   ██ ███████ ███████  ██████   ██████  ██   ██  ██████ ███████ ███████ 
#                                                                            

Resources:
  ############################################################################################
  # This AWS IoT Rule rule will update a shadow of an AWS IoT Thing with a decoded payload from a LoRaWAN
  #  device. It  will use of attributes  of the incoming payload message (e.g. value of attribute
  # WirelessDeviceId) as a Thing name
  ############################################################################################

  UpdateShadowWithLoRaWANPayloadRule:
    Type: "AWS::IoT::TopicRule"
    Properties:
      RuleName: !Sub "${AWS::StackName}_UpdateShadowWithLoRaWANPayload_${ParamBinaryDecoderName}"
      TopicRulePayload:
        AwsIotSqlVersion: "2016-03-23"
        RuleDisabled: false

        Sql: !Sub
          - |
            SELECT aws_lambda("${LambdaARN}",
                              {"PayloadDecoderName": "${ParamBinaryDecoderName}", 
                              "PayloadData":PayloadData, 
                              "WirelessDeviceId": WirelessDeviceId, 
                               "WirelessMetadata": WirelessMetadata}) as state.reported

          - { LambdaARN: !GetAtt TransformLoRaWANBinaryPayloadFunction.Arn }
        Actions:
          - Republish:
              RoleArn: !GetAtt UpdateShadowWithLoRaWANPayloadRuleActionRole.Arn
              Topic:
                !Join [
                  "",
                  [
                    "$$aws/things/${",
                    !Ref ParamShadowThingNamePath,
                    "}/shadow/name/",
                    !Ref ParamShadowName,
                    "/update",
                  ],
                ]
              Qos: 0
          - Republish:
              RoleArn: !GetAtt UpdateShadowWithLoRaWANPayloadRuleActionRole.Arn
              Topic: !Join ["", [!Ref TopicOutgoingDebug]]
              Qos: 0

        ErrorAction:
          Republish:
            RoleArn: !GetAtt UpdateShadowWithLoRaWANPayloadRuleActionRole.Arn
            Topic: !Ref TopicOutgoingErrors
            Qos: 0

  # Permission for AWS IoT Rule action
  UpdateShadowWithLoRaWANPayloadRuleActionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - iot.amazonaws.com
            Action:
              - "sts:AssumeRole"
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: iot:Publish
                Resource:
                  !Join [
                    "",
                    [
                      "arn:aws:iot:",
                      !Ref "AWS::Region",
                      ":",
                      !Ref "AWS::AccountId",
                      ":topic/*",
                    ],
                  ]

  ############################################################################################
  # This AWS IoT Rule rule will update a shadow of an AWS IoT Thing with a decoded payload from a LoRaWAN
  #  device. It allows  customization of the logic do derive a Thing name throught AWS Lambda function. An
  # example Lambda function. In this sample will perform a lookup in the IoT Registry based on the attribute # name of the thing.
  ############################################################################################
  UpdateShadowWithLoRaWANPayloadMappedThingNameRule:
    Type: "AWS::IoT::TopicRule"
    Properties:
      RuleName: !Sub "${AWS::StackName}_UpdateShadowWithLoRaWANPayload_MapThingName_${ParamBinaryDecoderName}"
      TopicRulePayload:
        AwsIotSqlVersion: "2016-03-23"
        RuleDisabled: false

        Sql: !Sub
          - |
            SELECT aws_lambda("${LambdaARN}",
                              {"PayloadDecoderName": "${ParamBinaryDecoderName}", 
                              "PayloadData":PayloadData, 
                              "WirelessDeviceId": WirelessDeviceId, 
                               "WirelessMetadata": WirelessMetadata}) as state.reported

          - { LambdaARN: !GetAtt TransformLoRaWANBinaryPayloadFunction.Arn }
        Actions:
          - Republish:
              RoleArn: !GetAtt UpdateShadowWithLoRaWANPayloadRuleActionRole.Arn
              Topic:
                !Join [
                  "",
                  [
                    "$$aws/things/${",
                    "aws_lambda(",
                    '"',
                    !GetAtt MapThingNameFunction.Arn,
                    '"',
                    ", {'searchvalue': WirelessDeviceId, 'searchtype': 'ASSOCIATED_THING'}).ThingName",
                    "}/shadow/name/",
                    !Ref ParamShadowName,
                    "/update",
                  ],
                ]
              Qos: 0
          - Republish:
              RoleArn: !GetAtt UpdateShadowWithLoRaWANPayloadRuleActionRole.Arn
              Topic: !Join ["", [!Ref TopicOutgoingDebug]]
              Qos: 0

        ErrorAction:
          Republish:
            RoleArn: !GetAtt UpdateShadowWithLoRaWANPayloadRuleActionRole.Arn
            Topic: !Ref TopicOutgoingErrors
            Qos: 0

  #########################################################################################################
  # AWS Lambda function for mapping of the message payload to the name of AWS IoT Thing for shadow update #
  #########################################################################################################
  MapThingNameFunction:
    Type: AWS::Serverless::Function
    Name: !Sub "${AWS::StackName}-MapThingNameFunction"
    Properties:
      CodeUri: src-mapthingname
      Handler: app.lambda_handler
      Runtime: python3.7
      Timeout: 10
      Environment:
        Variables:
          # Allowed valeu for SEARCH_TYPE:
          # ASSOCIATED_THING: Lookup the Thing associated to WirelessDeviceId (https://docs.aws.amazon.com/iot-wireless/2020-11-22/apireference/API_AssociateWirelessDeviceWithThing.html)
          # THING_INDEX: Lookup the Thing by search of IoT Registry index. If using THING_INDEX, also specify parameter SEARCH_THING_ATTRIBUTENAME to define name of an attribute for matching in thing index,
          # for example:
          # SEARCH_THING_ATTRIBUTENAME: WirelessDeviceId
          SEARCH_TYPE: ASSOCIATED_THING
      Policies:
        - Statement:
            - Sid: AllowSearchInIotIndex
              Effect: Allow
              Action: iot:SearchIndex
              Resource:
                !Join [
                  "",
                  [
                    "arn:aws:iot:",
                    !Ref "AWS::Region",
                    ":",
                    !Ref "AWS::AccountId",
                    ":index/AWS_Things",
                  ],
                ]

            - Sid: AllowGetWirelessDevice
              Effect: Allow
              Action: iotwireless:GetWirelessDevice
              Resource:
                !Join [
                  "",
                  [
                    "arn:aws:iotwireless:",
                    !Ref "AWS::Region",
                    ":",
                    !Ref "AWS::AccountId",
                    ":WirelessDevice/*",
                  ],
                ]
            - Sid: AllowDescribeThibg
              Effect: Allow
              Action: iot:DescribeThing
              Resource:
                !Join [
                  "",
                  [
                    "arn:aws:iot:",
                    !Ref "AWS::Region",
                    ":",
                    !Ref "AWS::AccountId",
                    ":thing/*",
                  ],
                ]

  # Provide AWS IoT a permission to invoke the lambda function
  MapThingNameFunctionInvocationPermission:
    Type: AWS::Lambda::Permission
    Properties:
      SourceArn:
        !Join [
          "",
          [
            "arn:aws:iot:",
            !Ref "AWS::Region",
            ":",
            !Ref "AWS::AccountId",
            ":rule/",
            !Ref UpdateShadowWithLoRaWANPayloadMappedThingNameRule,
          ],
        ]
      Action: lambda:InvokeFunction
      Principal: iot.amazonaws.com
      FunctionName: !Ref MapThingNameFunction

  ############################################################################################
  # AWS Lambda function for binary decoding. This function will refer to
  # layer "DecoderLayer" to include the necessary decoding libraries
  ############################################################################################

  TransformLoRaWANBinaryPayloadFunction:
    Type: AWS::Serverless::Function
    Name: !Sub "${AWS::StackName}-TransformLoRaWANBinaryPayloadFunction"
    Properties:
      CodeUri: src-iotrule-transformation
      Handler: app.lambda_handler
      Runtime: python3.7
      Layers:
        - Ref: LoRaWANPayloadDecoderLayer
      Environment:
        Variables:
          RETURN_RAW_DATA: True

  # Provide AWS IoT a permission to invoke the lambda function
  TransformLoRaWANBinaryPayloadFunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt TransformLoRaWANBinaryPayloadFunction.Arn
      Action: lambda:InvokeFunction
      Principal: iot.amazonaws.com

  ############################################################################################
  # AWS Lambda layer with decoders
  ############################################################################################
  LoRaWANPayloadDecoderLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: !Sub "${AWS::StackName}-LoRaWANPayloadDecoderLayer"
      Description: Payload decoders for LoRaWAN devices
      ContentUri: src-payload-decoders
      CompatibleRuntimes:
        - python3.8
      RetentionPolicy: Retain

#   ██████  ██    ██ ████████ ██████  ██    ██ ████████ ███████ 
#  ██    ██ ██    ██    ██    ██   ██ ██    ██    ██    ██      
#  ██    ██ ██    ██    ██    ██████  ██    ██    ██    ███████ 
#  ██    ██ ██    ██    ██    ██      ██    ██    ██         ██ 
#   ██████   ██████     ██    ██       ██████     ██    ███████ 
#                                                               

Outputs:
  UpdateShadowWithLoRaWANPayloadRuleName:
    Description: >
      Option 1: Please use the value below to configure the Destination in AWS IoT Core for LoRaWAN. This rule will  will update a shadow of an AWS IoT Thing with the name derived from the value of the attribute WirelessDeviceId. For example, if incoming payload is {"WirelessDeviceId": # "8b00de4a-0fac-407b-93e6-8c59fd411f16",..."}, the shadow of the AWS IoT Thing with the name "8b00de4a-0fac-407b-93e6-8c59fd411f16" will be updated with a decoded payload.

      To test this rule, please publish the payload below to the topic

    Value: !Ref UpdateShadowWithLoRaWANPayloadRule

  UpdateShadowWithLoRaWANPayloadMappedThingNameRuleName:
    Description: >
      Option 2: please use the value below to configure the Destination in AWS IoT Core for LoRaWAN. This rule will  will update a shadow of an AWS IoT Thing with the name calculated by the AWS Lambda function <stack name>-MapThingNameFunction". For example, if incoming payload is {"WirelessDeviceId": # "8b00de4a-0fac-407b-93e6-8c59fd411f16",..."}, the Lambda function will make a lookup in the AWS registry for a Thing with an attribue 'WirelessDeviceId=8b00de4a-0fac-407b-93e6-8c59fd411f16'. If it finds such a thing, the shadow of this thing will be updated with a decoded payload.

    Value: !Ref UpdateShadowWithLoRaWANPayloadMappedThingNameRule