pysamsung-dplug 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.
- pysamsung_dplug-0.1.0/.gitignore +6 -0
- pysamsung_dplug-0.1.0/LICENSE +21 -0
- pysamsung_dplug-0.1.0/PKG-INFO +82 -0
- pysamsung_dplug-0.1.0/README.md +65 -0
- pysamsung_dplug-0.1.0/pyproject.toml +31 -0
- pysamsung_dplug-0.1.0/src/samsung_dplug/__init__.py +22 -0
- pysamsung_dplug-0.1.0/src/samsung_dplug/ac14k_m.pem +131 -0
- pysamsung_dplug-0.1.0/src/samsung_dplug/client.py +204 -0
- pysamsung_dplug-0.1.0/src/samsung_dplug/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 porech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pysamsung-dplug
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async client for old Samsung air conditioners using the DPLUG/AC14K protocol (TLS, port 2878)
|
|
5
|
+
Project-URL: Homepage, https://github.com/porech/pysamsung-dplug
|
|
6
|
+
Project-URL: Issues, https://github.com/porech/pysamsung-dplug/issues
|
|
7
|
+
Author: porech
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: 2878,ac14k,air conditioner,dplug,home assistant,hvac,samsung
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Home Automation
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# pysamsung-dplug
|
|
19
|
+
|
|
20
|
+
Async Python client for **old Samsung air conditioners** that speak the legacy
|
|
21
|
+
**DPLUG / AC14K** protocol over TLS on **port 2878** (Wi-Fi modules such as
|
|
22
|
+
`SWL-B70F`, used by the AR\*\*HSFS generation, ~2013–2015).
|
|
23
|
+
|
|
24
|
+
These units were dropped by SmartThings. This library lets you control them
|
|
25
|
+
locally again — no cloud. It is the protocol layer used by the
|
|
26
|
+
[`samsung_ac_dplug`](https://github.com/porech/samsung_ac_dplug) Home Assistant
|
|
27
|
+
integration, but works standalone.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Mutual-TLS handshake with the bundled Samsung client certificate (legacy
|
|
32
|
+
TLS 1.0 / weak ciphers handled for you).
|
|
33
|
+
- Token acquisition (`GetToken`), authentication (`AuthToken`).
|
|
34
|
+
- Read full device state (`DeviceState`) and send commands (`DeviceControl`).
|
|
35
|
+
- Auto-discover the device id (`DeviceList`) and a passive `async_probe()`.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install pysamsung-dplug
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio
|
|
47
|
+
from samsung_dplug import SamsungAcClient, build_ssl_context
|
|
48
|
+
|
|
49
|
+
async def main():
|
|
50
|
+
ctx = build_ssl_context() # uses the bundled certificate
|
|
51
|
+
client = SamsungAcClient("192.168.1.53", token="xxxxxxxx-....", ssl_context=ctx)
|
|
52
|
+
state = await client.async_get_state()
|
|
53
|
+
print("Power:", state["AC_FUN_POWER"], "Room:", state["AC_FUN_TEMPNOW"])
|
|
54
|
+
await client.async_set("AC_FUN_POWER", "On")
|
|
55
|
+
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Getting a token
|
|
60
|
+
|
|
61
|
+
The unit only issues a token at **power-on** (a physical proof-of-access step):
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
client = SamsungAcClient("192.168.1.53", ssl_context=ctx)
|
|
65
|
+
# turn the unit OFF, call this, then turn it ON within ~30 s:
|
|
66
|
+
token = await client.async_get_token()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> `build_ssl_context()` does blocking file I/O; inside async frameworks run it in
|
|
70
|
+
> an executor (e.g. Home Assistant: `await hass.async_add_executor_job(build_ssl_context)`).
|
|
71
|
+
|
|
72
|
+
## Protocol notes
|
|
73
|
+
|
|
74
|
+
Reverse-engineered from the official *Smart Air Conditioner* app and live
|
|
75
|
+
devices. The unit greets with `DPLUG-1.x`, requires mutual TLS, and uses an
|
|
76
|
+
XML request/response protocol. The DUID equals the Wi-Fi module MAC without
|
|
77
|
+
separators. See the Home Assistant integration repo for the full write-up,
|
|
78
|
+
including the undocumented `APConnectionConfig` Wi-Fi provisioning command.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# pysamsung-dplug
|
|
2
|
+
|
|
3
|
+
Async Python client for **old Samsung air conditioners** that speak the legacy
|
|
4
|
+
**DPLUG / AC14K** protocol over TLS on **port 2878** (Wi-Fi modules such as
|
|
5
|
+
`SWL-B70F`, used by the AR\*\*HSFS generation, ~2013–2015).
|
|
6
|
+
|
|
7
|
+
These units were dropped by SmartThings. This library lets you control them
|
|
8
|
+
locally again — no cloud. It is the protocol layer used by the
|
|
9
|
+
[`samsung_ac_dplug`](https://github.com/porech/samsung_ac_dplug) Home Assistant
|
|
10
|
+
integration, but works standalone.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Mutual-TLS handshake with the bundled Samsung client certificate (legacy
|
|
15
|
+
TLS 1.0 / weak ciphers handled for you).
|
|
16
|
+
- Token acquisition (`GetToken`), authentication (`AuthToken`).
|
|
17
|
+
- Read full device state (`DeviceState`) and send commands (`DeviceControl`).
|
|
18
|
+
- Auto-discover the device id (`DeviceList`) and a passive `async_probe()`.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install pysamsung-dplug
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import asyncio
|
|
30
|
+
from samsung_dplug import SamsungAcClient, build_ssl_context
|
|
31
|
+
|
|
32
|
+
async def main():
|
|
33
|
+
ctx = build_ssl_context() # uses the bundled certificate
|
|
34
|
+
client = SamsungAcClient("192.168.1.53", token="xxxxxxxx-....", ssl_context=ctx)
|
|
35
|
+
state = await client.async_get_state()
|
|
36
|
+
print("Power:", state["AC_FUN_POWER"], "Room:", state["AC_FUN_TEMPNOW"])
|
|
37
|
+
await client.async_set("AC_FUN_POWER", "On")
|
|
38
|
+
|
|
39
|
+
asyncio.run(main())
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Getting a token
|
|
43
|
+
|
|
44
|
+
The unit only issues a token at **power-on** (a physical proof-of-access step):
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
client = SamsungAcClient("192.168.1.53", ssl_context=ctx)
|
|
48
|
+
# turn the unit OFF, call this, then turn it ON within ~30 s:
|
|
49
|
+
token = await client.async_get_token()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> `build_ssl_context()` does blocking file I/O; inside async frameworks run it in
|
|
53
|
+
> an executor (e.g. Home Assistant: `await hass.async_add_executor_job(build_ssl_context)`).
|
|
54
|
+
|
|
55
|
+
## Protocol notes
|
|
56
|
+
|
|
57
|
+
Reverse-engineered from the official *Smart Air Conditioner* app and live
|
|
58
|
+
devices. The unit greets with `DPLUG-1.x`, requires mutual TLS, and uses an
|
|
59
|
+
XML request/response protocol. The DUID equals the Wi-Fi module MAC without
|
|
60
|
+
separators. See the Home Assistant integration repo for the full write-up,
|
|
61
|
+
including the undocumented `APConnectionConfig` Wi-Fi provisioning command.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pysamsung-dplug"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Async client for old Samsung air conditioners using the DPLUG/AC14K protocol (TLS, port 2878)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "porech" }]
|
|
13
|
+
keywords = ["samsung", "air conditioner", "hvac", "dplug", "ac14k", "2878", "home assistant"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Home Automation",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/porech/pysamsung-dplug"
|
|
24
|
+
Issues = "https://github.com/porech/pysamsung-dplug/issues"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/samsung_dplug"]
|
|
28
|
+
artifacts = ["*.pem", "py.typed"]
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.sdist]
|
|
31
|
+
include = ["src/samsung_dplug", "README.md", "LICENSE"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""pysamsung-dplug: async client for old Samsung air conditioners (DPLUG / port 2878)."""
|
|
2
|
+
from .client import (
|
|
3
|
+
DEFAULT_PORT,
|
|
4
|
+
AuthError,
|
|
5
|
+
SamsungAcClient,
|
|
6
|
+
SamsungAcError,
|
|
7
|
+
async_probe,
|
|
8
|
+
build_ssl_context,
|
|
9
|
+
default_cert_path,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"SamsungAcClient",
|
|
14
|
+
"SamsungAcError",
|
|
15
|
+
"AuthError",
|
|
16
|
+
"build_ssl_context",
|
|
17
|
+
"async_probe",
|
|
18
|
+
"default_cert_path",
|
|
19
|
+
"DEFAULT_PORT",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Bag Attributes
|
|
2
|
+
friendlyName: ac14k_m
|
|
3
|
+
localKeyID: 54 69 6D 65 20 31 34 35 35 34 39 32 33 35 32 36 39 36
|
|
4
|
+
Key Attributes: <No Attributes>
|
|
5
|
+
-----BEGIN PRIVATE KEY-----
|
|
6
|
+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDeXvhcsqRFWfQt
|
|
7
|
+
Qr2TGW+ePJzrKQVNOZmCGFXrBmOKa2gcZvXqDe71upkCmbXxDZbsqU1nFox6WtKy
|
|
8
|
+
za+JE1EaWIjVFV/D0hnnF+CA56851rFjAx7YYVtd9TwJYV1lSfJaQBU/ecUys0SX
|
|
9
|
+
lKZJtjIoJ/PyLREE79TjOqTxXMXnDpiAt3oiwApZMweJ5z2QViqtRepkI33GDgYc
|
|
10
|
+
LzamIengSG6WZkEUr2roY0il4aVXf3IRVRX+5cJ2L5462kFPKm/UnrXrSsrLwbF9
|
|
11
|
+
ltSlNA8FgpHN3d9ZyqB3MB46oGYyxYYU7a+/R3RAx0joNfVPFe8riQXQcoNEgalb
|
|
12
|
+
c8f7N4HPAgMBAAECggEABL80QA5UMWLNMpYlI9m8Jz2V//MdONvM6hkI5H57a34F
|
|
13
|
+
d+2+vCNWAYrdL1AGsUGgAidPDq9NimMb8lMvtxZhedV//kR5id2XTfaVhUrs06hA
|
|
14
|
+
myN66hWR9LyCbpTUgJAGi2Soz3US/5USFsZGknZANdk8fOP3ZAqWmc8rrDdVxivg
|
|
15
|
+
Z3qjiqgIZg24XsZmnK/QJejP4FLMqm6YUouH//u9xSKvTwkg89qxvygW9xNBNfi/
|
|
16
|
+
LrBHip/k8LnynKRE2odQWt74HcTjbZW4rxXrJ0tqDSIh8bUB23mRjFh1k4aKXnz3
|
|
17
|
+
Y/CDsfxvAutVi85/zyxYaIT6daP+PxvywwgjVhYHIQKBgQDyMxmGdi5kk3ePv0lM
|
|
18
|
+
lC28gVNhgKfhsXL8xIzcd/UM3eEK4baA+AKI9p6ifZ8g90NUJGuHxCp8/9yOKcmk
|
|
19
|
+
tY5toE45nH4fH9Z3j9NtWHMhXJDGWV+DjeiWmshbUqd7/OoIl1vig1npQqX+PXJR
|
|
20
|
+
pwDHnjkQbkyum4k8/IruHx813wKBgQDrCqK0rBkMaarr5eyOJ9BxhCej8i5kCzm7
|
|
21
|
+
XSaNgXtpBIQ4Y4r412M2JWaSLnDxlAc0iUhNGnIn4zkEP2HzX5JU4Yto9YAlRZnu
|
|
22
|
+
NQSvuVgyLiBCbS7WrRAlsNpTeCU3m+c5QNXBzBlHCiTdw3WS4bINOftsB3xnlJ+D
|
|
23
|
+
y/0YZozSEQKBgQCgWV5z3Dh40/0bSVyA+7WQENsgOWpsjOwBFyvfJvgxLZC5gJgw
|
|
24
|
+
qIIdJZH/KEY7MBj+UyJx/1jV6xudb2MVzjHeuHwxvj7t4kk+XRVwVlfa5YrgFvma
|
|
25
|
+
glBTrWQquf0ypE5Zo8PsomPbgAmf2hSepH9qqYFENJJGI6lnnBdq8WXbZwKBgQCR
|
|
26
|
+
p3ye5At9wrnWCB0pFwk4X4JFOd5/xukW8CnlBTmaId9iJmXHwYpM0q6Wpkr9mhNA
|
|
27
|
+
/lYc2eemSkxaEoE71Z0UFtVSzNiFwHUcxiRKVVyPdEAvigO9q2/XO5qAoXLG3ElV
|
|
28
|
+
FJWizD1Z5bJk7yycQlsZkTX6g0UX12VmwnHsvhhEUQKBgF0AVToAk+/OPxlA3N4A
|
|
29
|
+
Xn624Ktxzy/58NSLUfQ57AtL2zivoJzfmhUwgYkPsp+63Wklpcq7X7Q2NB7WscC4
|
|
30
|
+
rICqHxNow/KSzwuR6L3u/kewvlsrgTIM2Pp//+QdTK9GGU3HHAZKaNiB8m20k1Bs
|
|
31
|
+
NTANFxBk7alY0G7ZUhuzWkg6
|
|
32
|
+
-----END PRIVATE KEY-----
|
|
33
|
+
Bag Attributes
|
|
34
|
+
friendlyName: ac14k_m
|
|
35
|
+
localKeyID: 54 69 6D 65 20 31 34 35 35 34 39 32 33 35 32 36 39 36
|
|
36
|
+
subject=/C=KR/O=Samsung Electronics/CN=AC14K_M/emailAddress=AC14K_M@samsung.com
|
|
37
|
+
issuer=/C=KR/O=Samsung Electronics/CN=RemoteAccessCA(CE)
|
|
38
|
+
-----BEGIN CERTIFICATE-----
|
|
39
|
+
MIIDmzCCAoOgAwIBAgIBCTANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQGEwJLUjEc
|
|
40
|
+
MBoGA1UECgwTU2Ftc3VuZyBFbGVjdHJvbmljczEbMBkGA1UEAwwSUmVtb3RlQWNj
|
|
41
|
+
ZXNzQ0EoQ0UpMCIYDzE5NjAwMTAxMDAwMDAwWhgPMjA2MDAxMDEwMDAwMDBaMGEx
|
|
42
|
+
CzAJBgNVBAYTAktSMRwwGgYDVQQKExNTYW1zdW5nIEVsZWN0cm9uaWNzMRAwDgYD
|
|
43
|
+
VQQDFAdBQzE0S19NMSIwIAYJKoZIhvcNAQkBFhNBQzE0S19NQHNhbXN1bmcuY29t
|
|
44
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3l74XLKkRVn0LUK9kxlv
|
|
45
|
+
njyc6ykFTTmZghhV6wZjimtoHGb16g3u9bqZApm18Q2W7KlNZxaMelrSss2viRNR
|
|
46
|
+
GliI1RVfw9IZ5xfggOevOdaxYwMe2GFbXfU8CWFdZUnyWkAVP3nFMrNEl5SmSbYy
|
|
47
|
+
KCfz8i0RBO/U4zqk8VzF5w6YgLd6IsAKWTMHiec9kFYqrUXqZCN9xg4GHC82piHp
|
|
48
|
+
4EhulmZBFK9q6GNIpeGlV39yEVUV/uXCdi+eOtpBTypv1J6160rKy8GxfZbUpTQP
|
|
49
|
+
BYKRzd3fWcqgdzAeOqBmMsWGFO2vv0d0QMdI6DX1TxXvK4kF0HKDRIGpW3PH+zeB
|
|
50
|
+
zwIDAQABo3MwcTAdBgNVHQ4EFgQUXzEjosLzA6xbR1KAqnmAp3BNM6MwHwYDVR0j
|
|
51
|
+
BBgwFoAU/12TkC/BOF7xDaZZWJ+DGN6nMxcwDAYDVR0TBAUwAwEB/zAhBgNVHREE
|
|
52
|
+
GjAYggtzYW1zdW5nLmNvbYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBBQUAA4IBAQBW
|
|
53
|
+
0mStlbdvrHqDJ+KOKVf0C/y9FKTODqo/6/wJNZeZ+8ezPza4nFq70MwQYTpSbZhz
|
|
54
|
+
5w8bQP9fwSAoa2Vki8ZwcSd85Vi2tHz9O4C7d7zBA3FU8AL3NoEMFv6OGWGPnTY5
|
|
55
|
+
mG/Hn+LxuwQddlysfbRDds1LBY8DBUJNAmIeeWqA5Eg8DW6xJUwHeXUElJpSXHW6
|
|
56
|
+
XGvpWgAhXqoIf6TirdCrPY6+IzV/FcuVtBDGi+JoxgrMfMLgLEVjeSY96DJinHgZ
|
|
57
|
+
RT0FkA5e06Z+fqHh9Btu+aed+kuGSmya/A5wStOkGeKEbezbbN2gtW07lN6VxX3J
|
|
58
|
+
OCgygA+hmnBVnRDA8Jzu
|
|
59
|
+
-----END CERTIFICATE-----
|
|
60
|
+
Bag Attributes
|
|
61
|
+
friendlyName: CN=RemoteAccessCA(CE),O=Samsung Electronics,C=KR
|
|
62
|
+
subject=/C=KR/O=Samsung Electronics/CN=RemoteAccessCA(CE)
|
|
63
|
+
issuer=/C=KR/O=Samsung Electronics/CN=CECA
|
|
64
|
+
-----BEGIN CERTIFICATE-----
|
|
65
|
+
MIIDUTCCAjmgAwIBAgIBADANBgkqhkiG9w0BAQUFADA6MQswCQYDVQQGEwJLUjEc
|
|
66
|
+
MBoGA1UECgwTU2Ftc3VuZyBFbGVjdHJvbmljczENMAsGA1UEAwwEQ0VDQTAiGA8x
|
|
67
|
+
OTYwMDEwMTAwMDAwMFoYDzIwNjAwMTAxMDAwMDAwWjBIMQswCQYDVQQGEwJLUjEc
|
|
68
|
+
MBoGA1UECgwTU2Ftc3VuZyBFbGVjdHJvbmljczEbMBkGA1UEAwwSUmVtb3RlQWNj
|
|
69
|
+
ZXNzQ0EoQ0UpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtatz9GvV
|
|
70
|
+
qbV395Whnad9MC9TEOiXuwnw37QHvQUwOTFgc6AenX5SORfb4UTw+0ApFNba9DlY
|
|
71
|
+
Xx/K9E5b5DGasDVGGTn+z+6MPB7GuAjkP+WSRwHMjrHRNqrBOr1YJUw3SIbMkRoT
|
|
72
|
+
460k9AD9DQDBORRtGBGwcBw6BvdasA+/L3Q63aJ7pDoj3qxocdcgk/zFq0OrxFDL
|
|
73
|
+
PMTL7a+a9DS8G10K73XGgES0RBwwhlXXVuLUprD6RgbeLHFsPpIq5vzzEpAYMCF6
|
|
74
|
+
vkZKjDGEW7JVTgUu0E37niN3NQv1gIXlJusDH6RWfFQxENZsdFkT/l+kTuY283Ga
|
|
75
|
+
2Ei1HsW3Xpt88QIDAQABo1AwTjAdBgNVHQ4EFgQU/12TkC/BOF7xDaZZWJ+DGN6n
|
|
76
|
+
MxcwHwYDVR0jBBgwFoAURwF9jkihypJa2u6zRwKrZwRlACswDAYDVR0TBAUwAwEB
|
|
77
|
+
/zANBgkqhkiG9w0BAQUFAAOCAQEAZkjxN4O92e1RTaXx1mpazyT98sJVl46R51s1
|
|
78
|
+
CTPq35HVfTiBOAu0C5MR6a9vIIFJScy5h69VN4OwDDbMhe/k3m6EfAutlL7lRrre
|
|
79
|
+
OT853HJahxdavzaXJ7tcrI/yDJI0X5GbQ8W74mmDt2/5rXsaB+h+NrToGqf6Hvf/
|
|
80
|
+
m7ZhUnCAt0hhLmltxTVYS25s9KoiIH0rXOb9cqUFsmBMEG2pHWC5AiSc0cXJm+kU
|
|
81
|
+
3z0B2GS+4IjGdVr3FTPzzTXrpqq/X1cIVKAum5WfsFMS0CRvqTVNVwYg52n69T2B
|
|
82
|
+
NPCCEpp9rsIieZ58jsnc506Uc+1Vp+NmBI2A/ecypZxSb6v9gg==
|
|
83
|
+
-----END CERTIFICATE-----
|
|
84
|
+
Bag Attributes
|
|
85
|
+
friendlyName: CN=CECA,O=Samsung Electronics,C=KR
|
|
86
|
+
subject=/C=KR/O=Samsung Electronics/CN=CECA
|
|
87
|
+
issuer=/C=KR/O=Samsung Electronics/CN=ROOTCA
|
|
88
|
+
-----BEGIN CERTIFICATE-----
|
|
89
|
+
MIIDRTCCAi2gAwIBAgIBBDANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJLUjEc
|
|
90
|
+
MBoGA1UECgwTU2Ftc3VuZyBFbGVjdHJvbmljczEPMA0GA1UEAwwGUk9PVENBMCIY
|
|
91
|
+
DzE5NjAwMTAxMDAwMDAwWhgPMjA2MDAxMDEwMDAwMDBaMDoxCzAJBgNVBAYTAktS
|
|
92
|
+
MRwwGgYDVQQKDBNTYW1zdW5nIEVsZWN0cm9uaWNzMQ0wCwYDVQQDDARDRUNBMIIB
|
|
93
|
+
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtv1WJJ7tTs/aa1ZZRjMPPLeb
|
|
94
|
+
n/Ev0Y28CSBj/6P031/veZSg/2z65QZUvPjv8MZnIgNoMpxMGbPPO4Dxj+QJthBk
|
|
95
|
+
WydWRPguPyE+w3U4SdayZXWpLZTpKfHco3CklFwEqZtG/wTxHD1oOvtT0e2g5c79
|
|
96
|
+
hNQt9lQ4Wwzqa3MvQd0JyeB4syy2zRLo5NjJZl1BVn2oTt4xGCjjtAXtAqqHEbEf
|
|
97
|
+
pcvB3hPdIpFe6M8zuN22kROKaQ5i4XP4CyEpbFlgKRcWBGQFX3I5f5TdD3Yw1Ril
|
|
98
|
+
OLLL9wFsJ+iWLka9tAIcJKCNOf48p7aXm6COFwmjtCNu4wjQozwi6cycKUgxNQID
|
|
99
|
+
AQABo1AwTjAdBgNVHQ4EFgQURwF9jkihypJa2u6zRwKrZwRlACswHwYDVR0jBBgw
|
|
100
|
+
FoAU7andrmFFrxYM8+93lrn/Fq47sXMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
|
|
101
|
+
AQUFAAOCAQEATexseQBXSfUR7fFTFxq6aAvHWIN+h3QLeN1sq8KCM4fbdkH3lOUP
|
|
102
|
+
rKW3w1ag62bnJVNjT4xPtzH/DyrqlzQUPTb7S0PfIXt2mu/VURnrmuXidS2grNwv
|
|
103
|
+
eu10gURZaz9N2UZEhY7E80tUZwcjAV+YP8+x3/iRQSrWvcMma/r01eUnwrF4xaE9
|
|
104
|
+
EYtJ/jTRre8MpEH/lg06m+rZf9Lk/yhG6at0YnUAIytThqFV4Cj8T8jBX+KG8BCo
|
|
105
|
+
VyUsFyrO+D6X98gMdTZnLqC1P1iWuxyrOWZTgsf44f5GXzmLqe5KLPvkDb4MywTa
|
|
106
|
+
nXrSOPSkcIgvS6WYw2Rii+e6lfVzqmhAmg==
|
|
107
|
+
-----END CERTIFICATE-----
|
|
108
|
+
Bag Attributes
|
|
109
|
+
friendlyName: CN=ROOTCA,O=Samsung Electronics,C=KR
|
|
110
|
+
subject=/C=KR/O=Samsung Electronics/CN=ROOTCA
|
|
111
|
+
issuer=/C=KR/O=Samsung Electronics/CN=ROOTCA
|
|
112
|
+
-----BEGIN CERTIFICATE-----
|
|
113
|
+
MIIDRzCCAi+gAwIBAgIBADANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJLUjEc
|
|
114
|
+
MBoGA1UECgwTU2Ftc3VuZyBFbGVjdHJvbmljczEPMA0GA1UEAwwGUk9PVENBMCIY
|
|
115
|
+
DzE5NjAwMTAxMDAwMDAwWhgPMjA2MDAxMDEwMDAwMDBaMDwxCzAJBgNVBAYTAktS
|
|
116
|
+
MRwwGgYDVQQKDBNTYW1zdW5nIEVsZWN0cm9uaWNzMQ8wDQYDVQQDDAZST09UQ0Ew
|
|
117
|
+
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCd67g2hzhbIeSBoFfeqbXi
|
|
118
|
+
tzbO4dCWeCigVfmwEDhR1SDA0MfHOVlFpvuFr3WyFPvQZ0ccNrsTpBs5YieI/jZi
|
|
119
|
+
FYWO0ktbqQorL1CIFqBL9kAF+34BYtpl98PgJ1grLOH5T3GugJA7Irw0plEFmOfs
|
|
120
|
+
IydlUIQHl3oqyMIWPa2nIZ/FGi3hAquEPrvzHZB+QO4c+6tV1WLIaCjn88xkYuwz
|
|
121
|
+
uGYxaqJpnGdqhjZRIuHb2DEZPlP1VGdTTAttno36CyWqeHrSC8fXCSu55Zk+1rbC
|
|
122
|
+
Py/phOJjSyce2qk0IebETAYLCLqU7ABJxUxrolMrP37OB+Kqe4RWovaeMcdcNOOt
|
|
123
|
+
AgMBAAGjUDBOMB0GA1UdDgQWBBTtqd2uYUWvFgzz73eWuf8WrjuxczAfBgNVHSME
|
|
124
|
+
GDAWgBTtqd2uYUWvFgzz73eWuf8WrjuxczAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
|
|
125
|
+
DQEBBQUAA4IBAQBkwK95x8JCAnY0F2bMwG5+7QfY+ci8s8m1ODi3v19HECS6nG9j
|
|
126
|
+
SXgwihEtQ3HqvUler+n7aOeAZlgm+BymM2GvuicveYN/nevIvzlpMOn2L6xU19/H
|
|
127
|
+
zM2eoDVfS49+i/cwoi/A7fcZmIYggZho2UJR/GvKc79g6EAhT7/i5alBZF0enMsA
|
|
128
|
+
9okzakb/aohQE9SzsEHnhVKpGAjvu0/TJK9WwX6mkiIEJY+mzQMWgEeQt6WWIgAb
|
|
129
|
+
gSX9NueH80tpZ9KqFnqnOoLxTAa7k0RPBRwyUO9CDhSnlWIEcsD9sqR2M+niOFnT
|
|
130
|
+
KBHcLDDiEU3llprD8FRV3unYrl0F0B2GGdRk
|
|
131
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Async client for the Samsung 'DPLUG' air-conditioner protocol (TLS, port 2878).
|
|
2
|
+
|
|
3
|
+
Reverse-engineered from the AC14K / DPLUG-1.x firmware used by older Samsung
|
|
4
|
+
air conditioners with the SWL-Bxxx Wi-Fi modules. Mutual TLS using the public
|
|
5
|
+
Samsung client certificate (bundled ac14k_m.pem), with legacy ciphers enabled.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import ssl
|
|
13
|
+
from importlib import resources
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
DEFAULT_PORT = 2878
|
|
18
|
+
_TERM = b"\r\n"
|
|
19
|
+
_ATTR_RE = re.compile(r'Attr ID="([^"]*)" Type="([^"]*)" Value="([^"]*)"')
|
|
20
|
+
_DUID_RE = re.compile(r'Device DUID="([^"]*)"')
|
|
21
|
+
_TOKEN_RE = re.compile(r'Token="([^"]*)"')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SamsungAcError(Exception):
|
|
25
|
+
"""Generic protocol error."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AuthError(SamsungAcError):
|
|
29
|
+
"""Authentication (token) rejected."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def default_cert_path() -> str:
|
|
33
|
+
"""Path to the bundled Samsung client certificate."""
|
|
34
|
+
return str(resources.files(__package__).joinpath("ac14k_m.pem"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_ssl_context(cert_path: str | None = None) -> ssl.SSLContext:
|
|
38
|
+
"""Build the legacy mutual-TLS context. Blocking (file I/O) -> run in executor."""
|
|
39
|
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
40
|
+
ctx.check_hostname = False
|
|
41
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
42
|
+
ctx.minimum_version = ssl.TLSVersion.TLSv1
|
|
43
|
+
ctx.set_ciphers("HIGH:!DH:!aNULL:@SECLEVEL=0")
|
|
44
|
+
ctx.load_cert_chain(cert_path or default_cert_path())
|
|
45
|
+
return ctx
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def async_probe(host: str, ssl_context: ssl.SSLContext, port: int = DEFAULT_PORT) -> bool:
|
|
49
|
+
"""Return True if `host` speaks the DPLUG protocol (used by discovery)."""
|
|
50
|
+
try:
|
|
51
|
+
reader, writer = await asyncio.wait_for(
|
|
52
|
+
asyncio.open_connection(host, port, ssl=ssl_context, server_hostname=host),
|
|
53
|
+
timeout=8,
|
|
54
|
+
)
|
|
55
|
+
except (OSError, asyncio.TimeoutError, ssl.SSLError):
|
|
56
|
+
return False
|
|
57
|
+
try:
|
|
58
|
+
line = await asyncio.wait_for(reader.readuntil(_TERM), 5)
|
|
59
|
+
return "DPLUG" in line.decode("utf-8", "replace")
|
|
60
|
+
except Exception: # noqa: BLE001
|
|
61
|
+
return False
|
|
62
|
+
finally:
|
|
63
|
+
writer.close()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SamsungAcClient:
|
|
67
|
+
"""Short-lived-connection client: connect, auth, do one exchange, close.
|
|
68
|
+
|
|
69
|
+
Serialised with a lock so polling and commands never overlap on the device
|
|
70
|
+
(the module accepts essentially one connection at a time).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, host, token=None, ssl_context=None, duid=None, port=DEFAULT_PORT):
|
|
74
|
+
self._host = host
|
|
75
|
+
self._port = port
|
|
76
|
+
self._token = token
|
|
77
|
+
self._ctx = ssl_context
|
|
78
|
+
self._duid = duid
|
|
79
|
+
self._lock = asyncio.Lock()
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def duid(self):
|
|
83
|
+
return self._duid
|
|
84
|
+
|
|
85
|
+
async def _readline(self, reader, timeout=5.0):
|
|
86
|
+
data = await asyncio.wait_for(reader.readuntil(_TERM), timeout)
|
|
87
|
+
return data.decode("utf-8", "replace").strip()
|
|
88
|
+
|
|
89
|
+
async def _connect(self):
|
|
90
|
+
reader, writer = await asyncio.wait_for(
|
|
91
|
+
asyncio.open_connection(
|
|
92
|
+
self._host, self._port, ssl=self._ctx, server_hostname=self._host
|
|
93
|
+
),
|
|
94
|
+
timeout=10,
|
|
95
|
+
)
|
|
96
|
+
greeting = await self._readline(reader)
|
|
97
|
+
if "DPLUG" not in greeting:
|
|
98
|
+
writer.close()
|
|
99
|
+
raise SamsungAcError(f"Unexpected greeting: {greeting!r}")
|
|
100
|
+
return reader, writer
|
|
101
|
+
|
|
102
|
+
async def _authenticate(self, reader, writer):
|
|
103
|
+
line = await self._readline(reader)
|
|
104
|
+
if "InvalidateAccount" not in line:
|
|
105
|
+
line = await self._readline(reader)
|
|
106
|
+
writer.write(
|
|
107
|
+
f'<Request Type="AuthToken"><User Token="{self._token}"/></Request>'.encode()
|
|
108
|
+
+ _TERM
|
|
109
|
+
)
|
|
110
|
+
await writer.drain()
|
|
111
|
+
for _ in range(4):
|
|
112
|
+
line = await self._readline(reader)
|
|
113
|
+
if 'Type="AuthToken"' in line and 'Status="Okay"' in line:
|
|
114
|
+
return
|
|
115
|
+
if 'Status="Fail"' in line and "Auth" in line:
|
|
116
|
+
raise AuthError(f"Token rejected: {line}")
|
|
117
|
+
raise AuthError("No AuthToken Okay received")
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _parse_state(line: str) -> dict:
|
|
121
|
+
return {m[0]: m[2] for m in _ATTR_RE.findall(line)}
|
|
122
|
+
|
|
123
|
+
async def _read_until(self, reader, needle, timeout=6.0):
|
|
124
|
+
loop = asyncio.get_running_loop()
|
|
125
|
+
end = loop.time() + timeout
|
|
126
|
+
while loop.time() < end:
|
|
127
|
+
line = await self._readline(reader, timeout=timeout)
|
|
128
|
+
if needle in line:
|
|
129
|
+
return line
|
|
130
|
+
raise SamsungAcError(f"Timeout waiting for {needle}")
|
|
131
|
+
|
|
132
|
+
async def async_discover_duid(self):
|
|
133
|
+
async with self._lock:
|
|
134
|
+
reader, writer = await self._connect()
|
|
135
|
+
try:
|
|
136
|
+
await self._authenticate(reader, writer)
|
|
137
|
+
writer.write(b'<Request Type="DeviceList"></Request>' + _TERM)
|
|
138
|
+
await writer.drain()
|
|
139
|
+
line = await self._read_until(reader, "DeviceList")
|
|
140
|
+
m = _DUID_RE.search(line)
|
|
141
|
+
if not m:
|
|
142
|
+
raise SamsungAcError(f"No DUID in DeviceList: {line}")
|
|
143
|
+
self._duid = m.group(1)
|
|
144
|
+
return self._duid
|
|
145
|
+
finally:
|
|
146
|
+
writer.close()
|
|
147
|
+
|
|
148
|
+
async def async_get_state(self) -> dict:
|
|
149
|
+
if not self._duid:
|
|
150
|
+
await self.async_discover_duid()
|
|
151
|
+
async with self._lock:
|
|
152
|
+
reader, writer = await self._connect()
|
|
153
|
+
try:
|
|
154
|
+
await self._authenticate(reader, writer)
|
|
155
|
+
writer.write(
|
|
156
|
+
f'<Request Type="DeviceState" DUID="{self._duid}"></Request>'.encode()
|
|
157
|
+
+ _TERM
|
|
158
|
+
)
|
|
159
|
+
await writer.drain()
|
|
160
|
+
line = await self._read_until(reader, 'Type="DeviceState"')
|
|
161
|
+
if 'Status="Okay"' not in line:
|
|
162
|
+
raise SamsungAcError(f"DeviceState failed: {line}")
|
|
163
|
+
return self._parse_state(line)
|
|
164
|
+
finally:
|
|
165
|
+
writer.close()
|
|
166
|
+
|
|
167
|
+
async def async_set(self, attr: str, value: str) -> None:
|
|
168
|
+
if not self._duid:
|
|
169
|
+
await self.async_discover_duid()
|
|
170
|
+
async with self._lock:
|
|
171
|
+
reader, writer = await self._connect()
|
|
172
|
+
try:
|
|
173
|
+
await self._authenticate(reader, writer)
|
|
174
|
+
cmd = (
|
|
175
|
+
f'<Request Type="DeviceControl"><Control CommandID="cmd" '
|
|
176
|
+
f'DUID="{self._duid}"><Attr ID="{attr}" Value="{value}" />'
|
|
177
|
+
f"</Control></Request>"
|
|
178
|
+
)
|
|
179
|
+
writer.write(cmd.encode() + _TERM)
|
|
180
|
+
await writer.drain()
|
|
181
|
+
line = await self._read_until(reader, 'Type="DeviceControl"')
|
|
182
|
+
if 'Status="Okay"' not in line:
|
|
183
|
+
raise SamsungAcError(f"Control {attr}={value} failed: {line}")
|
|
184
|
+
finally:
|
|
185
|
+
writer.close()
|
|
186
|
+
|
|
187
|
+
async def async_get_token(self, power_on_timeout=40.0) -> str:
|
|
188
|
+
"""One-shot token acquisition. User must power the unit ON during the window."""
|
|
189
|
+
async with self._lock:
|
|
190
|
+
reader, writer = await self._connect()
|
|
191
|
+
try:
|
|
192
|
+
line = await self._readline(reader)
|
|
193
|
+
if "InvalidateAccount" not in line:
|
|
194
|
+
line = await self._readline(reader)
|
|
195
|
+
writer.write(b'<Request Type="GetToken" />' + _TERM)
|
|
196
|
+
await writer.drain()
|
|
197
|
+
await self._read_until(reader, 'Type="GetToken"') # Ready
|
|
198
|
+
token_line = await self._read_until(reader, "Token=", timeout=power_on_timeout)
|
|
199
|
+
m = _TOKEN_RE.search(token_line)
|
|
200
|
+
if not m:
|
|
201
|
+
raise SamsungAcError(f"No token in: {token_line}")
|
|
202
|
+
return m.group(1)
|
|
203
|
+
finally:
|
|
204
|
+
writer.close()
|
|
File without changes
|