twist-innovation-api 0.0.3__py3-none-any.whl → 0.0.4__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.
- twist/BackendAdapter.py +295 -0
- twist/TwistAPI.py +87 -13
- twist/TwistBinarySensor.py +48 -0
- twist/TwistButton.py +60 -0
- twist/TwistDevice.py +3 -12
- twist/TwistGarage.py +65 -0
- twist/TwistModel.py +15 -0
- twist/TwistRelay.py +68 -0
- twist/TwistTypes.py +0 -49
- {twist_innovation_api-0.0.3.dist-info → twist_innovation_api-0.0.4.dist-info}/METADATA +2 -11
- twist_innovation_api-0.0.4.dist-info/RECORD +20 -0
- {twist_innovation_api-0.0.3.dist-info → twist_innovation_api-0.0.4.dist-info}/WHEEL +1 -1
- twist/Variants.py +0 -29
- twist_innovation_api-0.0.3.dist-info/RECORD +0 -16
- {twist_innovation_api-0.0.3.dist-info/licenses → twist_innovation_api-0.0.4.dist-info}/LICENSE +0 -0
- {twist_innovation_api-0.0.3.dist-info → twist_innovation_api-0.0.4.dist-info}/top_level.txt +0 -0
twist/BackendAdapter.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
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 .TwistLouvre import TwistLouvre
|
|
28
|
+
from .TwistRgb import TwistRgb
|
|
29
|
+
from .TwistSensor import TwistSensor
|
|
30
|
+
from .TwistCbShutter import TwistCbShutter
|
|
31
|
+
from .TwistGarage import TwistGarage
|
|
32
|
+
from .TwistRelay import TwistRelay
|
|
33
|
+
from .TwistButton import TwistButton
|
|
34
|
+
from .TwistBinarySensor import TwistBinarySensor
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Type mapping: maps backend type strings to Twist model classes
|
|
38
|
+
# Add new mappings here when new device types are added
|
|
39
|
+
TYPE_TO_MODEL_MAP: Dict[str, Type[TwistModel]] = {
|
|
40
|
+
"Mono_Light": TwistLight,
|
|
41
|
+
"TB_Shutter": TwistLouvre,
|
|
42
|
+
"Louvre": TwistLouvre,
|
|
43
|
+
"RGB": TwistRgb,
|
|
44
|
+
"Sensor": TwistSensor,
|
|
45
|
+
"CB_Shutter": TwistCbShutter,
|
|
46
|
+
"PB_Shutter": TwistCbShutter,
|
|
47
|
+
"Garage": TwistGarage,
|
|
48
|
+
"Relay": TwistRelay,
|
|
49
|
+
"Button": TwistButton,
|
|
50
|
+
"Binary_Sensor": TwistBinarySensor,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DeviceInfo:
|
|
55
|
+
"""Container for device information from backend"""
|
|
56
|
+
def __init__(self, device_id: int, model_id: int, name: str, device_type: str,
|
|
57
|
+
device_key: str, device_allocation_model_key: Optional[str] = None):
|
|
58
|
+
self.device_id = device_id
|
|
59
|
+
self.model_id = model_id
|
|
60
|
+
self.name = name
|
|
61
|
+
self.device_type = device_type
|
|
62
|
+
self.device_key = device_key
|
|
63
|
+
self.device_allocation_model_key = device_allocation_model_key
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ProductModelInfo:
|
|
67
|
+
"""Container for product model information"""
|
|
68
|
+
def __init__(self, device_key: str, device_model_index: int, product_name: str,
|
|
69
|
+
product_model_label: str, product_model_alias: str):
|
|
70
|
+
self.device_key = device_key
|
|
71
|
+
self.device_model_index = device_model_index
|
|
72
|
+
self.product_name = product_name
|
|
73
|
+
self.product_model_label = product_model_label
|
|
74
|
+
self.product_model_alias = product_model_alias
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class BackendAdapter:
|
|
78
|
+
"""
|
|
79
|
+
Adapter for fetching device information from backend server.
|
|
80
|
+
|
|
81
|
+
If the backend API changes, only this class needs to be updated.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, base_url: str, api_key: str, installation_uuid: str):
|
|
85
|
+
"""
|
|
86
|
+
Initialize backend adapter
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
base_url: Base URL of the backend server (e.g., "https://backbone-dev.twist-innovation.com")
|
|
90
|
+
api_key: API key for authentication (X-Api-Key header)
|
|
91
|
+
installation_uuid: Installation UUID for the API endpoint
|
|
92
|
+
"""
|
|
93
|
+
self.base_url = base_url.rstrip('/')
|
|
94
|
+
self.api_key = api_key
|
|
95
|
+
self.installation_uuid = installation_uuid
|
|
96
|
+
|
|
97
|
+
async def fetch_products(self) -> dict[str, ProductModelInfo]:
|
|
98
|
+
"""
|
|
99
|
+
Fetch product information from backend server
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dictionary mapping deviceAllocationModelKey -> ProductModelInfo
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
aiohttp.ClientError: If request fails
|
|
106
|
+
"""
|
|
107
|
+
url = f"{self.base_url}/installations/V1/installations/{self.installation_uuid}/products"
|
|
108
|
+
headers = {
|
|
109
|
+
"accept": "text/plain",
|
|
110
|
+
"X-Api-Key": self.api_key
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async with aiohttp.ClientSession() as session:
|
|
114
|
+
async with session.get(url, headers=headers) as response:
|
|
115
|
+
response.raise_for_status()
|
|
116
|
+
data = await response.json()
|
|
117
|
+
|
|
118
|
+
return self._parse_products(data)
|
|
119
|
+
|
|
120
|
+
def _parse_products(self, data: dict) -> dict[str, ProductModelInfo]:
|
|
121
|
+
"""
|
|
122
|
+
Parse products response into dictionary for easy lookup by model key
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Dictionary mapping deviceAllocationModelKey -> ProductModelInfo
|
|
126
|
+
"""
|
|
127
|
+
result = {}
|
|
128
|
+
|
|
129
|
+
for product in data.get("results", []):
|
|
130
|
+
product_name = product.get("name", "")
|
|
131
|
+
|
|
132
|
+
for model in product.get("models", []):
|
|
133
|
+
model_key = model.get("key")
|
|
134
|
+
device_key = model.get("deviceKey")
|
|
135
|
+
device_model_index = model.get("deviceModelIndex")
|
|
136
|
+
product_model_label = model.get("productModelLabel", "")
|
|
137
|
+
product_model_alias = model.get("productModelAlias", "")
|
|
138
|
+
|
|
139
|
+
if model_key:
|
|
140
|
+
result[model_key] = ProductModelInfo(
|
|
141
|
+
device_key=device_key,
|
|
142
|
+
device_model_index=device_model_index,
|
|
143
|
+
product_name=product_name,
|
|
144
|
+
product_model_label=product_model_label,
|
|
145
|
+
product_model_alias=product_model_alias
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
async def fetch_installation_id(self) -> int:
|
|
151
|
+
"""
|
|
152
|
+
Fetch installation ID from backend server
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Installation ID
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
aiohttp.ClientError: If request fails
|
|
159
|
+
ValueError: If response format is invalid
|
|
160
|
+
"""
|
|
161
|
+
url = f"{self.base_url}/installations/V1/installations/{self.installation_uuid}"
|
|
162
|
+
headers = {
|
|
163
|
+
"accept": "text/plain",
|
|
164
|
+
"X-Api-Key": self.api_key
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async with aiohttp.ClientSession() as session:
|
|
168
|
+
async with session.get(url, headers=headers) as response:
|
|
169
|
+
response.raise_for_status()
|
|
170
|
+
data = await response.json()
|
|
171
|
+
|
|
172
|
+
installation_id = data.get("installation", {}).get("installationId")
|
|
173
|
+
if installation_id is None:
|
|
174
|
+
raise ValueError("Missing 'installationId' in installation response")
|
|
175
|
+
|
|
176
|
+
return installation_id
|
|
177
|
+
|
|
178
|
+
async def fetch_config(self) -> list[DeviceInfo]:
|
|
179
|
+
"""
|
|
180
|
+
Fetch device configuration from backend server
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
List of DeviceInfo objects
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
aiohttp.ClientError: If request fails
|
|
187
|
+
ValueError: If response format is invalid
|
|
188
|
+
"""
|
|
189
|
+
url = f"{self.base_url}/installations/V1/installations/{self.installation_uuid}/devices"
|
|
190
|
+
headers = {
|
|
191
|
+
"accept": "text/plain",
|
|
192
|
+
"X-Api-Key": self.api_key
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async with aiohttp.ClientSession() as session:
|
|
196
|
+
async with session.get(url, headers=headers) as response:
|
|
197
|
+
response.raise_for_status()
|
|
198
|
+
data = await response.json()
|
|
199
|
+
|
|
200
|
+
return self._parse_response(data)
|
|
201
|
+
|
|
202
|
+
def _parse_response(self, data: dict) -> list[DeviceInfo]:
|
|
203
|
+
"""
|
|
204
|
+
Parse backend response into device info list
|
|
205
|
+
|
|
206
|
+
This method contains the mapping logic for the current API format.
|
|
207
|
+
Update this method if the backend response format changes.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
data: JSON response from backend (new format with results array)
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
List of DeviceInfo objects
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
# Parse devices from new API format
|
|
217
|
+
results = data.get("results", [])
|
|
218
|
+
device_list = []
|
|
219
|
+
|
|
220
|
+
for device in results:
|
|
221
|
+
try:
|
|
222
|
+
twist_id = device.get("twistId")
|
|
223
|
+
device_key = device.get("key")
|
|
224
|
+
models = device.get("models", [])
|
|
225
|
+
|
|
226
|
+
# Process each model in the device
|
|
227
|
+
for model in models:
|
|
228
|
+
model_index = model.get("deviceModelIndex")
|
|
229
|
+
model_type_name = model.get("modelTypeName", "")
|
|
230
|
+
label = model.get("label", f"Model {model_index}")
|
|
231
|
+
device_allocation_model_key = model.get("deviceAllocationModelKey")
|
|
232
|
+
|
|
233
|
+
# Map model type name to device type string
|
|
234
|
+
device_type = self._map_model_type_to_device_type(model_type_name)
|
|
235
|
+
|
|
236
|
+
if device_type:
|
|
237
|
+
device_info = DeviceInfo(
|
|
238
|
+
device_id=twist_id,
|
|
239
|
+
model_id=model_index,
|
|
240
|
+
name=label,
|
|
241
|
+
device_type=device_type,
|
|
242
|
+
device_key=device_key,
|
|
243
|
+
device_allocation_model_key=device_allocation_model_key
|
|
244
|
+
)
|
|
245
|
+
device_list.append(device_info)
|
|
246
|
+
else:
|
|
247
|
+
print(f"Unknown model type '{model_type_name}' for device {twist_id}, model {model_index}")
|
|
248
|
+
|
|
249
|
+
except (KeyError, TypeError) as e:
|
|
250
|
+
print(f"Skipping device due to error: {e}")
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
return device_list
|
|
254
|
+
|
|
255
|
+
def _map_model_type_to_device_type(self, model_type_name: str) -> Optional[str]:
|
|
256
|
+
"""
|
|
257
|
+
Map the new API's modelTypeName to the device type string used internally
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
model_type_name: Model type name from the new API
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Device type string or None if unmapped
|
|
264
|
+
"""
|
|
265
|
+
# Mapping from new API model type names to internal device type strings
|
|
266
|
+
mapping = {
|
|
267
|
+
"Louvres": "Louvre",
|
|
268
|
+
"Mono Light": "Mono_Light",
|
|
269
|
+
"RGB": "RGB",
|
|
270
|
+
"Temperature": "Sensor",
|
|
271
|
+
"CB Shutter": "CB_Shutter",
|
|
272
|
+
"PB Shutter": "PB_Shutter",
|
|
273
|
+
"Garage": "Garage",
|
|
274
|
+
"Relay": "Relay",
|
|
275
|
+
"Button": "Button",
|
|
276
|
+
"Binary Sensor": "Binary_Sensor",
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return mapping.get(model_type_name)
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
def get_model_class(device_type: str) -> Type[TwistModel]:
|
|
283
|
+
"""
|
|
284
|
+
Get the appropriate model class for a device type.
|
|
285
|
+
|
|
286
|
+
This method maps backend device types to Twist model classes.
|
|
287
|
+
If a type is not recognized, it returns the base TwistModel class.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
device_type: Device type string from backend
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Appropriate TwistModel subclass
|
|
294
|
+
"""
|
|
295
|
+
return TYPE_TO_MODEL_MAP.get(device_type, TwistModel)
|
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 .
|
|
21
|
+
from .BackendAdapter import BackendAdapter, DeviceInfo, ProductModelInfo
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class TwistAPI:
|
|
24
|
-
def __init__(self,
|
|
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
|
-
#
|
|
36
|
-
self.
|
|
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,79 @@ class TwistAPI:
|
|
|
41
51
|
|
|
42
52
|
await self._subscribe(f"v2/{self.installation_id}/rx/#", self._on_message_received)
|
|
43
53
|
|
|
44
|
-
async def
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
50
|
-
|
|
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_class = BackendAdapter.get_model_class(info.device_type)
|
|
101
|
+
|
|
102
|
+
if model_class == TwistModel:
|
|
103
|
+
print(f"Skipping unknown device type '{info.device_type}' for '{info.name}' (device {info.device_id}, model {info.model_id})")
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
model = model_class(info.model_id, device)
|
|
107
|
+
|
|
108
|
+
# Get product info if available using deviceAllocationModelKey
|
|
109
|
+
product_info = None
|
|
110
|
+
if info.device_allocation_model_key:
|
|
111
|
+
product_info = products.get(info.device_allocation_model_key)
|
|
112
|
+
|
|
113
|
+
if product_info:
|
|
114
|
+
model.product_name = product_info.product_name
|
|
115
|
+
model.name = product_info.product_model_label or product_info.product_model_alias or info.name
|
|
116
|
+
else:
|
|
117
|
+
model.product_name = None
|
|
118
|
+
model.name = info.name
|
|
119
|
+
|
|
120
|
+
model.device_type = info.device_type
|
|
121
|
+
|
|
122
|
+
# Add model at correct position in model_list
|
|
123
|
+
while len(device.model_list) <= info.model_id:
|
|
124
|
+
device.model_list.append(None)
|
|
125
|
+
device.model_list[info.model_id] = model
|
|
126
|
+
|
|
51
127
|
model_list.append(model)
|
|
52
128
|
|
|
53
129
|
return model_list
|
|
@@ -93,7 +169,5 @@ class TwistAPI:
|
|
|
93
169
|
await device.context_msg(model_id, payload)
|
|
94
170
|
|
|
95
171
|
async def _get_board(self, twist_id, payload, model_id=None):
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
self.device_list.append(TwistDevice(twist_id, data["h"], DeviceVariant(data["v"]), self))
|
|
99
|
-
print(f"New device with id: {twist_id}")
|
|
172
|
+
"""Handle getboard MQTT message (legacy compatibility)"""
|
|
173
|
+
pass
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
|
|
23
|
+
|
|
24
|
+
class TwistBinarySensor(TwistModel):
|
|
25
|
+
def __init__(self, model_id: int, parent_device: TwistDevice):
|
|
26
|
+
super().__init__(model_id, parent_device)
|
|
27
|
+
|
|
28
|
+
async def context_msg(self, payload: str):
|
|
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]
|
|
36
|
+
|
|
37
|
+
if self._update_callback is not None:
|
|
38
|
+
await self._update_callback(self)
|
|
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
|
+
|
|
45
|
+
def print_context(self):
|
|
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,
|
|
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
|
-
|
|
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}")
|
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.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: twist-innovation-api
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
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,20 @@
|
|
|
1
|
+
twist/BackendAdapter.py,sha256=vvFomOgBiyKOZJ_n565AdHzdCgB-hNEU9UVDRONdhZc,10509
|
|
2
|
+
twist/TwistAPI.py,sha256=0wwIiJRwV92tVoLdSJ6NYnZHu-UAPAVYZ7jaGsLpQq0,6477
|
|
3
|
+
twist/TwistBinarySensor.py,sha256=k9gsF1dTR3E_kiB60R1SUuUc9oUGGRGs592fQqy2VPM,1747
|
|
4
|
+
twist/TwistButton.py,sha256=p5svcsU6maGwyWa06NnlKCGUXHoFzuwFPEQI0UqoGB4,2172
|
|
5
|
+
twist/TwistCbShutter.py,sha256=mWQa4ro30uI8qm0J1rkR8M0KP7IwxlCRfWt0wSyCOLA,1199
|
|
6
|
+
twist/TwistDevice.py,sha256=-ttj9LrZhR3BwLK4ADSe2TrTtY5Z3GMkz3BruUP_jCU,1159
|
|
7
|
+
twist/TwistGarage.py,sha256=RaE0Pi-1dE9FwHEIANf--Uig9tFt6EAZBt_lxjZgk18,2149
|
|
8
|
+
twist/TwistLight.py,sha256=6f-gPfP6v7BgaRJUFt7Q3r990y2L0812d76K_33KSTw,3230
|
|
9
|
+
twist/TwistLouvre.py,sha256=f2ocRjLkf3qC45hbcHdEOboA2mbkaxLaOa15tvkHOHY,3320
|
|
10
|
+
twist/TwistModel.py,sha256=HKDDm-vaRiNEF-a67N-cjWw2UZDiSPSRMkEmlAIUPTg,3667
|
|
11
|
+
twist/TwistRelay.py,sha256=VL9eKXSOoZ_9GHT8TGZKDU1hGH-cgoogoMmJWLfjOqc,2309
|
|
12
|
+
twist/TwistRgb.py,sha256=V7YuaqJaLTH3cL8CsQ3xIztfVKfqdMMJ7uyh9UZQDwA,3572
|
|
13
|
+
twist/TwistSensor.py,sha256=6eYrwtZFFFOHmoHS68JHmsyi6FVVzoQy19KooGU0vMI,1192
|
|
14
|
+
twist/TwistTypes.py,sha256=k1CdLqjhJi8MyrFZsHkDsEoL9gEXF-HQv6I-r3G7ngA,726
|
|
15
|
+
twist/__init__.py,sha256=gf8GHc9utiG0vD8GP3IfzLFHAG-ev1TXBIm0XUHxy38,201
|
|
16
|
+
twist_innovation_api-0.0.4.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
17
|
+
twist_innovation_api-0.0.4.dist-info/METADATA,sha256=c5eSo06IcHRdW1vTHfXxQdyEYcrUtUctP98MaL8Lx8c,4139
|
|
18
|
+
twist_innovation_api-0.0.4.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
19
|
+
twist_innovation_api-0.0.4.dist-info/top_level.txt,sha256=mkoeBkRPFodjnd-3UDf9YndjTQ6Sriz-EKSI4nQ7brs,6
|
|
20
|
+
twist_innovation_api-0.0.4.dist-info/RECORD,,
|
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,,
|
{twist_innovation_api-0.0.3.dist-info/licenses → twist_innovation_api-0.0.4.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|