compit-inext-api-beta 0.4.0b13__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.
@@ -0,0 +1,30 @@
1
+ """Module HTTP communication with the Inext Compit api."""
2
+
3
+ from .device_definitions import DeviceDefinitionsLoader
4
+ from .api import CompitAPI, CannotConnect, InvalidAuth
5
+ from .types.DeviceState import DeviceState, Param, DeviceInstance
6
+ from .types.DeviceDefinitions import DeviceDefinitions, Parameter, ParameterDetails
7
+ from .types.SystemInfo import SystemInfo, Gate, Device
8
+ from .consts import CompitHVACMode, CompitParameter, CompitFanMode, CompitPresetMode
9
+ from .connector import CompitApiConnector
10
+
11
+ __all__ = [
12
+ "DeviceDefinitionsLoader",
13
+ "CompitAPI",
14
+ "DeviceState",
15
+ "Param",
16
+ "DeviceInstance",
17
+ "DeviceDefinitions",
18
+ "Parameter",
19
+ "ParameterDetails",
20
+ "SystemInfo",
21
+ "Gate",
22
+ "Device",
23
+ "InvalidAuth",
24
+ "CannotConnect",
25
+ "CompitHVACMode",
26
+ "CompitParameter",
27
+ "CompitFanMode",
28
+ "CompitPresetMode",
29
+ "CompitApiConnector"
30
+ ]
@@ -0,0 +1,240 @@
1
+ """The Compit api connector."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Any
6
+
7
+ import aiohttp
8
+
9
+ from compit_inext_api.consts import CompitParameter
10
+
11
+ from .types.DeviceState import DeviceState
12
+ from .types.SystemInfo import SystemInfo
13
+
14
+ TIMEOUT = 10
15
+ _LOGGER: logging.Logger = logging.getLogger(__package__)
16
+ HEADERS = {"Content-type": "application/json; charset=UTF-8"}
17
+ API_URL = "https://inext.compit.pl/mobile/v2/compit"
18
+
19
+ class CompitAPI:
20
+ """API client for Compit."""
21
+
22
+ def __init__(self, email, password, session: aiohttp.ClientSession) -> None:
23
+ """Initialize the CompitAPI class."""
24
+ self.email = email
25
+ self.password = password
26
+ self.token = None
27
+ self._api_wrapper = ApiWrapper(session)
28
+
29
+ async def authenticate(self) -> SystemInfo | None:
30
+ """Authenticate the user."""
31
+ try:
32
+ response = await self._api_wrapper.post(
33
+ f"{API_URL}/authorize",
34
+ {
35
+ "email": self.email,
36
+ "password": self.password,
37
+ "uid": "HomeAssistant",
38
+ "label": "HomeAssistant",
39
+ },
40
+ )
41
+
42
+ if response.status == 401:
43
+ raise InvalidAuth()
44
+
45
+ if response.status == 422:
46
+ result = await self.get_result(response, ignore_response_code=True)
47
+ self.token = result["token"]
48
+ response = await self._api_wrapper.post(
49
+ f"{API_URL}/clients",
50
+ {
51
+ "fcm_token": None,
52
+ "uid": "HomeAssistant",
53
+ "label": "HomeAssistant",
54
+ },
55
+ auth=self.token,
56
+ )
57
+
58
+ result = await self.get_result(response)
59
+ return await self.authenticate()
60
+ elif response.status >= 400:
61
+ return None
62
+
63
+ result = await self.get_result(response)
64
+ self.token = result["token"]
65
+ return SystemInfo.from_json(result)
66
+ except aiohttp.ClientError as e:
67
+ _LOGGER.error(e)
68
+ raise CannotConnect()
69
+
70
+ async def get_gates(self) -> SystemInfo | None:
71
+ """Get the gates from the Compit API."""
72
+ try:
73
+ response = await self._api_wrapper.get(f"{API_URL}/gates", {}, self.token)
74
+
75
+ return SystemInfo.from_json(await self.get_result(response))
76
+ except aiohttp.ClientError as e:
77
+ _LOGGER.error(e)
78
+ return None
79
+
80
+ async def get_state(self, device_id: int) -> DeviceState | None:
81
+ """Get the state of a device."""
82
+ try:
83
+ response = await self._api_wrapper.get(
84
+ f"{API_URL}/devices/{device_id}/state", {}, self.token
85
+ )
86
+
87
+ return DeviceState.from_json(await self.get_result(response))
88
+
89
+ except aiohttp.ClientError as e:
90
+ _LOGGER.error(e)
91
+ return None
92
+
93
+ async def update_device_parameter(
94
+ self, device_id: int, parameter: CompitParameter | str, value: str | float
95
+ ) -> Any:
96
+ """Update the parameter of a device.
97
+
98
+ Args:
99
+ device_id (int): The ID of the device.
100
+ parameter (CompitParameter): The parameter to update.
101
+ value (str | float): The new value of the parameter.
102
+
103
+ Returns:
104
+ Any: The result of the update operation.
105
+
106
+ """
107
+ try:
108
+ _LOGGER.info("Set %s to %s for device %s", parameter, value, device_id)
109
+
110
+ data = {"values": [{"code": parameter.value if isinstance(parameter, CompitParameter) else parameter, "value": value}]}
111
+
112
+ response = await self._api_wrapper.put(
113
+ f"{API_URL}/devices/{device_id}/params", data=data, auth=self.token
114
+ )
115
+ return await self.get_result(response)
116
+
117
+ except aiohttp.ClientError as e:
118
+ _LOGGER.error(e)
119
+ return None
120
+
121
+ async def get_result(
122
+ self,
123
+ response: aiohttp.ClientResponse | None,
124
+ ignore_response_code: bool = False,
125
+ ) -> Any:
126
+ """Get the result from the response.
127
+
128
+ Args:
129
+ response (aiohttp.ClientResponse): The response object.
130
+ ignore_response_code (bool, optional): Whether to ignore the response code. Defaults to False.
131
+
132
+ Returns:
133
+ Any: The result from the response.
134
+
135
+ Raises:
136
+ Exception: If the server returns an error response.
137
+
138
+ """
139
+ if response is None:
140
+ raise ValueError("Server empty response")
141
+ if response.ok or ignore_response_code:
142
+ return await response.json()
143
+
144
+ raise ValueError(f"Server returned: {response.status}, {response.reason}")
145
+
146
+
147
+ class ApiWrapper:
148
+ """Helper wrapper class."""
149
+
150
+ def __init__(self, session: aiohttp.ClientSession) -> None:
151
+ """Initialize the Helper class.
152
+
153
+ Args:
154
+ session (aiohttp.ClientSession): The aiohttp client session.
155
+
156
+ """
157
+ self._session = session
158
+
159
+ async def get(
160
+ self, url: str, headers: dict | None = None, auth: Any = None
161
+ ) -> aiohttp.ClientResponse:
162
+ """Run http GET method."""
163
+ if headers is None:
164
+ headers = {}
165
+
166
+ if auth:
167
+ headers["Authorization"] = auth
168
+
169
+ return await self.api_wrapper("get", url, headers=headers, auth=None)
170
+
171
+ async def post(
172
+ self,
173
+ url: str,
174
+ data: dict | None = None,
175
+ headers: dict | None = None,
176
+ auth: Any = None,
177
+ ) -> aiohttp.ClientResponse:
178
+ """Run http POST method."""
179
+ if headers is None:
180
+ headers = {}
181
+
182
+ if auth:
183
+ headers["Authorization"] = auth
184
+
185
+ return await self.api_wrapper(
186
+ "post", url, data=data, headers=headers, auth=None
187
+ )
188
+
189
+ async def put(
190
+ self,
191
+ url: str,
192
+ data: dict | None = None,
193
+ headers: dict | None = None,
194
+ auth: Any = None,
195
+ ) -> aiohttp.ClientResponse:
196
+ """Run http PUT method."""
197
+ if headers is None:
198
+ headers = {}
199
+
200
+ if auth:
201
+ headers["Authorization"] = auth
202
+
203
+ return await self.api_wrapper("put", url, data=data, headers=headers, auth=None)
204
+
205
+ async def api_wrapper(
206
+ self,
207
+ method: str,
208
+ url: str,
209
+ data: dict | None = None,
210
+ headers: dict | None = None,
211
+ auth: Any = None,
212
+ ) -> Any:
213
+ """Get information from the API."""
214
+ try:
215
+ async with asyncio.timeout(TIMEOUT):
216
+ if method == "get":
217
+ return await self._session.get(url, headers=headers, auth=auth)
218
+
219
+ if method == "post":
220
+ return await self._session.post(
221
+ url, headers=headers, data=data, auth=auth
222
+ )
223
+ if method == "put":
224
+ return await self._session.put(
225
+ url, headers=headers, json=data, auth=auth
226
+ )
227
+
228
+ except TimeoutError as exception:
229
+ _LOGGER.error(
230
+ "Timeout error fetching information from %s - %s",
231
+ url,
232
+ exception,
233
+ )
234
+
235
+ class CannotConnect(Exception):
236
+ """Error to indicate we cannot connect."""
237
+
238
+
239
+ class InvalidAuth(Exception):
240
+ """Error to indicate there is invalid auth."""
@@ -0,0 +1,130 @@
1
+ import aiohttp
2
+ import logging
3
+
4
+ from compit_inext_api.api import CompitAPI
5
+ from compit_inext_api.consts import CompitParameter
6
+ from compit_inext_api.params_dictionary import PARAMS
7
+ from compit_inext_api.device_definitions import DeviceDefinitionsLoader
8
+ from compit_inext_api.types.DeviceState import DeviceInstance, DeviceState, GateInstance, Param
9
+
10
+
11
+ _LOGGER: logging.Logger = logging.getLogger(__package__)
12
+
13
+
14
+ class CompitApiConnector:
15
+ """Connector class for Compit API."""
16
+
17
+ gates: dict[int, GateInstance] = {}
18
+
19
+ @property
20
+ def all_devices(self) -> dict[int, DeviceInstance]:
21
+ devices = {}
22
+ for gate in self.gates.values():
23
+ devices.update(gate.devices)
24
+ return devices
25
+
26
+ def __init__(self, session: aiohttp.ClientSession) -> None:
27
+ self.session = session
28
+
29
+ def get_device(self, device_id: int) -> DeviceInstance | None:
30
+ for gate in self.gates.values():
31
+ if device_id in gate.devices:
32
+ return gate.devices[device_id]
33
+ return None
34
+
35
+ async def init(self, email: str, password: str, lang: str = "en") -> bool:
36
+ self.api = CompitAPI(email, password, self.session)
37
+ self.systemInfo = await self.api.authenticate()
38
+ if self.systemInfo is None:
39
+ _LOGGER.error("Failed to authenticate with Compit API")
40
+ return False
41
+
42
+ for gates in self.systemInfo.gates:
43
+ self.gates[gates.id] = GateInstance(gates.id, gates.label)
44
+ for device in gates.devices:
45
+ try:
46
+ self.gates[gates.id].devices[device.id] = DeviceInstance(device.label, await DeviceDefinitionsLoader.get_device_definition(device.type, lang))
47
+ state = await self.api.get_state(device.id)
48
+ if state:
49
+ self.gates[gates.id].devices[device.id].state = state
50
+ else:
51
+ _LOGGER.error("Failed to get state for device %s", device.id)
52
+ except ValueError:
53
+ _LOGGER.warning("No definition found for device with code %d", device.type)
54
+ return True
55
+
56
+ async def update_state(self, device_id: int | None) -> None:
57
+ if device_id is None:
58
+ for gate in self.gates.values():
59
+ for device in gate.devices.keys():
60
+ await self.update_state(device)
61
+ return
62
+
63
+ device = self.get_device(device_id)
64
+ if device is None:
65
+ _LOGGER.warning("No device found with ID %d", device_id)
66
+ return
67
+
68
+ state = await self.api.get_state(device_id)
69
+ if state:
70
+ device.state = state
71
+ else:
72
+ _LOGGER.error("Failed to get state for device %s", device_id)
73
+
74
+ def get_current_option(self, device_id: int, parameter: str | CompitParameter) -> str | None:
75
+ if isinstance(parameter, str):
76
+ parameter = CompitParameter(parameter)
77
+ device = self.get_device(device_id)
78
+ if not device:
79
+ return None
80
+
81
+ param = PARAMS.get(parameter, None)
82
+ if param is None:
83
+ return None
84
+
85
+ val = device.state.get_parameter_value(parameter.value)
86
+ if val is None:
87
+ return None
88
+
89
+ for key, value in param.items():
90
+ if value == val.value:
91
+ return key
92
+
93
+ return None
94
+
95
+ def get_device_parameter(self, device_id: int, parameter: str | CompitParameter) -> Param | str | None:
96
+ device = self.get_device(device_id)
97
+ if not device:
98
+ return None
99
+ return device.state.get_parameter_value(parameter if isinstance(parameter, str) else parameter.value)
100
+
101
+ async def select_device_option(self, device_id: int, parameter: str | CompitParameter, value: str) -> bool:
102
+ if isinstance(parameter, str):
103
+ parameter = CompitParameter(parameter)
104
+
105
+ param = PARAMS.get(parameter, None)
106
+ if param is None:
107
+ return False
108
+ val = param.get(value, None)
109
+ if val is None:
110
+ return False
111
+ result = await self.api.update_device_parameter(device_id, parameter, val)
112
+ if result is None:
113
+ return False
114
+
115
+ device = self.get_device(device_id)
116
+ if device is None:
117
+ return False
118
+
119
+ device.state.set_parameter_value(parameter if isinstance(parameter, str) else parameter.value, val)
120
+ return result
121
+
122
+ async def set_device_parameter(self, device_id: int, parameter: str | CompitParameter, value: str | float) -> bool:
123
+ result = await self.api.update_device_parameter(device_id, parameter, value)
124
+ if result:
125
+ device = self.get_device(device_id)
126
+ if device:
127
+ device.state.set_parameter_value(parameter if isinstance(parameter, str) else parameter.value, value)
128
+ return result
129
+
130
+
@@ -0,0 +1,94 @@
1
+ from enum import Enum
2
+
3
+
4
+ class CompitHVACMode(Enum):
5
+ """Enum for available HVAC modes."""
6
+
7
+ HEAT = 0
8
+ OFF = 1
9
+ COOL = 2
10
+
11
+ class CompitParameter(Enum):
12
+ """Enum for Compit device parameters."""
13
+
14
+ ADDITIONAL_VENTILATION_ZONE = "__trybaero2"
15
+ AEROKONFBYPASS = "__aerokonfbypass"
16
+ AIRING_PROGRAM_ZONE_3 = "__a3programwietrzenia"
17
+ AIRING_PROGRAM_ZONE_4 = "__a4programwietrzenia"
18
+ AIRING_PROGRAM_ZONE_5 = "__a5prwietrz"
19
+ BIOMAX_CIRCULATION_MODE = "__trybcyrkulacji"
20
+ BIOMAX_DHW_CIRCULATION_MODE = "__cwucyrkpraca"
21
+ BIOMAX_DHW_MODE = "__cwupraca"
22
+ BIOMAX_HEATING_SOURCE_OF_CORRECTION = "__pracakotla"
23
+ BIOMAX_MIXER_MODE_ZONE_1 = "__m1praca"
24
+ BIOMAX_MIXER_MODE_ZONE_2 = "__m2praca"
25
+ BUFFER_MODE = "__trprbufora"
26
+ CIRCUIT_MODE_HEATING_ZONE_1 = "__typ_obwo_co1"
27
+ CIRCUIT_MODE_HEATING_ZONE_2 = "__typ_obwo_co2"
28
+ CIRCUIT_MODE_HEATING_ZONE_3 = "__typ_obwo_co3"
29
+ CIRCUIT_MODE_HEATING_ZONE_4 = "__typ_obwo_co4"
30
+ CURRENT_TEMPERATURE = "__tpokojowa"
31
+ DHW_CIRCULATION_MODE = "__dhwcircmode"
32
+ DHW_MODE = "__dhwmode"
33
+ DHW_OPERATING_MODE = "__trybprcwu"
34
+ DHWC_CIRCULATION = "__cyrk_cwu"
35
+ FAN_MODE = "__trybaero"
36
+ HEATING_MODE_ZONE_1 = "__tr_pr_co1"
37
+ HEATING_MODE_ZONE_2 = "__tr_pr_co2"
38
+ HEATING_MODE_ZONE_3 = "__tr_pr_co3"
39
+ HEATING_MODE_ZONE_4 = "__tr_pr_co4"
40
+ HEATING_OPERATING_MODE_ZONE_1 = "__trprco1"
41
+ HEATING_OPERATING_MODE_ZONE_2 = "__trprco2"
42
+ HEATING_OPERATING_MODE_ZONE_3 = "__trprco3"
43
+ HEATING_OPERATING_MODE_ZONE_4 = "__trprco4"
44
+ HEATING_SOURCE_OF_CORRECTION = "__comode"
45
+ HEATING_SOURCE_OF_CORRECTION_ZONE_1 = "__co1zrodlokorekty"
46
+ HEATING_SOURCE_OF_CORRECTION_ZONE_2 = "_co2zrodlokorekty"
47
+ HEATING_SOURCE_OF_CORRECTION_ZONE_3 = "__co3zrodlokorekty"
48
+ HEATING_SOURCE_OF_CORRECTION_ZONE_4 = "__co4zrkorekty"
49
+ HVAC_MODE = "__trybpracyinstalacji"
50
+ LANGUAGE = "_jezyk"
51
+ MIXER_MODE = "__pracamieszacza"
52
+ MIXERMODE_ZONE_1 = "__mixer1mode"
53
+ NANO_MODE = "__nano_mode"
54
+ OPERATING_MODE = "__tr_pracy_pc"
55
+ PRE_HEATER_ZONE_3 = "__a3konfignagwst"
56
+ PRE_HEATER_ZONE_5 = "__a5trybnagrzwst"
57
+ PRESET_MODE = "__trybpracytermostatu"
58
+ R350_HEATING_SOURCE_OF_CORRECTION = "__tr_pr"
59
+ R470_OPERATING_MODE = "__mode"
60
+ R480_BUFFER_MODE = "__tr_buf"
61
+ R480_DHW_CIRCULATION = "__cwu_cyrkulacja"
62
+ R480_DHW_MODE = "__tryb_cwu"
63
+ R480_OPERATING_MODE = "__praca_pc"
64
+ R490_OPERATING_MODE = "__sezprinst"
65
+ R770_DHW_CIRCULATION_MODE = "__trybpracycyrkcwu"
66
+ R770_DHW_OPERATING_MODE = "__trybpracycwu"
67
+ R770_MIXER_MODE_ZONE_1 = "__trybmieszacza"
68
+ R770_MIXER_MODE_ZONE_2 = "__trybmie2"
69
+ SECONDARY_HEATER_ZONE_3 = "__a3konfignagwt"
70
+ SECONDARY_HEATER_ZONE_5 = "__a5trnagrzgl"
71
+ SET_TARGET_TEMPERATURE = "__tempzadpracareczna"
72
+ SOLAR_COMP_OPERATING_MODE = "__trybpracy"
73
+ TARGET_TEMPERATURE = "__tpokzadana"
74
+ VENTILATION_COMFORT_ZONE = "__wentkomfort"
75
+ VENTILATION_ECO_ZONE = "__wenteko"
76
+ VENTILATION_HOLIDAY_MODE = "__wenturlop"
77
+
78
+ class CompitFanMode(Enum):
79
+ """Enum for available fan modes."""
80
+
81
+ OFF = 0
82
+ LOW = 1
83
+ MEDIUM = 2
84
+ HIGH = 3
85
+ HOLIDAY = 4
86
+ AUTO = 5
87
+
88
+ class CompitPresetMode(Enum):
89
+ """Enum for available preset modes."""
90
+
91
+ AUTO = 0
92
+ HOLIDAY = 1
93
+ MANUAL = 2
94
+ AWAY = 3
@@ -0,0 +1 @@
1
+ # Device definitions package