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.
Files changed (34) hide show
  1. blaueis_client-0.1.0rc1/LICENSE +21 -0
  2. blaueis_client-0.1.0rc1/PKG-INFO +44 -0
  3. blaueis_client-0.1.0rc1/README.md +16 -0
  4. blaueis_client-0.1.0rc1/pyproject.toml +52 -0
  5. blaueis_client-0.1.0rc1/setup.cfg +4 -0
  6. blaueis_client-0.1.0rc1/src/blaueis/client/__init__.py +1 -0
  7. blaueis_client-0.1.0rc1/src/blaueis/client/cli.py +173 -0
  8. blaueis_client-0.1.0rc1/src/blaueis/client/device.py +1280 -0
  9. blaueis_client-0.1.0rc1/src/blaueis/client/status_db.py +628 -0
  10. blaueis_client-0.1.0rc1/src/blaueis/client/ws_client.py +339 -0
  11. blaueis_client-0.1.0rc1/src/blaueis_client.egg-info/PKG-INFO +44 -0
  12. blaueis_client-0.1.0rc1/src/blaueis_client.egg-info/SOURCES.txt +32 -0
  13. blaueis_client-0.1.0rc1/src/blaueis_client.egg-info/dependency_links.txt +1 -0
  14. blaueis_client-0.1.0rc1/src/blaueis_client.egg-info/requires.txt +6 -0
  15. blaueis_client-0.1.0rc1/src/blaueis_client.egg-info/top_level.txt +1 -0
  16. blaueis_client-0.1.0rc1/tests/test_b1_polling.py +135 -0
  17. blaueis_client-0.1.0rc1/tests/test_cli.py +60 -0
  18. blaueis_client-0.1.0rc1/tests/test_constraint_gate.py +275 -0
  19. blaueis_client-0.1.0rc1/tests/test_device.py +468 -0
  20. blaueis_client-0.1.0rc1/tests/test_device_auth_failure.py +96 -0
  21. blaueis_client-0.1.0rc1/tests/test_device_freshness.py +82 -0
  22. blaueis_client-0.1.0rc1/tests/test_device_glossary_override.py +121 -0
  23. blaueis_client-0.1.0rc1/tests/test_device_live.py +139 -0
  24. blaueis_client-0.1.0rc1/tests/test_device_reconnect_race.py +215 -0
  25. blaueis_client-0.1.0rc1/tests/test_feature_gate.py +93 -0
  26. blaueis_client-0.1.0rc1/tests/test_mutex_forces.py +273 -0
  27. blaueis_client-0.1.0rc1/tests/test_nak_frame_dispatch.py +157 -0
  28. blaueis_client-0.1.0rc1/tests/test_optimistic.py +82 -0
  29. blaueis_client-0.1.0rc1/tests/test_power_gate.py +123 -0
  30. blaueis_client-0.1.0rc1/tests/test_status_db.py +315 -0
  31. blaueis_client-0.1.0rc1/tests/test_status_db_field_cache.py +173 -0
  32. blaueis_client-0.1.0rc1/tests/test_test_suppression.py +128 -0
  33. blaueis_client-0.1.0rc1/tests/test_write_lock.py +196 -0
  34. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()