samsung-exlink 1.0.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,222 @@
1
+ Metadata-Version: 2.4
2
+ Name: samsung-exlink
3
+ Version: 1.0.0
4
+ Summary: Async library to control Samsung consumer TVs over RS232
5
+ Author: Paulus Schoutsen
6
+ Author-email: Paulus Schoutsen <balloob@gmail.com>
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Home Automation
16
+ Classifier: Topic :: System :: Hardware
17
+ Classifier: Framework :: AsyncIO
18
+ Requires-Dist: serialx[esphome]>=1.2.0
19
+ Requires-Python: >=3.12
20
+ Project-URL: Repository, https://github.com/home-assistant-libs/samsung-exlink
21
+ Description-Content-Type: text/markdown
22
+
23
+ # samsung-exlink
24
+
25
+ Async Python library to control Samsung consumer TVs (Frame, Q-series, 8000-series, etc.) over their RS232 serial connection -- which Samsung markets as **ExLink** (also written EX-Link / EXT Link) -- built on [serialx](https://github.com/puddly/serialx).
26
+
27
+ "ExLink" and "RS232" refer to the same thing here: the TV's serial control port. This library speaks that protocol; it does not use the network/WiFi APIs.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install samsung-exlink
33
+ ```
34
+
35
+ Requires Python 3.12+.
36
+
37
+ ## Hardware
38
+
39
+ Samsung consumer TVs expose RS232 either via:
40
+
41
+ - A native 3.5 mm "ExLink" jack (Q70 and above) -- plug in and go.
42
+ - 3.5 mm pin-out: tip = TV TX (RX into your controller), ring = TV RX, sleeve = GND.
43
+ - A USB port via the proprietary Samsung USB-to-ExLink dongle (8000 / Q60).
44
+ - The TV's service menu (Mute -> 1 -> 8 -> 2 -> Power on the IR remote) must
45
+ have **EXT Link Support** and **USB Serial** enabled.
46
+
47
+ The serial link is 9600 8N1.
48
+
49
+ ## Quick start
50
+
51
+ ```python
52
+ import asyncio
53
+ from samsung_exlink import SamsungTV, InputSource, Key, PictureMode
54
+
55
+ async def main():
56
+ tv = SamsungTV("/dev/ttyUSB0")
57
+ await tv.connect()
58
+ try:
59
+ await tv.power_on()
60
+ await tv.set_volume(25)
61
+ await tv.select_input_source(InputSource.HDMI1)
62
+ await tv.set_picture_mode(PictureMode.MOVIE)
63
+ await tv.send_key(Key.KEY_MENU)
64
+ finally:
65
+ await tv.disconnect()
66
+
67
+ asyncio.run(main())
68
+ ```
69
+
70
+ The constructor also accepts an `esphome://host/?port_name=NAME` URL for use
71
+ with an ESPHome UART proxy.
72
+
73
+ ## CLI
74
+
75
+ A built-in CLI lets you quickly test your connection:
76
+
77
+ ```bash
78
+ python -m samsung_exlink /dev/ttyUSB0 power-on
79
+ python -m samsung_exlink /dev/ttyUSB0 power-off
80
+ python -m samsung_exlink /dev/ttyUSB0 volume 25
81
+ python -m samsung_exlink /dev/ttyUSB0 volume-up
82
+ python -m samsung_exlink /dev/ttyUSB0 volume-down
83
+ python -m samsung_exlink /dev/ttyUSB0 mute
84
+ python -m samsung_exlink /dev/ttyUSB0 channel-up
85
+ python -m samsung_exlink /dev/ttyUSB0 channel-down
86
+ python -m samsung_exlink /dev/ttyUSB0 source HDMI1
87
+ python -m samsung_exlink /dev/ttyUSB0 key KEY_MENU
88
+ python -m samsung_exlink /dev/ttyUSB0 raw 0d 00 00 1a # MENU via raw command
89
+ python -m samsung_exlink /dev/ttyUSB0 status # query power/volume/mute/source/channel
90
+ python -m samsung_exlink /dev/ttyUSB0 listen # passively log frames
91
+ ```
92
+
93
+ The `status` command queries the TV directly. To translate the raw source
94
+ byte into a named input, pass a known generation with `--model` (one of
95
+ `frame_2022`, `h_series_2016`):
96
+
97
+ ```bash
98
+ python -m samsung_exlink --model frame_2022 /dev/ttyUSB0 status
99
+ ```
100
+
101
+ ## Protocol
102
+
103
+ Samsung consumer TVs use a fixed 7-byte binary frame:
104
+
105
+ ```
106
+ 08 22 [cmd1] [cmd2] [cmd3] [value] [checksum]
107
+ ```
108
+
109
+ - `08 22` is the fixed header.
110
+ - `cmd1`, `cmd2`, `cmd3`, `value` form the command (see the protocol PDF
111
+ bundled with this repo).
112
+ - `checksum = (256 - sum(bytes 0..5)) & 0xFF`.
113
+
114
+ The TV replies with a 3-byte response: `03 0C F1` for ACK, `03 0C FF` for
115
+ NACK. Most commands are fire-and-forget, so the library also tracks state
116
+ from what it sends. A subset of state (power, volume, mute, channel, source)
117
+ can additionally be read back with a `cmd1=0xF0` status query -- the TV
118
+ answers with `03 0C F5` plus a 10-byte payload. See
119
+ [State tracking](#state-tracking) below.
120
+
121
+ ### Examples
122
+
123
+ | Operation | Frame |
124
+ |------------------|-------------------------------------|
125
+ | Power off | `08 22 00 00 00 01 D5` |
126
+ | Power on | `08 22 00 00 00 02 D4` |
127
+ | Volume = 25 | `08 22 01 00 00 19 BC` |
128
+ | Mute toggle | `08 22 02 00 00 00 D4` |
129
+ | Source: HDMI1 | `08 22 0A 00 05 00 C7` |
130
+ | Source: TV | `08 22 0A 00 00 00 CC` |
131
+ | Key: MENU | `08 22 0D 00 00 1A AF` |
132
+
133
+ ## Features
134
+
135
+ ### State tracking
136
+
137
+ ```python
138
+ state = tv.state
139
+ state.power # bool | None (None = unknown)
140
+ state.volume # int | None (0-100)
141
+ state.mute # bool | None
142
+ state.input_source # InputSource | None
143
+ state.picture_mode # PictureMode | None
144
+ state.sound_mode # SoundMode | None
145
+ ```
146
+
147
+ Fields are populated as commands succeed, and also refreshed by the `query_*`
148
+ methods below. Power state is unknown after connect; call `power_on()` /
149
+ `power_off()`, or `query_power()` to read it from the TV.
150
+
151
+ ### Status queries
152
+
153
+ Some TVs answer a small set of status queries directly:
154
+
155
+ ```python
156
+ await tv.query_power() # PowerState.ON / STANDBY / OFF
157
+ await tv.query_volume() # 0-100
158
+ await tv.query_mute() # bool
159
+ await tv.query_channel() # (major, mid, minor)
160
+ await tv.query_source() # raw source byte (generation-specific)
161
+ await tv.query_source_input() # InputSource | None (needs a source map)
162
+ ```
163
+
164
+ Each query updates `tv.state` and returns the value. The byte returned by
165
+ `query_source()` differs by TV generation, so mapping it back to an
166
+ `InputSource` needs a source map. Supply a known one via the constructor:
167
+
168
+ ```python
169
+ from samsung_exlink import SamsungTV, MODELS
170
+
171
+ tv = SamsungTV("/dev/ttyUSB0", model=MODELS["frame_2022"])
172
+ ```
173
+
174
+ or discover it for your TV once with `await tv.probe_sources()` (which
175
+ cycles every input and records the byte each reports) and pass the returned
176
+ mapping back as `source_map=...` on future runs. Queries raise `TimeoutError`
177
+ on TVs/firmwares that don't support them.
178
+
179
+ ### Event subscription
180
+
181
+ ```python
182
+ def on_state_change(state):
183
+ if state is None:
184
+ print("Disconnected")
185
+ return
186
+ print(f"Volume: {state.volume}, Source: {state.input_source}")
187
+
188
+ unsubscribe = tv.subscribe(on_state_change)
189
+ ```
190
+
191
+ The callback is called with a `TVState` snapshot on each change, or `None`
192
+ when the connection is torn down.
193
+
194
+ ### Raw access
195
+
196
+ ```python
197
+ response = await tv.send_raw(0x0d, 0x00, 0x00, 0x1A) # KEY_MENU
198
+ # response is the 3-byte reply (e.g. b"\x03\x0c\xf1")
199
+ ```
200
+
201
+ `send_raw` returns the reply without raising on NACK, for diagnostics.
202
+
203
+ ## Available commands
204
+
205
+ First-class helpers cover power (`power_on`/`power_off`/`power_toggle`),
206
+ volume (`set_volume`/`volume_up`/`volume_down`), `mute`, channels
207
+ (`channel_up`/`channel_down`), `select_input_source`, `set_picture_mode`,
208
+ `set_sound_mode`, and the Frame/Anynet+ toggles `set_art_mode`,
209
+ `set_ambient_mode`, and `set_hdmi_cec`.
210
+
211
+ The `Key` enum mirrors the Samsung remote-control key map and includes
212
+ power, source, navigation, transport, color, and number keys. The
213
+ `InputSource` enum covers TV, AV1-3, S-Video1-3, Component1-3, PC1-3,
214
+ HDMI1-4, DVI1-3, and RVU.
215
+
216
+ For commands not yet wrapped (e.g. white-balance, color space, screen
217
+ adjustments), use `send_raw()` with the values from the protocol PDF
218
+ (`docs/Samsung-RS232-Control.pdf`).
219
+
220
+ ## License
221
+
222
+ MIT
@@ -0,0 +1,200 @@
1
+ # samsung-exlink
2
+
3
+ Async Python library to control Samsung consumer TVs (Frame, Q-series, 8000-series, etc.) over their RS232 serial connection -- which Samsung markets as **ExLink** (also written EX-Link / EXT Link) -- built on [serialx](https://github.com/puddly/serialx).
4
+
5
+ "ExLink" and "RS232" refer to the same thing here: the TV's serial control port. This library speaks that protocol; it does not use the network/WiFi APIs.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install samsung-exlink
11
+ ```
12
+
13
+ Requires Python 3.12+.
14
+
15
+ ## Hardware
16
+
17
+ Samsung consumer TVs expose RS232 either via:
18
+
19
+ - A native 3.5 mm "ExLink" jack (Q70 and above) -- plug in and go.
20
+ - 3.5 mm pin-out: tip = TV TX (RX into your controller), ring = TV RX, sleeve = GND.
21
+ - A USB port via the proprietary Samsung USB-to-ExLink dongle (8000 / Q60).
22
+ - The TV's service menu (Mute -> 1 -> 8 -> 2 -> Power on the IR remote) must
23
+ have **EXT Link Support** and **USB Serial** enabled.
24
+
25
+ The serial link is 9600 8N1.
26
+
27
+ ## Quick start
28
+
29
+ ```python
30
+ import asyncio
31
+ from samsung_exlink import SamsungTV, InputSource, Key, PictureMode
32
+
33
+ async def main():
34
+ tv = SamsungTV("/dev/ttyUSB0")
35
+ await tv.connect()
36
+ try:
37
+ await tv.power_on()
38
+ await tv.set_volume(25)
39
+ await tv.select_input_source(InputSource.HDMI1)
40
+ await tv.set_picture_mode(PictureMode.MOVIE)
41
+ await tv.send_key(Key.KEY_MENU)
42
+ finally:
43
+ await tv.disconnect()
44
+
45
+ asyncio.run(main())
46
+ ```
47
+
48
+ The constructor also accepts an `esphome://host/?port_name=NAME` URL for use
49
+ with an ESPHome UART proxy.
50
+
51
+ ## CLI
52
+
53
+ A built-in CLI lets you quickly test your connection:
54
+
55
+ ```bash
56
+ python -m samsung_exlink /dev/ttyUSB0 power-on
57
+ python -m samsung_exlink /dev/ttyUSB0 power-off
58
+ python -m samsung_exlink /dev/ttyUSB0 volume 25
59
+ python -m samsung_exlink /dev/ttyUSB0 volume-up
60
+ python -m samsung_exlink /dev/ttyUSB0 volume-down
61
+ python -m samsung_exlink /dev/ttyUSB0 mute
62
+ python -m samsung_exlink /dev/ttyUSB0 channel-up
63
+ python -m samsung_exlink /dev/ttyUSB0 channel-down
64
+ python -m samsung_exlink /dev/ttyUSB0 source HDMI1
65
+ python -m samsung_exlink /dev/ttyUSB0 key KEY_MENU
66
+ python -m samsung_exlink /dev/ttyUSB0 raw 0d 00 00 1a # MENU via raw command
67
+ python -m samsung_exlink /dev/ttyUSB0 status # query power/volume/mute/source/channel
68
+ python -m samsung_exlink /dev/ttyUSB0 listen # passively log frames
69
+ ```
70
+
71
+ The `status` command queries the TV directly. To translate the raw source
72
+ byte into a named input, pass a known generation with `--model` (one of
73
+ `frame_2022`, `h_series_2016`):
74
+
75
+ ```bash
76
+ python -m samsung_exlink --model frame_2022 /dev/ttyUSB0 status
77
+ ```
78
+
79
+ ## Protocol
80
+
81
+ Samsung consumer TVs use a fixed 7-byte binary frame:
82
+
83
+ ```
84
+ 08 22 [cmd1] [cmd2] [cmd3] [value] [checksum]
85
+ ```
86
+
87
+ - `08 22` is the fixed header.
88
+ - `cmd1`, `cmd2`, `cmd3`, `value` form the command (see the protocol PDF
89
+ bundled with this repo).
90
+ - `checksum = (256 - sum(bytes 0..5)) & 0xFF`.
91
+
92
+ The TV replies with a 3-byte response: `03 0C F1` for ACK, `03 0C FF` for
93
+ NACK. Most commands are fire-and-forget, so the library also tracks state
94
+ from what it sends. A subset of state (power, volume, mute, channel, source)
95
+ can additionally be read back with a `cmd1=0xF0` status query -- the TV
96
+ answers with `03 0C F5` plus a 10-byte payload. See
97
+ [State tracking](#state-tracking) below.
98
+
99
+ ### Examples
100
+
101
+ | Operation | Frame |
102
+ |------------------|-------------------------------------|
103
+ | Power off | `08 22 00 00 00 01 D5` |
104
+ | Power on | `08 22 00 00 00 02 D4` |
105
+ | Volume = 25 | `08 22 01 00 00 19 BC` |
106
+ | Mute toggle | `08 22 02 00 00 00 D4` |
107
+ | Source: HDMI1 | `08 22 0A 00 05 00 C7` |
108
+ | Source: TV | `08 22 0A 00 00 00 CC` |
109
+ | Key: MENU | `08 22 0D 00 00 1A AF` |
110
+
111
+ ## Features
112
+
113
+ ### State tracking
114
+
115
+ ```python
116
+ state = tv.state
117
+ state.power # bool | None (None = unknown)
118
+ state.volume # int | None (0-100)
119
+ state.mute # bool | None
120
+ state.input_source # InputSource | None
121
+ state.picture_mode # PictureMode | None
122
+ state.sound_mode # SoundMode | None
123
+ ```
124
+
125
+ Fields are populated as commands succeed, and also refreshed by the `query_*`
126
+ methods below. Power state is unknown after connect; call `power_on()` /
127
+ `power_off()`, or `query_power()` to read it from the TV.
128
+
129
+ ### Status queries
130
+
131
+ Some TVs answer a small set of status queries directly:
132
+
133
+ ```python
134
+ await tv.query_power() # PowerState.ON / STANDBY / OFF
135
+ await tv.query_volume() # 0-100
136
+ await tv.query_mute() # bool
137
+ await tv.query_channel() # (major, mid, minor)
138
+ await tv.query_source() # raw source byte (generation-specific)
139
+ await tv.query_source_input() # InputSource | None (needs a source map)
140
+ ```
141
+
142
+ Each query updates `tv.state` and returns the value. The byte returned by
143
+ `query_source()` differs by TV generation, so mapping it back to an
144
+ `InputSource` needs a source map. Supply a known one via the constructor:
145
+
146
+ ```python
147
+ from samsung_exlink import SamsungTV, MODELS
148
+
149
+ tv = SamsungTV("/dev/ttyUSB0", model=MODELS["frame_2022"])
150
+ ```
151
+
152
+ or discover it for your TV once with `await tv.probe_sources()` (which
153
+ cycles every input and records the byte each reports) and pass the returned
154
+ mapping back as `source_map=...` on future runs. Queries raise `TimeoutError`
155
+ on TVs/firmwares that don't support them.
156
+
157
+ ### Event subscription
158
+
159
+ ```python
160
+ def on_state_change(state):
161
+ if state is None:
162
+ print("Disconnected")
163
+ return
164
+ print(f"Volume: {state.volume}, Source: {state.input_source}")
165
+
166
+ unsubscribe = tv.subscribe(on_state_change)
167
+ ```
168
+
169
+ The callback is called with a `TVState` snapshot on each change, or `None`
170
+ when the connection is torn down.
171
+
172
+ ### Raw access
173
+
174
+ ```python
175
+ response = await tv.send_raw(0x0d, 0x00, 0x00, 0x1A) # KEY_MENU
176
+ # response is the 3-byte reply (e.g. b"\x03\x0c\xf1")
177
+ ```
178
+
179
+ `send_raw` returns the reply without raising on NACK, for diagnostics.
180
+
181
+ ## Available commands
182
+
183
+ First-class helpers cover power (`power_on`/`power_off`/`power_toggle`),
184
+ volume (`set_volume`/`volume_up`/`volume_down`), `mute`, channels
185
+ (`channel_up`/`channel_down`), `select_input_source`, `set_picture_mode`,
186
+ `set_sound_mode`, and the Frame/Anynet+ toggles `set_art_mode`,
187
+ `set_ambient_mode`, and `set_hdmi_cec`.
188
+
189
+ The `Key` enum mirrors the Samsung remote-control key map and includes
190
+ power, source, navigation, transport, color, and number keys. The
191
+ `InputSource` enum covers TV, AV1-3, S-Video1-3, Component1-3, PC1-3,
192
+ HDMI1-4, DVI1-3, and RVU.
193
+
194
+ For commands not yet wrapped (e.g. white-balance, color space, screen
195
+ adjustments), use `send_raw()` with the values from the protocol PDF
196
+ (`docs/Samsung-RS232-Control.pdf`).
197
+
198
+ ## License
199
+
200
+ MIT
@@ -0,0 +1,54 @@
1
+ [project]
2
+ name = "samsung-exlink"
3
+ version = "1.0.0"
4
+ description = "Async library to control Samsung consumer TVs over RS232"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Paulus Schoutsen", email = "balloob@gmail.com" }
9
+ ]
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "serialx[esphome]>=1.2.0",
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Topic :: Home Automation",
23
+ "Topic :: System :: Hardware",
24
+ "Framework :: AsyncIO",
25
+ ]
26
+
27
+ [project.scripts]
28
+ samsung-exlink = "samsung_exlink.__main__:main"
29
+
30
+ [project.urls]
31
+ Repository = "https://github.com/home-assistant-libs/samsung-exlink"
32
+
33
+ [build-system]
34
+ requires = ["uv_build>=0.8.4,<0.9.0"]
35
+ build-backend = "uv_build"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "pytest>=9.0",
40
+ "pytest-asyncio>=1.0",
41
+ "pytest-timeout>=2.4.0",
42
+ "ruff>=0.10",
43
+ ]
44
+
45
+ [tool.pytest.ini_options]
46
+ asyncio_mode = "auto"
47
+ timeout = 1
48
+
49
+ [tool.ruff]
50
+ target-version = "py312"
51
+ src = ["src", "tests", "scripts"]
52
+
53
+ [tool.ruff.lint]
54
+ select = ["E", "F", "I", "UP", "B"]
@@ -0,0 +1,79 @@
1
+ """Async library to control Samsung consumer TVs over RS232 using serialx."""
2
+
3
+ from .const import (
4
+ ACK_RESPONSE,
5
+ BAUD_RATE,
6
+ COMMAND_TIMEOUT,
7
+ FRAME_LENGTH,
8
+ HEADER,
9
+ NACK_RESPONSE,
10
+ QUERY_DATA_FOLLOWS,
11
+ QUERY_PAYLOAD_LENGTH,
12
+ QUERY_RESPONSE_HEADER,
13
+ RESPONSE_LENGTH,
14
+ ColorTone,
15
+ InputSource,
16
+ Key,
17
+ PictureMode,
18
+ PictureSize,
19
+ PowerCommand,
20
+ PowerState,
21
+ QueryCategory,
22
+ SoundMode,
23
+ )
24
+ from .models import ALL_MODELS, MODELS, TVModel
25
+ from .protocol import (
26
+ PendingResponse,
27
+ QueryResponse,
28
+ build_frame,
29
+ calculate_checksum,
30
+ is_ack,
31
+ is_nack,
32
+ parse_frame,
33
+ parse_query_payload,
34
+ )
35
+ from .state import TVState
36
+ from .tv import (
37
+ CommandRejected,
38
+ SamsungTV,
39
+ SamsungTVError,
40
+ StateCallback,
41
+ )
42
+
43
+ __all__ = [
44
+ "ACK_RESPONSE",
45
+ "ALL_MODELS",
46
+ "BAUD_RATE",
47
+ "COMMAND_TIMEOUT",
48
+ "ColorTone",
49
+ "CommandRejected",
50
+ "FRAME_LENGTH",
51
+ "HEADER",
52
+ "InputSource",
53
+ "Key",
54
+ "MODELS",
55
+ "NACK_RESPONSE",
56
+ "PendingResponse",
57
+ "PictureMode",
58
+ "PictureSize",
59
+ "PowerCommand",
60
+ "PowerState",
61
+ "QUERY_DATA_FOLLOWS",
62
+ "QUERY_PAYLOAD_LENGTH",
63
+ "QUERY_RESPONSE_HEADER",
64
+ "QueryCategory",
65
+ "QueryResponse",
66
+ "RESPONSE_LENGTH",
67
+ "SamsungTV",
68
+ "SamsungTVError",
69
+ "SoundMode",
70
+ "StateCallback",
71
+ "TVModel",
72
+ "TVState",
73
+ "build_frame",
74
+ "calculate_checksum",
75
+ "is_ack",
76
+ "is_nack",
77
+ "parse_frame",
78
+ "parse_query_payload",
79
+ ]