pyControl4 1.5.0__tar.gz → 1.6.0__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.
Files changed (22) hide show
  1. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/PKG-INFO +2 -2
  2. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/account.py +7 -8
  3. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/alarm.py +10 -10
  4. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/director.py +9 -17
  5. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/light.py +30 -0
  6. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/websocket.py +44 -24
  7. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4.egg-info/PKG-INFO +2 -2
  8. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/setup.py +2 -2
  9. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/LICENSE +0 -0
  10. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/README.md +0 -0
  11. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/__init__.py +0 -0
  12. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/blind.py +0 -0
  13. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/climate.py +0 -0
  14. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/error_handling.py +0 -0
  15. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/fan.py +0 -0
  16. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/relay.py +0 -0
  17. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4/room.py +0 -0
  18. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4.egg-info/SOURCES.txt +0 -0
  19. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4.egg-info/dependency_links.txt +0 -0
  20. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4.egg-info/requires.txt +0 -0
  21. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/pyControl4.egg-info/top_level.txt +0 -0
  22. {pycontrol4-1.5.0 → pycontrol4-1.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyControl4
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Python 3 asyncio package for interacting with Control4 systems
5
5
  Home-page: https://github.com/lawtancool/pyControl4
6
6
  Author: lawtancool
@@ -8,7 +8,7 @@ Author-email: contact@lawrencetan.ca
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: License :: OSI Approved :: Apache Software License
10
10
  Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.6
11
+ Requires-Python: >=3.11
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: aiohttp
@@ -3,10 +3,9 @@ controller info, and retrieves a bearer token for connecting to a Control4 Direc
3
3
  """
4
4
 
5
5
  import aiohttp
6
- import async_timeout
6
+ import asyncio
7
7
  import json
8
8
  import logging
9
- import datetime
10
9
 
11
10
  from .error_handling import checkResponseForError
12
11
 
@@ -64,14 +63,14 @@ class C4Account:
64
63
  }
65
64
  if self.session is None:
66
65
  async with aiohttp.ClientSession() as session:
67
- with async_timeout.timeout(10):
66
+ async with asyncio.timeout(10):
68
67
  async with session.post(
69
68
  AUTHENTICATION_ENDPOINT, json=dataDictionary
70
69
  ) as resp:
71
70
  await checkResponseForError(await resp.text())
72
71
  return await resp.text()
73
72
  else:
74
- with async_timeout.timeout(10):
73
+ async with asyncio.timeout(10):
75
74
  async with self.session.post(
76
75
  AUTHENTICATION_ENDPOINT, json=dataDictionary
77
76
  ) as resp:
@@ -94,12 +93,12 @@ class C4Account:
94
93
  raise
95
94
  if self.session is None:
96
95
  async with aiohttp.ClientSession() as session:
97
- with async_timeout.timeout(10):
96
+ async with asyncio.timeout(10):
98
97
  async with session.get(uri, headers=headers) as resp:
99
98
  await checkResponseForError(await resp.text())
100
99
  return await resp.text()
101
100
  else:
102
- with async_timeout.timeout(10):
101
+ async with asyncio.timeout(10):
103
102
  async with self.session.get(uri, headers=headers) as resp:
104
103
  await checkResponseForError(await resp.text())
105
104
  return await resp.text()
@@ -125,7 +124,7 @@ class C4Account:
125
124
  }
126
125
  if self.session is None:
127
126
  async with aiohttp.ClientSession() as session:
128
- with async_timeout.timeout(10):
127
+ async with asyncio.timeout(10):
129
128
  async with session.post(
130
129
  CONTROLLER_AUTHORIZATION_ENDPOINT,
131
130
  headers=headers,
@@ -134,7 +133,7 @@ class C4Account:
134
133
  await checkResponseForError(await resp.text())
135
134
  return await resp.text()
136
135
  else:
137
- with async_timeout.timeout(10):
136
+ async with asyncio.timeout(10):
138
137
  async with self.session.post(
139
138
  CONTROLLER_AUTHORIZATION_ENDPOINT,
140
139
  headers=headers,
@@ -141,30 +141,30 @@ class C4SecurityPanel(C4Entity):
141
141
  data = await self.director.getItemInfo(self.item_id)
142
142
  jsonDictionary = json.loads(data)
143
143
 
144
- if jsonDictionary[0]["capabilities"]["has_fire"]:
144
+ capabilities = (
145
+ jsonDictionary[0].get("capabilities", {}) if jsonDictionary else {}
146
+ )
147
+ if capabilities.get("has_fire"):
145
148
  types_list.append("Fire")
146
- if jsonDictionary[0]["capabilities"]["has_medical"]:
149
+ if capabilities.get("has_medical"):
147
150
  types_list.append("Medical")
148
- if jsonDictionary[0]["capabilities"]["has_panic"]:
151
+ if capabilities.get("has_panic"):
149
152
  types_list.append("Panic")
150
- if jsonDictionary[0]["capabilities"]["has_police"]:
153
+ if capabilities.get("has_police"):
151
154
  types_list.append("Police")
152
155
 
153
156
  return types_list
154
157
 
155
- async def triggerEmergency(self, usercode, type):
158
+ async def triggerEmergency(self, type):
156
159
  """Triggers an emergency of the specified type.
157
160
 
158
161
  Parameters:
159
- `usercode` - PIN/code for disarming the system.
160
-
161
162
  `type` - Type of emergency: "Fire", "Medical", "Panic", or "Police"
162
163
  """
163
- usercode = str(usercode)
164
164
  await self.director.sendPostRequest(
165
165
  "/api/v1/items/{}/commands".format(self.item_id),
166
- "PARTITION_DISARM",
167
- {"UserCode": usercode},
166
+ "EXECUTE_EMERGENCY",
167
+ {"EmergencyType": type},
168
168
  )
169
169
 
170
170
  async def sendKeyPress(self, key):
@@ -3,7 +3,7 @@ getting details about items on the Director.
3
3
  """
4
4
 
5
5
  import aiohttp
6
- import async_timeout
6
+ import asyncio
7
7
  import json
8
8
 
9
9
  from .error_handling import checkResponseForError
@@ -50,14 +50,14 @@ class C4Director:
50
50
  async with aiohttp.ClientSession(
51
51
  connector=aiohttp.TCPConnector(verify_ssl=False)
52
52
  ) as session:
53
- with async_timeout.timeout(10):
53
+ async with asyncio.timeout(10):
54
54
  async with session.get(
55
55
  self.base_url + uri, headers=self.headers
56
56
  ) as resp:
57
57
  await checkResponseForError(await resp.text())
58
58
  return await resp.text()
59
59
  else:
60
- with async_timeout.timeout(10):
60
+ async with asyncio.timeout(10):
61
61
  async with self.session.get(
62
62
  self.base_url + uri, headers=self.headers
63
63
  ) as resp:
@@ -86,14 +86,14 @@ class C4Director:
86
86
  async with aiohttp.ClientSession(
87
87
  connector=aiohttp.TCPConnector(verify_ssl=False)
88
88
  ) as session:
89
- with async_timeout.timeout(10):
89
+ async with asyncio.timeout(10):
90
90
  async with session.post(
91
91
  self.base_url + uri, headers=self.headers, json=dataDictionary
92
92
  ) as resp:
93
93
  await checkResponseForError(await resp.text())
94
94
  return await resp.text()
95
95
  else:
96
- with async_timeout.timeout(10):
96
+ async with asyncio.timeout(10):
97
97
  async with self.session.post(
98
98
  self.base_url + uri, headers=self.headers, json=dataDictionary
99
99
  ) as resp:
@@ -162,12 +162,8 @@ class C4Director:
162
162
  "/api/v1/items/{}/variables?varnames={}".format(item_id, var_name)
163
163
  )
164
164
  if data == "[]":
165
- raise ValueError(
166
- "Empty response recieved from Director! The variable {} \
167
- doesn't seem to exist for item {}.".format(
168
- var_name, item_id
169
- )
170
- )
165
+ raise ValueError("Empty response recieved from Director! The variable {} \
166
+ doesn't seem to exist for item {}.".format(var_name, item_id))
171
167
  jsonDictionary = json.loads(data)
172
168
  return jsonDictionary[0]["value"]
173
169
 
@@ -185,12 +181,8 @@ class C4Director:
185
181
  "/api/v1/items/variables?varnames={}".format(var_name)
186
182
  )
187
183
  if data == "[]":
188
- raise ValueError(
189
- "Empty response recieved from Director! The variable {} \
190
- doesn't seem to exist for any items.".format(
191
- var_name
192
- )
193
- )
184
+ raise ValueError("Empty response recieved from Director! The variable {} \
185
+ doesn't seem to exist for any items.".format(var_name))
194
186
  jsonDictionary = json.loads(data)
195
187
  return jsonDictionary
196
188
 
@@ -43,3 +43,33 @@ class C4Light(C4Entity):
43
43
  "RAMP_TO_LEVEL",
44
44
  {"LEVEL": level, "TIME": time},
45
45
  )
46
+
47
+ async def setColorXY(self, x: float, y: float, *, rate: int | None = None):
48
+ """Sends SET_COLOR_TARGET with xy"""
49
+ params = {
50
+ "LIGHT_COLOR_TARGET_X": float(x),
51
+ "LIGHT_COLOR_TARGET_Y": float(y),
52
+ "LIGHT_COLOR_TARGET_MODE": 0,
53
+ }
54
+ if rate is not None:
55
+ params["RATE"] = int(rate)
56
+
57
+ await self.director.sendPostRequest(
58
+ f"/api/v1/items/{self.item_id}/commands",
59
+ "SET_COLOR_TARGET",
60
+ params,
61
+ )
62
+
63
+ async def setColorTemperature(self, kelvin: int, *, rate: int | None = None):
64
+ params = {
65
+ "LIGHT_COLOR_TARGET_COLOR_CORRELATED_TEMPERATURE": int(kelvin),
66
+ "LIGHT_COLOR_TARGET_MODE": 1,
67
+ }
68
+ if rate is not None:
69
+ params["RATE"] = int(rate)
70
+
71
+ await self.director.sendPostRequest(
72
+ f"/api/v1/items/{self.item_id}/commands",
73
+ "SET_COLOR_TARGET",
74
+ params,
75
+ )
@@ -1,7 +1,7 @@
1
1
  """Handles Websocket connections to a Control4 Director, allowing for real-time updates using callbacks."""
2
2
 
3
3
  import aiohttp
4
- import async_timeout
4
+ import asyncio
5
5
  import socketio_v4 as socketio
6
6
  import logging
7
7
 
@@ -60,7 +60,7 @@ class _C4DirectorNamespace(socketio.AsyncClientNamespace):
60
60
  async with aiohttp.ClientSession(
61
61
  connector=aiohttp.TCPConnector(verify_ssl=False)
62
62
  ) as session:
63
- with async_timeout.timeout(10):
63
+ async with asyncio.timeout(10):
64
64
  async with session.get(
65
65
  self.url + self.uri,
66
66
  params={"JWT": self.token, "SubscriptionClient": clientId},
@@ -71,7 +71,7 @@ class _C4DirectorNamespace(socketio.AsyncClientNamespace):
71
71
  self.subscriptionId = data["subscriptionId"]
72
72
  await self.emit("startSubscription", self.subscriptionId)
73
73
  else:
74
- with async_timeout.timeout(10):
74
+ async with asyncio.timeout(10):
75
75
  async with self.session.get(
76
76
  self.url + self.uri,
77
77
  params={"JWT": self.token, "SubscriptionClient": clientId},
@@ -116,40 +116,54 @@ class C4Websocket:
116
116
  self.connect_callback = connect_callback
117
117
  self.disconnect_callback = disconnect_callback
118
118
 
119
- # Keep track of the callbacks registered for each item id
120
119
  self._item_callbacks = dict()
121
120
  # Initialize self._sio to None
122
121
  self._sio = None
123
122
 
124
123
  @property
125
124
  def item_callbacks(self):
126
- """Returns a dictionary of registered item ids (key) and their callbacks (value).
127
-
128
- item_callbacks cannot be modified directly. Use add_item_callback() and remove_item_callback() instead.
129
- """
130
- return self._item_callbacks
125
+ """Returns a dictionary of registered item ids (key) and their callbacks (value)."""
126
+ return {
127
+ item_id: callbacks[0] if callbacks else None
128
+ for item_id, callbacks in self._item_callbacks.items()
129
+ }
131
130
 
132
131
  def add_item_callback(self, item_id, callback):
133
132
  """Register a callback to receive updates about an item.
134
- If a callback is already registered for the item, it will be overwritten with the provided callback.
135
-
136
133
  Parameters:
137
134
  `item_id` - The Control4 item ID.
138
-
139
135
  `callback` - The callback to be called when an update is received for the provided item id.
140
136
  """
141
-
142
137
  _LOGGER.debug("Subscribing to updates for item id: %s", item_id)
143
138
 
144
- self._item_callbacks[item_id] = callback
139
+ if item_id not in self._item_callbacks:
140
+ self._item_callbacks[item_id] = []
145
141
 
146
- def remove_item_callback(self, item_id):
147
- """Unregister callback for an item.
142
+ # Avoid duplicates
143
+ if callback not in self._item_callbacks[item_id]:
144
+ self._item_callbacks[item_id].append(callback)
148
145
 
146
+ def remove_item_callback(self, item_id, callback=None):
147
+ """Unregister callback(s) for an item.
149
148
  Parameters:
150
149
  `item_id` - The Control4 item ID.
150
+ `callback` - (Optional) Specific callback to remove. If None, removes all callbacks for this item_id.
151
151
  """
152
- self._item_callbacks.pop(item_id)
152
+ if item_id not in self._item_callbacks:
153
+ return
154
+
155
+ if callback is None:
156
+ # Remove all callbacks for this item_id
157
+ del self._item_callbacks[item_id]
158
+ else:
159
+ # Remove a specific callback
160
+ try:
161
+ self._item_callbacks[item_id].remove(callback)
162
+ # If no more callbacks, remove the entry
163
+ if not self._item_callbacks[item_id]:
164
+ del self._item_callbacks[item_id]
165
+ except ValueError:
166
+ pass
153
167
 
154
168
  async def sio_connect(self, director_bearer_token):
155
169
  """Start WebSockets connection and listen, using the provided director_bearer_token to authenticate with the Control4 Director.
@@ -199,19 +213,25 @@ class C4Websocket:
199
213
  """Process an incoming event message."""
200
214
  _LOGGER.debug(message)
201
215
  try:
202
- c = self._item_callbacks[message["iddevice"]]
216
+ callbacks = self._item_callbacks[message["iddevice"]]
203
217
  except KeyError:
204
218
  _LOGGER.debug("No Callback for device id {}".format(message["iddevice"]))
205
219
  return True
206
220
 
207
- if isinstance(message, list):
208
- for m in message:
209
- await c(message["iddevice"], m)
210
- else:
211
- await c(message["iddevice"], message)
221
+ for callback in callbacks[:]:
222
+ try:
223
+ if isinstance(message, list):
224
+ for m in message:
225
+ await callback(message["iddevice"], m)
226
+ else:
227
+ await callback(message["iddevice"], message)
228
+ except Exception as exc:
229
+ _LOGGER.warning(
230
+ "Captured exception during callback: {}".format(str(exc))
231
+ )
212
232
 
213
233
  async def _execute_callback(self, callback, *args, **kwargs):
214
- """Callback with some data capturing any excpetions."""
234
+ """Callback with some data capturing any exceptions."""
215
235
  try:
216
236
  self.sio.emit("ping")
217
237
  await callback(*args, **kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyControl4
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Python 3 asyncio package for interacting with Control4 systems
5
5
  Home-page: https://github.com/lawtancool/pyControl4
6
6
  Author: lawtancool
@@ -8,7 +8,7 @@ Author-email: contact@lawrencetan.ca
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: License :: OSI Approved :: Apache Software License
10
10
  Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.6
11
+ Requires-Python: >=3.11
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: aiohttp
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="pyControl4", # Replace with your own username
8
- version="1.5.0",
8
+ version="1.6.0",
9
9
  author="lawtancool",
10
10
  author_email="contact@lawrencetan.ca",
11
11
  description="Python 3 asyncio package for interacting with Control4 systems",
@@ -18,7 +18,7 @@ setuptools.setup(
18
18
  "License :: OSI Approved :: Apache Software License",
19
19
  "Operating System :: OS Independent",
20
20
  ],
21
- python_requires=">=3.6",
21
+ python_requires=">=3.11",
22
22
  install_requires=[
23
23
  "aiohttp",
24
24
  "xmltodict",
File without changes
File without changes
File without changes
File without changes