blaueis-client 0.1.0rc1__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.
- blaueis_client-0.1.0rc1/LICENSE +21 -0
- blaueis_client-0.1.0rc1/PKG-INFO +44 -0
- blaueis_client-0.1.0rc1/README.md +16 -0
- blaueis_client-0.1.0rc1/pyproject.toml +52 -0
- blaueis_client-0.1.0rc1/setup.cfg +4 -0
- blaueis_client-0.1.0rc1/src/blaueis/client/__init__.py +1 -0
- blaueis_client-0.1.0rc1/src/blaueis/client/cli.py +173 -0
- blaueis_client-0.1.0rc1/src/blaueis/client/device.py +1280 -0
- blaueis_client-0.1.0rc1/src/blaueis/client/status_db.py +628 -0
- blaueis_client-0.1.0rc1/src/blaueis/client/ws_client.py +339 -0
- blaueis_client-0.1.0rc1/src/blaueis_client.egg-info/PKG-INFO +44 -0
- blaueis_client-0.1.0rc1/src/blaueis_client.egg-info/SOURCES.txt +32 -0
- blaueis_client-0.1.0rc1/src/blaueis_client.egg-info/dependency_links.txt +1 -0
- blaueis_client-0.1.0rc1/src/blaueis_client.egg-info/requires.txt +6 -0
- blaueis_client-0.1.0rc1/src/blaueis_client.egg-info/top_level.txt +1 -0
- blaueis_client-0.1.0rc1/tests/test_b1_polling.py +135 -0
- blaueis_client-0.1.0rc1/tests/test_cli.py +60 -0
- blaueis_client-0.1.0rc1/tests/test_constraint_gate.py +275 -0
- blaueis_client-0.1.0rc1/tests/test_device.py +468 -0
- blaueis_client-0.1.0rc1/tests/test_device_auth_failure.py +96 -0
- blaueis_client-0.1.0rc1/tests/test_device_freshness.py +82 -0
- blaueis_client-0.1.0rc1/tests/test_device_glossary_override.py +121 -0
- blaueis_client-0.1.0rc1/tests/test_device_live.py +139 -0
- blaueis_client-0.1.0rc1/tests/test_device_reconnect_race.py +215 -0
- blaueis_client-0.1.0rc1/tests/test_feature_gate.py +93 -0
- blaueis_client-0.1.0rc1/tests/test_mutex_forces.py +273 -0
- blaueis_client-0.1.0rc1/tests/test_nak_frame_dispatch.py +157 -0
- blaueis_client-0.1.0rc1/tests/test_optimistic.py +82 -0
- blaueis_client-0.1.0rc1/tests/test_power_gate.py +123 -0
- blaueis_client-0.1.0rc1/tests/test_status_db.py +315 -0
- blaueis_client-0.1.0rc1/tests/test_status_db_field_cache.py +173 -0
- blaueis_client-0.1.0rc1/tests/test_test_suppression.py +128 -0
- blaueis_client-0.1.0rc1/tests/test_write_lock.py +196 -0
- blaueis_client-0.1.0rc1/tests/test_ws_client.py +382 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fabian Schwamborn
|
|
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,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blaueis-client
|
|
3
|
+
Version: 0.1.0rc1
|
|
4
|
+
Summary: Blaueis HVAC gateway — WebSocket client library and CLI
|
|
5
|
+
Author: Fabian Schwamborn
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fabcoded/blaueis-libmidea
|
|
8
|
+
Project-URL: Repository, https://github.com/fabcoded/blaueis-libmidea
|
|
9
|
+
Project-URL: Documentation, https://github.com/fabcoded/blaueis-libmidea/tree/main/docs
|
|
10
|
+
Project-URL: Issues, https://github.com/fabcoded/blaueis-libmidea/issues
|
|
11
|
+
Keywords: midea,hvac,air-conditioner,websocket,home-automation
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Home Automation
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: blaueis-core~=0.1.0rc1
|
|
23
|
+
Requires-Dist: websockets>=12.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# blaueis-client
|
|
30
|
+
|
|
31
|
+
Async WebSocket client and Device wrapper for talking to a
|
|
32
|
+
[blaueis-gateway](https://pypi.org/project/blaueis-gateway/) instance.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install blaueis-client
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Documentation
|
|
41
|
+
|
|
42
|
+
Part of the [blaueis-libmidea](https://github.com/fabcoded/blaueis-libmidea)
|
|
43
|
+
project. See the [repo docs](https://github.com/fabcoded/blaueis-libmidea/tree/main/docs)
|
|
44
|
+
for the WebSocket protocol and architecture details.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# blaueis-client
|
|
2
|
+
|
|
3
|
+
Async WebSocket client and Device wrapper for talking to a
|
|
4
|
+
[blaueis-gateway](https://pypi.org/project/blaueis-gateway/) instance.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
pip install blaueis-client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Documentation
|
|
13
|
+
|
|
14
|
+
Part of the [blaueis-libmidea](https://github.com/fabcoded/blaueis-libmidea)
|
|
15
|
+
project. See the [repo docs](https://github.com/fabcoded/blaueis-libmidea/tree/main/docs)
|
|
16
|
+
for the WebSocket protocol and architecture details.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "blaueis-client"
|
|
7
|
+
version = "0.1.0rc1"
|
|
8
|
+
description = "Blaueis HVAC gateway — WebSocket client library and CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{name = "Fabian Schwamborn"}]
|
|
14
|
+
keywords = ["midea", "hvac", "air-conditioner", "websocket", "home-automation"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Home Automation",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"blaueis-core~=0.1.0rc1",
|
|
26
|
+
"websockets>=12.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = ["pytest>=7.0", "pytest-asyncio>=0.23"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/fabcoded/blaueis-libmidea"
|
|
34
|
+
Repository = "https://github.com/fabcoded/blaueis-libmidea"
|
|
35
|
+
Documentation = "https://github.com/fabcoded/blaueis-libmidea/tree/main/docs"
|
|
36
|
+
Issues = "https://github.com/fabcoded/blaueis-libmidea/issues"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
asyncio_mode = "auto"
|
|
44
|
+
# ``pytest-homeassistant-custom-component`` is installed in this dev env
|
|
45
|
+
# to support ha-midea integration tests. Pytest auto-discovers any
|
|
46
|
+
# installed plugin, and phcc's ``pytest_runtest_setup`` hook calls
|
|
47
|
+
# ``pytest_socket.disable_socket()`` unconditionally — correct for an
|
|
48
|
+
# HA integration test, but blocks our own ``test_device_live.py`` which
|
|
49
|
+
# legitimately opens a socket to talk to the gateway. libmidea is not
|
|
50
|
+
# an HA test suite, so we drop both plugins at the runner level. They
|
|
51
|
+
# stay installed; they're just not active when running libmidea tests.
|
|
52
|
+
addopts = "-p no:homeassistant -p no:socket"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""blaueis.client — placeholder, implementation coming in a later phase."""
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""HVAC Gateway CLI — interactive client for the Pi gateway.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python cli.py --host 192.168.1.50 --psk <hex>
|
|
6
|
+
python cli.py --host 192.168.1.50 --no-encrypt
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
query Send status query (0x41), show response
|
|
10
|
+
caps Send capability query (B5)
|
|
11
|
+
raw <hex> Send raw hex frame
|
|
12
|
+
monitor Continuous frame display (Ctrl+C to stop)
|
|
13
|
+
pi Show Pi gateway stats
|
|
14
|
+
ping Keepalive ping
|
|
15
|
+
help Show commands
|
|
16
|
+
quit Disconnect and exit
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import asyncio
|
|
21
|
+
import contextlib
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
# tools-serial has the canonical frame builders. Inserted LAST so it's
|
|
25
|
+
# searched FIRST in sys.path (overrides gateway's stripped-down midea_frame).
|
|
26
|
+
from blaueis.client.ws_client import HvacClient
|
|
27
|
+
from blaueis.core.frame import build_cap_query_extended, build_cap_query_simple, build_status_query
|
|
28
|
+
|
|
29
|
+
log = logging.getLogger("cli")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def format_frame(hex_str: str, ts: float) -> str:
|
|
33
|
+
"""Format a received frame for display."""
|
|
34
|
+
clean = hex_str.replace(" ", "")
|
|
35
|
+
if len(clean) < 4:
|
|
36
|
+
return f" [{ts:.3f}] {hex_str}"
|
|
37
|
+
|
|
38
|
+
# Parse basic info
|
|
39
|
+
body_start = clean[20:] # after 10-byte header (20 hex chars)
|
|
40
|
+
cmd_byte = body_start[:2] if body_start else "??"
|
|
41
|
+
msg_type = clean[18:20]
|
|
42
|
+
|
|
43
|
+
labels = {
|
|
44
|
+
"c0": "C0 Status Response",
|
|
45
|
+
"c1": "C1 Extended Data",
|
|
46
|
+
"b5": "B5 Capabilities",
|
|
47
|
+
"a1": "A1 Heartbeat",
|
|
48
|
+
"a0": "A0 Heartbeat ACK",
|
|
49
|
+
"41": "41 Status Query",
|
|
50
|
+
"40": "40 Set Command",
|
|
51
|
+
}
|
|
52
|
+
label = labels.get(cmd_byte.lower(), f"CMD 0x{cmd_byte}")
|
|
53
|
+
|
|
54
|
+
return f" [{ts:.3f}] {label} (msg=0x{msg_type}, {len(clean) // 2}B): {hex_str}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def interactive_loop(client: HvacClient):
|
|
58
|
+
"""Run the interactive command loop."""
|
|
59
|
+
# Start listener in background
|
|
60
|
+
listen_task = asyncio.create_task(client.listen())
|
|
61
|
+
|
|
62
|
+
# Set up frame display callback
|
|
63
|
+
monitoring = [False]
|
|
64
|
+
|
|
65
|
+
def on_frame(hex_str, ts):
|
|
66
|
+
if monitoring[0]:
|
|
67
|
+
print(format_frame(hex_str, ts))
|
|
68
|
+
|
|
69
|
+
def on_pi_status(stats):
|
|
70
|
+
print(
|
|
71
|
+
f"\n Pi: cpu={stats.get('cpu_percent', '?')}% "
|
|
72
|
+
f"ram={stats.get('ram_used_mb', '?')}/{stats.get('ram_total_mb', '?')}MB "
|
|
73
|
+
f"temp={stats.get('temp_c', '?')}°C "
|
|
74
|
+
f"up={stats.get('uptime_s', 0) // 3600}h "
|
|
75
|
+
f"state={stats.get('protocol_state', '?')}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
client.on_frame = on_frame
|
|
79
|
+
client.on_pi_status = on_pi_status
|
|
80
|
+
|
|
81
|
+
print("Connected. Type 'help' for commands.\n")
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
while True:
|
|
85
|
+
try:
|
|
86
|
+
line = await asyncio.get_event_loop().run_in_executor(None, lambda: input("> "))
|
|
87
|
+
except EOFError:
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
parts = line.strip().split()
|
|
91
|
+
if not parts:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
cmd = parts[0].lower()
|
|
95
|
+
|
|
96
|
+
if cmd == "quit" or cmd == "exit":
|
|
97
|
+
break
|
|
98
|
+
elif cmd == "help":
|
|
99
|
+
print(" query — send status query (0x41)")
|
|
100
|
+
print(" caps — send capability query (B5)")
|
|
101
|
+
print(" raw <hex>— send raw hex frame")
|
|
102
|
+
print(" monitor — toggle continuous frame display")
|
|
103
|
+
print(" pi — request Pi stats")
|
|
104
|
+
print(" ping — keepalive")
|
|
105
|
+
print(" quit — exit")
|
|
106
|
+
elif cmd == "query":
|
|
107
|
+
print(" Sending status query...")
|
|
108
|
+
await client.send_frame(build_status_query().hex(" "))
|
|
109
|
+
elif cmd == "caps":
|
|
110
|
+
print(" Sending capability queries (extended + simple)...")
|
|
111
|
+
await client.send_frame(build_cap_query_extended().hex(" "))
|
|
112
|
+
await asyncio.sleep(0.2)
|
|
113
|
+
await client.send_frame(build_cap_query_simple().hex(" "))
|
|
114
|
+
elif cmd == "raw":
|
|
115
|
+
hex_str = " ".join(parts[1:])
|
|
116
|
+
if not hex_str:
|
|
117
|
+
print(" Usage: raw AA 23 AC ...")
|
|
118
|
+
continue
|
|
119
|
+
ref = await client.send_frame(hex_str)
|
|
120
|
+
print(f" Sent (ref={ref})")
|
|
121
|
+
elif cmd == "monitor":
|
|
122
|
+
monitoring[0] = not monitoring[0]
|
|
123
|
+
print(f" Monitor {'ON' if monitoring[0] else 'OFF'}")
|
|
124
|
+
elif cmd == "pi":
|
|
125
|
+
await client.send_ping() # stats come on their own interval
|
|
126
|
+
print(" Pi stats will appear on next interval")
|
|
127
|
+
elif cmd == "ping":
|
|
128
|
+
await client.send_ping()
|
|
129
|
+
print(" Ping sent")
|
|
130
|
+
else:
|
|
131
|
+
print(f" Unknown command: {cmd}. Type 'help'.")
|
|
132
|
+
|
|
133
|
+
except KeyboardInterrupt:
|
|
134
|
+
pass
|
|
135
|
+
finally:
|
|
136
|
+
listen_task.cancel()
|
|
137
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
138
|
+
await listen_task
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def run(args):
|
|
142
|
+
from blaueis.core.crypto import psk_to_bytes
|
|
143
|
+
|
|
144
|
+
psk = psk_to_bytes(args.psk) if args.psk else None
|
|
145
|
+
client = HvacClient(args.host, args.port, psk=psk, no_encrypt=args.no_encrypt)
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
await client.connect()
|
|
149
|
+
await interactive_loop(client)
|
|
150
|
+
finally:
|
|
151
|
+
await client.close()
|
|
152
|
+
print("Disconnected.")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main():
|
|
156
|
+
parser = argparse.ArgumentParser(description="HVAC Gateway CLI Client")
|
|
157
|
+
parser.add_argument("--host", required=True, help="Gateway hostname/IP")
|
|
158
|
+
parser.add_argument("--port", type=int, default=8765, help="Gateway WebSocket port")
|
|
159
|
+
parser.add_argument("--psk", help="Pre-shared key (hex)")
|
|
160
|
+
parser.add_argument("--no-encrypt", action="store_true", help="Disable encryption")
|
|
161
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
162
|
+
args = parser.parse_args()
|
|
163
|
+
|
|
164
|
+
logging.basicConfig(
|
|
165
|
+
level=logging.DEBUG if args.debug else logging.INFO,
|
|
166
|
+
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
asyncio.run(run(args))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
main()
|