juniconnlib-core 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- juniconnlib_core/__init__.py +81 -0
- juniconnlib_core/_version.py +34 -0
- juniconnlib_core/data_models/__init__.py +0 -0
- juniconnlib_core/data_models/basic_models.py +150 -0
- juniconnlib_core/data_models/fiware_models.py +277 -0
- juniconnlib_core/modules/__init__.py +0 -0
- juniconnlib_core/modules/devtools.py +106 -0
- juniconnlib_core/modules/fiware_iota_communicators.py +349 -0
- juniconnlib_core/modules/fiware_iota_device_provisioner.py +448 -0
- juniconnlib_core/modules/fiware_ocb_communicators.py +545 -0
- juniconnlib_core/modules/fiware_ocb_entity_provisioner.py +266 -0
- juniconnlib_core/modules/fiware_subscriptions.py +674 -0
- juniconnlib_core/modules/influxdb_writer.py +308 -0
- juniconnlib_core/modules/json_schema_parser.py +283 -0
- juniconnlib_core/utils/__init__.py +0 -0
- juniconnlib_core/utils/keycloak_iota.py +411 -0
- juniconnlib_core/utils/keycloak_oauth.py +168 -0
- juniconnlib_core/utils/keycloak_ocb.py +553 -0
- juniconnlib_core/utils/location_fiware_provisioner.py +690 -0
- juniconnlib_core/utils/logging.py +85 -0
- juniconnlib_core/utils/meta.py +27 -0
- juniconnlib_core/utils/mqtt.py +179 -0
- juniconnlib_core/utils/validators.py +60 -0
- juniconnlib_core-1.1.1.dist-info/METADATA +187 -0
- juniconnlib_core-1.1.1.dist-info/RECORD +28 -0
- juniconnlib_core-1.1.1.dist-info/WHEEL +5 -0
- juniconnlib_core-1.1.1.dist-info/licenses/LICENSE +14 -0
- juniconnlib_core-1.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from agentlib.utils.plugin_import import ModuleImport
|
|
2
|
+
|
|
3
|
+
import juniconnlib_core._version as package_version
|
|
4
|
+
from juniconnlib_core.modules import (
|
|
5
|
+
devtools,
|
|
6
|
+
fiware_iota_communicators,
|
|
7
|
+
fiware_iota_device_provisioner,
|
|
8
|
+
fiware_ocb_communicators,
|
|
9
|
+
fiware_ocb_entity_provisioner,
|
|
10
|
+
fiware_subscriptions,
|
|
11
|
+
influxdb_writer,
|
|
12
|
+
json_schema_parser,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
version = package_version.version
|
|
16
|
+
__version__ = package_version.__version__
|
|
17
|
+
__version_tuple__ = package_version.__version_tuple__
|
|
18
|
+
version_tuple = package_version.version_tuple
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"__version__",
|
|
22
|
+
"__version_tuple__",
|
|
23
|
+
"version",
|
|
24
|
+
"version_tuple",
|
|
25
|
+
"MODULE_TYPES",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# This property MODULE_TYPES has to exist with exactly this name, so that the plugin
|
|
30
|
+
# is usable in the agentlib
|
|
31
|
+
|
|
32
|
+
MODULE_TYPES = {
|
|
33
|
+
"json_schema_parser": ModuleImport(
|
|
34
|
+
import_path="juniconnlib_core.modules.json_schema_parser",
|
|
35
|
+
class_name=json_schema_parser.GeneralJsonSchemaParser.__name__,
|
|
36
|
+
),
|
|
37
|
+
"mqtt_communicator_send_to_iota": ModuleImport(
|
|
38
|
+
import_path="juniconnlib_core.modules.fiware_iota_communicators",
|
|
39
|
+
class_name=fiware_iota_communicators.MQTTCommunicatorSendToIoTA.__name__,
|
|
40
|
+
),
|
|
41
|
+
"mqtt_communicator_receive_from_iota": ModuleImport(
|
|
42
|
+
import_path="juniconnlib_core.modules.fiware_iota_communicators",
|
|
43
|
+
class_name=fiware_iota_communicators.MQTTCommunicatorReceiveFromIoTA.__name__,
|
|
44
|
+
),
|
|
45
|
+
"debug_listener": ModuleImport(
|
|
46
|
+
import_path="juniconnlib_core.modules.devtools",
|
|
47
|
+
class_name=devtools.DebugListener.__name__,
|
|
48
|
+
),
|
|
49
|
+
"debug_sender": ModuleImport(
|
|
50
|
+
import_path="juniconnlib_core.modules.devtools",
|
|
51
|
+
class_name=devtools.DebugSender.__name__,
|
|
52
|
+
),
|
|
53
|
+
"fiware_iota_device_provisioner": ModuleImport(
|
|
54
|
+
import_path="juniconnlib_core.modules.fiware_iota_device_provisioner",
|
|
55
|
+
class_name=fiware_iota_device_provisioner.FiwareIoTADeviceProvisioner.__name__,
|
|
56
|
+
),
|
|
57
|
+
"influxdb_writer": ModuleImport(
|
|
58
|
+
import_path="juniconnlib_core.modules.influxdb_writer",
|
|
59
|
+
class_name=influxdb_writer.InfluxdbWriter.__name__,
|
|
60
|
+
),
|
|
61
|
+
"fiware_ocb_entity_provisioner": ModuleImport(
|
|
62
|
+
import_path="juniconnlib_core.modules.fiware_ocb_entity_provisioner",
|
|
63
|
+
class_name=fiware_ocb_entity_provisioner.FiwareOCBEntityProvisioner.__name__,
|
|
64
|
+
),
|
|
65
|
+
"fiware_mqtt_subscription_handler": ModuleImport(
|
|
66
|
+
import_path="juniconnlib_core.modules.fiware_subscriptions",
|
|
67
|
+
class_name=fiware_subscriptions.FiwareMQTTSubscriptionHandler.__name__,
|
|
68
|
+
),
|
|
69
|
+
"fiware_http_subscription_handler": ModuleImport(
|
|
70
|
+
import_path="juniconnlib_core.modules.fiware_subscriptions",
|
|
71
|
+
class_name=fiware_subscriptions.FiwareHTTPSubscriptionHandler.__name__,
|
|
72
|
+
),
|
|
73
|
+
"mqtt_communicator_receive_from_ocb": ModuleImport(
|
|
74
|
+
import_path="juniconnlib_core.modules.fiware_ocb_communicators",
|
|
75
|
+
class_name=fiware_ocb_communicators.MQTTCommunicatorReceiveFromOCB.__name__,
|
|
76
|
+
),
|
|
77
|
+
"http_communicator_send_to_ocb": ModuleImport(
|
|
78
|
+
import_path="juniconnlib_core.modules.fiware_ocb_communicators",
|
|
79
|
+
class_name=fiware_ocb_communicators.HTTPCommunicatorSendToOCB.__name__,
|
|
80
|
+
),
|
|
81
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '1.1.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 1, 1)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
File without changes
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Optional, Type, Union
|
|
7
|
+
|
|
8
|
+
from fidere.models import Identifier
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, field_serializer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def timestamp_with_timezone():
|
|
13
|
+
return dt.datetime.now(tz=dt.timezone.utc)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Location(BaseModel):
|
|
17
|
+
"""Representation of a single location.
|
|
18
|
+
|
|
19
|
+
It can either identify a specific room together with the building
|
|
20
|
+
and precise wing it is situated in or a specific wing together with
|
|
21
|
+
the according building.
|
|
22
|
+
|
|
23
|
+
The class is mainly planned to be used in context of the FZJ campus.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
building: str = Field(
|
|
27
|
+
description="Building number in a standardized format consisting "
|
|
28
|
+
"of just 4"
|
|
29
|
+
"e.g. 2.1 -> 0210, 12.15 -> 1215, 9.7 -> 0970, etc.",
|
|
30
|
+
)
|
|
31
|
+
wing: str = Field(
|
|
32
|
+
description="Building wing, e.g. U, V, Z. The wing is mandatory."
|
|
33
|
+
)
|
|
34
|
+
room: str | None = Field(default=None, description="Room number")
|
|
35
|
+
|
|
36
|
+
class Config:
|
|
37
|
+
extra = "ignore"
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def number2code(cls: Type[BaseModel], building_number: str | int) -> str:
|
|
41
|
+
"""This function transforms buildings names like "02.1", "12.15"
|
|
42
|
+
into the standardized format "0210", "1215".
|
|
43
|
+
|
|
44
|
+
For more information on the standardized format, please take a
|
|
45
|
+
look at :class:juniconnlib_core.data_models.fiware_models.BuildingNumber
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
building_number (str, int): number of the building
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
building code in the standardized format, e.g. 0210, 1215, 0970
|
|
52
|
+
"""
|
|
53
|
+
if re.match(r"^\d{4}(\D*)", building_number):
|
|
54
|
+
return building_number
|
|
55
|
+
# return '{0[0]:0>2}{0[1]:0<2}'.format(building_number.split('.'))
|
|
56
|
+
# simpler to understand is below:
|
|
57
|
+
# (Does the replacement by adding '0' only if after removing ".", we don't have a 4-digit number.)
|
|
58
|
+
# We keep the building wing, if present.
|
|
59
|
+
return re.sub(
|
|
60
|
+
r"^(\d{3})(\D*)$", r"\g<1>0\g<2>", building_number.replace(".", "")
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AttributeUpdate(BaseModel):
|
|
65
|
+
"""Representation of an attribute update of a FIWARE entity.
|
|
66
|
+
|
|
67
|
+
This is meant to be used as a container for all necessary information
|
|
68
|
+
needed to exchange information on updates for or from FIWARE attributes"""
|
|
69
|
+
|
|
70
|
+
entity_id: str = Field(description="FIWARE entity identifier")
|
|
71
|
+
entity_type: str = Field(description="FIWARE entity type")
|
|
72
|
+
payload: dict[str, Any] = Field(description="MQTT payload")
|
|
73
|
+
timestamp: dt.datetime | None = Field(
|
|
74
|
+
description="Timestamp when the attribute update was initiated",
|
|
75
|
+
default_factory=timestamp_with_timezone,
|
|
76
|
+
)
|
|
77
|
+
additional_data: dict[str, Any] | None = Field(
|
|
78
|
+
default={},
|
|
79
|
+
description="Additional data that can be added to the for other "
|
|
80
|
+
"modules to work upon",
|
|
81
|
+
)
|
|
82
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
|
|
83
|
+
|
|
84
|
+
@field_serializer("timestamp")
|
|
85
|
+
def serialize_datetime(self, timestamp: dt.datetime) -> str:
|
|
86
|
+
return timestamp.isoformat(timespec="milliseconds")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class IotaMqttMessage(BaseModel):
|
|
90
|
+
"""Contains data for an MQTT message for the IoT agent such as
|
|
91
|
+
topic, payload, etc."""
|
|
92
|
+
|
|
93
|
+
device_id: str = Field(description="FIWARE device ID")
|
|
94
|
+
device_type: Optional[str] = Field(
|
|
95
|
+
default=None, description="FIWARE device type"
|
|
96
|
+
)
|
|
97
|
+
apikey: Optional[str] = Field(
|
|
98
|
+
default=None, description="IoT agent API key"
|
|
99
|
+
)
|
|
100
|
+
payload: Dict = Field(description="MQTT payload")
|
|
101
|
+
|
|
102
|
+
def topic(self) -> str:
|
|
103
|
+
"""Creates a MQTT-topic in the format required by the FIWARE MQTT IoT
|
|
104
|
+
agent.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
MQTT-topic compatible to the requirements of the FIWARE IoT
|
|
108
|
+
agent.
|
|
109
|
+
"""
|
|
110
|
+
return f"/json/{self.apikey}/{self.device_id}/attrs"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class CredentialsConfig(BaseModel):
|
|
114
|
+
"""Model class for storing username and password (e.g. for use with MQTT)"""
|
|
115
|
+
|
|
116
|
+
username: str
|
|
117
|
+
password: str
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def load_from_file(
|
|
121
|
+
cls: Type[BaseModel], credentials_file: str | Path
|
|
122
|
+
) -> BaseModel:
|
|
123
|
+
"""Loads credentials from a file and returns an instance of
|
|
124
|
+
:class:`CredentialsConfig`
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
credentials_file (str, Path): path to credentials file. The file
|
|
128
|
+
should be in JSON format and contain at least the keys
|
|
129
|
+
"username" and "password".
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
CredentialsConfig: Credentials read from file and validated
|
|
133
|
+
"""
|
|
134
|
+
credentials_file = credentials_file
|
|
135
|
+
if not os.path.exists(credentials_file):
|
|
136
|
+
raise FileNotFoundError(
|
|
137
|
+
f"Could not find keycloak credentials file {credentials_file}!"
|
|
138
|
+
)
|
|
139
|
+
with open(credentials_file) as json_input:
|
|
140
|
+
json_data = json.load(json_input)
|
|
141
|
+
|
|
142
|
+
return CredentialsConfig.model_validate(json_data)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class JsonSchemaInstance(BaseModel):
|
|
146
|
+
"""A JSON schema to be used with the `JsonSchemaParser` module."""
|
|
147
|
+
|
|
148
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, allow_extra=False)
|
|
149
|
+
instance: dict[str, Any]
|
|
150
|
+
identifier: Union[str, Identifier]
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Literal, Type
|
|
3
|
+
|
|
4
|
+
from filip.models import FiwareHeader
|
|
5
|
+
from filip.models.ngsi_v2.context import ContextAttribute, ContextEntity
|
|
6
|
+
from pydantic import Field, field_validator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_entity_id(entity_type: str, entity_content: Any) -> str:
|
|
10
|
+
"""Generate an entity id based on the entity type and entity content.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
entity_type (str): Type of the entity, e.g. Building, Wing, MessdasMeter
|
|
14
|
+
entity_content (Any): Hashable object, e.g. 0970, 0130_W, 0130_W_2001A, etc.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
unique FIWARE entity id based on a SHAKE256 hash of the `entity_content`.
|
|
18
|
+
"""
|
|
19
|
+
from hashlib import shake_256
|
|
20
|
+
|
|
21
|
+
plain_id = f"{entity_type}_{entity_content}".encode("utf-8")
|
|
22
|
+
hash_id = shake_256(plain_id).hexdigest(16)
|
|
23
|
+
|
|
24
|
+
return f"urn:ngsi-ld:{entity_type}:{hash_id}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_api_key(
|
|
28
|
+
fiware_header: FiwareHeader, entity_type: str, resource: str = "/iot/json"
|
|
29
|
+
) -> str:
|
|
30
|
+
"""Generate a unique API key based on the FIWARE service, entity type and
|
|
31
|
+
resource.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
fiware_header (FiwareHeader): FIWARE header.
|
|
35
|
+
entity_type (str): Type of the entity or more specific device.
|
|
36
|
+
resource (str): Resource of the devices service group (default is "/iot/json").
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Unique API key.
|
|
40
|
+
"""
|
|
41
|
+
from hashlib import shake_256
|
|
42
|
+
|
|
43
|
+
plain_id = (
|
|
44
|
+
f"{fiware_header.service}_{fiware_header.service_path}_"
|
|
45
|
+
f"{entity_type}_{resource}"
|
|
46
|
+
).encode("utf-8")
|
|
47
|
+
return shake_256(plain_id).hexdigest(16)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BuildingNumber(ContextAttribute):
|
|
51
|
+
"""A string of four numbers to identify a building. The
|
|
52
|
+
:class:`BuildingNumber` is meant to be used as an attribute in a
|
|
53
|
+
`ContextEntity`. The building number should be a string consisting of
|
|
54
|
+
exactly four numbers. For any further usage this formatting of four digits
|
|
55
|
+
is called the "standardized format".
|
|
56
|
+
|
|
57
|
+
A certain amount of conversion between incompatible formats is done
|
|
58
|
+
as shown in examples.
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
9.7 -> 0970
|
|
62
|
+
10.21 -> 1021
|
|
63
|
+
02.1 -> 0210
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
value: str = Field(
|
|
67
|
+
title="Building number",
|
|
68
|
+
description="Building number in the standardized format, e.g. 0970, 1021, etc.",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@field_validator("value")
|
|
72
|
+
def validate_number(cls, building_number: str) -> str:
|
|
73
|
+
"""Validate building number. It must be in the standardized format,
|
|
74
|
+
e.g. 0970, 1021, etc.
|
|
75
|
+
|
|
76
|
+
For more information on the standardized format, please take a
|
|
77
|
+
look at :class:juniconnlib_core.data_models.fiware_models.BuildingNumber
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
building_number: building number to validate.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
str: building number in case of the corrected format.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ValueError: If building number is not in the
|
|
87
|
+
standard format (four digits).
|
|
88
|
+
"""
|
|
89
|
+
number = BuildingNumber.normalize_building_number_format(
|
|
90
|
+
building_number
|
|
91
|
+
)
|
|
92
|
+
if number != building_number:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
"Building number has a wrong format! Expected the "
|
|
95
|
+
"standardized format, e.g. 0970, 1021, etc."
|
|
96
|
+
)
|
|
97
|
+
return building_number
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def normalize_building_number_format(cls, building_number: str) -> str:
|
|
101
|
+
"""Format the supplied `building_number` in the standardized format.
|
|
102
|
+
|
|
103
|
+
In practice, the buildings are named similar to: 02.1, 12.15, 09.7
|
|
104
|
+
In the standardized format: 0210, 1215, 0970. Some have 15.83-A,
|
|
105
|
+
so the regex can be a little more complex. It's idempotent if
|
|
106
|
+
building_number is already in standardized format or any other
|
|
107
|
+
format lacking a '.'
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
building_number (str): e.g. 09.7
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
str: The normalized number e.g. 0970
|
|
114
|
+
"""
|
|
115
|
+
if re.match(r"^\d{4}(\D*)", building_number):
|
|
116
|
+
return building_number
|
|
117
|
+
|
|
118
|
+
parts = building_number.split(".")
|
|
119
|
+
# add leading or trailing zero if necessary
|
|
120
|
+
area_number = re.sub(r"^(\d)$", r"0\g<1>", parts[0])
|
|
121
|
+
part2 = re.sub(r"^(\d)(\D*)$", r"\g<1>0\g<2>", parts[1])
|
|
122
|
+
|
|
123
|
+
return f"{area_number}{part2}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Building(ContextEntity):
|
|
127
|
+
"""A data model based `ContextEntity` used as representation of a single
|
|
128
|
+
building as FIWARE entity. It holds a number for the building in addition
|
|
129
|
+
to some other data like area.
|
|
130
|
+
|
|
131
|
+
For more information on the standardized format, please take a
|
|
132
|
+
look at :class:juniconnlib_core.data_models.fiware_models.BuildingNumber
|
|
133
|
+
|
|
134
|
+
This class is meant to be used in scope of the FZJ campus, but can
|
|
135
|
+
easily be used for buildings outside this scope as well."""
|
|
136
|
+
|
|
137
|
+
type: Literal["Building"] = Field(description="Entity type 'Building'")
|
|
138
|
+
number: BuildingNumber | ContextAttribute = Field(
|
|
139
|
+
title="Building number",
|
|
140
|
+
description="Building number in the standardized format, e.g. "
|
|
141
|
+
"0970, 1021, etc. or or `ContextAttribute`",
|
|
142
|
+
)
|
|
143
|
+
area: ContextAttribute = Field(
|
|
144
|
+
description="`ContextAttribute`-object containing the area of the "
|
|
145
|
+
"building in [m^2]"
|
|
146
|
+
)
|
|
147
|
+
year: ContextAttribute = Field(
|
|
148
|
+
description="`ContextAttribute`-object containing the year "
|
|
149
|
+
"of the building."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@field_validator("number")
|
|
153
|
+
def cast_to_building_number(cls, v):
|
|
154
|
+
"""Convert a possible `ContextAttribute` to a `BuildingNumber` instance"""
|
|
155
|
+
if isinstance(v, ContextAttribute):
|
|
156
|
+
v = BuildingNumber(**dict(v))
|
|
157
|
+
return v
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def generate_building_id(
|
|
161
|
+
cls: Type["Building"], building_number: str
|
|
162
|
+
) -> "str":
|
|
163
|
+
"""Generates a FIWARE building ID from a building number.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
building_number (str): building number in the standardized
|
|
167
|
+
format, e.g. 0970, 1021, etc.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
FIWARE building entity ID.
|
|
171
|
+
"""
|
|
172
|
+
entity_content = BuildingNumber.normalize_building_number_format(
|
|
173
|
+
building_number
|
|
174
|
+
)
|
|
175
|
+
return generate_entity_id(
|
|
176
|
+
"Building", entity_content=entity_content
|
|
177
|
+
) # TODO: should use the type attribute instead of the hardcoded string, but that is not working?
|
|
178
|
+
|
|
179
|
+
@field_validator("number", mode="before")
|
|
180
|
+
def convert_from_context_entity(cls, value):
|
|
181
|
+
if isinstance(value, ContextAttribute):
|
|
182
|
+
return value.model_dump()
|
|
183
|
+
return value
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class Wing(ContextEntity):
|
|
187
|
+
type: Literal["Wing"] = Field(description="Has to be 'Wing'")
|
|
188
|
+
# _WING_TYPE = 'Wing'
|
|
189
|
+
refBuilding: ContextAttribute = Field(description="number of the building")
|
|
190
|
+
number: ContextAttribute = Field(
|
|
191
|
+
description="letter of the wing, e.g. U, V, W"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
@field_validator("number")
|
|
195
|
+
def validate_number(cls, number: ContextAttribute):
|
|
196
|
+
"""Validate building number.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
number (ContextAttribute): Wing number as ContextAttribute
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
ContextAttribute: building number
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
ValueError: In case the value of the ContextAttribute is
|
|
206
|
+
not an uppercase letter
|
|
207
|
+
"""
|
|
208
|
+
if not number.value.isupper():
|
|
209
|
+
raise ValueError(
|
|
210
|
+
"Wing number has a wrong format! Expected upper case, e.g. U, V, W etc."
|
|
211
|
+
)
|
|
212
|
+
return number
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def generate_wing_id(
|
|
216
|
+
cls: Type["Wing"], wing_number: str, building_number: str
|
|
217
|
+
) -> "str":
|
|
218
|
+
"""Generates a FIWARE wing id by hashing a wing letter and building number.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
wing_number (str): letter of the wing, e.g. U, V, W
|
|
222
|
+
building_number (str): building number in the standardized
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
str: wing entity ID
|
|
226
|
+
"""
|
|
227
|
+
normalized_building_number = (
|
|
228
|
+
BuildingNumber.normalize_building_number_format(building_number)
|
|
229
|
+
)
|
|
230
|
+
normalized_wing_number = wing_number.upper()
|
|
231
|
+
wing_entity_content = (
|
|
232
|
+
f"{normalized_building_number}_{normalized_wing_number}"
|
|
233
|
+
)
|
|
234
|
+
return generate_entity_id(
|
|
235
|
+
entity_type="Wing", entity_content=wing_entity_content
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Room(ContextEntity):
|
|
240
|
+
type: Literal["Room"] = Field(description="Has to be 'Room'")
|
|
241
|
+
# _ROOM_TYPE = 'Room'
|
|
242
|
+
refBuilding: ContextAttribute = Field(description="id of the building")
|
|
243
|
+
refWing: ContextAttribute = Field(description="id of the wing")
|
|
244
|
+
number: ContextAttribute = Field(
|
|
245
|
+
description="number of the room, e.g. 300, 4002, 120a"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
@field_validator("number")
|
|
249
|
+
def validate_number(cls, number: ContextAttribute):
|
|
250
|
+
"""Room numbers have very different formats, so for now no validation
|
|
251
|
+
is done."""
|
|
252
|
+
return number
|
|
253
|
+
|
|
254
|
+
@classmethod
|
|
255
|
+
def generate_room_id(
|
|
256
|
+
cls: Type["Room"],
|
|
257
|
+
room_number: str,
|
|
258
|
+
wing_number: str,
|
|
259
|
+
building_number: str,
|
|
260
|
+
) -> "str":
|
|
261
|
+
"""Generates a FIWARE room id by hashing a room descriptor, wing
|
|
262
|
+
letter and building number.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
room_number (str): number of the room, e.g. 300, 4002, 120a
|
|
266
|
+
wing_number (str): number of the wing, e.g. U, V, W
|
|
267
|
+
building_number (str): building number in the standardized format
|
|
268
|
+
"""
|
|
269
|
+
normalized_building_number = (
|
|
270
|
+
BuildingNumber.normalize_building_number_format(building_number)
|
|
271
|
+
)
|
|
272
|
+
normalized_wing_number = wing_number.upper()
|
|
273
|
+
normalized_room_number = room_number.upper()
|
|
274
|
+
room_entity_content = f"{normalized_building_number}_{normalized_wing_number}_{normalized_room_number}"
|
|
275
|
+
return generate_entity_id(
|
|
276
|
+
entity_type="Room", entity_content=room_entity_content
|
|
277
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from itertools import cycle
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
from agentlib.core import Agent, AgentVariable, BaseModule, BaseModuleConfig
|
|
5
|
+
from pydantic import Field, ValidationError, field_validator
|
|
6
|
+
|
|
7
|
+
from juniconnlib_core.utils.logging import create_logger_for_module
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SendConfig(BaseModuleConfig):
|
|
11
|
+
delay: float = Field(
|
|
12
|
+
default=0,
|
|
13
|
+
ge=0,
|
|
14
|
+
description="Time delay to tick down before a value "
|
|
15
|
+
"should be send (again)",
|
|
16
|
+
)
|
|
17
|
+
values: List[dict[str, Any]] = Field(
|
|
18
|
+
None,
|
|
19
|
+
description="Variable to be send. Should have keys "
|
|
20
|
+
"'value' and 'alias'",
|
|
21
|
+
examples=[
|
|
22
|
+
'[{"value": ..., "alias": ...}, ' '{"value": ..., "alias": ...}]'
|
|
23
|
+
],
|
|
24
|
+
)
|
|
25
|
+
repeat: bool = Field(
|
|
26
|
+
False,
|
|
27
|
+
description="Decision if messages should be send "
|
|
28
|
+
"repeatedly. Cycles through the list of "
|
|
29
|
+
"values.",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@field_validator("values")
|
|
33
|
+
def check_keys(cls, values):
|
|
34
|
+
for val in values:
|
|
35
|
+
if "value" not in val and "alias" not in val:
|
|
36
|
+
raise ValidationError(
|
|
37
|
+
"Provide 'value' and 'alias' for every "
|
|
38
|
+
"variable to be send"
|
|
39
|
+
)
|
|
40
|
+
return values
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DebugSender(BaseModule):
|
|
44
|
+
"""General sending type module that can be used for debugging purposes.
|
|
45
|
+
It sends a list of `values` to the agent broker with a given `delay`"""
|
|
46
|
+
|
|
47
|
+
config: SendConfig
|
|
48
|
+
|
|
49
|
+
def __init__(self, config: Dict, agent: Agent):
|
|
50
|
+
super().__init__(config=config, agent=agent)
|
|
51
|
+
# overwrite the logger of agent liv modules, since it has a fixed formatter
|
|
52
|
+
self.logger = create_logger_for_module(self)
|
|
53
|
+
|
|
54
|
+
self.loop = True
|
|
55
|
+
if self.config.repeat:
|
|
56
|
+
self.vals = cycle(self.config.values)
|
|
57
|
+
else:
|
|
58
|
+
self.vals = iter(self.config.values)
|
|
59
|
+
|
|
60
|
+
def process(self):
|
|
61
|
+
while True:
|
|
62
|
+
# try getting a value. If the iterator is exhausted
|
|
63
|
+
try:
|
|
64
|
+
val = next(self.vals)
|
|
65
|
+
self.logger.debug(f"Sending {val}")
|
|
66
|
+
except StopIteration:
|
|
67
|
+
self.logger.debug("All values have been sent")
|
|
68
|
+
yield self.env.event()
|
|
69
|
+
|
|
70
|
+
var = AgentVariable(**val, name="DevVar", source=self.source)
|
|
71
|
+
self.agent.data_broker.send_variable(var)
|
|
72
|
+
yield self.env.timeout(5)
|
|
73
|
+
|
|
74
|
+
def terminate(self):
|
|
75
|
+
self.loop = False
|
|
76
|
+
self.logger.info(f"Terminated {self.__class__.__name__}")
|
|
77
|
+
|
|
78
|
+
def register_callbacks(self):
|
|
79
|
+
"""No callback necessary."""
|
|
80
|
+
...
|
|
81
|
+
# self.agent.data_broker.register_callback()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class DebugListener(BaseModule):
|
|
85
|
+
"""This module ist meant for debugging and development purposes.
|
|
86
|
+
It logs any variable on the broker on level DEBUG"""
|
|
87
|
+
|
|
88
|
+
config: BaseModuleConfig
|
|
89
|
+
|
|
90
|
+
def __init__(self, config: Dict, agent: Agent):
|
|
91
|
+
super().__init__(config=config, agent=agent)
|
|
92
|
+
# overwrite the logger of agent liv modules, since it has a fixed formatter
|
|
93
|
+
self.logger = create_logger_for_module(self)
|
|
94
|
+
|
|
95
|
+
def process(self):
|
|
96
|
+
yield self.env.event()
|
|
97
|
+
|
|
98
|
+
def terminate(self):
|
|
99
|
+
self.logger.info(f"Terminated {self.__class__.__name__}")
|
|
100
|
+
|
|
101
|
+
def register_callbacks(self):
|
|
102
|
+
"""Registers a simple callback that logs ALL variables on the broker as
|
|
103
|
+
debug message."""
|
|
104
|
+
self.agent.data_broker.register_callback(
|
|
105
|
+
callback=lambda var: self.logger.debug(var)
|
|
106
|
+
)
|