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 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 """
@@ -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
  ]
@@ -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"""
@@ -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
- data['sequence'] = f'{data["trigger"].code}/{sequence}'
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
 
@@ -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,10 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class ModbusSchema(StrEnum):
5
+ """Modbus schema enumeration"""
6
+
7
+ SINGLE = 'SINGLE'
8
+ """ Defines a single Modbus request. """
9
+ MULTIPLE = 'MULTIPLE'
10
+ """ Defines multiple Modbus requests. """
@@ -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,6 @@
1
+ """Telemetry models"""
2
+
3
+ from .assetmessage import AssetMessage
4
+ from .devicemessage import DeviceMessage
5
+
6
+ __all__ = ['AssetMessage', 'DeviceMessage']
@@ -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.13
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=Vvk_dYMGbkKWj6OHMeR09Bg6oqBkFzhYRLwkMLZMfI8,70
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=hPTwrCvl1-k5qfZE-a0tHnLsrUZGtGXT5KyopVPsQJI,3694
5
- layrz_sdk/entities/asset.py,sha256=RwsZv9AIho9kCOiG6EHeEPwfkb8fyIB4pyKQXN2pr3Y,2137
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=Rns7bVXhWzeuqsUGP70SxO8YKbUKBlWoP_2n682LG20,1735
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=M3xUn5uwA1xeEC1UT4gCRO-H58H52aWr_Ife8Vjp5rE,495
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.13.dist-info/licenses/LICENSE,sha256=d5ZrU--lIPER7QByXDKcrtOTOMk1JvN_9FdYDuoWi7Y,1057
72
- layrz_sdk-3.1.13.dist-info/METADATA,sha256=I6iV7UTX0SrDNbLrO5fs4QvPcGl7QH6s-q7I2PkuCoo,1852
73
- layrz_sdk-3.1.13.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
74
- layrz_sdk-3.1.13.dist-info/top_level.txt,sha256=yUTMMzfdZ0HDWQH5TaSlFM4xtwmP1fSGxmlL1dmu4l4,10
75
- layrz_sdk-3.1.13.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5