zcc-helper 3.4.dev1__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.
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/PKG-INFO +12 -7
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/setup.py +1 -1
- zcc_helper-3.4.dev2/tests/test_controller.py +59 -0
- zcc_helper-3.4.dev2/tests/test_device.py +42 -0
- zcc_helper-3.4.dev2/tests/test_server.py +11 -0
- zcc_helper-3.4.dev2/tests/test_socket.py +52 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/__main__.py +9 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/constants.py +1 -1
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/discovery.py +70 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc_helper.egg-info/PKG-INFO +13 -8
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc_helper.egg-info/SOURCES.txt +4 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/LICENSE.txt +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/README.md +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/setup.cfg +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/__init__.py +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/controller.py +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/description.py +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/device.py +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/errors.py +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/manufacture_info.py +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/protocol.py +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/socket.py +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/trace.py +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc/watchdog.py +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc.py +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc_helper.egg-info/dependency_links.txt +0 -0
- {zcc_helper-3.4.dev1 → zcc_helper-3.4.dev2}/zcc_helper.egg-info/top_level.txt +0 -0
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: zcc_helper
|
|
3
|
-
Version: 3.4.
|
|
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
|
-
|
|
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.
|
|
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,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:
|
|
@@ -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
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name:
|
|
3
|
-
Version: 3.4.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|