aiohomematic 2025.8.6__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.
Potentially problematic release.
This version of aiohomematic might be problematic. Click here for more details.
- aiohomematic/__init__.py +47 -0
- aiohomematic/async_support.py +146 -0
- aiohomematic/caches/__init__.py +10 -0
- aiohomematic/caches/dynamic.py +554 -0
- aiohomematic/caches/persistent.py +459 -0
- aiohomematic/caches/visibility.py +774 -0
- aiohomematic/central/__init__.py +2034 -0
- aiohomematic/central/decorators.py +110 -0
- aiohomematic/central/xml_rpc_server.py +267 -0
- aiohomematic/client/__init__.py +1746 -0
- aiohomematic/client/json_rpc.py +1193 -0
- aiohomematic/client/xml_rpc.py +222 -0
- aiohomematic/const.py +795 -0
- aiohomematic/context.py +8 -0
- aiohomematic/converter.py +82 -0
- aiohomematic/decorators.py +188 -0
- aiohomematic/exceptions.py +145 -0
- aiohomematic/hmcli.py +159 -0
- aiohomematic/model/__init__.py +137 -0
- aiohomematic/model/calculated/__init__.py +65 -0
- aiohomematic/model/calculated/climate.py +230 -0
- aiohomematic/model/calculated/data_point.py +319 -0
- aiohomematic/model/calculated/operating_voltage_level.py +311 -0
- aiohomematic/model/calculated/support.py +174 -0
- aiohomematic/model/custom/__init__.py +175 -0
- aiohomematic/model/custom/climate.py +1334 -0
- aiohomematic/model/custom/const.py +146 -0
- aiohomematic/model/custom/cover.py +741 -0
- aiohomematic/model/custom/data_point.py +318 -0
- aiohomematic/model/custom/definition.py +861 -0
- aiohomematic/model/custom/light.py +1092 -0
- aiohomematic/model/custom/lock.py +389 -0
- aiohomematic/model/custom/siren.py +268 -0
- aiohomematic/model/custom/support.py +40 -0
- aiohomematic/model/custom/switch.py +172 -0
- aiohomematic/model/custom/valve.py +112 -0
- aiohomematic/model/data_point.py +1109 -0
- aiohomematic/model/decorators.py +173 -0
- aiohomematic/model/device.py +1347 -0
- aiohomematic/model/event.py +210 -0
- aiohomematic/model/generic/__init__.py +211 -0
- aiohomematic/model/generic/action.py +32 -0
- aiohomematic/model/generic/binary_sensor.py +28 -0
- aiohomematic/model/generic/button.py +25 -0
- aiohomematic/model/generic/data_point.py +162 -0
- aiohomematic/model/generic/number.py +73 -0
- aiohomematic/model/generic/select.py +36 -0
- aiohomematic/model/generic/sensor.py +72 -0
- aiohomematic/model/generic/switch.py +52 -0
- aiohomematic/model/generic/text.py +27 -0
- aiohomematic/model/hub/__init__.py +334 -0
- aiohomematic/model/hub/binary_sensor.py +22 -0
- aiohomematic/model/hub/button.py +26 -0
- aiohomematic/model/hub/data_point.py +332 -0
- aiohomematic/model/hub/number.py +37 -0
- aiohomematic/model/hub/select.py +47 -0
- aiohomematic/model/hub/sensor.py +35 -0
- aiohomematic/model/hub/switch.py +42 -0
- aiohomematic/model/hub/text.py +28 -0
- aiohomematic/model/support.py +599 -0
- aiohomematic/model/update.py +136 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +75 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
- aiohomematic/rega_scripts/set_program_state.fn +12 -0
- aiohomematic/rega_scripts/set_system_variable.fn +15 -0
- aiohomematic/support.py +482 -0
- aiohomematic/validator.py +65 -0
- aiohomematic-2025.8.6.dist-info/METADATA +69 -0
- aiohomematic-2025.8.6.dist-info/RECORD +77 -0
- aiohomematic-2025.8.6.dist-info/WHEEL +5 -0
- aiohomematic-2025.8.6.dist-info/licenses/LICENSE +21 -0
- aiohomematic-2025.8.6.dist-info/top_level.txt +2 -0
- aiohomematic_support/__init__.py +1 -0
- aiohomematic_support/client_local.py +349 -0
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Asynchronous JSON-RPC client for HomeMatic CCU-compatible backends.
|
|
3
|
+
|
|
4
|
+
Overview
|
|
5
|
+
--------
|
|
6
|
+
JsonRpcAioHttpClient wraps CCU JSON-RPC endpoints to provide:
|
|
7
|
+
- Login and session handling with automatic renewal
|
|
8
|
+
- Execution of ReGa scripts and JSON-RPC methods
|
|
9
|
+
- Access to system variables, programs, and device/channel metadata
|
|
10
|
+
- Reading/writing paramsets and values where supported
|
|
11
|
+
- Robust error handling, optional TLS, and rate-limiting via semaphores
|
|
12
|
+
|
|
13
|
+
Usage
|
|
14
|
+
-----
|
|
15
|
+
This client is usually managed by CentralUnit through ClientJsonCCU, but can be
|
|
16
|
+
used directly for advanced tasks. Typical flow:
|
|
17
|
+
|
|
18
|
+
client = JsonRpcAioHttpClient(username, password, device_url, connection_state, aiohttp_session, tls=True)
|
|
19
|
+
await client.get_system_information()
|
|
20
|
+
data = await client.get_all_device_data(interface)
|
|
21
|
+
|
|
22
|
+
Notes
|
|
23
|
+
-----
|
|
24
|
+
- Some JSON-RPC methods are backend/firmware dependent. The client detects and
|
|
25
|
+
caches supported methods at runtime.
|
|
26
|
+
- Binary/text encodings are handled carefully (UTF-8 / ISO-8859-1) for script IO.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from asyncio import Semaphore
|
|
33
|
+
from collections.abc import Mapping
|
|
34
|
+
from datetime import datetime
|
|
35
|
+
from enum import StrEnum
|
|
36
|
+
from functools import partial
|
|
37
|
+
from json import JSONDecodeError
|
|
38
|
+
import logging
|
|
39
|
+
import os
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from ssl import SSLContext
|
|
42
|
+
from typing import Any, Final
|
|
43
|
+
from urllib.parse import unquote
|
|
44
|
+
|
|
45
|
+
from aiohttp import (
|
|
46
|
+
ClientConnectorCertificateError,
|
|
47
|
+
ClientError,
|
|
48
|
+
ClientResponse,
|
|
49
|
+
ClientSession,
|
|
50
|
+
ClientTimeout,
|
|
51
|
+
TCPConnector,
|
|
52
|
+
)
|
|
53
|
+
import orjson
|
|
54
|
+
|
|
55
|
+
from aiohomematic import central as hmcu
|
|
56
|
+
from aiohomematic.async_support import Looper
|
|
57
|
+
from aiohomematic.const import (
|
|
58
|
+
ALWAYS_ENABLE_SYSVARS_BY_ID,
|
|
59
|
+
DEFAULT_INCLUDE_INTERNAL_PROGRAMS,
|
|
60
|
+
DEFAULT_INCLUDE_INTERNAL_SYSVARS,
|
|
61
|
+
ISO_8859_1,
|
|
62
|
+
JSON_SESSION_AGE,
|
|
63
|
+
MAX_CONCURRENT_HTTP_SESSIONS,
|
|
64
|
+
PATH_JSON_RPC,
|
|
65
|
+
REGA_SCRIPT_PATH,
|
|
66
|
+
RENAME_SYSVAR_BY_NAME,
|
|
67
|
+
TIMEOUT,
|
|
68
|
+
UTF_8,
|
|
69
|
+
DescriptionMarker,
|
|
70
|
+
DeviceDescription,
|
|
71
|
+
Interface,
|
|
72
|
+
ParameterData,
|
|
73
|
+
ParamsetKey,
|
|
74
|
+
ProgramData,
|
|
75
|
+
RegaScript,
|
|
76
|
+
SystemInformation,
|
|
77
|
+
SystemVariableData,
|
|
78
|
+
SysvarType,
|
|
79
|
+
)
|
|
80
|
+
from aiohomematic.exceptions import (
|
|
81
|
+
AuthFailure,
|
|
82
|
+
BaseHomematicException,
|
|
83
|
+
ClientException,
|
|
84
|
+
InternalBackendException,
|
|
85
|
+
NoConnectionException,
|
|
86
|
+
UnsupportedException,
|
|
87
|
+
)
|
|
88
|
+
from aiohomematic.model.support import convert_value
|
|
89
|
+
from aiohomematic.support import (
|
|
90
|
+
cleanup_text_from_html_tags,
|
|
91
|
+
element_matches_key,
|
|
92
|
+
extract_exc_args,
|
|
93
|
+
get_tls_context,
|
|
94
|
+
parse_sys_var,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _JsonKey(StrEnum):
|
|
101
|
+
"""Enum for homematic json keys."""
|
|
102
|
+
|
|
103
|
+
ADDRESS = "address"
|
|
104
|
+
CHANNEL_IDS = "channelIds"
|
|
105
|
+
DESCRIPTION = "description"
|
|
106
|
+
ERROR = "error"
|
|
107
|
+
ID = "id"
|
|
108
|
+
INTERFACE = "interface"
|
|
109
|
+
IS_ACTIVE = "isActive"
|
|
110
|
+
IS_INTERNAL = "isInternal"
|
|
111
|
+
LAST_EXECUTE_TIME = "lastExecuteTime"
|
|
112
|
+
MAX_VALUE = "maxValue"
|
|
113
|
+
MESSAGE = "message"
|
|
114
|
+
MIN_VALUE = "minValue"
|
|
115
|
+
NAME = "name"
|
|
116
|
+
PARAMSET_KEY = "paramsetKey"
|
|
117
|
+
PASSWORD = "password"
|
|
118
|
+
RESULT = "result"
|
|
119
|
+
SCRIPT = "script"
|
|
120
|
+
SERIAL = "serial"
|
|
121
|
+
SESSION_ID = "_session_id_"
|
|
122
|
+
SET = "set"
|
|
123
|
+
STATE = "state"
|
|
124
|
+
TYPE = "type"
|
|
125
|
+
UNIT = "unit"
|
|
126
|
+
USERNAME = "username"
|
|
127
|
+
VALUE = "value"
|
|
128
|
+
VALUE_KEY = "valueKey"
|
|
129
|
+
VALUE_LIST = "valueList"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class _JsonRpcMethod(StrEnum):
|
|
133
|
+
"""Enum for homematic json rpc methods types."""
|
|
134
|
+
|
|
135
|
+
CCU_GET_AUTH_ENABLED = "CCU.getAuthEnabled"
|
|
136
|
+
CCU_GET_HTTPS_REDIRECT_ENABLED = "CCU.getHttpsRedirectEnabled"
|
|
137
|
+
CHANNEL_HAS_PROGRAM_IDS = "Channel.hasProgramIds"
|
|
138
|
+
DEVICE_LIST_ALL_DETAIL = "Device.listAllDetail"
|
|
139
|
+
INTERFACE_GET_DEVICE_DESCRIPTION = "Interface.getDeviceDescription"
|
|
140
|
+
INTERFACE_GET_MASTER_VALUE = "Interface.getMasterValue"
|
|
141
|
+
INTERFACE_GET_PARAMSET = "Interface.getParamset"
|
|
142
|
+
INTERFACE_GET_PARAMSET_DESCRIPTION = "Interface.getParamsetDescription"
|
|
143
|
+
INTERFACE_GET_VALUE = "Interface.getValue"
|
|
144
|
+
INTERFACE_IS_PRESENT = "Interface.isPresent"
|
|
145
|
+
INTERFACE_LIST_DEVICES = "Interface.listDevices"
|
|
146
|
+
INTERFACE_LIST_INTERFACES = "Interface.listInterfaces"
|
|
147
|
+
INTERFACE_PUT_PARAMSET = "Interface.putParamset"
|
|
148
|
+
INTERFACE_SET_VALUE = "Interface.setValue"
|
|
149
|
+
PROGRAM_EXECUTE = "Program.execute"
|
|
150
|
+
PROGRAM_GET_ALL = "Program.getAll"
|
|
151
|
+
REGA_RUN_SCRIPT = "ReGa.runScript"
|
|
152
|
+
ROOM_GET_ALL = "Room.getAll"
|
|
153
|
+
SESSION_LOGIN = "Session.login"
|
|
154
|
+
SESSION_LOGOUT = "Session.logout"
|
|
155
|
+
SESSION_RENEW = "Session.renew"
|
|
156
|
+
SUBSECTION_GET_ALL = "Subsection.getAll"
|
|
157
|
+
SYSTEM_LIST_METHODS = "system.listMethods"
|
|
158
|
+
SYSVAR_DELETE_SYSVAR_BY_NAME = "SysVar.deleteSysVarByName"
|
|
159
|
+
SYSVAR_GET_ALL = "SysVar.getAll"
|
|
160
|
+
SYSVAR_GET_VALUE_BY_NAME = "SysVar.getValueByName"
|
|
161
|
+
SYSVAR_SET_BOOL = "SysVar.setBool"
|
|
162
|
+
SYSVAR_SET_FLOAT = "SysVar.setFloat"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
_PARALLEL_EXECUTION_LIMITED_JSONRPC_METHODS: Final = (
|
|
166
|
+
_JsonRpcMethod.INTERFACE_GET_DEVICE_DESCRIPTION,
|
|
167
|
+
_JsonRpcMethod.INTERFACE_GET_MASTER_VALUE,
|
|
168
|
+
_JsonRpcMethod.INTERFACE_GET_PARAMSET,
|
|
169
|
+
_JsonRpcMethod.INTERFACE_GET_PARAMSET_DESCRIPTION,
|
|
170
|
+
_JsonRpcMethod.INTERFACE_GET_VALUE,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class JsonRpcAioHttpClient:
|
|
175
|
+
"""Connection to CCU JSON-RPC Server."""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
username: str,
|
|
180
|
+
password: str,
|
|
181
|
+
device_url: str,
|
|
182
|
+
connection_state: hmcu.CentralConnectionState,
|
|
183
|
+
client_session: ClientSession | None,
|
|
184
|
+
tls: bool = False,
|
|
185
|
+
verify_tls: bool = False,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Session setup."""
|
|
188
|
+
self._client_session: Final = (
|
|
189
|
+
ClientSession(connector=TCPConnector(limit=MAX_CONCURRENT_HTTP_SESSIONS))
|
|
190
|
+
if client_session is None
|
|
191
|
+
else client_session
|
|
192
|
+
)
|
|
193
|
+
self._is_internal_session: Final = bool(client_session is None)
|
|
194
|
+
self._connection_state: Final = connection_state
|
|
195
|
+
self._username: Final = username
|
|
196
|
+
self._password: Final = password
|
|
197
|
+
self._looper = Looper()
|
|
198
|
+
self._tls: Final = tls
|
|
199
|
+
self._tls_context: Final[SSLContext | bool] = get_tls_context(verify_tls) if tls else False
|
|
200
|
+
self._url: Final = f"{device_url}{PATH_JSON_RPC}"
|
|
201
|
+
self._script_cache: Final[dict[str, str]] = {}
|
|
202
|
+
self._last_session_id_refresh: datetime | None = None
|
|
203
|
+
self._session_id: str | None = None
|
|
204
|
+
self._supported_methods: tuple[str, ...] | None = None
|
|
205
|
+
self._sema: Final = Semaphore(value=MAX_CONCURRENT_HTTP_SESSIONS)
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def is_activated(self) -> bool:
|
|
209
|
+
"""If session exists, then it is activated."""
|
|
210
|
+
return self._session_id is not None
|
|
211
|
+
|
|
212
|
+
async def _login_or_renew(self) -> bool:
|
|
213
|
+
"""Renew JSON-RPC session or perform login."""
|
|
214
|
+
if not self.is_activated:
|
|
215
|
+
self._session_id = await self._do_login()
|
|
216
|
+
self._last_session_id_refresh = datetime.now()
|
|
217
|
+
return self._session_id is not None
|
|
218
|
+
if self._session_id:
|
|
219
|
+
self._session_id = await self._do_renew_login(self._session_id)
|
|
220
|
+
return self._session_id is not None
|
|
221
|
+
|
|
222
|
+
async def _do_renew_login(self, session_id: str) -> str | None:
|
|
223
|
+
"""Renew JSON-RPC session or perform login."""
|
|
224
|
+
if self._has_session_recently_refreshed:
|
|
225
|
+
return session_id
|
|
226
|
+
method = _JsonRpcMethod.SESSION_RENEW
|
|
227
|
+
response = await self._do_post(
|
|
228
|
+
session_id=session_id,
|
|
229
|
+
method=method,
|
|
230
|
+
extra_params={_JsonKey.SESSION_ID: session_id},
|
|
231
|
+
)
|
|
232
|
+
if response[_JsonKey.RESULT] is True:
|
|
233
|
+
self._last_session_id_refresh = datetime.now()
|
|
234
|
+
_LOGGER.debug("DO_RENEW_LOGIN: method: %s [%s]", method, session_id)
|
|
235
|
+
return session_id
|
|
236
|
+
|
|
237
|
+
return await self._do_login()
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def _has_session_recently_refreshed(self) -> bool:
|
|
241
|
+
"""Check if session id has been modified within 90 seconds."""
|
|
242
|
+
if self._last_session_id_refresh is None:
|
|
243
|
+
return False
|
|
244
|
+
delta = datetime.now() - self._last_session_id_refresh
|
|
245
|
+
return delta.seconds < JSON_SESSION_AGE
|
|
246
|
+
|
|
247
|
+
async def _do_login(self) -> str | None:
|
|
248
|
+
"""Login to CCU and return session."""
|
|
249
|
+
if not self._has_credentials:
|
|
250
|
+
_LOGGER.warning("DO_LOGIN failed: No credentials set")
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
session_id: str | None = None
|
|
254
|
+
|
|
255
|
+
params = {
|
|
256
|
+
_JsonKey.USERNAME: self._username,
|
|
257
|
+
_JsonKey.PASSWORD: self._password,
|
|
258
|
+
}
|
|
259
|
+
method = _JsonRpcMethod.SESSION_LOGIN
|
|
260
|
+
response = await self._do_post(
|
|
261
|
+
session_id=False,
|
|
262
|
+
method=method,
|
|
263
|
+
extra_params=params,
|
|
264
|
+
use_default_params=False,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if result := response[_JsonKey.RESULT]:
|
|
268
|
+
session_id = result
|
|
269
|
+
|
|
270
|
+
_LOGGER.debug("DO_LOGIN: method: %s [%s]", method, session_id)
|
|
271
|
+
|
|
272
|
+
return session_id
|
|
273
|
+
|
|
274
|
+
async def _post(
|
|
275
|
+
self,
|
|
276
|
+
method: _JsonRpcMethod,
|
|
277
|
+
extra_params: dict[_JsonKey, Any] | None = None,
|
|
278
|
+
use_default_params: bool = True,
|
|
279
|
+
keep_session: bool = True,
|
|
280
|
+
) -> dict[str, Any] | Any:
|
|
281
|
+
"""Reusable JSON-RPC POST function."""
|
|
282
|
+
if keep_session:
|
|
283
|
+
await self._login_or_renew()
|
|
284
|
+
session_id = self._session_id
|
|
285
|
+
else:
|
|
286
|
+
session_id = await self._do_login()
|
|
287
|
+
|
|
288
|
+
if not session_id:
|
|
289
|
+
raise ClientException("Error while logging in")
|
|
290
|
+
|
|
291
|
+
if self._supported_methods is None:
|
|
292
|
+
await self._check_supported_methods()
|
|
293
|
+
|
|
294
|
+
response = await self._do_post(
|
|
295
|
+
session_id=session_id,
|
|
296
|
+
method=method,
|
|
297
|
+
extra_params=extra_params,
|
|
298
|
+
use_default_params=use_default_params,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if extra_params:
|
|
302
|
+
_LOGGER.debug("POST method: %s [%s]", method, extra_params)
|
|
303
|
+
else:
|
|
304
|
+
_LOGGER.debug("POST method: %s", method)
|
|
305
|
+
|
|
306
|
+
if not keep_session:
|
|
307
|
+
await self._do_logout(session_id=session_id)
|
|
308
|
+
|
|
309
|
+
return response
|
|
310
|
+
|
|
311
|
+
async def _post_script(
|
|
312
|
+
self,
|
|
313
|
+
script_name: str,
|
|
314
|
+
extra_params: dict[_JsonKey, Any] | None = None,
|
|
315
|
+
keep_session: bool = True,
|
|
316
|
+
) -> dict[str, Any] | Any:
|
|
317
|
+
"""Reusable JSON-RPC POST_SCRIPT function."""
|
|
318
|
+
if keep_session:
|
|
319
|
+
await self._login_or_renew()
|
|
320
|
+
session_id = self._session_id
|
|
321
|
+
else:
|
|
322
|
+
session_id = await self._do_login()
|
|
323
|
+
|
|
324
|
+
if not session_id:
|
|
325
|
+
raise ClientException("Error while logging in")
|
|
326
|
+
|
|
327
|
+
if self._supported_methods is None:
|
|
328
|
+
await self._check_supported_methods()
|
|
329
|
+
|
|
330
|
+
if (script := await self._get_script(script_name=script_name)) is None:
|
|
331
|
+
raise ClientException(f"Script file for {script_name} does not exist")
|
|
332
|
+
|
|
333
|
+
if extra_params:
|
|
334
|
+
for variable, value in extra_params.items():
|
|
335
|
+
script = script.replace(f"##{variable}##", value)
|
|
336
|
+
|
|
337
|
+
method = _JsonRpcMethod.REGA_RUN_SCRIPT
|
|
338
|
+
response = await self._do_post(
|
|
339
|
+
session_id=session_id,
|
|
340
|
+
method=method,
|
|
341
|
+
extra_params={_JsonKey.SCRIPT: script},
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
_LOGGER.debug("POST_SCRIPT: method: %s [%s]", method, script_name)
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
if not response[_JsonKey.ERROR]:
|
|
348
|
+
response[_JsonKey.RESULT] = orjson.loads(response[_JsonKey.RESULT])
|
|
349
|
+
finally:
|
|
350
|
+
if not keep_session:
|
|
351
|
+
await self._do_logout(session_id=session_id)
|
|
352
|
+
|
|
353
|
+
return response
|
|
354
|
+
|
|
355
|
+
async def _get_script(self, script_name: str) -> str | None:
|
|
356
|
+
"""Return a script from the script cache. Load if required."""
|
|
357
|
+
if script_name in self._script_cache:
|
|
358
|
+
return self._script_cache[script_name]
|
|
359
|
+
|
|
360
|
+
def _load_script(script_name: str) -> str | None:
|
|
361
|
+
"""Load script from file system."""
|
|
362
|
+
script_file = os.path.join(Path(__file__).resolve().parent, REGA_SCRIPT_PATH, script_name)
|
|
363
|
+
if script := Path(script_file).read_text(encoding=UTF_8):
|
|
364
|
+
self._script_cache[script_name] = script
|
|
365
|
+
return script
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
return await self._looper.async_add_executor_job(_load_script, script_name, name=f"load_script-{script_name}")
|
|
369
|
+
|
|
370
|
+
async def _do_post(
|
|
371
|
+
self,
|
|
372
|
+
session_id: bool | str,
|
|
373
|
+
method: _JsonRpcMethod,
|
|
374
|
+
extra_params: dict[_JsonKey, Any] | None = None,
|
|
375
|
+
use_default_params: bool = True,
|
|
376
|
+
) -> dict[str, Any] | Any:
|
|
377
|
+
"""Reusable JSON-RPC POST function."""
|
|
378
|
+
if not self._client_session:
|
|
379
|
+
raise ClientException("ClientSession not initialized")
|
|
380
|
+
if not self._has_credentials:
|
|
381
|
+
raise ClientException("No credentials set")
|
|
382
|
+
if self._supported_methods and method not in self._supported_methods:
|
|
383
|
+
raise UnsupportedException(f"POST: method '{method} not supported by backend.")
|
|
384
|
+
|
|
385
|
+
params = _get_params(session_id=session_id, extra_params=extra_params, use_default_params=use_default_params)
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
payload = orjson.dumps({"method": method, "params": params, "jsonrpc": "1.1", "id": 0})
|
|
389
|
+
|
|
390
|
+
headers = {
|
|
391
|
+
"Content-Type": "application/json",
|
|
392
|
+
"Content-Length": str(len(payload)),
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
post_call = partial(
|
|
396
|
+
self._client_session.post,
|
|
397
|
+
url=self._url,
|
|
398
|
+
data=payload,
|
|
399
|
+
headers=headers,
|
|
400
|
+
timeout=ClientTimeout(total=TIMEOUT),
|
|
401
|
+
ssl=self._tls_context,
|
|
402
|
+
)
|
|
403
|
+
if method in _PARALLEL_EXECUTION_LIMITED_JSONRPC_METHODS:
|
|
404
|
+
async with self._sema:
|
|
405
|
+
if (response := await post_call()) is None:
|
|
406
|
+
raise ClientException("POST method failed with no response")
|
|
407
|
+
elif (response := await post_call()) is None:
|
|
408
|
+
raise ClientException("POST method failed with no response")
|
|
409
|
+
|
|
410
|
+
if response.status == 200:
|
|
411
|
+
json_response = await self._get_json_reponse(response=response)
|
|
412
|
+
|
|
413
|
+
if error := json_response[_JsonKey.ERROR]:
|
|
414
|
+
error_message = error[_JsonKey.MESSAGE]
|
|
415
|
+
message = f"POST method '{method}' failed: {error_message}"
|
|
416
|
+
if error_message.startswith("access denied"):
|
|
417
|
+
_LOGGER.debug(message)
|
|
418
|
+
raise AuthFailure(message)
|
|
419
|
+
if "internal error" in error_message:
|
|
420
|
+
message = f"An internal error happened within your backend (Fix or ignore it): {message}"
|
|
421
|
+
_LOGGER.debug(message)
|
|
422
|
+
raise InternalBackendException(message)
|
|
423
|
+
_LOGGER.debug(message)
|
|
424
|
+
raise ClientException(message)
|
|
425
|
+
|
|
426
|
+
return json_response
|
|
427
|
+
|
|
428
|
+
message = f"Status: {response.status}"
|
|
429
|
+
json_response = await self._get_json_reponse(response=response)
|
|
430
|
+
if error := json_response[_JsonKey.ERROR]:
|
|
431
|
+
error_message = error[_JsonKey.MESSAGE]
|
|
432
|
+
message = f"{message}: {error_message}"
|
|
433
|
+
raise ClientException(message)
|
|
434
|
+
except BaseHomematicException:
|
|
435
|
+
if method in (_JsonRpcMethod.SESSION_LOGIN, _JsonRpcMethod.SESSION_LOGOUT, _JsonRpcMethod.SESSION_RENEW):
|
|
436
|
+
self.clear_session()
|
|
437
|
+
raise
|
|
438
|
+
except ClientConnectorCertificateError as cccerr:
|
|
439
|
+
self.clear_session()
|
|
440
|
+
message = f"ClientConnectorCertificateError[{cccerr}]"
|
|
441
|
+
if self._tls is False and cccerr.ssl is True:
|
|
442
|
+
message = (
|
|
443
|
+
f"{message}. Possible reason: 'Automatic forwarding to HTTPS' is enabled in backend, "
|
|
444
|
+
f"but this integration is not configured to use TLS"
|
|
445
|
+
)
|
|
446
|
+
raise ClientException(message) from cccerr
|
|
447
|
+
except (ClientError, OSError) as err:
|
|
448
|
+
self.clear_session()
|
|
449
|
+
raise NoConnectionException(err) from err
|
|
450
|
+
except (TypeError, Exception) as exc:
|
|
451
|
+
self.clear_session()
|
|
452
|
+
raise ClientException(exc) from exc
|
|
453
|
+
|
|
454
|
+
async def _get_json_reponse(self, response: ClientResponse) -> dict[str, Any] | Any:
|
|
455
|
+
"""Return the json object from response."""
|
|
456
|
+
try:
|
|
457
|
+
return await response.json(encoding=UTF_8)
|
|
458
|
+
except ValueError as verr:
|
|
459
|
+
_LOGGER.debug(
|
|
460
|
+
"DO_POST: ValueError [%s] Unable to parse JSON. Trying workaround",
|
|
461
|
+
extract_exc_args(exc=verr),
|
|
462
|
+
)
|
|
463
|
+
# Workaround for bug in CCU
|
|
464
|
+
return orjson.loads((await response.read()).decode(encoding=UTF_8))
|
|
465
|
+
|
|
466
|
+
async def logout(self) -> None:
|
|
467
|
+
"""Logout of CCU."""
|
|
468
|
+
try:
|
|
469
|
+
await self._looper.block_till_done()
|
|
470
|
+
await self._do_logout(self._session_id)
|
|
471
|
+
except BaseHomematicException:
|
|
472
|
+
_LOGGER.debug("LOGOUT: logout failed")
|
|
473
|
+
|
|
474
|
+
async def stop(self) -> None:
|
|
475
|
+
"""Stop the json rpc client."""
|
|
476
|
+
if self._is_internal_session:
|
|
477
|
+
await self._client_session.close()
|
|
478
|
+
|
|
479
|
+
async def _do_logout(self, session_id: str | None) -> None:
|
|
480
|
+
"""Logout of CCU."""
|
|
481
|
+
if not session_id:
|
|
482
|
+
_LOGGER.debug("DO_LOGOUT: Not logged in. Not logging out.")
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
method = _JsonRpcMethod.SESSION_LOGOUT
|
|
486
|
+
params = {_JsonKey.SESSION_ID: session_id}
|
|
487
|
+
try:
|
|
488
|
+
await self._do_post(
|
|
489
|
+
session_id=session_id,
|
|
490
|
+
method=method,
|
|
491
|
+
extra_params=params,
|
|
492
|
+
)
|
|
493
|
+
_LOGGER.debug("DO_LOGOUT: method: %s [%s]", method, session_id)
|
|
494
|
+
finally:
|
|
495
|
+
self.clear_session()
|
|
496
|
+
|
|
497
|
+
@property
|
|
498
|
+
def _has_credentials(self) -> bool:
|
|
499
|
+
"""Return if credentials are available."""
|
|
500
|
+
return self._username is not None and self._username != "" and self._password is not None
|
|
501
|
+
|
|
502
|
+
def clear_session(self) -> None:
|
|
503
|
+
"""Clear the current session."""
|
|
504
|
+
self._session_id = None
|
|
505
|
+
|
|
506
|
+
async def execute_program(self, pid: str) -> bool:
|
|
507
|
+
"""Execute a program on CCU / Homegear."""
|
|
508
|
+
params = {
|
|
509
|
+
_JsonKey.ID: pid,
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
response = await self._post(method=_JsonRpcMethod.PROGRAM_EXECUTE, extra_params=params)
|
|
513
|
+
_LOGGER.debug("EXECUTE_PROGRAM: Executing a program")
|
|
514
|
+
|
|
515
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
516
|
+
_LOGGER.debug(
|
|
517
|
+
"EXECUTE_PROGRAM: Result while executing program: %s",
|
|
518
|
+
str(json_result),
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
return True
|
|
522
|
+
|
|
523
|
+
async def set_program_state(self, pid: str, state: bool) -> bool:
|
|
524
|
+
"""Set the program state on CCU / Homegear."""
|
|
525
|
+
params = {
|
|
526
|
+
_JsonKey.ID: pid,
|
|
527
|
+
_JsonKey.STATE: "1" if state else "0",
|
|
528
|
+
}
|
|
529
|
+
response = await self._post_script(script_name=RegaScript.SET_PROGRAM_STATE, extra_params=params)
|
|
530
|
+
|
|
531
|
+
_LOGGER.debug("SET_PROGRAM_STATE: Setting program state: %s", state)
|
|
532
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
533
|
+
_LOGGER.debug(
|
|
534
|
+
"SET_PROGRAM_STATE: Result while setting program state: %s",
|
|
535
|
+
str(json_result),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
return True
|
|
539
|
+
|
|
540
|
+
async def set_system_variable(self, legacy_name: str, value: Any) -> bool:
|
|
541
|
+
"""Set a system variable on CCU / Homegear."""
|
|
542
|
+
params = {_JsonKey.NAME: legacy_name, _JsonKey.VALUE: value}
|
|
543
|
+
if isinstance(value, bool):
|
|
544
|
+
params[_JsonKey.VALUE] = int(value)
|
|
545
|
+
response = await self._post(method=_JsonRpcMethod.SYSVAR_SET_BOOL, extra_params=params)
|
|
546
|
+
elif isinstance(value, str):
|
|
547
|
+
if (clean_text := cleanup_text_from_html_tags(text=value)) != value:
|
|
548
|
+
params[_JsonKey.VALUE] = clean_text
|
|
549
|
+
_LOGGER.warning(
|
|
550
|
+
"SET_SYSTEM_VARIABLE: Value (%s) contains html tags. These are filtered out when writing.",
|
|
551
|
+
value,
|
|
552
|
+
)
|
|
553
|
+
response = await self._post_script(script_name=RegaScript.SET_SYSTEM_VARIABLE, extra_params=params)
|
|
554
|
+
else:
|
|
555
|
+
response = await self._post(method=_JsonRpcMethod.SYSVAR_SET_FLOAT, extra_params=params)
|
|
556
|
+
|
|
557
|
+
_LOGGER.debug("SET_SYSTEM_VARIABLE: Setting System variable")
|
|
558
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
559
|
+
_LOGGER.debug(
|
|
560
|
+
"SET_SYSTEM_VARIABLE: Result while setting variable: %s",
|
|
561
|
+
str(json_result),
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return True
|
|
565
|
+
|
|
566
|
+
async def delete_system_variable(self, name: str) -> bool:
|
|
567
|
+
"""Delete a system variable from CCU / Homegear."""
|
|
568
|
+
params = {_JsonKey.NAME: name}
|
|
569
|
+
response = await self._post(
|
|
570
|
+
method=_JsonRpcMethod.SYSVAR_DELETE_SYSVAR_BY_NAME,
|
|
571
|
+
extra_params=params,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
_LOGGER.debug("DELETE_SYSTEM_VARIABLE: Getting System variable")
|
|
575
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
576
|
+
deleted = json_result
|
|
577
|
+
_LOGGER.debug("DELETE_SYSTEM_VARIABLE: Deleted: %s", str(deleted))
|
|
578
|
+
|
|
579
|
+
return True
|
|
580
|
+
|
|
581
|
+
async def get_system_variable(self, name: str) -> Any:
|
|
582
|
+
"""Get single system variable from CCU / Homegear."""
|
|
583
|
+
params = {_JsonKey.NAME: name}
|
|
584
|
+
response = await self._post(
|
|
585
|
+
method=_JsonRpcMethod.SYSVAR_GET_VALUE_BY_NAME,
|
|
586
|
+
extra_params=params,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
_LOGGER.debug("GET_SYSTEM_VARIABLE: Getting System variable")
|
|
590
|
+
return response[_JsonKey.RESULT]
|
|
591
|
+
|
|
592
|
+
async def get_all_system_variables(
|
|
593
|
+
self, markers: tuple[DescriptionMarker | str, ...]
|
|
594
|
+
) -> tuple[SystemVariableData, ...]:
|
|
595
|
+
"""Get all system variables from CCU / Homegear."""
|
|
596
|
+
variables: list[SystemVariableData] = []
|
|
597
|
+
|
|
598
|
+
response = await self._post(
|
|
599
|
+
method=_JsonRpcMethod.SYSVAR_GET_ALL,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
_LOGGER.debug("GET_ALL_SYSTEM_VARIABLES: Getting all system variables")
|
|
603
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
604
|
+
descriptions = await self._get_system_variable_descriptions()
|
|
605
|
+
for var in json_result:
|
|
606
|
+
enabled_default = False
|
|
607
|
+
extended_sysvar = False
|
|
608
|
+
var_id = var[_JsonKey.ID]
|
|
609
|
+
legacy_name = var[_JsonKey.NAME]
|
|
610
|
+
is_internal = var[_JsonKey.IS_INTERNAL]
|
|
611
|
+
if new_name := RENAME_SYSVAR_BY_NAME.get(legacy_name):
|
|
612
|
+
legacy_name = new_name
|
|
613
|
+
if var_id in ALWAYS_ENABLE_SYSVARS_BY_ID:
|
|
614
|
+
enabled_default = True
|
|
615
|
+
|
|
616
|
+
if enabled_default is False and is_internal is True:
|
|
617
|
+
if var_id in ALWAYS_ENABLE_SYSVARS_BY_ID:
|
|
618
|
+
enabled_default = True
|
|
619
|
+
elif markers:
|
|
620
|
+
if DescriptionMarker.INTERNAL not in markers:
|
|
621
|
+
continue
|
|
622
|
+
enabled_default = True
|
|
623
|
+
elif DEFAULT_INCLUDE_INTERNAL_SYSVARS is False:
|
|
624
|
+
continue # type: ignore[unreachable]
|
|
625
|
+
|
|
626
|
+
description = descriptions.get(var_id)
|
|
627
|
+
if enabled_default is False and not is_internal and markers:
|
|
628
|
+
if not element_matches_key(
|
|
629
|
+
search_elements=markers,
|
|
630
|
+
compare_with=description,
|
|
631
|
+
ignore_case=False,
|
|
632
|
+
do_left_wildcard_search=True,
|
|
633
|
+
):
|
|
634
|
+
continue
|
|
635
|
+
enabled_default = True
|
|
636
|
+
|
|
637
|
+
org_data_type = var[_JsonKey.TYPE]
|
|
638
|
+
raw_value = var[_JsonKey.VALUE]
|
|
639
|
+
if org_data_type == SysvarType.NUMBER:
|
|
640
|
+
data_type = SysvarType.FLOAT if "." in raw_value else SysvarType.INTEGER
|
|
641
|
+
else:
|
|
642
|
+
data_type = org_data_type
|
|
643
|
+
|
|
644
|
+
if description:
|
|
645
|
+
extended_sysvar = DescriptionMarker.HAHM in description
|
|
646
|
+
# Remove default markers from description
|
|
647
|
+
for marker in DescriptionMarker:
|
|
648
|
+
description = description.replace(marker, "").strip()
|
|
649
|
+
unit = var[_JsonKey.UNIT]
|
|
650
|
+
values: tuple[str, ...] | None = None
|
|
651
|
+
if val_list := var.get(_JsonKey.VALUE_LIST):
|
|
652
|
+
values = tuple(val_list.split(";"))
|
|
653
|
+
try:
|
|
654
|
+
value = parse_sys_var(data_type=data_type, raw_value=raw_value)
|
|
655
|
+
max_value = None
|
|
656
|
+
if raw_max_value := var.get(_JsonKey.MAX_VALUE):
|
|
657
|
+
max_value = parse_sys_var(data_type=data_type, raw_value=raw_max_value)
|
|
658
|
+
min_value = None
|
|
659
|
+
if raw_min_value := var.get(_JsonKey.MIN_VALUE):
|
|
660
|
+
min_value = parse_sys_var(data_type=data_type, raw_value=raw_min_value)
|
|
661
|
+
variables.append(
|
|
662
|
+
SystemVariableData(
|
|
663
|
+
vid=var_id,
|
|
664
|
+
legacy_name=legacy_name,
|
|
665
|
+
data_type=data_type,
|
|
666
|
+
description=description,
|
|
667
|
+
unit=unit,
|
|
668
|
+
value=value,
|
|
669
|
+
values=values,
|
|
670
|
+
max_value=max_value,
|
|
671
|
+
min_value=min_value,
|
|
672
|
+
extended_sysvar=extended_sysvar,
|
|
673
|
+
enabled_default=enabled_default,
|
|
674
|
+
)
|
|
675
|
+
)
|
|
676
|
+
except (ValueError, TypeError) as vterr:
|
|
677
|
+
_LOGGER.warning(
|
|
678
|
+
"GET_ALL_SYSTEM_VARIABLES failed: %s [%s] Failed to parse SysVar %s ",
|
|
679
|
+
vterr.__class__.__name__,
|
|
680
|
+
extract_exc_args(exc=vterr),
|
|
681
|
+
legacy_name,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
return tuple(variables)
|
|
685
|
+
|
|
686
|
+
async def _get_program_descriptions(self) -> Mapping[str, str]:
|
|
687
|
+
"""Get all program descriptions from CCU via script."""
|
|
688
|
+
descriptions: dict[str, str] = {}
|
|
689
|
+
try:
|
|
690
|
+
response = await self._post_script(script_name=RegaScript.GET_PROGRAM_DESCRIPTIONS)
|
|
691
|
+
|
|
692
|
+
_LOGGER.debug("GET_PROGRAM_DESCRIPTIONS: Getting program descriptions")
|
|
693
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
694
|
+
for data in json_result:
|
|
695
|
+
descriptions[data[_JsonKey.ID]] = cleanup_text_from_html_tags(
|
|
696
|
+
text=unquote(string=data[_JsonKey.DESCRIPTION], encoding=ISO_8859_1)
|
|
697
|
+
)
|
|
698
|
+
except JSONDecodeError as jderr:
|
|
699
|
+
_LOGGER.error(
|
|
700
|
+
"GET_PROGRAM_DESCRIPTIONS failed: Unable to decode json: %s",
|
|
701
|
+
extract_exc_args(exc=jderr),
|
|
702
|
+
)
|
|
703
|
+
return descriptions
|
|
704
|
+
|
|
705
|
+
async def _get_system_variable_descriptions(self) -> Mapping[str, str]:
|
|
706
|
+
"""Get all system variable descriptions from CCU via script."""
|
|
707
|
+
descriptions: dict[str, str] = {}
|
|
708
|
+
try:
|
|
709
|
+
response = await self._post_script(script_name=RegaScript.GET_SYSTEM_VARIABLE_DESCRIPTIONS)
|
|
710
|
+
|
|
711
|
+
_LOGGER.debug("GET_SYSTEM_VARIABLE_DESCRIPTIONS: Getting system variable descriptions")
|
|
712
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
713
|
+
for data in json_result:
|
|
714
|
+
descriptions[data[_JsonKey.ID]] = cleanup_text_from_html_tags(
|
|
715
|
+
text=unquote(string=data[_JsonKey.DESCRIPTION], encoding=ISO_8859_1)
|
|
716
|
+
)
|
|
717
|
+
except JSONDecodeError as jderr:
|
|
718
|
+
_LOGGER.error(
|
|
719
|
+
"GET_SYSTEM_VARIABLE_DESCRIPTIONS failed: Unable to decode json: %s",
|
|
720
|
+
extract_exc_args(exc=jderr),
|
|
721
|
+
)
|
|
722
|
+
return descriptions
|
|
723
|
+
|
|
724
|
+
async def get_all_channel_ids_room(self) -> Mapping[str, set[str]]:
|
|
725
|
+
"""Get all channel_ids per room from CCU / Homegear."""
|
|
726
|
+
channel_ids_room: dict[str, set[str]] = {}
|
|
727
|
+
|
|
728
|
+
response = await self._post(
|
|
729
|
+
method=_JsonRpcMethod.ROOM_GET_ALL,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
_LOGGER.debug("GET_ALL_CHANNEL_IDS_PER_ROOM: Getting all rooms")
|
|
733
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
734
|
+
for room in json_result:
|
|
735
|
+
room_id = room[_JsonKey.ID]
|
|
736
|
+
room_name = room[_JsonKey.NAME]
|
|
737
|
+
if room_id not in channel_ids_room:
|
|
738
|
+
channel_ids_room[room_id] = set()
|
|
739
|
+
channel_ids_room[room_id].add(room_name)
|
|
740
|
+
for channel_id in room[_JsonKey.CHANNEL_IDS]:
|
|
741
|
+
if channel_id not in channel_ids_room:
|
|
742
|
+
channel_ids_room[channel_id] = set()
|
|
743
|
+
channel_ids_room[channel_id].add(room_name)
|
|
744
|
+
|
|
745
|
+
return channel_ids_room
|
|
746
|
+
|
|
747
|
+
async def get_all_channel_ids_function(self) -> Mapping[str, set[str]]:
|
|
748
|
+
"""Get all channel_ids per function from CCU / Homegear."""
|
|
749
|
+
channel_ids_function: dict[str, set[str]] = {}
|
|
750
|
+
|
|
751
|
+
response = await self._post(
|
|
752
|
+
method=_JsonRpcMethod.SUBSECTION_GET_ALL,
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
_LOGGER.debug("GET_ALL_CHANNEL_IDS_PER_FUNCTION: Getting all functions")
|
|
756
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
757
|
+
for function in json_result:
|
|
758
|
+
function_id = function[_JsonKey.ID]
|
|
759
|
+
function_name = function[_JsonKey.NAME]
|
|
760
|
+
if function_id not in channel_ids_function:
|
|
761
|
+
channel_ids_function[function_id] = set()
|
|
762
|
+
channel_ids_function[function_id].add(function_name)
|
|
763
|
+
for channel_id in function[_JsonKey.CHANNEL_IDS]:
|
|
764
|
+
if channel_id not in channel_ids_function:
|
|
765
|
+
channel_ids_function[channel_id] = set()
|
|
766
|
+
channel_ids_function[channel_id].add(function_name)
|
|
767
|
+
|
|
768
|
+
return channel_ids_function
|
|
769
|
+
|
|
770
|
+
async def get_device_description(self, interface: Interface, address: str) -> DeviceDescription | None:
|
|
771
|
+
"""Get device descriptions from CCU."""
|
|
772
|
+
device_description: DeviceDescription | None = None
|
|
773
|
+
params = {
|
|
774
|
+
_JsonKey.INTERFACE: interface,
|
|
775
|
+
_JsonKey.ADDRESS: address,
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
response = await self._post(method=_JsonRpcMethod.INTERFACE_GET_DEVICE_DESCRIPTION, extra_params=params)
|
|
779
|
+
|
|
780
|
+
_LOGGER.debug("GET_DEVICE_DESCRIPTION: Getting the device description")
|
|
781
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
782
|
+
device_description = self._convert_device_description(json_data=json_result)
|
|
783
|
+
|
|
784
|
+
return device_description
|
|
785
|
+
|
|
786
|
+
@staticmethod
|
|
787
|
+
def _convert_device_description(json_data: dict[str, Any]) -> DeviceDescription:
|
|
788
|
+
"""Convert json data dor device description."""
|
|
789
|
+
device_description = DeviceDescription(
|
|
790
|
+
TYPE=json_data["type"],
|
|
791
|
+
ADDRESS=json_data["address"],
|
|
792
|
+
PARAMSETS=json_data["paramsets"],
|
|
793
|
+
)
|
|
794
|
+
if available_firmware := json_data.get("availableFirmware"):
|
|
795
|
+
device_description["AVAILABLE_FIRMWARE"] = available_firmware
|
|
796
|
+
if children := json_data.get("children"):
|
|
797
|
+
device_description["CHILDREN"] = children
|
|
798
|
+
if firmware := json_data.get("firmware"):
|
|
799
|
+
device_description["FIRMWARE"] = firmware
|
|
800
|
+
if firmware_updatable := json_data.get("firmwareUpdatable"):
|
|
801
|
+
device_description["FIRMWARE_UPDATABLE"] = firmware_updatable
|
|
802
|
+
if firmware_update_state := json_data.get("firmwareUpdateState"):
|
|
803
|
+
device_description["FIRMWARE_UPDATE_STATE"] = firmware_update_state
|
|
804
|
+
if interface := json_data.get("interface"):
|
|
805
|
+
device_description["INTERFACE"] = interface
|
|
806
|
+
if parent := json_data.get("parent"):
|
|
807
|
+
device_description["PARENT"] = parent
|
|
808
|
+
if rx_mode := json_data.get("rxMode"):
|
|
809
|
+
device_description["RX_MODE"] = rx_mode
|
|
810
|
+
if subtype := json_data.get("subType"):
|
|
811
|
+
device_description["SUBTYPE"] = subtype
|
|
812
|
+
if updatable := json_data.get("updatable"):
|
|
813
|
+
device_description["UPDATABLE"] = updatable
|
|
814
|
+
return device_description
|
|
815
|
+
|
|
816
|
+
async def get_device_details(self) -> tuple[dict[str, Any], ...]:
|
|
817
|
+
"""Get the device details of the backend."""
|
|
818
|
+
device_details: tuple[dict[str, Any], ...] = ()
|
|
819
|
+
|
|
820
|
+
response = await self._post(
|
|
821
|
+
method=_JsonRpcMethod.DEVICE_LIST_ALL_DETAIL,
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
_LOGGER.debug("GET_DEVICE_DETAILS: Getting the device details")
|
|
825
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
826
|
+
device_details = tuple(json_result)
|
|
827
|
+
|
|
828
|
+
return device_details
|
|
829
|
+
|
|
830
|
+
async def get_paramset(
|
|
831
|
+
self, interface: Interface, address: str, paramset_key: ParamsetKey | str
|
|
832
|
+
) -> dict[str, Any] | None:
|
|
833
|
+
"""Get paramset from CCU."""
|
|
834
|
+
paramset: dict[str, Any] = {}
|
|
835
|
+
params = {
|
|
836
|
+
_JsonKey.INTERFACE: interface,
|
|
837
|
+
_JsonKey.ADDRESS: address,
|
|
838
|
+
_JsonKey.PARAMSET_KEY: paramset_key,
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
response = await self._post(
|
|
842
|
+
method=_JsonRpcMethod.INTERFACE_GET_PARAMSET,
|
|
843
|
+
extra_params=params,
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
_LOGGER.debug("GET_PARAMSET: Getting the paramset")
|
|
847
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
848
|
+
paramset = json_result
|
|
849
|
+
|
|
850
|
+
return paramset
|
|
851
|
+
|
|
852
|
+
async def put_paramset(
|
|
853
|
+
self,
|
|
854
|
+
interface: Interface,
|
|
855
|
+
address: str,
|
|
856
|
+
paramset_key: ParamsetKey | str,
|
|
857
|
+
values: list[dict[str, Any]],
|
|
858
|
+
) -> None:
|
|
859
|
+
"""Set paramset to CCU."""
|
|
860
|
+
params = {
|
|
861
|
+
_JsonKey.INTERFACE: interface,
|
|
862
|
+
_JsonKey.ADDRESS: address,
|
|
863
|
+
_JsonKey.PARAMSET_KEY: paramset_key,
|
|
864
|
+
_JsonKey.SET: values,
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
response = await self._post(
|
|
868
|
+
method=_JsonRpcMethod.INTERFACE_PUT_PARAMSET,
|
|
869
|
+
extra_params=params,
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
_LOGGER.debug("PUT_PARAMSET: Putting the paramset")
|
|
873
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
874
|
+
_LOGGER.debug(
|
|
875
|
+
"PUT_PARAMSET: Result while putting the paramset: %s",
|
|
876
|
+
str(json_result),
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
async def get_value(self, interface: Interface, address: str, paramset_key: ParamsetKey, parameter: str) -> Any:
|
|
880
|
+
"""Get value from CCU."""
|
|
881
|
+
value: Any = None
|
|
882
|
+
params = {
|
|
883
|
+
_JsonKey.INTERFACE: interface,
|
|
884
|
+
_JsonKey.ADDRESS: address,
|
|
885
|
+
_JsonKey.VALUE_KEY: parameter,
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
response = (
|
|
889
|
+
await self._post(method=_JsonRpcMethod.INTERFACE_GET_MASTER_VALUE, extra_params=params)
|
|
890
|
+
if paramset_key == ParamsetKey.MASTER
|
|
891
|
+
else await self._post(method=_JsonRpcMethod.INTERFACE_GET_VALUE, extra_params=params)
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
_LOGGER.debug("GET_VALUE: Getting the value")
|
|
895
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
896
|
+
value = json_result
|
|
897
|
+
|
|
898
|
+
return value
|
|
899
|
+
|
|
900
|
+
async def set_value(self, interface: Interface, address: str, parameter: str, value_type: str, value: Any) -> None:
|
|
901
|
+
"""Set value to CCU."""
|
|
902
|
+
params = {
|
|
903
|
+
_JsonKey.INTERFACE: interface,
|
|
904
|
+
_JsonKey.ADDRESS: address,
|
|
905
|
+
_JsonKey.VALUE_KEY: parameter,
|
|
906
|
+
_JsonKey.TYPE: value_type,
|
|
907
|
+
_JsonKey.VALUE: value,
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
response = await self._post(
|
|
911
|
+
method=_JsonRpcMethod.INTERFACE_SET_VALUE,
|
|
912
|
+
extra_params=params,
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
_LOGGER.debug("SET_VALUE: Setting the value")
|
|
916
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
917
|
+
_LOGGER.debug(
|
|
918
|
+
"SET_VALUE: Result while setting the value: %s",
|
|
919
|
+
str(json_result),
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
async def get_paramset_description(
|
|
923
|
+
self, interface: Interface, address: str, paramset_key: ParamsetKey
|
|
924
|
+
) -> Mapping[str, ParameterData] | None:
|
|
925
|
+
"""Get paramset description from CCU."""
|
|
926
|
+
paramset_description: dict[str, ParameterData] = {}
|
|
927
|
+
params = {
|
|
928
|
+
_JsonKey.INTERFACE: interface,
|
|
929
|
+
_JsonKey.ADDRESS: address,
|
|
930
|
+
_JsonKey.PARAMSET_KEY: paramset_key,
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
response = await self._post(method=_JsonRpcMethod.INTERFACE_GET_PARAMSET_DESCRIPTION, extra_params=params)
|
|
934
|
+
|
|
935
|
+
_LOGGER.debug("GET_PARAMSET_DESCRIPTIONS: Getting the paramset descriptions")
|
|
936
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
937
|
+
paramset_description = {data["NAME"]: self._convert_parameter_data(json_data=data) for data in json_result}
|
|
938
|
+
|
|
939
|
+
return paramset_description
|
|
940
|
+
|
|
941
|
+
@staticmethod
|
|
942
|
+
def _convert_parameter_data(json_data: dict[str, Any]) -> ParameterData:
|
|
943
|
+
"""Convert json data to parameter data."""
|
|
944
|
+
|
|
945
|
+
_type = json_data["TYPE"]
|
|
946
|
+
_value_list = json_data.get("VALUE_LIST", ())
|
|
947
|
+
|
|
948
|
+
parameter_data = ParameterData(
|
|
949
|
+
DEFAULT=convert_value(value=json_data["DEFAULT"], target_type=_type, value_list=_value_list),
|
|
950
|
+
FLAGS=int(json_data["FLAGS"]),
|
|
951
|
+
ID=json_data["ID"],
|
|
952
|
+
MAX=convert_value(value=json_data.get("MAX"), target_type=_type, value_list=_value_list),
|
|
953
|
+
MIN=convert_value(value=json_data.get("MIN"), target_type=_type, value_list=_value_list),
|
|
954
|
+
OPERATIONS=int(json_data["OPERATIONS"]),
|
|
955
|
+
TYPE=_type,
|
|
956
|
+
)
|
|
957
|
+
if special := json_data.get("SPECIAL"):
|
|
958
|
+
parameter_data["SPECIAL"] = special
|
|
959
|
+
if unit := json_data.get("UNIT"):
|
|
960
|
+
parameter_data["UNIT"] = str(unit)
|
|
961
|
+
if value_list := _value_list:
|
|
962
|
+
parameter_data["VALUE_LIST"] = value_list.split(" ")
|
|
963
|
+
|
|
964
|
+
return parameter_data
|
|
965
|
+
|
|
966
|
+
async def get_all_device_data(self, interface: Interface) -> Mapping[str, Any]:
|
|
967
|
+
"""Get the all device data of the backend."""
|
|
968
|
+
all_device_data: dict[str, dict[str, dict[str, Any]]] = {}
|
|
969
|
+
params = {
|
|
970
|
+
_JsonKey.INTERFACE: interface,
|
|
971
|
+
}
|
|
972
|
+
try:
|
|
973
|
+
response = await self._post_script(script_name=RegaScript.FETCH_ALL_DEVICE_DATA, extra_params=params)
|
|
974
|
+
|
|
975
|
+
_LOGGER.debug("GET_ALL_DEVICE_DATA: Getting all device data for interface %s", interface)
|
|
976
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
977
|
+
all_device_data = json_result
|
|
978
|
+
|
|
979
|
+
except JSONDecodeError as jderr:
|
|
980
|
+
raise ClientException(
|
|
981
|
+
f"GET_ALL_DEVICE_DATA failed: Unable to fetch device data for interface {interface}"
|
|
982
|
+
) from jderr
|
|
983
|
+
|
|
984
|
+
return all_device_data
|
|
985
|
+
|
|
986
|
+
async def get_all_programs(self, markers: tuple[DescriptionMarker | str, ...]) -> tuple[ProgramData, ...]:
|
|
987
|
+
"""Get the all programs of the backend."""
|
|
988
|
+
all_programs: list[ProgramData] = []
|
|
989
|
+
|
|
990
|
+
response = await self._post(
|
|
991
|
+
method=_JsonRpcMethod.PROGRAM_GET_ALL,
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
_LOGGER.debug("GET_ALL_PROGRAMS: Getting all programs")
|
|
995
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
996
|
+
descriptions = await self._get_program_descriptions()
|
|
997
|
+
for prog in json_result:
|
|
998
|
+
enabled_default = False
|
|
999
|
+
if (is_internal := prog[_JsonKey.IS_INTERNAL]) is True:
|
|
1000
|
+
if markers:
|
|
1001
|
+
if DescriptionMarker.INTERNAL not in markers:
|
|
1002
|
+
continue
|
|
1003
|
+
enabled_default = True
|
|
1004
|
+
elif DEFAULT_INCLUDE_INTERNAL_PROGRAMS is False:
|
|
1005
|
+
continue
|
|
1006
|
+
|
|
1007
|
+
pid = prog[_JsonKey.ID]
|
|
1008
|
+
description = descriptions.get(pid)
|
|
1009
|
+
if not is_internal and markers:
|
|
1010
|
+
if not element_matches_key(
|
|
1011
|
+
search_elements=markers,
|
|
1012
|
+
compare_with=description,
|
|
1013
|
+
ignore_case=False,
|
|
1014
|
+
do_left_wildcard_search=True,
|
|
1015
|
+
):
|
|
1016
|
+
continue
|
|
1017
|
+
enabled_default = True
|
|
1018
|
+
if description:
|
|
1019
|
+
# Remove default markers from description
|
|
1020
|
+
for marker in DescriptionMarker:
|
|
1021
|
+
description = description.replace(marker, "").strip()
|
|
1022
|
+
name = prog[_JsonKey.NAME]
|
|
1023
|
+
is_active = prog[_JsonKey.IS_ACTIVE]
|
|
1024
|
+
last_execute_time = prog[_JsonKey.LAST_EXECUTE_TIME]
|
|
1025
|
+
|
|
1026
|
+
all_programs.append(
|
|
1027
|
+
ProgramData(
|
|
1028
|
+
pid=pid,
|
|
1029
|
+
legacy_name=name,
|
|
1030
|
+
description=description,
|
|
1031
|
+
is_active=is_active,
|
|
1032
|
+
is_internal=is_internal,
|
|
1033
|
+
last_execute_time=last_execute_time,
|
|
1034
|
+
enabled_default=enabled_default,
|
|
1035
|
+
)
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
return tuple(all_programs)
|
|
1039
|
+
|
|
1040
|
+
async def is_present(self, interface: Interface) -> bool:
|
|
1041
|
+
"""Get value from CCU."""
|
|
1042
|
+
value: bool = False
|
|
1043
|
+
params = {_JsonKey.INTERFACE: interface}
|
|
1044
|
+
|
|
1045
|
+
response = await self._post(method=_JsonRpcMethod.INTERFACE_IS_PRESENT, extra_params=params)
|
|
1046
|
+
|
|
1047
|
+
_LOGGER.debug("IS_PRESENT: Getting the value")
|
|
1048
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
1049
|
+
value = bool(json_result)
|
|
1050
|
+
|
|
1051
|
+
return value
|
|
1052
|
+
|
|
1053
|
+
async def has_program_ids(self, channel_hmid: str) -> bool:
|
|
1054
|
+
"""Return if a channel has program ids."""
|
|
1055
|
+
params = {_JsonKey.ID: channel_hmid}
|
|
1056
|
+
response = await self._post(
|
|
1057
|
+
method=_JsonRpcMethod.CHANNEL_HAS_PROGRAM_IDS,
|
|
1058
|
+
extra_params=params,
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
_LOGGER.debug("HAS_PROGRAM_IDS: Checking if channel has program ids")
|
|
1062
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
1063
|
+
return bool(json_result)
|
|
1064
|
+
|
|
1065
|
+
return False
|
|
1066
|
+
|
|
1067
|
+
async def _get_supported_methods(self) -> tuple[str, ...]:
|
|
1068
|
+
"""Get the supported methods of the backend."""
|
|
1069
|
+
supported_methods: tuple[str, ...] = ()
|
|
1070
|
+
|
|
1071
|
+
await self._login_or_renew()
|
|
1072
|
+
if not (session_id := self._session_id):
|
|
1073
|
+
raise ClientException("Error while logging in")
|
|
1074
|
+
|
|
1075
|
+
try:
|
|
1076
|
+
response = await self._do_post(
|
|
1077
|
+
session_id=session_id,
|
|
1078
|
+
method=_JsonRpcMethod.SYSTEM_LIST_METHODS,
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
_LOGGER.debug("GET_SUPPORTED_METHODS: Getting the supported methods")
|
|
1082
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
1083
|
+
supported_methods = tuple(method_description[_JsonKey.NAME] for method_description in json_result)
|
|
1084
|
+
except BaseHomematicException:
|
|
1085
|
+
return ()
|
|
1086
|
+
|
|
1087
|
+
return supported_methods
|
|
1088
|
+
|
|
1089
|
+
async def _check_supported_methods(self) -> bool:
|
|
1090
|
+
"""Check, if all required api methods are supported by backend."""
|
|
1091
|
+
if self._supported_methods is None:
|
|
1092
|
+
self._supported_methods = await self._get_supported_methods()
|
|
1093
|
+
if unsupport_methods := tuple(method for method in _JsonRpcMethod if method not in self._supported_methods):
|
|
1094
|
+
_LOGGER.warning(
|
|
1095
|
+
"CHECK_SUPPORTED_METHODS: methods not supported by backend: %s",
|
|
1096
|
+
", ".join(unsupport_methods),
|
|
1097
|
+
)
|
|
1098
|
+
return False
|
|
1099
|
+
return True
|
|
1100
|
+
|
|
1101
|
+
async def get_system_information(self) -> SystemInformation:
|
|
1102
|
+
"""Get system information of the backend."""
|
|
1103
|
+
|
|
1104
|
+
if (auth_enabled := await self._get_auth_enabled()) is not None and (
|
|
1105
|
+
system_information := SystemInformation(
|
|
1106
|
+
auth_enabled=auth_enabled,
|
|
1107
|
+
available_interfaces=await self._list_interfaces(),
|
|
1108
|
+
https_redirect_enabled=await self._get_https_redirect_enabled(),
|
|
1109
|
+
serial=await self._get_serial(),
|
|
1110
|
+
)
|
|
1111
|
+
):
|
|
1112
|
+
return system_information
|
|
1113
|
+
|
|
1114
|
+
return SystemInformation(auth_enabled=True)
|
|
1115
|
+
|
|
1116
|
+
async def _get_auth_enabled(self) -> bool:
|
|
1117
|
+
"""Get the auth_enabled flag of the backend."""
|
|
1118
|
+
_LOGGER.debug("GET_AUTH_ENABLED: Getting the flag auth_enabled")
|
|
1119
|
+
try:
|
|
1120
|
+
response = await self._post(method=_JsonRpcMethod.CCU_GET_AUTH_ENABLED)
|
|
1121
|
+
if (json_result := response[_JsonKey.RESULT]) is not None:
|
|
1122
|
+
return bool(json_result)
|
|
1123
|
+
except InternalBackendException:
|
|
1124
|
+
return True
|
|
1125
|
+
|
|
1126
|
+
return True
|
|
1127
|
+
|
|
1128
|
+
async def list_devices(self, interface: Interface) -> tuple[DeviceDescription, ...]:
|
|
1129
|
+
"""List devices from CCU / Homegear."""
|
|
1130
|
+
devices: tuple[DeviceDescription, ...] = ()
|
|
1131
|
+
_LOGGER.debug("LIST_DEVICES: Getting all available interfaces")
|
|
1132
|
+
params = {
|
|
1133
|
+
_JsonKey.INTERFACE: interface,
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
response = await self._post(
|
|
1137
|
+
method=_JsonRpcMethod.INTERFACE_LIST_DEVICES,
|
|
1138
|
+
extra_params=params,
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
1142
|
+
devices = tuple(self._convert_device_description(json_data=data) for data in json_result)
|
|
1143
|
+
|
|
1144
|
+
return devices
|
|
1145
|
+
|
|
1146
|
+
async def _list_interfaces(self) -> tuple[str, ...]:
|
|
1147
|
+
"""List all available interfaces from CCU / Homegear."""
|
|
1148
|
+
_LOGGER.debug("LIST_INTERFACES: Getting all available interfaces")
|
|
1149
|
+
|
|
1150
|
+
response = await self._post(
|
|
1151
|
+
method=_JsonRpcMethod.INTERFACE_LIST_INTERFACES,
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
1155
|
+
return tuple(interface[_JsonKey.NAME] for interface in json_result)
|
|
1156
|
+
return ()
|
|
1157
|
+
|
|
1158
|
+
async def _get_https_redirect_enabled(self) -> bool | None:
|
|
1159
|
+
"""Get the auth_enabled flag of the backend."""
|
|
1160
|
+
_LOGGER.debug("GET_HTTPS_REDIRECT_ENABLED: Getting the flag https_redirect_enabled")
|
|
1161
|
+
|
|
1162
|
+
response = await self._post(method=_JsonRpcMethod.CCU_GET_HTTPS_REDIRECT_ENABLED)
|
|
1163
|
+
if (json_result := response[_JsonKey.RESULT]) is not None:
|
|
1164
|
+
return bool(json_result)
|
|
1165
|
+
return None
|
|
1166
|
+
|
|
1167
|
+
async def _get_serial(self) -> str | None:
|
|
1168
|
+
"""Get the serial of the backend."""
|
|
1169
|
+
_LOGGER.debug("GET_SERIAL: Getting the backend serial")
|
|
1170
|
+
try:
|
|
1171
|
+
response = await self._post_script(script_name=RegaScript.GET_SERIAL)
|
|
1172
|
+
|
|
1173
|
+
if json_result := response[_JsonKey.RESULT]:
|
|
1174
|
+
serial: str = json_result[_JsonKey.SERIAL]
|
|
1175
|
+
if len(serial) > 10:
|
|
1176
|
+
serial = serial[-10:]
|
|
1177
|
+
return serial
|
|
1178
|
+
except JSONDecodeError as jderr:
|
|
1179
|
+
raise ClientException(jderr) from jderr
|
|
1180
|
+
return None
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def _get_params(
|
|
1184
|
+
session_id: bool | str,
|
|
1185
|
+
extra_params: dict[_JsonKey, Any] | None,
|
|
1186
|
+
use_default_params: bool,
|
|
1187
|
+
) -> Mapping[str, Any]:
|
|
1188
|
+
"""Add additional params to default prams."""
|
|
1189
|
+
params: dict[_JsonKey, Any] = {_JsonKey.SESSION_ID: session_id} if use_default_params else {}
|
|
1190
|
+
if extra_params:
|
|
1191
|
+
params.update(extra_params)
|
|
1192
|
+
|
|
1193
|
+
return {str(key): str(value) for key, value in params.items()}
|