9 | | * Device |
10 | | * Functional Block |
11 | | * Datapoint |
12 | | * Group Object |
13 | | * Flags |
14 | | * ETS |
15 | | * Stack |
16 | | * Group Address |
| 7 | Before we dive into this tutorial, lets define a few words. I tried to use KNX vocabulary, and hope I did understand them correclty. Feel free to contact me if you find mistakes. |
| 8 | |
| 9 | KNX bases are needed. |
| 10 | |
| 11 | * Group Address: multicast destination address of a bus datagram |
| 12 | * Individual Address: physical source address of a bus datagram |
| 13 | * Flags: |
| 14 | * Stack: code implementing KNX OSI layers |
| 15 | * Datapoint: internal variable of a Functional Block |
| 16 | * Group Object: multicast communication proxy of a Datapoint |
| 17 | * Functional Block: black box containing Datapoints and implementing features |
| 18 | * Device: set of Functional Block-s running as a process |
| 19 | * ETS: tool used to weave Device Functional Blocks Groups Objects through Group Address-es |
| 20 | |
| 21 | == Functional Block == |
| 22 | |
| 23 | This is the central feature of '''pKNyX'''. This is where we will implement needed logic. |
| 24 | |
| 25 | A Function Block is implemented as a python class, and have Datapoint-s and Group Object-s attributes. The Datapoint-s reflect the state of the Functional Block; the Group Object-s expose the Datapoint-s to the KNX bus. When implementating a custom Functional Block, we will set/get its Datapoint-s. On the other hand, we won't alter its Group Object-s, which are only used internally for bus communication; we only control how they will be used through their Flags. |
| 26 | |
| 27 | Usually, a Functional Block should remains simple; if complex features are needed, it is better to split them in several Function Block-s, weaved through Group Address-es. This is better for readability, reusability and allow better granularity during deployement. |
| 28 | |
| 29 | Functional Block-s can be deployed as one or several Device-s. In '''pKNyX''', each Device runs as a process. All Device-s can run on the same machine, or spread over a set of machines. Like in real KNX world. |
| 30 | |
| 31 | == Create the device == |
| 32 | |
| 33 | As said, '''pKNyX''' main entry is a Device, which will run as a process. As any good framework, '''pKNyX''' helps in starting writing code by auto-generating some usefull files, in a specific hierarchy, to implement this Device in a correct way. |
| 34 | |
| 35 | The tool to do that is '''{{{pknyx-admin.py}}}'''. This a global tool to manage devices (create/check/start). Let's see how to use it to create a fresh device: |
| 36 | |
| 37 | {{{ |
| 38 | $ pknyx-admin.py createdevice timer |
| 39 | create 'timer' from template... |
| 40 | done |
| 41 | }}} |
| 42 | |
| 43 | This will create a little arborescence under a dir named '''{{{timer}}}''': |
| 44 | |
| 45 | {{{ |
| 46 | $ tree timer |
| 47 | timer |
| 48 | ├── admin.py |
| 49 | └── timer |
| 50 | ├── config.py |
| 51 | ├── device.py |
| 52 | └── __init__.py |
| 53 | |
| 54 | 1 directory, 4 files |
| 55 | }}} |
| 56 | |
| 57 | The top-level dir {{{timer}}} can be renamed as you want. It contains a script called '''{{{admin.py}}}'''; this script is in fact the {{{pknyx-admin.py}}} one, with a pre-defined env var pointing on our timer structure, in order to manage it. It is possible to use the global {{{pknyx-admin.py}}} script, but it would need to define $PKNX_DEVICE_PATH var and make it point to the second {{{timer}}} dir, so that the python interpreter can correctly import our files. So, for now, let's use the {{{admin.py}}} script. |
| 58 | |
| 59 | The '''{{{config.py}}}''' file contains a few pre-defined constants: |
| 60 | |
| 61 | {{{ |
| 62 | #!python |
| 63 | # -*- coding: utf-8 -*- |
| 64 | |
| 65 | from pknyx.common import config |
| 66 | |
| 67 | DEVICE_NAME = "timer" |
| 68 | DEVICE_IND_ADDR = "1.1.1" |
| 69 | DEVICE_VERSION = "0.1" |
| 70 | |
| 71 | config.LOGGER_LEVEL = "info" |
| 72 | }}} |
| 73 | |
| 74 | This is where we will add new configs values for our Device, if needed. |
| 75 | |
| 76 | The last file is '''{{{device.py}}}'''; this is the most important one, where we will implement our Device code: |
| 77 | |
| 78 | {{{ |
| 79 | #!python |
| 80 | # -*- coding: utf-8 -*- |
| 81 | |
| 82 | from pknyx.api import Device, FunctionalBlock |
| 83 | from pknyx.api import logger, schedule, notify |
| 84 | |
| 85 | |
| 86 | class MyFB(FunctionalBlock): |
| 87 | DP_01 = dict(name="mydp", access="output", dptId="1.001", default="Off") |
| 88 | |
| 89 | GO_01 = dict(dp="mydp", flags="CWT", priority="low") |
| 90 | |
| 91 | DESC = "My FB" |
| 92 | |
| 93 | |
| 94 | class MyDevice(Device): |
| 95 | FB_01 = dict(cls=MyFB, name="myfb", desc="my fb") |
| 96 | |
| 97 | LNK_01 = dict(fb="myfb", dp="mydp", gad="1/1/1") |
| 98 | |
| 99 | DESC = "My device" |
| 100 | |
| 101 | |
| 102 | DEVICE = MyDevice |
| 103 | }}} |
| 104 | |
| 105 | As you see, it already contains a dummy Functional Block, and a dummy Device, in order to show how things work. |
20 | | '''Functional Block''' is the central feature of '''pKNyX''', allowing user to create virtual Devices which mimics real KNX Devices. The Device itself is implemented as a process. |
21 | | |
22 | | Here is a very simple example which implements a timer. This timer can monitor the state of a light, and switch it off automatically after a delay. The code is taken from the {{{examples/3_timer}}} directory of '''pKNyX''' sources. |
23 | | |
24 | | {{{ |
25 | | #!python |
26 | | from pknyx.api import Logger |
27 | | from pknyx.api import FunctionalBlock, Stack, ETS |
28 | | from pknyx.api import Scheduler, Notifier |
29 | | |
30 | | NAME = "timer" |
31 | | IND_ADDR = "1.2.3" |
32 | | LEVEL = "info" |
33 | | |
34 | | # ETS group address map |
35 | | GAD_MAP = {"1": dict(name="lights", desc="Lights"), |
36 | | "1/1": dict(name="lights_cmds", desc="Lights commands"), |
37 | | "1/1/1": dict(name="light_test_cmd", desc="Light 'test' (cmd)"), |
38 | | "1/2": dict(name="states", desc="Lights states"), |
39 | | "1/2/1": dict(name="light_test_state", desc="Ligh 'test' (state)"), |
40 | | "1/3": dict(name="lights_delays", desc="Lights delays"), |
41 | | "1/3/1": dict(name="light_test_delay", desc="Light 'test' (delay)"), |
42 | | } |
43 | | |
44 | | |
45 | | logger = Logger("%s-%s" % (NAME, IND_ADDR)) |
46 | | logger.setLevel(LEVEL) |
47 | | |
48 | | stack = Stack(individualAddress=IND_ADDR) |
49 | | ets = ETS(stack=stack, gadMap=GAD_MAP)) |
50 | | |
51 | | schedule = Scheduler() |
52 | | notify = Notifier() |
53 | | |
| 109 | Ok, lets's start to implement as simple example: a timer. This timer monitors the state of a light, and switches it off automatically after a delay. |
| 110 | |
| 111 | Let's modify the {{{device.py}}} according to this: |
| 112 | |
| 113 | {{{ |
| 114 | #!python |
| 115 | from pknyx.api import Device, FunctionalBlock |
| 116 | from pknyx.api import logger, schedule, notify |
78 | | """ Method called when the 'state' datapoint changes |
79 | | """ |
80 | | logger.debug("TimerFB.stateChanged(): event=%s" % repr(event)) |
81 | | |
| 134 | if event['newValue'] == "On": |
| 135 | delay = self.dp["delay"].value |
| 136 | logger.info("%s: start timer for %ds" % (self._name, delay)) |
| 137 | self._timer = delay |
| 138 | elif event['newValue'] == "Off": |
| 139 | if self._timer: |
| 140 | logger.info("%s: switched off detected; cancel timer" % self._name) |
| 141 | self._timer = 0 |
| 142 | |
| 143 | @schedule.every(seconds=1) |
| 144 | def updateTimer(self): |
| 145 | if self._timer: |
| 146 | self._timer -= 1 |
| 147 | if not self._timer: |
| 148 | logger.info("%s: timer expired; switch off" % self._name) |
| 149 | self.dp["cmd"].value = "Off" |
| 150 | |
| 151 | @notify.datapoint(dp="delay", condition="change") |
| 152 | def delayChanged(self, event): |
| 153 | if self._timer: |
| 154 | delay = self.dp["delay"].value |
| 155 | logger.info("%s: delay changed; restart timer" % self._name) |
| 156 | self._timer = delay |
| 157 | |
| 158 | |
| 159 | class Timer(Device): |
| 160 | FB_01 = dict(cls=TimerFB, name="timerfb", desc="timer fb") |
| 161 | |
| 162 | LNK_01 = dict(fb="timerfb", dp="cmd", gad="1/1/1") |
| 163 | LNK_02 = dict(fb="timerfb", dp="state", gad="1/2/1") |
| 164 | LNK_03 = dict(fb="timerfb", dp="delay", gad="1/3/1") |
| 165 | |
| 166 | DESC = "Timer device" |
| 167 | |
| 168 | |
| 169 | DEVICE = Timer |
| 170 | }}} |
| 171 | |
| 172 | That's it! As you can see, concepts used here are simple... This Functional Block can now be used from any other real device of your installation, through Groups Addresses {{{1/1/1}}}, {{{1/2/1}}} and {{{1/3/1}}}. All you have to do, is to weave the Group Object-s of your real devices to these Group Address-es, using the real '''ETS''' application. Sure, you maye have to change the Group Address-es to match your installation. |
| 173 | |
| 174 | Ok, now, lets check our device, using the {{{admin.py}}} script: |
| 175 | |
| 176 | {{{ |
| 177 | $ ./admin.py checkdevice |
| 178 | MainThread::AdminUtility._checkRunDevice(): logger level is 'info' |
| 179 | MainThread::AdminUtility._checkRunDevice(): config path is './timer' |
| 180 | MainThread::AdminUtility._checkRunDevice(): device name is 'timer' |
| 181 | MainThread::AdminUtility._checkRunDevice(): device individual address is '1.1.1' |
| 182 | no error found |
| 183 | $ |
| 184 | }}} |
| 185 | |
| 186 | Looks good. So, we can now really run the device: |
| 187 | |
| 188 | {{{ |
| 189 | $ ./admin.py rundevice |
| 190 | MainThread::AdminUtility._checkRunDevice(): logger level is 'info' |
| 191 | MainThread::AdminUtility._checkRunDevice(): config path is './timer' |
| 192 | MainThread::AdminUtility._checkRunDevice(): device name is 'timer' |
| 193 | MainThread::AdminUtility._checkRunDevice(): device individual address is '1.1.1' |
| 194 | MainThread::AdminUtility._runDevice(): detach is 'False' |
| 195 | MainThread::Scheduler started |
| 196 | MainThread::Stack running |
| 197 | }}} |
| 198 | |
| 199 | Here, the call is blocking, on the Stack mainloop, waiting for incoming telegrams from the bus. Here is what you can see if you switch on the light: |
| 200 | |
| 201 | {{{ |
| 202 | LinkLayer::timerfb: start timer for 10s |
| 203 | |
| 204 | Thread-15::timerfb: timer expired; switch off |
| 205 | }}} |
| 206 | |
| 207 | And the light should have been switched off after 10s. |
| 208 | |
| 209 | To exist from the device, just use '''Ctrl-C'''. |
| 210 | |
| 211 | '''Important note: to avoid internal loops, a device drops all telegrams sent by itself. So, if you want 2 virtual devices to communicate, you must ensure that they don't use the same source address, like in a real installation. Keep this in mind ; if you don't see telegrams you are expecting, the problem may be there.''' |
| 212 | |
| 213 | So, lets deep inside this example to explain how things work. First, we import some python objects: |
| 214 | |
| 215 | {{{ |
| 216 | #!python |
| 217 | from pknyx.api import Device, FunctionalBlock |
| 218 | from pknyx.api import logger, schedule, notify |
| 219 | }}} |
| 220 | |
| 221 | '''{{{Device}}}''' and '''{{{FunctionalBlock}}}''' are classes; '''{{{logger}}}''', '''{{{schedule}}}''', '''{{{notify}}}''' are instances. |
| 222 | |
| 223 | The {{{logger}}} object is a global logger service, which allows you to output all informations you want, at different levels. '''pKNyX''' makes a big usage of that logger. By default, the template set the logger level to '''{{{info}}}''', but you can set it to other levels. See the documentation. |
| 224 | |
| 225 | {{{schedule}}} is a helper implementating a powerfull scheduler, based on [http://pythonhosted.org/APScheduler APScheduler] (see below). |
| 226 | {{{notify}}} works the same way {{{schedule}}} does, but provides bus notifications rather than time notifications (see below). |
| 227 | |
| 228 | Now, it is time to write the central part of our device: a custom Functional Block; this is done by subclassing the '''!FunctionBlock''' base class, and adding a few attributes/methods: |
| 229 | |
| 230 | {{{ |
| 231 | #!python |
| 232 | class TimerFB(FunctionalBlock): |
| 233 | DP_01 = dict(name="cmd", access="output", dptId="1.001", default="Off") |
| 234 | DP_02 = dict(name="state", access="input", dptId="1.001", default="Off") |
| 235 | DP_03 = dict(name="delay", access="input", dptId="7.005", default=10) |
| 236 | |
| 237 | GO_01 = dict(dp="cmd", flags="CWT", priority="low") |
| 238 | GO_02 = dict(dp="state", flags="CWUI", priority="low") |
| 239 | GO_03 = dict(dp="delay", flags="CWU", priority="low") |
| 240 | |
| 241 | DESC = "Timer FB" |
| 242 | |
| 243 | def _init(self): |
| 244 | self._timer = 0 |
| 245 | }}} |
| 246 | |
| 247 | The '''{{{DP_}}}''' class attributes are the Datapoint-s of our Functional Block. |
| 248 | |
| 249 | The '''{{{GO_}}}''' class attributes are the Group Object-s mapping the Datapoint-s to the bus through multicast service (Group Address). |
| 250 | |
| 251 | 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 it starts with {{{DP_}}} for Datapoints, and {{{GO_}}} for Group Objects. |
| 252 | |
| 253 | 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 tutorial. |
| 254 | |
| 255 | The '''{{{_init()}}}''' method is called at the end of the Functional Block instanciation (creation); it can be used to init some additionnal global vars, or make initial tasks. Here, we just create the '''{{{_timer}}}''' var, and set it to 0. |
| 256 | |
| 257 | Ok, it's time to dig in the active part of our functional block: |
| 258 | |
| 259 | {{{ |
| 260 | #!python |
| 261 | @notify.datapoint(dp="state", condition="change") |
| 262 | def stateChanged(self, event): |
112 | | |
113 | | |
114 | | def main(): |
115 | | |
116 | | # Register functional block |
117 | | ets.register(TimerFB, name="timer", desc="") |
118 | | |
119 | | # Weave datapoints |
120 | | ets.weave(fb="timer", dp="cmd", gad="1/1/1") |
121 | | ets.weave(fb="timer", dp="state", gad="1/2/1") |
122 | | ets.weave(fb="timer", dp="delay", gad="1/3/1") |
123 | | |
124 | | print |
125 | | ets.printGroat("gad") |
126 | | print |
127 | | ets.printGroat("go") |
128 | | print |
129 | | schedule.printJobs() |
130 | | print |
131 | | |
132 | | # Run the stack main loop (blocking call) |
133 | | stack.mainLoop() |
134 | | |
135 | | |
136 | | if __name__ == "__main__": |
137 | | try: |
138 | | main() |
139 | | except: |
140 | | logger.exception("3_main") |
141 | | }}} |
142 | | |
143 | | That's it! As you can see, concepts used here are simple... This Functional Block can now be used from any other real device of your installation, through Groups Addresses {{{1/1/1}}}, {{{1/2/1}}} and {{{1/3/1}}}. 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. Sure, you maye have to change the GAD to match your installation. |
144 | | |
145 | | Ok, now, lets start this device (invoke the python interpreter as for any other python script): |
146 | | |
147 | | {{{ |
148 | | MainThread::Logger.__init__(): start new logger 'timer-1.2.3' |
149 | | MainThread::Scheduler started |
150 | | |
151 | | GAD Datapoint Functional block DPTID Flags Priority |
152 | | ----------------------------------------------------------------------------------------------------------------------------- |
153 | | 1 Lights |
154 | | ├── 1 Lights commands |
155 | | │ ├── 1 Light 'test' (cmd) cmd timer 1.001 CWT low |
156 | | ├── 2 Lights States |
157 | | │ ├── 1 Ligh 'test' (state) state timer 1.001 CWUI low |
158 | | ├── 3 Lights delays |
159 | | │ ├── 1 Light 'test' (delay) delay timer 7.005 CWU low |
160 | | |
161 | | Functional block Datapoint DPTID GAD Flags Priority |
162 | | ------------------------------------------------------------------------------------------------------------------------ |
163 | | timer delay 7.005 1/3/1 CWU low |
164 | | timer state 1.001 1/2/1 CWUI low |
165 | | timer cmd 1.001 1/1/1 CWT low |
166 | | |
167 | | Jobstore default: |
168 | | TimerFB.updateTimer (trigger: interval[0:00:01], next run at: 2013-08-14 20:21:46.943959) |
169 | | |
170 | | MainThread::Stack running |
171 | | }}} |
172 | | |
173 | | Here, your device waits for bus events! |
174 | | |
175 | | Lets deep inside this example. First, we import some python objects: |
176 | | |
177 | | {{{ |
178 | | #!python |
179 | | from pknyx.api import Logger |
180 | | from pknyx.api import FunctionalBlock, Stack, ETS |
181 | | from pknyx.api import Scheduler, Notifier |
182 | | }}} |
183 | | |
184 | | These objects are all classes. |
185 | | |
186 | | Let's define a few constants: |
187 | | |
188 | | {{{ |
189 | | #!python |
190 | | NAME = "timer" |
191 | | IND_ADDR = "1.2.3" |
192 | | LEVEL = "info" |
193 | | |
194 | | # ETS group address map |
195 | | GAD_MAP = {"1": dict(name="lights", desc="Lights"), |
196 | | "1/1": dict(name="lights_cmds", desc="Lights commands"), |
197 | | "1/1/1": dict(name="light_test_cmd", desc="Light 'test' (cmd)"), |
198 | | "1/2": dict(name="states", desc="States"), |
199 | | "1/2/1": dict(name="light_test_state", desc="Ligh 'test' (state)"), |
200 | | "1/3": dict(name="lights_delays", desc="Lights delays"), |
201 | | "1/3/1": dict(name="light_test_delay", desc="Light 'test' (delay)"), |
202 | | } |
203 | | }}} |
204 | | |
205 | | We then instanciante some high level objects and helpers: |
206 | | |
207 | | {{{ |
208 | | #!python |
209 | | logger = Logger("%s-%s" % (NAME, IND_ADDR)) |
210 | | logger.setLevel(LEVEL) |
211 | | |
212 | | stack = Stack(individualAddress=IND_ADDR) |
213 | | ets = ETS(stack=stack, gadMap=GAD_MAP)) |
214 | | |
215 | | schedule = Scheduler() |
216 | | notify = Notifier() |
217 | | }}} |
218 | | |
219 | | The {{{Logger}}} object is a global logger service, which allows you to output all informations you want, at different levels. '''pKNyX''' makes a big usage of that logger. Here, we set the {{{info}}} level to avoid too much outputs (default level is {{{trace}}}, which logs everything). |
220 | | |
221 | | 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. The address {{{0/0/0}}} is used by default. |
222 | | |
223 | | '''Important: to avoid internal loops, a device drops all telegrams sent by itself. So, if you want 2 virtual devices to communicate, you must ensure that they don't use the same source address, like in a real installation. Keep this in mind ; if you don't see telegrams you are expecting, the problem may be there.''' |
224 | | |
225 | | The {{{ETS}}} object is a tool which works more or less like the real ETS application (see below). It needs one mandatory parameter: the {{{Stack}}} object. The {{{gadMap}}} param is optionnal, and is a simple dictionnary which maps group addresses to names/description. '''pKNyX''' does not need this to work, but it can be easier to debug things. Note that it is not really used now, but it will be in the future. |
226 | | |
227 | | The {{{Scheduler}}} is a helper implementating a powerfull scheduler, based on [http://pythonhosted.org/APScheduler APScheduler] (see below). |
228 | | The {{{Notifier}}} works the same way the {{{Scheduler}}} does, but provides bus notifications rather than time notifications (see below). |
229 | | |
230 | | Now, it is time to write the central part of our device: a custom Functional Block; this is done by subclassing the '''!FunctionBlock''' base class, and adding a few attributes/methods: |
231 | | |
232 | | {{{ |
233 | | #!python |
234 | | class TimerFB(FunctionalBlock): |
235 | | """ Timer functional block |
236 | | """ |
237 | | |
238 | | # Datapoints definition |
239 | | DP_01 = dict(name="cmd", access="output", dptId="1.001", default="Off") |
240 | | DP_02 = dict(name="state", access="input", dptId="1.001", default="Off") |
241 | | DP_03 = dict(name="delay", access="input", dptId="7.005", default=10) |
242 | | |
243 | | # Group Objects definition |
244 | | GO_01 = dict(dp="cmd", flags="CWT", priority="low") |
245 | | GO_02 = dict(dp="state", flags="CWUI", priority="low") |
246 | | GO_03 = dict(dp="delay", flags="CWU", priority="low") |
247 | | |
248 | | DESC = "Timer" |
249 | | |
250 | | def _init(self): |
251 | | """ Additionnal init of the timer |
252 | | """ |
253 | | self._timer = 0 |
254 | | }}} |
255 | | |
256 | | The {{{DP_xx}}} class attributes are the '''Datapoints''' of our Functional Block. Datapoints are the internal vars of the functional bloc which can be exposed to the bus. |
257 | | |
258 | | The {{{GO_xx}}} class attributes are the '''Group Objects''' mapping the Datapoints to the bus through multicast service (Group Address). |
259 | | |
260 | | 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 it starts with {{{DP_}}} for Datapoints, and {{{GO_}}} for Group Objects. |
261 | | |
262 | | 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 tutorial. |
263 | | |
264 | | The {{{_init()}}} method is called when the functional block is instanciated; it can be used to init some additionnal global vars, or make initial tasks. Here, we just create the {{{_timer}}} var, and set it to 0. |
265 | | |
266 | | Ok, it's time to dig in the active part of our functional block. Let's start with the {{{stateChanged()}}} method: |
267 | | |
268 | | {{{ |
269 | | #!python |
270 | | @notify.datapoint(dp="state", condition="change") |
271 | | def stateChanged(self, event): |
272 | | """ Method called when the 'state' datapoint changes |
273 | | """ |
274 | | logger.debug("TimerFB.stateChanged(): event=%s" % repr(event)) |
275 | | |
276 | | if event['newValue'] == "On": |
277 | | delay = self.dp["delay"].value |
278 | | Logger().info("%s: start timer for %ds" % (self._name, delay)) |
279 | | self._timer = delay |
280 | | elif event['newValue'] == "Off": |
281 | | if self._timer: |
282 | | Logger().info("%s: switched off detected; cancel timer" % self._name) |
283 | | self._timer = 0 |
284 | | }}} |
285 | | |
286 | | The role of this method is to start/stop the timer depending of the state. |
287 | | |
288 | | This method as a '''decorator'''. '''pKNyX''' does not use decorators as they usually are made for; here, the decorator registers the decorated method in the {{{Notifier}}}, which will call this method as soon as the ''state'' Datapoint changes because of a bus activity. |
289 | | |
290 | | Note how we retreive Datapoint value: our functional block has a {{{dp}}} dictionnary, which contains all our datapoints; the keys are the names we used to describe them in the {{{DP_xxx}}} dictionnary. To get the value of the Datapoint, just use the {{{.value}}} property. |
291 | | |
292 | | Lets have a look at the timer treatement: |
293 | | |
294 | | {{{ |
295 | | #!python |
296 | | @schedule.every(seconds=1) |
297 | | def updateTimer(self): |
298 | | """ Method called every second. |
299 | | """ |
300 | | if self._timer: |
301 | | self._timer -= 1 |
302 | | if not self._timer: |
303 | | logger.info("%s: timer expired; switch off" % self._name) |
304 | | self.dp["cmd"].value = "Off" |
305 | | }}} |
306 | | |
307 | | Note how this method is periodically called, using the {{{schedule.every()}}} method as python decorator, as for the {{{Notifier}}}. This decorator will automatically register our method and call it every second. |
308 | | |
309 | | There, we just decrement the timer (if not null), and check if the delay expired. If it is the case, we change the ''cmd'' Datapoint value by assigning a new value to the {{{.value}}} property. Doing this, '''pKNyX''' will automatically notify the bus of this change, according to the flags of the corresponding {{{GO_xxx}}} object! If things do not work as expected, check that you didn't omit the {{{.value}}} property; if it is the caase, you will simply overwrite the Datapoint itself with the value you want to assign to it! |
310 | | |
311 | | The last method just manages the ''delay'' Datapoint: |
312 | | |
313 | | {{{ |
314 | | #!python |
315 | | @notify.datapoint(dp="delay", condition="change") |
316 | | def delayChanged(self, event): |
317 | | """ Method called when the 'delay' datapoint changes |
318 | | """ |
319 | | logger.debug("TimerFB.delayChanged(): event=%s" % repr(event)) |
320 | | |
321 | | # If the timer is running, we reset it to the new delay |
322 | | if self._timer: |
323 | | delay = self.dp["delay"].value |
324 | | Logger().info("%s: delay changed; restart timer" % self._name) |
325 | | self._timer = delay |
330 | | Ok, we need to write a few more things to get our device working. First, we need to register the functional bloc: |
331 | | |
332 | | {{{ |
333 | | #!python |
334 | | def main(): |
335 | | |
336 | | # Register functional block |
337 | | ets.register(TimerFB, name="timer", desc="") |
338 | | }}} |
339 | | |
340 | | and use the {{{ETS}}} object to weave (bind, link...) our Datapoints (their matching Group Objects, in fact) to our group addresses: |
341 | | |
342 | | {{{ |
343 | | #!python |
344 | | # Weave datapoints |
345 | | ets.weave(fb="timer", dp="cmd", gad="1/1/1") |
346 | | ets.weave(fb="timer", dp="state", gad="1/2/1") |
347 | | ets.weave(fb="timer", dp="delay", gad="1/3/1") |
348 | | }}} |
349 | | |
350 | | We can print a summary of our mapping: |
351 | | |
352 | | {{{ |
353 | | #!python |
354 | | print |
355 | | ets.printGroat("gad") |
356 | | print |
357 | | ets.printGroat("go") |
358 | | print |
359 | | schedule.printJobs() |
360 | | print |
361 | | }}} |
362 | | |
363 | | And finally, launch the framework main loop: |
364 | | |
365 | | {{{ |
366 | | #!python |
367 | | # Run the stack main loop (blocking call) |
368 | | stack.mainLoop() |
369 | | }}} |
370 | | |
371 | | 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. |
372 | | |
373 | | That's it for now. This is the first working dev. release; their are many additional things to do, and final implementation may change, according to feedback/suggestions I will get. But the core is there. Again, the goal of the framework is to provide very high level tools to build complete and powerfull applications and KNX extensions. Unlink '''linknx''', you have a little more to do that simply write a configuration file. But you will find that it is not that complicated ('''pKNyX''' handles all nasty stuff for you), and may even simplify things for complex tasks (linknx rules are not that simple). |
| 310 | Now we need to implement our Device: |
| 311 | |
| 312 | {{{ |
| 313 | #!python |
| 314 | class Timer(Device): |
| 315 | FB_01 = dict(cls=TimerFB, name="timerfb", desc="timer fb") |
| 316 | |
| 317 | LNK_01 = dict(fb="timerfb", dp="cmd", gad="1/1/1") |
| 318 | LNK_02 = dict(fb="timerfb", dp="state", gad="1/2/1") |
| 319 | LNK_03 = dict(fb="timerfb", dp="delay", gad="1/3/1") |
| 320 | |
| 321 | DESC = "Timer device" |
| 322 | }}} |
| 323 | |
| 324 | As you see, it must inherits the {{{Device}}} class. Like for the Function Block, things are defined through class attributes; '''pKNyX''', like python, tries to limit the number of paradigms. |
| 325 | |
| 326 | We first need to tell which Function Block(-s) our device will use. This is done by creating a dict, which name must start with '''{{{FB_}}}'''. Here, we only have one Functional Block. |
| 327 | |
| 328 | Then, we weave our Datapoint-s (our Group Object-s, in fact) to our group addresses. Again, a simple dict is used, with names satring with '''{{{LNK_}}}'''. |
| 329 | |
| 330 | The last thing we need to write is to tell '''pKNyX''' what is our Device implementation class: |
| 331 | |
| 332 | {{{ |
| 333 | #!python |
| 334 | DEVICE = Timer |
| 335 | }}} |
| 336 | |
| 337 | That's it for now with this tutorial. This is the first working dev. release; their are many additional things to do, and final implementation may change, according to feedback/suggestions I will get. But the core is there. Again, the goal of the framework is to provide very high level tools to build complete and powerfull applications and KNX extensions. Unlink '''linknx''', you have a little more to do that simply write a configuration file. But you will find that it is not that complicated ('''pKNyX''' handles all nasty stuff for you), and may even simplify things for complex tasks (linknx rules are not that simple in this case). |