aio-ld2410 0.1.0__tar.gz

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.
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Mòrian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include LICENSE
2
+ include README.md
3
+ global-exclude __pycache__
4
+ global-exclude *.py[cod]
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.1
2
+ Name: aio_ld2410
3
+ Version: 0.1.0
4
+ Summary: Asynchonous library for the HiLink LD2410 presence detector module
5
+ Author-email: Romain Bezut <morian@xdec.net>
6
+ Classifier: Intended Audience :: Developers
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: Home Automation
14
+ Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
15
+ Classifier: Topic :: Software Development :: Embedded Systems
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: System :: Hardware
18
+ Classifier: Topic :: Terminals :: Serial
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: async-timeout>=4; python_version < "3.11"
23
+ Requires-Dist: construct<3.0,>=2.10
24
+ Requires-Dist: dacite<2.0,>=1.8
25
+ Requires-Dist: pyserial-asyncio-fast<1.0,>=0.14
26
+
27
+ Python asyncio LD2410 Library
28
+ =============================
29
+
30
+ `aio_ld2410` allows you to interact with the LD2410 radar sensors from Hi-Link using asyncio.
31
+
32
+
33
+ ## How to install
34
+
35
+ This package requires python 3.9 or later, and depends on the following packages:
36
+ - [construct](https://pypi.org/project/construct/) for binary serialization/deserialization
37
+ - [dacite](https://pypi.org/project/dacite/) to build dataclasses with a minimal footprint
38
+ - [pyserial-asyncio-fast](https://pypi.org/project/pyserial-asyncio-fast/) for serial async communication
39
+
40
+
41
+ ### Install from pip
42
+
43
+ ```console
44
+ $ pip install aio-ld2410
45
+ ```
46
+
47
+ ### Install for development
48
+ ```console
49
+ $ python -m venv venv
50
+ $ source venv/bin/activate
51
+ $ make install
52
+ ```
53
+
54
+ ## Implementation references
55
+
56
+ User manual and serial communication protocol can be found on
57
+ [Hi-Link Website](https://www.hlktech.net/index.php?id=1095).
58
+
59
+ This implementation was originally based on `LD2410C Serial communication protocol V1.00.pdf`.
60
+
61
+ Auxiliary commands were implemented based on `LD2410B Serial communication protocol V1.06.pdf`,
62
+ translated from Chinese, as mentioned in the following comment:
63
+ - https://github.com/esphome/feature-requests/issues/2156#issuecomment-1472962509
64
+
65
+ Note that some commands may not be available depending on your device model and firmware version.
66
+
67
+
68
+ ## Example usage
69
+
70
+ ```python
71
+ from aio_ld2410 import LD2410
72
+
73
+ async def async_main():
74
+ async with LD2410('/dev/ttyUSB0', baudrate=256000) as device:
75
+ async with device.configure():
76
+ ver = await device.get_firmware_version()
77
+ print(f'Running with firmware {ver}')
78
+
79
+ # Ask for engineering (advanced) reports as well.
80
+ await device.set_engineering_mode(True)
81
+
82
+ # Reports are generated every 100ms.
83
+ async for report in device.get_reports():
84
+ print(report)
85
+ ```
86
+
87
+ Full documentation is not written yet.
@@ -0,0 +1,61 @@
1
+ Python asyncio LD2410 Library
2
+ =============================
3
+
4
+ `aio_ld2410` allows you to interact with the LD2410 radar sensors from Hi-Link using asyncio.
5
+
6
+
7
+ ## How to install
8
+
9
+ This package requires python 3.9 or later, and depends on the following packages:
10
+ - [construct](https://pypi.org/project/construct/) for binary serialization/deserialization
11
+ - [dacite](https://pypi.org/project/dacite/) to build dataclasses with a minimal footprint
12
+ - [pyserial-asyncio-fast](https://pypi.org/project/pyserial-asyncio-fast/) for serial async communication
13
+
14
+
15
+ ### Install from pip
16
+
17
+ ```console
18
+ $ pip install aio-ld2410
19
+ ```
20
+
21
+ ### Install for development
22
+ ```console
23
+ $ python -m venv venv
24
+ $ source venv/bin/activate
25
+ $ make install
26
+ ```
27
+
28
+ ## Implementation references
29
+
30
+ User manual and serial communication protocol can be found on
31
+ [Hi-Link Website](https://www.hlktech.net/index.php?id=1095).
32
+
33
+ This implementation was originally based on `LD2410C Serial communication protocol V1.00.pdf`.
34
+
35
+ Auxiliary commands were implemented based on `LD2410B Serial communication protocol V1.06.pdf`,
36
+ translated from Chinese, as mentioned in the following comment:
37
+ - https://github.com/esphome/feature-requests/issues/2156#issuecomment-1472962509
38
+
39
+ Note that some commands may not be available depending on your device model and firmware version.
40
+
41
+
42
+ ## Example usage
43
+
44
+ ```python
45
+ from aio_ld2410 import LD2410
46
+
47
+ async def async_main():
48
+ async with LD2410('/dev/ttyUSB0', baudrate=256000) as device:
49
+ async with device.configure():
50
+ ver = await device.get_firmware_version()
51
+ print(f'Running with firmware {ver}')
52
+
53
+ # Ask for engineering (advanced) reports as well.
54
+ await device.set_engineering_mode(True)
55
+
56
+ # Reports are generated every 100ms.
57
+ async for report in device.get_reports():
58
+ print(report)
59
+ ```
60
+
61
+ Full documentation is not written yet.
@@ -0,0 +1,40 @@
1
+ from .exception import AioLd2410Error, CommandError, CommandStatusError, ModuleRestartedError
2
+ from .ld2410 import LD2410
3
+ from .models import (
4
+ AuxiliaryControlConfig,
5
+ AuxiliaryControlStatus,
6
+ ConfigModeStatus,
7
+ FirmwareVersion,
8
+ GateSensitivityConfig,
9
+ ParametersConfig,
10
+ ParametersStatus,
11
+ ReportBasicStatus,
12
+ ReportEngineeringStatus,
13
+ ReportStatus,
14
+ )
15
+ from .protocol import AuxiliaryControl, BaudRateIndex, OutPinLevel, TargetStatus
16
+ from .version import version
17
+
18
+ __version__ = version
19
+ __all__ = [
20
+ 'AioLd2410Error',
21
+ 'AuxiliaryControl',
22
+ 'AuxiliaryControlConfig',
23
+ 'AuxiliaryControlStatus',
24
+ 'BaudRateIndex',
25
+ 'CommandError',
26
+ 'CommandStatusError',
27
+ 'ConfigModeStatus',
28
+ 'FirmwareVersion',
29
+ 'GateSensitivityConfig',
30
+ 'LD2410',
31
+ 'ModuleRestartedError',
32
+ 'OutPinLevel',
33
+ 'ParametersConfig',
34
+ 'ParametersStatus',
35
+ 'ReportBasicStatus',
36
+ 'ReportEngineeringStatus',
37
+ 'ReportStatus',
38
+ 'TargetStatus',
39
+ 'version',
40
+ ]
@@ -0,0 +1,17 @@
1
+ class AioLd2410Error(Exception):
2
+ """Base error for exceptions generated by this library."""
3
+
4
+
5
+ class CommandError(AioLd2410Error):
6
+ """An error raised after a command was sent."""
7
+
8
+
9
+ class CommandStatusError(CommandError):
10
+ """An error raised after a failure status was received."""
11
+
12
+
13
+ class ModuleRestartedError(BaseException):
14
+ """Raised when the module is restarting.
15
+
16
+ Do not catch this exception, it is used by the configuration context.
17
+ """
@@ -0,0 +1,408 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import copy
5
+ import logging
6
+ import sys
7
+ from asyncio import StreamReader, StreamWriter
8
+ from contextlib import AsyncExitStack, asynccontextmanager, suppress
9
+ from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, cast
10
+
11
+ from construct import Container
12
+ from serial_asyncio_fast import open_serial_connection
13
+
14
+ from .exception import CommandError, CommandStatusError, ModuleRestartedError
15
+ from .models import (
16
+ AuxiliaryControlConfig,
17
+ AuxiliaryControlStatus,
18
+ ConfigModeStatus,
19
+ FirmwareVersion,
20
+ GateSensitivityConfig,
21
+ ParametersConfig,
22
+ ParametersStatus,
23
+ ReportStatus,
24
+ container_to_model,
25
+ )
26
+ from .protocol import (
27
+ BaudRateIndex,
28
+ Command,
29
+ CommandCode,
30
+ CommandFrame,
31
+ FrameType,
32
+ Reply,
33
+ ReplyStatus,
34
+ Report,
35
+ ResolutionIndex,
36
+ )
37
+ from .stream import FrameStream
38
+
39
+ if sys.version_info >= (3, 11): # pragma: no branch
40
+ from asyncio import timeout
41
+ else: # pragma: no cover
42
+ from async_timeout import timeout # type: ignore[import-not-found]
43
+
44
+ if TYPE_CHECKING:
45
+ from collections.abc import AsyncIterator, Awaitable, Mapping
46
+ from types import TracebackType
47
+
48
+ from typing_extensions import Concatenate, Never, ParamSpec, Self, TypeAlias, Unpack
49
+
50
+ _P = ParamSpec('_P')
51
+ _T = TypeVar('_T')
52
+
53
+ _ReplyType: TypeAlias = Container[Any]
54
+ logger = logging.getLogger(__package__)
55
+
56
+
57
+ def configuration(
58
+ func: Callable[Concatenate[LD2410, _P], Awaitable[_T]],
59
+ ) -> Callable[Concatenate[LD2410, _P], Awaitable[_T]]:
60
+ """Decorate an async method so we can check for the configuration context."""
61
+ if not asyncio.iscoroutinefunction(func):
62
+ raise RuntimeError('@configuration decorator is only suitable for async methods.')
63
+
64
+ async def _check_config_context(
65
+ self: LD2410,
66
+ *args: _P.args,
67
+ **kwargs: _P.kwargs,
68
+ ) -> _T:
69
+ if not self.configuring:
70
+ raise CommandError('This method requires a configuration context')
71
+ return await func(self, *args, **kwargs)
72
+
73
+ return _check_config_context
74
+
75
+
76
+ class LD2410:
77
+ """Client of the LD2410 sensor."""
78
+
79
+ DEFAULT_COMMAND_TIMEOUT = 2.0
80
+ DEFAULT_BAUDRATE = 256000
81
+
82
+ def __init__(
83
+ self,
84
+ device: str,
85
+ *,
86
+ baudrate: int = DEFAULT_BAUDRATE,
87
+ command_timeout: float | None = DEFAULT_COMMAND_TIMEOUT,
88
+ read_bufsize: int | None = None,
89
+ ) -> None:
90
+ """Create a new client the supplied device."""
91
+ self._baudrate = baudrate
92
+ self._command_timeout = command_timeout
93
+ self._device = device
94
+ self._config_lock = asyncio.Lock()
95
+ self._read_bufsize = read_bufsize
96
+ self._report = None # type: ReportStatus | None
97
+ self._report_condition = asyncio.Condition()
98
+ self._request_lock = asyncio.Lock()
99
+ self._connected = False
100
+ self._context = None # type: AsyncExitStack | None
101
+ self._replies = None # type: asyncio.Queue[_ReplyType | None] | None
102
+ self._restarted = False
103
+ self._rdtask = None # type: asyncio.Task[None] | None
104
+ self._writer = None # type: StreamWriter | None
105
+
106
+ @property
107
+ def configuring(self) -> bool:
108
+ """Tell whether configuration mode is currently entered."""
109
+ return bool(not self._restarted and self._config_lock.locked())
110
+
111
+ @property
112
+ def connected(self) -> bool:
113
+ """Tell whether we are still connected and listening to frames."""
114
+ return bool(self._connected and self._writer is not None and self._replies is not None)
115
+
116
+ @property
117
+ def entered(self) -> bool:
118
+ """Tell whether the context manager is already entered."""
119
+ return bool(self._context is not None)
120
+
121
+ async def __aenter__(self) -> Self:
122
+ """Enter the device's context, open the device."""
123
+ if self.entered:
124
+ raise RuntimeError("LD2410's instance is already entered!")
125
+
126
+ context = await AsyncExitStack().__aenter__()
127
+ try:
128
+ reader, writer = await self._open_serial_connection()
129
+ context.push_async_callback(writer.wait_closed)
130
+ context.callback(writer.close)
131
+
132
+ replies = asyncio.Queue() # type: asyncio.Queue[_ReplyType | None]
133
+ rdtask = asyncio.create_task(
134
+ self._reader_task(reader, replies),
135
+ name='aio_ld2410.ld2410.reader',
136
+ )
137
+
138
+ async def cancel_reader(task: asyncio.Task[None]) -> None:
139
+ task.cancel('Device is closing')
140
+ await asyncio.gather(task, return_exceptions=True)
141
+
142
+ context.push_async_callback(cancel_reader, rdtask)
143
+ except BaseException:
144
+ await context.aclose()
145
+ raise
146
+ else:
147
+ self._context = context
148
+ self._connected = True
149
+ self._replies = replies
150
+ self._rdtask = rdtask
151
+ self._writer = writer
152
+ return self
153
+
154
+ async def __aexit__(
155
+ self,
156
+ exc_type: type[BaseException] | None,
157
+ exc_val: BaseException | None,
158
+ exc_tb: TracebackType | None,
159
+ ) -> bool:
160
+ """Exit the device's context."""
161
+ context = self._context
162
+ try:
163
+ if context is not None:
164
+ await context.__aexit__(exc_type, exc_val, exc_tb)
165
+ finally:
166
+ self._connected = False
167
+ self._context = None
168
+ self._replies = None
169
+ self._rdtask = None
170
+ self._writer = None
171
+
172
+ # Do not prevent the original exception from going further.
173
+ return False
174
+
175
+ async def _open_serial_connection(self) -> tuple[StreamReader, StreamWriter]:
176
+ """Open a serial connection for this device."""
177
+ # This cannot be tested and is supersed during tests.
178
+ return await open_serial_connection(
179
+ baudrate=self._baudrate,
180
+ limit=self._read_bufsize,
181
+ url=self._device,
182
+ ) # pragma: no cover
183
+
184
+ async def _reader_task(
185
+ self,
186
+ reader: StreamReader,
187
+ replies: asyncio.Queue[_ReplyType | None],
188
+ ) -> None:
189
+ stream = FrameStream()
190
+ try:
191
+ while chunk := await reader.read(2048):
192
+ stream.append(chunk)
193
+ for frame in stream.read_frames():
194
+ try:
195
+ if frame.type == FrameType.COMMAND:
196
+ reply = Reply.parse(frame.data)
197
+ await replies.put(reply)
198
+ elif frame.type == FrameType.REPORT: # pragma: no branch
199
+ report = Report.parse(frame.data)
200
+ async with self._report_condition:
201
+ self._report = container_to_model(ReportStatus, report.data)
202
+ self._report_condition.notify_all()
203
+ except Exception:
204
+ # Happens when we received a frame with unknown content.
205
+ # For the user perpective this will most likely ends with a timeout.
206
+ logger.exception('Unable to handle frame: %s', frame.data.hex(' '))
207
+ finally:
208
+ self._connected = False
209
+ # This is needed here because we may be stuck waiting on a reply.
210
+ with suppress(asyncio.QueueFull):
211
+ replies.put_nowait(None)
212
+
213
+ async def _request(
214
+ self,
215
+ code: CommandCode,
216
+ args: Mapping[str, Any] | None = None,
217
+ ) -> _ReplyType:
218
+ """Send any kind of command to the device.
219
+
220
+ Wait and dequeue the corresponding reply.
221
+ """
222
+ command = Command.build({'code': code, 'data': args})
223
+ async with self._request_lock:
224
+ if not self.connected:
225
+ raise ConnectionError('We are not connected to the device anymore!')
226
+
227
+ async with timeout(self._command_timeout):
228
+ frame = CommandFrame.build({'data': command})
229
+ # Casts are valid here since we just checked `self.connected`.
230
+ replies = cast(asyncio.Queue[Optional[_ReplyType]], self._replies)
231
+ writer = cast(StreamWriter, self._writer)
232
+
233
+ writer.write(frame)
234
+ await writer.drain()
235
+
236
+ # Loop until we get our reply.
237
+ valid_reply = False
238
+ while not valid_reply:
239
+ reply = await replies.get()
240
+ replies.task_done()
241
+ if reply is None:
242
+ raise ConnectionError('Device has disconnected')
243
+
244
+ valid_reply = bool(code == int(reply.code))
245
+ if not valid_reply:
246
+ logger.warning('Got reply code %u (request was %u)', reply.code, code)
247
+
248
+ # MyPy does not see that reply cannot be None on here.
249
+ reply = cast(_ReplyType, reply)
250
+ if int(reply.status) != ReplyStatus.SUCCESS:
251
+ raise CommandStatusError(f'Command {code} received bad status: {reply.status}')
252
+
253
+ return reply
254
+
255
+ @asynccontextmanager
256
+ async def configure(self) -> AsyncIterator[ConfigModeStatus]:
257
+ """Enter configuration mode."""
258
+ async with self._config_lock:
259
+ resp = await self._request(CommandCode.CONFIG_ENABLE)
260
+ try:
261
+ yield container_to_model(ConfigModeStatus, resp.data)
262
+ except ModuleRestartedError:
263
+ logger.info('Configuration context has closed due to module restart.')
264
+ finally:
265
+ if not self._restarted and self.connected:
266
+ await self._request(CommandCode.CONFIG_DISABLE)
267
+ self._restarted = False
268
+
269
+ @configuration
270
+ async def get_auxiliary_controls(self) -> AuxiliaryControlStatus:
271
+ """Get the auxiliary controls (OUT pin)."""
272
+ resp = await self._request(CommandCode.AUXILIARY_CONTROL_GET)
273
+ return container_to_model(AuxiliaryControlStatus, resp.data)
274
+
275
+ @configuration
276
+ async def get_bluetooth_address(self) -> bytes:
277
+ """Get the module's bluetooth mac address."""
278
+ resp = await self._request(CommandCode.BLUETOOTH_MAC_GET)
279
+ return bytes(resp.data.address)
280
+
281
+ @configuration
282
+ async def get_distance_resolution(self) -> int:
283
+ """Get the gate distance resolution (in centimeter).
284
+
285
+ This command seems to be available for a few devices / firmwares.
286
+ """
287
+ resp = await self._request(CommandCode.DISTANCE_RESOLUTION_GET)
288
+ index = int(resp.data.resolution)
289
+ if index == ResolutionIndex.RESOLUTION_20CM:
290
+ return 20
291
+ if index == ResolutionIndex.RESOLUTION_75CM:
292
+ return 75
293
+ raise CommandError(f'Unhandled distance resolution index {index}')
294
+
295
+ @configuration
296
+ async def get_firmware_version(self) -> FirmwareVersion:
297
+ """Get the current firmware version."""
298
+ resp = await self._request(CommandCode.FIRMWARE_VERSION)
299
+ return container_to_model(FirmwareVersion, resp.data)
300
+
301
+ def get_last_report(self) -> ReportStatus | None:
302
+ """Get the latest report available, if any."""
303
+ return copy.deepcopy(self._report)
304
+
305
+ @configuration
306
+ async def get_parameters(self) -> ParametersStatus:
307
+ """Read general parameters."""
308
+ resp = await self._request(CommandCode.PARAMETERS_READ)
309
+ return container_to_model(ParametersStatus, resp.data)
310
+
311
+ async def get_next_report(self) -> ReportStatus:
312
+ """Wait and get the next available report."""
313
+ async with self._report_condition:
314
+ await self._report_condition.wait()
315
+ report = cast(ReportStatus, self._report)
316
+ return copy.deepcopy(report)
317
+
318
+ async def get_reports(self) -> AsyncIterator[ReportStatus]:
319
+ """Get reports as an asynchronous iterator."""
320
+ while True:
321
+ yield await self.get_next_report()
322
+
323
+ @configuration
324
+ async def reset_to_factory(self) -> None:
325
+ """Reset the module to its factory settings.
326
+
327
+ This command is effective after a module restart.
328
+ """
329
+ await self._request(CommandCode.FACTORY_RESET)
330
+
331
+ @configuration
332
+ async def restart_module(self) -> Never:
333
+ """Restart the module.
334
+
335
+ Please note that it can take at least 1100ms for it to be available again.
336
+ Raises a `ModuleRestartedError` intended to be caught by the configuration context.
337
+ """
338
+ await self._request(CommandCode.MODULE_RESTART)
339
+ self._restarted = True
340
+ raise ModuleRestartedError('Module is being restarted')
341
+
342
+ @configuration
343
+ async def set_auxiliary_controls(self, **kwargs: Unpack[AuxiliaryControlConfig]) -> None:
344
+ """Configure the auxiliary controls (OUT pin)."""
345
+ await self._request(
346
+ CommandCode.AUXILIARY_CONTROL_SET,
347
+ AuxiliaryControlConfig(**kwargs),
348
+ )
349
+
350
+ @configuration
351
+ async def set_baudrate(self, baudrate: int) -> None:
352
+ """Set the serial baud rate to operate.
353
+
354
+ Only baud rates from `BaudRateIndex` are valid, a KeyError is raised otherwise.
355
+ This command is effective after a module restart.
356
+ """
357
+ await self._request(
358
+ CommandCode.BAUD_RATE_SET,
359
+ {'index': int(BaudRateIndex.from_integer(baudrate))},
360
+ )
361
+
362
+ @configuration
363
+ async def set_bluetooth_mode(self, enabled: bool) -> None:
364
+ """Set device bluetooth mode."""
365
+ await self._request(CommandCode.BLUETOOTH_SET, {'enabled': enabled})
366
+
367
+ @configuration
368
+ async def set_bluetooth_password(self, password: str) -> None:
369
+ """Set device bluetooth password.
370
+
371
+ This command seems to be available for a few devices / firmwares.
372
+ The password must have no more than 6 ascii characters.
373
+ """
374
+ if len(password) > 6 or not password.isascii():
375
+ raise CommandError('Bluetooth password must have less than 7 ascii characters.')
376
+ await self._request(CommandCode.BLUETOOTH_PASSWORD_SET, {'password': password})
377
+
378
+ @configuration
379
+ async def set_distance_resolution(self, resolution: int) -> None:
380
+ """Set the gate distance resolution (in centimeter).
381
+
382
+ This command seems to be available for a few devices / firmwares.
383
+ This command requires a module restart to be effective.
384
+ `resolution` can only be 20 or 75 centimeters.
385
+ """
386
+ index = ResolutionIndex.RESOLUTION_75CM
387
+ if resolution == 20:
388
+ index = ResolutionIndex.RESOLUTION_20CM
389
+ elif resolution != 75:
390
+ raise CommandError(f'Unknown index for distance resolution {resolution}')
391
+ await self._request(CommandCode.DISTANCE_RESOLUTION_SET, {'resolution': index})
392
+
393
+ @configuration
394
+ async def set_engineering_mode(self, enabled: bool) -> None:
395
+ """Set device in engineering mode."""
396
+ code = CommandCode.ENGINEERING_ENABLE if enabled else CommandCode.ENGINEERING_DISABLE
397
+ await self._request(code)
398
+
399
+ @configuration
400
+ async def set_parameters(self, **kwargs: Unpack[ParametersConfig]) -> None:
401
+ """Set general parameters."""
402
+ # This step is needed to ensure argument correctness.
403
+ await self._request(CommandCode.PARAMETERS_WRITE, ParametersConfig(**kwargs))
404
+
405
+ @configuration
406
+ async def set_gate_sentivity(self, **kwargs: Unpack[GateSensitivityConfig]) -> None:
407
+ """Set the sensor sensitivity."""
408
+ await self._request(CommandCode.GATE_SENSITIVITY_SET, GateSensitivityConfig(**kwargs))