25 | | This will be the central feature of '''pKNyX''', allowing user to create virtual devices which mimics real KNX devices. The Device itself will be implemented as the process level. |
26 | | |
27 | | Here is a very simple example of what I have in mind: creating a virtual minimalistic weather station, which uses informations from a non-KNX real weather-station, or from a web site. This station only implements temperature/humidity. |
28 | | |
29 | | {{{ |
30 | | #!python |
31 | | from pknyx.api import FunctionalBlock, Stack, ETS, Scheduler |
32 | | |
33 | | |
34 | | stack = Stack(individualAddress="1.2.3") |
35 | | ets = ETS(stack) |
36 | | schedule = Scheduler() |
37 | | |
38 | | |
39 | | class WeatherTemperatureBlock(FunctionalBlock): |
40 | | |
41 | | DP_01 = dict(name="temperature", access="output", dptId="9.001", default=19.) |
42 | | DP_02 = dict(name="humidity", access="output", dptId="9.007", default=50.) |
43 | | |
44 | | GO_01 = dict(dp="temperature", flags="CRT", priority="low") |
45 | | GO_02 = dict(dp="humidity", flags="CRT", priority="low") |
46 | | |
47 | | @schedule.every(minute=5) |
48 | | def updateTemperatureHumidity(self, event): |
49 | | |
50 | | # temperature = xxx |
51 | | # humidity = xxx |
52 | | self.dp["temperature"] = temperature |
53 | | self.dp["humidity"] = humidity |
54 | | |
55 | | |
56 | | ets.register(WeatherTemperatureBlock, name="weather_temperature", desc="A simple weather temperature/humidity example") |
57 | | |
58 | | ets.weave(fb="weatherTempBlock", dp="temperature", gad="1/1/1") |
59 | | ets.weave(fb="weatherTempBlock", dp="humidity", gad="1/1/2") |
60 | | |
61 | | stack.mainLoop() |
62 | | }}} |
63 | | |
64 | | That's it! As you can see, concepts used here are simple... This Functional Block can be then used from any other real device of your installation, through Groups Addresses {{{1/1/1}}} and {{{1/1/2}}}. All you have to do, is to weave (link, bind...) the Group Objects of your real devices to these Groups Addresses, using the real '''ETS''' application. |
65 | | |
66 | | Lets have a closer look to this example. First, we import some python objects: |
67 | | |
68 | | {{{ |
69 | | #!python |
70 | | from pknyx.api import FunctionalBlock, Stack, ETS, Scheduler |
71 | | }}} |
72 | | |
73 | | These objects are classes. |
74 | | |
75 | | We then instanciante some high level objects and helpers: |
76 | | |
77 | | {{{ |
78 | | #!python |
79 | | stack = Stack(individualAddress="1.2.3") |
80 | | ets = ETS(stack) |
81 | | schedule = Scheduler() |
82 | | }}} |
83 | | |
84 | | The {{{Stack}}} object is used to communicate over the bus (real bus, of course, but also virtual bus). We can give it an Individual Address, to mimic real devices behaviour. This address will be used as Source Address when communicating over the bus. |
85 | | |
86 | | The {{{ETS}}} object is a tool which works more or less like the real ETS application (see below). |
87 | | |
88 | | The {{{Scheduler}}} is a helper implementating a powerfull scheduler (see below). |
89 | | |
90 | | The main part is to create our custom Functional Block; this is done by subclassing the '''!FunctionBlock''' base class, and adding a few attributes/methods: |
91 | | |
92 | | {{{ |
93 | | #!python |
94 | | class WeatherTemperatureBlock(FunctionalBlock): |
95 | | |
96 | | DP_01 = dict(name="temperature", access="output", dptId="9.001", default=19.) |
97 | | DP_02 = dict(name="humidity", access="output", dptId="9.007", default=50.) |
98 | | |
99 | | GO_01 = dict(dp="temperature", flags="CRT", priority="low") |
100 | | GO_02 = dict(dp="humidity", flags="CRT", priority="low") |
101 | | }}} |
102 | | |
103 | | The {{{DP_xx}}} class attributes are the Datapoints of our Functional Block. The {{{GO_xx}}} class attributes are the Group Objects mapping the Datapoints to the bus. They are both defined as python dictionnary; they will be automatically instanciated for us by the framework. The named used here do not matter, as long as they start with {{{DP_}}} for Datapoints, and {{{GO_}}} for Group Objects. |
104 | | |
105 | | There are a few parameters in the dicts; some are obvious, some will need more explanations. But this is out of the scope of this proposal. |
106 | | |
107 | | In our class, we also defined a method which role is to update the temperature and humidity Datapoints values: |
108 | | |
109 | | {{{ |
110 | | #!python |
111 | | @schedule.every(minute=5) |
112 | | def updateTemperatureHumidity(self, event): |
113 | | |
114 | | # temperature = xxx |
115 | | # humidity = xxx |
116 | | self.dp["temperature"].value = temperature |
117 | | self.dp["humidity"].value = humidity |
118 | | }}} |
119 | | |
120 | | Note how this method is periodically called, using the {{{schedule.every()}}} method as python decorator. This decorator will automatically register our method and call it every 5 minutes. |
121 | | |
122 | | In this method, we get the temperature and humidity values (not explained here), and give these values to the respective Datapoints. |
123 | | |
124 | | Then, we register our new Funtional Block (this will automatically instanciate it - and do other things): |
125 | | |
126 | | {{{ |
127 | | #!python |
128 | | ets.register(WeatherTemperatureBlock, name="weather_temperature", desc="A simple weather block example") |
129 | | }}} |
130 | | |
131 | | and use the {{{ETS}}} object to weave (bind, link...) our Datapoints (their matching Group Objects, in fact) to Group Addresses: |
132 | | |
133 | | {{{ |
134 | | #!python |
135 | | ets.weave(fb="weatherTempBlock", dp="temperature", gad="1/1/1") |
136 | | ets.weave(fb="weatherTempBlock", dp="humidity", gad="1/1/2") |
137 | | }}} |
138 | | |
139 | | And finally, we launch the framework main loop: |
140 | | |
141 | | {{{ |
142 | | #!python |
143 | | stack.mainLoop() |
144 | | }}} |
145 | | |
146 | | (this call is blocking). |
147 | | |
148 | | We now have a process (Device) running, listening to the bus. The Datapoints, through their respective Group Objects, will react to requests on the Group Addresses they are weaved to ("1/1/1" and "1/1/2"). According to the flags, they will transmit their internal value on Read requests or if their value internally change (updated). Yes, you don't have to manage this: '''pKNyX''' does it for you! This is the all point of a framework, isn't it? |
149 | | |
150 | | That's it for now. This is only a draft version; final implementation may change, according to feedback/suggestions I will get. But the core is all there. Again, the goal of the framework is to provide very high level tools to build complete and powerfull applications and KNX extensions. |
151 | | == Simple rule == |
152 | | |
153 | | My first idea was to provide a special API to create rules, but in fact, they can be implemented as Functional Block. This way, we use the same paradigm, wich is always better ;o) |
154 | | |
155 | | Let's have a look at another example; this is something I currently implemented as a rule in '''linknx'''. Here, I juste create a Functional Block: |
156 | | |
157 | | {{{ |
158 | | #!python |
159 | | from pknyx.api import FunctionalBlock, Stack, ETS, Scheduler |
160 | | |
161 | | |
162 | | stack = Stack(individualAddress="1.2.3") |
163 | | ets = ETS(stack) |
164 | | schedule = Scheduler() |
165 | | |
166 | | |
167 | | class HeatingManagerBlock(FunctionalBlock): |
168 | | |
169 | | DP_01 = dict(name="temperature", access="input", dptId="9.001", default=19.) |
170 | | DP_02 = dict(name="setup", access="input", dptId="9.001", default=19.) |
171 | | DP_03 = dict(name="heater", access="output", dptId="1.001", default="Off") |
172 | | |
173 | | GO_01 = dict(dp="temperature", flags="CWU", priority="low") |
174 | | GO_02 = dict(dp="setup", flags="CWU", priority="low") |
175 | | GO_03 = dict(dp="heater", flags="CRT", priority="low") |
176 | | |
177 | | @schedule.every(minute=5) |
178 | | def manageHeater(self): |
179 | | |
180 | | # Read inputs |
181 | | temperature = self.dp["bathroom"].value |
182 | | setup = self.dp["setup"].value |
183 | | |
184 | | # Manage heater |
185 | | if temperature < setup - 0.25: |
186 | | heater = "On" |
187 | | elif temperature > setup + 0.25: |
188 | | heater = "Off" |
189 | | |
190 | | # Set outputs |
191 | | self.dp["bathroom_heater"].value = heater |
192 | | |
193 | | |
194 | | ets.register(HeatingManagerBlock, name="heating_manager", desc="A simple heating manager block example") |
195 | | |
196 | | ets.weave(fb="heatingManagerBlock", dp="temperature", gad="1/1/1") |
197 | | ets.weave(fb="heatingManagerBlock", dp="setup", gad="1/1/2") |
198 | | ets.weave(fb="heatingManagerBlock", dp="heater", gad="1/1/3") |
199 | | |
200 | | stack.mainLoop() |
201 | | }}} |
202 | | |
203 | | All you have to do is to use the Group Addresses you use in your real installation, through ETS application. The first one will update the temperature the Functional Block needs; the second one is used to give the setpoint, and the last one is used to switch on/off a real heater, through a KNX actuator. |
204 | | |
205 | | Note that it is possible to instanciate several heating managers, and weave them to different heaters. |
206 | | |
207 | | A more complex heating manager could compute a PID and output the power to use to heat. |