velbus-aio 2024.4.0__py3-none-any.whl → 2024.5.1__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.
Potentially problematic release.
This version of velbus-aio might be problematic. Click here for more details.
- {velbus_aio-2024.4.0.dist-info → velbus_aio-2024.5.1.dist-info}/METADATA +2 -2
- {velbus_aio-2024.4.0.dist-info → velbus_aio-2024.5.1.dist-info}/RECORD +21 -21
- velbusaio/channels.py +5 -11
- velbusaio/command_registry.py +12 -1
- velbusaio/const.py +2 -1
- velbusaio/controller.py +51 -79
- velbusaio/handler.py +3 -2
- velbusaio/helpers.py +1 -1
- velbusaio/message.py +0 -1
- velbusaio/messages/cover_off.py +0 -2
- velbusaio/messages/cover_position.py +0 -2
- velbusaio/messages/set_date.py +4 -10
- velbusaio/messages/set_daylight_saving.py +2 -6
- velbusaio/messages/set_dimmer.py +7 -12
- velbusaio/messages/set_realtime_clock.py +4 -10
- velbusaio/module.py +46 -42
- velbusaio/protocol.json +9 -5
- velbusaio/util.py +4 -0
- {velbus_aio-2024.4.0.dist-info → velbus_aio-2024.5.1.dist-info}/LICENSE +0 -0
- {velbus_aio-2024.4.0.dist-info → velbus_aio-2024.5.1.dist-info}/WHEEL +0 -0
- {velbus_aio-2024.4.0.dist-info → velbus_aio-2024.5.1.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: velbus-aio
|
|
3
|
-
Version: 2024.
|
|
3
|
+
Version: 2024.5.1
|
|
4
4
|
Summary: Open-source home automation platform running on Python 3.
|
|
5
5
|
Author-email: Maikel Punie <maikel.punie@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -26,7 +26,7 @@ Requires-Python: >=3.8.0
|
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: pyserial >=3.5.0
|
|
29
|
-
Requires-Dist: pyserial-asyncio >=0.
|
|
29
|
+
Requires-Dist: pyserial-asyncio-fast >=0.11
|
|
30
30
|
Requires-Dist: backoff >=1.10.0
|
|
31
31
|
|
|
32
32
|

|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
velbusaio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
velbusaio/channels.py,sha256=
|
|
3
|
-
velbusaio/command_registry.py,sha256=
|
|
4
|
-
velbusaio/const.py,sha256=
|
|
5
|
-
velbusaio/controller.py,sha256=
|
|
2
|
+
velbusaio/channels.py,sha256=1fqxsNHmJmb6lYRr4UqP9HLeTprbvektzrjQ8QBvWi0,22889
|
|
3
|
+
velbusaio/command_registry.py,sha256=j9KuLmada41PMEdtfPvLmXVOAZOkBM9zi-3kluwYffk,5140
|
|
4
|
+
velbusaio/const.py,sha256=aysTlQcpACZaDVqoXAjWmaEDGlCB7KT04Rv-SAusPFc,1575
|
|
5
|
+
velbusaio/controller.py,sha256=txOd0JuYi_OGMzO1t2u7KXlF085HWa1HVlszz7A2O5c,8846
|
|
6
6
|
velbusaio/discovery.py,sha256=Px6qoZl4QhF17aMz6JxstCORBpLzZGWEK9h4Vyvg57o,1649
|
|
7
7
|
velbusaio/exceptions.py,sha256=FHkXaM3dK5Gkk-QGAf9dLE3FPlCU2FRZWUyY-4KRNnA,515
|
|
8
|
-
velbusaio/handler.py,sha256=
|
|
9
|
-
velbusaio/helpers.py,sha256=
|
|
10
|
-
velbusaio/message.py,sha256=
|
|
11
|
-
velbusaio/module.py,sha256=
|
|
12
|
-
velbusaio/protocol.json,sha256=
|
|
8
|
+
velbusaio/handler.py,sha256=jTWIV26ibAqs3rcTku81NIHlPUeyWjStSih1Nfx-an0,5946
|
|
9
|
+
velbusaio/helpers.py,sha256=iqpoereRH4JY5WAkozIqWvXWyRmhko-J-UGXDylFyEM,2537
|
|
10
|
+
velbusaio/message.py,sha256=gnhXpA5NAG4qnI9vlxwlWQKETWneDb1lp0P9w_-nxhQ,5020
|
|
11
|
+
velbusaio/module.py,sha256=_yFxZIi71eqq9xxNal-9ATNHGMjZDOr-dcpKYIxa7_8,35458
|
|
12
|
+
velbusaio/protocol.json,sha256=7PifRZXctU5zSsodDGsUAhnnSsGNlcGcim1gr2su720,250396
|
|
13
13
|
velbusaio/protocol.py,sha256=ofDwJfwyrVQDQ40WX2QdH1nTK-y2HibQPTo5dFY7SkQ,8224
|
|
14
14
|
velbusaio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
velbusaio/raw_message.py,sha256=ODoTPVAvyXXQSMXxtQO1U5OxcoQ4W8JJN5fotEDGSTo,4690
|
|
16
|
-
velbusaio/util.py,sha256=
|
|
16
|
+
velbusaio/util.py,sha256=FW6YCiPYWOCgqHDs8-LbzME0h81mqftYVG0qqZ-oo7Y,1568
|
|
17
17
|
velbusaio/messages/__init__.py,sha256=hLkLhsJ5hcgRnhaoyFWz1g2vphdnjrXURxgwezkDBwU,5893
|
|
18
18
|
velbusaio/messages/blind_status.py,sha256=yzjoYLBxH85tBjihiGzLqebRg1urz0obBA-3q1pqSCE,3418
|
|
19
19
|
velbusaio/messages/bus_active.py,sha256=AB1mEvbMXRuOaC2CQ7hzKSLmbaJnyFqqXrARi-aSdtg,749
|
|
@@ -28,8 +28,8 @@ velbusaio/messages/clear_led.py,sha256=AcGRCD0OdlHcFonvHA075nVwvOtZ6KfyjrXeE6A7D
|
|
|
28
28
|
velbusaio/messages/counter_status.py,sha256=UgSRw-pipFb8C2DxauHw8Pjem1qTvzUFpiahui73-Is,1240
|
|
29
29
|
velbusaio/messages/counter_status_request.py,sha256=VLFaI7MFnTCH1s8acfKlcAGscJElwxYRC56eT3bbB7Y,919
|
|
30
30
|
velbusaio/messages/cover_down.py,sha256=tdjsRijrEa5oNBZk6uKWUK39k5gIjcWq0kgfOf4f1lQ,2552
|
|
31
|
-
velbusaio/messages/cover_off.py,sha256=
|
|
32
|
-
velbusaio/messages/cover_position.py,sha256=
|
|
31
|
+
velbusaio/messages/cover_off.py,sha256=0RHAJb4et99yAoo8LD85Rzlnhrqr40Wjba-BhzHSr4o,2243
|
|
32
|
+
velbusaio/messages/cover_position.py,sha256=PhRocdkbwf8CUfdL8-rZHEDFD2Z9u7DLZOpH3h6gAtg,1268
|
|
33
33
|
velbusaio/messages/cover_up.py,sha256=P20AZx8qQScbyV0sxtZOlO03Q8pFO8NunpqIr4mZ6Lw,2547
|
|
34
34
|
velbusaio/messages/dali_device_settings.py,sha256=KzJ7OFkFRPuCXSOThuplM48J_kH0I3RE82ZRCpmiqYA,4870
|
|
35
35
|
velbusaio/messages/dali_device_settings_request.py,sha256=InTnrwLD7l9gCHXKlJ69FHMXcejtYMUZBtnffx9nDQU,1476
|
|
@@ -67,11 +67,11 @@ velbusaio/messages/select_program.py,sha256=9jLhsCuYGQsw-FMxpmNljPZifGeE_oJ6A5bg
|
|
|
67
67
|
velbusaio/messages/sensor_settings_request.py,sha256=Uwywf0JGwBwc_2ZlfxVJicm3oUlWBNEcclcHA2_Mv5I,819
|
|
68
68
|
velbusaio/messages/sensor_temp_request.py,sha256=bTChj9udOjIww9vJ2SWmMtbry_8jLDvbWSAy_ZPmrb4,599
|
|
69
69
|
velbusaio/messages/sensor_temperature.py,sha256=ThA_4FFhQutCSt8147yCy7ocUFfizWaUV2JHoNzesNc,1558
|
|
70
|
-
velbusaio/messages/set_date.py,sha256=
|
|
71
|
-
velbusaio/messages/set_daylight_saving.py,sha256=
|
|
72
|
-
velbusaio/messages/set_dimmer.py,sha256=
|
|
70
|
+
velbusaio/messages/set_date.py,sha256=jcWb8c5mB71HNBwLCQ9ocEPSHQdZP0yfGJL4_swsRWQ,1359
|
|
71
|
+
velbusaio/messages/set_daylight_saving.py,sha256=CpoKrSt62UzzNxl8SDcpDcb0GKnLcD1f73-ULzqDEzU,1049
|
|
72
|
+
velbusaio/messages/set_dimmer.py,sha256=b32lBHrvZRQ7kKIFhQwuBqk9rXagnBtcJxo1V7V5x_0,2050
|
|
73
73
|
velbusaio/messages/set_led.py,sha256=X8tIDzJgtf5EsH92rjoUyrECNFW-ECgAQoQ9TnvJEQk,900
|
|
74
|
-
velbusaio/messages/set_realtime_clock.py,sha256=
|
|
74
|
+
velbusaio/messages/set_realtime_clock.py,sha256=h34pCvyPm7tSQ5BdB8yC8SRulrtldeX-ZJFv3Wcvdk0,1209
|
|
75
75
|
velbusaio/messages/set_temperature.py,sha256=78LfGOBZo2bw7eF38uyXOjUcJrsSvQPDW2qbXLOYd0o,959
|
|
76
76
|
velbusaio/messages/slider_status.py,sha256=AlBI1DJPLUwxOFfPIKYv15bFOnVXm__Tym7AW4xGMxo,1418
|
|
77
77
|
velbusaio/messages/slow_blinking_led.py,sha256=UktzpRtyM4mR7_KQuNjqenPEeM3P1vgTyMW8dnVwylQ,909
|
|
@@ -96,8 +96,8 @@ velbusaio/messages/very_fast_blinking_led.py,sha256=vlMEern8PoOvtO5JaAk9erMR4IKJ
|
|
|
96
96
|
velbusaio/messages/write_data_to_memory.py,sha256=gr6bi4SzK8Mw8fnp8yV-STq5jts7NoeV7zZgdptH5Vs,1039
|
|
97
97
|
velbusaio/messages/write_memory_block.py,sha256=zGnNxx_M66HpBQ8S7kagtNw8_qSRHsOLk1MuiS0uygM,1032
|
|
98
98
|
velbusaio/messages/write_module_address_and_serial_number.py,sha256=6y57j-md3btNtQddX5CUREtSs1Dzgkd953sQPZ3Pioo,1597
|
|
99
|
-
velbus_aio-2024.
|
|
100
|
-
velbus_aio-2024.
|
|
101
|
-
velbus_aio-2024.
|
|
102
|
-
velbus_aio-2024.
|
|
103
|
-
velbus_aio-2024.
|
|
99
|
+
velbus_aio-2024.5.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
100
|
+
velbus_aio-2024.5.1.dist-info/METADATA,sha256=z1Q-cGFQfsDQYGHAppFwXJUDplFi6qaFKIGJW0Me3c4,3259
|
|
101
|
+
velbus_aio-2024.5.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
102
|
+
velbus_aio-2024.5.1.dist-info/top_level.txt,sha256=W0-lSOwD23mm8FqaIe9vY20fKicBMIdUVjF-zmfxRnY,15
|
|
103
|
+
velbus_aio-2024.5.1.dist-info/RECORD,,
|
velbusaio/channels.py
CHANGED
|
@@ -136,17 +136,11 @@ class Channel:
|
|
|
136
136
|
if k != "_writer" and k != "_on_status_update" and k != "_name_parts"
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
def
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if k != "_writer"
|
|
145
|
-
and k != "_on_status_update"
|
|
146
|
-
and k != "_name_parts"
|
|
147
|
-
and k != "_module"
|
|
148
|
-
and k != "__name__"
|
|
149
|
-
}
|
|
139
|
+
def to_cache(self) -> dict:
|
|
140
|
+
dst = {"name": self._name, "type": type(self).__name__}
|
|
141
|
+
if hasattr(self, "_Unit"):
|
|
142
|
+
dst["Unit"] = self._Unit
|
|
143
|
+
return dst
|
|
150
144
|
|
|
151
145
|
def __setstate__(self, state):
|
|
152
146
|
self.__dict__.update(state)
|
velbusaio/command_registry.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Command registry.
|
|
2
|
+
|
|
2
3
|
:author: Maikel Punie <maikel.punie@gmail.com> and Thomas Delaet <thomas@delaet.org>
|
|
3
4
|
"""
|
|
4
5
|
|
|
@@ -87,7 +88,10 @@ MODULE_DIRECTORY = {
|
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
class CommandRegistry:
|
|
91
|
+
"""Command registry class."""
|
|
92
|
+
|
|
90
93
|
def __init__(self, module_directory: dict) -> None:
|
|
94
|
+
"""Init method."""
|
|
91
95
|
self._module_directory = module_directory
|
|
92
96
|
self._default_commands = {}
|
|
93
97
|
self._overrides = {}
|
|
@@ -95,6 +99,7 @@ class CommandRegistry:
|
|
|
95
99
|
def register_command(
|
|
96
100
|
self, command_value: int, command_class: type, module_name: str | None = None
|
|
97
101
|
) -> None:
|
|
102
|
+
"""Register a command."""
|
|
98
103
|
if command_value < 0 or command_value > 255:
|
|
99
104
|
raise ValueError("Command_value should be >=0 and <=255")
|
|
100
105
|
if module_name and module_name not in self._module_directory.values():
|
|
@@ -115,6 +120,7 @@ class CommandRegistry:
|
|
|
115
120
|
def _register_override(
|
|
116
121
|
self, command_value: int, command_class: type, module_type: str
|
|
117
122
|
) -> None:
|
|
123
|
+
"""Register and override."""
|
|
118
124
|
if module_type not in self._overrides:
|
|
119
125
|
self._overrides[module_type] = {}
|
|
120
126
|
if command_value not in self._overrides[module_type]:
|
|
@@ -125,12 +131,14 @@ class CommandRegistry:
|
|
|
125
131
|
)
|
|
126
132
|
|
|
127
133
|
def _register_default(self, command_value: int, command_class: type) -> None:
|
|
134
|
+
"""Register a default command."""
|
|
128
135
|
if command_value not in self._default_commands:
|
|
129
136
|
self._default_commands[command_value] = command_class
|
|
130
137
|
else:
|
|
131
138
|
raise Exception("double registration in command registry")
|
|
132
139
|
|
|
133
140
|
def has_command(self, command_value: int, module_type: int = 0) -> bool:
|
|
141
|
+
"""Find a command."""
|
|
134
142
|
if module_type in self._overrides:
|
|
135
143
|
if command_value in self._overrides[module_type]:
|
|
136
144
|
return True
|
|
@@ -139,6 +147,7 @@ class CommandRegistry:
|
|
|
139
147
|
return False
|
|
140
148
|
|
|
141
149
|
def get_command(self, command_value: int, module_type: int = 0) -> None | type:
|
|
150
|
+
"""Search a command in the registry."""
|
|
142
151
|
if module_type in self._overrides:
|
|
143
152
|
if command_value in self._overrides[module_type]:
|
|
144
153
|
return self._overrides[module_type][command_value]
|
|
@@ -151,6 +160,8 @@ commandRegistry = CommandRegistry(MODULE_DIRECTORY)
|
|
|
151
160
|
|
|
152
161
|
|
|
153
162
|
def register(command_value: int, module_types: list[str] | None = None):
|
|
163
|
+
"""Register decorator."""
|
|
164
|
+
|
|
154
165
|
def inner_register(command_class):
|
|
155
166
|
if module_types:
|
|
156
167
|
for module_type in module_types:
|
velbusaio/const.py
CHANGED
velbusaio/controller.py
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Main interface for the velbusaio lib
|
|
3
|
-
"""
|
|
1
|
+
"""Main interface for the velbusaio lib."""
|
|
4
2
|
|
|
5
3
|
from __future__ import annotations
|
|
6
4
|
|
|
7
5
|
import asyncio
|
|
8
6
|
import logging
|
|
9
7
|
import pathlib
|
|
10
|
-
import pickle
|
|
11
8
|
import re
|
|
12
9
|
import ssl
|
|
10
|
+
import time
|
|
13
11
|
from urllib.parse import urlparse
|
|
14
12
|
|
|
15
13
|
import serial
|
|
16
|
-
import
|
|
14
|
+
import serial_asyncio_fast
|
|
17
15
|
|
|
18
16
|
from velbusaio.channels import Channel
|
|
19
17
|
from velbusaio.const import LOAD_TIMEOUT
|
|
@@ -31,15 +29,14 @@ from velbusaio.raw_message import RawMessage
|
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
class Velbus:
|
|
34
|
-
"""
|
|
35
|
-
A velbus controller
|
|
36
|
-
"""
|
|
32
|
+
"""A velbus controller."""
|
|
37
33
|
|
|
38
34
|
def __init__(
|
|
39
35
|
self,
|
|
40
36
|
dsn: str,
|
|
41
37
|
cache_dir: str = get_cache_dir(),
|
|
42
38
|
) -> None:
|
|
39
|
+
"""Init the Velbus controller."""
|
|
43
40
|
self._log = logging.getLogger("velbus")
|
|
44
41
|
|
|
45
42
|
self._protocol = VelbusProtocol(
|
|
@@ -60,6 +57,7 @@ class Velbus:
|
|
|
60
57
|
pathlib.Path(self._cache_dir).mkdir(parents=True, exist_ok=True)
|
|
61
58
|
|
|
62
59
|
async def _on_message_received(self, msg: RawMessage) -> None:
|
|
60
|
+
"""On message received function."""
|
|
63
61
|
await self._handler.handle(msg)
|
|
64
62
|
|
|
65
63
|
def _on_connection_lost(self, exc: Exception) -> None:
|
|
@@ -69,6 +67,7 @@ class Velbus:
|
|
|
69
67
|
asyncio.ensure_future(self.connect())
|
|
70
68
|
|
|
71
69
|
def _on_end_of_scan(self) -> None:
|
|
70
|
+
"""Notify the scan failure."""
|
|
72
71
|
self._handler.scan_finished()
|
|
73
72
|
|
|
74
73
|
async def add_module(
|
|
@@ -81,31 +80,23 @@ class Velbus:
|
|
|
81
80
|
build_year: int | None = None,
|
|
82
81
|
build_week: int | None = None,
|
|
83
82
|
) -> None:
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
self.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
data,
|
|
99
|
-
serial=serial,
|
|
100
|
-
build_year=build_year,
|
|
101
|
-
build_week=build_week,
|
|
102
|
-
memorymap=memorymap,
|
|
103
|
-
cache_dir=self._cache_dir,
|
|
104
|
-
)
|
|
105
|
-
self._modules[addr].initialize(self.send)
|
|
106
|
-
await self._modules[addr].load()
|
|
83
|
+
"""Add a found module to the module cache."""
|
|
84
|
+
self._log.info(f"Found module: type:{typ} address:{addr}")
|
|
85
|
+
self._modules[addr] = Module.factory(
|
|
86
|
+
addr,
|
|
87
|
+
typ,
|
|
88
|
+
data,
|
|
89
|
+
serial=serial,
|
|
90
|
+
build_year=build_year,
|
|
91
|
+
build_week=build_week,
|
|
92
|
+
memorymap=memorymap,
|
|
93
|
+
cache_dir=self._cache_dir,
|
|
94
|
+
)
|
|
95
|
+
self._modules[addr].initialize(self.send)
|
|
96
|
+
await self._modules[addr].load()
|
|
107
97
|
|
|
108
98
|
async def add_submodules(self, addr: int, subList: dict[int, int]) -> None:
|
|
99
|
+
"""Add submodules address to module."""
|
|
109
100
|
for sub_num, sub_addr in subList.items():
|
|
110
101
|
if sub_addr == 0xFF:
|
|
111
102
|
continue
|
|
@@ -114,48 +105,30 @@ class Velbus:
|
|
|
114
105
|
self._modules[sub_addr] = self._modules[addr]
|
|
115
106
|
self._modules[addr].cleanupSubChannels()
|
|
116
107
|
|
|
117
|
-
def _load_module_from_cache(self, cache_dir: str, address: int) -> None | Module:
|
|
118
|
-
try:
|
|
119
|
-
cfile = pathlib.Path(f"{cache_dir}/{address}.p")
|
|
120
|
-
with cfile.open("rb") as fl:
|
|
121
|
-
o = pickle.load(fl)
|
|
122
|
-
if isinstance(o, Module):
|
|
123
|
-
return o
|
|
124
|
-
except OSError:
|
|
125
|
-
pass
|
|
126
|
-
return None
|
|
127
|
-
|
|
128
108
|
def get_modules(self) -> dict:
|
|
129
|
-
"""
|
|
130
|
-
Return the module cache
|
|
131
|
-
"""
|
|
109
|
+
"""Return the module cache."""
|
|
132
110
|
return self._modules
|
|
133
111
|
|
|
134
112
|
def get_module(self, addr: str) -> None | Module:
|
|
135
|
-
"""
|
|
136
|
-
|
|
137
|
-
"""
|
|
138
|
-
if addr in self._modules.keys():
|
|
113
|
+
"""Get a module on an address."""
|
|
114
|
+
if addr in self._modules:
|
|
139
115
|
return self._modules[addr]
|
|
140
116
|
return None
|
|
141
117
|
|
|
142
118
|
def get_channels(self, addr: str) -> None | dict:
|
|
143
|
-
"""
|
|
144
|
-
Get the channels for an address
|
|
145
|
-
"""
|
|
119
|
+
"""Get the channels for an address."""
|
|
146
120
|
if addr in self._modules:
|
|
147
121
|
return (self._modules[addr]).get_channels()
|
|
148
122
|
return None
|
|
149
123
|
|
|
150
124
|
async def stop(self) -> None:
|
|
125
|
+
"""Stop the controller."""
|
|
151
126
|
self._closing = True
|
|
152
127
|
self._auto_reconnect = False
|
|
153
128
|
self._protocol.close()
|
|
154
129
|
|
|
155
130
|
async def connect(self, test_connect: bool = False) -> None:
|
|
156
|
-
"""
|
|
157
|
-
Connect to the bus and load all the data
|
|
158
|
-
"""
|
|
131
|
+
"""Connect to the bus and load all the data."""
|
|
159
132
|
auth = None
|
|
160
133
|
# connect to the bus
|
|
161
134
|
if ":" in self._dsn:
|
|
@@ -182,23 +155,25 @@ class Velbus:
|
|
|
182
155
|
)
|
|
183
156
|
|
|
184
157
|
except (ConnectionRefusedError, OSError) as err:
|
|
185
|
-
raise VelbusConnectionFailed
|
|
158
|
+
raise VelbusConnectionFailed from err
|
|
186
159
|
else:
|
|
187
160
|
# serial port
|
|
188
161
|
try:
|
|
189
|
-
_transport, _protocol =
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
162
|
+
_transport, _protocol = (
|
|
163
|
+
await serial_asyncio_fast.create_serial_connection(
|
|
164
|
+
asyncio.get_event_loop(),
|
|
165
|
+
lambda: self._protocol,
|
|
166
|
+
url=self._dsn,
|
|
167
|
+
baudrate=38400,
|
|
168
|
+
bytesize=serial.EIGHTBITS,
|
|
169
|
+
parity=serial.PARITY_NONE,
|
|
170
|
+
stopbits=serial.STOPBITS_ONE,
|
|
171
|
+
xonxoff=0,
|
|
172
|
+
rtscts=1,
|
|
173
|
+
)
|
|
199
174
|
)
|
|
200
175
|
except (FileNotFoundError, serial.SerialException) as err:
|
|
201
|
-
raise VelbusConnectionFailed
|
|
176
|
+
raise VelbusConnectionFailed from err
|
|
202
177
|
if test_connect:
|
|
203
178
|
return
|
|
204
179
|
# if auth is required send the auth key
|
|
@@ -209,6 +184,7 @@ class Velbus:
|
|
|
209
184
|
await self.scan()
|
|
210
185
|
|
|
211
186
|
async def scan(self) -> None:
|
|
187
|
+
"""Scan the bus."""
|
|
212
188
|
self._handler.scan_started()
|
|
213
189
|
for addr in range(1, 256):
|
|
214
190
|
msg = ModuleTypeRequestMessage(addr)
|
|
@@ -230,9 +206,7 @@ class Velbus:
|
|
|
230
206
|
)
|
|
231
207
|
|
|
232
208
|
async def _check_if_modules_are_loaded(self) -> None:
|
|
233
|
-
"""
|
|
234
|
-
Task to wait until modules are loaded
|
|
235
|
-
"""
|
|
209
|
+
"""Task to wait until modules are loaded."""
|
|
236
210
|
while True:
|
|
237
211
|
mods_loaded = 0
|
|
238
212
|
for mod in (self.get_modules()).values():
|
|
@@ -247,9 +221,7 @@ class Velbus:
|
|
|
247
221
|
await asyncio.sleep(15)
|
|
248
222
|
|
|
249
223
|
async def send(self, msg: Message) -> None:
|
|
250
|
-
"""
|
|
251
|
-
Send a packet
|
|
252
|
-
"""
|
|
224
|
+
"""Send a packet."""
|
|
253
225
|
await self._protocol.send_message(
|
|
254
226
|
RawMessage(
|
|
255
227
|
priority=msg.priority,
|
|
@@ -260,6 +232,7 @@ class Velbus:
|
|
|
260
232
|
)
|
|
261
233
|
|
|
262
234
|
def get_all(self, class_name: str) -> list[Channel]:
|
|
235
|
+
"""Get all channels."""
|
|
263
236
|
lst = []
|
|
264
237
|
for addr, mod in (self.get_modules()).items():
|
|
265
238
|
if addr in self._submodules:
|
|
@@ -270,9 +243,8 @@ class Velbus:
|
|
|
270
243
|
return lst
|
|
271
244
|
|
|
272
245
|
async def sync_clock(self) -> None:
|
|
273
|
-
"""
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
await self.send(
|
|
277
|
-
await self.send(
|
|
278
|
-
await self.send(SetDaylightSaving())
|
|
246
|
+
"""Will send all the needed messages to sync the clock."""
|
|
247
|
+
lclt = time.localtime()
|
|
248
|
+
await self.send(SetRealtimeClock(wday=lclt[6], hour=lclt[3], min=lclt[4]))
|
|
249
|
+
await self.send(SetDate(day=lclt[2], mon=lclt[1], year=lclt[0]))
|
|
250
|
+
await self.send(SetDaylightSaving(ds=not lclt[8]))
|
velbusaio/handler.py
CHANGED
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
|
-
import re
|
|
12
11
|
from typing import TYPE_CHECKING, Awaitable, Callable
|
|
13
12
|
import pkg_resources
|
|
14
13
|
|
|
@@ -105,7 +104,9 @@ class PacketHandler:
|
|
|
105
104
|
else:
|
|
106
105
|
self._log.warning(
|
|
107
106
|
"NOT FOUND IN command_registry: addr={} cmd={} packet={}".format(
|
|
108
|
-
address,
|
|
107
|
+
address,
|
|
108
|
+
command_value,
|
|
109
|
+
":".join(format(x, "02x") for x in data),
|
|
109
110
|
)
|
|
110
111
|
)
|
|
111
112
|
elif self._scan_complete:
|
velbusaio/helpers.py
CHANGED
velbusaio/message.py
CHANGED
velbusaio/messages/cover_off.py
CHANGED
velbusaio/messages/set_date.py
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
import time
|
|
8
|
-
|
|
9
7
|
from velbusaio.command_registry import register
|
|
10
8
|
from velbusaio.message import Message
|
|
11
9
|
|
|
@@ -18,11 +16,11 @@ class SetDate(Message):
|
|
|
18
16
|
received by all modules
|
|
19
17
|
"""
|
|
20
18
|
|
|
21
|
-
def __init__(self, address=0x00) -> None:
|
|
19
|
+
def __init__(self, address=0x00, day=None, mon=None, year=None) -> None:
|
|
22
20
|
Message.__init__(self)
|
|
23
|
-
self._day =
|
|
24
|
-
self._mon =
|
|
25
|
-
self._year =
|
|
21
|
+
self._day = day
|
|
22
|
+
self._mon = mon
|
|
23
|
+
self._year = year
|
|
26
24
|
self.set_defaults(address)
|
|
27
25
|
|
|
28
26
|
def set_defaults(self, address) -> None:
|
|
@@ -30,10 +28,6 @@ class SetDate(Message):
|
|
|
30
28
|
self.set_address(address)
|
|
31
29
|
self.set_low_priority()
|
|
32
30
|
self.set_no_rtr()
|
|
33
|
-
lclt = time.localtime()
|
|
34
|
-
self._day = lclt[2]
|
|
35
|
-
self._mon = lclt[1]
|
|
36
|
-
self._year = lclt[0]
|
|
37
31
|
|
|
38
32
|
def populate(self, priority, address, rtr, data) -> None:
|
|
39
33
|
"""
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
import time
|
|
8
|
-
|
|
9
7
|
from velbusaio.command_registry import register
|
|
10
8
|
from velbusaio.message import Message
|
|
11
9
|
|
|
@@ -18,9 +16,9 @@ class SetDaylightSaving(Message):
|
|
|
18
16
|
received by all modules
|
|
19
17
|
"""
|
|
20
18
|
|
|
21
|
-
def __init__(self, address=0x00) -> None:
|
|
19
|
+
def __init__(self, address=0x00, ds=None) -> None:
|
|
22
20
|
Message.__init__(self)
|
|
23
|
-
self._ds =
|
|
21
|
+
self._ds = ds
|
|
24
22
|
self.set_defaults(address)
|
|
25
23
|
|
|
26
24
|
def set_defaults(self, address) -> None:
|
|
@@ -28,8 +26,6 @@ class SetDaylightSaving(Message):
|
|
|
28
26
|
self.set_address(address)
|
|
29
27
|
self.set_low_priority()
|
|
30
28
|
self.set_no_rtr()
|
|
31
|
-
lclt = time.localtime()
|
|
32
|
-
self._ds = not lclt[8]
|
|
33
29
|
|
|
34
30
|
def populate(self, priority, address, rtr, data) -> None:
|
|
35
31
|
"""
|
velbusaio/messages/set_dimmer.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
:author: Frank van Breugel
|
|
1
|
+
""":author: Frank van Breugel
|
|
3
2
|
"""
|
|
4
3
|
|
|
5
4
|
from __future__ import annotations
|
|
@@ -12,10 +11,10 @@ COMMAND_CODE = 0x07
|
|
|
12
11
|
|
|
13
12
|
@register(
|
|
14
13
|
COMMAND_CODE,
|
|
15
|
-
["VMB1DM", "VMBDME", "VMB4DC", "
|
|
14
|
+
["VMB1DM", "VMBDME", "VMB4DC", "VMB1LED"],
|
|
16
15
|
)
|
|
17
16
|
class SetDimmerMessage(Message):
|
|
18
|
-
"""
|
|
17
|
+
"""with this message the channel numbering is a bitnumber
|
|
19
18
|
send by:
|
|
20
19
|
received by: VMBDME, VMB4DC
|
|
21
20
|
"""
|
|
@@ -34,9 +33,7 @@ class SetDimmerMessage(Message):
|
|
|
34
33
|
self.set_no_rtr()
|
|
35
34
|
|
|
36
35
|
def populate(self, priority, address, rtr, data):
|
|
37
|
-
"""
|
|
38
|
-
:return: None
|
|
39
|
-
"""
|
|
36
|
+
""":return: None"""
|
|
40
37
|
self.needs_high_priority(priority)
|
|
41
38
|
self.needs_no_rtr(rtr)
|
|
42
39
|
self.needs_data(data, 4)
|
|
@@ -48,9 +45,7 @@ class SetDimmerMessage(Message):
|
|
|
48
45
|
)
|
|
49
46
|
|
|
50
47
|
def data_to_binary(self):
|
|
51
|
-
"""
|
|
52
|
-
:return: bytes
|
|
53
|
-
"""
|
|
48
|
+
""":return: bytes"""
|
|
54
49
|
return bytes(
|
|
55
50
|
[
|
|
56
51
|
COMMAND_CODE,
|
|
@@ -60,9 +55,9 @@ class SetDimmerMessage(Message):
|
|
|
60
55
|
) + self.dimmer_transitiontime.to_bytes(2, byteorder="big", signed=False)
|
|
61
56
|
|
|
62
57
|
|
|
63
|
-
@register(COMMAND_CODE, ["VMBDALI", "VMBDALI-20"])
|
|
58
|
+
@register(COMMAND_CODE, ["VMBDALI", "VMBDALI-20", "VMBDMI", "VMBDMI-R", "VMB8DC-20"])
|
|
64
59
|
class SetDimmerMessage2(SetDimmerMessage):
|
|
65
|
-
"""
|
|
60
|
+
"""This with this message the channel numbering is an integer
|
|
66
61
|
send by:
|
|
67
62
|
received by: VMBDALI
|
|
68
63
|
"""
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
import time
|
|
8
|
-
|
|
9
7
|
from velbusaio.command_registry import register
|
|
10
8
|
from velbusaio.message import Message
|
|
11
9
|
|
|
@@ -18,11 +16,11 @@ class SetRealtimeClock(Message):
|
|
|
18
16
|
received by all modules
|
|
19
17
|
"""
|
|
20
18
|
|
|
21
|
-
def __init__(self, address=0x00) -> None:
|
|
19
|
+
def __init__(self, address=0x00, wday=None, hour=None, min=None) -> None:
|
|
22
20
|
Message.__init__(self)
|
|
23
|
-
self._wday =
|
|
24
|
-
self._hour =
|
|
25
|
-
self._min =
|
|
21
|
+
self._wday = wday
|
|
22
|
+
self._hour = hour
|
|
23
|
+
self._min = min
|
|
26
24
|
self.set_defaults(address)
|
|
27
25
|
|
|
28
26
|
def set_defaults(self, address) -> None:
|
|
@@ -30,10 +28,6 @@ class SetRealtimeClock(Message):
|
|
|
30
28
|
self.set_address(address)
|
|
31
29
|
self.set_low_priority()
|
|
32
30
|
self.set_no_rtr()
|
|
33
|
-
lclt = time.localtime()
|
|
34
|
-
self._wday = lclt[6]
|
|
35
|
-
self._hour = lclt[3]
|
|
36
|
-
self._min = lclt[4]
|
|
37
31
|
|
|
38
32
|
def populate(self, priority, address, rtr, data) -> None:
|
|
39
33
|
"""
|
velbusaio/module.py
CHANGED
|
@@ -5,11 +5,10 @@ This represents a velbus module
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
import os
|
|
9
8
|
import pathlib
|
|
10
|
-
import pickle
|
|
11
9
|
import struct
|
|
12
10
|
import sys
|
|
11
|
+
import json
|
|
13
12
|
from typing import Awaitable, Callable
|
|
14
13
|
|
|
15
14
|
from velbusaio.channels import (
|
|
@@ -37,7 +36,7 @@ from velbusaio.const import (
|
|
|
37
36
|
)
|
|
38
37
|
from velbusaio.helpers import handle_match, keys_exists
|
|
39
38
|
from velbusaio.message import Message
|
|
40
|
-
from velbusaio.messages import DaliDeviceSettingMsg
|
|
39
|
+
from velbusaio.messages.dali_device_settings import DaliDeviceSettingMsg
|
|
41
40
|
from velbusaio.messages.blind_status import BlindStatusMessage, BlindStatusNgMessage
|
|
42
41
|
from velbusaio.messages.channel_name_part1 import (
|
|
43
42
|
ChannelNamePart1Message,
|
|
@@ -81,8 +80,6 @@ from velbusaio.messages.module_status import (
|
|
|
81
80
|
ModuleStatusPirMessage,
|
|
82
81
|
)
|
|
83
82
|
from velbusaio.messages.module_status_request import ModuleStatusRequestMessage
|
|
84
|
-
from velbusaio.messages.module_subtype import ModuleSubTypeMessage
|
|
85
|
-
from velbusaio.messages.module_type import ModuleTypeMessage, ModuleType2Message
|
|
86
83
|
from velbusaio.messages.push_button_status import PushButtonStatusMessage
|
|
87
84
|
from velbusaio.messages.read_data_from_memory import ReadDataFromMemoryMessage
|
|
88
85
|
from velbusaio.messages.relay_status import RelayStatusMessage, RelayStatusMessage2
|
|
@@ -188,9 +185,9 @@ class Module:
|
|
|
188
185
|
del self._channels[i]
|
|
189
186
|
|
|
190
187
|
def _cache(self) -> None:
|
|
191
|
-
cfile = pathlib.Path(f"{self._cache_dir}/{self._address}.
|
|
192
|
-
with cfile.open("
|
|
193
|
-
|
|
188
|
+
cfile = pathlib.Path(f"{self._cache_dir}/{self._address}.json")
|
|
189
|
+
with cfile.open("w") as fl:
|
|
190
|
+
json.dump(self.to_cache(), fl, indent=4)
|
|
194
191
|
|
|
195
192
|
def __getstate__(self) -> dict:
|
|
196
193
|
d = self.__dict__
|
|
@@ -201,20 +198,17 @@ class Module:
|
|
|
201
198
|
self.__dict__ = state
|
|
202
199
|
|
|
203
200
|
def __repr__(self) -> str:
|
|
204
|
-
return
|
|
205
|
-
"<{}: {{{}}} @ {{{}}} loaded:{{{}}} loading:{{{}}} channels{{:{}}}>".format(
|
|
206
|
-
self._name,
|
|
207
|
-
self._type,
|
|
208
|
-
self._address,
|
|
209
|
-
self.loaded,
|
|
210
|
-
self._is_loading,
|
|
211
|
-
self._channels,
|
|
212
|
-
)
|
|
213
|
-
)
|
|
201
|
+
return f"<{self._name} type:{self._type} address:{self._address} loaded:{self.loaded} loading:{self._is_loading} channels: {self._channels}>"
|
|
214
202
|
|
|
215
203
|
def __str__(self) -> str:
|
|
216
204
|
return self.__repr__()
|
|
217
205
|
|
|
206
|
+
def to_cache(self) -> dict:
|
|
207
|
+
d = {"name": self._name, "channels": {}}
|
|
208
|
+
for num, chan in self._channels.items():
|
|
209
|
+
d["channels"][num] = chan.to_cache()
|
|
210
|
+
return d
|
|
211
|
+
|
|
218
212
|
def get_addresses(self) -> list:
|
|
219
213
|
"""
|
|
220
214
|
Get all addresses for this module
|
|
@@ -232,7 +226,9 @@ class Module:
|
|
|
232
226
|
return self._type
|
|
233
227
|
|
|
234
228
|
def get_type_name(self) -> str:
|
|
235
|
-
|
|
229
|
+
if "Type" in self._data:
|
|
230
|
+
return self._data["Type"]
|
|
231
|
+
return "UNKNOWN"
|
|
236
232
|
|
|
237
233
|
def get_serial(self) -> str | None:
|
|
238
234
|
return self.serial
|
|
@@ -241,12 +237,7 @@ class Module:
|
|
|
241
237
|
return self._name
|
|
242
238
|
|
|
243
239
|
def get_sw_version(self) -> str:
|
|
244
|
-
return "{}-{}.{}.{}"
|
|
245
|
-
self.serial,
|
|
246
|
-
self.memory_map_version,
|
|
247
|
-
self.build_year,
|
|
248
|
-
self.build_week,
|
|
249
|
-
)
|
|
240
|
+
return f"{self.serial}-{self.memory_map_version}.{self.build_year}.{self.build_week}"
|
|
250
241
|
|
|
251
242
|
def calc_channel_offset(self, address: int) -> int:
|
|
252
243
|
_channel_offset = 0
|
|
@@ -549,14 +540,31 @@ class Module:
|
|
|
549
540
|
self._log.info("Load Module")
|
|
550
541
|
# start the loading
|
|
551
542
|
self._is_loading = True
|
|
543
|
+
# see if we have a cache
|
|
544
|
+
try:
|
|
545
|
+
cfile = pathlib.Path(f"{self._cache_dir}/{self._address}.json")
|
|
546
|
+
with cfile.open("r") as fl:
|
|
547
|
+
cache = json.load(fl)
|
|
548
|
+
except OSError:
|
|
549
|
+
cache = {}
|
|
552
550
|
# load default channels
|
|
553
|
-
await self.
|
|
551
|
+
await self._load_default_channels()
|
|
554
552
|
# load the data from memory ( the stuff that we need)
|
|
555
|
-
|
|
553
|
+
if "name" in cache and cache["name"] != "":
|
|
554
|
+
self._name = cache["name"]
|
|
555
|
+
else:
|
|
556
|
+
await self.__load_memory()
|
|
556
557
|
# load the module status
|
|
557
558
|
await self._request_module_status()
|
|
558
559
|
# load the channel names
|
|
559
|
-
|
|
560
|
+
if "channels" in cache:
|
|
561
|
+
for num, chan in cache["channels"].items():
|
|
562
|
+
self._channels[int(num)]._name = chan["name"]
|
|
563
|
+
if "Unit" in chan:
|
|
564
|
+
self._channels[int(num)]._Unit = chan["Unit"]
|
|
565
|
+
self._channels[int(num)]._is_loaded = True
|
|
566
|
+
else:
|
|
567
|
+
await self._request_channel_name()
|
|
560
568
|
# load the module specific stuff
|
|
561
569
|
self._load()
|
|
562
570
|
# stop the loading
|
|
@@ -720,7 +728,7 @@ class Module:
|
|
|
720
728
|
msg.low_address = addr[1]
|
|
721
729
|
await self._writer(msg)
|
|
722
730
|
|
|
723
|
-
async def
|
|
731
|
+
async def _load_default_channels(self) -> None:
|
|
724
732
|
if "Channels" not in self._data:
|
|
725
733
|
return
|
|
726
734
|
|
|
@@ -768,19 +776,15 @@ class VmbDali(Module):
|
|
|
768
776
|
)
|
|
769
777
|
self.group_members: dict[int, set[int]] = {}
|
|
770
778
|
|
|
771
|
-
async def
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
# Until the DaliDeviceSettings messages either delete or replace these placeholder's
|
|
781
|
-
# with actual channels
|
|
782
|
-
|
|
783
|
-
await self._request_dali_channels()
|
|
779
|
+
async def _load_default_channels(self) -> None:
|
|
780
|
+
for chan in range(1, 64 + 1):
|
|
781
|
+
self._channels[chan] = Channel(
|
|
782
|
+
self, chan, "placeholder", True, self._writer, self._address
|
|
783
|
+
)
|
|
784
|
+
# Placeholders will keep this module loading
|
|
785
|
+
# Until the DaliDeviceSettings messages either delete or replace these placeholder's
|
|
786
|
+
# with actual channels
|
|
787
|
+
await self._request_dali_channels()
|
|
784
788
|
|
|
785
789
|
async def _request_dali_channels(self):
|
|
786
790
|
msg_type = commandRegistry.get_command(
|
velbusaio/protocol.json
CHANGED
|
@@ -8792,7 +8792,7 @@
|
|
|
8792
8792
|
},
|
|
8793
8793
|
"45": {
|
|
8794
8794
|
"Info": "VMBDALI-20 DALI gateway module",
|
|
8795
|
-
"Type": "VMBDALI"
|
|
8795
|
+
"Type": "VMBDALI-20"
|
|
8796
8796
|
},
|
|
8797
8797
|
"48": {
|
|
8798
8798
|
"Channels": {
|
|
@@ -9361,7 +9361,8 @@
|
|
|
9361
9361
|
}
|
|
9362
9362
|
},
|
|
9363
9363
|
"TemperatureChannel": "09",
|
|
9364
|
-
"ThermostatAddr": "0"
|
|
9364
|
+
"ThermostatAddr": "0",
|
|
9365
|
+
"Type": "VMBEL1-20"
|
|
9365
9366
|
},
|
|
9366
9367
|
"50": {
|
|
9367
9368
|
"AllChannelStatus": "FF",
|
|
@@ -9449,7 +9450,8 @@
|
|
|
9449
9450
|
}
|
|
9450
9451
|
},
|
|
9451
9452
|
"TemperatureChannel": "09",
|
|
9452
|
-
"ThermostatAddr": "0"
|
|
9453
|
+
"ThermostatAddr": "0",
|
|
9454
|
+
"Type": "VMBEL2-20"
|
|
9453
9455
|
},
|
|
9454
9456
|
"51": {
|
|
9455
9457
|
"AllChannelStatus": "FF",
|
|
@@ -9537,7 +9539,8 @@
|
|
|
9537
9539
|
}
|
|
9538
9540
|
},
|
|
9539
9541
|
"TemperatureChannel": "09",
|
|
9540
|
-
"ThermostatAddr": "0"
|
|
9542
|
+
"ThermostatAddr": "0",
|
|
9543
|
+
"Type": "VMBEL4-20"
|
|
9541
9544
|
},
|
|
9542
9545
|
"52": {
|
|
9543
9546
|
"AllChannelStatus": "FF",
|
|
@@ -9725,7 +9728,8 @@
|
|
|
9725
9728
|
}
|
|
9726
9729
|
},
|
|
9727
9730
|
"TemperatureChannel": "33",
|
|
9728
|
-
"ThermostatAddr": "3"
|
|
9731
|
+
"ThermostatAddr": "3",
|
|
9732
|
+
"Type": "VMBELO-20"
|
|
9729
9733
|
},
|
|
9730
9734
|
"54": {
|
|
9731
9735
|
"Info": "1 Button Touch panel",
|
velbusaio/util.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|