PyPlumIO 0.5.25__py3-none-any.whl → 0.5.26__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.
Files changed (37) hide show
  1. {PyPlumIO-0.5.25.dist-info → PyPlumIO-0.5.26.dist-info}/METADATA +7 -7
  2. PyPlumIO-0.5.26.dist-info/RECORD +60 -0
  3. {PyPlumIO-0.5.25.dist-info → PyPlumIO-0.5.26.dist-info}/WHEEL +1 -1
  4. pyplumio/_version.py +2 -2
  5. pyplumio/devices/__init__.py +27 -28
  6. pyplumio/devices/ecomax.py +51 -57
  7. pyplumio/devices/ecoster.py +3 -5
  8. pyplumio/devices/mixer.py +5 -8
  9. pyplumio/devices/thermostat.py +3 -3
  10. pyplumio/filters.py +6 -6
  11. pyplumio/frames/__init__.py +6 -6
  12. pyplumio/frames/messages.py +3 -3
  13. pyplumio/frames/requests.py +18 -18
  14. pyplumio/frames/responses.py +15 -15
  15. pyplumio/helpers/data_types.py +104 -68
  16. pyplumio/helpers/event_manager.py +7 -7
  17. pyplumio/helpers/factory.py +5 -6
  18. pyplumio/helpers/parameter.py +17 -15
  19. pyplumio/helpers/schedule.py +50 -46
  20. pyplumio/helpers/timeout.py +1 -1
  21. pyplumio/protocol.py +6 -7
  22. pyplumio/structures/alerts.py +8 -6
  23. pyplumio/structures/ecomax_parameters.py +30 -26
  24. pyplumio/structures/frame_versions.py +2 -3
  25. pyplumio/structures/mixer_parameters.py +9 -6
  26. pyplumio/structures/mixer_sensors.py +10 -8
  27. pyplumio/structures/modules.py +9 -7
  28. pyplumio/structures/network_info.py +16 -16
  29. pyplumio/structures/program_version.py +3 -0
  30. pyplumio/structures/regulator_data.py +2 -4
  31. pyplumio/structures/regulator_data_schema.py +2 -3
  32. pyplumio/structures/schedules.py +33 -35
  33. pyplumio/structures/thermostat_parameters.py +6 -4
  34. pyplumio/structures/thermostat_sensors.py +13 -10
  35. PyPlumIO-0.5.25.dist-info/RECORD +0 -60
  36. {PyPlumIO-0.5.25.dist-info → PyPlumIO-0.5.26.dist-info}/LICENSE +0 -0
  37. {PyPlumIO-0.5.25.dist-info → PyPlumIO-0.5.26.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, ClassVar
5
+ from typing import Any
6
6
 
7
7
  from pyplumio.const import (
8
8
  ATTR_COUNT,
@@ -25,7 +25,7 @@ class ProgramVersionRequest(Request):
25
25
 
26
26
  __slots__ = ()
27
27
 
28
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_PROGRAM_VERSION
28
+ frame_type = FrameType.REQUEST_PROGRAM_VERSION
29
29
 
30
30
  def response(self, **kwargs: Any) -> Response | None:
31
31
  """Return a response frame."""
@@ -37,7 +37,7 @@ class CheckDeviceRequest(Request):
37
37
 
38
38
  __slots__ = ()
39
39
 
40
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_CHECK_DEVICE
40
+ frame_type = FrameType.REQUEST_CHECK_DEVICE
41
41
 
42
42
  def response(self, **kwargs: Any) -> Response | None:
43
43
  """Return a response frame."""
@@ -49,7 +49,7 @@ class UIDRequest(Request):
49
49
 
50
50
  __slots__ = ()
51
51
 
52
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_UID
52
+ frame_type = FrameType.REQUEST_UID
53
53
 
54
54
 
55
55
  class PasswordRequest(Request):
@@ -57,7 +57,7 @@ class PasswordRequest(Request):
57
57
 
58
58
  __slots__ = ()
59
59
 
60
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_PASSWORD
60
+ frame_type = FrameType.REQUEST_PASSWORD
61
61
 
62
62
 
63
63
  class EcomaxParametersRequest(Request):
@@ -68,7 +68,7 @@ class EcomaxParametersRequest(Request):
68
68
 
69
69
  __slots__ = ()
70
70
 
71
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_ECOMAX_PARAMETERS
71
+ frame_type = FrameType.REQUEST_ECOMAX_PARAMETERS
72
72
 
73
73
  def create_message(self, data: dict[str, Any]) -> bytearray:
74
74
  """Create a frame message."""
@@ -84,7 +84,7 @@ class MixerParametersRequest(Request):
84
84
 
85
85
  __slots__ = ()
86
86
 
87
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_MIXER_PARAMETERS
87
+ frame_type = FrameType.REQUEST_MIXER_PARAMETERS
88
88
 
89
89
  def create_message(self, data: dict[str, Any]) -> bytearray:
90
90
  """Create a frame message."""
@@ -100,7 +100,7 @@ class ThermostatParametersRequest(Request):
100
100
 
101
101
  __slots__ = ()
102
102
 
103
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_THERMOSTAT_PARAMETERS
103
+ frame_type = FrameType.REQUEST_THERMOSTAT_PARAMETERS
104
104
 
105
105
  def create_message(self, data: dict[str, Any]) -> bytearray:
106
106
  """Create a frame message."""
@@ -112,7 +112,7 @@ class RegulatorDataSchemaRequest(Request):
112
112
 
113
113
  __slots__ = ()
114
114
 
115
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_REGULATOR_DATA_SCHEMA
115
+ frame_type = FrameType.REQUEST_REGULATOR_DATA_SCHEMA
116
116
 
117
117
 
118
118
  class SetEcomaxParameterRequest(Request):
@@ -123,7 +123,7 @@ class SetEcomaxParameterRequest(Request):
123
123
 
124
124
  __slots__ = ()
125
125
 
126
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_SET_ECOMAX_PARAMETER
126
+ frame_type = FrameType.REQUEST_SET_ECOMAX_PARAMETER
127
127
 
128
128
  def create_message(self, data: dict[str, Any]) -> bytearray:
129
129
  """Create a frame message."""
@@ -141,7 +141,7 @@ class SetMixerParameterRequest(Request):
141
141
 
142
142
  __slots__ = ()
143
143
 
144
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_SET_MIXER_PARAMETER
144
+ frame_type = FrameType.REQUEST_SET_MIXER_PARAMETER
145
145
 
146
146
  def create_message(self, data: dict[str, Any]) -> bytearray:
147
147
  """Create a frame message."""
@@ -169,7 +169,7 @@ class SetThermostatParameterRequest(Request):
169
169
 
170
170
  __slots__ = ()
171
171
 
172
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_SET_THERMOSTAT_PARAMETER
172
+ frame_type = FrameType.REQUEST_SET_THERMOSTAT_PARAMETER
173
173
 
174
174
  def create_message(self, data: dict[str, Any]) -> bytearray:
175
175
  """Create a frame message."""
@@ -192,7 +192,7 @@ class EcomaxControlRequest(Request):
192
192
 
193
193
  __slots__ = ()
194
194
 
195
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_ECOMAX_CONTROL
195
+ frame_type = FrameType.REQUEST_ECOMAX_CONTROL
196
196
 
197
197
  def create_message(self, data: dict[str, Any]) -> bytearray:
198
198
  """Create a frame message."""
@@ -211,7 +211,7 @@ class StartMasterRequest(Request):
211
211
 
212
212
  __slots__ = ()
213
213
 
214
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_START_MASTER
214
+ frame_type = FrameType.REQUEST_START_MASTER
215
215
 
216
216
 
217
217
  class StopMasterRequest(Request):
@@ -223,7 +223,7 @@ class StopMasterRequest(Request):
223
223
 
224
224
  __slots__ = ()
225
225
 
226
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_STOP_MASTER
226
+ frame_type = FrameType.REQUEST_STOP_MASTER
227
227
 
228
228
 
229
229
  class AlertsRequest(Request):
@@ -235,7 +235,7 @@ class AlertsRequest(Request):
235
235
 
236
236
  __slots__ = ()
237
237
 
238
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_ALERTS
238
+ frame_type = FrameType.REQUEST_ALERTS
239
239
 
240
240
  def create_message(self, data: dict[str, Any]) -> bytearray:
241
241
  """Create a frame message."""
@@ -247,7 +247,7 @@ class SchedulesRequest(Request):
247
247
 
248
248
  __slots__ = ()
249
249
 
250
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_SCHEDULES
250
+ frame_type = FrameType.REQUEST_SCHEDULES
251
251
 
252
252
 
253
253
  class SetScheduleRequest(Request):
@@ -255,7 +255,7 @@ class SetScheduleRequest(Request):
255
255
 
256
256
  __slots__ = ()
257
257
 
258
- frame_type: ClassVar[FrameType] = FrameType.REQUEST_SET_SCHEDULE
258
+ frame_type = FrameType.REQUEST_SET_SCHEDULE
259
259
 
260
260
  def create_message(self, data: dict[str, Any]) -> bytearray:
261
261
  """Create a frame message."""
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, ClassVar
5
+ from typing import Any
6
6
 
7
7
  from pyplumio.const import ATTR_PASSWORD, FrameType
8
8
  from pyplumio.frames import Response
@@ -25,7 +25,7 @@ class ProgramVersionResponse(Response):
25
25
 
26
26
  __slots__ = ()
27
27
 
28
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_PROGRAM_VERSION
28
+ frame_type = FrameType.RESPONSE_PROGRAM_VERSION
29
29
 
30
30
  def create_message(self, data: dict[str, Any]) -> bytearray:
31
31
  """Create a frame message."""
@@ -44,7 +44,7 @@ class DeviceAvailableResponse(Response):
44
44
 
45
45
  __slots__ = ()
46
46
 
47
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_DEVICE_AVAILABLE
47
+ frame_type = FrameType.RESPONSE_DEVICE_AVAILABLE
48
48
 
49
49
  def create_message(self, data: dict[str, Any]) -> bytearray:
50
50
  """Create a frame message."""
@@ -63,7 +63,7 @@ class UIDResponse(Response):
63
63
 
64
64
  __slots__ = ()
65
65
 
66
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_UID
66
+ frame_type = FrameType.RESPONSE_UID
67
67
 
68
68
  def create_message(self, data: dict[str, Any]) -> bytearray:
69
69
  """Create a frame message."""
@@ -82,7 +82,7 @@ class PasswordResponse(Response):
82
82
 
83
83
  __slots__ = ()
84
84
 
85
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_PASSWORD
85
+ frame_type = FrameType.RESPONSE_PASSWORD
86
86
 
87
87
  def decode_message(self, message: bytearray) -> dict[str, Any]:
88
88
  """Decode a frame message."""
@@ -98,7 +98,7 @@ class EcomaxParametersResponse(Response):
98
98
 
99
99
  __slots__ = ()
100
100
 
101
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_ECOMAX_PARAMETERS
101
+ frame_type = FrameType.RESPONSE_ECOMAX_PARAMETERS
102
102
 
103
103
  def decode_message(self, message: bytearray) -> dict[str, Any]:
104
104
  """Decode a frame message."""
@@ -113,7 +113,7 @@ class MixerParametersResponse(Response):
113
113
 
114
114
  __slots__ = ()
115
115
 
116
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_MIXER_PARAMETERS
116
+ frame_type = FrameType.RESPONSE_MIXER_PARAMETERS
117
117
 
118
118
  def decode_message(self, message: bytearray) -> dict[str, Any]:
119
119
  """Decode a frame message."""
@@ -128,7 +128,7 @@ class ThermostatParametersResponse(Response):
128
128
 
129
129
  __slots__ = ()
130
130
 
131
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_THERMOSTAT_PARAMETERS
131
+ frame_type = FrameType.RESPONSE_THERMOSTAT_PARAMETERS
132
132
 
133
133
  def decode_message(self, message: bytearray) -> dict[str, Any]:
134
134
  """Decode a frame message."""
@@ -144,7 +144,7 @@ class RegulatorDataSchemaResponse(Response):
144
144
 
145
145
  __slots__ = ()
146
146
 
147
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_REGULATOR_DATA_SCHEMA
147
+ frame_type = FrameType.RESPONSE_REGULATOR_DATA_SCHEMA
148
148
 
149
149
  def decode_message(self, message: bytearray) -> dict[str, Any]:
150
150
  """Decode a frame message."""
@@ -160,7 +160,7 @@ class SetEcomaxParameterResponse(Response):
160
160
 
161
161
  __slots__ = ()
162
162
 
163
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_SET_ECOMAX_PARAMETER
163
+ frame_type = FrameType.RESPONSE_SET_ECOMAX_PARAMETER
164
164
 
165
165
 
166
166
  class SetMixerParameterResponse(Response):
@@ -172,7 +172,7 @@ class SetMixerParameterResponse(Response):
172
172
 
173
173
  __slots__ = ()
174
174
 
175
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_SET_MIXER_PARAMETER
175
+ frame_type = FrameType.RESPONSE_SET_MIXER_PARAMETER
176
176
 
177
177
 
178
178
  class SetThermostatParameterResponse(Response):
@@ -184,7 +184,7 @@ class SetThermostatParameterResponse(Response):
184
184
 
185
185
  __slots__ = ()
186
186
 
187
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_SET_THERMOSTAT_PARAMETER
187
+ frame_type = FrameType.RESPONSE_SET_THERMOSTAT_PARAMETER
188
188
 
189
189
 
190
190
  class EcomaxControlResponse(Response):
@@ -196,7 +196,7 @@ class EcomaxControlResponse(Response):
196
196
 
197
197
  __slots__ = ()
198
198
 
199
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_ECOMAX_CONTROL
199
+ frame_type = FrameType.RESPONSE_ECOMAX_CONTROL
200
200
 
201
201
 
202
202
  class AlertsResponse(Response):
@@ -204,7 +204,7 @@ class AlertsResponse(Response):
204
204
 
205
205
  __slots__ = ()
206
206
 
207
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_ALERTS
207
+ frame_type = FrameType.RESPONSE_ALERTS
208
208
 
209
209
  def decode_message(self, message: bytearray) -> dict[str, Any]:
210
210
  """Decode a frame message."""
@@ -216,7 +216,7 @@ class SchedulesResponse(Response):
216
216
 
217
217
  __slots__ = ()
218
218
 
219
- frame_type: ClassVar[FrameType] = FrameType.RESPONSE_SCHEDULES
219
+ frame_type = FrameType.RESPONSE_SCHEDULES
220
220
 
221
221
  def decode_message(self, message: bytearray) -> dict[str, Any]:
222
222
  """Decode a frame message."""
@@ -5,36 +5,58 @@ from __future__ import annotations
5
5
  from abc import ABC, abstractmethod
6
6
  import socket
7
7
  import struct
8
- from typing import Any, ClassVar, Final, TypeVar
8
+ from typing import ClassVar, Final, Generic, TypeVar
9
9
 
10
+ T = TypeVar("T")
11
+ DataTypeT = TypeVar("DataTypeT", bound="DataType")
10
12
 
11
- class DataType(ABC):
12
- """Represents a base data type."""
13
+
14
+ class DataType(ABC, Generic[T]):
15
+ """Represents a data type."""
13
16
 
14
17
  __slots__ = ("_value", "_size")
15
18
 
16
- _value: Any
19
+ _value: T
17
20
  _size: int
18
21
 
19
- def __init__(self, value: Any = None):
22
+ def __init__(self, value: T | None = None):
20
23
  """Initialize a new data type."""
21
- self._value = value
24
+ if value is not None:
25
+ self._value = value
26
+
22
27
  self._size = 0
23
28
 
24
29
  def __repr__(self) -> str:
25
30
  """Return serializable string representation of the class."""
26
- return f"{self.__class__.__name__}(value={self._value})"
31
+ if hasattr(self, "_value"):
32
+ return f"{self.__class__.__name__}(value={self._value})"
33
+
34
+ return f"{self.__class__.__name__}()"
27
35
 
28
36
  def __eq__(self, other: object) -> bool:
29
37
  """Compare if this data type is equal to other."""
30
- if isinstance(other, DataType):
38
+ if (
39
+ isinstance(other, DataType)
40
+ and hasattr(other, "_value")
41
+ and hasattr(self, "_value")
42
+ ):
31
43
  return bool(self._value == other._value)
32
44
 
33
- return bool(self._value == other)
45
+ if (
46
+ isinstance(other, DataType)
47
+ and not hasattr(other, "_value")
48
+ and not hasattr(self, "_value")
49
+ ):
50
+ return type(self) is type(other)
51
+
52
+ if hasattr(self, "_value"):
53
+ return bool(self._value == other)
54
+
55
+ return NotImplemented
34
56
 
35
57
  def _slice_data(self, data: bytes) -> bytes:
36
58
  """Slice the data to data type size."""
37
- return data[: self.size] if self.size is not None else data
59
+ return data if self.size == 0 else data[: self.size]
38
60
 
39
61
  @classmethod
40
62
  def from_bytes(cls: type[DataTypeT], data: bytes, offset: int = 0) -> DataTypeT:
@@ -48,7 +70,7 @@ class DataType(ABC):
48
70
  return self.pack()
49
71
 
50
72
  @property
51
- def value(self) -> Any:
73
+ def value(self) -> T:
52
74
  """Return the data type value."""
53
75
  return self._value
54
76
 
@@ -66,9 +88,6 @@ class DataType(ABC):
66
88
  """Unpack the data."""
67
89
 
68
90
 
69
- DataTypeT = TypeVar("DataTypeT", bound=DataType)
70
-
71
-
72
91
  class Undefined(DataType):
73
92
  """Represents an undefined."""
74
93
 
@@ -86,21 +105,26 @@ class Undefined(DataType):
86
105
  BITARRAY_LAST_INDEX: Final = 7
87
106
 
88
107
 
89
- class BitArray(DataType):
108
+ class BitArray(DataType[int]):
90
109
  """Represents a bit array."""
91
110
 
92
111
  __slots__ = ("_index",)
93
112
 
94
113
  _index: int
95
114
 
96
- def __init__(self, value: Any = None, index: int = 0):
115
+ def __init__(self, value: bool | None = None, index: int = 0):
97
116
  """Initialize a new bit array."""
98
117
  super().__init__(value)
99
118
  self._index = index
100
119
 
101
120
  def __repr__(self) -> str:
102
121
  """Return serializable string representation of the class."""
103
- return f"{self.__class__.__name__}(value={self._value}, index={self._index})"
122
+ if hasattr(self, "_value"):
123
+ return (
124
+ f"{self.__class__.__name__}(value={self._value}, index={self._index})"
125
+ )
126
+
127
+ return f"{self.__class__.__name__}(index={self._index})"
104
128
 
105
129
  def next(self, index: int = 0) -> int:
106
130
  """Set current bit and return the next index in the bit array."""
@@ -109,16 +133,22 @@ class BitArray(DataType):
109
133
 
110
134
  def pack(self) -> bytes:
111
135
  """Pack the data."""
112
- return UnsignedChar(self._value).to_bytes()
136
+ if hasattr(self, "_value"):
137
+ return UnsignedChar(self._value).to_bytes()
138
+
139
+ return b""
113
140
 
114
141
  def unpack(self, data: bytes) -> None:
115
142
  """Unpack the data."""
116
143
  self._value = UnsignedChar.from_bytes(data[:1]).value
117
144
 
118
145
  @property
119
- def value(self) -> bool | None:
146
+ def value(self) -> bool:
120
147
  """Return the data type value."""
121
- return None if self._value is None else bool(self._value & (1 << self._index))
148
+ if hasattr(self, "_value"):
149
+ return bool(self._value & (1 << self._index))
150
+
151
+ raise ValueError
122
152
 
123
153
  @property
124
154
  def size(self) -> int:
@@ -126,7 +156,7 @@ class BitArray(DataType):
126
156
  return 1 if self._index == BITARRAY_LAST_INDEX else 0
127
157
 
128
158
 
129
- class IPv4(DataType):
159
+ class IPv4(DataType[str]):
130
160
  """Represents an IPv4 address."""
131
161
 
132
162
  __slots__ = ()
@@ -145,7 +175,7 @@ class IPv4(DataType):
145
175
  self._value = socket.inet_ntoa(self._slice_data(data))
146
176
 
147
177
 
148
- class IPv6(DataType):
178
+ class IPv6(DataType[str]):
149
179
  """Represents an IPv6 address."""
150
180
 
151
181
  __slots__ = ()
@@ -164,20 +194,19 @@ class IPv6(DataType):
164
194
  self._value = socket.inet_ntop(socket.AF_INET6, self._slice_data(data))
165
195
 
166
196
 
167
- class String(DataType):
168
- """Represents a null terminated string."""
197
+ class String(DataType[str]):
198
+ """Represents a null-terminated string."""
169
199
 
170
200
  __slots__ = ()
171
201
 
172
- def __init__(self, value: Any = ""):
173
- """Initialize a new null terminated string data type."""
202
+ def __init__(self, value: str = ""):
203
+ """Initialize a new null-terminated string data type."""
174
204
  super().__init__(value)
175
205
  self._size = len(self.value) + 1
176
206
 
177
207
  def pack(self) -> bytes:
178
208
  """Pack the data."""
179
- value: str = self.value
180
- return value.encode() + b"\0"
209
+ return self.value.encode() + b"\0"
181
210
 
182
211
  def unpack(self, data: bytes) -> None:
183
212
  """Unpack the data."""
@@ -185,20 +214,19 @@ class String(DataType):
185
214
  self._size = len(self.value) + 1
186
215
 
187
216
 
188
- class VarBytes(DataType):
189
- """Represents a variable length bytes."""
217
+ class VarBytes(DataType[bytes]):
218
+ """Represents a variable-length bytes."""
190
219
 
191
220
  __slots__ = ()
192
221
 
193
- def __init__(self, value: Any = b""):
194
- """Initialize a new variable length bytes data type."""
222
+ def __init__(self, value: bytes = b""):
223
+ """Initialize a new variable-length bytes data type."""
195
224
  super().__init__(value)
196
225
  self._size = len(value) + 1
197
226
 
198
227
  def pack(self) -> bytes:
199
228
  """Pack the data."""
200
- value: bytes = self.value
201
- return UnsignedChar(self.size - 1).to_bytes() + value
229
+ return UnsignedChar(self.size - 1).to_bytes() + self.value
202
230
 
203
231
  def unpack(self, data: bytes) -> None:
204
232
  """Unpack the data."""
@@ -206,26 +234,30 @@ class VarBytes(DataType):
206
234
  self._value = data[1 : self.size]
207
235
 
208
236
 
209
- class VarString(VarBytes):
237
+ class VarString(DataType[str]):
210
238
  """Represents a variable length string."""
211
239
 
212
240
  __slots__ = ()
213
241
 
242
+ def __init__(self, value: str = ""):
243
+ """Initialize a new variable length bytes data type."""
244
+ super().__init__(value)
245
+ self._size = len(value) + 1
246
+
214
247
  def pack(self) -> bytes:
215
248
  """Pack the data."""
216
- value: str = self.value
217
- return UnsignedChar(self.size - 1).to_bytes() + value.encode()
249
+ return UnsignedChar(self.size - 1).to_bytes() + self.value.encode()
218
250
 
219
251
  def unpack(self, data: bytes) -> None:
220
252
  """Unpack the data."""
221
- super().unpack(data)
222
- self._value = self.value.decode()
253
+ self._size = data[0] + 1
254
+ self._value = data[1 : self.size].decode("utf-8", "replace")
223
255
 
224
256
 
225
- class BuiltInDataType(DataType, ABC):
257
+ class BuiltInDataType(DataType[T], ABC):
226
258
  """Represents a data type that is supported by the struct module."""
227
259
 
228
- __slots__ = ()
260
+ __slots__ = ("_struct",)
229
261
 
230
262
  _struct: ClassVar[struct.Struct]
231
263
 
@@ -240,86 +272,90 @@ class BuiltInDataType(DataType, ABC):
240
272
  @property
241
273
  def size(self) -> int:
242
274
  """Return a data type size."""
243
- return self._struct.size
275
+ if not self._size:
276
+ self._size = self._struct.size
244
277
 
278
+ return self._size
245
279
 
246
- class SignedChar(BuiltInDataType):
280
+
281
+ class SignedChar(BuiltInDataType[int]):
247
282
  """Represents a signed char."""
248
283
 
249
284
  __slots__ = ()
250
285
 
251
- _struct: ClassVar[struct.Struct] = struct.Struct("<b")
286
+ _struct = struct.Struct("<b")
252
287
 
253
288
 
254
- class UnsignedChar(BuiltInDataType):
289
+ class UnsignedChar(BuiltInDataType[int]):
255
290
  """Represents an unsigned char."""
256
291
 
257
292
  __slots__ = ()
258
293
 
259
- _struct: ClassVar[struct.Struct] = struct.Struct("<B")
294
+ _struct = struct.Struct("<B")
260
295
 
261
296
 
262
- class Short(BuiltInDataType):
263
- """Represents a 16 bit integer."""
297
+ class Short(BuiltInDataType[int]):
298
+ """Represents a 16-bit integer."""
264
299
 
265
300
  __slots__ = ()
266
301
 
267
- _struct: ClassVar[struct.Struct] = struct.Struct("<h")
302
+ _struct = struct.Struct("<h")
268
303
 
269
304
 
270
- class UnsignedShort(BuiltInDataType):
271
- """Represents an unsigned 16 bit integer."""
305
+ class UnsignedShort(BuiltInDataType[int]):
306
+ """Represents an unsigned 16-bit integer."""
272
307
 
273
308
  __slots__ = ()
274
309
 
275
- _struct: ClassVar[struct.Struct] = struct.Struct("<H")
310
+ _struct = struct.Struct("<H")
276
311
 
277
312
 
278
- class Int(BuiltInDataType):
279
- """Represents a 32 bit integer."""
313
+ class Int(BuiltInDataType[int]):
314
+ """Represents a 32-bit integer."""
280
315
 
281
316
  __slots__ = ()
282
317
 
283
- _struct: ClassVar[struct.Struct] = struct.Struct("<i")
318
+ _struct = struct.Struct("<i")
284
319
 
285
320
 
286
- class UnsignedInt(BuiltInDataType):
287
- """Represents a unsigned 32 bit integer."""
321
+ class UnsignedInt(BuiltInDataType[int]):
322
+ """Represents a unsigned 32-bit integer."""
288
323
 
289
324
  __slots__ = ()
290
325
 
291
- _struct: ClassVar[struct.Struct] = struct.Struct("<I")
326
+ _struct = struct.Struct("<I")
292
327
 
293
328
 
294
- class Float(BuiltInDataType):
329
+ class Float(BuiltInDataType[int]):
295
330
  """Represents a float."""
296
331
 
297
332
  __slots__ = ()
298
333
 
299
- _struct: ClassVar[struct.Struct] = struct.Struct("<f")
334
+ _struct = struct.Struct("<f")
300
335
 
301
336
 
302
- class Double(BuiltInDataType):
337
+ class Double(BuiltInDataType[int]):
303
338
  """Represents a double."""
304
339
 
305
340
  __slots__ = ()
306
341
 
307
- _struct: ClassVar[struct.Struct] = struct.Struct("<d")
342
+ _struct = struct.Struct("<d")
308
343
 
309
344
 
310
- class Int64(BuiltInDataType):
311
- """Represents a 64 bit signed integer."""
345
+ class Int64(BuiltInDataType[int]):
346
+ """Represents a 64-bit signed integer."""
312
347
 
313
348
  __slots__ = ()
314
349
 
315
- _struct: ClassVar[struct.Struct] = struct.Struct("<q")
350
+ _struct = struct.Struct("<q")
316
351
 
317
352
 
318
- class UInt64(BuiltInDataType):
319
- """Represents a 64 bit unsigned integer."""
353
+ class UInt64(BuiltInDataType[int]):
354
+ """Represents a 64-bit unsigned integer."""
320
355
 
321
356
  __slots__ = ()
322
- _struct: ClassVar[struct.Struct] = struct.Struct("<Q")
357
+
358
+ _struct = struct.Struct("<Q")
323
359
 
324
360
 
325
361
  # The regdata type map links data type classes to their