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.
Files changed (59) hide show
  1. wirely/__init__.py +406 -0
  2. wirely/__main__.py +45 -0
  3. wirely/ast.py +1135 -0
  4. wirely/block.py +542 -0
  5. wirely/block_type.py +342 -0
  6. wirely/blocks.lark +126 -0
  7. wirely/compiler.py +125 -0
  8. wirely/engine.py +418 -0
  9. wirely/enum.py +15 -0
  10. wirely/example.py +36 -0
  11. wirely/graphviz.py +107 -0
  12. wirely/knx/__init__.py +0 -0
  13. wirely/knx/connection_adapter.py +90 -0
  14. wirely/knx/connection_config.py +152 -0
  15. wirely/lib/abb_jra.wrl +64 -0
  16. wirely/lib/csv.wrl +32 -0
  17. wirely/lib/ext_prog.wrl +12 -0
  18. wirely/lib/gpio.wrl +23 -0
  19. wirely/lib/knx.wrl +80 -0
  20. wirely/lib/memory.wrl +45 -0
  21. wirely/lib/mqtt.wrl +66 -0
  22. wirely/lib/std.wrl +134 -0
  23. wirely/lib/time.wrl +93 -0
  24. wirely/lib/two_n.wrl +111 -0
  25. wirely/misc/__init__.py +0 -0
  26. wirely/misc/service.py +48 -0
  27. wirely/native/__init__.py +74 -0
  28. wirely/native/abb_jra.py +66 -0
  29. wirely/native/csv.py +194 -0
  30. wirely/native/ext_prog.py +77 -0
  31. wirely/native/gpio.py +125 -0
  32. wirely/native/knx.py +455 -0
  33. wirely/native/memory.py +113 -0
  34. wirely/native/mqtt.py +605 -0
  35. wirely/native/std.py +334 -0
  36. wirely/native/test_library.py +14 -0
  37. wirely/native/time.py +359 -0
  38. wirely/native/two_n.py +632 -0
  39. wirely/node.py +125 -0
  40. wirely/parser.py +175 -0
  41. wirely/program.py +155 -0
  42. wirely/run.py +143 -0
  43. wirely/service.py +65 -0
  44. wirely/test.py +569 -0
  45. wirely/time.py +172 -0
  46. wirely/two_n/__init__.py +0 -0
  47. wirely/two_n/cert.py +126 -0
  48. wirely/two_n/client.py +402 -0
  49. wirely/two_n/fake_server.py +415 -0
  50. wirely/two_n/grab_images.py +146 -0
  51. wirely/types.py +522 -0
  52. wirely/util.py +111 -0
  53. wirely/value.py +163 -0
  54. wirely-0.1.0.dist-info/METADATA +94 -0
  55. wirely-0.1.0.dist-info/RECORD +59 -0
  56. wirely-0.1.0.dist-info/WHEEL +5 -0
  57. wirely-0.1.0.dist-info/entry_points.txt +2 -0
  58. wirely-0.1.0.dist-info/licenses/LICENSE +21 -0
  59. 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)
@@ -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