pyvlx 0.2.27__py3-none-any.whl
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.
- pyvlx/__init__.py +21 -0
- pyvlx/api/__init__.py +23 -0
- pyvlx/api/activate_scene.py +63 -0
- pyvlx/api/api_event.py +73 -0
- pyvlx/api/command_send.py +85 -0
- pyvlx/api/factory_default.py +34 -0
- pyvlx/api/frame_creation.py +202 -0
- pyvlx/api/frames/__init__.py +76 -0
- pyvlx/api/frames/alias_array.py +45 -0
- pyvlx/api/frames/frame.py +56 -0
- pyvlx/api/frames/frame_activate_scene.py +92 -0
- pyvlx/api/frames/frame_activation_log_updated.py +14 -0
- pyvlx/api/frames/frame_command_send.py +280 -0
- pyvlx/api/frames/frame_discover_nodes.py +64 -0
- pyvlx/api/frames/frame_error_notification.py +42 -0
- pyvlx/api/frames/frame_facory_default.py +32 -0
- pyvlx/api/frames/frame_get_all_nodes_information.py +218 -0
- pyvlx/api/frames/frame_get_limitation.py +127 -0
- pyvlx/api/frames/frame_get_local_time.py +38 -0
- pyvlx/api/frames/frame_get_network_setup.py +64 -0
- pyvlx/api/frames/frame_get_node_information.py +223 -0
- pyvlx/api/frames/frame_get_protocol_version.py +53 -0
- pyvlx/api/frames/frame_get_scene_list.py +82 -0
- pyvlx/api/frames/frame_get_state.py +47 -0
- pyvlx/api/frames/frame_get_version.py +72 -0
- pyvlx/api/frames/frame_helper.py +40 -0
- pyvlx/api/frames/frame_house_status_monitor_disable_cfm.py +14 -0
- pyvlx/api/frames/frame_house_status_monitor_disable_req.py +14 -0
- pyvlx/api/frames/frame_house_status_monitor_enable_cfm.py +14 -0
- pyvlx/api/frames/frame_house_status_monitor_enable_req.py +14 -0
- pyvlx/api/frames/frame_leave_learn_state.py +41 -0
- pyvlx/api/frames/frame_node_information_changed.py +57 -0
- pyvlx/api/frames/frame_node_state_position_changed_notification.py +84 -0
- pyvlx/api/frames/frame_password_change.py +114 -0
- pyvlx/api/frames/frame_password_enter.py +70 -0
- pyvlx/api/frames/frame_reboot.py +32 -0
- pyvlx/api/frames/frame_set_node_name.py +73 -0
- pyvlx/api/frames/frame_set_utc.py +45 -0
- pyvlx/api/frames/frame_status_request.py +212 -0
- pyvlx/api/get_all_nodes_information.py +46 -0
- pyvlx/api/get_limitation.py +64 -0
- pyvlx/api/get_local_time.py +34 -0
- pyvlx/api/get_network_setup.py +34 -0
- pyvlx/api/get_node_information.py +42 -0
- pyvlx/api/get_protocol_version.py +40 -0
- pyvlx/api/get_scene_list.py +49 -0
- pyvlx/api/get_state.py +43 -0
- pyvlx/api/get_version.py +34 -0
- pyvlx/api/house_status_monitor.py +52 -0
- pyvlx/api/leave_learn_state.py +33 -0
- pyvlx/api/password_enter.py +39 -0
- pyvlx/api/reboot.py +33 -0
- pyvlx/api/session_id.py +20 -0
- pyvlx/api/set_node_name.py +32 -0
- pyvlx/api/set_utc.py +31 -0
- pyvlx/api/status_request.py +48 -0
- pyvlx/config.py +54 -0
- pyvlx/connection.py +182 -0
- pyvlx/const.py +685 -0
- pyvlx/dataobjects.py +161 -0
- pyvlx/discovery.py +100 -0
- pyvlx/exception.py +26 -0
- pyvlx/heartbeat.py +79 -0
- pyvlx/klf200gateway.py +167 -0
- pyvlx/lightening_device.py +102 -0
- pyvlx/log.py +4 -0
- pyvlx/node.py +74 -0
- pyvlx/node_helper.py +165 -0
- pyvlx/node_updater.py +162 -0
- pyvlx/nodes.py +99 -0
- pyvlx/on_off_switch.py +44 -0
- pyvlx/opening_device.py +644 -0
- pyvlx/parameter.py +357 -0
- pyvlx/py.typed +0 -0
- pyvlx/pyvlx.py +124 -0
- pyvlx/scene.py +53 -0
- pyvlx/scenes.py +60 -0
- pyvlx/slip.py +48 -0
- pyvlx/string_helper.py +20 -0
- pyvlx-0.2.27.dist-info/METADATA +122 -0
- pyvlx-0.2.27.dist-info/RECORD +84 -0
- pyvlx-0.2.27.dist-info/WHEEL +5 -0
- pyvlx-0.2.27.dist-info/licenses/LICENSE +165 -0
- pyvlx-0.2.27.dist-info/top_level.txt +1 -0
pyvlx/dataobjects.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Module for Dataobjects."""
|
|
2
|
+
import time
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from .const import (
|
|
7
|
+
DHCPParameter, GatewayState, GatewaySubState,
|
|
8
|
+
LeaveLearnStateConfirmationStatus)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DtoLocalTime:
|
|
12
|
+
"""Dataobject to hold KLF200 Time Data."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, utctime: Optional[datetime] = None, localtime: Optional[datetime] = None):
|
|
15
|
+
"""Initialize DtoLocalTime class."""
|
|
16
|
+
if utctime is None:
|
|
17
|
+
utctime = datetime.fromtimestamp(0)
|
|
18
|
+
if localtime is None:
|
|
19
|
+
localtime = datetime.fromtimestamp(0)
|
|
20
|
+
self.utctime = utctime
|
|
21
|
+
self.localtime = localtime
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
"""Return human readable string."""
|
|
25
|
+
return (
|
|
26
|
+
'<{} utctime="{}" localtime="{}"/>'.format(
|
|
27
|
+
type(self).__name__, self.utctime, self.localtime)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def from_payload(self, payload: bytes) -> None:
|
|
31
|
+
"""Build the Dto From Data."""
|
|
32
|
+
self.utctime = datetime.fromtimestamp(int.from_bytes(payload[0:4], "big"))
|
|
33
|
+
weekday = payload[11] - 1
|
|
34
|
+
if weekday == -1:
|
|
35
|
+
weekday = 6
|
|
36
|
+
|
|
37
|
+
self.localtime = datetime.fromtimestamp(time.mktime(
|
|
38
|
+
(int.from_bytes(payload[9:11], byteorder='big') + 1900, # Year
|
|
39
|
+
payload[8], # month
|
|
40
|
+
payload[7], # day
|
|
41
|
+
payload[6], # hour
|
|
42
|
+
payload[5], # minute
|
|
43
|
+
payload[4], # second
|
|
44
|
+
weekday,
|
|
45
|
+
int.from_bytes(payload[12:14], byteorder='big'), # day of year
|
|
46
|
+
int.from_bytes(payload[14:15], byteorder='big', signed=True))))
|
|
47
|
+
|
|
48
|
+
def to_payload(self) -> bytes:
|
|
49
|
+
"""Build the Dto From Data."""
|
|
50
|
+
payload = b''
|
|
51
|
+
payload = int(self.utctime.timestamp()).to_bytes(4, byteorder='big')
|
|
52
|
+
payload += self.localtime.second.to_bytes(1, "big")
|
|
53
|
+
payload += self.localtime.minute.to_bytes(1, "big")
|
|
54
|
+
payload += self.localtime.hour.to_bytes(1, "big")
|
|
55
|
+
payload += self.localtime.day.to_bytes(1, "big")
|
|
56
|
+
payload += self.localtime.month.to_bytes(1, "big")
|
|
57
|
+
payload += (self.localtime.year - 1900).to_bytes(2, "big")
|
|
58
|
+
if self.localtime.weekday == 6:
|
|
59
|
+
payload += (0).to_bytes(1, "big")
|
|
60
|
+
else:
|
|
61
|
+
payload += (self.localtime.weekday() + 1).to_bytes(1, "big")
|
|
62
|
+
payload += self.localtime.timetuple().tm_yday.to_bytes(2, "big")
|
|
63
|
+
payload += (self.localtime.timetuple().tm_isdst).to_bytes(1, "big", signed=True)
|
|
64
|
+
return payload
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DtoNetworkSetup:
|
|
68
|
+
"""Dataobject to hold KLF200 Network Setup."""
|
|
69
|
+
|
|
70
|
+
def __init__(self,
|
|
71
|
+
ipaddress: Optional[str] = None,
|
|
72
|
+
gateway: Optional[str] = None,
|
|
73
|
+
netmask: Optional[str] = None,
|
|
74
|
+
dhcp: Optional[DHCPParameter] = None):
|
|
75
|
+
"""Initialize DtoNetworkSetup class."""
|
|
76
|
+
self.ipaddress = ipaddress
|
|
77
|
+
self.gateway = gateway
|
|
78
|
+
self.netmask = netmask
|
|
79
|
+
self.dhcp = dhcp
|
|
80
|
+
|
|
81
|
+
def __str__(self) -> str:
|
|
82
|
+
"""Return human readable string."""
|
|
83
|
+
return '<{} ipaddress="{}" gateway="{}" gateway="{}" dhcp="{}"/>'.format(
|
|
84
|
+
type(self).__name__, self.ipaddress, self.gateway,
|
|
85
|
+
self.gateway, self.dhcp
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DtoProtocolVersion:
|
|
90
|
+
"""KLF 200 Dataobject for Protocol version."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, majorversion: Optional[int] = None, minorversion: Optional[int] = None):
|
|
93
|
+
"""Initialize DtoProtocolVersion class."""
|
|
94
|
+
self.majorversion = majorversion
|
|
95
|
+
self.minorversion = minorversion
|
|
96
|
+
|
|
97
|
+
def __str__(self) -> str:
|
|
98
|
+
"""Return human readable string."""
|
|
99
|
+
return (
|
|
100
|
+
'<{} majorversion="{}" minorversion="{}"/>'.format(
|
|
101
|
+
type(self).__name__, self.majorversion, self.minorversion
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class DtoState:
|
|
107
|
+
"""Data Object for Gateway State."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, gateway_state: Optional[GatewayState] = None, gateway_sub_state: Optional[GatewaySubState] = None):
|
|
110
|
+
"""Initialize DtoState class."""
|
|
111
|
+
self.gateway_state = gateway_state
|
|
112
|
+
self.gateway_sub_state = gateway_sub_state
|
|
113
|
+
|
|
114
|
+
def __str__(self) -> str:
|
|
115
|
+
"""Return human readable string."""
|
|
116
|
+
return (
|
|
117
|
+
'<{} gateway_state="{}" gateway_sub_state="{}"/>'.format(
|
|
118
|
+
type(self).__name__, self.gateway_state, self.gateway_sub_state
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class DtoVersion:
|
|
124
|
+
"""Object for KLF200 Version Information."""
|
|
125
|
+
|
|
126
|
+
def __init__(self,
|
|
127
|
+
softwareversion: Optional[str] = None,
|
|
128
|
+
hardwareversion: Optional[int] = None,
|
|
129
|
+
productgroup: Optional[int] = None,
|
|
130
|
+
producttype: Optional[int] = None):
|
|
131
|
+
"""Initialize DtoVersion class."""
|
|
132
|
+
self.softwareversion = softwareversion
|
|
133
|
+
self.hardwareversion = hardwareversion
|
|
134
|
+
self.productgroup = productgroup
|
|
135
|
+
self.producttype = producttype
|
|
136
|
+
|
|
137
|
+
def __str__(self) -> str:
|
|
138
|
+
"""Return human readable string."""
|
|
139
|
+
return (
|
|
140
|
+
'<{} softwareversion="{}" hardwareversion="{}" '
|
|
141
|
+
'productgroup="{}" producttype="{}"/>'.format(
|
|
142
|
+
type(self).__name__,
|
|
143
|
+
self.softwareversion, self.hardwareversion, self.productgroup, self.producttype
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class DtoLeaveLearnState:
|
|
149
|
+
"""Dataobject to hold KLF200 Data."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, status: Optional[LeaveLearnStateConfirmationStatus] = None):
|
|
152
|
+
"""Initialize DtoLeaveLearnState class."""
|
|
153
|
+
self.status = status
|
|
154
|
+
|
|
155
|
+
def __str__(self) -> str:
|
|
156
|
+
"""Return human readable string."""
|
|
157
|
+
return (
|
|
158
|
+
'<{} status="{}"/>'.format(
|
|
159
|
+
type(self).__name__, self.status
|
|
160
|
+
)
|
|
161
|
+
)
|
pyvlx/discovery.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Module to discover Velux KLF200 devices on the network."""
|
|
2
|
+
import asyncio
|
|
3
|
+
from asyncio import Event, Future, Task
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Optional, Set
|
|
6
|
+
|
|
7
|
+
from zeroconf import IPVersion
|
|
8
|
+
from zeroconf.asyncio import (
|
|
9
|
+
AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf)
|
|
10
|
+
|
|
11
|
+
SERVICE_STARTS_WITH: str = "VELUX_KLF_LAN"
|
|
12
|
+
SERVICE_TYPE: str = "_http._tcp.local."
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class VeluxHost():
|
|
17
|
+
"""Class to store Velux KLF200 host information."""
|
|
18
|
+
|
|
19
|
+
hostname: str
|
|
20
|
+
ip_address: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sanitize_hostname(hostname: str) -> str:
|
|
24
|
+
"""Sanitize KLF200 hostname."""
|
|
25
|
+
hostname = hostname.upper()
|
|
26
|
+
hostname = hostname.replace("._HTTP._TCP.LOCAL.", "")
|
|
27
|
+
# KLF report it's hostname within Zeroconf ServiceInfo with the "LAN_" prefix, on DHCP requests without
|
|
28
|
+
hostname = hostname.replace("LAN_", "")
|
|
29
|
+
return hostname
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class VeluxDiscovery():
|
|
33
|
+
"""Class to discover Velux KLF200 devices on the network."""
|
|
34
|
+
|
|
35
|
+
hosts: list[VeluxHost] = []
|
|
36
|
+
infos: list[AsyncServiceInfo | None] = []
|
|
37
|
+
|
|
38
|
+
def __init__(self, zeroconf: AsyncZeroconf,) -> None:
|
|
39
|
+
"""Initialize VeluxDiscovery object."""
|
|
40
|
+
self.zc: AsyncZeroconf = zeroconf
|
|
41
|
+
|
|
42
|
+
async def _async_discover_hosts(self, min_wait_time: float, expected_hosts: int | None) -> None:
|
|
43
|
+
"""Listen for zeroconf ServiceInfo."""
|
|
44
|
+
self.hosts.clear()
|
|
45
|
+
service_names: list[str] = []
|
|
46
|
+
tasks: Set[Task] = set()
|
|
47
|
+
got_host: Event = Event()
|
|
48
|
+
|
|
49
|
+
def add_info_and_host(fut: Future) -> None:
|
|
50
|
+
info: AsyncServiceInfo = fut.result()
|
|
51
|
+
self.infos.append(info)
|
|
52
|
+
hostname = sanitize_hostname(info.name)
|
|
53
|
+
host = VeluxHost(
|
|
54
|
+
hostname=hostname,
|
|
55
|
+
ip_address=info.parsed_addresses(version=IPVersion.V4Only)[0],
|
|
56
|
+
)
|
|
57
|
+
self.hosts.append(host)
|
|
58
|
+
got_host.set()
|
|
59
|
+
|
|
60
|
+
def handler(name: str, **kwargs: Any) -> None: # pylint: disable=W0613:unused-argument
|
|
61
|
+
if name.startswith(SERVICE_STARTS_WITH):
|
|
62
|
+
if name not in service_names:
|
|
63
|
+
service_names.append(name)
|
|
64
|
+
task = asyncio.create_task(self.zc.async_get_service_info(type_=SERVICE_TYPE, name=name))
|
|
65
|
+
tasks.add(task)
|
|
66
|
+
task.add_done_callback(add_info_and_host)
|
|
67
|
+
task.add_done_callback(tasks.remove)
|
|
68
|
+
|
|
69
|
+
browser: AsyncServiceBrowser = AsyncServiceBrowser(self.zc.zeroconf, SERVICE_TYPE, handlers=[handler])
|
|
70
|
+
if expected_hosts:
|
|
71
|
+
while len(self.hosts) < expected_hosts:
|
|
72
|
+
await got_host.wait()
|
|
73
|
+
got_host.clear()
|
|
74
|
+
while not self.hosts:
|
|
75
|
+
await asyncio.sleep(min_wait_time)
|
|
76
|
+
await browser.async_cancel()
|
|
77
|
+
if tasks:
|
|
78
|
+
await asyncio.gather(*tasks)
|
|
79
|
+
|
|
80
|
+
async def async_discover_hosts(
|
|
81
|
+
self,
|
|
82
|
+
timeout: float = 10,
|
|
83
|
+
min_wait_time: float = 3,
|
|
84
|
+
expected_hosts: Optional[int] = None
|
|
85
|
+
) -> bool:
|
|
86
|
+
"""Return true if Velux KLF200 devices found on the network.
|
|
87
|
+
|
|
88
|
+
This function creates a zeroconf AsyncServiceBrowser and
|
|
89
|
+
waits min_wait_time (seconds) for ServiceInfos from hosts.
|
|
90
|
+
Some devices may take some time to respond (i.e. if they currently have a high CPU load).
|
|
91
|
+
If one or more Hosts are found, the function cancels the ServiceBrowser and returns true.
|
|
92
|
+
If expected_hosts is set, the function ignores min_wait_time and returns true once expected_hosts are found.
|
|
93
|
+
If timeout (seconds) is exceeded, the function returns false.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
async with asyncio.timeout(timeout):
|
|
97
|
+
await self._async_discover_hosts(min_wait_time, expected_hosts)
|
|
98
|
+
except TimeoutError:
|
|
99
|
+
return False
|
|
100
|
+
return True
|
pyvlx/exception.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Module for PyVLX Exceptions."""
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PyVLXException(Exception):
|
|
6
|
+
"""Default PyVLX Exception."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, description: str, **kwargs: Any):
|
|
9
|
+
"""Initialize PyVLXException class."""
|
|
10
|
+
super().__init__(description)
|
|
11
|
+
self.description = description
|
|
12
|
+
self.parameter = kwargs
|
|
13
|
+
|
|
14
|
+
def _format_parameter(self) -> str:
|
|
15
|
+
return " ".join(
|
|
16
|
+
[
|
|
17
|
+
'%s="%s"' % (key, value)
|
|
18
|
+
for (key, value) in sorted(self.parameter.items())
|
|
19
|
+
]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
"""Return object as readable string."""
|
|
24
|
+
return '<{} description="{}" {}/>'.format(
|
|
25
|
+
type(self).__name__, self.description, self._format_parameter()
|
|
26
|
+
)
|
pyvlx/heartbeat.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Module for sending get state requests to API in regular periods."""
|
|
2
|
+
import asyncio
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from .api import GetState
|
|
6
|
+
from .api.status_request import StatusRequest
|
|
7
|
+
from .exception import PyVLXException
|
|
8
|
+
from .log import PYVLXLOG
|
|
9
|
+
from .opening_device import Blind, DualRollerShutter
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pyvlx import PyVLX
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Heartbeat:
|
|
16
|
+
"""Class for sending heartbeats to API."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, pyvlx: "PyVLX", interval: int = 30, load_all_states: bool = True
|
|
20
|
+
):
|
|
21
|
+
"""Initialize Heartbeat object."""
|
|
22
|
+
PYVLXLOG.debug("Heartbeat __init__")
|
|
23
|
+
self.pyvlx = pyvlx
|
|
24
|
+
self.interval = interval
|
|
25
|
+
self.load_all_states = load_all_states
|
|
26
|
+
self.task: Any = None
|
|
27
|
+
|
|
28
|
+
async def _run(self) -> None:
|
|
29
|
+
PYVLXLOG.debug("Heartbeat: task started")
|
|
30
|
+
while True:
|
|
31
|
+
PYVLXLOG.debug("Heartbeat: sleeping")
|
|
32
|
+
await asyncio.sleep(self.interval)
|
|
33
|
+
PYVLXLOG.debug("Heartbeat: pulsing")
|
|
34
|
+
try:
|
|
35
|
+
await self.pulse()
|
|
36
|
+
except (OSError, PyVLXException) as e:
|
|
37
|
+
PYVLXLOG.debug("Heartbeat: pulsing failed: %s", e)
|
|
38
|
+
|
|
39
|
+
async def _start(self) -> None:
|
|
40
|
+
if self.task is not None:
|
|
41
|
+
await self.stop()
|
|
42
|
+
PYVLXLOG.debug("Heartbeat: creating task")
|
|
43
|
+
self.task = asyncio.create_task(self._run())
|
|
44
|
+
|
|
45
|
+
def start(self) -> None:
|
|
46
|
+
"""Start heartbeat."""
|
|
47
|
+
PYVLXLOG.debug("Heartbeat start")
|
|
48
|
+
asyncio.run_coroutine_threadsafe(self._start(), self.pyvlx.loop)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def stopped(self) -> bool:
|
|
52
|
+
"""Return Heartbeat running state."""
|
|
53
|
+
return self.task is None
|
|
54
|
+
|
|
55
|
+
async def stop(self) -> None:
|
|
56
|
+
"""Stop heartbeat."""
|
|
57
|
+
if self.task is not None:
|
|
58
|
+
self.task.cancel()
|
|
59
|
+
self.task = None
|
|
60
|
+
PYVLXLOG.debug("Heartbeat stopped")
|
|
61
|
+
else:
|
|
62
|
+
PYVLXLOG.debug("Heartbeat was not running")
|
|
63
|
+
|
|
64
|
+
async def pulse(self) -> None:
|
|
65
|
+
"""Send get state request to API to keep the connection alive."""
|
|
66
|
+
PYVLXLOG.debug("Heartbeat pulse")
|
|
67
|
+
get_state = GetState(pyvlx=self.pyvlx)
|
|
68
|
+
await get_state.do_api_call()
|
|
69
|
+
if not get_state.success:
|
|
70
|
+
raise PyVLXException("Unable to send get state.")
|
|
71
|
+
|
|
72
|
+
# If nodes contain Blind or DualRollerShutter device, refresh orientation or upper/lower curtain positions because House Monitoring
|
|
73
|
+
# delivers wrong values for FP1, FP2 and FP3 parameter
|
|
74
|
+
for node in self.pyvlx.nodes:
|
|
75
|
+
if isinstance(node, (Blind, DualRollerShutter)) or self.load_all_states:
|
|
76
|
+
status_request = StatusRequest(self.pyvlx, node.node_id)
|
|
77
|
+
await status_request.do_api_call()
|
|
78
|
+
# give user requests a chance
|
|
79
|
+
await asyncio.sleep(0.5)
|
pyvlx/klf200gateway.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Module for basic klf200 gateway functions."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional
|
|
4
|
+
|
|
5
|
+
from .api import (
|
|
6
|
+
FactoryDefault, GetLocalTime, GetNetworkSetup, GetProtocolVersion,
|
|
7
|
+
GetState, GetVersion, HouseStatusMonitorDisable, HouseStatusMonitorEnable,
|
|
8
|
+
LeaveLearnState, PasswordEnter, Reboot, SetUTC)
|
|
9
|
+
from .dataobjects import (
|
|
10
|
+
DtoLocalTime, DtoNetworkSetup, DtoProtocolVersion, DtoState, DtoVersion)
|
|
11
|
+
from .exception import PyVLXException
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pyvlx import PyVLX
|
|
15
|
+
|
|
16
|
+
CallbackType = Callable[["Klf200Gateway"], Awaitable[None]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Klf200Gateway:
|
|
20
|
+
"""Class for node abstraction."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, pyvlx: "PyVLX"):
|
|
23
|
+
"""Initialize Node object."""
|
|
24
|
+
self.pyvlx = pyvlx
|
|
25
|
+
self.state: Optional[DtoState] = None
|
|
26
|
+
self.network_setup: Optional[DtoNetworkSetup] = None
|
|
27
|
+
self.password: Optional[str] = None
|
|
28
|
+
self.time: Optional[DtoLocalTime] = None
|
|
29
|
+
self.protocol_version: Optional[DtoProtocolVersion] = None
|
|
30
|
+
self.version: Optional[DtoVersion] = None
|
|
31
|
+
self.device_updated_cbs: List[CallbackType] = []
|
|
32
|
+
self.house_status_monitor_enabled = False
|
|
33
|
+
|
|
34
|
+
def register_device_updated_cb(self, device_updated_cb: CallbackType) -> None:
|
|
35
|
+
"""Register device updated callback."""
|
|
36
|
+
self.device_updated_cbs.append(device_updated_cb)
|
|
37
|
+
|
|
38
|
+
def unregister_device_updated_cb(self, device_updated_cb: CallbackType) -> None:
|
|
39
|
+
"""Unregister device updated callback."""
|
|
40
|
+
self.device_updated_cbs.remove(device_updated_cb)
|
|
41
|
+
|
|
42
|
+
async def after_update(self) -> None:
|
|
43
|
+
"""Execute callbacks after internal state has been changed."""
|
|
44
|
+
for device_updated_cb in self.device_updated_cbs:
|
|
45
|
+
# pylint: disable=not-callable
|
|
46
|
+
await device_updated_cb(self)
|
|
47
|
+
|
|
48
|
+
async def get_state(self) -> bool:
|
|
49
|
+
"""Retrieve state from API."""
|
|
50
|
+
get_state = GetState(pyvlx=self.pyvlx)
|
|
51
|
+
await get_state.do_api_call()
|
|
52
|
+
if not get_state.success:
|
|
53
|
+
raise PyVLXException("Unable to retrieve state")
|
|
54
|
+
self.state = get_state.state
|
|
55
|
+
return get_state.success
|
|
56
|
+
|
|
57
|
+
async def get_network_setup(self) -> bool:
|
|
58
|
+
"""Retrieve network setup from API."""
|
|
59
|
+
get_network_setup = GetNetworkSetup(pyvlx=self.pyvlx)
|
|
60
|
+
await get_network_setup.do_api_call()
|
|
61
|
+
if not get_network_setup.success:
|
|
62
|
+
raise PyVLXException("Unable to retrieve network setup")
|
|
63
|
+
self.network_setup = get_network_setup.networksetup
|
|
64
|
+
return get_network_setup.success
|
|
65
|
+
|
|
66
|
+
async def get_version(self) -> bool:
|
|
67
|
+
"""Retrieve version from API."""
|
|
68
|
+
get_version = GetVersion(pyvlx=self.pyvlx)
|
|
69
|
+
await get_version.do_api_call()
|
|
70
|
+
if not get_version.success:
|
|
71
|
+
raise PyVLXException("Unable to retrieve version")
|
|
72
|
+
self.version = get_version.version
|
|
73
|
+
return get_version.success
|
|
74
|
+
|
|
75
|
+
async def get_protocol_version(self) -> bool:
|
|
76
|
+
"""Retrieve protocol version from API."""
|
|
77
|
+
get_protocol_version = GetProtocolVersion(pyvlx=self.pyvlx)
|
|
78
|
+
await get_protocol_version.do_api_call()
|
|
79
|
+
if not get_protocol_version.success:
|
|
80
|
+
raise PyVLXException("Unable to retrieve protocol version")
|
|
81
|
+
self.protocol_version = get_protocol_version.protocolversion
|
|
82
|
+
return get_protocol_version.success
|
|
83
|
+
|
|
84
|
+
async def leave_learn_state(self) -> bool:
|
|
85
|
+
"""Leave Learn state from API."""
|
|
86
|
+
leave_learn_state = LeaveLearnState(pyvlx=self.pyvlx)
|
|
87
|
+
await leave_learn_state.do_api_call()
|
|
88
|
+
if not leave_learn_state.success:
|
|
89
|
+
raise PyVLXException("Unable to leave learn state")
|
|
90
|
+
return leave_learn_state.success
|
|
91
|
+
|
|
92
|
+
async def set_utc(self) -> bool:
|
|
93
|
+
"""Set UTC Clock."""
|
|
94
|
+
setutc = SetUTC(pyvlx=self.pyvlx)
|
|
95
|
+
await setutc.do_api_call()
|
|
96
|
+
if not setutc.success:
|
|
97
|
+
raise PyVLXException("Unable to set utc.")
|
|
98
|
+
return setutc.success
|
|
99
|
+
|
|
100
|
+
async def set_rtc_time_zone(self) -> None:
|
|
101
|
+
"""Set the RTC Time Zone."""
|
|
102
|
+
# idontwant = setrtctimezone(pyvlx=self.pyvlx)
|
|
103
|
+
raise PyVLXException("KLF 200 RTC Timezone Set not implemented")
|
|
104
|
+
# return setrtctimezone.success
|
|
105
|
+
|
|
106
|
+
async def reboot(self) -> bool:
|
|
107
|
+
"""Reboot gateway."""
|
|
108
|
+
reboot = Reboot(pyvlx=self.pyvlx)
|
|
109
|
+
await reboot.do_api_call()
|
|
110
|
+
if not reboot.success:
|
|
111
|
+
raise PyVLXException("Unable to reboot gateway.")
|
|
112
|
+
await self.pyvlx.disconnect()
|
|
113
|
+
return reboot.success
|
|
114
|
+
|
|
115
|
+
async def set_factory_default(self) -> bool:
|
|
116
|
+
"""Set Gateway to Factory Default."""
|
|
117
|
+
factorydefault = FactoryDefault(pyvlx=self.pyvlx)
|
|
118
|
+
await factorydefault.do_api_call()
|
|
119
|
+
if not factorydefault.success:
|
|
120
|
+
raise PyVLXException("Unable to factory Default Reset gateway.")
|
|
121
|
+
return factorydefault.success
|
|
122
|
+
|
|
123
|
+
async def get_local_time(self) -> bool:
|
|
124
|
+
"""Get local time from gateway."""
|
|
125
|
+
getlocaltime = GetLocalTime(pyvlx=self.pyvlx)
|
|
126
|
+
await getlocaltime.do_api_call()
|
|
127
|
+
if not getlocaltime.success:
|
|
128
|
+
raise PyVLXException("Unable to get local time.")
|
|
129
|
+
self.time = getlocaltime.localtime
|
|
130
|
+
return getlocaltime.success
|
|
131
|
+
|
|
132
|
+
async def password_enter(self, password: str) -> bool:
|
|
133
|
+
"""Get enter Password for gateway."""
|
|
134
|
+
self.password = password
|
|
135
|
+
passwordenter = PasswordEnter(pyvlx=self.pyvlx, password=self.password)
|
|
136
|
+
await passwordenter.do_api_call()
|
|
137
|
+
if not passwordenter.success:
|
|
138
|
+
raise PyVLXException("Login to KLF 200 failed, check credentials")
|
|
139
|
+
return passwordenter.success
|
|
140
|
+
|
|
141
|
+
async def house_status_monitor_enable(self, pyvlx: "PyVLX") -> None:
|
|
142
|
+
"""Enable house status monitor."""
|
|
143
|
+
status_monitor_enable = HouseStatusMonitorEnable(pyvlx=pyvlx)
|
|
144
|
+
await status_monitor_enable.do_api_call()
|
|
145
|
+
if not status_monitor_enable.success:
|
|
146
|
+
raise PyVLXException("Unable enable house status monitor.")
|
|
147
|
+
self.house_status_monitor_enabled = True
|
|
148
|
+
|
|
149
|
+
async def house_status_monitor_disable(self, pyvlx: "PyVLX", timeout: Optional[int] = None) -> None:
|
|
150
|
+
"""Disable house status monitor."""
|
|
151
|
+
status_monitor_disable = HouseStatusMonitorDisable(pyvlx=pyvlx)
|
|
152
|
+
if timeout is not None:
|
|
153
|
+
status_monitor_disable.timeout_in_seconds = timeout
|
|
154
|
+
await status_monitor_disable.do_api_call()
|
|
155
|
+
if not status_monitor_disable.success:
|
|
156
|
+
raise PyVLXException("Unable disable house status monitor.")
|
|
157
|
+
self.house_status_monitor_enabled = False
|
|
158
|
+
|
|
159
|
+
def __str__(self) -> str:
|
|
160
|
+
"""Return object as readable string."""
|
|
161
|
+
return '<{} state="{}" network_setup="{}" version="{}" protocol_version="{}"/>'.format(
|
|
162
|
+
type(self).__name__,
|
|
163
|
+
str(self.state),
|
|
164
|
+
str(self.network_setup),
|
|
165
|
+
str(self.version),
|
|
166
|
+
str(self.protocol_version),
|
|
167
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Module for lights."""
|
|
2
|
+
from typing import TYPE_CHECKING, Optional
|
|
3
|
+
|
|
4
|
+
from .api import CommandSend
|
|
5
|
+
from .node import Node
|
|
6
|
+
from .parameter import Intensity
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pyvlx import PyVLX
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LighteningDevice(Node):
|
|
13
|
+
"""Meta class for turning on device with one main parameter for intensity."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, pyvlx: "PyVLX", node_id: int, name: str, serial_number: Optional[str]):
|
|
16
|
+
"""Initialize turning on device.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
* pyvlx: PyVLX object
|
|
20
|
+
* node_id: internal id for addressing nodes.
|
|
21
|
+
Provided by KLF 200 device
|
|
22
|
+
* name: node name
|
|
23
|
+
* serial_number: serial number of the node.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
super().__init__(
|
|
27
|
+
pyvlx=pyvlx, node_id=node_id, name=name, serial_number=serial_number
|
|
28
|
+
)
|
|
29
|
+
self.intensity = Intensity()
|
|
30
|
+
|
|
31
|
+
async def set_intensity(self, intensity: Intensity, wait_for_completion: bool = True) -> None:
|
|
32
|
+
"""Set light to desired intensity.
|
|
33
|
+
|
|
34
|
+
Parameters:
|
|
35
|
+
* intensity: Intensity object containing the target intensity.
|
|
36
|
+
* wait_for_completion: If set, function will return
|
|
37
|
+
after device has reached target intensity.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
command = CommandSend(
|
|
41
|
+
pyvlx=self.pyvlx,
|
|
42
|
+
wait_for_completion=wait_for_completion,
|
|
43
|
+
node_id=self.node_id,
|
|
44
|
+
parameter=intensity,
|
|
45
|
+
)
|
|
46
|
+
await command.send()
|
|
47
|
+
await self.after_update()
|
|
48
|
+
|
|
49
|
+
async def turn_on(self, wait_for_completion: bool = True) -> None:
|
|
50
|
+
"""Turn on light.
|
|
51
|
+
|
|
52
|
+
Parameters:
|
|
53
|
+
* wait_for_completion: If set, function will return
|
|
54
|
+
after device has reached target intensity.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
await self.set_intensity(
|
|
58
|
+
intensity=Intensity(intensity_percent=0),
|
|
59
|
+
wait_for_completion=wait_for_completion,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async def turn_off(self, wait_for_completion: bool = True) -> None:
|
|
63
|
+
"""Turn off light.
|
|
64
|
+
|
|
65
|
+
Parameters:
|
|
66
|
+
* wait_for_completion: If set, function will return
|
|
67
|
+
after device has reached target intensity.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
await self.set_intensity(
|
|
71
|
+
intensity=Intensity(intensity_percent=100),
|
|
72
|
+
wait_for_completion=wait_for_completion,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Light(LighteningDevice):
|
|
77
|
+
"""Light object."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, pyvlx: "PyVLX", node_id: int, name: str, serial_number: Optional[str]):
|
|
80
|
+
"""Initialize Light class.
|
|
81
|
+
|
|
82
|
+
Parameters:
|
|
83
|
+
* pyvlx: PyVLX object
|
|
84
|
+
* node_id: internal id for addressing nodes.
|
|
85
|
+
Provided by KLF 200 device
|
|
86
|
+
* name: node name
|
|
87
|
+
* serial_number: serial number of the node.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
super().__init__(
|
|
91
|
+
pyvlx=pyvlx, node_id=node_id, name=name, serial_number=serial_number
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def __str__(self) -> str:
|
|
95
|
+
"""Return object as readable string."""
|
|
96
|
+
return (
|
|
97
|
+
'<{} name="{}" '
|
|
98
|
+
'node_id="{}" '
|
|
99
|
+
'serial_number="{}"/>'.format(
|
|
100
|
+
type(self).__name__, self.name, self.node_id, self.serial_number
|
|
101
|
+
)
|
|
102
|
+
)
|
pyvlx/log.py
ADDED