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/engine.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import datetime
|
|
4
|
+
|
|
5
|
+
import wirely as blocks
|
|
6
|
+
from wirely.util import check_parameters
|
|
7
|
+
|
|
8
|
+
class Engine:
|
|
9
|
+
def __init__(self, program):
|
|
10
|
+
if not isinstance(program, blocks.Program):
|
|
11
|
+
raise Exception('program parameter is not a Program')
|
|
12
|
+
|
|
13
|
+
self.program = program
|
|
14
|
+
|
|
15
|
+
self.cache = None
|
|
16
|
+
|
|
17
|
+
self.debug = False
|
|
18
|
+
|
|
19
|
+
# Is a datetime during async task shutdown unless the shutdown was triggered before all async tasks were started
|
|
20
|
+
self.shutdown_time = None
|
|
21
|
+
|
|
22
|
+
# The time of the last complete async task shutdown. Should be used only for debugging / tests.
|
|
23
|
+
self.last_shutdown_time = None
|
|
24
|
+
|
|
25
|
+
self.triggered_procs = set()
|
|
26
|
+
self.triggered_procs_event = asyncio.Event()
|
|
27
|
+
|
|
28
|
+
self.wakeups = {}
|
|
29
|
+
self.next_wakeup = None
|
|
30
|
+
|
|
31
|
+
self.state = blocks.VariableBlockInstance(self.program.state_variables)
|
|
32
|
+
self.native_states = {}
|
|
33
|
+
|
|
34
|
+
self.fake_monotonic_time_offset = None
|
|
35
|
+
|
|
36
|
+
# TODO: Clean up iterating over all blocks paths
|
|
37
|
+
done_block_path = set()
|
|
38
|
+
for block_name in self.program.graph.blocks:
|
|
39
|
+
for block_path in self.program.graph.blocks[block_name].get_all_block_paths(()):
|
|
40
|
+
if block_path in done_block_path:
|
|
41
|
+
if len(block_path) != 1 or not isinstance(block_path[-1], blocks.BlockType):
|
|
42
|
+
raise Exception('Got duplicate non-singleton block')
|
|
43
|
+
continue
|
|
44
|
+
done_block_path.add(block_path)
|
|
45
|
+
|
|
46
|
+
block = block_path[-1]
|
|
47
|
+
instance = self.program.instances[block_path]
|
|
48
|
+
# TODO: Clean up
|
|
49
|
+
if instance.is_singleton:
|
|
50
|
+
if block != instance.block_type:
|
|
51
|
+
raise Exception('block != instance.block_type')
|
|
52
|
+
else:
|
|
53
|
+
if block != instance.block:
|
|
54
|
+
raise Exception('block != instance.block')
|
|
55
|
+
|
|
56
|
+
# TODO: Pass full context to native state constructor?
|
|
57
|
+
self.native_states[block_path] = block.create_native_state(block_path, self, static_block_context=instance.static_context)
|
|
58
|
+
# TODO: Remove hack
|
|
59
|
+
if not isinstance(block, blocks.Block):
|
|
60
|
+
block = block.singleton_instance
|
|
61
|
+
for var in block.block_statevars:
|
|
62
|
+
self.state.add(self.program.state_map[block_path, var], var.initial_value)
|
|
63
|
+
|
|
64
|
+
async def start_tasks_async(self, task_group, async_exit_stack: contextlib.AsyncExitStack):
|
|
65
|
+
def start_task_cleanup():
|
|
66
|
+
now_monotonic = blocks.MonotonicTime.current_time()
|
|
67
|
+
if self.fake_monotonic_time_offset is not None:
|
|
68
|
+
now_monotonic += self.fake_monotonic_time_offset
|
|
69
|
+
|
|
70
|
+
now_utc = datetime.datetime.now(datetime.timezone.utc)
|
|
71
|
+
now = now_utc.astimezone()
|
|
72
|
+
|
|
73
|
+
print(f'Task cleanup starting at {now}')
|
|
74
|
+
|
|
75
|
+
# TODO: Set monotonic_shutdown_time?
|
|
76
|
+
self.shutdown_time = now
|
|
77
|
+
|
|
78
|
+
def finish_task_cleanup():
|
|
79
|
+
print('Task cleanup done')
|
|
80
|
+
|
|
81
|
+
self.last_shutdown_time = self.shutdown_time
|
|
82
|
+
self.shutdown_time = None
|
|
83
|
+
|
|
84
|
+
async_exit_stack.callback(finish_task_cleanup)
|
|
85
|
+
|
|
86
|
+
# TODO: Currently this (e.g. connecting to knxd) is blocking and done sequentially. Should ths be non-blocking instead? (I.e. should the connection be created on the background task and should start_tasks_async() be removed?)
|
|
87
|
+
for state in self.native_states.values():
|
|
88
|
+
if state is not None:
|
|
89
|
+
state.start_tasks(task_group, async_exit_stack)
|
|
90
|
+
await state.start_tasks_async(task_group, async_exit_stack)
|
|
91
|
+
|
|
92
|
+
async_exit_stack.callback(start_task_cleanup)
|
|
93
|
+
|
|
94
|
+
# TODO: Threading?
|
|
95
|
+
def trigger(self, path, proc):
|
|
96
|
+
# TODO: More checks with path
|
|
97
|
+
if not isinstance(proc, blocks.Block.Procedure):
|
|
98
|
+
raise Exception('proc parameter is not a Block.Procedure')
|
|
99
|
+
|
|
100
|
+
if (path, proc) not in self.program.all_procs_set:
|
|
101
|
+
# print(self.program.all_procs_set)
|
|
102
|
+
raise Exception(f'Got procedure {path} {proc} which is not in {self.program.graph}')
|
|
103
|
+
|
|
104
|
+
self.triggered_procs.add((path, proc))
|
|
105
|
+
self.triggered_procs_event.set()
|
|
106
|
+
|
|
107
|
+
# Update self.next_wakeup to include wakeup_time
|
|
108
|
+
def update_next_wakeup(self, now, wakeup_time):
|
|
109
|
+
if wakeup_time is not None and not wakeup_time.is_invalid and not blocks.MonotonicTime.is_comparable(now, wakeup_time):
|
|
110
|
+
print('Warning: Got non-current wakeup time {wakeup_time}', flush=True)
|
|
111
|
+
wakeup_time = blocks.MonotonicTime.create_invalid()
|
|
112
|
+
if wakeup_time is None:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if self.next_wakeup is None or wakeup_time.is_invalid or blocks.MonotonicTime.compare(self.next_wakeup, wakeup_time) > 0:
|
|
116
|
+
self.next_wakeup = wakeup_time
|
|
117
|
+
|
|
118
|
+
def step(self):
|
|
119
|
+
if self.debug:
|
|
120
|
+
print(f'Starting step of {self.program.graph}')
|
|
121
|
+
# print(f'Wakeups: {self.wakeups}')
|
|
122
|
+
|
|
123
|
+
self.next_wakeup = None
|
|
124
|
+
|
|
125
|
+
cache = self.cache
|
|
126
|
+
state_old = self.state
|
|
127
|
+
state_new = blocks.VariableBlockInstance(self.program.state_variables)
|
|
128
|
+
vars = blocks.VariableBlockInstance(self.program.temporaries)
|
|
129
|
+
run_count = 0
|
|
130
|
+
change_count = 0
|
|
131
|
+
state_change_count = 0
|
|
132
|
+
step_info = blocks.StepContext()
|
|
133
|
+
for stage, stage_procs in self.program.procs.items():
|
|
134
|
+
# print(f'Stage {stage}', flush=True)
|
|
135
|
+
step_info.start_stage(self, stage)
|
|
136
|
+
|
|
137
|
+
now = step_info.stage_start_time_monotonic[stage]
|
|
138
|
+
|
|
139
|
+
for path, proc in stage_procs:
|
|
140
|
+
if path[-1] != proc.block:
|
|
141
|
+
if isinstance(path[-1], blocks.Block): # TODO: Handle singletons here?
|
|
142
|
+
raise Exception('path[-1] != proc.block')
|
|
143
|
+
native_state = self.native_states[path]
|
|
144
|
+
|
|
145
|
+
# print(proc)
|
|
146
|
+
deps = {}
|
|
147
|
+
skip_step = cache is not None and (path, proc) not in self.triggered_procs
|
|
148
|
+
for dep in proc.dependencies:
|
|
149
|
+
if isinstance(dep, blocks.Block.StateVariable):
|
|
150
|
+
# TODO
|
|
151
|
+
raise Exception('isinstance(dep, blocks.Block.StateVariable)')
|
|
152
|
+
if skip_step and not dep.impl_var_type.equals(state_old[self.program.state_map[path, dep]], cache[dep]):
|
|
153
|
+
skip_step = False
|
|
154
|
+
elif isinstance(dep, blocks.Block.Input) and dep.is_list:
|
|
155
|
+
for child in dep.members:
|
|
156
|
+
# TODO: Move this code somewhere else? / Use already resolved type?
|
|
157
|
+
var_type = child.impl_var_type
|
|
158
|
+
for block in reversed(path):
|
|
159
|
+
var_type = var_type.resolve_self(block.par_values)
|
|
160
|
+
|
|
161
|
+
deps[child] = vars[self.program.variable_map[path, child.var_identifier]]
|
|
162
|
+
if skip_step and not var_type.equals(vars[self.program.variable_map[path, child.var_identifier]], cache[self.program.variable_map[path, child.var_identifier]]):
|
|
163
|
+
skip_step = False
|
|
164
|
+
else:
|
|
165
|
+
# TODO: Move this code somewhere else? / Use already resolved type?
|
|
166
|
+
var_type = dep.impl_var_type
|
|
167
|
+
for block in reversed(path):
|
|
168
|
+
var_type = var_type.resolve_self(block.par_values)
|
|
169
|
+
|
|
170
|
+
# TODO: Program should contain some mapping from a procedures dependencies to variables, this should not be done here
|
|
171
|
+
# print(path, dep, dep.var_identifier)
|
|
172
|
+
if isinstance(path[-1], blocks.Block) and path[-1] != dep.block:
|
|
173
|
+
if dep.block.block_type.is_singleton:
|
|
174
|
+
variable = self.program.variable_map[(dep.block.block_type,), dep.var_identifier]
|
|
175
|
+
else:
|
|
176
|
+
# TODO: This assumes that dep.block is a direct child of the current block, this will not work otherwise
|
|
177
|
+
variable = self.program.variable_map[path + (dep.block,), dep.var_identifier]
|
|
178
|
+
else:
|
|
179
|
+
variable = self.program.variable_map[path, dep.var_identifier]
|
|
180
|
+
|
|
181
|
+
deps[dep] = vars[variable]
|
|
182
|
+
if skip_step and not var_type.equals(vars[variable], cache[variable]):
|
|
183
|
+
skip_step = False
|
|
184
|
+
# TODO: Avoid depending on the value of all state variables?
|
|
185
|
+
for dep in proc.block.block_statevars:
|
|
186
|
+
if skip_step and cache[self.program.state_map_changed[path, dep]]:
|
|
187
|
+
skip_step = False
|
|
188
|
+
res = {}
|
|
189
|
+
|
|
190
|
+
if (path, proc) in self.wakeups:
|
|
191
|
+
if not blocks.MonotonicTime.is_comparable(now, self.wakeups[(path, proc)]) or blocks.MonotonicTime.compare(now, self.wakeups[(path, proc)]) >= 0:
|
|
192
|
+
# Timeout, run step
|
|
193
|
+
skip_step = False
|
|
194
|
+
|
|
195
|
+
if skip_step:
|
|
196
|
+
for res_var in proc.results:
|
|
197
|
+
if isinstance(res_var, blocks.Block.StateVariable):
|
|
198
|
+
old_value = state_old[self.program.state_map[path, res_var]]
|
|
199
|
+
state_new.add(self.program.state_map[path, res_var], old_value)
|
|
200
|
+
vars.add(self.program.state_map_new[path, res_var], old_value)
|
|
201
|
+
vars.add(self.program.state_map_changed[path, res_var], False)
|
|
202
|
+
else:
|
|
203
|
+
# TODO: Clean up temporary / output stuff
|
|
204
|
+
vars.add(self.program.variable_map[path, res_var.var_identifier], cache[self.program.variable_map[path, res_var.var_identifier]])
|
|
205
|
+
|
|
206
|
+
if (path, proc) in self.wakeups:
|
|
207
|
+
self.update_next_wakeup(now, self.wakeups[(path, proc)])
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
if self.debug:
|
|
211
|
+
print(f'Running procedure {path} {proc}')
|
|
212
|
+
|
|
213
|
+
run_count += 1
|
|
214
|
+
|
|
215
|
+
if (path, proc) in self.triggered_procs:
|
|
216
|
+
self.triggered_procs.remove((path, proc))
|
|
217
|
+
|
|
218
|
+
state_old_block = {}
|
|
219
|
+
# TODO: Remove state_new_block, use res instead?
|
|
220
|
+
state_new_block = {}
|
|
221
|
+
for var in proc.block.block_statevars:
|
|
222
|
+
state_old_block[var] = state_old[self.program.state_map[path, var]]
|
|
223
|
+
# TODO: Should this be initialized here?
|
|
224
|
+
# state_new_block[var] = state_old[var]
|
|
225
|
+
|
|
226
|
+
variables = None # TODO
|
|
227
|
+
instance = self.program.instances[path]
|
|
228
|
+
pcontext = blocks.ProcedureContext(dep_values=deps, results=res, state_old=state_old_block, state_new=state_new_block, static=self.program.proc_static_contexts[(instance, proc)], step_info=step_info)
|
|
229
|
+
proc.impl(context=pcontext, native_state=native_state)
|
|
230
|
+
|
|
231
|
+
if pcontext.wakeup_time is None:
|
|
232
|
+
if (path, proc) in self.wakeups:
|
|
233
|
+
del self.wakeups[(path, proc)]
|
|
234
|
+
else:
|
|
235
|
+
self.wakeups[(path, proc)] = pcontext.wakeup_time
|
|
236
|
+
self.update_next_wakeup(now=now, wakeup_time=pcontext.wakeup_time)
|
|
237
|
+
|
|
238
|
+
unknown = set(res)
|
|
239
|
+
unknown_newstate = set(state_new_block)
|
|
240
|
+
# TODO: Clean up, more checking for state variables
|
|
241
|
+
for res_var in proc.results:
|
|
242
|
+
if isinstance(res_var, blocks.Block.StateVariable):
|
|
243
|
+
if res_var not in unknown_newstate:
|
|
244
|
+
raise Exception(f'State output {res_var.name} of native procedure {proc} is missing')
|
|
245
|
+
unknown_newstate.remove(res_var)
|
|
246
|
+
val = state_new_block[res_var]
|
|
247
|
+
state_new.add(self.program.state_map[path, res_var], val)
|
|
248
|
+
|
|
249
|
+
new_value = res_var.impl_var_type.verify_value(val)
|
|
250
|
+
vars.add(self.program.state_map_new[path, res_var], state_old_block[res_var])
|
|
251
|
+
if not res_var.impl_var_type.equals(state_old_block[res_var], new_value):
|
|
252
|
+
state_change_count += 1
|
|
253
|
+
vars.add(self.program.state_map_changed[path, res_var], True)
|
|
254
|
+
else:
|
|
255
|
+
vars.add(self.program.state_map_changed[path, res_var], False)
|
|
256
|
+
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
if res_var not in unknown:
|
|
260
|
+
raise Exception(f'Output {res_var.name_desc} of native procedure {proc} is missing')
|
|
261
|
+
unknown.remove(res_var)
|
|
262
|
+
val = res[res_var]
|
|
263
|
+
# print(f'Writing {res_var}')
|
|
264
|
+
# TODO: Clean up temporary / output stuff
|
|
265
|
+
|
|
266
|
+
# TODO: Move this code somewhere else? / Use already resolved type?
|
|
267
|
+
var_type = res_var.impl_var_type
|
|
268
|
+
for block in reversed(path):
|
|
269
|
+
if isinstance(block, blocks.Block): # TODO: Handle singletons here?
|
|
270
|
+
var_type = var_type.resolve_self(block.par_values)
|
|
271
|
+
|
|
272
|
+
# print('RESULT', proc.block.block_type, proc, res_var)
|
|
273
|
+
new_value = var_type.verify_value(val)
|
|
274
|
+
if cache is None or not var_type.equals(new_value, cache[self.program.variable_map[path, res_var.var_identifier]]):
|
|
275
|
+
change_count += 1
|
|
276
|
+
# print(self.program.variable_map, [path, res_var.var_identifier])
|
|
277
|
+
vars.add(self.program.variable_map[path, res_var.var_identifier], new_value)
|
|
278
|
+
if len(unknown):
|
|
279
|
+
raise Exception(f'{proc} returned unknown values: {sorted(unknown)}')
|
|
280
|
+
if len(unknown_newstate):
|
|
281
|
+
raise Exception(f'{proc} returned unknown state values: {sorted(unknown_newstate)}')
|
|
282
|
+
|
|
283
|
+
self.cache = vars
|
|
284
|
+
self.state = state_new
|
|
285
|
+
|
|
286
|
+
if self.debug:
|
|
287
|
+
print(f'Finished step of {self.program.graph}')
|
|
288
|
+
|
|
289
|
+
# print(state_old, state_new)
|
|
290
|
+
# print(run_count, change_count, state_change_count)
|
|
291
|
+
# return run_count
|
|
292
|
+
# return change_count
|
|
293
|
+
# TODO: Clean up return values
|
|
294
|
+
return run_count, state_change_count, step_info
|
|
295
|
+
|
|
296
|
+
async def run(self):
|
|
297
|
+
while True:
|
|
298
|
+
await asyncio.sleep(0)
|
|
299
|
+
run_count, state_change_count, step_info = self.step()
|
|
300
|
+
if state_change_count == 0:
|
|
301
|
+
now = blocks.MonotonicTime.current_time()
|
|
302
|
+
# TODO: This probably is not really used or tested currently
|
|
303
|
+
if self.fake_monotonic_time_offset is not None:
|
|
304
|
+
now_monotonic += self.fake_monotonic_time_offset
|
|
305
|
+
|
|
306
|
+
# TODO: If there is a "while True" here, the code will crash (instead of looping) because of EAGAIN when writing to the terminal, seems like NONBLOCK is set
|
|
307
|
+
# while True:
|
|
308
|
+
# import os; import fcntl; fcntl.fcntl(1, fcntl.F_GETFL)
|
|
309
|
+
# print(f'Waiting, wakeup is {self.next_wakeup}', flush=True)
|
|
310
|
+
# import os; print(os.
|
|
311
|
+
if self.next_wakeup is None:
|
|
312
|
+
# Sleep without timeout
|
|
313
|
+
await self.triggered_procs_event.wait()
|
|
314
|
+
elif self.next_wakeup.is_invalid or not blocks.MonotonicTime.is_comparable(now, self.next_wakeup):
|
|
315
|
+
# Don't sleep
|
|
316
|
+
pass
|
|
317
|
+
else:
|
|
318
|
+
diff = self.next_wakeup - now
|
|
319
|
+
# Sleep until timeout
|
|
320
|
+
try:
|
|
321
|
+
async with asyncio.timeout(diff.total_seconds):
|
|
322
|
+
await self.triggered_procs_event.wait()
|
|
323
|
+
except TimeoutError:
|
|
324
|
+
pass
|
|
325
|
+
self.triggered_procs_event.clear()
|
|
326
|
+
|
|
327
|
+
def get_block_path_list(self, name_parts):
|
|
328
|
+
path = []
|
|
329
|
+
graph = self.program.graph
|
|
330
|
+
for part in name_parts:
|
|
331
|
+
# print('XXX', graph, part, list(graph.blocks))
|
|
332
|
+
if part not in graph.blocks:
|
|
333
|
+
# TODO: Clean up
|
|
334
|
+
# raise Exception('Cannot find block ' + '.'.join([block.name for block in path] + [part]))
|
|
335
|
+
raise Exception('Cannot find block ' + '.'.join([(block.name if isinstance(block, blocks.Block) else block.name.id_str) for block in path] + [part]))
|
|
336
|
+
block = graph.blocks[part]
|
|
337
|
+
path.append(block)
|
|
338
|
+
graph = block.block_type
|
|
339
|
+
|
|
340
|
+
# For singletons, return the block type
|
|
341
|
+
if path[-1].block_type.is_singleton:
|
|
342
|
+
return (path[-1].block_type,)
|
|
343
|
+
|
|
344
|
+
return tuple(path)
|
|
345
|
+
|
|
346
|
+
def get_block_path(self, name):
|
|
347
|
+
return self.get_block_path_list(name.split('.'))
|
|
348
|
+
|
|
349
|
+
def get_block_path_singleton(self, name):
|
|
350
|
+
scope = self.program.graph
|
|
351
|
+
parts = name.split('.')
|
|
352
|
+
for part in parts[:-1]:
|
|
353
|
+
scope = scope.libraries[part]
|
|
354
|
+
ty = scope.block_type_implementations[parts[-1]]
|
|
355
|
+
return (ty,)
|
|
356
|
+
|
|
357
|
+
# TODO: Name? get_native_state? get_native_block? get_block_instance?
|
|
358
|
+
def get_native_state(self, name):
|
|
359
|
+
path = self.get_block_path(name)
|
|
360
|
+
return self.native_states[path]
|
|
361
|
+
|
|
362
|
+
def get_native_state_singleton(self, name):
|
|
363
|
+
path = self.get_block_path_singleton(name)
|
|
364
|
+
return self.native_states[path]
|
|
365
|
+
|
|
366
|
+
def get_variable(self, name):
|
|
367
|
+
name_parts = name.split('.')
|
|
368
|
+
|
|
369
|
+
# TODO: Avoid special case
|
|
370
|
+
if len(name_parts) == 1:
|
|
371
|
+
path = self.get_block_path_list(name_parts)
|
|
372
|
+
block = path[-1]
|
|
373
|
+
# return blocks.resolve_variable(path, block.block_outputs_byname[''])
|
|
374
|
+
path, var = blocks.resolve_variable(path, block.block_outputs_byname[''])
|
|
375
|
+
return path, var.var_identifier
|
|
376
|
+
|
|
377
|
+
path = self.get_block_path_list(name_parts[:-1])
|
|
378
|
+
block = path[-1]
|
|
379
|
+
lname = name_parts[-1]
|
|
380
|
+
|
|
381
|
+
# TODO: Clean this up
|
|
382
|
+
# TODO: How to handle input/output clashes?
|
|
383
|
+
for value in block.block_inputs:
|
|
384
|
+
if value.name == lname:
|
|
385
|
+
return path, value.var_identifier
|
|
386
|
+
for value in block.block_outputs:
|
|
387
|
+
if value.name == lname:
|
|
388
|
+
return path, value.var_identifier
|
|
389
|
+
for value in block.block_statevars:
|
|
390
|
+
if value.name == lname:
|
|
391
|
+
return path, value.var_identifier
|
|
392
|
+
for value in block.block_temporaries:
|
|
393
|
+
if value.name == lname:
|
|
394
|
+
return path, value.var_identifier
|
|
395
|
+
|
|
396
|
+
# print(block.block_inputs, block.block_temporaries, block.block_statevars)
|
|
397
|
+
raise Exception(f'Could not find {name}')
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def get_value(self, name):
|
|
401
|
+
var = self.program.variable_map[self.get_variable(name)]
|
|
402
|
+
return self.cache[var]
|
|
403
|
+
|
|
404
|
+
def assert_value_is(self, name, expected_value):
|
|
405
|
+
actual_value = self.get_value(name)
|
|
406
|
+
if actual_value != expected_value:
|
|
407
|
+
raise Exception(f'Got unexpected value for {name}: Expected {expected_value}, got {actual_value}')
|
|
408
|
+
|
|
409
|
+
def assert_is_idle(self):
|
|
410
|
+
run_count, state_change_count, step_info = self.step()
|
|
411
|
+
if run_count != 0:
|
|
412
|
+
raise Exception(f'Is not idle, executed {run_count} procs')
|
|
413
|
+
|
|
414
|
+
def advance_fake_monotonic_time(self, offset):
|
|
415
|
+
if self.fake_monotonic_time_offset is None:
|
|
416
|
+
print('Warning: Setting fake monotonic time offset', flush=True)
|
|
417
|
+
self.fake_monotonic_time_offset = blocks.Duration(duration_ns=0)
|
|
418
|
+
self.fake_monotonic_time_offset += offset
|
wirely/enum.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# TODO: Move enums somewhere else?
|
|
5
|
+
# See Matter Application Cluster Specification
|
|
6
|
+
class ThermostatSystemMode(enum.Enum):
|
|
7
|
+
Off = 0
|
|
8
|
+
Auto = 1
|
|
9
|
+
Cool = 3
|
|
10
|
+
Heat = 4
|
|
11
|
+
EmergencyHeat = 5
|
|
12
|
+
Precooling = 6
|
|
13
|
+
FanOnly = 7
|
|
14
|
+
Dry = 8
|
|
15
|
+
Sleep = 9
|
wirely/example.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import wirely as blocks
|
|
6
|
+
from wirely.util import check_parameters
|
|
7
|
+
|
|
8
|
+
# TODO: Where should these imports be done?
|
|
9
|
+
import wirely.compiler
|
|
10
|
+
import wirely.run
|
|
11
|
+
|
|
12
|
+
fn = os.path.dirname(__file__) + '/../examples/test.wrl'
|
|
13
|
+
global_block_type = blocks.compiler.compile_files([fn])
|
|
14
|
+
if global_block_type is None:
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
17
|
+
prog = blocks.Program.compile(global_block_type)
|
|
18
|
+
if prog is None:
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
eng = blocks.Engine(prog)
|
|
22
|
+
|
|
23
|
+
# eng.debug = True
|
|
24
|
+
|
|
25
|
+
inp1_inst = eng.native_states[(global_block_type.blocks['inp1'],)]
|
|
26
|
+
inp2_inst = eng.native_states[(global_block_type.blocks['inp2'],)]
|
|
27
|
+
|
|
28
|
+
if True:
|
|
29
|
+
inp1_inst.set_value(True)
|
|
30
|
+
inp2_inst.set_value(True)
|
|
31
|
+
eng.step()
|
|
32
|
+
eng.step()
|
|
33
|
+
|
|
34
|
+
inp2_inst.set_value(False)
|
|
35
|
+
|
|
36
|
+
asyncio.run(blocks.run.run(eng))
|
wirely/graphviz.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import graphviz
|
|
2
|
+
|
|
3
|
+
def htmlescape(s):
|
|
4
|
+
return s.replace('&', '&').replace('"', '"').replace('<', '<').replace('>', '>')
|
|
5
|
+
|
|
6
|
+
def get_graph(graph, engine=None):
|
|
7
|
+
digraph = graphviz.Digraph()
|
|
8
|
+
|
|
9
|
+
for block_name in graph.blocks:
|
|
10
|
+
block = graph.blocks[block_name]
|
|
11
|
+
|
|
12
|
+
# label = f'{block.name}\n{block.block_type.name}'
|
|
13
|
+
|
|
14
|
+
inputs = ''
|
|
15
|
+
for input in block.all_block_inputs:
|
|
16
|
+
label = input.name
|
|
17
|
+
if label == '':
|
|
18
|
+
label = '()'
|
|
19
|
+
if engine is not None and engine.cache is not None:
|
|
20
|
+
# TODO: Clean up .temporary
|
|
21
|
+
key = (input.temporary.block), input.temporary
|
|
22
|
+
if key in engine.program.variable_map:
|
|
23
|
+
label += f' = {engine.cache[engine.program.variable_map[key]]}'
|
|
24
|
+
else:
|
|
25
|
+
# TODO: Handle this
|
|
26
|
+
label += f' = MISSING'
|
|
27
|
+
inputs += f'<TD PORT="{htmlescape(input.name)}">{htmlescape(label)}</TD>'
|
|
28
|
+
if inputs != '':
|
|
29
|
+
inputs = f'''
|
|
30
|
+
<TR><TD>
|
|
31
|
+
<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
|
|
32
|
+
<TR>{inputs}</TR>
|
|
33
|
+
</TABLE>
|
|
34
|
+
</TD></TR>
|
|
35
|
+
'''
|
|
36
|
+
|
|
37
|
+
outputs = ''
|
|
38
|
+
for output in block.block_outputs:
|
|
39
|
+
label = output.name
|
|
40
|
+
if label == '':
|
|
41
|
+
label = '()'
|
|
42
|
+
if engine is not None and engine.cache is not None:
|
|
43
|
+
# TODO: Clean up .temporary
|
|
44
|
+
key = (output.temporary.block,), output.temporary
|
|
45
|
+
if key in engine.program.variable_map:
|
|
46
|
+
label += f' = {engine.cache[engine.program.variable_map[key]]}'
|
|
47
|
+
else:
|
|
48
|
+
# TODO: Handle this
|
|
49
|
+
label += f' = MISSING'
|
|
50
|
+
outputs += f'<TD PORT="{htmlescape(output.name)}">{htmlescape(label)}</TD>'
|
|
51
|
+
if outputs != '':
|
|
52
|
+
outputs = f'''
|
|
53
|
+
<TR><TD>
|
|
54
|
+
<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
|
|
55
|
+
<TR>{outputs}</TR>
|
|
56
|
+
</TABLE>
|
|
57
|
+
</TD></TR>
|
|
58
|
+
'''
|
|
59
|
+
|
|
60
|
+
s = f'''<
|
|
61
|
+
<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0">
|
|
62
|
+
{inputs}
|
|
63
|
+
<TR><TD>
|
|
64
|
+
<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
|
|
65
|
+
<TR>
|
|
66
|
+
<TD>{htmlescape(block.name)}</TD>
|
|
67
|
+
</TR>
|
|
68
|
+
<TR>
|
|
69
|
+
<TD>{htmlescape(block.block_type.name.id_str)}</TD>
|
|
70
|
+
</TR>
|
|
71
|
+
</TABLE>
|
|
72
|
+
</TD></TR>
|
|
73
|
+
{outputs}
|
|
74
|
+
</TABLE>
|
|
75
|
+
>'''
|
|
76
|
+
# digraph.node(block.name, label, shape='box')
|
|
77
|
+
digraph.node(block.name, s, shape='plaintext')
|
|
78
|
+
|
|
79
|
+
for block_name in graph.blocks:
|
|
80
|
+
block = graph.blocks[block_name]
|
|
81
|
+
for input in block.all_block_inputs:
|
|
82
|
+
if input.connected_source is not None:
|
|
83
|
+
source = input.connected_source
|
|
84
|
+
source_block = source.block
|
|
85
|
+
|
|
86
|
+
# label = f'{source.name} -> {input.name}'
|
|
87
|
+
# digraph.edge(source_block.name, block.name, label=label)
|
|
88
|
+
|
|
89
|
+
# edge_name = block.name + '/' + input.name
|
|
90
|
+
# digraph.node(edge_name, '', shape='point')
|
|
91
|
+
# digraph.edge(source_block.name, edge_name, label=source.name)
|
|
92
|
+
# digraph.edge(edge_name, block.name, label=input.name)
|
|
93
|
+
|
|
94
|
+
digraph.edge(source_block.name + ':' + source.name, block.name + ':' + input.name)
|
|
95
|
+
digraph.node(block_name)
|
|
96
|
+
|
|
97
|
+
return digraph
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run(args):
|
|
101
|
+
import wirely.compiler
|
|
102
|
+
|
|
103
|
+
graph = wirely.compiler.compile_files([args.input_filename])
|
|
104
|
+
if graph is None:
|
|
105
|
+
return 1
|
|
106
|
+
|
|
107
|
+
get_graph(graph).view()
|
wirely/knx/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Connect xknx TCP tunnels through Wirely-provided stream connections."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import xknx
|
|
6
|
+
from xknx.io import ConnectionConfig, ConnectionType, KNXIPInterface, TCPTunnel
|
|
7
|
+
from xknx.io.transport import TCPTransport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StreamConnectionConfig(ConnectionConfig):
|
|
11
|
+
"""TCP tunnel configuration with a callback for opening its stream."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, connect_cb, individual_address=None):
|
|
14
|
+
super().__init__(
|
|
15
|
+
connection_type=ConnectionType.TUNNELING_TCP,
|
|
16
|
+
individual_address=individual_address,
|
|
17
|
+
)
|
|
18
|
+
self.connect_cb = connect_cb
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StreamTCPTransport(TCPTransport):
|
|
22
|
+
"""Use xknx's TCP framing over a caller-provided stream."""
|
|
23
|
+
|
|
24
|
+
__slots__ = ("_stream_connect_cb",)
|
|
25
|
+
|
|
26
|
+
def __init__(self, connect_cb, connection_lost_cb):
|
|
27
|
+
super().__init__(
|
|
28
|
+
remote_addr=("0.0.0.0", 0),
|
|
29
|
+
connection_lost_cb=connection_lost_cb,
|
|
30
|
+
)
|
|
31
|
+
self._stream_connect_cb = connect_cb
|
|
32
|
+
|
|
33
|
+
async def connect(self):
|
|
34
|
+
protocol = self.TCPTransportFactory(
|
|
35
|
+
data_received_callback=self.data_received_callback,
|
|
36
|
+
connection_lost_callback=self._connection_lost,
|
|
37
|
+
)
|
|
38
|
+
self.transport, _ = await self._stream_connect_cb(
|
|
39
|
+
asyncio.get_running_loop(), lambda: protocol
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class StreamTCPTunnel(TCPTunnel):
|
|
44
|
+
"""Create the stream transport while retaining xknx's tunnel logic."""
|
|
45
|
+
|
|
46
|
+
__slots__ = ("_stream_connect_cb",)
|
|
47
|
+
|
|
48
|
+
def __init__(self, xknx_instance, connect_cb, individual_address,
|
|
49
|
+
cemi_received_callback, auto_reconnect, auto_reconnect_wait):
|
|
50
|
+
self._stream_connect_cb = connect_cb
|
|
51
|
+
super().__init__(
|
|
52
|
+
xknx_instance,
|
|
53
|
+
cemi_received_callback=cemi_received_callback,
|
|
54
|
+
gateway_ip="0.0.0.0",
|
|
55
|
+
gateway_port=0,
|
|
56
|
+
individual_address=individual_address,
|
|
57
|
+
auto_reconnect=auto_reconnect,
|
|
58
|
+
auto_reconnect_wait=auto_reconnect_wait,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def _init_transport(self):
|
|
62
|
+
self.transport = StreamTCPTransport(
|
|
63
|
+
connect_cb=self._stream_connect_cb,
|
|
64
|
+
connection_lost_cb=self._tunnel_lost,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class StreamKNXIPInterface(KNXIPInterface):
|
|
69
|
+
"""Start a TCP tunnel using a StreamConnectionConfig."""
|
|
70
|
+
|
|
71
|
+
async def start(self):
|
|
72
|
+
config = self.connection_config
|
|
73
|
+
self.xknx.cemi_handler.data_secure_init(keyring=None)
|
|
74
|
+
self._interface = StreamTCPTunnel(
|
|
75
|
+
self.xknx,
|
|
76
|
+
connect_cb=config.connect_cb,
|
|
77
|
+
individual_address=config.individual_address,
|
|
78
|
+
cemi_received_callback=self.cemi_received,
|
|
79
|
+
auto_reconnect=config.auto_reconnect,
|
|
80
|
+
auto_reconnect_wait=config.auto_reconnect_wait,
|
|
81
|
+
)
|
|
82
|
+
await self._interface.connect()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def create_xknx(connection_config):
|
|
86
|
+
"""Create xknx with the stream interface when the configuration needs one."""
|
|
87
|
+
knx = xknx.XKNX(connection_config=connection_config)
|
|
88
|
+
if isinstance(connection_config, StreamConnectionConfig):
|
|
89
|
+
knx.knxip_interface = StreamKNXIPInterface(knx, connection_config)
|
|
90
|
+
return knx
|