aiorexense 0.1.1__py3-none-any.whl

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.
aiorexense/__init__.py ADDED
@@ -0,0 +1,86 @@
1
+ """
2
+ Rexense WS client library init.
3
+ """
4
+
5
+ __version__ = "0.1.1"
6
+
7
+ from .api import get_basic_info
8
+ from .const import (
9
+ DEFAULT_PORT,
10
+ VENDOR_CODE,
11
+ API_VERSION,
12
+ FUNCTION_GET_BASIC_INFO,
13
+ FUNCTION_NOTIFY_STATUS,
14
+ FUNCTION_INVOKE_CMD,
15
+ REXENSE_SENSOR_CURRENT,
16
+ REXENSE_SENSOR_VOLTAGE,
17
+ REXENSE_SENSOR_POWER_FACTOR,
18
+ REXENSE_SENSOR_ACTIVE_POWER,
19
+ REXENSE_SENSOR_APPARENT_POWER,
20
+ REXENSE_SENSOR_B_CURRENT,
21
+ REXENSE_SENSOR_B_VOLTAGE,
22
+ REXENSE_SENSOR_B_POWER_FACTOR,
23
+ REXENSE_SENSOR_B_ACTIVE_POWER,
24
+ REXENSE_SENSOR_B_APPARENT_POWER,
25
+ REXENSE_SENSOR_C_CURRENT,
26
+ REXENSE_SENSOR_C_VOLTAGE,
27
+ REXENSE_SENSOR_C_POWER_FACTOR,
28
+ REXENSE_SENSOR_C_ACTIVE_POWER,
29
+ REXENSE_SENSOR_C_APPARENT_POWER,
30
+ REXENSE_SENSOR_TOTAL_ACTIVE_POWER,
31
+ REXENSE_SENSOR_TOTAL_APPARENT_POWER,
32
+ REXENSE_SENSOR_CEI,
33
+ REXENSE_SENSOR_CEE,
34
+ REXENSE_SENSOR_A_CEI,
35
+ REXENSE_SENSOR_A_CEE,
36
+ REXENSE_SENSOR_B_CEI,
37
+ REXENSE_SENSOR_B_CEE,
38
+ REXENSE_SENSOR_C_CEI,
39
+ REXENSE_SENSOR_C_CEE,
40
+ REXENSE_SENSOR_TEMPERATURE,
41
+ REXENSE_SENSOR_BATTERY_PERCENTAGE,
42
+ REXENSE_SENSOR_BATTERY_VOLTAGE,
43
+ REXENSE_SWITCH_ONOFF,
44
+ )
45
+ from .ws_client import RexenseWebsocketClient
46
+
47
+ __all__ = [
48
+ "get_basic_info",
49
+ "RexenseWebsocketClient",
50
+ # constants
51
+ "DEFAULT_PORT",
52
+ "VENDOR_CODE",
53
+ "API_VERSION",
54
+ "FUNCTION_GET_BASIC_INFO",
55
+ "FUNCTION_NOTIFY_STATUS",
56
+ "FUNCTION_INVOKE_CMD",
57
+ "REXENSE_SENSOR_CURRENT",
58
+ "REXENSE_SENSOR_VOLTAGE",
59
+ "REXENSE_SENSOR_POWER_FACTOR",
60
+ "REXENSE_SENSOR_ACTIVE_POWER",
61
+ "REXENSE_SENSOR_APPARENT_POWER",
62
+ "REXENSE_SENSOR_B_CURRENT",
63
+ "REXENSE_SENSOR_B_VOLTAGE",
64
+ "REXENSE_SENSOR_B_POWER_FACTOR",
65
+ "REXENSE_SENSOR_B_ACTIVE_POWER",
66
+ "REXENSE_SENSOR_B_APPARENT_POWER",
67
+ "REXENSE_SENSOR_C_CURRENT",
68
+ "REXENSE_SENSOR_C_VOLTAGE",
69
+ "REXENSE_SENSOR_C_POWER_FACTOR",
70
+ "REXENSE_SENSOR_C_ACTIVE_POWER",
71
+ "REXENSE_SENSOR_C_APPARENT_POWER",
72
+ "REXENSE_SENSOR_TOTAL_ACTIVE_POWER",
73
+ "REXENSE_SENSOR_TOTAL_APPARENT_POWER",
74
+ "REXENSE_SENSOR_CEI",
75
+ "REXENSE_SENSOR_CEE",
76
+ "REXENSE_SENSOR_A_CEI",
77
+ "REXENSE_SENSOR_A_CEE",
78
+ "REXENSE_SENSOR_B_CEI",
79
+ "REXENSE_SENSOR_B_CEE",
80
+ "REXENSE_SENSOR_C_CEI",
81
+ "REXENSE_SENSOR_C_CEE",
82
+ "REXENSE_SENSOR_TEMPERATURE",
83
+ "REXENSE_SENSOR_BATTERY_PERCENTAGE",
84
+ "REXENSE_SENSOR_BATTERY_VOLTAGE",
85
+ "REXENSE_SWITCH_ONOFF",
86
+ ]
aiorexense/api.py ADDED
@@ -0,0 +1,61 @@
1
+ """
2
+ HTTP API for Rexense device basic info
3
+ """
4
+ import asyncio
5
+ import logging
6
+ from typing import Any, Tuple
7
+
8
+ from aiohttp import ClientSession, ClientError
9
+
10
+ from .const import (
11
+ API_VERSION,
12
+ VENDOR_CODE,
13
+ FUNCTION_GET_BASIC_INFO,
14
+ )
15
+
16
+ _LOGGER = logging.getLogger(__name__)
17
+
18
+
19
+ async def get_basic_info(
20
+ host: str,
21
+ port: int,
22
+ session: ClientSession,
23
+ timeout: int = 5,
24
+ ) -> Tuple[str, str, str, list[dict[str, Any]]]:
25
+ """
26
+ Send an HTTP request to the device, query device_id, model, sw_build_id, feature_map。
27
+ """
28
+ url = f"http://{host}:{port}/rex/device/v1/operate"
29
+ payload = {
30
+ "Version": API_VERSION,
31
+ "VendorCode": VENDOR_CODE,
32
+ "Timestamp": "0",
33
+ "Seq": "0",
34
+ "DeviceId": "",
35
+ "FunctionCode": FUNCTION_GET_BASIC_INFO,
36
+ "Payload": {},
37
+ }
38
+ try:
39
+ async with asyncio.timeout(timeout):
40
+ resp = await session.get(url, json=payload)
41
+ except (asyncio.TimeoutError, ClientError) as err:
42
+ _LOGGER.error("HTTP get_basic_info failed: %s", err)
43
+ raise
44
+
45
+ if resp.status != 200:
46
+ _LOGGER.error("Device %s:%s HTTP status %s", host, port, resp.status)
47
+ raise ClientError(f"Status {resp.status}")
48
+
49
+ data = await resp.json()
50
+
51
+ if data.get("FunctionCode") != FUNCTION_GET_BASIC_INFO.replace("GetBasic", "ReportBasic") or not data.get("Payload"):
52
+ _LOGGER.error("Invalid response format: %s", data)
53
+ raise ClientError("Invalid response format")
54
+
55
+ device_id = data.get("DeviceId", "")
56
+ payload = data["Payload"]
57
+ model = payload.get("ModelId", "")
58
+ sw_build_id = payload.get("SwBuildId", "")
59
+ feature_map = payload.get("FeatureMap", [])
60
+
61
+ return device_id, model, sw_build_id, feature_map
aiorexense/const.py ADDED
@@ -0,0 +1,42 @@
1
+ """
2
+ Rexense const & sensor config
3
+ """
4
+
5
+ DEFAULT_PORT = 80
6
+
7
+ API_VERSION = "1.0"
8
+ VENDOR_CODE = "Rexense"
9
+ FUNCTION_GET_BASIC_INFO = "GetBasicInfo"
10
+ FUNCTION_NOTIFY_STATUS = "NotifyStatus"
11
+ FUNCTION_INVOKE_CMD = "InvokeCmd"
12
+
13
+ REXENSE_SENSOR_CURRENT = {"name":"Current","unit":"A"}
14
+ REXENSE_SENSOR_VOLTAGE = {"name":"Voltage","unit":"V"}
15
+ REXENSE_SENSOR_POWER_FACTOR = {"name":"PowerFactor","unit":"%"}
16
+ REXENSE_SENSOR_ACTIVE_POWER = {"name":"ActivePower","unit":"W"}
17
+ REXENSE_SENSOR_APPARENT_POWER = {"name":"AprtPower","unit":"VA"}
18
+ REXENSE_SENSOR_B_CURRENT = {"name":"B_Current","unit":"A"}
19
+ REXENSE_SENSOR_B_VOLTAGE = {"name":"B_Voltage","unit":"V"}
20
+ REXENSE_SENSOR_B_POWER_FACTOR = {"name":"B_PowerFactor","unit":"%"}
21
+ REXENSE_SENSOR_B_ACTIVE_POWER = {"name":"B_ActivePower","unit":"W"}
22
+ REXENSE_SENSOR_B_APPARENT_POWER = {"name":"B_AprtPower","unit":"VA"}
23
+ REXENSE_SENSOR_C_CURRENT = {"name":"C_Current","unit":"A"}
24
+ REXENSE_SENSOR_C_VOLTAGE = {"name":"C_Voltage","unit":"V"}
25
+ REXENSE_SENSOR_C_POWER_FACTOR = {"name":"C_PowerFactor","unit":"%"}
26
+ REXENSE_SENSOR_C_ACTIVE_POWER = {"name":"C_ActivePower","unit":"W"}
27
+ REXENSE_SENSOR_C_APPARENT_POWER = {"name":"C_AprtPower","unit":"VA"}
28
+ REXENSE_SENSOR_TOTAL_ACTIVE_POWER = {"name":"TotalActivePower","unit":"W"}
29
+ REXENSE_SENSOR_TOTAL_APPARENT_POWER = {"name":"TotalAprtPower","unit":"VA"}
30
+ REXENSE_SENSOR_CEI = {"name":"CEI","unit":"Wh"}
31
+ REXENSE_SENSOR_CEE = {"name":"CEE","unit":"Wh"}
32
+ REXENSE_SENSOR_A_CEI = {"name":"A_CEI","unit":"Wh"}
33
+ REXENSE_SENSOR_A_CEE = {"name":"A_CEE","unit":"Wh"}
34
+ REXENSE_SENSOR_B_CEI = {"name":"B_CEI","unit":"Wh"}
35
+ REXENSE_SENSOR_B_CEE = {"name":"B_CEE","unit":"Wh"}
36
+ REXENSE_SENSOR_C_CEI = {"name":"C_CEI","unit":"Wh"}
37
+ REXENSE_SENSOR_C_CEE = {"name":"C_CEE","unit":"Wh"}
38
+ REXENSE_SENSOR_TEMPERATURE = {"name":"Temperature","unit":"°C"}
39
+ REXENSE_SENSOR_BATTERY_PERCENTAGE = {"name":"BatteryPercentage","unit":"%"}
40
+ REXENSE_SENSOR_BATTERY_VOLTAGE = {"name":"BatteryVoltage","unit":"V"}
41
+
42
+ REXENSE_SWITCH_ONOFF = {"name":"PowerSwitch","unit":""}
@@ -0,0 +1,197 @@
1
+ """
2
+ WebSocket client for Rexense devices, independent of Home Assistant.
3
+ """
4
+ import asyncio
5
+ import logging
6
+ from typing import Any, Callable, Optional
7
+
8
+ from aiohttp import ClientSession, ClientWebSocketResponse, ClientWSTimeout, WSMsgType
9
+ from .const import REXENSE_SWITCH_ONOFF
10
+
11
+ _LOGGER = logging.getLogger(__name__)
12
+
13
+
14
+ class RexenseWebsocketClient:
15
+ """
16
+ Manages WebSocket connection to a Rexense device.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ device_id: str,
22
+ model: str,
23
+ url: str,
24
+ sw_build_id: str,
25
+ feature_map: list[dict[str, Any]],
26
+ session: Optional[ClientSession] = None,
27
+ on_update: Optional[Callable[[], None]] = None,
28
+ ) -> None:
29
+ """Initialize the WebSocket client."""
30
+ self.device_id = device_id
31
+ self.model = model
32
+ self.sw_build_id = sw_build_id
33
+ self.feature_map = feature_map
34
+ self.url = url
35
+ self.ws: Optional[ClientWebSocketResponse] = None
36
+ self.connected: bool = False
37
+ self._running: bool = False
38
+ self._task: Optional[asyncio.Task] = None
39
+ self.last_values: dict[str, Any] = {}
40
+ self.switch_state: Optional[bool] = None
41
+ self.ping_interval: int = 30
42
+
43
+ self.on_update = on_update
44
+ self._session = session
45
+ self.signal_update = f"{device_id}_update"
46
+
47
+ async def connect(self) -> None:
48
+ """Connect to the device and start listening."""
49
+ if self._running:
50
+ return
51
+ # Prepare session
52
+ if self._session is None:
53
+ self._session = ClientSession()
54
+
55
+ _LOGGER.debug("Attempting WebSocket connection to %s", self.url)
56
+ try:
57
+ ws = await self._session.ws_connect(
58
+ self.url,
59
+ timeout=ClientWSTimeout(ws_close=10),
60
+ heartbeat=self.ping_interval,
61
+ autoping=True,
62
+ )
63
+ except Exception as err:
64
+ _LOGGER.error(
65
+ "Initial WebSocket connect failed for %s: %s", self.device_id, err
66
+ )
67
+ self._running = False
68
+ raise
69
+ else:
70
+ self._running = True
71
+ self.ws = ws
72
+ self.connected = True
73
+ _LOGGER.info("WebSocket connected to device %s", self.device_id)
74
+ self._task = asyncio.create_task(self._run_loop())
75
+
76
+ async def _run_loop(self) -> None:
77
+ """Run the WebSocket listen and auto-reconnect loop."""
78
+ first_try = True
79
+ while self._running:
80
+ try:
81
+ if not first_try:
82
+ _LOGGER.info("Reconnecting to device %s", self.device_id)
83
+ ws = await self._session.ws_connect(
84
+ self.url,
85
+ timeout=ClientWSTimeout(ws_close=10),
86
+ heartbeat=self.ping_interval,
87
+ autoping=True,
88
+ )
89
+ self.ws = ws
90
+ self.connected = True
91
+ _LOGGER.info("WebSocket reconnected to device %s", self.device_id)
92
+ else:
93
+ first_try = False
94
+
95
+ assert self.ws is not None
96
+ async for msg in self.ws:
97
+ if msg.type == WSMsgType.TEXT:
98
+ try:
99
+ data = msg.json()
100
+ except ValueError as e:
101
+ _LOGGER.error("Received invalid JSON: %s, data: %s", e, msg.data)
102
+ continue
103
+ _LOGGER.debug("Received message: %s", data)
104
+ self._handle_message(data)
105
+ elif msg.type == WSMsgType.ERROR:
106
+ assert self.ws is not None
107
+ _LOGGER.error(
108
+ "WebSocket error for %s: %s",
109
+ self.device_id,
110
+ self.ws.exception(),
111
+ )
112
+ break
113
+ elif msg.type in (WSMsgType.CLOSED, WSMsgType.CLOSING):
114
+ _LOGGER.warning(
115
+ "WebSocket connection closed for %s", self.device_id
116
+ )
117
+ break
118
+ except Exception as err:
119
+ _LOGGER.error(
120
+ "WebSocket connection failed for %s: %s", self.device_id, err
121
+ )
122
+ # Clean up and maybe reconnect
123
+ self.connected = False
124
+ if self.ws is not None:
125
+ try:
126
+ await self.ws.close()
127
+ except Exception:
128
+ pass
129
+ finally:
130
+ self.ws = None
131
+
132
+ if self._running:
133
+ await asyncio.sleep(5)
134
+ continue
135
+
136
+ def _handle_message(self, data: dict[str, Any]) -> None:
137
+ """Process incoming message from WebSocket."""
138
+ func = (data.get("FunctionCode") or data.get("function") or data.get("func"))
139
+ if isinstance(func, str):
140
+ func = func.lower()
141
+
142
+ if func == "notifystatus":
143
+ payload = data.get("Payload") or {}
144
+ _LOGGER.debug("Received payload: %s", payload)
145
+ for k, v in payload.items():
146
+ key = k.replace("_1", "")
147
+ if key == REXENSE_SWITCH_ONOFF['name']:
148
+ self.switch_state = v not in ("0", False)
149
+ _LOGGER.debug("Update switch state: %s", self.switch_state)
150
+ else:
151
+ _LOGGER.debug("Update sensor %s: %s", key, v)
152
+ self.last_values[key] = v
153
+ # Trigger update callback
154
+ if self.on_update:
155
+ try:
156
+ self.on_update()
157
+ except Exception as e:
158
+ _LOGGER.error("Error in on_update callback: %s", e)
159
+ else:
160
+ _LOGGER.debug("Unhandled function %s: %s", func, data)
161
+
162
+ async def async_set_switch(self, on: bool) -> None:
163
+ """Send ON/OFF command to device via WebSocket."""
164
+ if not self.connected or self.ws is None:
165
+ raise RuntimeError("WebSocket is not connected.")
166
+ control = "On" if on else "Off"
167
+ payload = {
168
+ "FunctionCode": "InvokeCmd",
169
+ "Payload": {control: {}},
170
+ }
171
+ try:
172
+ await self.ws.send_json(payload)
173
+ except Exception as err:
174
+ _LOGGER.error("Failed to send switch command: %s", err)
175
+ raise
176
+
177
+ async def disconnect(self) -> None:
178
+ """Disconnect and stop the WebSocket client."""
179
+ _LOGGER.info("Disconnecting WebSocket for device %s", self.device_id)
180
+ self._running = False
181
+ if self.ws is not None:
182
+ await self.ws.close()
183
+ if self._task:
184
+ await self._task
185
+ self.ws = None
186
+ self.connected = False
187
+
188
+ async def disconnect(self) -> None:
189
+ """Disconnect and stop the WebSocket client."""
190
+ _LOGGER.info("Disconnecting WebSocket for device %s", self.device_id)
191
+ self._running = False
192
+ if self.ws is not None:
193
+ await self.ws.close()
194
+ if self._task:
195
+ await self._task
196
+ self.ws = None
197
+ self.connected = False
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiorexense
3
+ Version: 0.1.1
4
+ Summary: Rexense device client library: HTTP API + WebSocket
5
+ Home-page: https://github.com/RexenseIoT/aiorexense.git
6
+ Author: Zhejiang Rexense IoT Technology Co., Ltd
7
+ Author-email: RexenseIoT <yuxiaoqiang@rexense.com>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2025 Zhejiang Rexense IoT Technology Co., Ltd
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the “Software”), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ Project-URL: Homepage, https://github.com/RexenseIoT/aiorexense.git
31
+ Project-URL: Repository, https://github.com/RexenseIoT/aiorexense.git
32
+ Requires-Python: >=3.8
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: aiohttp>=3.8.0
36
+ Dynamic: author
37
+ Dynamic: home-page
38
+ Dynamic: license-file
39
+ Dynamic: requires-python
40
+
41
+ # aiorexense
42
+
43
+ A Rexense device client library featuring an HTTP API for retrieving basic information and WebSocket-based status push functionality.
44
+
45
+ ## Install
46
+ ```bash
47
+ pip install aiorexense
@@ -0,0 +1,9 @@
1
+ aiorexense/__init__.py,sha256=PSsMF6Ub1HJ5uyxbkTme2Cqt0UQe5e4fcxaA7lAZPL8,2486
2
+ aiorexense/api.py,sha256=h4ZcySrbgoul5gtd7k6FDF0Ph5S-SZFBd18h5rRnaKw,1817
3
+ aiorexense/const.py,sha256=fzU3gjbKKNR9zDlhmkA4Qua89NNkeIh5jWu1K8Mg3_s,2036
4
+ aiorexense/ws_client.py,sha256=IkZX6vd3HDTn0xFjVw_Q2lYJtfPtuLapjCgYzkpM6Cw,7453
5
+ aiorexense-0.1.1.dist-info/licenses/LICENSE,sha256=BAzGiyUG1qUIc93Ns3z14Ttl2wCHtUortRWIP_rU_Wg,1126
6
+ aiorexense-0.1.1.dist-info/METADATA,sha256=Zuo8HPKFgCRRDbUzPzgzuBBqX-z3ThS5WGeYEvPSrlo,2128
7
+ aiorexense-0.1.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
8
+ aiorexense-0.1.1.dist-info/top_level.txt,sha256=G9Xrl6LXJ6WXbprRvKbytk3-bbNQSwHoK0p0yYXBs5I,31
9
+ aiorexense-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (79.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Zhejiang Rexense IoT Technology Co., Ltd
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,4 @@
1
+ aiorexense
2
+ api
3
+ const
4
+ ws_client