wirely 0.1.0__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.
- wirely/__init__.py +406 -0
- wirely/__main__.py +45 -0
- wirely/ast.py +1135 -0
- wirely/block.py +542 -0
- wirely/block_type.py +342 -0
- wirely/blocks.lark +126 -0
- wirely/compiler.py +125 -0
- wirely/engine.py +418 -0
- wirely/enum.py +15 -0
- wirely/example.py +36 -0
- wirely/graphviz.py +107 -0
- wirely/knx/__init__.py +0 -0
- wirely/knx/connection_adapter.py +90 -0
- wirely/knx/connection_config.py +152 -0
- wirely/lib/abb_jra.wrl +64 -0
- wirely/lib/csv.wrl +32 -0
- wirely/lib/ext_prog.wrl +12 -0
- wirely/lib/gpio.wrl +23 -0
- wirely/lib/knx.wrl +80 -0
- wirely/lib/memory.wrl +45 -0
- wirely/lib/mqtt.wrl +66 -0
- wirely/lib/std.wrl +134 -0
- wirely/lib/time.wrl +93 -0
- wirely/lib/two_n.wrl +111 -0
- wirely/misc/__init__.py +0 -0
- wirely/misc/service.py +48 -0
- wirely/native/__init__.py +74 -0
- wirely/native/abb_jra.py +66 -0
- wirely/native/csv.py +194 -0
- wirely/native/ext_prog.py +77 -0
- wirely/native/gpio.py +125 -0
- wirely/native/knx.py +455 -0
- wirely/native/memory.py +113 -0
- wirely/native/mqtt.py +605 -0
- wirely/native/std.py +334 -0
- wirely/native/test_library.py +14 -0
- wirely/native/time.py +359 -0
- wirely/native/two_n.py +632 -0
- wirely/node.py +125 -0
- wirely/parser.py +175 -0
- wirely/program.py +155 -0
- wirely/run.py +143 -0
- wirely/service.py +65 -0
- wirely/test.py +569 -0
- wirely/time.py +172 -0
- wirely/two_n/__init__.py +0 -0
- wirely/two_n/cert.py +126 -0
- wirely/two_n/client.py +402 -0
- wirely/two_n/fake_server.py +415 -0
- wirely/two_n/grab_images.py +146 -0
- wirely/types.py +522 -0
- wirely/util.py +111 -0
- wirely/value.py +163 -0
- wirely-0.1.0.dist-info/METADATA +94 -0
- wirely-0.1.0.dist-info/RECORD +59 -0
- wirely-0.1.0.dist-info/WHEEL +5 -0
- wirely-0.1.0.dist-info/entry_points.txt +2 -0
- wirely-0.1.0.dist-info/licenses/LICENSE +21 -0
- wirely-0.1.0.dist-info/top_level.txt +1 -0
wirely/native/gpio.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
import wirely as blocks
|
|
4
|
+
from wirely.native import native_block_type, native_procedure
|
|
5
|
+
|
|
6
|
+
native_block_types = {}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass(frozen=True)
|
|
10
|
+
class GPIOData:
|
|
11
|
+
chip_device_file: str
|
|
12
|
+
name: str
|
|
13
|
+
active_state: str
|
|
14
|
+
drive: str
|
|
15
|
+
bias: str
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def create(*, parameter_values):
|
|
19
|
+
chip_device_file = parameter_values['ChipDeviceFile'].value
|
|
20
|
+
name = parameter_values['Name'].value
|
|
21
|
+
|
|
22
|
+
# TODO: Should not be needed, this should use the default parameter value automatically
|
|
23
|
+
if 'ActiveState' in parameter_values:
|
|
24
|
+
active_state = parameter_values['ActiveState'].value
|
|
25
|
+
else:
|
|
26
|
+
active_state = 'High'
|
|
27
|
+
if 'Drive' in parameter_values:
|
|
28
|
+
drive = parameter_values['Drive'].value
|
|
29
|
+
else:
|
|
30
|
+
drive = 'PushPull'
|
|
31
|
+
if 'Bias' in parameter_values:
|
|
32
|
+
bias = parameter_values['Bias'].value
|
|
33
|
+
else:
|
|
34
|
+
bias = 'AsIs'
|
|
35
|
+
|
|
36
|
+
return GPIOData(chip_device_file=chip_device_file, name=name, active_state=active_state, drive=drive, bias=bias)
|
|
37
|
+
|
|
38
|
+
def dump(self):
|
|
39
|
+
print(f' Chip device file: {self.chip_device_file!r}', flush=True)
|
|
40
|
+
print(f' GPIO name: {self.name!r}', flush=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@native_block_type(native_block_types, has_state=True, data_type=GPIOData)
|
|
44
|
+
class Output(blocks.NativeState):
|
|
45
|
+
def __init__(self, path, engine, static_block_context):
|
|
46
|
+
super().__init__(path, engine, static_block_context)
|
|
47
|
+
self.data = static_block_context.parsed_native_instance
|
|
48
|
+
self.gpio_request = None
|
|
49
|
+
self.direction_set = False
|
|
50
|
+
|
|
51
|
+
def start_tasks(self, task_group, async_exit_stack):
|
|
52
|
+
super().start_tasks(task_group, async_exit_stack)
|
|
53
|
+
|
|
54
|
+
import gpiod
|
|
55
|
+
from gpiod.line import Bias, Direction, Drive
|
|
56
|
+
|
|
57
|
+
bias = {
|
|
58
|
+
'AsIs': Bias.AS_IS,
|
|
59
|
+
'Disable': Bias.DISABLED,
|
|
60
|
+
'PullDown': Bias.PULL_DOWN,
|
|
61
|
+
'PullUp': Bias.PULL_UP,
|
|
62
|
+
}
|
|
63
|
+
drive = {
|
|
64
|
+
'PushPull': Drive.PUSH_PULL,
|
|
65
|
+
'OpenDrain': Drive.OPEN_DRAIN,
|
|
66
|
+
'OpenSource': Drive.OPEN_SOURCE,
|
|
67
|
+
}
|
|
68
|
+
if self.data.active_state not in ('High', 'Low'):
|
|
69
|
+
raise ValueError(f'Unknown active state: {self.data.active_state!r}')
|
|
70
|
+
if self.data.drive not in drive:
|
|
71
|
+
raise ValueError(f'Unknown drive: {self.data.drive!r}')
|
|
72
|
+
if self.data.bias not in bias:
|
|
73
|
+
raise ValueError(f'Unknown bias: {self.data.bias!r}')
|
|
74
|
+
|
|
75
|
+
chip = async_exit_stack.enter_context(gpiod.Chip(self.data.chip_device_file))
|
|
76
|
+
try:
|
|
77
|
+
line_info = chip.get_line_info(self.data.name)
|
|
78
|
+
if line_info.name != self.data.name:
|
|
79
|
+
raise ValueError('Line name does not match')
|
|
80
|
+
self.gpio_offset = line_info.offset
|
|
81
|
+
except (OSError, ValueError) as exc:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f'Cannot find GPIO {self.data.name!r} on chip '
|
|
84
|
+
f'{self.data.chip_device_file!r}'
|
|
85
|
+
) from exc
|
|
86
|
+
self.gpio_request = async_exit_stack.enter_context(chip.request_lines(
|
|
87
|
+
config={self.gpio_offset: gpiod.LineSettings(
|
|
88
|
+
direction=Direction.INPUT, bias=bias[self.data.bias])},
|
|
89
|
+
consumer='Wirely',
|
|
90
|
+
))
|
|
91
|
+
self.direction_set = False
|
|
92
|
+
|
|
93
|
+
@native_procedure
|
|
94
|
+
def set_value(self, *, context):
|
|
95
|
+
import gpiod
|
|
96
|
+
from gpiod.line import Bias, Direction, Drive, Value
|
|
97
|
+
|
|
98
|
+
var_input = context.static.sources['implicit input']
|
|
99
|
+
value = context.dep_values[var_input]
|
|
100
|
+
logical_value = Value.ACTIVE if value else Value.INACTIVE
|
|
101
|
+
|
|
102
|
+
if not self.direction_set:
|
|
103
|
+
bias = {
|
|
104
|
+
'AsIs': Bias.AS_IS,
|
|
105
|
+
'Disable': Bias.DISABLED,
|
|
106
|
+
'PullDown': Bias.PULL_DOWN,
|
|
107
|
+
'PullUp': Bias.PULL_UP,
|
|
108
|
+
}
|
|
109
|
+
drive = {
|
|
110
|
+
'PushPull': Drive.PUSH_PULL,
|
|
111
|
+
'OpenDrain': Drive.OPEN_DRAIN,
|
|
112
|
+
'OpenSource': Drive.OPEN_SOURCE,
|
|
113
|
+
}
|
|
114
|
+
self.gpio_request.reconfigure_lines({
|
|
115
|
+
self.gpio_offset: gpiod.LineSettings(
|
|
116
|
+
direction=Direction.OUTPUT,
|
|
117
|
+
output_value=logical_value,
|
|
118
|
+
active_low=self.data.active_state == 'Low',
|
|
119
|
+
drive=drive[self.data.drive],
|
|
120
|
+
bias=bias[self.data.bias],
|
|
121
|
+
),
|
|
122
|
+
})
|
|
123
|
+
self.direction_set = True
|
|
124
|
+
else:
|
|
125
|
+
self.gpio_request.set_value(self.gpio_offset, logical_value)
|
wirely/native/knx.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import dataclasses
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
import wirely as blocks
|
|
7
|
+
from wirely.native import native_block_type, native_procedure
|
|
8
|
+
|
|
9
|
+
native_block_types = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclasses.dataclass(frozen=True)
|
|
13
|
+
class KnxData:
|
|
14
|
+
group_address_raw: int
|
|
15
|
+
value_type: blocks.Type
|
|
16
|
+
# TODO: Should this be here or should this be created later?
|
|
17
|
+
value_type_knx: str
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def create(*, parameter_values):
|
|
21
|
+
group_address_str = parameter_values['GroupAddress'].value
|
|
22
|
+
par_type = parameter_values['Type']
|
|
23
|
+
|
|
24
|
+
import xknx
|
|
25
|
+
group_address = xknx.telegram.GroupAddress(group_address_str)
|
|
26
|
+
group_address_raw = group_address.raw
|
|
27
|
+
|
|
28
|
+
if par_type == blocks.types.boolean:
|
|
29
|
+
value_type_knx = "binary"
|
|
30
|
+
elif par_type == blocks.types.int16:
|
|
31
|
+
value_type_knx = "2byte_signed"
|
|
32
|
+
elif par_type == blocks.types.uint16:
|
|
33
|
+
value_type_knx = "2byte_unsigned"
|
|
34
|
+
elif par_type == blocks.types.int32:
|
|
35
|
+
value_type_knx = "4byte_signed"
|
|
36
|
+
elif par_type == blocks.types.uint32:
|
|
37
|
+
value_type_knx = "4byte_unsigned"
|
|
38
|
+
else:
|
|
39
|
+
raise Exception(f'TODO: Unsupported type for KNX.ValueReceiver: {par_type}')
|
|
40
|
+
|
|
41
|
+
return KnxData(group_address_raw=group_address_raw, value_type=par_type, value_type_knx=value_type_knx)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def group_address(self):
|
|
45
|
+
import xknx
|
|
46
|
+
return xknx.telegram.GroupAddress(self.group_address_raw)
|
|
47
|
+
|
|
48
|
+
def dump(self):
|
|
49
|
+
print(f' Using group address: {self.group_address!r}', flush=True)
|
|
50
|
+
print(f' Using type: {self.value_type!r} / {self.value_type_knx}', flush=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@native_block_type(native_block_types, has_state=True)
|
|
54
|
+
class Connection(blocks.NativeState):
|
|
55
|
+
def __init__(self, path, engine, static_block_context):
|
|
56
|
+
super().__init__(path, engine, static_block_context)
|
|
57
|
+
|
|
58
|
+
from wirely.knx.connection_config import create_connection_config
|
|
59
|
+
from wirely.knx.connection_adapter import create_xknx
|
|
60
|
+
|
|
61
|
+
print('Create KNX connection', flush=True)
|
|
62
|
+
|
|
63
|
+
parameter_values = static_block_context.parameter_values
|
|
64
|
+
|
|
65
|
+
def parameter(name):
|
|
66
|
+
value = parameter_values.get(name)
|
|
67
|
+
return None if value is None else value.value
|
|
68
|
+
|
|
69
|
+
self.connection_config = create_connection_config(
|
|
70
|
+
host=parameter('Host'),
|
|
71
|
+
protocol=parameter('Protocol'),
|
|
72
|
+
port=parameter('Port'),
|
|
73
|
+
unix_path=parameter('UnixPath'),
|
|
74
|
+
ssh_target=parameter('SSHTarget'),
|
|
75
|
+
individual_address=parameter('IndividualAddress'),
|
|
76
|
+
)
|
|
77
|
+
self.knx = create_xknx(self.connection_config)
|
|
78
|
+
|
|
79
|
+
# TODO: Do start() later, while everything is already running?
|
|
80
|
+
async def start_tasks_async(self, task_group, async_exit_stack: contextlib.AsyncExitStack):
|
|
81
|
+
await super().start_tasks_async(task_group=task_group, async_exit_stack=async_exit_stack)
|
|
82
|
+
|
|
83
|
+
print('Connecting to KNX bus', flush=True)
|
|
84
|
+
await async_exit_stack.enter_async_context(self.knx)
|
|
85
|
+
print('Connecting to KNX bus done', flush=True)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@native_block_type(native_block_types, has_state=True)
|
|
89
|
+
class ValueReceiver(blocks.NativeState):
|
|
90
|
+
def __init__(self, path, engine, static_block_context):
|
|
91
|
+
super().__init__(path, engine, static_block_context)
|
|
92
|
+
self.current_value = None
|
|
93
|
+
|
|
94
|
+
import xknx
|
|
95
|
+
|
|
96
|
+
print('Create KNX ValueReceiver', flush=True)
|
|
97
|
+
|
|
98
|
+
# TODO: Clean this up
|
|
99
|
+
proc = None
|
|
100
|
+
for p in self.block_or_singleton.block_procedures:
|
|
101
|
+
if p.name == 'get_value':
|
|
102
|
+
proc = p
|
|
103
|
+
if proc is None:
|
|
104
|
+
raise Exception('proc is None')
|
|
105
|
+
self.proc = proc
|
|
106
|
+
|
|
107
|
+
# TODO: Clean up
|
|
108
|
+
par_values = engine.program.instances[path].parameter_values
|
|
109
|
+
par_group_address = None
|
|
110
|
+
par_type = None
|
|
111
|
+
for par in par_values:
|
|
112
|
+
if par.name.id_str == 'GroupAddress':
|
|
113
|
+
par_group_address = par
|
|
114
|
+
if par.name.id_str == 'Type':
|
|
115
|
+
par_type = par
|
|
116
|
+
if par_group_address is None:
|
|
117
|
+
raise Exception('par_group_address is None')
|
|
118
|
+
if par_type is None:
|
|
119
|
+
raise Exception('par_type is None')
|
|
120
|
+
group_address = par_values[par_group_address].value
|
|
121
|
+
ty = par_values[par_type]
|
|
122
|
+
print(f' Using group address: {group_address!r}', flush=True)
|
|
123
|
+
print(f' Using type: {ty!r}', flush=True)
|
|
124
|
+
|
|
125
|
+
# TODO: Clean up
|
|
126
|
+
# TODO: Properly follow connections
|
|
127
|
+
bus_obj = path[-1].block_inputs_byname['Bus'].connected_source
|
|
128
|
+
print(f' Using bus object: {bus_obj}', flush=True)
|
|
129
|
+
bus_block = bus_obj.block
|
|
130
|
+
# TODO: This is a hack
|
|
131
|
+
bus_path = (bus_block,)
|
|
132
|
+
|
|
133
|
+
self.native_bus = engine.native_states[bus_path]
|
|
134
|
+
print(f' Using native bus object: {self.native_bus}', flush=True)
|
|
135
|
+
|
|
136
|
+
self.address = xknx.telegram.GroupAddress(group_address)
|
|
137
|
+
|
|
138
|
+
self.value_type = ty
|
|
139
|
+
if ty == blocks.types.boolean:
|
|
140
|
+
self.value_type_knx = "binary"
|
|
141
|
+
elif ty == blocks.types.uint16:
|
|
142
|
+
self.value_type_knx = "2byte_unsigned"
|
|
143
|
+
else:
|
|
144
|
+
raise Exception(f'TODO: Unsupported type for KNX.ValueReceiver: {ty}')
|
|
145
|
+
|
|
146
|
+
# TODO: Where does this get cleaned up?
|
|
147
|
+
if self.value_type_knx == "binary":
|
|
148
|
+
self.sensor = xknx.devices.BinarySensor(self.native_bus.knx, name="Sensor", group_address_state=self.address, device_updated_cb=self.update_callback)
|
|
149
|
+
else:
|
|
150
|
+
self.sensor = xknx.devices.Sensor(self.native_bus.knx, name="Sensor", group_address_state=self.address, value_type=self.value_type_knx, device_updated_cb=self.update_callback)
|
|
151
|
+
self.native_bus.knx.devices.async_add(self.sensor)
|
|
152
|
+
|
|
153
|
+
# TODO: Make sure exceptions here are handled somehow?
|
|
154
|
+
def update_callback(self, device):
|
|
155
|
+
# print('update_callback()', flush=True)
|
|
156
|
+
if self.value_type_knx == "binary":
|
|
157
|
+
val = device.state
|
|
158
|
+
else:
|
|
159
|
+
val = device.sensor_value.value
|
|
160
|
+
# print(f' got value: {val!r}', flush=True)
|
|
161
|
+
self.value_type.verify_value(val)
|
|
162
|
+
|
|
163
|
+
self.current_value = val
|
|
164
|
+
|
|
165
|
+
self.engine.trigger(self.block_path, self.proc)
|
|
166
|
+
|
|
167
|
+
@native_procedure
|
|
168
|
+
def get_value(self, *, context):
|
|
169
|
+
# TODO: Add checks whether everything is running?
|
|
170
|
+
|
|
171
|
+
var_output = context.static.targets['']
|
|
172
|
+
|
|
173
|
+
res = self.current_value
|
|
174
|
+
|
|
175
|
+
if res is None:
|
|
176
|
+
res_opt = None
|
|
177
|
+
else:
|
|
178
|
+
res_opt = (res,)
|
|
179
|
+
# print(f'get_value: {res_opt}')
|
|
180
|
+
|
|
181
|
+
context.results[var_output] = res_opt
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@native_block_type(native_block_types, has_state=True)
|
|
185
|
+
class ValueOptionProvider(blocks.NativeState):
|
|
186
|
+
def __init__(self, path, engine, static_block_context):
|
|
187
|
+
super().__init__(path, engine, static_block_context)
|
|
188
|
+
|
|
189
|
+
# TODO: Is this needed?
|
|
190
|
+
self.current_value = None
|
|
191
|
+
|
|
192
|
+
# Task is needed for sending out values
|
|
193
|
+
self.task = None
|
|
194
|
+
self.value_to_send = None
|
|
195
|
+
# Note: Use an event instead of a condition because setting an event does not require await
|
|
196
|
+
# self.condition_var = asyncio.Condition()
|
|
197
|
+
self.event = asyncio.Event()
|
|
198
|
+
|
|
199
|
+
import xknx
|
|
200
|
+
|
|
201
|
+
print('Create KNX ValueOptionProvider', flush=True)
|
|
202
|
+
|
|
203
|
+
# TODO: Clean up
|
|
204
|
+
par_values = engine.program.instances[path].parameter_values
|
|
205
|
+
par_group_address = None
|
|
206
|
+
par_type = None
|
|
207
|
+
for par in par_values:
|
|
208
|
+
if par.name.id_str == 'GroupAddress':
|
|
209
|
+
par_group_address = par
|
|
210
|
+
if par.name.id_str == 'Type':
|
|
211
|
+
par_type = par
|
|
212
|
+
if par_group_address is None:
|
|
213
|
+
raise Exception('par_group_address is None')
|
|
214
|
+
if par_type is None:
|
|
215
|
+
raise Exception('par_type is None')
|
|
216
|
+
group_address = par_values[par_group_address].value
|
|
217
|
+
ty = par_values[par_type]
|
|
218
|
+
print(f' Using group address: {group_address!r}', flush=True)
|
|
219
|
+
print(f' Using type: {ty!r}', flush=True)
|
|
220
|
+
|
|
221
|
+
# TODO: Clean up
|
|
222
|
+
bus_path, bus_var = blocks.resolve_variable(path, path[-1].block_inputs_byname['Bus'])
|
|
223
|
+
print(f' Using bus object: {bus_path}', flush=True)
|
|
224
|
+
|
|
225
|
+
self.native_bus = engine.native_states[bus_path]
|
|
226
|
+
print(f' Using native bus object: {self.native_bus}', flush=True)
|
|
227
|
+
|
|
228
|
+
self.address = xknx.telegram.GroupAddress(group_address)
|
|
229
|
+
|
|
230
|
+
self.value_type = ty
|
|
231
|
+
if ty == blocks.types.boolean:
|
|
232
|
+
self.value_type_knx = "binary"
|
|
233
|
+
elif ty == blocks.types.uint16:
|
|
234
|
+
self.value_type_knx = "2byte_unsigned"
|
|
235
|
+
else:
|
|
236
|
+
raise Exception(f'TODO: Unsupported type for KNX.ValueOptionProvider: {ty}')
|
|
237
|
+
|
|
238
|
+
# TODO: Where does this get cleaned up?
|
|
239
|
+
self.expose = xknx.devices.ExposeSensor(self.native_bus.knx, name="Expose", group_address=self.address, value_type=self.value_type_knx)
|
|
240
|
+
self.native_bus.knx.devices.async_add(self.expose) # TODO: Is this needed?
|
|
241
|
+
|
|
242
|
+
def start_tasks(self, task_group, async_exit_stack: contextlib.AsyncExitStack):
|
|
243
|
+
super().start_tasks(task_group, async_exit_stack)
|
|
244
|
+
|
|
245
|
+
if self.task is not None:
|
|
246
|
+
raise Exception('self.task is not None')
|
|
247
|
+
|
|
248
|
+
self.task = self.start_task(task_group, async_exit_stack, self.run_task())
|
|
249
|
+
|
|
250
|
+
@native_procedure
|
|
251
|
+
def set_value(self, *, context):
|
|
252
|
+
# TODO: Add checks whether everything is running?
|
|
253
|
+
|
|
254
|
+
var_input = context.static.sources['implicit input']
|
|
255
|
+
|
|
256
|
+
value_opt = context.dep_values[var_input]
|
|
257
|
+
|
|
258
|
+
if value_opt is None:
|
|
259
|
+
# TODO: Somehow reset the self.expose value in this case?
|
|
260
|
+
value = None
|
|
261
|
+
else:
|
|
262
|
+
value = value_opt[0]
|
|
263
|
+
# TODO: Any checks needed here?
|
|
264
|
+
|
|
265
|
+
print(f'Setting output {self.address} to {value}, old value_to_send is {self.value_to_send}', flush=True)
|
|
266
|
+
|
|
267
|
+
# This does not work because this function is not async
|
|
268
|
+
# Also this would have to handle None values
|
|
269
|
+
# await self.expose.set(value)
|
|
270
|
+
# Note: This can overwrite a non-None value if sending is too slow. In this case, the overwritten value will be discarded and not sent at all.
|
|
271
|
+
# TODO: Provide some notification if this is happening?
|
|
272
|
+
self.value_to_send = value
|
|
273
|
+
self.event.set()
|
|
274
|
+
|
|
275
|
+
async def run_task(self):
|
|
276
|
+
while True:
|
|
277
|
+
while self.value_to_send is None:
|
|
278
|
+
# Note: This relies on the fact that everything is running on one thread and therefore everything without await is atomic with regard to other tasks
|
|
279
|
+
self.event.clear()
|
|
280
|
+
await self.event.wait()
|
|
281
|
+
value = self.value_to_send
|
|
282
|
+
self.value_to_send = None
|
|
283
|
+
|
|
284
|
+
print(f'Sending {value} to {self.address}', flush=True)
|
|
285
|
+
await self.expose.set(value)
|
|
286
|
+
print(f'Sending {value} to {self.address} done', flush=True)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@native_block_type(native_block_types, has_state=True, data_type=KnxData)
|
|
290
|
+
class EventReceiver(blocks.NativeState):
|
|
291
|
+
def __init__(self, path, engine, static_block_context):
|
|
292
|
+
super().__init__(path, engine, static_block_context)
|
|
293
|
+
|
|
294
|
+
self.task = None
|
|
295
|
+
self.received_values = []
|
|
296
|
+
|
|
297
|
+
self.data = static_block_context.parsed_native_instance
|
|
298
|
+
|
|
299
|
+
print('Create KNX EventReceiver', flush=True)
|
|
300
|
+
|
|
301
|
+
import xknx
|
|
302
|
+
|
|
303
|
+
# TODO: Clean this up
|
|
304
|
+
proc = None
|
|
305
|
+
for p in self.block_or_singleton.block_procedures:
|
|
306
|
+
if p.name == 'get_value':
|
|
307
|
+
proc = p
|
|
308
|
+
if proc is None:
|
|
309
|
+
raise Exception('proc is None')
|
|
310
|
+
self.proc = proc
|
|
311
|
+
|
|
312
|
+
self.data.dump()
|
|
313
|
+
|
|
314
|
+
# TODO: Clean up
|
|
315
|
+
# TODO: Properly follow connections
|
|
316
|
+
bus_obj = path[-1].block_inputs_byname['Bus'].connected_source
|
|
317
|
+
print(f' Using bus object: {bus_obj}', flush=True)
|
|
318
|
+
bus_block = bus_obj.block
|
|
319
|
+
# TODO: This is a hack
|
|
320
|
+
bus_path = (bus_block,)
|
|
321
|
+
|
|
322
|
+
self.native_bus = engine.native_states[bus_path]
|
|
323
|
+
print(f' Using native bus object: {self.native_bus}', flush=True)
|
|
324
|
+
|
|
325
|
+
self.address = xknx.telegram.GroupAddress(self.data.group_address)
|
|
326
|
+
|
|
327
|
+
# TODO: Where does this get cleaned up?
|
|
328
|
+
# TODO: Use RemoteValue directly?
|
|
329
|
+
# TODO: Try to make sure that GroupValueResponses are ignored and only GroupValueWrites are used?
|
|
330
|
+
if self.data.value_type_knx == "binary":
|
|
331
|
+
self.sensor = xknx.devices.BinarySensor(self.native_bus.knx, name="Sensor", group_address_state=self.address, device_updated_cb=self.event_callback, sync_state=False, ignore_internal_state=True)
|
|
332
|
+
else:
|
|
333
|
+
self.sensor = xknx.devices.Sensor(self.native_bus.knx, name="Sensor", group_address_state=self.address, value_type=self.data.value_type_knx, device_updated_cb=self.event_callback, sync_state=False, always_callback=True)
|
|
334
|
+
self.native_bus.knx.devices.async_add(self.sensor)
|
|
335
|
+
|
|
336
|
+
# TODO: Make sure exceptions here are handled somehow?
|
|
337
|
+
def event_callback(self, device):
|
|
338
|
+
# print('update_callback()', flush=True)
|
|
339
|
+
if self.data.value_type_knx == "binary":
|
|
340
|
+
val = device.state
|
|
341
|
+
else:
|
|
342
|
+
val = device.sensor_value.value
|
|
343
|
+
print(f'KNX.EventReceiver: got value: {val!r}', flush=True)
|
|
344
|
+
value = self.data.value_type.verify_value(val)
|
|
345
|
+
|
|
346
|
+
self.received_values.append(value)
|
|
347
|
+
self.engine.trigger(self.block_path, self.proc)
|
|
348
|
+
|
|
349
|
+
@native_procedure
|
|
350
|
+
def get_value(self, *, context):
|
|
351
|
+
# TODO: Add checks whether everything is running?
|
|
352
|
+
|
|
353
|
+
var_output = context.static.targets['']
|
|
354
|
+
|
|
355
|
+
if len(self.received_values) == 0:
|
|
356
|
+
context.results[var_output] = None
|
|
357
|
+
return
|
|
358
|
+
value = self.received_values.pop(0)
|
|
359
|
+
|
|
360
|
+
res_opt = (value,)
|
|
361
|
+
# print(f'get_value: {res_opt}')
|
|
362
|
+
|
|
363
|
+
context.results[var_output] = res_opt
|
|
364
|
+
|
|
365
|
+
self.engine.trigger(self.block_path, self.proc)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@native_block_type(native_block_types, has_state=True, data_type=KnxData)
|
|
369
|
+
class EventEmitter(blocks.NativeState):
|
|
370
|
+
def __init__(self, path, engine, static_block_context):
|
|
371
|
+
super().__init__(path, engine, static_block_context)
|
|
372
|
+
|
|
373
|
+
# Task is needed for sending out values
|
|
374
|
+
self.task = None
|
|
375
|
+
self.values_to_send = []
|
|
376
|
+
# Note: Use an event instead of a condition because setting an event does not require await
|
|
377
|
+
# self.condition_var = asyncio.Condition()
|
|
378
|
+
self.event = asyncio.Event()
|
|
379
|
+
|
|
380
|
+
import xknx
|
|
381
|
+
|
|
382
|
+
print('Create KNX ValueOptionProvider', flush=True)
|
|
383
|
+
|
|
384
|
+
self.data = static_block_context.parsed_native_instance
|
|
385
|
+
|
|
386
|
+
# TODO: Clean this up
|
|
387
|
+
proc = None
|
|
388
|
+
for p in self.block_or_singleton.block_procedures:
|
|
389
|
+
if p.name == 'send_value':
|
|
390
|
+
proc = p
|
|
391
|
+
if proc is None:
|
|
392
|
+
raise Exception('proc is None')
|
|
393
|
+
self.proc = proc
|
|
394
|
+
|
|
395
|
+
# TODO: Clean up
|
|
396
|
+
bus_path, bus_var = blocks.resolve_variable(path, path[-1].block_inputs_byname['Bus'])
|
|
397
|
+
self.data.dump()
|
|
398
|
+
|
|
399
|
+
self.native_bus = engine.native_states[bus_path]
|
|
400
|
+
print(f' Using native bus object: {self.native_bus}', flush=True)
|
|
401
|
+
|
|
402
|
+
self.address = xknx.telegram.GroupAddress(self.data.group_address)
|
|
403
|
+
|
|
404
|
+
# TODO: Where does this get cleaned up?
|
|
405
|
+
# Use ExposeSensor instead of Sensor because Sensor does not support binary and BinarySensor does not support writing values
|
|
406
|
+
self.sensor = xknx.devices.ExposeSensor(self.native_bus.knx, name="EventSender", group_address=self.address, respond_to_read=False, value_type=self.data.value_type_knx)
|
|
407
|
+
self.native_bus.knx.devices.async_add(self.sensor) # TODO: Is this needed?
|
|
408
|
+
|
|
409
|
+
def start_tasks(self, task_group, async_exit_stack: contextlib.AsyncExitStack):
|
|
410
|
+
super().start_tasks(task_group, async_exit_stack)
|
|
411
|
+
|
|
412
|
+
if self.task is not None:
|
|
413
|
+
raise Exception('self.task is not None')
|
|
414
|
+
|
|
415
|
+
self.task = self.start_task(task_group, async_exit_stack, self.run_task())
|
|
416
|
+
|
|
417
|
+
@native_procedure
|
|
418
|
+
def send_value(self, *, context):
|
|
419
|
+
# TODO: Add checks whether everything is running?
|
|
420
|
+
|
|
421
|
+
data = context.static.block.parsed_native_instance
|
|
422
|
+
|
|
423
|
+
var_input = context.static.sources['implicit input']
|
|
424
|
+
|
|
425
|
+
value_opt = context.dep_values[var_input]
|
|
426
|
+
|
|
427
|
+
# TODO: Is this correct or should this distinguish between a value which has not yet been sent/received and an empty string?
|
|
428
|
+
if value_opt is None:
|
|
429
|
+
return
|
|
430
|
+
else:
|
|
431
|
+
value = value_opt[0]
|
|
432
|
+
# TODO: Any checks needed here?
|
|
433
|
+
|
|
434
|
+
print(f'Sending event {value} to {data.group_address}, old values_to_send is {self.values_to_send}', flush=True)
|
|
435
|
+
|
|
436
|
+
# Note: If sending is too slow, there might be multiple events queued up.
|
|
437
|
+
# TODO: Provide a notification in this case?
|
|
438
|
+
self.values_to_send.append(value)
|
|
439
|
+
self.event.set()
|
|
440
|
+
|
|
441
|
+
# This makes sure that the value is sent again if the input does not change
|
|
442
|
+
self.engine.trigger(self.block_path, self.proc)
|
|
443
|
+
|
|
444
|
+
async def run_task(self):
|
|
445
|
+
while True:
|
|
446
|
+
while len(self.values_to_send) == 0:
|
|
447
|
+
# Note: This relies on the fact that everything is running on one thread and therefore everything without await is atomic with regard to other tasks
|
|
448
|
+
self.event.clear()
|
|
449
|
+
await self.event.wait()
|
|
450
|
+
value = self.values_to_send.pop(0)
|
|
451
|
+
|
|
452
|
+
print(f'Sending {value} to {self.address}', flush=True)
|
|
453
|
+
# TODO: Call self.sensor.sensor_value.set(value) directly instead?
|
|
454
|
+
await self.sensor.set(value)
|
|
455
|
+
print(f'Sending {value} to {self.address} done', flush=True)
|
wirely/native/memory.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
import wirely as blocks
|
|
4
|
+
from wirely.native import native_block_type, native_procedure
|
|
5
|
+
|
|
6
|
+
native_block_types = {}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass(frozen=True)
|
|
10
|
+
class ToggleFlipFlopTriggerData:
|
|
11
|
+
output_value: object
|
|
12
|
+
minimum_delay: blocks.Duration
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def create(*, parameter_values):
|
|
16
|
+
# TODO: default parameters should be handled somewhere else
|
|
17
|
+
if 'OutputType' not in parameter_values:
|
|
18
|
+
par_type = blocks.types.unit
|
|
19
|
+
else:
|
|
20
|
+
par_type = parameter_values['OutputType']
|
|
21
|
+
|
|
22
|
+
if 'OutputValue' not in parameter_values:
|
|
23
|
+
output_value = None
|
|
24
|
+
if par_type != blocks.types.unit:
|
|
25
|
+
# TODO: Provide error location in a better way / avoid stack trace?
|
|
26
|
+
raise Exception(f'Trigger type is not unit ({par_type}) but no value is given')
|
|
27
|
+
else:
|
|
28
|
+
output_value = parameter_values['OutputValue'].value
|
|
29
|
+
|
|
30
|
+
output_value = par_type.verify_value(output_value)
|
|
31
|
+
|
|
32
|
+
# TODO: default parameters should be handled somewhere else
|
|
33
|
+
if 'MinimumDelay' not in parameter_values:
|
|
34
|
+
minimum_delay = blocks.Duration.from_seconds(1)
|
|
35
|
+
else:
|
|
36
|
+
minimum_delay = parameter_values['MinimumDelay'].value
|
|
37
|
+
|
|
38
|
+
return ToggleFlipFlopTriggerData(output_value=output_value, minimum_delay=minimum_delay)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@native_block_type(native_block_types, has_state=False, data_type=ToggleFlipFlopTriggerData)
|
|
42
|
+
class ToggleFlipFlopTrigger:
|
|
43
|
+
@native_procedure
|
|
44
|
+
@staticmethod
|
|
45
|
+
def read_time(*, context):
|
|
46
|
+
var_output = context.static.targets['NextEventAllowedAtOld']
|
|
47
|
+
var_state = context.static.state['NextEventAllowedAt']
|
|
48
|
+
|
|
49
|
+
value = context.state_old[var_state]
|
|
50
|
+
|
|
51
|
+
# Turn value from Option[MonotonicTime] into MonotonicTime
|
|
52
|
+
if value is None:
|
|
53
|
+
value = blocks.MonotonicTime.create_invalid()
|
|
54
|
+
else:
|
|
55
|
+
value = value[0]
|
|
56
|
+
|
|
57
|
+
context.results[var_output] = value
|
|
58
|
+
|
|
59
|
+
@native_procedure
|
|
60
|
+
@staticmethod
|
|
61
|
+
def update(*, context):
|
|
62
|
+
var_output = context.static.targets['']
|
|
63
|
+
var_input = context.static.sources['implicit input']
|
|
64
|
+
var_feedback = context.static.sources['Feedback']
|
|
65
|
+
var_reached = context.static.sources['NextEventAllowedAtReached']
|
|
66
|
+
var_next = context.static.state['NextEventAllowedAt']
|
|
67
|
+
var_pending = context.static.state['PendingEvent']
|
|
68
|
+
|
|
69
|
+
data = context.static.block.parsed_native_instance
|
|
70
|
+
now = context.step_info.stage_start_time_monotonic[blocks.RunStage.Default]
|
|
71
|
+
|
|
72
|
+
inp = context.dep_values[var_input]
|
|
73
|
+
feedback = context.dep_values[var_feedback]
|
|
74
|
+
reached = context.dep_values[var_reached]
|
|
75
|
+
next = context.state_old[var_next]
|
|
76
|
+
pending = context.state_old[var_pending]
|
|
77
|
+
# print(f'ToggleFlipFlopTrigger.run inp={inp} feedback={feedback} reached={reached} next={next} pending={pending}')
|
|
78
|
+
|
|
79
|
+
if next is not None and reached is None:
|
|
80
|
+
# Time has been lost while waiting for timeout.
|
|
81
|
+
# Reset pending, but don't ignore input
|
|
82
|
+
pending = None
|
|
83
|
+
|
|
84
|
+
# Always overwrite pending with input
|
|
85
|
+
if inp is not None:
|
|
86
|
+
pending = inp
|
|
87
|
+
|
|
88
|
+
# Check whether pending is current value
|
|
89
|
+
if feedback is None:
|
|
90
|
+
# No feedback => Reset pending value and ignore input
|
|
91
|
+
pending = None
|
|
92
|
+
elif pending == feedback:
|
|
93
|
+
# Trying to set current value => Unset pending
|
|
94
|
+
pending = None
|
|
95
|
+
|
|
96
|
+
if next is not None and reached == (False,):
|
|
97
|
+
# Cannot emit event right now
|
|
98
|
+
output = None
|
|
99
|
+
# Keep value of next
|
|
100
|
+
else:
|
|
101
|
+
# Set output if needed
|
|
102
|
+
if pending is None:
|
|
103
|
+
output = None
|
|
104
|
+
next = None
|
|
105
|
+
else:
|
|
106
|
+
# Trigger change
|
|
107
|
+
output = (data.output_value,)
|
|
108
|
+
pending = None
|
|
109
|
+
next = (now + data.minimum_delay,)
|
|
110
|
+
|
|
111
|
+
context.results[var_output] = output
|
|
112
|
+
context.state_new[var_next] = next
|
|
113
|
+
context.state_new[var_pending] = pending
|