layrz-sdk 3.1.13__py3-none-any.whl → 3.1.15__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.
Potentially problematic release.
This version of layrz-sdk might be problematic. Click here for more details.
- layrz_sdk/constants.py +14 -0
- layrz_sdk/entities/__init__.py +9 -0
- layrz_sdk/entities/asset.py +11 -0
- layrz_sdk/entities/case.py +5 -1
- layrz_sdk/entities/device.py +5 -0
- layrz_sdk/entities/modbus/__init__.py +9 -0
- layrz_sdk/entities/modbus/config.py +19 -0
- layrz_sdk/entities/modbus/parameter.py +110 -0
- layrz_sdk/entities/modbus/schema.py +10 -0
- layrz_sdk/entities/modbus/status.py +16 -0
- layrz_sdk/entities/modbus/wait.py +134 -0
- layrz_sdk/entities/static_position.py +17 -0
- layrz_sdk/entities/telemetry/__init__.py +6 -0
- layrz_sdk/entities/telemetry/assetmessage.py +159 -0
- layrz_sdk/entities/telemetry/devicemessage.py +122 -0
- {layrz_sdk-3.1.13.dist-info → layrz_sdk-3.1.15.dist-info}/METADATA +3 -1
- {layrz_sdk-3.1.13.dist-info → layrz_sdk-3.1.15.dist-info}/RECORD +20 -10
- {layrz_sdk-3.1.13.dist-info → layrz_sdk-3.1.15.dist-info}/WHEEL +1 -1
- {layrz_sdk-3.1.13.dist-info → layrz_sdk-3.1.15.dist-info}/licenses/LICENSE +0 -0
- {layrz_sdk-3.1.13.dist-info → layrz_sdk-3.1.15.dist-info}/top_level.txt +0 -0
layrz_sdk/constants.py
CHANGED
|
@@ -3,3 +3,17 @@
|
|
|
3
3
|
from zoneinfo import ZoneInfo
|
|
4
4
|
|
|
5
5
|
UTC = ZoneInfo('UTC')
|
|
6
|
+
""" UTC timezone constant for use in datetime fields. """
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
REJECTED_KEYS = (
|
|
10
|
+
'timestamp',
|
|
11
|
+
'ident',
|
|
12
|
+
'server.timestamp',
|
|
13
|
+
'protocol.id',
|
|
14
|
+
'channel.id',
|
|
15
|
+
'device.name',
|
|
16
|
+
'device.id',
|
|
17
|
+
'device.type.id',
|
|
18
|
+
)
|
|
19
|
+
""" Defines the ignored raw keys from a telemetry object """
|
layrz_sdk/entities/__init__.py
CHANGED
|
@@ -46,6 +46,7 @@ from .event import Event
|
|
|
46
46
|
from .geofence import Geofence
|
|
47
47
|
from .last_message import LastMessage
|
|
48
48
|
from .message import Message
|
|
49
|
+
from .modbus import ModbusConfig, ModbusParameter, ModbusSchema, ModbusStatus, ModbusWait
|
|
49
50
|
from .outbound_service import OutboundService
|
|
50
51
|
from .position import Position
|
|
51
52
|
from .presence_type import PresenceType
|
|
@@ -58,6 +59,7 @@ from .report_header import ReportHeader
|
|
|
58
59
|
from .report_page import ReportPage
|
|
59
60
|
from .report_row import ReportRow
|
|
60
61
|
from .sensor import Sensor
|
|
62
|
+
from .telemetry import AssetMessage, DeviceMessage
|
|
61
63
|
from .text_alignment import TextAlignment
|
|
62
64
|
from .trigger import Trigger
|
|
63
65
|
from .user import User
|
|
@@ -126,4 +128,11 @@ __all__ = [
|
|
|
126
128
|
'Trigger',
|
|
127
129
|
'User',
|
|
128
130
|
'Waypoint',
|
|
131
|
+
'ModbusConfig',
|
|
132
|
+
'ModbusParameter',
|
|
133
|
+
'ModbusSchema',
|
|
134
|
+
'ModbusStatus',
|
|
135
|
+
'ModbusWait',
|
|
136
|
+
'AssetMessage',
|
|
137
|
+
'DeviceMessage',
|
|
129
138
|
]
|
layrz_sdk/entities/asset.py
CHANGED
|
@@ -14,6 +14,7 @@ from .asset_operation_mode import AssetOperationMode
|
|
|
14
14
|
from .custom_field import CustomField
|
|
15
15
|
from .device import Device
|
|
16
16
|
from .sensor import Sensor
|
|
17
|
+
from .static_position import StaticPosition
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class Asset(BaseModel):
|
|
@@ -35,6 +36,16 @@ class Asset(BaseModel):
|
|
|
35
36
|
devices: list[Device] = Field(default_factory=list, description='Defines the list of devices of the asset')
|
|
36
37
|
children: list[Self] = Field(default_factory=list, description='Defines the list of children of the asset')
|
|
37
38
|
|
|
39
|
+
static_position: StaticPosition | None = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
description='Static position of the asset',
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
points: list[StaticPosition] = Field(
|
|
45
|
+
default_factory=list,
|
|
46
|
+
description='List of static positions for the asset. The altitude of StaticPosition is not used in this case.',
|
|
47
|
+
)
|
|
48
|
+
|
|
38
49
|
@model_validator(mode='before')
|
|
39
50
|
def _validate_model(cls: Self, data: dict[str, Any]) -> dict[str, Any]:
|
|
40
51
|
"""Validate model"""
|
layrz_sdk/entities/case.py
CHANGED
|
@@ -41,7 +41,11 @@ class Case(BaseModel):
|
|
|
41
41
|
"""Validate model"""
|
|
42
42
|
sequence = data.get('sequence')
|
|
43
43
|
if sequence is not None and isinstance(sequence, int):
|
|
44
|
-
|
|
44
|
+
trigger = data['trigger']
|
|
45
|
+
if not isinstance(trigger, Trigger):
|
|
46
|
+
data['sequence'] = f'{trigger["code"]}/{data["pk"]}'
|
|
47
|
+
else:
|
|
48
|
+
data['sequence'] = f'{trigger.code}/{sequence}'
|
|
45
49
|
else:
|
|
46
50
|
data['sequence'] = f'GENERIC/{data["pk"]}'
|
|
47
51
|
|
layrz_sdk/entities/device.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
+
from .modbus import ModbusConfig
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
class Device(BaseModel):
|
|
7
9
|
"""Device entity"""
|
|
@@ -9,5 +11,8 @@ class Device(BaseModel):
|
|
|
9
11
|
pk: int = Field(description='Defines the primary key of the device')
|
|
10
12
|
name: str = Field(description='Defines the name of the device')
|
|
11
13
|
ident: str = Field(description='Defines the identifier of the device')
|
|
14
|
+
protocol_id: int = Field(description='Defines the protocol ID of the device')
|
|
12
15
|
protocol: str = Field(description='Defines the protocol of the device')
|
|
13
16
|
is_primary: bool = Field(default=False, description='Defines if the device is the primary device')
|
|
17
|
+
|
|
18
|
+
modbus: ModbusConfig | None = Field(default=None, description='Modbus configuration')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Modbus Models"""
|
|
2
|
+
|
|
3
|
+
from .config import ModbusConfig
|
|
4
|
+
from .parameter import ModbusParameter
|
|
5
|
+
from .schema import ModbusSchema
|
|
6
|
+
from .status import ModbusStatus
|
|
7
|
+
from .wait import ModbusWait
|
|
8
|
+
|
|
9
|
+
__all__ = ['ModbusConfig', 'ModbusParameter', 'ModbusSchema', 'ModbusStatus', 'ModbusWait']
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
|
|
3
|
+
from .parameter import ModbusParameter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ModbusConfig(BaseModel):
|
|
7
|
+
port_id: str = Field(
|
|
8
|
+
...,
|
|
9
|
+
description='Port ID for Modbus communication',
|
|
10
|
+
)
|
|
11
|
+
is_enabled: bool = Field(
|
|
12
|
+
default=False,
|
|
13
|
+
description='Flag to enable or disable Modbus communication',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
parameters: list[ModbusParameter] = Field(
|
|
17
|
+
default_factory=list,
|
|
18
|
+
description='List of Modbus parameters to be used in communication',
|
|
19
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, field_validator
|
|
4
|
+
|
|
5
|
+
from .schema import ModbusSchema
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModbusParameter(BaseModel):
|
|
9
|
+
"""Modbus parameter model"""
|
|
10
|
+
|
|
11
|
+
schema_: ModbusSchema = Field(
|
|
12
|
+
...,
|
|
13
|
+
description='Modbus schema',
|
|
14
|
+
alias='schema',
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
split_each: int = Field(
|
|
18
|
+
...,
|
|
19
|
+
description='Number of bytes to split each Modbus parameter',
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@field_validator('split_each', mode='before')
|
|
23
|
+
def validate_split_each(cls, value: Any) -> int:
|
|
24
|
+
"""Validate and convert split_each to integer."""
|
|
25
|
+
if isinstance(value, int):
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
if isinstance(value, str):
|
|
29
|
+
try:
|
|
30
|
+
return int(value)
|
|
31
|
+
except ValueError as e:
|
|
32
|
+
raise ValueError(f'Invalid Modbus split_each value: {value}') from e
|
|
33
|
+
|
|
34
|
+
raise ValueError(f'Invalid Modbus split_each type: {type(value)}')
|
|
35
|
+
|
|
36
|
+
data_length: int = Field(
|
|
37
|
+
...,
|
|
38
|
+
description='Length of data for the Modbus parameter, from Hexadecimal representation',
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@field_validator('data_length', mode='before')
|
|
42
|
+
def validate_data_length(cls, value: Any) -> int:
|
|
43
|
+
"""Validate and convert data_length to integer."""
|
|
44
|
+
if isinstance(value, int):
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
if isinstance(value, str):
|
|
48
|
+
try:
|
|
49
|
+
return int(value, 16) # Convert from hexadecimal string to integer
|
|
50
|
+
except ValueError as e:
|
|
51
|
+
raise ValueError(f'Invalid Modbus data_length value: {value}') from e
|
|
52
|
+
|
|
53
|
+
raise ValueError(f'Invalid Modbus data_length type: {type(value)}')
|
|
54
|
+
|
|
55
|
+
data_address: int = Field(
|
|
56
|
+
...,
|
|
57
|
+
description='Address of the Modbus parameter, from Hexadecimal representation',
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@field_validator('data_address', mode='before')
|
|
61
|
+
def validate_data_address(cls, value: Any) -> int:
|
|
62
|
+
"""Validate and convert data_address to integer."""
|
|
63
|
+
if isinstance(value, int):
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
if isinstance(value, str):
|
|
67
|
+
try:
|
|
68
|
+
return int(value, 16) # Convert from hexadecimal string to integer
|
|
69
|
+
except ValueError as e:
|
|
70
|
+
raise ValueError(f'Invalid Modbus data_address value: {value}') from e
|
|
71
|
+
|
|
72
|
+
raise ValueError(f'Invalid Modbus data_address type: {type(value)}')
|
|
73
|
+
|
|
74
|
+
function_code: int = Field(
|
|
75
|
+
...,
|
|
76
|
+
description='Function code for the Modbus parameter',
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@field_validator('function_code', mode='before')
|
|
80
|
+
def validate_function_code(cls, value: Any) -> int:
|
|
81
|
+
"""Validate and convert function_code to integer."""
|
|
82
|
+
if isinstance(value, int):
|
|
83
|
+
return value
|
|
84
|
+
|
|
85
|
+
if isinstance(value, str):
|
|
86
|
+
try:
|
|
87
|
+
return int(value, 16) # Convert from hexadecimal string to integer
|
|
88
|
+
except ValueError as e:
|
|
89
|
+
raise ValueError(f'Invalid Modbus function_code value: {value}') from e
|
|
90
|
+
|
|
91
|
+
raise ValueError(f'Invalid Modbus function_code type: {type(value)}')
|
|
92
|
+
|
|
93
|
+
controller_address: int = Field(
|
|
94
|
+
...,
|
|
95
|
+
description='Controller address for the Modbus parameter',
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@field_validator('controller_address', mode='before')
|
|
99
|
+
def validate_controller_address(cls, value: Any) -> int:
|
|
100
|
+
"""Validate and convert controller_address to integer."""
|
|
101
|
+
if isinstance(value, int):
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
if isinstance(value, str):
|
|
105
|
+
try:
|
|
106
|
+
return int(value, 16) # Convert from hexadecimal string to integer
|
|
107
|
+
except ValueError as e:
|
|
108
|
+
raise ValueError(f'Invalid Modbus controller_address value: {value}') from e
|
|
109
|
+
|
|
110
|
+
raise ValueError(f'Invalid Modbus controller_address type: {type(value)}')
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ModbusStatus(StrEnum):
|
|
5
|
+
"""Modbus schema enumeration"""
|
|
6
|
+
|
|
7
|
+
PENDING = 'PENDING'
|
|
8
|
+
""" Defines the pending state, indicating that the request is waiting to be processed. """
|
|
9
|
+
WAITING_FOR_SEND = 'WAITING_FOR_SEND'
|
|
10
|
+
""" Indicates that the request is ready to be sent but has not yet been dispatched. """
|
|
11
|
+
SENT = 'SENT'
|
|
12
|
+
""" Indicates that the request has been sent to the device. """
|
|
13
|
+
ACK_RECEIVED = 'ACK_RECEIVED'
|
|
14
|
+
""" Indicates that an acknowledgment has been received from the device. """
|
|
15
|
+
CANCELLED = 'CANCELLED'
|
|
16
|
+
""" Indicates that the request has been cancelled. """
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, field_validator
|
|
4
|
+
|
|
5
|
+
from .schema import ModbusSchema
|
|
6
|
+
from .status import ModbusStatus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModbusWait(BaseModel):
|
|
10
|
+
"""Modbus parameter model"""
|
|
11
|
+
|
|
12
|
+
status: ModbusStatus = Field(
|
|
13
|
+
...,
|
|
14
|
+
description='Status of the Modbus command',
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
structure: ModbusSchema = Field(
|
|
18
|
+
...,
|
|
19
|
+
description='Modbus structure schema',
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
port_id: int = Field(
|
|
23
|
+
...,
|
|
24
|
+
description='Port ID for the Modbus command',
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@field_validator('port_id', mode='before')
|
|
28
|
+
def validate_port_id(cls, value: Any) -> int:
|
|
29
|
+
"""Validate and convert port_id to integer."""
|
|
30
|
+
if isinstance(value, int):
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
if isinstance(value, str):
|
|
34
|
+
try:
|
|
35
|
+
return int(value)
|
|
36
|
+
except ValueError as e:
|
|
37
|
+
raise ValueError(f'Invalid Modbus port_id value: {value}') from e
|
|
38
|
+
|
|
39
|
+
raise ValueError(f'Invalid Modbus port_id type: {type(value)}')
|
|
40
|
+
|
|
41
|
+
split_each: int = Field(
|
|
42
|
+
...,
|
|
43
|
+
description='Number of bytes to split each Modbus parameter',
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@field_validator('split_each', mode='before')
|
|
47
|
+
def validate_split_each(cls, value: Any) -> int:
|
|
48
|
+
"""Validate and convert split_each to integer."""
|
|
49
|
+
if isinstance(value, int):
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
if isinstance(value, str):
|
|
53
|
+
try:
|
|
54
|
+
return int(value)
|
|
55
|
+
except ValueError as e:
|
|
56
|
+
raise ValueError(f'Invalid Modbus split_each value: {value}') from e
|
|
57
|
+
|
|
58
|
+
raise ValueError(f'Invalid Modbus split_each type: {type(value)}')
|
|
59
|
+
|
|
60
|
+
data_length: int = Field(
|
|
61
|
+
...,
|
|
62
|
+
description='Length of data for the Modbus parameter, from Hexadecimal representation',
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@field_validator('data_length', mode='before')
|
|
66
|
+
def validate_data_length(cls, value: Any) -> int:
|
|
67
|
+
"""Validate and convert data_length to integer."""
|
|
68
|
+
if isinstance(value, int):
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
if isinstance(value, str):
|
|
72
|
+
try:
|
|
73
|
+
return int(value, 16) # Convert from hexadecimal string to integer
|
|
74
|
+
except ValueError as e:
|
|
75
|
+
raise ValueError(f'Invalid Modbus data_length value: {value}') from e
|
|
76
|
+
|
|
77
|
+
raise ValueError(f'Invalid Modbus data_length type: {type(value)}')
|
|
78
|
+
|
|
79
|
+
data_address: int = Field(
|
|
80
|
+
...,
|
|
81
|
+
description='Address of the Modbus parameter, from Hexadecimal representation',
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@field_validator('data_address', mode='before')
|
|
85
|
+
def validate_data_address(cls, value: Any) -> int:
|
|
86
|
+
"""Validate and convert data_address to integer."""
|
|
87
|
+
if isinstance(value, int):
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
if isinstance(value, str):
|
|
91
|
+
try:
|
|
92
|
+
return int(value, 16) # Convert from hexadecimal string to integer
|
|
93
|
+
except ValueError as e:
|
|
94
|
+
raise ValueError(f'Invalid Modbus data_address value: {value}') from e
|
|
95
|
+
|
|
96
|
+
raise ValueError(f'Invalid Modbus data_address type: {type(value)}')
|
|
97
|
+
|
|
98
|
+
function_code: int = Field(
|
|
99
|
+
...,
|
|
100
|
+
description='Function code for the Modbus parameter',
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@field_validator('function_code', mode='before')
|
|
104
|
+
def validate_function_code(cls, value: Any) -> int:
|
|
105
|
+
"""Validate and convert function_code to integer."""
|
|
106
|
+
if isinstance(value, int):
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
if isinstance(value, str):
|
|
110
|
+
try:
|
|
111
|
+
return int(value, 16) # Convert from hexadecimal string to integer
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
raise ValueError(f'Invalid Modbus function_code value: {value}') from e
|
|
114
|
+
|
|
115
|
+
raise ValueError(f'Invalid Modbus function_code type: {type(value)}')
|
|
116
|
+
|
|
117
|
+
controller_address: int = Field(
|
|
118
|
+
...,
|
|
119
|
+
description='Controller address for the Modbus parameter',
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@field_validator('controller_address', mode='before')
|
|
123
|
+
def validate_controller_address(cls, value: Any) -> int:
|
|
124
|
+
"""Validate and convert controller_address to integer."""
|
|
125
|
+
if isinstance(value, int):
|
|
126
|
+
return value
|
|
127
|
+
|
|
128
|
+
if isinstance(value, str):
|
|
129
|
+
try:
|
|
130
|
+
return int(value, 16) # Convert from hexadecimal string to integer
|
|
131
|
+
except ValueError as e:
|
|
132
|
+
raise ValueError(f'Invalid Modbus controller_address value: {value}') from e
|
|
133
|
+
|
|
134
|
+
raise ValueError(f'Invalid Modbus controller_address type: {type(value)}')
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StaticPosition(BaseModel):
|
|
5
|
+
latitude: float = Field(
|
|
6
|
+
...,
|
|
7
|
+
description='Latitude of the static position',
|
|
8
|
+
)
|
|
9
|
+
longitude: float = Field(
|
|
10
|
+
...,
|
|
11
|
+
description='Longitude of the static position',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
altitude: float | None = Field(
|
|
15
|
+
default=None,
|
|
16
|
+
description='Altitude of the static position',
|
|
17
|
+
)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Asset message"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from layrz_sdk.entities.position import Position
|
|
11
|
+
|
|
12
|
+
if sys.version_info >= (3, 11):
|
|
13
|
+
from typing import Self
|
|
14
|
+
else:
|
|
15
|
+
from typing_extensions import Self
|
|
16
|
+
|
|
17
|
+
from geopy.distance import geodesic
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
from shapely.geometry import MultiPoint
|
|
20
|
+
|
|
21
|
+
from layrz_sdk.constants import UTC
|
|
22
|
+
from layrz_sdk.entities.asset import Asset
|
|
23
|
+
from layrz_sdk.entities.asset_operation_mode import AssetOperationMode
|
|
24
|
+
from layrz_sdk.entities.message import Message
|
|
25
|
+
from layrz_sdk.entities.telemetry.devicemessage import DeviceMessage
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AssetMessage(BaseModel):
|
|
29
|
+
"""Asset message model"""
|
|
30
|
+
|
|
31
|
+
pk: int | None = Field(
|
|
32
|
+
default=None,
|
|
33
|
+
description='Message ID',
|
|
34
|
+
alias='id',
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
asset_id: int = Field(
|
|
38
|
+
...,
|
|
39
|
+
description='Asset ID',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
position: dict[str, float | int] = Field(
|
|
43
|
+
default_factory=dict,
|
|
44
|
+
description='Current position of the device',
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
payload: dict[str, Any] = Field(
|
|
48
|
+
default_factory=dict,
|
|
49
|
+
description='Payload data of the device message',
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
sensors: dict[str, Any] = Field(
|
|
53
|
+
default_factory=dict,
|
|
54
|
+
description='Sensor data of the device message',
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
geofences_ids: list[int] = Field(
|
|
58
|
+
default_factory=list,
|
|
59
|
+
description='List of geofence IDs associated with the message',
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
distance_traveled: float = Field(
|
|
63
|
+
default=0.0,
|
|
64
|
+
description='Distance traveled since the last message',
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
received_at: datetime = Field(
|
|
68
|
+
default_factory=lambda: datetime.now(UTC),
|
|
69
|
+
description='Timestamp when the message was received',
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
elapsed_time: timedelta = Field(
|
|
73
|
+
default_factory=lambda: timedelta(seconds=0),
|
|
74
|
+
description='Elapsed time since the last message',
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def datum_gis(self: Self) -> int:
|
|
79
|
+
"""Get the GIS datum of the message."""
|
|
80
|
+
return 4326
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def point_gis(self: Self) -> str | None:
|
|
84
|
+
"""Get the GIS point of the message on WKT (Well-Known Text) format for OGC (Open Geospatial Consortium)."""
|
|
85
|
+
latitude = self.position.get('latitude')
|
|
86
|
+
longitude = self.position.get('longitude')
|
|
87
|
+
|
|
88
|
+
if latitude is not None and longitude is not None:
|
|
89
|
+
return f'POINT({longitude} {latitude})'
|
|
90
|
+
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def has_point(self: Self) -> bool:
|
|
95
|
+
"""Check if the message has a point."""
|
|
96
|
+
latitude = self.position.get('latitude')
|
|
97
|
+
longitude = self.position.get('longitude')
|
|
98
|
+
|
|
99
|
+
return latitude is not None and longitude is not None
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def parse_from_devicemessage(cls, *, device_message: DeviceMessage, asset: Asset) -> AssetMessage:
|
|
103
|
+
obj = cls(
|
|
104
|
+
asset_id=asset.pk,
|
|
105
|
+
position={},
|
|
106
|
+
payload={},
|
|
107
|
+
sensors={},
|
|
108
|
+
received_at=device_message.received_at,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
match asset.operation_mode:
|
|
112
|
+
case AssetOperationMode.DISCONNECTED:
|
|
113
|
+
obj.position = {}
|
|
114
|
+
case AssetOperationMode.STATIC:
|
|
115
|
+
obj.position = asset.static_position.model_dump(exclude_none=True) if asset.static_position else {}
|
|
116
|
+
case AssetOperationMode.ZONE:
|
|
117
|
+
points = MultiPoint([(p.longitude, p.latitude) for p in asset.points])
|
|
118
|
+
obj.position = {'latitude': points.centroid.y, 'longitude': points.centroid.x}
|
|
119
|
+
case _:
|
|
120
|
+
obj.position = device_message.position
|
|
121
|
+
|
|
122
|
+
for key, value in device_message.payload.items():
|
|
123
|
+
obj.payload[f'{device_message.ident}.{key}'] = value
|
|
124
|
+
|
|
125
|
+
return obj
|
|
126
|
+
|
|
127
|
+
def compute_distance_traveled(self: Self, *, previous_message: AssetMessage | None = None) -> float:
|
|
128
|
+
"""Compute the distance traveled since the last message."""
|
|
129
|
+
if not self.has_point or not previous_message or not previous_message.has_point:
|
|
130
|
+
return 0.0
|
|
131
|
+
|
|
132
|
+
return cast(
|
|
133
|
+
float,
|
|
134
|
+
geodesic(
|
|
135
|
+
(self.position['latitude'], self.position['longitude']),
|
|
136
|
+
(previous_message.position['latitude'], previous_message.position['longitude']),
|
|
137
|
+
).meters,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def compute_elapsed_time(self: Self, *, previous_message: AssetMessage | None = None) -> timedelta:
|
|
141
|
+
"""Compute the elapsed time since the last message."""
|
|
142
|
+
if not previous_message:
|
|
143
|
+
return timedelta(seconds=0)
|
|
144
|
+
|
|
145
|
+
if self.received_at < previous_message.received_at:
|
|
146
|
+
return timedelta(seconds=0)
|
|
147
|
+
|
|
148
|
+
return self.received_at - previous_message.received_at
|
|
149
|
+
|
|
150
|
+
def to_message(self: Self) -> Message:
|
|
151
|
+
"""Convert the asset message to a Message object."""
|
|
152
|
+
return Message(
|
|
153
|
+
pk=self.pk if self.pk is not None else 0,
|
|
154
|
+
asset_id=self.asset_id,
|
|
155
|
+
position=Position.model_validate(self.position),
|
|
156
|
+
payload=self.payload,
|
|
157
|
+
sensors=self.sensors,
|
|
158
|
+
received_at=self.received_at,
|
|
159
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Device message"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from layrz_sdk.entities.message import Message
|
|
11
|
+
from layrz_sdk.entities.position import Position
|
|
12
|
+
|
|
13
|
+
if sys.version_info >= (3, 11):
|
|
14
|
+
from typing import Self
|
|
15
|
+
else:
|
|
16
|
+
from typing_extensions import Self
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
|
|
20
|
+
from layrz_sdk.constants import REJECTED_KEYS, UTC
|
|
21
|
+
from layrz_sdk.entities.device import Device
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DeviceMessage(BaseModel):
|
|
25
|
+
"""Device message model"""
|
|
26
|
+
|
|
27
|
+
pk: int | None = Field(
|
|
28
|
+
default=None,
|
|
29
|
+
description='Device message ID',
|
|
30
|
+
alias='id',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
ident: str = Field(
|
|
34
|
+
...,
|
|
35
|
+
description='Device identifier',
|
|
36
|
+
)
|
|
37
|
+
device_id: int = Field(
|
|
38
|
+
...,
|
|
39
|
+
description='Device ID',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
protocol_id: int = Field(
|
|
43
|
+
...,
|
|
44
|
+
description='Protocol ID',
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
position: dict[str, float | int] = Field(
|
|
48
|
+
default_factory=dict,
|
|
49
|
+
description='Current position of the device',
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
payload: dict[str, Any] = Field(
|
|
53
|
+
default_factory=dict,
|
|
54
|
+
description='Payload data of the device message',
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
received_at: datetime = Field(
|
|
58
|
+
default_factory=lambda: datetime.now(UTC),
|
|
59
|
+
description='Timestamp when the message was received',
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def datum_gis(self: Self) -> int:
|
|
64
|
+
"""Get the GIS datum of the message."""
|
|
65
|
+
return 4326
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def point_gis(self: Self) -> str | None:
|
|
69
|
+
"""Get the GIS point of the message on WKT (Well-Known Text) format for OGC (Open Geospatial Consortium)."""
|
|
70
|
+
latitude = self.position.get('latitude')
|
|
71
|
+
longitude = self.position.get('longitude')
|
|
72
|
+
|
|
73
|
+
if latitude is not None and longitude is not None:
|
|
74
|
+
return f'POINT({longitude} {latitude})'
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def has_point(self: Self) -> bool:
|
|
80
|
+
"""Check if the message has a point."""
|
|
81
|
+
latitude = self.position.get('latitude')
|
|
82
|
+
longitude = self.position.get('longitude')
|
|
83
|
+
|
|
84
|
+
return latitude is not None and longitude is not None
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def parse_from_dict(cls, *, raw_payload: dict[str, Any], device: Device) -> DeviceMessage:
|
|
88
|
+
"""Format a DeviceMessage from a dictionary."""
|
|
89
|
+
received_at: datetime
|
|
90
|
+
position: dict[str, float | int] = {}
|
|
91
|
+
payload: dict[str, Any] = {}
|
|
92
|
+
|
|
93
|
+
if 'timestamp' in raw_payload:
|
|
94
|
+
received_at = datetime.fromtimestamp(raw_payload['timestamp'], tz=UTC)
|
|
95
|
+
else:
|
|
96
|
+
received_at = datetime.now(UTC)
|
|
97
|
+
|
|
98
|
+
for key, value in raw_payload.items():
|
|
99
|
+
if key.startswith('position.'):
|
|
100
|
+
position[key[9:]] = value
|
|
101
|
+
|
|
102
|
+
if key not in REJECTED_KEYS:
|
|
103
|
+
payload[key] = value
|
|
104
|
+
|
|
105
|
+
return cls(
|
|
106
|
+
ident=device.ident,
|
|
107
|
+
device_id=device.pk,
|
|
108
|
+
protocol_id=device.protocol_id,
|
|
109
|
+
position=position,
|
|
110
|
+
payload=payload,
|
|
111
|
+
received_at=received_at,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def to_message(self: Self) -> Message:
|
|
115
|
+
"""Convert the asset message to a Message object."""
|
|
116
|
+
return Message(
|
|
117
|
+
pk=self.pk if self.pk is not None else 0,
|
|
118
|
+
asset_id=self.device_id if self.device_id is not None else 0,
|
|
119
|
+
position=Position.model_validate(self.position),
|
|
120
|
+
payload=self.payload,
|
|
121
|
+
received_at=self.received_at,
|
|
122
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: layrz-sdk
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.15
|
|
4
4
|
Summary: Layrz SDK for Python
|
|
5
5
|
Author-email: "Golden M, Inc." <software@goldenm.com>
|
|
6
6
|
Maintainer-email: Kenny Mochizuki <kenny@goldenm.com>, Luis Reyes <lreyes@goldenm.com>, Kasen Li <kli@goldenm.com>
|
|
@@ -21,6 +21,8 @@ Requires-Dist: xlsxwriter
|
|
|
21
21
|
Requires-Dist: tzdata
|
|
22
22
|
Requires-Dist: pydantic>=2.10.6
|
|
23
23
|
Requires-Dist: typing-extensions>=4.10.0
|
|
24
|
+
Requires-Dist: geopy>=2.4.1
|
|
25
|
+
Requires-Dist: shapely>=2.1.1
|
|
24
26
|
Dynamic: license-file
|
|
25
27
|
|
|
26
28
|
# Layrz SDK
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
layrz_sdk/__init__.py,sha256=OutylN0QazaeDVIA5NRDVyzwfYnZkAwVQzT-2F6iX2M,28
|
|
2
|
-
layrz_sdk/constants.py,sha256=
|
|
2
|
+
layrz_sdk/constants.py,sha256=guXfIsVAcex76OEMv6DAJy1km1A_WUfWJuUO2Lo3kXE,344
|
|
3
3
|
layrz_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
layrz_sdk/entities/__init__.py,sha256=
|
|
5
|
-
layrz_sdk/entities/asset.py,sha256=
|
|
4
|
+
layrz_sdk/entities/__init__.py,sha256=FhAtb02yGzjqyQAV-MkMQBWJHjZ4s99uwz_uPkB5GP0,3963
|
|
5
|
+
layrz_sdk/entities/asset.py,sha256=DnTQ3_2W9CV31TZUD9l3pAyx_vCeRjuyYshfFsiZbKE,2489
|
|
6
6
|
layrz_sdk/entities/asset_operation_mode.py,sha256=--lXiayLBS_m4to2sHNOJO5duawvtqMYQKQrXBaL7tQ,634
|
|
7
7
|
layrz_sdk/entities/broadcast_request.py,sha256=MQb9kyDSAMXtp4_sPOamBsYyCVZ02wj8NXmi8IEmM68,260
|
|
8
8
|
layrz_sdk/entities/broadcast_response.py,sha256=hK3D05pecln9wbkdGr892Kzmd7bMzjsaayPxzJPVHsw,263
|
|
9
9
|
layrz_sdk/entities/broadcast_result.py,sha256=2OEd-1DVQt0T1KQR8S_wrYEplXKFVXbIouTW9wFHTh4,686
|
|
10
10
|
layrz_sdk/entities/broadcast_status.py,sha256=OJr0kt-m5R6WK_o0XwVexPLDd362_xhQObVOC1LgpNc,592
|
|
11
|
-
layrz_sdk/entities/case.py,sha256=
|
|
11
|
+
layrz_sdk/entities/case.py,sha256=0dSfZCezDuEEED42rws-zntqa_Em9oFevlXSPrIeIC8,1877
|
|
12
12
|
layrz_sdk/entities/case_ignored_status.py,sha256=0d29wzUR1XNOiWN57l6-DIlzwwDujnZ0a1BqlBjzAOw,512
|
|
13
13
|
layrz_sdk/entities/case_status.py,sha256=72UxRWBs60FVTDQNoscGmb60lsXwep1x1CMqlxd3Zao,438
|
|
14
14
|
layrz_sdk/entities/checkpoint.py,sha256=Y_LduHugWdn3sLvXmiqjaIXnGzoJ5aN_qLCzD9YXOhQ,513
|
|
15
15
|
layrz_sdk/entities/comment.py,sha256=1YSWVxXlpoicVVHv3mvkNlfUTfcOMxZYfb4YmwrxchY,414
|
|
16
16
|
layrz_sdk/entities/custom_field.py,sha256=jurONu01Ph-Qq8uuxdzxoQ6i7BqbZe64vO2ESvQDSFg,277
|
|
17
17
|
layrz_sdk/entities/custom_report_page.py,sha256=eqtoFOgip0z4_cMql1tHgaR4JiVEYlfbzd9ZJ7ZLyrM,1231
|
|
18
|
-
layrz_sdk/entities/device.py,sha256=
|
|
18
|
+
layrz_sdk/entities/device.py,sha256=Ql0Pz5VsCYIK2BtTeFL3i2AfOEEdTHScaSFVLTOG7Uk,698
|
|
19
19
|
layrz_sdk/entities/event.py,sha256=cFjFRrKQp4TykPkrCoXy5PYPykcAzrcHtAhRl_V4hdk,754
|
|
20
20
|
layrz_sdk/entities/geofence.py,sha256=jPuG5OCknkM37Q4h5haOXdV4OpVyd4gX77dFjd-FaP4,326
|
|
21
21
|
layrz_sdk/entities/last_message.py,sha256=QNgF0xCQ6uLFDMmKQCjSRhGS1Bq7akw5pv6ZIiFlwfY,268
|
|
@@ -32,6 +32,7 @@ layrz_sdk/entities/report_header.py,sha256=ChsxCqHe-Ik2t3DO3kxyja2WgILzxg12NPFcr
|
|
|
32
32
|
layrz_sdk/entities/report_page.py,sha256=ebhFEI6dYRM2jZzjedyIJSyPBAggySpmZjtcaILltxA,550
|
|
33
33
|
layrz_sdk/entities/report_row.py,sha256=t2Rk7LeforU-sD5abqnTQES2Oac8dmEXMLLo_18xg6Y,788
|
|
34
34
|
layrz_sdk/entities/sensor.py,sha256=9Q6RGb9qDvyLS4tELZOZtPlaiWi9An5Vf0gGLGWIRhE,312
|
|
35
|
+
layrz_sdk/entities/static_position.py,sha256=xTbTWRPQLZqTgPQnyIMOoMHiNi42AzmVRfgDMM4m03c,365
|
|
35
36
|
layrz_sdk/entities/text_alignment.py,sha256=H6noz4jMsuFfshpBQVMPx7K02g6t_82BBIgJ0_5YRRE,491
|
|
36
37
|
layrz_sdk/entities/trigger.py,sha256=fDcF72awhwk43j9heE053QneYk4rUfdm3RY2hitP91w,318
|
|
37
38
|
layrz_sdk/entities/user.py,sha256=S2mJoW44xbBe-O3I_ajy5l4V9-azVLUfKvcfsuqfodQ,236
|
|
@@ -64,12 +65,21 @@ layrz_sdk/entities/charts/table_row.py,sha256=WIqYbn0FF71AU7d-OqS--qXL7phBX95WC6
|
|
|
64
65
|
layrz_sdk/entities/charts/timeline_chart.py,sha256=6HQyf3LtTf3aWjF1-EctHV6A9jwVK9GcmD8mWhiPffY,2118
|
|
65
66
|
layrz_sdk/entities/charts/timeline_serie.py,sha256=lQtqPLiFrEuYoKhP6PybwQV0wCUPO_8SCT0St3YBRGk,322
|
|
66
67
|
layrz_sdk/entities/charts/timeline_serie_item.py,sha256=5wP2lpcD8MQYRbk_f3NB5tSiEtjJdnewatRb4ea3TG0,394
|
|
68
|
+
layrz_sdk/entities/modbus/__init__.py,sha256=tb37MWV2FyvPOU01adaekA-r59bsBU4chC6n1aUNmF0,281
|
|
69
|
+
layrz_sdk/entities/modbus/config.py,sha256=sgzSjA3CrjShD_Q_XdZWLI1Oki5nDRNtFK2tbwLtNcY,466
|
|
70
|
+
layrz_sdk/entities/modbus/parameter.py,sha256=at3Hg9xKCl85U-hSgBH_sggk68BLZAGGkl9tUn59h6o,3392
|
|
71
|
+
layrz_sdk/entities/modbus/schema.py,sha256=gyL0VyGSXTZWzsamjLUGoW5cqhn7MfXisCbH_FlFnu8,222
|
|
72
|
+
layrz_sdk/entities/modbus/status.py,sha256=VnsWAR7C2dz7Y6_dPCnvS3CBaMaP7odZSu6db2KaTlE,611
|
|
73
|
+
layrz_sdk/entities/modbus/wait.py,sha256=n3p_miYfGde1sxo4OznqWJAOb_WKblOnwFRib3RY_uM,4031
|
|
74
|
+
layrz_sdk/entities/telemetry/__init__.py,sha256=hfyekj-4_iB-aEbw43DXtSuMLBtMR1uoL8vvkRa_nJg,149
|
|
75
|
+
layrz_sdk/entities/telemetry/assetmessage.py,sha256=dChtlZfjlXxB_Ci4nHzaJuZEcoq2LaAI73Vtty3tMxc,4672
|
|
76
|
+
layrz_sdk/entities/telemetry/devicemessage.py,sha256=IHJD-euFFqtex9cPIdS2Qb-_VB2-U78fjCpGJipGYqo,3128
|
|
67
77
|
layrz_sdk/helpers/__init__.py,sha256=5iW3z2m3jrYhvTfxX-p-QTkR9X9oTKfEsbtVOg9jFFY,115
|
|
68
78
|
layrz_sdk/helpers/color.py,sha256=dlpMafbM-4Wd9D9hMbbnZJf4ALkpie_ZmBR2Vz_YCmM,1203
|
|
69
79
|
layrz_sdk/lcl/__init__.py,sha256=U967AWANkL3u_YVxMNAYlh8jkZ6hqHfStacz7yz6sOA,89
|
|
70
80
|
layrz_sdk/lcl/core.py,sha256=T80A3hL7SeqRNSfl5SrPAgwIEf-enoAVv9ldwJNzqsA,24786
|
|
71
|
-
layrz_sdk-3.1.
|
|
72
|
-
layrz_sdk-3.1.
|
|
73
|
-
layrz_sdk-3.1.
|
|
74
|
-
layrz_sdk-3.1.
|
|
75
|
-
layrz_sdk-3.1.
|
|
81
|
+
layrz_sdk-3.1.15.dist-info/licenses/LICENSE,sha256=d5ZrU--lIPER7QByXDKcrtOTOMk1JvN_9FdYDuoWi7Y,1057
|
|
82
|
+
layrz_sdk-3.1.15.dist-info/METADATA,sha256=ezVi7P-KQFBZFVK5JBPH2lnZdYxjecuUJkZEP_TpVp8,1910
|
|
83
|
+
layrz_sdk-3.1.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
84
|
+
layrz_sdk-3.1.15.dist-info/top_level.txt,sha256=yUTMMzfdZ0HDWQH5TaSlFM4xtwmP1fSGxmlL1dmu4l4,10
|
|
85
|
+
layrz_sdk-3.1.15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|