rf-protocols 2.2.0__tar.gz → 3.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.
Files changed (77) hide show
  1. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/PKG-INFO +1 -1
  2. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/pyproject.toml +1 -1
  3. rf_protocols-3.1.0/rf_protocols/__init__.py +5 -0
  4. rf_protocols-3.1.0/rf_protocols/codes/honeywell/string_lights/__init__.py +5 -0
  5. rf_protocols-3.1.0/rf_protocols/codes/novy/cooker_hood/__init__.py +8 -0
  6. rf_protocols-3.1.0/rf_protocols/codes/somfy/rts/__init__.py +32 -0
  7. rf_protocols-2.2.0/rf_protocols/commands.py → rf_protocols-3.1.0/rf_protocols/commands/__init__.py +1 -32
  8. rf_protocols-3.1.0/rf_protocols/commands/kaku.py +153 -0
  9. rf_protocols-3.1.0/rf_protocols/commands/ook.py +37 -0
  10. rf_protocols-3.1.0/rf_protocols/commands/pt2262.py +102 -0
  11. rf_protocols-3.1.0/rf_protocols/commands/somfy_rts.py +116 -0
  12. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/parser.py +2 -1
  13. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols.egg-info/PKG-INFO +1 -1
  14. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols.egg-info/SOURCES.txt +8 -2
  15. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/tests/test_loader.py +3 -1
  16. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/tests/test_parser.py +2 -1
  17. rf_protocols-2.2.0/rf_protocols/__init__.py +0 -14
  18. rf_protocols-2.2.0/tests/test_commands.py +0 -103
  19. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/LICENSE +0 -0
  20. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/README.md +0 -0
  21. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/honeywell/string_lights/turn_off.sub +0 -0
  22. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/honeywell/string_lights/turn_on.sub +0 -0
  23. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_1/ambient_light.sub +0 -0
  24. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_1/light.sub +0 -0
  25. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_1/minus.sub +0 -0
  26. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_1/plus.sub +0 -0
  27. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_1/power.sub +0 -0
  28. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_10/ambient_light.sub +0 -0
  29. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_10/light.sub +0 -0
  30. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_10/minus.sub +0 -0
  31. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_10/plus.sub +0 -0
  32. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_10/power.sub +0 -0
  33. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_2/ambient_light.sub +0 -0
  34. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_2/light.sub +0 -0
  35. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_2/minus.sub +0 -0
  36. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_2/plus.sub +0 -0
  37. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_2/power.sub +0 -0
  38. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_3/ambient_light.sub +0 -0
  39. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_3/light.sub +0 -0
  40. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_3/minus.sub +0 -0
  41. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_3/plus.sub +0 -0
  42. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_3/power.sub +0 -0
  43. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_4/ambient_light.sub +0 -0
  44. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_4/light.sub +0 -0
  45. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_4/minus.sub +0 -0
  46. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_4/plus.sub +0 -0
  47. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_4/power.sub +0 -0
  48. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_5/ambient_light.sub +0 -0
  49. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_5/light.sub +0 -0
  50. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_5/minus.sub +0 -0
  51. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_5/plus.sub +0 -0
  52. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_5/power.sub +0 -0
  53. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_6/ambient_light.sub +0 -0
  54. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_6/light.sub +0 -0
  55. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_6/minus.sub +0 -0
  56. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_6/plus.sub +0 -0
  57. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_6/power.sub +0 -0
  58. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_7/ambient_light.sub +0 -0
  59. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_7/light.sub +0 -0
  60. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_7/minus.sub +0 -0
  61. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_7/plus.sub +0 -0
  62. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_7/power.sub +0 -0
  63. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_8/ambient_light.sub +0 -0
  64. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_8/light.sub +0 -0
  65. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_8/minus.sub +0 -0
  66. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_8/plus.sub +0 -0
  67. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_8/power.sub +0 -0
  68. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_9/ambient_light.sub +0 -0
  69. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_9/light.sub +0 -0
  70. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_9/minus.sub +0 -0
  71. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_9/plus.sub +0 -0
  72. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/codes/novy/cooker_hood/code_9/power.sub +0 -0
  73. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols/loader.py +0 -0
  74. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols.egg-info/dependency_links.txt +0 -0
  75. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols.egg-info/requires.txt +0 -0
  76. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/rf_protocols.egg-info/top_level.txt +0 -0
  77. {rf_protocols-2.2.0 → rf_protocols-3.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rf-protocols
3
- Version: 2.2.0
3
+ Version: 3.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rf-protocols"
7
- version = "2.2.0"
7
+ version = "3.1.0"
8
8
  license = "MIT"
9
9
  description = "Library to decode and encode radio frequency signals."
10
10
  readme = "README.md"
@@ -0,0 +1,5 @@
1
+ """Library to decode and encode radio frequency signals."""
2
+
3
+ from .commands import ModulationType, RadioFrequencyCommand
4
+
5
+ __all__ = ["ModulationType", "RadioFrequencyCommand"]
@@ -0,0 +1,5 @@
1
+ """Honeywell String Lights RF codes."""
2
+
3
+ from rf_protocols.loader import get_codes
4
+
5
+ CODES = get_codes("honeywell/string_lights")
@@ -0,0 +1,8 @@
1
+ """Novy cooker-hood RF codes."""
2
+
3
+ from rf_protocols.loader import CodeCollection, get_codes
4
+
5
+
6
+ def get_codes_for_code(code: int) -> CodeCollection:
7
+ """Return the bundled `rf-protocols` collection for a Novy cooker-hood code."""
8
+ return get_codes(f"novy/cooker_hood/code_{code}")
@@ -0,0 +1,32 @@
1
+ """Somfy RTS button codes."""
2
+
3
+ from enum import IntEnum
4
+
5
+ from ....commands.somfy_rts import SomfyRTSCommand
6
+
7
+
8
+ class SomfyRTSButton(IntEnum):
9
+ """Somfy RTS button identifiers.
10
+
11
+ Values are the protocol command nibbles transmitted in the frame.
12
+ """
13
+
14
+ MY = 0x1
15
+ UP = 0x2
16
+ DOWN = 0x4
17
+ PROG = 0x8
18
+
19
+ def to_command(
20
+ self,
21
+ *,
22
+ address: int,
23
+ rolling_code: int,
24
+ frame_repeats: int = 3,
25
+ ) -> SomfyRTSCommand:
26
+ """Build a SomfyRTSCommand for this button."""
27
+ return SomfyRTSCommand(
28
+ address=address,
29
+ rolling_code=rolling_code,
30
+ button=self.value,
31
+ frame_repeats=frame_repeats,
32
+ )
@@ -1,10 +1,9 @@
1
- """Common RF command definitions."""
1
+ """RF command encoders. Import directly from each protocol submodule."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import abc
6
6
  from enum import StrEnum
7
- from typing import override
8
7
 
9
8
 
10
9
  class ModulationType(StrEnum):
@@ -55,33 +54,3 @@ class RadioFrequencyCommand(abc.ABC):
55
54
  f"{self.frequency / 1_000_000:g} MHz, "
56
55
  f"repeat={self.repeat_count})"
57
56
  )
58
-
59
-
60
- class OOKCommand(RadioFrequencyCommand):
61
- """OOK command with raw timings."""
62
-
63
- timings: list[int]
64
-
65
- def __init__(
66
- self,
67
- *,
68
- frequency: int,
69
- timings: list[int],
70
- repeat_count: int = 0,
71
- symbol_rate: int | None = None,
72
- output_power: float | None = None,
73
- ) -> None:
74
- """Initialize the OOK command."""
75
- super().__init__(
76
- frequency=frequency,
77
- modulation=ModulationType.OOK,
78
- repeat_count=repeat_count,
79
- symbol_rate=symbol_rate,
80
- output_power=output_power,
81
- )
82
- self.timings = timings
83
-
84
- @override
85
- def get_raw_timings(self) -> list[int]:
86
- """Get raw timings."""
87
- return self.timings
@@ -0,0 +1,153 @@
1
+ """Protocol for KAKU compatible devices.
2
+
3
+ Pulse Position Modulation with 32 or 36 symbols, used by self-learning
4
+ Intertechno-type sockets and compatible devices (for example some Hama models).
5
+
6
+ Symbols use a time base ``T``:
7
+
8
+ ____ ____
9
+ Bit 0 : _| |____| |____________________
10
+ |<1T>|<1T>|<1T>|<--------5T-------->|
11
+
12
+ ____ ____
13
+ Bit 1 : _| |____________________| |____
14
+ |<1T>|<--------5T-------->|<1T>|<1T>|
15
+
16
+ ____ ____
17
+ Bit X : _| |____| |____
18
+ |<1T>|<1T>|<1T>|<1T>|
19
+
20
+ ____
21
+ Sync : _| |_________ _ _ _ ________
22
+ |<1T>|<----------11T--------->|
23
+
24
+ ____
25
+ Pause : _| |___________ _ _ _ ____________
26
+ |<1T>|<-----------39T-------------->|
27
+
28
+ Telegram layout:
29
+ - Sync
30
+ - 32 or 36 symbols
31
+ - Pause
32
+
33
+ Each transmission is typically repeated four times.
34
+
35
+ Common bit assignment for KAKU-style devices:
36
+ - 26 bits system code
37
+ - 1 bit group (0=single, 1=group)
38
+ - 1 bit on/off
39
+ - 4 bits channel
40
+
41
+ Dimming commands:
42
+ - 26 bits system code
43
+ - 1 bit group (0=single, 1=group)
44
+ - 1 bit X
45
+ - 4 bits channel
46
+ - 4 bits dim level (0-15)
47
+
48
+ Manufacturer-specific notes from the reference:
49
+ - Intertechno (self-learning): step duration around 275 us
50
+ - Hama 00121938: similar format, step duration around 250 us, inverted channel bits
51
+
52
+ Reference chapter:
53
+ https://www.seegel-systeme.de/2015/09/05/funksteckdosen-mit-dem-raspberry-pi-steuern/
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ from typing import override
59
+
60
+ from . import ModulationType, RadioFrequencyCommand
61
+
62
+ _DEFAULT_FREQUENCY = 433_920_000
63
+ _DEFAULT_REPEATS = 4
64
+ _DEFAULT_TIMEBASE_US = 275
65
+
66
+
67
+ class KakuCommand(RadioFrequencyCommand):
68
+ """Encode a KAKU-compatible PPM frame.
69
+
70
+ Data layout is either:
71
+ - 32 symbols: 26-bit id, group bit, on/off bit, 4-bit channel
72
+ - 36 symbols: 26-bit id, group bit, X symbol, 4-bit channel, 4-bit dim level
73
+ """
74
+
75
+ id: int
76
+ group: bool
77
+ channel: int
78
+ on: bool | None
79
+ dimlevel: int | None
80
+ timebase_us: int
81
+
82
+ def __init__(
83
+ self,
84
+ *,
85
+ id: int,
86
+ group: bool,
87
+ channel: int | None = None,
88
+ on: bool | None = None,
89
+ dimlevel: int | None = None,
90
+ frame_repeats: int = _DEFAULT_REPEATS,
91
+ frequency: int = _DEFAULT_FREQUENCY,
92
+ timebase_us: int = _DEFAULT_TIMEBASE_US,
93
+ ) -> None:
94
+ """Initialize the KAKU command."""
95
+
96
+ if id < 0 or id >= (1 << 26):
97
+ raise ValueError("id must be in range 0..67108863 (26-bit)")
98
+ if channel is None:
99
+ if not group:
100
+ raise ValueError("channel is required when group is False")
101
+ channel = 1
102
+ if channel < 1 or channel > 16:
103
+ raise ValueError("channel must be in range 1..16")
104
+ if (on is None) == (dimlevel is None):
105
+ raise ValueError("provide exactly one of 'on' or 'dimlevel'")
106
+ if dimlevel is not None and (dimlevel < 0 or dimlevel > 100):
107
+ raise ValueError("dimlevel must be in range 0..100")
108
+
109
+ super().__init__(
110
+ frequency=frequency,
111
+ modulation=ModulationType.OOK,
112
+ repeat_count=frame_repeats,
113
+ )
114
+ self.id = id
115
+ self.group = group
116
+ self.channel = channel
117
+ self.on = on
118
+ self.dimlevel = dimlevel
119
+ self.timebase_us = timebase_us
120
+
121
+ @override
122
+ def get_raw_timings(self) -> list[int]:
123
+ """Compute KAKU PPM frame timings."""
124
+ _symbols = {
125
+ "0": [self.timebase_us, -self.timebase_us, self.timebase_us, -5 * self.timebase_us],
126
+ "1": [self.timebase_us, -5 * self.timebase_us, self.timebase_us, -self.timebase_us],
127
+ "X": [self.timebase_us, -self.timebase_us, self.timebase_us, -self.timebase_us],
128
+ "sync": [self.timebase_us, -11 * self.timebase_us],
129
+ "pause": [self.timebase_us, -39 * self.timebase_us],
130
+ }
131
+
132
+ # add ID and group bit
133
+ symstr: list[str] = [*format(self.id, "026b"), "1" if self.group else "0"]
134
+
135
+ if self.dimlevel is None:
136
+ # add on/off bit
137
+ symstr.append("1" if self.on else "0")
138
+ else:
139
+ # add X symbol
140
+ symstr.append("X")
141
+
142
+ symstr.extend(format(self.channel - 1, "04b"))
143
+ if self.dimlevel is not None:
144
+ dim_steps = round(self.dimlevel * 15 / 100)
145
+ symstr.extend(format(dim_steps, "04b"))
146
+
147
+ timings: list[int] = []
148
+ timings.extend(_symbols["sync"])
149
+ for s in symstr:
150
+ timings.extend(_symbols[s])
151
+ timings.extend(_symbols["pause"])
152
+
153
+ return timings
@@ -0,0 +1,37 @@
1
+ """OOK (On-Off Keying) RF command with raw timings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import override
6
+
7
+ from . import ModulationType, RadioFrequencyCommand
8
+
9
+
10
+ class OOKCommand(RadioFrequencyCommand):
11
+ """OOK command with raw timings."""
12
+
13
+ timings: list[int]
14
+
15
+ def __init__(
16
+ self,
17
+ *,
18
+ frequency: int,
19
+ timings: list[int],
20
+ repeat_count: int = 0,
21
+ symbol_rate: int | None = None,
22
+ output_power: float | None = None,
23
+ ) -> None:
24
+ """Initialize the OOK command."""
25
+ super().__init__(
26
+ frequency=frequency,
27
+ modulation=ModulationType.OOK,
28
+ repeat_count=repeat_count,
29
+ symbol_rate=symbol_rate,
30
+ output_power=output_power,
31
+ )
32
+ self.timings = timings
33
+
34
+ @override
35
+ def get_raw_timings(self) -> list[int]:
36
+ """Get raw timings."""
37
+ return self.timings
@@ -0,0 +1,102 @@
1
+ """Encoder for PT2262 / HX2262 and compatible tristate RF chips.
2
+
3
+ Chip datasheet: https://cdn-shop.adafruit.com/datasheets/PT2262.pdf
4
+
5
+ Devices which use PT2262 or compatible chips include:
6
+
7
+ - BAT RC 3500-A
8
+ - Brennenstuhl RCS 1000 N Comfort
9
+ - Emil Lux GmbH RCS-14G
10
+ - ELRO AB440D, AB440IS, AB440L, AB440S, AB440WD
11
+ - Intertechno CMR-1000, CMR-1224, CMR-300, CMR-500, ...
12
+ - Intertechno IT-1500, IT-1500R, IT-2000, IT-2000R, ...
13
+ - me FLS 100
14
+ - mumbi FS300
15
+ - REV Ritter 8342C
16
+ - Vivanco FSS 31000W
17
+ - RSL366
18
+ - Goobay 94503
19
+
20
+ A frame consists of 12 symbols (0/1/F) followed by a sync symbol. The chip has
21
+ 12 inputs A0..A11 which can be tied to VCC, GND, or left floating. GND sends
22
+ symbol 0, VCC sends symbol 1, and floating sends symbol F. The symbols are
23
+ encoded against a time base ``a`` determined by the crystal connected to the
24
+ chip::
25
+
26
+ ____ ____
27
+ Bit 0 : _| |____________| |____________
28
+ |<4a>|<---12a---->|<4a>|<---12a---->|
29
+
30
+ ____________ ____________
31
+ Bit 1 : _| |____| |____
32
+ |<---12a---->|<4a>|<---12a---->|<4a>|
33
+
34
+ ____ ____________
35
+ Bit F : _| |____________| |____
36
+ |<4a>|<---12a---->|<---12a---->|<4a>|
37
+
38
+ ____
39
+ Sync : _| |___________ _ _ _ ____________
40
+ |<4a>|<-----------124a------------->|
41
+
42
+ References:
43
+
44
+ - https://www.seegel-systeme.de/2017/03/28/uebersicht-funksteckdosen/
45
+ - https://www.seegel-systeme.de/2015/09/05/funksteckdosen-mit-dem-raspberry-pi-steuern/
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ from typing import override
51
+
52
+ from . import ModulationType, RadioFrequencyCommand
53
+
54
+ _VALID_SYMBOLS = frozenset("01F")
55
+
56
+
57
+ class PT2262Command(RadioFrequencyCommand):
58
+ """OOK command for PT2262 / HX2262 and compatible tristate chips."""
59
+
60
+ data: str
61
+ timebase_us: int
62
+
63
+ def __init__(
64
+ self,
65
+ *,
66
+ data: str,
67
+ timebase_us: int = 350,
68
+ frequency: int = 433_920_000,
69
+ repeat_count: int = 5,
70
+ ) -> None:
71
+ """Initialize the PT2262 command from a 12-symbol tristate string."""
72
+ normalized_data = data.upper()
73
+ if len(normalized_data) != 12:
74
+ raise ValueError("data must be exactly 12 characters long")
75
+ if not set(normalized_data).issubset(_VALID_SYMBOLS):
76
+ raise ValueError("data must contain only symbols '0', '1', and 'F'")
77
+
78
+ super().__init__(
79
+ frequency=frequency,
80
+ modulation=ModulationType.OOK,
81
+ repeat_count=repeat_count,
82
+ )
83
+ self.data = normalized_data
84
+ self.timebase_us = timebase_us
85
+
86
+ @override
87
+ def get_raw_timings(self) -> list[int]:
88
+ """Compute the PT2262 frame timings followed by the sync symbol."""
89
+ short_us = 4 * self.timebase_us
90
+ long_us = 12 * self.timebase_us
91
+ sync_low_us = 124 * self.timebase_us
92
+ symbols = {
93
+ "0": [short_us, -long_us, short_us, -long_us],
94
+ "1": [long_us, -short_us, long_us, -short_us],
95
+ "F": [short_us, -long_us, long_us, -short_us],
96
+ }
97
+
98
+ timings: list[int] = []
99
+ for symbol in self.data:
100
+ timings.extend(symbols[symbol])
101
+ timings.extend([short_us, -sync_low_us])
102
+ return timings
@@ -0,0 +1,116 @@
1
+ """Somfy RTS RF command definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import override
6
+
7
+ from . import ModulationType, RadioFrequencyCommand
8
+
9
+ # Somfy RTS operates at 433.42 MHz with OOK modulation.
10
+ _SOMFY_RTS_FREQUENCY = 433_420_000
11
+ _SOMFY_RTS_REPEAT_COUNT = 2
12
+
13
+
14
+ class SomfyRTSCommand(RadioFrequencyCommand):
15
+ """Somfy RTS rolling code command.
16
+
17
+ Encodes a complete Somfy RTS transmission, including wake-up pulse, the
18
+ initial 2-sync frame, and ``frame_repeats`` additional 7-sync frames.
19
+
20
+ All frame parameters are supplied at construction time. The rolling code
21
+ counter must be tracked and incremented by the caller between
22
+ transmissions.
23
+
24
+ Protocol reference: https://pushstack.wordpress.com/somfy-rts-protocol/
25
+ """
26
+
27
+ address: int
28
+ rolling_code: int
29
+ button: int
30
+ frame_repeats: int
31
+
32
+ def __init__(
33
+ self,
34
+ *,
35
+ address: int,
36
+ rolling_code: int,
37
+ button: int,
38
+ frame_repeats: int = 3,
39
+ ) -> None:
40
+ """Initialize the Somfy RTS command."""
41
+ super().__init__(
42
+ frequency=_SOMFY_RTS_FREQUENCY,
43
+ modulation=ModulationType.OOK,
44
+ repeat_count=_SOMFY_RTS_REPEAT_COUNT,
45
+ )
46
+ self.address = address
47
+ self.rolling_code = rolling_code
48
+ self.button = button
49
+ self.frame_repeats = frame_repeats
50
+
51
+ @override
52
+ def get_raw_timings(self) -> list[int]:
53
+ """Compute Somfy RTS frame timings.
54
+
55
+ Builds the 7-byte frame, applies checksum and obfuscation, then
56
+ encodes the result as Manchester-coded OOK pulses. Consecutive
57
+ same-polarity values produced by Manchester transitions are merged so
58
+ the output strictly alternates between positive (mark) and negative
59
+ (space) microseconds.
60
+ """
61
+ # ── Build 7-byte frame ────────────────────────────────────────────
62
+ frame = bytearray(7)
63
+ frame[0] = 0xA7 # encryption key (fixed)
64
+ frame[1] = self.button << 4 # command nibble; lower = checksum
65
+ frame[2] = (self.rolling_code >> 8) & 0xFF
66
+ frame[3] = self.rolling_code & 0xFF
67
+ frame[4] = (self.address >> 16) & 0xFF
68
+ frame[5] = (self.address >> 8) & 0xFF
69
+ frame[6] = self.address & 0xFF
70
+
71
+ # Checksum: XOR of all nibbles across all 7 bytes
72
+ cksum = 0
73
+ for byte in frame:
74
+ cksum ^= byte ^ (byte >> 4)
75
+ frame[1] |= cksum & 0x0F
76
+
77
+ # Obfuscation: rolling XOR
78
+ for i in range(1, 7):
79
+ frame[i] ^= frame[i - 1]
80
+
81
+ # ── Build OOK pulse sequence ──────────────────────────────────────
82
+ # Positive = mark (HIGH), negative = space (LOW), units = µs.
83
+ # Consecutive same-polarity values are merged to maintain alternation.
84
+ timings: list[int] = []
85
+
86
+ def add(us: int) -> None:
87
+ if timings and (us > 0) == (timings[-1] > 0):
88
+ timings[-1] += us
89
+ else:
90
+ timings.append(us)
91
+
92
+ def encode_frame(sync: int) -> None:
93
+ if sync == 2:
94
+ add(9415)
95
+ add(-89565) # wake-up pulse + silence
96
+ for _ in range(sync):
97
+ add(2416)
98
+ add(-2416) # hardware sync pulses
99
+ add(4550)
100
+ add(-604) # software sync
101
+ # 56 data bits, MSB first, Manchester encoded
102
+ # Bit 1 = rising edge (LOW→HIGH), Bit 0 = falling edge (HIGH→LOW)
103
+ for b in range(56):
104
+ if (frame[b // 8] >> (7 - b % 8)) & 1:
105
+ add(-604)
106
+ add(604)
107
+ else:
108
+ add(604)
109
+ add(-604)
110
+ add(-30415) # inter-frame gap
111
+
112
+ encode_frame(2)
113
+ for _ in range(self.frame_repeats):
114
+ encode_frame(7)
115
+
116
+ return timings
@@ -4,7 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
- from .commands import OOKCommand, RadioFrequencyCommand
7
+ from .commands import RadioFrequencyCommand
8
+ from .commands.ook import OOKCommand
8
9
 
9
10
 
10
11
  def parse_sub_content(content: str) -> RadioFrequencyCommand:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rf-protocols
3
- Version: 2.2.0
3
+ Version: 3.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
@@ -2,7 +2,6 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  rf_protocols/__init__.py
5
- rf_protocols/commands.py
6
5
  rf_protocols/loader.py
7
6
  rf_protocols/parser.py
8
7
  rf_protocols.egg-info/PKG-INFO
@@ -10,8 +9,10 @@ rf_protocols.egg-info/SOURCES.txt
10
9
  rf_protocols.egg-info/dependency_links.txt
11
10
  rf_protocols.egg-info/requires.txt
12
11
  rf_protocols.egg-info/top_level.txt
12
+ rf_protocols/codes/honeywell/string_lights/__init__.py
13
13
  rf_protocols/codes/honeywell/string_lights/turn_off.sub
14
14
  rf_protocols/codes/honeywell/string_lights/turn_on.sub
15
+ rf_protocols/codes/novy/cooker_hood/__init__.py
15
16
  rf_protocols/codes/novy/cooker_hood/code_1/ambient_light.sub
16
17
  rf_protocols/codes/novy/cooker_hood/code_1/light.sub
17
18
  rf_protocols/codes/novy/cooker_hood/code_1/minus.sub
@@ -62,6 +63,11 @@ rf_protocols/codes/novy/cooker_hood/code_9/light.sub
62
63
  rf_protocols/codes/novy/cooker_hood/code_9/minus.sub
63
64
  rf_protocols/codes/novy/cooker_hood/code_9/plus.sub
64
65
  rf_protocols/codes/novy/cooker_hood/code_9/power.sub
65
- tests/test_commands.py
66
+ rf_protocols/codes/somfy/rts/__init__.py
67
+ rf_protocols/commands/__init__.py
68
+ rf_protocols/commands/kaku.py
69
+ rf_protocols/commands/ook.py
70
+ rf_protocols/commands/pt2262.py
71
+ rf_protocols/commands/somfy_rts.py
66
72
  tests/test_loader.py
67
73
  tests/test_parser.py
@@ -6,7 +6,9 @@ from pathlib import Path
6
6
  import pytest
7
7
 
8
8
  import rf_protocols
9
- from rf_protocols import CodeCollection, ModulationType, OOKCommand, get_codes
9
+ from rf_protocols import ModulationType
10
+ from rf_protocols.commands.ook import OOKCommand
11
+ from rf_protocols.loader import CodeCollection, get_codes
10
12
 
11
13
  _BUNDLED_CODES_ROOT = Path(rf_protocols.__file__).parent / "codes"
12
14
  _BUNDLED_SUB_FILES = sorted(_BUNDLED_CODES_ROOT.rglob("*.sub"))
@@ -2,7 +2,8 @@
2
2
 
3
3
  import pytest
4
4
 
5
- from rf_protocols import OOKCommand, parse_sub_content
5
+ from rf_protocols.commands.ook import OOKCommand
6
+ from rf_protocols.parser import parse_sub_content
6
7
 
7
8
  _SAMPLE_SUB = """\
8
9
  Filetype: Flipper SubGhz RAW File
@@ -1,14 +0,0 @@
1
- """Library to decode and encode radio frequency signals."""
2
-
3
- from .commands import ModulationType, OOKCommand, RadioFrequencyCommand
4
- from .loader import CodeCollection, get_codes
5
- from .parser import parse_sub_content
6
-
7
- __all__ = [
8
- "CodeCollection",
9
- "ModulationType",
10
- "OOKCommand",
11
- "RadioFrequencyCommand",
12
- "get_codes",
13
- "parse_sub_content",
14
- ]
@@ -1,103 +0,0 @@
1
- """Tests for the RF protocol definitions."""
2
-
3
- from rf_protocols import ModulationType, OOKCommand, RadioFrequencyCommand
4
-
5
-
6
- def test_modulation_type_ook() -> None:
7
- """Test ModulationType enum has OOK value."""
8
- assert ModulationType.OOK == "OOK"
9
- assert ModulationType.OOK.value == "OOK"
10
-
11
-
12
- class _MockCommand(RadioFrequencyCommand):
13
- """Simple concrete command for testing the base class."""
14
-
15
- def __init__(
16
- self,
17
- *,
18
- frequency: int = 433_920_000,
19
- modulation: ModulationType = ModulationType.OOK,
20
- repeat_count: int = 0,
21
- symbol_rate: int | None = None,
22
- output_power: float | None = None,
23
- ) -> None:
24
- super().__init__(
25
- frequency=frequency,
26
- modulation=modulation,
27
- repeat_count=repeat_count,
28
- symbol_rate=symbol_rate,
29
- output_power=output_power,
30
- )
31
-
32
- def get_raw_timings(self) -> list[int]:
33
- """Return a simple test pattern."""
34
- return [350, -1050, 350, -350]
35
-
36
-
37
- def test_command_defaults() -> None:
38
- """Test RadioFrequencyCommand default values."""
39
- cmd = _MockCommand()
40
- assert cmd.frequency == 433_920_000
41
- assert cmd.repeat_count == 0
42
- assert cmd.modulation == ModulationType.OOK
43
- assert cmd.symbol_rate is None
44
- assert cmd.output_power is None
45
-
46
-
47
- def test_command_custom_values() -> None:
48
- """Test RadioFrequencyCommand with custom values."""
49
- cmd = _MockCommand(
50
- frequency=868_000_000,
51
- repeat_count=3,
52
- symbol_rate=4800,
53
- output_power=10.0,
54
- )
55
- assert cmd.frequency == 868_000_000
56
- assert cmd.repeat_count == 3
57
- assert cmd.modulation == ModulationType.OOK
58
- assert cmd.symbol_rate == 4800
59
- assert cmd.output_power == 10.0
60
-
61
-
62
- def test_command_get_raw_timings() -> None:
63
- """Test get_raw_timings returns expected timings."""
64
- cmd = _MockCommand()
65
- assert cmd.get_raw_timings() == [350, -1050, 350, -350]
66
-
67
-
68
- def test_ook_command() -> None:
69
- """Test OOKCommand with raw timings."""
70
- timings = [350, -1050, 350, -350]
71
- cmd = OOKCommand(frequency=433_920_000, timings=timings)
72
- assert cmd.frequency == 433_920_000
73
- assert cmd.modulation == ModulationType.OOK
74
- assert cmd.repeat_count == 0
75
- assert cmd.get_raw_timings() == timings
76
-
77
-
78
- def test_command_repr() -> None:
79
- """Test RadioFrequencyCommand has a readable repr for subclasses."""
80
- cmd = OOKCommand(
81
- frequency=433_920_000,
82
- timings=[350, -1050],
83
- repeat_count=5,
84
- )
85
- assert repr(cmd) == "OOKCommand(OOK, 433.92 MHz, repeat=5)"
86
-
87
-
88
- def test_ook_command_custom_values() -> None:
89
- """Test OOKCommand with custom radio parameters."""
90
- timings = [500, -1000]
91
- cmd = OOKCommand(
92
- frequency=868_000_000,
93
- timings=timings,
94
- repeat_count=2,
95
- symbol_rate=4800,
96
- output_power=10.0,
97
- )
98
- assert cmd.frequency == 868_000_000
99
- assert cmd.modulation == ModulationType.OOK
100
- assert cmd.repeat_count == 2
101
- assert cmd.symbol_rate == 4800
102
- assert cmd.output_power == 10.0
103
- assert cmd.get_raw_timings() == timings
File without changes
File without changes
File without changes