rf-protocols 2.0.0__tar.gz → 2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rf-protocols
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Library to decode and encode radio frequency signals.
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/home-assistant-libs/rf-protocols
@@ -11,6 +11,7 @@ Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  Provides-Extra: dev
13
13
  Requires-Dist: pytest; extra == "dev"
14
+ Requires-Dist: pytest-asyncio; extra == "dev"
14
15
  Requires-Dist: prek; extra == "dev"
15
16
  Dynamic: license-file
16
17
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rf-protocols"
7
- version = "2.0.0"
7
+ version = "2.1.0"
8
8
  license = "MIT"
9
9
  description = "Library to decode and encode radio frequency signals."
10
10
  readme = "README.md"
@@ -46,4 +46,7 @@ max-complexity = 25
46
46
  typeCheckingMode = "standard"
47
47
 
48
48
  [project.optional-dependencies]
49
- dev = ["pytest", "prek"]
49
+ dev = ["pytest", "pytest-asyncio", "prek"]
50
+
51
+ [tool.pytest.ini_options]
52
+ asyncio_mode = "auto"
@@ -2,6 +2,7 @@
2
2
 
3
3
  from .commands import ModulationType, OOKCommand, RadioFrequencyCommand
4
4
  from .loader import CodeCollection, get_codes
5
+ from .parser import parse_sub_content
5
6
 
6
7
  __all__ = [
7
8
  "CodeCollection",
@@ -9,4 +10,5 @@ __all__ = [
9
10
  "OOKCommand",
10
11
  "RadioFrequencyCommand",
11
12
  "get_codes",
13
+ "parse_sub_content",
12
14
  ]
@@ -2,10 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  from pathlib import Path
6
- from typing import Any
7
7
 
8
- from .commands import OOKCommand, RadioFrequencyCommand
8
+ from .commands import RadioFrequencyCommand
9
+ from .parser import parse_sub_content
9
10
 
10
11
  _DEFAULT_BASE_DIR = Path(__file__).parent / "codes"
11
12
 
@@ -34,10 +35,22 @@ class CodeCollection:
34
35
  sub_file = self._codes_dir / f"{name.lower()}.sub"
35
36
  if not sub_file.is_file():
36
37
  raise KeyError(f"No command {name!r} in {self._codes_dir}")
37
- cmd = _parse_sub_content(sub_file.read_text())
38
+ cmd = parse_sub_content(sub_file.read_text())
38
39
  self._cache[name] = cmd
39
40
  return cmd
40
41
 
42
+ async def async_load_command(self, name: str) -> RadioFrequencyCommand:
43
+ """Async variant of :meth:`load_command`.
44
+
45
+ Returns the cached command synchronously when available; otherwise
46
+ reads and parses the ``.sub`` file in the default executor.
47
+ """
48
+ if (cached := self._cache.get(name)) is not None:
49
+ return cached
50
+ return await asyncio.get_running_loop().run_in_executor(
51
+ None, self.load_command, name
52
+ )
53
+
41
54
  def __repr__(self) -> str:
42
55
  """Return a concise representation."""
43
56
  return f"CodeCollection({self._codes_dir})"
@@ -57,48 +70,3 @@ def get_codes(name: str, base_dir: Path | str | None = None) -> CodeCollection:
57
70
  if not codes_dir.is_dir():
58
71
  raise FileNotFoundError(f"No codes directory at {codes_dir}")
59
72
  return CodeCollection(codes_dir)
60
-
61
-
62
- def _parse_sub_content(content: str) -> RadioFrequencyCommand:
63
- """Parse a Flipper ``.sub`` file into a :class:`RadioFrequencyCommand`."""
64
- fields: dict[str, str] = {}
65
- raw_parts: list[str] = []
66
- for raw_line in content.splitlines():
67
- line = raw_line.strip()
68
- if not line:
69
- continue
70
- key, sep, value = line.partition(":")
71
- if not sep:
72
- continue
73
- key = key.strip()
74
- value = value.strip()
75
- if key == "RAW_Data":
76
- raw_parts.append(value)
77
- else:
78
- fields[key] = value
79
-
80
- protocol = fields.get("Protocol", "")
81
- if protocol != "RAW":
82
- raise ValueError(f"Unsupported Protocol {protocol!r}; only RAW is supported")
83
-
84
- preset = fields.get("Preset", "")
85
- if "Ook" not in preset:
86
- raise ValueError(
87
- f"Unsupported Preset {preset!r}; only OOK presets are supported"
88
- )
89
-
90
- if "Frequency" not in fields:
91
- raise ValueError("Missing Frequency field")
92
- if not raw_parts:
93
- raise ValueError("Missing RAW_Data field")
94
-
95
- timings = [int(v) for part in raw_parts for v in part.split()]
96
- if len(timings) % 2 != 0:
97
- raise ValueError("RAW_Data must contain an even number of values")
98
-
99
- kwargs: dict[str, Any] = {
100
- "frequency": int(fields["Frequency"]),
101
- "timings": timings,
102
- "repeat_count": int(fields.get("Repeat", "0")),
103
- }
104
- return OOKCommand(**kwargs)
@@ -0,0 +1,52 @@
1
+ """Parse Flipper ``.sub`` file contents into :class:`RadioFrequencyCommand`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .commands import OOKCommand, RadioFrequencyCommand
8
+
9
+
10
+ def parse_sub_content(content: str) -> RadioFrequencyCommand:
11
+ """Parse a Flipper ``.sub`` file into a :class:`RadioFrequencyCommand`."""
12
+ fields: dict[str, str] = {}
13
+ raw_parts: list[str] = []
14
+ for raw_line in content.splitlines():
15
+ line = raw_line.strip()
16
+ if not line:
17
+ continue
18
+ key, sep, value = line.partition(":")
19
+ if not sep:
20
+ continue
21
+ key = key.strip()
22
+ value = value.strip()
23
+ if key == "RAW_Data":
24
+ raw_parts.append(value)
25
+ else:
26
+ fields[key] = value
27
+
28
+ protocol = fields.get("Protocol", "")
29
+ if protocol != "RAW":
30
+ raise ValueError(f"Unsupported Protocol {protocol!r}; only RAW is supported")
31
+
32
+ preset = fields.get("Preset", "")
33
+ if "Ook" not in preset:
34
+ raise ValueError(
35
+ f"Unsupported Preset {preset!r}; only OOK presets are supported"
36
+ )
37
+
38
+ if "Frequency" not in fields:
39
+ raise ValueError("Missing Frequency field")
40
+ if not raw_parts:
41
+ raise ValueError("Missing RAW_Data field")
42
+
43
+ timings = [int(v) for part in raw_parts for v in part.split()]
44
+ if len(timings) % 2 != 0:
45
+ raise ValueError("RAW_Data must contain an even number of values")
46
+
47
+ kwargs: dict[str, Any] = {
48
+ "frequency": int(fields["Frequency"]),
49
+ "timings": timings,
50
+ "repeat_count": int(fields.get("Repeat", "0")),
51
+ }
52
+ return OOKCommand(**kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rf-protocols
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Library to decode and encode radio frequency signals.
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/home-assistant-libs/rf-protocols
@@ -11,6 +11,7 @@ Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  Provides-Extra: dev
13
13
  Requires-Dist: pytest; extra == "dev"
14
+ Requires-Dist: pytest-asyncio; extra == "dev"
14
15
  Requires-Dist: prek; extra == "dev"
15
16
  Dynamic: license-file
16
17
 
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  rf_protocols/__init__.py
5
5
  rf_protocols/commands.py
6
6
  rf_protocols/loader.py
7
+ rf_protocols/parser.py
7
8
  rf_protocols.egg-info/PKG-INFO
8
9
  rf_protocols.egg-info/SOURCES.txt
9
10
  rf_protocols.egg-info/dependency_links.txt
@@ -12,4 +13,5 @@ rf_protocols.egg-info/top_level.txt
12
13
  rf_protocols/codes/honeywell/string_lights/turn_off.sub
13
14
  rf_protocols/codes/honeywell/string_lights/turn_on.sub
14
15
  tests/test_commands.py
15
- tests/test_loader.py
16
+ tests/test_loader.py
17
+ tests/test_parser.py
@@ -1,4 +1,5 @@
1
1
 
2
2
  [dev]
3
3
  pytest
4
+ pytest-asyncio
4
5
  prek
@@ -0,0 +1,111 @@
1
+ """Tests for the .sub file loader."""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ import rf_protocols
9
+ from rf_protocols import CodeCollection, ModulationType, OOKCommand, get_codes
10
+
11
+ _BUNDLED_CODES_ROOT = Path(rf_protocols.__file__).parent / "codes"
12
+ _BUNDLED_SUB_FILES = sorted(_BUNDLED_CODES_ROOT.rglob("*.sub"))
13
+ _HONEYWELL_ROOT = _BUNDLED_CODES_ROOT / "honeywell"
14
+
15
+
16
+ def test_bundled_sub_files_exist() -> None:
17
+ """At least one bundled .sub file ships with the package."""
18
+ assert _BUNDLED_SUB_FILES, f"No .sub files found under {_BUNDLED_CODES_ROOT}"
19
+
20
+
21
+ @pytest.mark.parametrize(
22
+ "sub_file",
23
+ _BUNDLED_SUB_FILES,
24
+ ids=[str(p.relative_to(_BUNDLED_CODES_ROOT)) for p in _BUNDLED_SUB_FILES],
25
+ )
26
+ def test_bundled_sub_file_parses(sub_file: Path) -> None:
27
+ """Every bundled .sub file parses into a valid OOK command."""
28
+ device_dir = sub_file.parent.relative_to(_BUNDLED_CODES_ROOT)
29
+ codes = get_codes(str(device_dir))
30
+ cmd = codes.load_command(sub_file.stem.upper())
31
+ assert isinstance(cmd, OOKCommand)
32
+ assert cmd.frequency > 0
33
+ assert cmd.modulation == ModulationType.OOK
34
+ assert cmd.timings
35
+ assert len(cmd.timings) % 2 == 0
36
+
37
+
38
+ def test_get_codes_bundled_honeywell() -> None:
39
+ """Bundled Honeywell string lights codes can be discovered and loaded."""
40
+ codes = get_codes("honeywell/string_lights")
41
+ assert isinstance(codes, CodeCollection)
42
+ cmd = codes.load_command("TURN_ON")
43
+ assert isinstance(cmd, OOKCommand)
44
+ assert cmd.frequency == 433_920_000
45
+ assert cmd.repeat_count == 50
46
+ assert cmd.timings[:2] == [2000, -550]
47
+
48
+
49
+ def test_load_command_caches_results() -> None:
50
+ """Repeated load_command calls return the same instance."""
51
+ codes = get_codes("honeywell/string_lights")
52
+ assert codes.load_command("TURN_ON") is codes.load_command("TURN_ON")
53
+
54
+
55
+ def test_load_command_unknown_name() -> None:
56
+ """Requesting an unknown command name raises KeyError."""
57
+ codes = get_codes("honeywell/string_lights")
58
+ with pytest.raises(KeyError, match="NOPE"):
59
+ codes.load_command("NOPE")
60
+
61
+
62
+ def test_load_command_base_dir_override() -> None:
63
+ """Passing base_dir loads codes from an alternate location."""
64
+ codes = get_codes("string_lights", base_dir=_HONEYWELL_ROOT)
65
+ cmd = codes.load_command("TURN_ON")
66
+ assert isinstance(cmd, OOKCommand)
67
+ assert cmd.frequency == 433_920_000
68
+ assert cmd.repeat_count == 50
69
+
70
+
71
+ def test_load_command_base_dir_accepts_str() -> None:
72
+ """base_dir accepts a str as well as a Path."""
73
+ codes = get_codes("string_lights", base_dir=str(_HONEYWELL_ROOT))
74
+ assert isinstance(codes.load_command("TURN_ON"), OOKCommand)
75
+
76
+
77
+ def test_get_codes_missing_directory(tmp_path: Path) -> None:
78
+ """get_codes raises FileNotFoundError when the directory is missing."""
79
+ with pytest.raises(FileNotFoundError):
80
+ get_codes("nope", base_dir=tmp_path)
81
+
82
+
83
+ def test_get_codes_rejects_path_escape() -> None:
84
+ """Paths that resolve outside of base_dir are rejected."""
85
+ with pytest.raises(ValueError, match="outside"):
86
+ get_codes("../honeywell", base_dir=_HONEYWELL_ROOT / "string_lights")
87
+
88
+
89
+ async def test_async_load_command_loads_from_disk() -> None:
90
+ """async_load_command parses the command from disk."""
91
+ codes = get_codes("honeywell/string_lights")
92
+ cmd = await codes.async_load_command("TURN_ON")
93
+ assert isinstance(cmd, OOKCommand)
94
+ assert cmd.repeat_count == 50
95
+
96
+
97
+ async def test_async_load_command_returns_cached_without_executor(
98
+ monkeypatch: pytest.MonkeyPatch,
99
+ ) -> None:
100
+ """Cached commands bypass the executor entirely."""
101
+ codes = get_codes("honeywell/string_lights")
102
+ first = codes.load_command("TURN_ON")
103
+
104
+ def _boom(*_args: object, **_kwargs: object) -> None:
105
+ raise AssertionError("executor must not be used for cached commands")
106
+
107
+ monkeypatch.setattr(
108
+ asyncio.AbstractEventLoop, "run_in_executor", _boom, raising=True
109
+ )
110
+ second = await codes.async_load_command("TURN_ON")
111
+ assert first is second
@@ -0,0 +1,63 @@
1
+ """Tests for :func:`parse_sub_content`."""
2
+
3
+ import pytest
4
+
5
+ from rf_protocols import OOKCommand, parse_sub_content
6
+
7
+ _SAMPLE_SUB = """\
8
+ Filetype: Flipper SubGhz RAW File
9
+ Version: 1
10
+ Frequency: 433920000
11
+ Preset: FuriHalSubGhzPresetOok650Async
12
+ Protocol: RAW
13
+ Repeat: 7
14
+ RAW_Data: 2000 -550 450 -1000
15
+ """
16
+
17
+
18
+ def test_parse_sub_content_parses_sample() -> None:
19
+ """A well-formed .sub payload parses into an OOKCommand."""
20
+ cmd = parse_sub_content(_SAMPLE_SUB)
21
+ assert isinstance(cmd, OOKCommand)
22
+ assert cmd.frequency == 433_920_000
23
+ assert cmd.repeat_count == 7
24
+ assert cmd.timings == [2000, -550, 450, -1000]
25
+
26
+
27
+ def test_parse_sub_content_rejects_non_raw_protocol() -> None:
28
+ """Non-RAW protocols are rejected."""
29
+ with pytest.raises(ValueError, match="Protocol"):
30
+ parse_sub_content(_SAMPLE_SUB.replace("Protocol: RAW", "Protocol: Princeton"))
31
+
32
+
33
+ def test_parse_sub_content_rejects_non_ook_preset() -> None:
34
+ """Non-OOK presets are rejected."""
35
+ with pytest.raises(ValueError, match="Preset"):
36
+ parse_sub_content(
37
+ _SAMPLE_SUB.replace(
38
+ "FuriHalSubGhzPresetOok650Async", "FuriHalSubGhzPreset2FSKDev238Async"
39
+ )
40
+ )
41
+
42
+
43
+ def test_parse_sub_content_multiline_raw_data() -> None:
44
+ """Multiple RAW_Data lines are concatenated."""
45
+ content = (
46
+ "Filetype: Flipper SubGhz RAW File\n"
47
+ "Version: 1\n"
48
+ "Frequency: 433920000\n"
49
+ "Preset: FuriHalSubGhzPresetOok650Async\n"
50
+ "Protocol: RAW\n"
51
+ "RAW_Data: 2000 -550\n"
52
+ "RAW_Data: 450 -1000\n"
53
+ )
54
+ cmd = parse_sub_content(content)
55
+ assert isinstance(cmd, OOKCommand)
56
+ assert cmd.timings == [2000, -550, 450, -1000]
57
+ assert cmd.repeat_count == 0
58
+
59
+
60
+ def test_parse_sub_content_rejects_odd_raw_data() -> None:
61
+ """Odd-length RAW_Data is rejected."""
62
+ with pytest.raises(ValueError, match="even number"):
63
+ parse_sub_content(_SAMPLE_SUB.replace("2000 -550 450 -1000", "2000 -550 450"))
@@ -1,166 +0,0 @@
1
- """Tests for the .sub file loader."""
2
-
3
- from pathlib import Path
4
-
5
- import pytest
6
-
7
- import rf_protocols
8
- from rf_protocols import CodeCollection, ModulationType, OOKCommand, get_codes
9
-
10
- _BUNDLED_CODES_ROOT = Path(rf_protocols.__file__).parent / "codes"
11
- _BUNDLED_SUB_FILES = sorted(_BUNDLED_CODES_ROOT.rglob("*.sub"))
12
-
13
- _SAMPLE_SUB = """\
14
- Filetype: Flipper SubGhz RAW File
15
- Version: 1
16
- Frequency: 433920000
17
- Preset: FuriHalSubGhzPresetOok650Async
18
- Protocol: RAW
19
- Repeat: 7
20
- RAW_Data: 2000 -550 450 -1000
21
- """
22
-
23
-
24
- def _write_sub(path: Path, content: str = _SAMPLE_SUB) -> None:
25
- path.parent.mkdir(parents=True, exist_ok=True)
26
- path.write_text(content)
27
-
28
-
29
- def test_bundled_sub_files_exist() -> None:
30
- """At least one bundled .sub file ships with the package."""
31
- assert _BUNDLED_SUB_FILES, f"No .sub files found under {_BUNDLED_CODES_ROOT}"
32
-
33
-
34
- @pytest.mark.parametrize(
35
- "sub_file",
36
- _BUNDLED_SUB_FILES,
37
- ids=[str(p.relative_to(_BUNDLED_CODES_ROOT)) for p in _BUNDLED_SUB_FILES],
38
- )
39
- def test_bundled_sub_file_parses(sub_file: Path) -> None:
40
- """Every bundled .sub file parses into a valid OOK command."""
41
- device_dir = sub_file.parent.relative_to(_BUNDLED_CODES_ROOT)
42
- codes = get_codes(str(device_dir))
43
- cmd = codes.load_command(sub_file.stem.upper())
44
- assert isinstance(cmd, OOKCommand)
45
- assert cmd.frequency > 0
46
- assert cmd.modulation == ModulationType.OOK
47
- assert cmd.timings
48
- assert len(cmd.timings) % 2 == 0
49
-
50
-
51
- def test_get_codes_bundled_honeywell() -> None:
52
- """Bundled Honeywell string lights codes can be discovered and loaded."""
53
- codes = get_codes("honeywell/string_lights")
54
- assert isinstance(codes, CodeCollection)
55
- cmd = codes.load_command("TURN_ON")
56
- assert isinstance(cmd, OOKCommand)
57
- assert cmd.frequency == 433_920_000
58
- assert cmd.repeat_count == 50
59
- assert cmd.timings[:2] == [2000, -550]
60
-
61
-
62
- def test_load_command_caches_results(tmp_path: Path) -> None:
63
- """Repeated load_command calls return the same instance without re-reading."""
64
- sub_path = tmp_path / "vendor" / "device" / "power.sub"
65
- _write_sub(sub_path)
66
- codes = get_codes("vendor/device", base_dir=tmp_path)
67
- first = codes.load_command("POWER")
68
- sub_path.unlink()
69
- second = codes.load_command("POWER")
70
- assert first is second
71
-
72
-
73
- def test_load_command_unknown_name(tmp_path: Path) -> None:
74
- """Requesting an unknown command name raises KeyError."""
75
- _write_sub(tmp_path / "vendor" / "device" / "power.sub")
76
- codes = get_codes("vendor/device", base_dir=tmp_path)
77
- with pytest.raises(KeyError, match="NOPE"):
78
- codes.load_command("NOPE")
79
-
80
-
81
- def test_load_command_base_dir_override(tmp_path: Path) -> None:
82
- """Passing base_dir loads codes from an alternate location."""
83
- _write_sub(tmp_path / "vendor" / "device" / "power.sub")
84
- codes = get_codes("vendor/device", base_dir=tmp_path)
85
- cmd = codes.load_command("POWER")
86
- assert isinstance(cmd, OOKCommand)
87
- assert cmd.frequency == 433_920_000
88
- assert cmd.repeat_count == 7
89
- assert cmd.timings == [2000, -550, 450, -1000]
90
-
91
-
92
- def test_load_command_base_dir_accepts_str(tmp_path: Path) -> None:
93
- """base_dir accepts a str as well as a Path."""
94
- _write_sub(tmp_path / "vendor" / "device" / "power.sub")
95
- codes = get_codes("vendor/device", base_dir=str(tmp_path))
96
- assert isinstance(codes.load_command("POWER"), OOKCommand)
97
-
98
-
99
- def test_get_codes_missing_directory(tmp_path: Path) -> None:
100
- """get_codes raises FileNotFoundError when the directory is missing."""
101
- with pytest.raises(FileNotFoundError):
102
- get_codes("nope", base_dir=tmp_path)
103
-
104
-
105
- def test_load_command_rejects_non_raw_protocol(tmp_path: Path) -> None:
106
- """Non-RAW protocols are rejected on load_command."""
107
- _write_sub(
108
- tmp_path / "vendor" / "device" / "power.sub",
109
- _SAMPLE_SUB.replace("Protocol: RAW", "Protocol: Princeton"),
110
- )
111
- codes = get_codes("vendor/device", base_dir=tmp_path)
112
- with pytest.raises(ValueError, match="Protocol"):
113
- codes.load_command("POWER")
114
-
115
-
116
- def test_load_command_rejects_non_ook_preset(tmp_path: Path) -> None:
117
- """Non-OOK presets are rejected on load_command."""
118
- _write_sub(
119
- tmp_path / "vendor" / "device" / "power.sub",
120
- _SAMPLE_SUB.replace(
121
- "FuriHalSubGhzPresetOok650Async", "FuriHalSubGhzPreset2FSKDev238Async"
122
- ),
123
- )
124
- codes = get_codes("vendor/device", base_dir=tmp_path)
125
- with pytest.raises(ValueError, match="Preset"):
126
- codes.load_command("POWER")
127
-
128
-
129
- def test_get_codes_rejects_path_escape(tmp_path: Path) -> None:
130
- """Paths that resolve outside of base_dir are rejected."""
131
- (tmp_path / "inside").mkdir()
132
- outside = tmp_path.parent / "outside_codes"
133
- outside.mkdir(exist_ok=True)
134
- _write_sub(outside / "power.sub")
135
- with pytest.raises(ValueError, match="outside"):
136
- get_codes("../outside_codes", base_dir=tmp_path / "inside")
137
-
138
-
139
- def test_load_command_multiline_raw_data(tmp_path: Path) -> None:
140
- """Multiple RAW_Data lines are concatenated."""
141
- content = (
142
- "Filetype: Flipper SubGhz RAW File\n"
143
- "Version: 1\n"
144
- "Frequency: 433920000\n"
145
- "Preset: FuriHalSubGhzPresetOok650Async\n"
146
- "Protocol: RAW\n"
147
- "RAW_Data: 2000 -550\n"
148
- "RAW_Data: 450 -1000\n"
149
- )
150
- _write_sub(tmp_path / "vendor" / "device" / "power.sub", content)
151
- codes = get_codes("vendor/device", base_dir=tmp_path)
152
- cmd = codes.load_command("POWER")
153
- assert isinstance(cmd, OOKCommand)
154
- assert cmd.timings == [2000, -550, 450, -1000]
155
- assert cmd.repeat_count == 0
156
-
157
-
158
- def test_load_command_rejects_odd_raw_data(tmp_path: Path) -> None:
159
- """Odd-length RAW_Data is rejected on load_command."""
160
- _write_sub(
161
- tmp_path / "vendor" / "device" / "power.sub",
162
- _SAMPLE_SUB.replace("2000 -550 450 -1000", "2000 -550 450"),
163
- )
164
- codes = get_codes("vendor/device", base_dir=tmp_path)
165
- with pytest.raises(ValueError, match="even number"):
166
- codes.load_command("POWER")
File without changes
File without changes
File without changes