Asset Connector Documentation
Loading...
Searching...
No Matches
main.py
Go to the documentation of this file.
1"""FastAPI connector for handling `/set-config` and `/get-value` endpoints.
2
3This module provides endpoints to set configuration and retrieve values using JSON payloads.
4"""
5
6import json
7import logging
8import os
9import threading
10from typing import cast
11
12import uvicorn
13from aas_standard_parser import collection_helpers
14from basyx.aas.model import Submodel, SubmodelElementCollection
15from fastapi import FastAPI, HTTPException
16
17from .core.asset_connector import IAssetConnector
18from .http_connector.http_asset_connector import HttpAssetConnector
19from .models.get_value_payload import GetValuePayload
20from .models.response_body import ResponseBody, create_response
21from .models.set_config_payload import SetConfigPayload
22from .models.set_value_payload import SetValuePayload
23from .mqtt.mqtt_asset_connector import MqttAssetConnector
24from .opc_ua.opcua_asset_connector import OpcuaAssetConnector
25
26app = FastAPI()
27
28logger = logging.getLogger(__name__)
29
30# Store AssetConnector instances mapped by (submodel id + interface idshort) in thread-safe way
31connector_store: dict[str, IAssetConnector] = {}
32connector_store_lock = threading.Lock()
33
34
35@app.get("/")
36async def root() -> ResponseBody:
37 """Root endpoint that returns available endpoints."""
38
39 return create_response(status_code=200, message="Available endpoints are `/add-config` and `/get-value`", payload=None)
40
41
42@app.post("/add-config")
43async def add_or_update_config(payload: SetConfigPayload) -> ResponseBody:
44 """Set or add configuration using an AID submodel.
45
46 All interfaces defined in the provided AID will be analyzed and cached in an internal data structure.
47 An interface is defined by the unique combination of submodel ID (ID of the AID) and interface name (IDshort).
48 If an interface with the same AID-ID and name already exists, it will be overwritten.
49
50 If the protocol of the interface it supported by the implementation, an according co-routine will be added
51 to handle the communication with the asset via the specified protocol and endpoints.
52 Supported protocols are: MQTT, OPC UA, HTTP.
53
54 :param payload: JSON object that is parsed as `SetConfigPayload`
55 :return: JSON object that is parsed as `ResponseBody` containing a status code and confirmation.
56 """
57
58 aid_sm: Submodel = payload._aid_sm # noqa: SLF001
59
60 logger.info(f"Received `/set-config` request with AID submodel ID: {aid_sm.id}")
61
62 error_messages = []
63
64 try:
65 # iterate over all interface SMCs and create IAssetConnector for each of them
66 for iface_smc in aid_sm.submodel_element:
67 asset_connector: IAssetConnector = None
68
69 if iface_smc.supplemental_semantic_id is None or len(iface_smc.supplemental_semantic_id) == 0:
70 logger.warning(f"Skipping interface submodel '{iface_smc.id_short}' as it has no supplemental semantic ID")
71 continue
72
73 if collection_helpers.contains_supplemental_semantic_id(iface_smc, "http://www.w3.org/2011/mqtt"):
74 logger.info("Creating MQTT AssetConnector")
75 asset_connector = MqttAssetConnector(aid_sm.id, iface_smc)
76
77 elif collection_helpers.contains_supplemental_semantic_id(iface_smc, "http://www.w3.org/2011/opcua"):
78 logger.info("Creating OPC UA AssetConnector")
79 asset_connector = OpcuaAssetConnector(aid_sm.id, iface_smc)
80
81 elif collection_helpers.contains_supplemental_semantic_id(iface_smc, "http://www.w3.org/2011/http"):
82 logger.info("Creating HTTP AssetConnector")
83 asset_connector = HttpAssetConnector(aid_sm.id, cast(SubmodelElementCollection, iface_smc))
84 # TODO: check for other protocols
85
86 else:
87 logger.warning(
88 f"Unsupported protocol '{iface_smc.supplemental_semantic_id[0].key[0].value}' in interface submodel '{iface_smc.id_short}'."
89 )
90 continue
91
92 connector_id = f"{aid_sm.id}-{iface_smc.id_short}"
93 logger.debug(f"Storing AssetConnector with ID '{connector_id}'")
94 with connector_store_lock:
95 connector_store[connector_id] = asset_connector
96
97 try:
98 await asset_connector.connect()
99 except Exception as e:
100 logger.error(f"Failed to connect AssetConnector to '{asset_connector.base}': {e}")
101 error_messages.append(f"Failed to connect AssetConnector to '{asset_connector.base}': {e}")
102
103 if asset_connector.is_connected:
104 logger.debug(f"Successfully connected AssetConnector to '{asset_connector.base}'")
105 else:
106 logger.error(f"AssetConnector to '{asset_connector.base}' is not connected after connect attempt")
107 error_messages.append(f"AssetConnector to '{asset_connector.base}' is not connected after connect attempt")
108
109 except Exception as e:
110 raise HTTPException(status_code=500, detail=f"Internal server error: {e}") from e
111
112 # search not connected connectors
113 with connector_store_lock:
114 connected = [connector_id for connector_id, asset_connector in connector_store.items() if asset_connector.is_connected]
115
116 if len(connected) == 0:
117 errors: str = "; ".join(error_messages)
118 raise HTTPException(status_code=500, detail=errors)
119
120 logger.debug(f"Connected AssetConnectors after `/set-config`: {len(connected)}")
121
122 return create_response(
123 status_code=200,
124 message="Successfully invoked `/set-config` with raw JSON in payload",
125 payload=None,
126 value=connector_id,
127 )
128
129
130@app.post("/get-value")
131async def get_value(payload: GetValuePayload) -> ResponseBody:
132 """Get a current reading from a protocol-specific endpoint of the asset as specified in the AID submodel.
133
134 Using a cached AID submodel (more concisely, an interface definition in the AID),
135 a connection to the asset is established.
136 A current reading from the endpoint of the asset if obtained and returned.
137
138 :param payload: JSON object that is parsed as `GetValuePayload`
139 :return: JSON object that is parsed as `ResponseBody` containing a status code and the read value.
140 """
141
142 reference = payload._aid_ref
143 aid_id = reference.key[0].value
144 iface_smc = reference.key[1].value
145 connector_id = f"{aid_id}-{iface_smc}"
146 with connector_store_lock:
147 asset_connector = connector_store.get(connector_id)
148 if asset_connector is None:
149 return create_response(
150 status_code=404,
151 message=f"No AssetConnector found for AID (decoded: {connector_id})",
152 payload=None,
153 )
154 try:
155 result = await asset_connector.get_value(payload._aid_ref) # noqa: SLF001
156 return create_response(
157 status_code=200, message="Successfully invoked `/get-value` with raw JSON in payload", payload=json.loads(result) if result else None
158 )
159 except Exception as e:
160 return create_response(
161 status_code=500,
162 message=f"Error processing `/get-value`: {e!s}",
163 payload=None,
164 )
165
166
167@app.post("/set-value")
168async def set_value(payload: SetValuePayload) -> ResponseBody:
169 """Set a new value to a protocol-specific endpoint of the asset as specified in the AID submodel.
170
171 Using a cached AID submodel (more concisely, an interface definition in the AID),
172 a connection to the asset is established.
173 The provided value is written to the endpoint of the asset.
174
175 :param payload: JSON object that is parsed as `SetValuePayload`
176 :return: JSON object that is parsed as `ResponseBody` containing a status code and the read value.
177 """
178
179 reference = payload._aid_ref
180 aid_id = reference.key[0].value
181 iface_smc = reference.key[1].value
182 connector_id = f"{aid_id}-{iface_smc}"
183 with connector_store_lock:
184 asset_connector = connector_store.get(connector_id)
185 if asset_connector is None:
186 return create_response(
187 status_code=404,
188 message=f"No AssetConnector found for AID (decoded: {connector_id})",
189 payload=None,
190 )
191 try:
192 await asset_connector.set_value(reference, payload.value)
193 return create_response(status_code=200, message="Successfully invoked `/set-value` with raw JSON in payload", payload=None)
194 except Exception as e:
195 return create_response(
196 status_code=500,
197 message=f"Error processing `/set-value`: {e!s}",
198 payload=None,
199 )
200
201
202def start_app():
203 """Function to start the FastAPI application.
204
205 Will use the `APP_HOST` and `APP_PORT` environment variables to specify IP interface and port for listening.
206 Default: `0.0.0.0:8090`
207 Overwritten by Dockerfile: `0.0.0.0:8000`
208 """
209 host = os.getenv("APP_HOST", "0.0.0.0")
210 port = int(os.getenv("APP_PORT", "8090"))
211 uvicorn.run(app, host=host, port=port)
ResponseBody get_value(GetValuePayload payload)
Get a current reading from a protocol-specific endpoint of the asset as specified in the AID submodel...
Definition main.py:140
ResponseBody root()
Root endpoint that returns available endpoints.
Definition main.py:37
ResponseBody set_value(SetValuePayload payload)
Set a new value to a protocol-specific endpoint of the asset as specified in the AID submodel.
Definition main.py:177
start_app()
Function to start the FastAPI application.
Definition main.py:208
ResponseBody add_or_update_config(SetConfigPayload payload)
Set or add configuration using an AID submodel.
Definition main.py:56