twist-innovation-api 0.0.3__py3-none-any.whl → 0.0.5__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,267 @@
1
+ # Copyright (C) 2025 Twist Innovation
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
+ #
11
+ # See the GNU General Public License for more details:
12
+ # https://www.gnu.org/licenses/gpl-3.0.html
13
+
14
+ """
15
+ Backend Adapter for Twist Device Discovery
16
+
17
+ This module handles communication with the backend server to fetch device information.
18
+ It's designed to be easily replaceable if the server API changes.
19
+
20
+ The adapter maps backend device types to Twist model classes dynamically.
21
+ """
22
+
23
+ import aiohttp
24
+ from typing import Optional, Type, Dict
25
+ from .TwistModel import TwistModel
26
+ from .TwistLight import TwistLight
27
+ from .TwistShutter import TwistShutter
28
+ from .TwistRgb import TwistRgb
29
+ from .TwistSensor import TwistSensor
30
+ from .TwistGarage import TwistGarage
31
+ from .TwistRelay import TwistRelay
32
+ from .TwistButton import TwistButton
33
+ from .TwistBinarySensor import TwistBinarySensor
34
+
35
+
36
+ class DeviceInfo:
37
+ """Container for device information from backend"""
38
+ def __init__(self, device_id: int, model_id: int, name: str, model_class: Type[TwistModel],
39
+ device_key: str, device_allocation_model_key: Optional[str] = None):
40
+ self.device_id = device_id
41
+ self.model_id = model_id
42
+ self.name = name
43
+ self.model_class = model_class
44
+ self.device_key = device_key
45
+ self.device_allocation_model_key = device_allocation_model_key
46
+
47
+
48
+ class ProductModelInfo:
49
+ """Container for product model information"""
50
+ def __init__(self, device_key: str, device_model_index: int, product_name: str,
51
+ product_model_label: str, product_model_alias: str):
52
+ self.device_key = device_key
53
+ self.device_model_index = device_model_index
54
+ self.product_name = product_name
55
+ self.product_model_label = product_model_label
56
+ self.product_model_alias = product_model_alias
57
+
58
+
59
+ class BackendAdapter:
60
+ """
61
+ Adapter for fetching device information from backend server.
62
+
63
+ If the backend API changes, only this class needs to be updated.
64
+ """
65
+
66
+ def __init__(self, base_url: str, api_key: str, installation_uuid: str):
67
+ """
68
+ Initialize backend adapter
69
+
70
+ Args:
71
+ base_url: Base URL of the backend server (e.g., "https://backbone-dev.twist-innovation.com")
72
+ api_key: API key for authentication (X-Api-Key header)
73
+ installation_uuid: Installation UUID for the API endpoint
74
+ """
75
+ self.base_url = base_url.rstrip('/')
76
+ self.api_key = api_key
77
+ self.installation_uuid = installation_uuid
78
+
79
+ async def fetch_products(self) -> dict[str, ProductModelInfo]:
80
+ """
81
+ Fetch product information from backend server
82
+
83
+ Returns:
84
+ Dictionary mapping deviceAllocationModelKey -> ProductModelInfo
85
+
86
+ Raises:
87
+ aiohttp.ClientError: If request fails
88
+ """
89
+ url = f"{self.base_url}/installations/V1/installations/{self.installation_uuid}/products"
90
+ headers = {
91
+ "accept": "text/plain",
92
+ "X-Api-Key": self.api_key
93
+ }
94
+
95
+ async with aiohttp.ClientSession() as session:
96
+ async with session.get(url, headers=headers) as response:
97
+ response.raise_for_status()
98
+ data = await response.json()
99
+
100
+ return self._parse_products(data)
101
+
102
+ def _parse_products(self, data: dict) -> dict[str, ProductModelInfo]:
103
+ """
104
+ Parse products response into dictionary for easy lookup by model key
105
+
106
+ Returns:
107
+ Dictionary mapping deviceAllocationModelKey -> ProductModelInfo
108
+ """
109
+ result = {}
110
+
111
+ for product in data.get("results", []):
112
+ product_name = product.get("name", "")
113
+
114
+ for model in product.get("models", []):
115
+ model_key = model.get("key")
116
+ device_key = model.get("deviceKey")
117
+ device_model_index = model.get("deviceModelIndex")
118
+ product_model_label = model.get("productModelLabel", "")
119
+ product_model_alias = model.get("productModelAlias", "")
120
+
121
+ if model_key:
122
+ result[model_key] = ProductModelInfo(
123
+ device_key=device_key,
124
+ device_model_index=device_model_index,
125
+ product_name=product_name,
126
+ product_model_label=product_model_label,
127
+ product_model_alias=product_model_alias
128
+ )
129
+
130
+ return result
131
+
132
+ async def fetch_installation_id(self) -> int:
133
+ """
134
+ Fetch installation ID from backend server
135
+
136
+ Returns:
137
+ Installation ID
138
+
139
+ Raises:
140
+ aiohttp.ClientError: If request fails
141
+ ValueError: If response format is invalid
142
+ """
143
+ url = f"{self.base_url}/installations/V1/installations/{self.installation_uuid}"
144
+ headers = {
145
+ "accept": "text/plain",
146
+ "X-Api-Key": self.api_key
147
+ }
148
+
149
+ async with aiohttp.ClientSession() as session:
150
+ async with session.get(url, headers=headers) as response:
151
+ response.raise_for_status()
152
+ data = await response.json()
153
+
154
+ installation_id = data.get("installation", {}).get("installationId")
155
+ if installation_id is None:
156
+ raise ValueError("Missing 'installationId' in installation response")
157
+
158
+ return installation_id
159
+
160
+ async def fetch_config(self) -> list[DeviceInfo]:
161
+ """
162
+ Fetch device configuration from backend server
163
+
164
+ Returns:
165
+ List of DeviceInfo objects
166
+
167
+ Raises:
168
+ aiohttp.ClientError: If request fails
169
+ ValueError: If response format is invalid
170
+ """
171
+ url = f"{self.base_url}/installations/V1/installations/{self.installation_uuid}/devices"
172
+ headers = {
173
+ "accept": "text/plain",
174
+ "X-Api-Key": self.api_key
175
+ }
176
+
177
+ async with aiohttp.ClientSession() as session:
178
+ async with session.get(url, headers=headers) as response:
179
+ response.raise_for_status()
180
+ data = await response.json()
181
+
182
+ return self._parse_response(data)
183
+
184
+ def _parse_response(self, data: dict) -> list[DeviceInfo]:
185
+ """
186
+ Parse backend response into device info list
187
+
188
+ This method contains the mapping logic for the current API format.
189
+ Update this method if the backend response format changes.
190
+
191
+ Args:
192
+ data: JSON response from backend (new format with results array)
193
+
194
+ Returns:
195
+ List of DeviceInfo objects
196
+ """
197
+
198
+ # Parse devices from new API format
199
+ results = data.get("results", [])
200
+ device_list = []
201
+
202
+ for device in results:
203
+ try:
204
+ twist_id = device.get("twistId")
205
+ device_key = device.get("key")
206
+ models = device.get("models", [])
207
+
208
+ # Process each model in the device
209
+ for model in models:
210
+ model_index = model.get("deviceModelIndex")
211
+ model_type_name = model.get("modelTypeName", "")
212
+ label = model.get("label", f"Model {model_index}")
213
+ device_allocation_model_key = model.get("deviceAllocationModelKey")
214
+
215
+ # Map model type name directly to model class
216
+ model_class = self._map_model_type_to_class(model_type_name)
217
+
218
+ if model_class:
219
+ device_info = DeviceInfo(
220
+ device_id=twist_id,
221
+ model_id=model_index,
222
+ name=label,
223
+ model_class=model_class,
224
+ device_key=device_key,
225
+ device_allocation_model_key=device_allocation_model_key
226
+ )
227
+ device_list.append(device_info)
228
+ else:
229
+ print(f"Unknown model type '{model_type_name}' for device {twist_id}, model {model_index}")
230
+
231
+ except (KeyError, TypeError) as e:
232
+ print(f"Skipping device due to error: {e}")
233
+ continue
234
+
235
+ return device_list
236
+
237
+ def _map_model_type_to_class(self, model_type_name: str) -> Optional[Type[TwistModel]]:
238
+ """
239
+ Map the API's modelTypeName directly to Twist model class
240
+
241
+ Args:
242
+ model_type_name: Model type name from the API
243
+
244
+ Returns:
245
+ TwistModel subclass or None if unmapped
246
+ """
247
+ mapping = {
248
+ "Louvres": TwistShutter,
249
+ "Mono Light": TwistLight,
250
+ "RGB": TwistRgb,
251
+ "Temperature": TwistSensor,
252
+ "CB Shutter": TwistShutter,
253
+ "PB Shutter": TwistShutter,
254
+ "Shutter": TwistShutter,
255
+ "Garage": TwistGarage,
256
+ "Relay": TwistRelay,
257
+ "Button": TwistButton,
258
+ "Binary Sensor": TwistBinarySensor,
259
+ "Gateway": None, # Skip Gateway models
260
+ "Weather": None, # Skip Weather models
261
+ "Wind": None, # Skip Wind models
262
+ "Rain Sensor": None, # Skip Rain Sensor models
263
+ "Pergola Protection": None, # Skip logic models
264
+ "Unknown": None, # Skip unknown models
265
+ }
266
+
267
+ return mapping.get(model_type_name)
twist/TwistAPI.py CHANGED
@@ -15,13 +15,22 @@
15
15
  import asyncio
16
16
  import json
17
17
  import random
18
+ from typing import Optional
18
19
 
19
20
  from .TwistDevice import TwistDevice, TwistModel
20
- from .TwistTypes import DeviceVariant
21
+ from .BackendAdapter import BackendAdapter, DeviceInfo, ProductModelInfo
21
22
 
22
23
 
23
24
  class TwistAPI:
24
- def __init__(self, installation_id: int):
25
+ def __init__(self, backend_url: str, api_key: str, installation_uuid: str):
26
+ """
27
+ Initialize TwistAPI
28
+
29
+ Args:
30
+ backend_url: Backend server URL (e.g., "https://backbone-dev.twist-innovation.com")
31
+ api_key: API key for backend authentication
32
+ installation_uuid: Installation UUID for the backend API
33
+ """
25
34
  self._ext_publish = None
26
35
  self._subscribe = None
27
36
 
@@ -31,9 +40,10 @@ class TwistAPI:
31
40
  }
32
41
 
33
42
  self.device_list: list[TwistDevice] = list()
43
+ self.installation_id: Optional[int] = None
34
44
 
35
- # TODO: this should come from the API
36
- self.installation_id = installation_id
45
+ # Backend adapter for fetching device info
46
+ self._backend_adapter = BackendAdapter(backend_url, api_key, installation_uuid)
37
47
 
38
48
  async def add_mqtt(self, publisher, subscriber):
39
49
  self._ext_publish = publisher
@@ -41,13 +51,71 @@ class TwistAPI:
41
51
 
42
52
  await self._subscribe(f"v2/{self.installation_id}/rx/#", self._on_message_received)
43
53
 
44
- async def search_models(self):
45
- await self.getboard(0xffffffff)
46
- await asyncio.sleep(3)
54
+ async def get_models(self):
55
+ """
56
+ Get models from backend
47
57
 
58
+ Returns:
59
+ List of TwistModel objects
60
+ """
61
+ return await self._get_models_from_backend()
62
+
63
+ async def _get_models_from_backend(self):
64
+ """
65
+ Fetch devices from backend and create model list
66
+
67
+ Returns:
68
+ List of TwistModel objects
69
+ """
70
+ # Fetch installation_id from API
71
+ self.installation_id = await self._backend_adapter.fetch_installation_id()
72
+
73
+ # Fetch product info for naming
74
+ products = await self._backend_adapter.fetch_products()
75
+
76
+ # Fetch device info from API
77
+ device_infos = await self._backend_adapter.fetch_config()
78
+
79
+ # Group devices by device_id
80
+ devices_dict = {}
81
+ for device_info in device_infos:
82
+ if device_info.device_id not in devices_dict:
83
+ devices_dict[device_info.device_id] = []
84
+ devices_dict[device_info.device_id].append(device_info)
85
+
86
+ # Create TwistDevice objects with models
48
87
  model_list: list[TwistModel] = list()
49
- for device in self.device_list:
50
- for model in device.model_list:
88
+ for device_id, infos in devices_dict.items():
89
+ # Sort by model_id to ensure correct order
90
+ infos.sort(key=lambda x: x.model_id)
91
+
92
+ # Create device if it doesn't exist
93
+ device = next((d for d in self.device_list if d.twist_id == device_id), None)
94
+ if device is None:
95
+ device = TwistDevice(device_id, 0, self)
96
+ self.device_list.append(device)
97
+
98
+ # Create models for this device
99
+ for info in infos:
100
+ model = info.model_class(info.model_id, device)
101
+
102
+ # Get product info if available using deviceAllocationModelKey
103
+ product_info = None
104
+ if info.device_allocation_model_key:
105
+ product_info = products.get(info.device_allocation_model_key)
106
+
107
+ if product_info:
108
+ model.product_name = product_info.product_name
109
+ model.name = product_info.product_model_label or product_info.product_model_alias or info.name
110
+ else:
111
+ model.product_name = None
112
+ model.name = info.name
113
+
114
+ # Add model at correct position in model_list
115
+ while len(device.model_list) <= info.model_id:
116
+ device.model_list.append(None)
117
+ device.model_list[info.model_id] = model
118
+
51
119
  model_list.append(model)
52
120
 
53
121
  return model_list
@@ -93,7 +161,5 @@ class TwistAPI:
93
161
  await device.context_msg(model_id, payload)
94
162
 
95
163
  async def _get_board(self, twist_id, payload, model_id=None):
96
- data = json.loads(payload)
97
- if not any(dev.twist_id == twist_id for dev in self.device_list):
98
- self.device_list.append(TwistDevice(twist_id, data["h"], DeviceVariant(data["v"]), self))
99
- print(f"New device with id: {twist_id}")
164
+ """Handle getboard MQTT message (legacy compatibility)"""
165
+ pass
@@ -12,22 +12,37 @@
12
12
  # https://www.gnu.org/licenses/gpl-3.0.html
13
13
 
14
14
  from __future__ import annotations
15
+
15
16
  from typing import TYPE_CHECKING
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from TwistDevice import TwistDevice
20
+ from .TwistTypes import ContextErrors
19
21
  from .TwistDevice import TwistModel
20
22
 
21
23
 
22
- class TwistCbShutter(TwistModel):
24
+ class TwistBinarySensor(TwistModel):
23
25
  def __init__(self, model_id: int, parent_device: TwistDevice):
24
26
  super().__init__(model_id, parent_device)
25
27
 
26
28
  async def context_msg(self, payload: str):
27
- self.parse_general_context(payload)
29
+ data = self.parse_general_context(payload)
30
+
31
+ for ctx in data["cl"]:
32
+ index, value = self._get_value_from_context(ctx)
33
+ if index < ContextErrors.MAX.value:
34
+ if ContextErrors(index) == ContextErrors.ACTUAL:
35
+ self.actual_state = value[0]
28
36
 
29
37
  if self._update_callback is not None:
30
38
  await self._update_callback(self)
31
39
 
40
+ @property
41
+ def is_active(self) -> bool:
42
+ """Check if binary sensor is active (1) or inactive (0)"""
43
+ return bool(self.actual_state)
44
+
32
45
  def print_context(self):
33
- print(f"Cb Shutter Device: {self.parent_device.twist_id}, Model: {self.model_id},")
46
+ state = "ACTIVE" if self.is_active else "INACTIVE"
47
+ print(
48
+ f"Binary Sensor Device: {self.parent_device.twist_id}, Model: {self.model_id}, State: {state}")
twist/TwistButton.py ADDED
@@ -0,0 +1,60 @@
1
+ # Copyright (C) 2025 Twist Innovation
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
+ #
11
+ # See the GNU General Public License for more details:
12
+ # https://www.gnu.org/licenses/gpl-3.0.html
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import TYPE_CHECKING
17
+
18
+ if TYPE_CHECKING:
19
+ from TwistDevice import TwistDevice
20
+ from .TwistTypes import ContextErrors
21
+ from .TwistDevice import TwistModel
22
+ from enum import Enum
23
+
24
+
25
+ class TwistButton(TwistModel):
26
+ class ButtonEvent(Enum):
27
+ PUSH = 0x00
28
+ RELEASE = 0x01
29
+ LONG_PUSH = 0x02
30
+ LONG_RELEASE = 0x03
31
+ DOUBLE_PUSH = 0x04
32
+ DOUBLE_RELEASE = 0x05
33
+ DOUBLE_LONG_PUSH = 0x06
34
+ DOUBLE_LONG_RELEASE = 0x07
35
+
36
+ def __init__(self, model_id: int, parent_device: TwistDevice):
37
+ super().__init__(model_id, parent_device)
38
+ self.last_event: TwistButton.ButtonEvent | None = None
39
+
40
+ async def context_msg(self, payload: str):
41
+ data = self.parse_general_context(payload)
42
+
43
+ for ctx in data["cl"]:
44
+ index, value = self._get_value_from_context(ctx)
45
+ if index < ContextErrors.MAX.value:
46
+ if ContextErrors(index) == ContextErrors.ACTUAL:
47
+ self.actual_state = value[0]
48
+ # Store the event type
49
+ try:
50
+ self.last_event = TwistButton.ButtonEvent(value[0])
51
+ except ValueError:
52
+ self.last_event = None
53
+
54
+ if self._update_callback is not None:
55
+ await self._update_callback(self)
56
+
57
+ def print_context(self):
58
+ event_name = self.last_event.name if self.last_event else "UNKNOWN"
59
+ print(
60
+ f"Button Device: {self.parent_device.twist_id}, Model: {self.model_id}, Last Event: {event_name}")
twist/TwistDevice.py CHANGED
@@ -17,25 +17,16 @@ from typing import TYPE_CHECKING
17
17
  if TYPE_CHECKING:
18
18
  from TwistAPI import TwistAPI
19
19
 
20
- from .TwistTypes import DeviceVariant
21
20
  from .TwistModel import TwistModel
22
21
 
23
- from .Variants import model_dict
24
-
25
22
  class TwistDevice:
26
- def __init__(self, twist_id: int, hw: int, var: DeviceVariant, api: TwistAPI):
23
+ def __init__(self, twist_id: int, hw: int, api: TwistAPI):
27
24
  self.twist_id = twist_id
28
25
  self.hw = hw
29
- self.var = var
30
26
  self.api = api
31
27
 
32
28
  self.model_list: list[TwistModel] = list()
33
29
 
34
- model_id = 0
35
- if self.var in model_dict:
36
- for model in model_dict[self.var]:
37
- self.model_list.append(model(model_id, self))
38
- model_id += 1
39
-
40
30
  async def context_msg(self, model_id: int, payload: str):
41
- await self.model_list[model_id].context_msg(payload)
31
+ if model_id < len(self.model_list) and self.model_list[model_id] is not None:
32
+ await self.model_list[model_id].context_msg(payload)
twist/TwistGarage.py ADDED
@@ -0,0 +1,65 @@
1
+ # Copyright (C) 2025 Twist Innovation
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
+ #
11
+ # See the GNU General Public License for more details:
12
+ # https://www.gnu.org/licenses/gpl-3.0.html
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import TYPE_CHECKING
17
+
18
+ if TYPE_CHECKING:
19
+ from TwistDevice import TwistDevice
20
+ from .TwistTypes import ContextErrors
21
+ from .TwistDevice import TwistModel
22
+ from enum import Enum
23
+
24
+
25
+ class TwistGarage(TwistModel):
26
+ class EventIndexes(Enum):
27
+ OPEN = 0x00
28
+ CLOSE = 0x01
29
+ TOGGLE = 0x02
30
+
31
+ def __init__(self, model_id: int, parent_device: TwistDevice):
32
+ super().__init__(model_id, parent_device)
33
+
34
+ async def context_msg(self, payload: str):
35
+ data = self.parse_general_context(payload)
36
+
37
+ for ctx in data["cl"]:
38
+ index, value = self._get_value_from_context(ctx)
39
+ if index < ContextErrors.MAX.value:
40
+ if ContextErrors(index) == ContextErrors.ACTUAL:
41
+ self.actual_state = value[0]
42
+
43
+ if self._update_callback is not None:
44
+ await self._update_callback(self)
45
+
46
+ async def open(self):
47
+ await self._activate_event(TwistGarage.EventIndexes.OPEN)
48
+
49
+ async def close(self):
50
+ await self._activate_event(TwistGarage.EventIndexes.CLOSE)
51
+
52
+ async def toggle(self):
53
+ await self._activate_event(TwistGarage.EventIndexes.TOGGLE)
54
+
55
+ async def _activate_event(self, index: TwistGarage.EventIndexes):
56
+ data = {
57
+ "i": index.value,
58
+ "vl": []
59
+ }
60
+
61
+ await self.parent_device.api.activate_event(self, data)
62
+
63
+ def print_context(self):
64
+ print(
65
+ f"Garage Device: {self.parent_device.twist_id}, Model: {self.model_id}, Actual: {self.actual_state}")
twist/TwistModel.py CHANGED
@@ -39,6 +39,21 @@ class TwistModel():
39
39
 
40
40
  self._update_callback: Callable[[TwistModel], Awaitable[None]] | None = None
41
41
 
42
+ @property
43
+ def friendly_name(self) -> str:
44
+ """
45
+ Convert name attribute to friendly format.
46
+ Example: "light_living_lamp_1" -> "Light living lamp 1"
47
+
48
+ Returns:
49
+ Friendly formatted name, or "Unknown" if name is not set
50
+ """
51
+ if not hasattr(self, 'name') or not self.name:
52
+ return "Unknown"
53
+
54
+ # Replace underscores with spaces and capitalize first letter
55
+ return self.name.replace('_', ' ').capitalize()
56
+
42
57
 
43
58
  def print_context(self):
44
59
  print("function is not supported")
twist/TwistRelay.py ADDED
@@ -0,0 +1,68 @@
1
+ # Copyright (C) 2025 Twist Innovation
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
+ #
11
+ # See the GNU General Public License for more details:
12
+ # https://www.gnu.org/licenses/gpl-3.0.html
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import TYPE_CHECKING
17
+
18
+ if TYPE_CHECKING:
19
+ from TwistDevice import TwistDevice
20
+ from .TwistTypes import ContextErrors
21
+ from .TwistDevice import TwistModel
22
+ from enum import Enum
23
+
24
+
25
+ class TwistRelay(TwistModel):
26
+ class EventIndexes(Enum):
27
+ SET = 0x00
28
+ CLEAR = 0x01
29
+ TOGGLE = 0x03
30
+
31
+ def __init__(self, model_id: int, parent_device: TwistDevice):
32
+ super().__init__(model_id, parent_device)
33
+
34
+ async def context_msg(self, payload: str):
35
+ data = self.parse_general_context(payload)
36
+
37
+ for ctx in data["cl"]:
38
+ index, value = self._get_value_from_context(ctx)
39
+ if index < ContextErrors.MAX.value:
40
+ if ContextErrors(index) == ContextErrors.ACTUAL:
41
+ self.actual_state = value[0]
42
+ elif ContextErrors(index) == ContextErrors.REQUESTED:
43
+ self.requested_state = value[0]
44
+
45
+ if self._update_callback is not None:
46
+ await self._update_callback(self)
47
+
48
+ async def turn_on(self):
49
+ await self._activate_event(TwistRelay.EventIndexes.SET)
50
+
51
+ async def turn_off(self):
52
+ await self._activate_event(TwistRelay.EventIndexes.CLEAR)
53
+
54
+ async def toggle(self):
55
+ await self._activate_event(TwistRelay.EventIndexes.TOGGLE)
56
+
57
+ async def _activate_event(self, index: TwistRelay.EventIndexes):
58
+ data = {
59
+ "i": index.value,
60
+ "vl": []
61
+ }
62
+
63
+ await self.parent_device.api.activate_event(self, data)
64
+
65
+ def print_context(self):
66
+ state = "ON" if self.actual_state else "OFF"
67
+ print(
68
+ f"Relay Device: {self.parent_device.twist_id}, Model: {self.model_id}, State: {state}")
@@ -22,7 +22,7 @@ from .TwistDevice import TwistModel
22
22
  from enum import Enum
23
23
 
24
24
 
25
- class TwistLouvre(TwistModel):
25
+ class TwistShutter(TwistModel):
26
26
  class EventIndexes(Enum):
27
27
  OPEN = 0
28
28
  STOP = 1
@@ -55,24 +55,24 @@ class TwistLouvre(TwistModel):
55
55
  await self._update_callback(self)
56
56
 
57
57
  async def open(self):
58
- await self._activate_event(TwistLouvre.EventIndexes.OPEN)
58
+ await self._activate_event(TwistShutter.EventIndexes.OPEN)
59
59
 
60
60
  async def stop(self):
61
- await self._activate_event(TwistLouvre.EventIndexes.STOP)
61
+ await self._activate_event(TwistShutter.EventIndexes.STOP)
62
62
 
63
63
  async def close(self):
64
- await self._activate_event(TwistLouvre.EventIndexes.CLOSE)
64
+ await self._activate_event(TwistShutter.EventIndexes.CLOSE)
65
65
 
66
66
  async def toggle(self):
67
- await self._activate_event(TwistLouvre.EventIndexes.TOGGLE)
67
+ await self._activate_event(TwistShutter.EventIndexes.TOGGLE)
68
68
 
69
69
  async def set_value(self, value: int, fading_time: int | None = None):
70
70
  if fading_time is not None:
71
71
  raise NotImplementedError("Fading time can't be used in this model")
72
72
  else:
73
- await self._activate_event(TwistLouvre.EventIndexes.VALUE, int(value * 655.35))
73
+ await self._activate_event(TwistShutter.EventIndexes.VALUE, int(value * 655.35))
74
74
 
75
- async def _activate_event(self, index: TwistLouvre.EventIndexes, value: int | None = None,
75
+ async def _activate_event(self, index: TwistShutter.EventIndexes, value: int | None = None,
76
76
  fading_time: int | None = None):
77
77
  data = {
78
78
  "i": index.value
@@ -89,5 +89,5 @@ class TwistLouvre(TwistModel):
89
89
 
90
90
  def print_context(self):
91
91
  print(
92
- f"Louvre Device: {self.parent_device.twist_id}, Model: {self.model_id}, Actual: {self.actual_state}, "
92
+ f"Shutter Device: {self.parent_device.twist_id}, Model: {self.model_id}, Actual: {self.actual_state}, "
93
93
  f"Requested: {self.requested_state}, Operating: {self.operating_time}, Current: {self.current}")
twist/TwistTypes.py CHANGED
@@ -13,55 +13,6 @@
13
13
 
14
14
  from enum import Enum
15
15
 
16
- class DeviceVariant(Enum):
17
- NO_VARIANT = 0xFFFF
18
- NONE = 0x0000
19
- LED_4_BUTTON_4 = 0x0001
20
- LED_1_BUTTON_1 = 0x0002
21
- MONO_LIGHT_4 = 0x0003
22
- MONO_LIGHT_2 = 0x0004
23
- TEMP_1_HUM_1_PIR_1_LUX_1_VOC_1 = 0x0005
24
- MONO_LIGHT_1_BUTTON_1 = 0x0006
25
- RGB_1_TUNABLE_WHITE_1 = 0x0007
26
- BUTTON_8 = 0x0008
27
- MONO_LIGHT_2_LUX2BRIGHTNESS_1 = 0x0009
28
- RGB_1_MONO_LIGHT_2 = 0x000A
29
- LUX_1_UV_1_TEMP_1_HUM_1_WINDSPEED_1_GUSTSPEED_1_WINDDIR_1_RAINFALL_1_PRES_1 = 0x000B
30
- RGB_1 = 0x000C
31
- BUTTON_6_LUX_6 = 0x00D2 # Corrected 0x000D2 to 0x00D2
32
- BUTTON_4 = 0x000E
33
- TUNABLE_WHITE_2 = 0x000F
34
- WIND_1_LUX_1_RAIN_1 = 0x0010
35
- BUTTON_1 = 0x0011
36
- PULSE_CONTACT_2 = 0x0012
37
- TBSHUTTER_1 = 0x0013
38
- MONO_LIGHT_32 = 0x0142
39
- REPEATER_1 = 0x0015
40
- REPEATER_1_LED_1 = 0x0016
41
- LED_12 = 0x0172
42
- BUTTON_12 = 0x0182
43
- CBSHUTTER_1_VOLTAGE_1_CURRENT_1 = 0x0019
44
- TBSHUTTER_6 = 0x001A
45
- CBSHUTTER_1_MONO_LIGHT_1 = 0x001B
46
- CBSHUTTER_1 = 0x001C
47
- MONO_LIGHT_1 = 0x001D
48
- HEATER_1 = 0x001E
49
- PBSHUTTER_1 = 0x001F
50
- CBSHUTTER_2_MONO_LIGHT_4_TEMPERATURE_1 = 0x0020
51
- GATEWAY_1_WEATHER_1_TEMPERATURE_1_WIND_1_RAIN_1 = 0x0021
52
- CBSHUTTER_3 = 0x0022
53
- BUTTON_10 = 0x0023
54
- MATRIX_1 = 0x0024
55
- RAIN_1 = 0x0025
56
- RAIN_1_TEMPERATURE_1_WIND_1_WEATHER_1 = 0x0026
57
- LOUVRE_2_MONO_LIGHT_4_TEMPERATURE_1 = 0x0027
58
- LOUVRE_2_MONO_LIGHT_4 = 0x0028
59
-
60
-
61
- class Models(Enum):
62
- LOUVRE = 0
63
- MONO_LIGHT = 1
64
-
65
16
  class ContextErrors(Enum):
66
17
  ERROR = 0
67
18
  WARNING = 1
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: twist-innovation-api
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Python library to talk to the twist-innovation api
5
5
  Home-page: https://github.com/twist-innovation/twist-innovation-api
6
6
  Author: Sibrecht Goudsmedt
@@ -11,15 +11,6 @@ Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.7
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Dynamic: author
15
- Dynamic: author-email
16
- Dynamic: classifier
17
- Dynamic: description
18
- Dynamic: description-content-type
19
- Dynamic: home-page
20
- Dynamic: license-file
21
- Dynamic: requires-python
22
- Dynamic: summary
23
14
 
24
15
  # Twist Innovation API
25
16
 
@@ -0,0 +1,19 @@
1
+ twist/BackendAdapter.py,sha256=v-kjbuuHyktIjWX6MG_I3sH3ZQBawJhSiwBzzatZ3C0,9758
2
+ twist/TwistAPI.py,sha256=wjLl9tFs9krEaw8elTdXQJMqeuWAMXNS0nwJScXNMnY,6125
3
+ twist/TwistBinarySensor.py,sha256=k9gsF1dTR3E_kiB60R1SUuUc9oUGGRGs592fQqy2VPM,1747
4
+ twist/TwistButton.py,sha256=p5svcsU6maGwyWa06NnlKCGUXHoFzuwFPEQI0UqoGB4,2172
5
+ twist/TwistDevice.py,sha256=-ttj9LrZhR3BwLK4ADSe2TrTtY5Z3GMkz3BruUP_jCU,1159
6
+ twist/TwistGarage.py,sha256=RaE0Pi-1dE9FwHEIANf--Uig9tFt6EAZBt_lxjZgk18,2149
7
+ twist/TwistLight.py,sha256=6f-gPfP6v7BgaRJUFt7Q3r990y2L0812d76K_33KSTw,3230
8
+ twist/TwistModel.py,sha256=HKDDm-vaRiNEF-a67N-cjWw2UZDiSPSRMkEmlAIUPTg,3667
9
+ twist/TwistRelay.py,sha256=VL9eKXSOoZ_9GHT8TGZKDU1hGH-cgoogoMmJWLfjOqc,2309
10
+ twist/TwistRgb.py,sha256=V7YuaqJaLTH3cL8CsQ3xIztfVKfqdMMJ7uyh9UZQDwA,3572
11
+ twist/TwistSensor.py,sha256=6eYrwtZFFFOHmoHS68JHmsyi6FVVzoQy19KooGU0vMI,1192
12
+ twist/TwistShutter.py,sha256=gXMbgV_Y-3GLzxE0K6XRP6qqDHEw0uySjZc82IOMsYY,3328
13
+ twist/TwistTypes.py,sha256=k1CdLqjhJi8MyrFZsHkDsEoL9gEXF-HQv6I-r3G7ngA,726
14
+ twist/__init__.py,sha256=gf8GHc9utiG0vD8GP3IfzLFHAG-ev1TXBIm0XUHxy38,201
15
+ twist_innovation_api-0.0.5.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
16
+ twist_innovation_api-0.0.5.dist-info/METADATA,sha256=qd3IkTYqKU2qVxeqaSltdVRQCKMJd_gdCu5zXXvzk5o,4139
17
+ twist_innovation_api-0.0.5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
18
+ twist_innovation_api-0.0.5.dist-info/top_level.txt,sha256=mkoeBkRPFodjnd-3UDf9YndjTQ6Sriz-EKSI4nQ7brs,6
19
+ twist_innovation_api-0.0.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: bdist_wheel (0.42.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
twist/Variants.py DELETED
@@ -1,29 +0,0 @@
1
- # Copyright (C) 2025 Twist Innovation
2
- # This program is free software: you can redistribute it and/or modify
3
- # it under the terms of the GNU General Public License as published by
4
- # the Free Software Foundation, either version 3 of the License, or
5
- # (at your option) any later version.
6
- #
7
- # This program is distributed in the hope that it will be useful,
8
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
- #
11
- # See the GNU General Public License for more details:
12
- # https://www.gnu.org/licenses/gpl-3.0.html
13
-
14
- from .TwistLight import TwistLight
15
- from .TwistLouvre import TwistLouvre
16
- from .TwistRgb import TwistRgb
17
- from .TwistSensor import TwistSensor
18
- from .TwistCbShutter import TwistCbShutter
19
- from .TwistTypes import DeviceVariant
20
-
21
- model_dict = {
22
- DeviceVariant.LOUVRE_2_MONO_LIGHT_4: [TwistLouvre, TwistLouvre, TwistLight, TwistLight, TwistLight,
23
- TwistLight],
24
- DeviceVariant.RGB_1: [TwistRgb],
25
- DeviceVariant.MONO_LIGHT_4: [TwistLight, TwistLight, TwistLight, TwistLight],
26
- DeviceVariant.GATEWAY_1_WEATHER_1_TEMPERATURE_1_WIND_1_RAIN_1: [TwistSensor, TwistSensor, TwistSensor,
27
- TwistSensor, TwistSensor],
28
- DeviceVariant.CBSHUTTER_1_MONO_LIGHT_1: [TwistCbShutter, TwistLight],
29
- }
@@ -1,16 +0,0 @@
1
- twist/TwistAPI.py,sha256=LBLBGgwY66lVBNSto4VjPEpzj9kmDzXTCwpW9f3OSic,3579
2
- twist/TwistCbShutter.py,sha256=mWQa4ro30uI8qm0J1rkR8M0KP7IwxlCRfWt0wSyCOLA,1199
3
- twist/TwistDevice.py,sha256=PoPKnNrbiy1j55MyZJW4nDg9OXtGZI7CgHlxcDEQhqs,1380
4
- twist/TwistLight.py,sha256=6f-gPfP6v7BgaRJUFt7Q3r990y2L0812d76K_33KSTw,3230
5
- twist/TwistLouvre.py,sha256=f2ocRjLkf3qC45hbcHdEOboA2mbkaxLaOa15tvkHOHY,3320
6
- twist/TwistModel.py,sha256=SQHpYmpdV7PfZij69-TIHDa_77QabamZ71f9ZUeyvOk,3179
7
- twist/TwistRgb.py,sha256=V7YuaqJaLTH3cL8CsQ3xIztfVKfqdMMJ7uyh9UZQDwA,3572
8
- twist/TwistSensor.py,sha256=6eYrwtZFFFOHmoHS68JHmsyi6FVVzoQy19KooGU0vMI,1192
9
- twist/TwistTypes.py,sha256=wsp-lnkaU70wNdoKWOWcnsq89vBheBYmM31ZXW3FMRw,2166
10
- twist/Variants.py,sha256=SFAZ7bSmTRomPspOfU1qNWEW-htRuyhTKuyGKK9Qh54,1369
11
- twist/__init__.py,sha256=gf8GHc9utiG0vD8GP3IfzLFHAG-ev1TXBIm0XUHxy38,201
12
- twist_innovation_api-0.0.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
13
- twist_innovation_api-0.0.3.dist-info/METADATA,sha256=aquMRxNsC_ZBZJLCrulRkXUxDfTyG4mxub0WDOgmBXo,4335
14
- twist_innovation_api-0.0.3.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
15
- twist_innovation_api-0.0.3.dist-info/top_level.txt,sha256=mkoeBkRPFodjnd-3UDf9YndjTQ6Sriz-EKSI4nQ7brs,6
16
- twist_innovation_api-0.0.3.dist-info/RECORD,,