1"""FastAPI connector for handling `/set-config` and `/get-value` endpoints.
3This module provides endpoints to set configuration and retrieve values using JSON payloads.
10from typing
import cast
13from aas_standard_parser
import collection_helpers
14from basyx.aas.model
import Submodel, SubmodelElementCollection
15from fastapi
import FastAPI, HTTPException
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
28logger = logging.getLogger(__name__)
31connector_store: dict[str, IAssetConnector] = {}
32connector_store_lock = threading.Lock()
36async def root() -> ResponseBody:
37 """Root endpoint that returns available endpoints."""
39 return create_response(status_code=200, message=
"Available endpoints are `/add-config` and `/get-value`", payload=
None)
42@app.post("/add-config")
44 """Set or add configuration using an AID submodel.
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.
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.
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.
58 aid_sm: Submodel = payload._aid_sm
60 logger.info(f
"Received `/set-config` request with AID submodel ID: {aid_sm.id}")
66 for iface_smc
in aid_sm.submodel_element:
67 asset_connector: IAssetConnector =
None
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")
73 if collection_helpers.contains_supplemental_semantic_id(iface_smc,
"http://www.w3.org/2011/mqtt"):
74 logger.info(
"Creating MQTT AssetConnector")
77 elif collection_helpers.contains_supplemental_semantic_id(iface_smc,
"http://www.w3.org/2011/opcua"):
78 logger.info(
"Creating OPC UA AssetConnector")
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))
88 f
"Unsupported protocol '{iface_smc.supplemental_semantic_id[0].key[0].value}' in interface submodel '{iface_smc.id_short}'."
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
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}")
103 if asset_connector.is_connected:
104 logger.debug(f
"Successfully connected AssetConnector to '{asset_connector.base}'")
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")
109 except Exception
as e:
110 raise HTTPException(status_code=500, detail=f
"Internal server error: {e}")
from e
113 with connector_store_lock:
114 connected = [connector_id
for connector_id, asset_connector
in connector_store.items()
if asset_connector.is_connected]
116 if len(connected) == 0:
117 errors: str =
"; ".join(error_messages)
118 raise HTTPException(status_code=500, detail=errors)
120 logger.debug(f
"Connected AssetConnectors after `/set-config`: {len(connected)}")
122 return create_response(
124 message=
"Successfully invoked `/set-config` with raw JSON in payload",
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.
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.
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.
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(
151 message=f
"No AssetConnector found for AID (decoded: {connector_id})",
155 result = await asset_connector.get_value(payload._aid_ref)
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
159 except Exception
as e:
160 return create_response(
162 message=f
"Error processing `/get-value`: {e!s}",
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.
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.
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.
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(
188 message=f
"No AssetConnector found for AID (decoded: {connector_id})",
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(
197 message=f
"Error processing `/set-value`: {e!s}",
203 """Function to start the FastAPI application.
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`
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)
Class to connect to an asset using its AID.
Class to connect to an asset using its AID.
Class to connect to an asset using its AID.
ResponseBody get_value(GetValuePayload payload)
Get a current reading from a protocol-specific endpoint of the asset as specified in the AID submodel...
ResponseBody root()
Root endpoint that returns available endpoints.
ResponseBody set_value(SetValuePayload payload)
Set a new value to a protocol-specific endpoint of the asset as specified in the AID submodel.
start_app()
Function to start the FastAPI application.
ResponseBody add_or_update_config(SetConfigPayload payload)
Set or add configuration using an AID submodel.