zcc-helper 3.3.1.dev2__tar.gz → 3.4__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 (28) hide show
  1. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/PKG-INFO +12 -7
  2. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/setup.py +1 -1
  3. zcc_helper-3.4/tests/test_controller.py +59 -0
  4. zcc_helper-3.4/tests/test_device.py +42 -0
  5. zcc_helper-3.4/tests/test_server.py +11 -0
  6. zcc_helper-3.4/tests/test_socket.py +52 -0
  7. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/__init__.py +5 -1
  8. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/__main__.py +10 -0
  9. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/constants.py +1 -1
  10. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/controller.py +8 -4
  11. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/discovery.py +56 -2
  12. zcc_helper-3.4/zcc/errors.py +16 -0
  13. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/protocol.py +4 -2
  14. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc_helper.egg-info/PKG-INFO +13 -8
  15. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc_helper.egg-info/SOURCES.txt +4 -0
  16. zcc_helper-3.3.1.dev2/zcc/errors.py +0 -5
  17. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/LICENSE.txt +0 -0
  18. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/README.md +0 -0
  19. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/setup.cfg +0 -0
  20. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/description.py +0 -0
  21. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/device.py +0 -0
  22. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/manufacture_info.py +0 -0
  23. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/socket.py +0 -0
  24. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/trace.py +0 -0
  25. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc/watchdog.py +0 -0
  26. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc.py +0 -0
  27. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/zcc_helper.egg-info/dependency_links.txt +0 -0
  28. {zcc_helper-3.3.1.dev2 → zcc_helper-3.4}/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
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
@@ -5,6 +5,10 @@ import sys
5
5
  from .controller import ControlPoint
6
6
  from .description import ControlPointDescription
7
7
  from .discovery import ControlPointDiscoveryService
8
- from .errors import ControlPointError
8
+ from .errors import ( ControlPointError,
9
+ ControlPointConnectionRefusedError,
10
+ ControlPointCannotConnectError,
11
+ ControlPointInvalidHostError,
12
+ ControlPointTimeoutError)
9
13
 
10
14
  logging.basicConfig(stream=sys.stderr)
@@ -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,13 @@ 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
+ result = await ControlPointDiscoveryService(
104
+ verbosity=options.verbosity
105
+ ).validate_connection(host=options.host, port=options.port)
106
+ print(result)
107
+ return
108
+
99
109
  if options.host and options.port:
100
110
  description = ControlPointDescription(host=options.host, port=options.port)
101
111
  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"
@@ -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,15 @@ 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
- from zcc.errors import ControlPointError
16
+ from zcc.errors import ( ControlPointError,
17
+ ControlPointConnectionRefusedError,
18
+ ControlPointCannotConnectError,
19
+ ControlPointInvalidHostError,
20
+ ControlPointTimeoutError)
15
21
  from zcc.protocol import ControlPointProtocol
16
22
 
17
-
18
23
  class ControlPointDiscoveryProtocol(asyncio.DatagramProtocol):
19
24
  """Listens for ZCC announcements on the defined UDP port."""
20
25
 
@@ -76,6 +81,7 @@ class ControlPointDiscoveryService:
76
81
  self.discovery_complete = self.loop.create_future()
77
82
  self.discovery_result: list[ControlPointDescription] = []
78
83
  self.never_completes = self.loop.create_future()
84
+ self.validation_result: ControlPointDescription
79
85
 
80
86
  async def discover(
81
87
  self, wait_for_all: bool = False
@@ -135,3 +141,51 @@ class ControlPointDiscoveryService:
135
141
  async def discovers(self) -> list[ControlPointDescription]:
136
142
  """Discover all local zimi controllers."""
137
143
  return await self.discover(wait_for_all=True)
144
+
145
+ async def validate_connection(
146
+ self, host: str, port: int
147
+ ) -> ControlPointDescription:
148
+ """Validate ability to connect and close a connection.
149
+
150
+ Returns ControlPointDescription if OK.
151
+ """
152
+
153
+ try:
154
+ socket.gethostbyname(host)
155
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
156
+ s.settimeout(10)
157
+ try:
158
+ s.connect((host, port))
159
+ s.close()
160
+ except ConnectionRefusedError as e:
161
+ raise ControlPointConnectionRefusedError() from e
162
+ except TimeoutError as e:
163
+ raise ControlPointTimeoutError() from e
164
+ except socket.gaierror as e:
165
+ raise ControlPointCannotConnectError() from e
166
+ except socket.gaierror as e:
167
+ raise ControlPointInvalidHostError() from e
168
+
169
+ api = ControlPoint(ControlPointDescription(host=host, port=port))
170
+
171
+ try:
172
+ await api.connect(fast=True)
173
+ except ControlPointError as e:
174
+ raise ControlPointCannotConnectError() from e
175
+
176
+ self.validation_result = ControlPointDescription(
177
+ brand=api.brand,
178
+ product=api.product,
179
+ host=host,
180
+ port=port,
181
+ mac=api.mac,
182
+ available_tcps=api.available_tcps,
183
+ api_version=api.api_version,
184
+ firmware_version=api.firmware_version
185
+ )
186
+
187
+ api.disconnect()
188
+
189
+ self.logger.info(self.validation_result)
190
+
191
+ return self.validation_result
@@ -0,0 +1,16 @@
1
+ '''ControlPoint error class'''
2
+
3
+
4
+ class ControlPointError(Exception):
5
+ '''Represents a ZCC controller error.'''
6
+
7
+ class ControlPointCannotConnectError(ControlPointError):
8
+ '''Represents a connect connect error when connecting to zcc.'''
9
+
10
+ class ControlPointInvalidHostError(ControlPointError):
11
+ '''Represents an invalid host error when connecting to zcc.'''
12
+ class ControlPointConnectionRefusedError(ControlPointError):
13
+ '''Represents a connection refused when connecting to zcc.'''
14
+
15
+ class ControlPointTimeoutError(ControlPointError):
16
+ '''Represents a connection timeout when connecting to zcc.'''
@@ -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
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
@@ -1,5 +0,0 @@
1
- '''ControlPoint error class'''
2
-
3
-
4
- class ControlPointError(Exception):
5
- '''Represents a ZCC controller error.'''
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes