zcc-helper 3.3.1.dev2__tar.gz → 3.4.dev2__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 (27) hide show
  1. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/PKG-INFO +12 -7
  2. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/setup.py +1 -1
  3. zcc_helper-3.4.dev2/tests/test_controller.py +59 -0
  4. zcc_helper-3.4.dev2/tests/test_device.py +42 -0
  5. zcc_helper-3.4.dev2/tests/test_server.py +11 -0
  6. zcc_helper-3.4.dev2/tests/test_socket.py +52 -0
  7. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/__main__.py +9 -0
  8. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/constants.py +1 -1
  9. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/controller.py +8 -4
  10. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/discovery.py +70 -0
  11. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/protocol.py +4 -2
  12. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc_helper.egg-info/PKG-INFO +13 -8
  13. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc_helper.egg-info/SOURCES.txt +4 -0
  14. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/LICENSE.txt +0 -0
  15. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/README.md +0 -0
  16. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/setup.cfg +0 -0
  17. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/__init__.py +0 -0
  18. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/description.py +0 -0
  19. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/device.py +0 -0
  20. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/errors.py +0 -0
  21. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/manufacture_info.py +0 -0
  22. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/socket.py +0 -0
  23. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/trace.py +0 -0
  24. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc/watchdog.py +0 -0
  25. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc.py +0 -0
  26. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc_helper.egg-info/dependency_links.txt +0 -0
  27. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4.dev2}/zcc_helper.egg-info/top_level.txt +0 -0
@@ -1,16 +1,23 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: zcc_helper
3
- Version: 3.3.1.dev2
3
+ Version: 3.4.dev2
4
4
  Summary: ZIMI ZCC helper module
5
- Home-page: UNKNOWN
6
5
  Author: Mark Hannon
7
6
  Author-email: mark.hannon@gmail.com
8
7
  License: MIT
9
8
  Project-URL: Source, https://bitbucket.org/mark_hannon/zcc
10
- Platform: UNKNOWN
11
- Classifier: Programming Language :: Python :: 3.9
9
+ Classifier: Programming Language :: Python :: 3.12
12
10
  Description-Content-Type: text/markdown
13
11
  License-File: LICENSE.txt
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: classifier
15
+ Dynamic: description
16
+ Dynamic: description-content-type
17
+ Dynamic: license
18
+ Dynamic: license-file
19
+ Dynamic: project-url
20
+ Dynamic: summary
14
21
 
15
22
  # ZCC-HELPER
16
23
 
@@ -274,5 +281,3 @@ python -m zcc --execute --device 'bddf0500-4d15-4457-b063-c12ed208a0b0_3' --acti
274
281
  ```
275
282
 
276
283
  This version of the command is relatively slow as it first of all discovers the ZCC on the local LAN, builds a device inventory and then executes the action.
277
-
278
-
@@ -19,7 +19,7 @@ setup(
19
19
  author_email="mark.hannon@gmail.com",
20
20
  license="MIT",
21
21
  classifiers=[
22
- "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.12",
23
23
  ],
24
24
  packages=find_packages(exclude=["contrib", "docs", "tests"]),
25
25
  project_urls={"Source": "https://bitbucket.org/mark_hannon/zcc"},
@@ -0,0 +1,59 @@
1
+ '''Test Basic Controller functionality.'''
2
+ import pytest
3
+
4
+ from zcc.controller import ControlPoint, ControlPointError
5
+ from zcc.device import ControlPointDevice
6
+
7
+ from tests.test_server import test_server
8
+
9
+
10
+ def test_controller_discover(test_server):
11
+ '''Test for connection to a Controller connected to the Test Server'''
12
+
13
+ controller = ControlPoint(timeout=1)
14
+
15
+ assert controller.ready is True
16
+ assert controller.host is not None
17
+ assert controller.port == 5003
18
+ assert controller.brand == 'zimi'
19
+ assert len(controller.devices) == 31
20
+ assert len(controller.doors) == 0
21
+ assert len(controller.fans) == 0
22
+ assert len(controller.lights) == 20
23
+ assert len(controller.outlets) == 0
24
+
25
+
26
+ def test_controller_discover_timeout():
27
+ '''Test if a UDP discovery times out - goes out over wire'''
28
+
29
+ try:
30
+ ControlPoint(timeout=1)
31
+ assert False
32
+ except ControlPointError as error:
33
+ assert error.args[0] == '__init() failed - unable to discover and connect to ZCC'
34
+
35
+
36
+ def test_controller_device(test_server):
37
+ '''Test turn_on works for valid devices'''
38
+
39
+ controller = ControlPoint(timeout=1)
40
+
41
+ light = controller.lights[0]
42
+
43
+ assert light.is_off() is not True
44
+ assert light.is_on() is True
45
+
46
+ assert light.is_opening is not True
47
+ assert light.is_open is not True
48
+ assert light.location == "lounge LED strip/Lounge"
49
+ assert light.name == "lounge LED strip"
50
+
51
+ with pytest.raises(ControlPointDeviceError):
52
+ light.open_door()
53
+
54
+ with pytest.raises(ControlPointDeviceError):
55
+ light.close_door()
56
+
57
+ light.turn_on()
58
+
59
+ light.turn_off()
@@ -0,0 +1,42 @@
1
+ import unittest
2
+
3
+ from zcc.device import ControlPointDevice
4
+
5
+
6
+ class DeviceTest(unittest.TestCase):
7
+
8
+ identifier = 'test_device_id'
9
+
10
+ def test_init(self):
11
+ device = ControlPointDevice(None, DeviceTest.identifier)
12
+ self.assertEqual(device.controller, None)
13
+ self.assertEqual(device.identifier, DeviceTest.identifier)
14
+ self.assertEqual(device.actions, {})
15
+ self.assertEqual(device.properties, {})
16
+ self.assertEqual(device.states, {})
17
+
18
+ def test_add_action(self):
19
+ device = ControlPointDevice(None, DeviceTest.identifier)
20
+ device.actions = {'TurnOn': {'actionParams': {}},
21
+ 'TurnOff': {'actionParams': {}}}
22
+ print(device)
23
+ self.assertEqual(device.controller, None)
24
+ self.assertEqual(device.identifier, DeviceTest.identifier)
25
+
26
+ def test_add_properties(self):
27
+ device = ControlPointDevice(None, DeviceTest.identifier)
28
+ device.properties = {'name': 'Entry Pendant',
29
+ 'controlPointType': 'switch', 'roomId': 2, 'roomName': 'Front Door '}
30
+ self.assertEqual(device.controller, None)
31
+ self.assertEqual(device.identifier, DeviceTest.identifier)
32
+
33
+ def test_add_states(self):
34
+ device = ControlPointDevice(None, DeviceTest.identifier)
35
+ device.states = {'controlState': {
36
+ 'switch': {...}}, 'isConnected': True}
37
+ self.assertEqual(device.controller, None)
38
+ self.assertEqual(device.identifier, DeviceTest.identifier)
39
+
40
+
41
+ if __name__ == '__main__':
42
+ unittest.main()
@@ -0,0 +1,11 @@
1
+ '''Setup Test Server.'''
2
+ import pytest
3
+
4
+ from zcc.controller import ControlPoint, ControlPointError
5
+ from tests.server import TestServer
6
+
7
+
8
+ @pytest.fixture
9
+ def test_server():
10
+ '''Create (and delete) a Test Server'''
11
+ return TestServer()
@@ -0,0 +1,52 @@
1
+ import socket
2
+ import pytest
3
+ from unittest.mock import Mock
4
+ from mockito import when, unstub
5
+
6
+ from zcc.socket import ControlPointSocket
7
+
8
+ TEST_HOST = '10.0.0.1'
9
+ TEST_PORT = 4567
10
+ TEST_TIMEOUT = 5
11
+
12
+
13
+ @pytest.fixture
14
+ def mock_tcp_socket():
15
+ mock_socket = Mock(spec=socket.socket)
16
+ return mock_socket
17
+
18
+
19
+ def test_socket_create(mock_tcp_socket, when):
20
+
21
+ when(socket).socket(socket.AF_INET,
22
+ socket.SOCK_STREAM)\
23
+ .thenReturn(mock_tcp_socket)
24
+
25
+ controller_socket = ControlPointSocket(TEST_HOST, TEST_PORT)
26
+
27
+ assert controller_socket.host == TEST_HOST
28
+ assert controller_socket.port == TEST_PORT
29
+ assert controller_socket.sock == mock_tcp_socket
30
+
31
+ controller_socket.close()
32
+ assert controller_socket.sock == None
33
+
34
+
35
+ def test_socket_create_timeout(mock_tcp_socket, when):
36
+
37
+ when(socket).socket(socket.AF_INET,
38
+ socket.SOCK_STREAM)\
39
+ .thenReturn(mock_tcp_socket)
40
+
41
+ controller_socket = ControlPointSocket(
42
+ TEST_HOST, TEST_PORT, timeout=TEST_TIMEOUT)
43
+
44
+ assert controller_socket.host == TEST_HOST
45
+ assert controller_socket.port == TEST_PORT
46
+ assert controller_socket.sock == mock_tcp_socket
47
+
48
+ assert mock_tcp_socket.settimeout.call_count == 1
49
+ assert mock_tcp_socket.settimeout.call_args.args == (TEST_TIMEOUT,)
50
+
51
+ controller_socket.close()
52
+ assert controller_socket.sock == None
@@ -52,6 +52,9 @@ def __options(args):
52
52
  cxg.add_argument(
53
53
  "--execute", action="store_true", help="execute an action on a device"
54
54
  )
55
+ cxg.add_argument(
56
+ "--test-connection", action="store_true", help="test the connection to a device"
57
+ )
55
58
 
56
59
  host_group = parser.add_argument_group("host")
57
60
  host_group.add_argument("--host", action="store", help="zcc host name|address")
@@ -96,6 +99,12 @@ async def main(args):
96
99
  await ControlPointDiscoveryService(verbosity=options.verbosity).discover()
97
100
  return
98
101
 
102
+ if options.test_connection and options.host and options.port:
103
+ await ControlPointDiscoveryService(
104
+ verbosity=options.verbosity
105
+ ).validate_connection(host=options.host, port=options.port)
106
+ return
107
+
99
108
  if options.host and options.port:
100
109
  description = ControlPointDescription(host=options.host, port=options.port)
101
110
  else:
@@ -9,4 +9,4 @@ APP_ID = "ZvWqWJQB"
9
9
  APP_TOKEN = "3422ecbb-cf6a-404c-b3dc-2023dd213535"
10
10
 
11
11
  NAME = "zcc_helper"
12
- VERSION = "3.3.1-dev2"
12
+ VERSION = "3.4-dev2"
@@ -313,9 +313,13 @@ class ControlPoint:
313
313
  """Get initial control point data from controller."""
314
314
 
315
315
  self.manufacture_info_ready = self.loop.create_future()
316
+ self.manufacture_info_received = 0
316
317
  self.properties_ready = self.loop.create_future()
318
+ self.properties_received = 0
317
319
  self.actions_ready = self.loop.create_future()
320
+ self.actions_received = 0
318
321
  self.states_ready = self.loop.create_future()
322
+ self.states_received = 0
319
323
 
320
324
  self.logger.debug("Getting manufacture_info")
321
325
  await self.socket.sendall(
@@ -332,7 +336,7 @@ class ControlPoint:
332
336
  "ZCC connection failed - didn't receive manufacture_info."
333
337
  ) from error
334
338
 
335
- await asyncio.sleep(ControlPointProtocol.DEVICE_GET_TIMEOUT)
339
+ await asyncio.sleep(ControlPointProtocol.STEP_TIMEOUT)
336
340
 
337
341
  self.logger.debug("Getting initial device properties")
338
342
  await self.socket.sendall(
@@ -348,7 +352,7 @@ class ControlPoint:
348
352
  "ZCC connection failed - didn't receive any properties."
349
353
  ) from error
350
354
 
351
- await asyncio.sleep(ControlPointProtocol.DEVICE_GET_TIMEOUT)
355
+ await asyncio.sleep(ControlPointProtocol.STEP_TIMEOUT)
352
356
 
353
357
  self.logger.debug("Getting initial device actions")
354
358
  await self.socket.sendall(
@@ -364,7 +368,7 @@ class ControlPoint:
364
368
  "ZCC connection failed - didn't receive any actions."
365
369
  ) from error
366
370
 
367
- await asyncio.sleep(ControlPointProtocol.DEVICE_GET_TIMEOUT)
371
+ await asyncio.sleep(ControlPointProtocol.STEP_TIMEOUT)
368
372
 
369
373
  self.logger.debug("Getting initial device states")
370
374
  await self.socket.sendall(
@@ -380,7 +384,7 @@ class ControlPoint:
380
384
  "ZCC connection failed - didn't receive any states."
381
385
  ) from error
382
386
 
383
- await asyncio.sleep(ControlPointProtocol.DEVICE_GET_TIMEOUT)
387
+ await asyncio.sleep(ControlPointProtocol.STEP_TIMEOUT)
384
388
 
385
389
  for key in self.devices.keys():
386
390
  identifier_msb = self.devices[key].identifier.split("_")[0]
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ from enum import StrEnum
6
7
  import json
7
8
  from json.decoder import JSONDecodeError
8
9
  import logging
@@ -10,11 +11,25 @@ import socket
10
11
  from typing import Tuple
11
12
 
12
13
  from zcc.constants import LEVEL_BY_VERBOSITY
14
+ from zcc.controller import ControlPoint
13
15
  from zcc.description import ControlPointDescription
14
16
  from zcc.errors import ControlPointError
15
17
  from zcc.protocol import ControlPointProtocol
16
18
 
17
19
 
20
+ class ControlPointDiscoveryErrors(StrEnum):
21
+ """Discovery errors."""
22
+
23
+ ALREADY_CONFIGURED = "already_configured"
24
+ CANNOT_CONNECT = "cannot_connect"
25
+ CONNECTION_REFUSED = "connection_refused"
26
+ DISCOVERY_FAILURE = "discovery_failure"
27
+ INVALID_HOST = "invalid_host"
28
+ INVALID_PORT = "invalid_port"
29
+ TIMEOUT = "timeout"
30
+ UNKNOWN = "unknown"
31
+
32
+
18
33
  class ControlPointDiscoveryProtocol(asyncio.DatagramProtocol):
19
34
  """Listens for ZCC announcements on the defined UDP port."""
20
35
 
@@ -76,6 +91,7 @@ class ControlPointDiscoveryService:
76
91
  self.discovery_complete = self.loop.create_future()
77
92
  self.discovery_result: list[ControlPointDescription] = []
78
93
  self.never_completes = self.loop.create_future()
94
+ self.validation_result: ControlPointDescription
79
95
 
80
96
  async def discover(
81
97
  self, wait_for_all: bool = False
@@ -135,3 +151,57 @@ class ControlPointDiscoveryService:
135
151
  async def discovers(self) -> list[ControlPointDescription]:
136
152
  """Discover all local zimi controllers."""
137
153
  return await self.discover(wait_for_all=True)
154
+
155
+ async def validate_connection(
156
+ self, host: str, port: int
157
+ ) -> ControlPointDescription | dict[str, str]:
158
+ """Validate ability to connect and close a connection.
159
+
160
+ Return a ControlPointDescription if OK or error dictionary if not.
161
+ """
162
+
163
+ error = None
164
+
165
+ try:
166
+ socket.gethostbyname(host)
167
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
168
+ s.settimeout(10)
169
+ try:
170
+ s.connect((host, port))
171
+ s.close()
172
+ except ConnectionRefusedError:
173
+ error = {"error": ControlPointDiscoveryErrors.CONNECTION_REFUSED}
174
+ except TimeoutError:
175
+ error = {"error": ControlPointDiscoveryErrors.TIMEOUT}
176
+ except socket.gaierror:
177
+ error = {"error": ControlPointDiscoveryErrors.CANNOT_CONNECT}
178
+ except socket.gaierror:
179
+ error = {"error": ControlPointDiscoveryErrors.INVALID_HOST}
180
+
181
+ if error:
182
+ self.logger.error(error)
183
+ return error
184
+
185
+ api = ControlPoint(ControlPointDescription(host=host, port=port))
186
+
187
+ try:
188
+ await api.connect(fast=True)
189
+ except ControlPointError:
190
+ return {"error": ControlPointDiscoveryErrors.CANNOT_CONNECT}
191
+
192
+ self.validation_result = ControlPointDescription(
193
+ brand=api.brand,
194
+ product=api.product,
195
+ host=host,
196
+ port=port,
197
+ mac=api.mac,
198
+ available_tcps=api.available_tcps,
199
+ api_version=api.api_version,
200
+ firmware_version=api.firmware_version
201
+ )
202
+
203
+ api.disconnect()
204
+
205
+ self.logger.info(self.validation_result)
206
+
207
+ return self.validation_result
@@ -23,9 +23,11 @@ class ControlPointProtocol:
23
23
  CONTROLPOINT_STATES_EVENTS = "controlpoint_states_events"
24
24
  ZCC_STATUS = "zcc_status"
25
25
 
26
- SUBSCRIBE_TIMEOUT = 5
26
+ SUBSCRIBE_TIMEOUT = 2
27
27
  GATEWAY_PROPERTIES = "gateway_properties"
28
- GATEWAY_PROPERTIES_TIMOUT = 10
28
+ GATEWAY_PROPERTIES_TIMEOUT = 10
29
+
30
+ STEP_TIMEOUT = 0
29
31
 
30
32
  RETRY_TIMEOUT = 90
31
33
 
@@ -1,16 +1,23 @@
1
- Metadata-Version: 2.1
2
- Name: zcc-helper
3
- Version: 3.3.1.dev2
1
+ Metadata-Version: 2.4
2
+ Name: zcc_helper
3
+ Version: 3.4.dev2
4
4
  Summary: ZIMI ZCC helper module
5
- Home-page: UNKNOWN
6
5
  Author: Mark Hannon
7
6
  Author-email: mark.hannon@gmail.com
8
7
  License: MIT
9
8
  Project-URL: Source, https://bitbucket.org/mark_hannon/zcc
10
- Platform: UNKNOWN
11
- Classifier: Programming Language :: Python :: 3.9
9
+ Classifier: Programming Language :: Python :: 3.12
12
10
  Description-Content-Type: text/markdown
13
11
  License-File: LICENSE.txt
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: classifier
15
+ Dynamic: description
16
+ Dynamic: description-content-type
17
+ Dynamic: license
18
+ Dynamic: license-file
19
+ Dynamic: project-url
20
+ Dynamic: summary
14
21
 
15
22
  # ZCC-HELPER
16
23
 
@@ -274,5 +281,3 @@ python -m zcc --execute --device 'bddf0500-4d15-4457-b063-c12ed208a0b0_3' --acti
274
281
  ```
275
282
 
276
283
  This version of the command is relatively slow as it first of all discovers the ZCC on the local LAN, builds a device inventory and then executes the action.
277
-
278
-
@@ -2,6 +2,10 @@ LICENSE.txt
2
2
  README.md
3
3
  setup.py
4
4
  zcc.py
5
+ tests/test_controller.py
6
+ tests/test_device.py
7
+ tests/test_server.py
8
+ tests/test_socket.py
5
9
  zcc/__init__.py
6
10
  zcc/__main__.py
7
11
  zcc/constants.py
File without changes
File without changes
File without changes