twist-innovation-api 0.0.3__tar.gz → 0.0.5__tar.gz
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_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/PKG-INFO +3 -12
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/setup.py +3 -3
- twist_innovation_api-0.0.5/twist/BackendAdapter.py +267 -0
- twist_innovation_api-0.0.5/twist/TwistAPI.py +165 -0
- twist_innovation_api-0.0.3/twist/TwistCbShutter.py → twist_innovation_api-0.0.5/twist/TwistBinarySensor.py +18 -3
- twist_innovation_api-0.0.5/twist/TwistButton.py +60 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist/TwistDevice.py +3 -12
- twist_innovation_api-0.0.5/twist/TwistGarage.py +65 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist/TwistModel.py +15 -0
- twist_innovation_api-0.0.5/twist/TwistRelay.py +68 -0
- twist_innovation_api-0.0.3/twist/TwistLouvre.py → twist_innovation_api-0.0.5/twist/TwistShutter.py +8 -8
- twist_innovation_api-0.0.5/twist/TwistTypes.py +23 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist_innovation_api.egg-info/PKG-INFO +2 -11
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist_innovation_api.egg-info/SOURCES.txt +6 -3
- twist_innovation_api-0.0.3/twist/TwistAPI.py +0 -99
- twist_innovation_api-0.0.3/twist/TwistTypes.py +0 -72
- twist_innovation_api-0.0.3/twist/Variants.py +0 -29
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/LICENSE +0 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/README.md +0 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/pyproject.toml +0 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/setup.cfg +0 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist/TwistLight.py +0 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist/TwistRgb.py +0 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist/TwistSensor.py +0 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist/__init__.py +0 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist_innovation_api.egg-info/dependency_links.txt +0 -0
- {twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist_innovation_api.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name:
|
|
3
|
-
Version: 0.0.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: twist_innovation_api
|
|
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
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from setuptools import setup, find_packages
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
|
-
name="
|
|
5
|
-
version="0.0.
|
|
4
|
+
name="twist_innovation_api",
|
|
5
|
+
version="0.0.5",
|
|
6
6
|
packages=find_packages(),
|
|
7
7
|
install_requires=[
|
|
8
8
|
# "requests" # For REST API
|
|
@@ -19,4 +19,4 @@ setup(
|
|
|
19
19
|
"Operating System :: OS Independent",
|
|
20
20
|
],
|
|
21
21
|
python_requires=">=3.7",
|
|
22
|
-
)
|
|
22
|
+
)
|
|
@@ -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)
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
import random
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from .TwistDevice import TwistDevice, TwistModel
|
|
21
|
+
from .BackendAdapter import BackendAdapter, DeviceInfo, ProductModelInfo
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TwistAPI:
|
|
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
|
+
"""
|
|
34
|
+
self._ext_publish = None
|
|
35
|
+
self._subscribe = None
|
|
36
|
+
|
|
37
|
+
self.function_map = {
|
|
38
|
+
"context": self._context_msg,
|
|
39
|
+
"getboard": self._get_board
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
self.device_list: list[TwistDevice] = list()
|
|
43
|
+
self.installation_id: Optional[int] = None
|
|
44
|
+
|
|
45
|
+
# Backend adapter for fetching device info
|
|
46
|
+
self._backend_adapter = BackendAdapter(backend_url, api_key, installation_uuid)
|
|
47
|
+
|
|
48
|
+
async def add_mqtt(self, publisher, subscriber):
|
|
49
|
+
self._ext_publish = publisher
|
|
50
|
+
self._subscribe = subscriber
|
|
51
|
+
|
|
52
|
+
await self._subscribe(f"v2/{self.installation_id}/rx/#", self._on_message_received)
|
|
53
|
+
|
|
54
|
+
async def get_models(self):
|
|
55
|
+
"""
|
|
56
|
+
Get models from backend
|
|
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
|
|
87
|
+
model_list: list[TwistModel] = 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
|
+
|
|
119
|
+
model_list.append(model)
|
|
120
|
+
|
|
121
|
+
return model_list
|
|
122
|
+
|
|
123
|
+
async def _publish(self, twist_id, opcode, payload: dict | None = None, model_id=None):
|
|
124
|
+
if self._ext_publish is not None:
|
|
125
|
+
topic = f"v2/{self.installation_id}/tx/{twist_id}/{opcode}"
|
|
126
|
+
if payload is None:
|
|
127
|
+
payload = dict()
|
|
128
|
+
payload["key"] = random.randint(1, 65535)
|
|
129
|
+
if model_id is not None:
|
|
130
|
+
topic = f"{topic}/{model_id}"
|
|
131
|
+
|
|
132
|
+
await self._ext_publish(topic, json.dumps(payload))
|
|
133
|
+
|
|
134
|
+
async def getboard(self, twist_id):
|
|
135
|
+
await self._publish(twist_id, "getboard")
|
|
136
|
+
|
|
137
|
+
async def activate_event(self, model: TwistModel, data: json):
|
|
138
|
+
await self._publish(model.parent_device.twist_id, "activate_event", data, model.model_id)
|
|
139
|
+
|
|
140
|
+
def _parse_topic(self, topic):
|
|
141
|
+
tpc_delim = topic.split('/')
|
|
142
|
+
|
|
143
|
+
model_id = None
|
|
144
|
+
if len(tpc_delim) == 6:
|
|
145
|
+
model_id = int(tpc_delim[5])
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"twist_id": int(tpc_delim[3]),
|
|
149
|
+
"opcode": tpc_delim[4],
|
|
150
|
+
"model_id": model_id
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async def _on_message_received(self, topic, payload, qos=None):
|
|
154
|
+
data = self._parse_topic(topic)
|
|
155
|
+
if any(dev.twist_id == data["twist_id"] for dev in self.device_list) or data["opcode"] == "getboard":
|
|
156
|
+
if data["opcode"] in self.function_map:
|
|
157
|
+
await self.function_map[data["opcode"]](data["twist_id"], payload, data["model_id"])
|
|
158
|
+
|
|
159
|
+
async def _context_msg(self, twist_id, payload, model_id):
|
|
160
|
+
device = next((d for d in self.device_list if d.twist_id == twist_id), None)
|
|
161
|
+
await device.context_msg(model_id, payload)
|
|
162
|
+
|
|
163
|
+
async def _get_board(self, twist_id, payload, model_id=None):
|
|
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
|
|
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
|
-
|
|
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}")
|
|
@@ -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}")
|
|
@@ -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)
|
|
@@ -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}")
|
|
@@ -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")
|
|
@@ -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_innovation_api-0.0.3/twist/TwistLouvre.py → twist_innovation_api-0.0.5/twist/TwistShutter.py
RENAMED
|
@@ -22,7 +22,7 @@ from .TwistDevice import TwistModel
|
|
|
22
22
|
from enum import Enum
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
class
|
|
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(
|
|
58
|
+
await self._activate_event(TwistShutter.EventIndexes.OPEN)
|
|
59
59
|
|
|
60
60
|
async def stop(self):
|
|
61
|
-
await self._activate_event(
|
|
61
|
+
await self._activate_event(TwistShutter.EventIndexes.STOP)
|
|
62
62
|
|
|
63
63
|
async def close(self):
|
|
64
|
-
await self._activate_event(
|
|
64
|
+
await self._activate_event(TwistShutter.EventIndexes.CLOSE)
|
|
65
65
|
|
|
66
66
|
async def toggle(self):
|
|
67
|
-
await self._activate_event(
|
|
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(
|
|
73
|
+
await self._activate_event(TwistShutter.EventIndexes.VALUE, int(value * 655.35))
|
|
74
74
|
|
|
75
|
-
async def _activate_event(self, index:
|
|
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"
|
|
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}")
|
|
@@ -0,0 +1,23 @@
|
|
|
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 enum import Enum
|
|
15
|
+
|
|
16
|
+
class ContextErrors(Enum):
|
|
17
|
+
ERROR = 0
|
|
18
|
+
WARNING = 1
|
|
19
|
+
INFO = 2
|
|
20
|
+
PRIO = 3
|
|
21
|
+
ACTUAL = 4
|
|
22
|
+
REQUESTED = 5
|
|
23
|
+
MAX = 6
|
{twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist_innovation_api.egg-info/PKG-INFO
RENAMED
|
@@ -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.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
|
|
{twist_innovation_api-0.0.3 → twist_innovation_api-0.0.5}/twist_innovation_api.egg-info/SOURCES.txt
RENAMED
|
@@ -2,16 +2,19 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
setup.py
|
|
5
|
+
twist/BackendAdapter.py
|
|
5
6
|
twist/TwistAPI.py
|
|
6
|
-
twist/
|
|
7
|
+
twist/TwistBinarySensor.py
|
|
8
|
+
twist/TwistButton.py
|
|
7
9
|
twist/TwistDevice.py
|
|
10
|
+
twist/TwistGarage.py
|
|
8
11
|
twist/TwistLight.py
|
|
9
|
-
twist/TwistLouvre.py
|
|
10
12
|
twist/TwistModel.py
|
|
13
|
+
twist/TwistRelay.py
|
|
11
14
|
twist/TwistRgb.py
|
|
12
15
|
twist/TwistSensor.py
|
|
16
|
+
twist/TwistShutter.py
|
|
13
17
|
twist/TwistTypes.py
|
|
14
|
-
twist/Variants.py
|
|
15
18
|
twist/__init__.py
|
|
16
19
|
twist_innovation_api.egg-info/PKG-INFO
|
|
17
20
|
twist_innovation_api.egg-info/SOURCES.txt
|
|
@@ -1,99 +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
|
-
|
|
15
|
-
import asyncio
|
|
16
|
-
import json
|
|
17
|
-
import random
|
|
18
|
-
|
|
19
|
-
from .TwistDevice import TwistDevice, TwistModel
|
|
20
|
-
from .TwistTypes import DeviceVariant
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class TwistAPI:
|
|
24
|
-
def __init__(self, installation_id: int):
|
|
25
|
-
self._ext_publish = None
|
|
26
|
-
self._subscribe = None
|
|
27
|
-
|
|
28
|
-
self.function_map = {
|
|
29
|
-
"context": self._context_msg,
|
|
30
|
-
"getboard": self._get_board
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
self.device_list: list[TwistDevice] = list()
|
|
34
|
-
|
|
35
|
-
# TODO: this should come from the API
|
|
36
|
-
self.installation_id = installation_id
|
|
37
|
-
|
|
38
|
-
async def add_mqtt(self, publisher, subscriber):
|
|
39
|
-
self._ext_publish = publisher
|
|
40
|
-
self._subscribe = subscriber
|
|
41
|
-
|
|
42
|
-
await self._subscribe(f"v2/{self.installation_id}/rx/#", self._on_message_received)
|
|
43
|
-
|
|
44
|
-
async def search_models(self):
|
|
45
|
-
await self.getboard(0xffffffff)
|
|
46
|
-
await asyncio.sleep(3)
|
|
47
|
-
|
|
48
|
-
model_list: list[TwistModel] = list()
|
|
49
|
-
for device in self.device_list:
|
|
50
|
-
for model in device.model_list:
|
|
51
|
-
model_list.append(model)
|
|
52
|
-
|
|
53
|
-
return model_list
|
|
54
|
-
|
|
55
|
-
async def _publish(self, twist_id, opcode, payload: dict | None = None, model_id=None):
|
|
56
|
-
if self._ext_publish is not None:
|
|
57
|
-
topic = f"v2/{self.installation_id}/tx/{twist_id}/{opcode}"
|
|
58
|
-
if payload is None:
|
|
59
|
-
payload = dict()
|
|
60
|
-
payload["key"] = random.randint(1, 65535)
|
|
61
|
-
if model_id is not None:
|
|
62
|
-
topic = f"{topic}/{model_id}"
|
|
63
|
-
|
|
64
|
-
await self._ext_publish(topic, json.dumps(payload))
|
|
65
|
-
|
|
66
|
-
async def getboard(self, twist_id):
|
|
67
|
-
await self._publish(twist_id, "getboard")
|
|
68
|
-
|
|
69
|
-
async def activate_event(self, model: TwistModel, data: json):
|
|
70
|
-
await self._publish(model.parent_device.twist_id, "activate_event", data, model.model_id)
|
|
71
|
-
|
|
72
|
-
def _parse_topic(self, topic):
|
|
73
|
-
tpc_delim = topic.split('/')
|
|
74
|
-
|
|
75
|
-
model_id = None
|
|
76
|
-
if len(tpc_delim) == 6:
|
|
77
|
-
model_id = int(tpc_delim[5])
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
"twist_id": int(tpc_delim[3]),
|
|
81
|
-
"opcode": tpc_delim[4],
|
|
82
|
-
"model_id": model_id
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async def _on_message_received(self, topic, payload, qos=None):
|
|
86
|
-
data = self._parse_topic(topic)
|
|
87
|
-
if any(dev.twist_id == data["twist_id"] for dev in self.device_list) or data["opcode"] == "getboard":
|
|
88
|
-
if data["opcode"] in self.function_map:
|
|
89
|
-
await self.function_map[data["opcode"]](data["twist_id"], payload, data["model_id"])
|
|
90
|
-
|
|
91
|
-
async def _context_msg(self, twist_id, payload, model_id):
|
|
92
|
-
device = next((d for d in self.device_list if d.twist_id == twist_id), None)
|
|
93
|
-
await device.context_msg(model_id, payload)
|
|
94
|
-
|
|
95
|
-
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}")
|
|
@@ -1,72 +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 enum import Enum
|
|
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
|
-
class ContextErrors(Enum):
|
|
66
|
-
ERROR = 0
|
|
67
|
-
WARNING = 1
|
|
68
|
-
INFO = 2
|
|
69
|
-
PRIO = 3
|
|
70
|
-
ACTUAL = 4
|
|
71
|
-
REQUESTED = 5
|
|
72
|
-
MAX = 6
|
|
@@ -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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|