Changes between Version 7 and Version 8 of Tutorial


Ignore:
Timestamp:
Aug 20, 2013, 4:31:58 PM (11 years ago)
Author:
Frédéric
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • Tutorial

    v7 v8  
    33= Tutorial =
    44
    5 == Architecture ==
    6 
    75== Vocabulary ==
    86
    9  * Device
    10  * Functional Block
    11  * Datapoint
    12  * Group Object
    13  * Flags
    14  * ETS
    15  * Stack
    16  * Group Address
     7Before 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
     9KNX 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
     23This is the central feature of '''pKNyX'''. This is where we will implement needed logic.
     24
     25A 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
     27Usually, 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
     29Functional 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
     33As 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
     35The 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
     39create 'timer' from template...
     40done
     41}}}
     42
     43This will create a little arborescence under a dir named '''{{{timer}}}''':
     44
     45{{{
     46$ tree timer
     47timer
     48├── admin.py
     49└── timer
     50    ├── config.py
     51    ├── device.py
     52    └── __init__.py
     53
     541 directory, 4 files
     55}}}
     56
     57The 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
     59The '''{{{config.py}}}''' file contains a few pre-defined constants:
     60
     61{{{
     62#!python
     63# -*- coding: utf-8 -*-
     64
     65from pknyx.common import config
     66
     67DEVICE_NAME = "timer"
     68DEVICE_IND_ADDR = "1.1.1"
     69DEVICE_VERSION = "0.1"
     70
     71config.LOGGER_LEVEL = "info"
     72}}}
     73
     74This is where we will add new configs values for our Device, if needed.
     75
     76The 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
     82from pknyx.api import Device, FunctionalBlock
     83from pknyx.api import logger, schedule, notify
     84
     85
     86class 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
     94class 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
     102DEVICE = MyDevice
     103}}}
     104
     105As you see, it already contains a dummy Functional Block, and a dummy Device, in order to show how things work.
    17106
    18107== Timer example ==
    19108
    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 
     109Ok, 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
     111Let's modify the {{{device.py}}} according to this:
     112
     113{{{
     114#!python
     115from pknyx.api import Device, FunctionalBlock
     116from pknyx.api import logger, schedule, notify
    54117
    55118class TimerFB(FunctionalBlock):
    56     """ Timer functional block
    57     """
    58 
    59     # Datapoints definition
    60119    DP_01 = dict(name="cmd", access="output", dptId="1.001", default="Off")
    61120    DP_02 = dict(name="state", access="input", dptId="1.001", default="Off")
    62121    DP_03 = dict(name="delay", access="input", dptId="7.005", default=10)
    63122
    64     # Group Objects definition
    65123    GO_01 = dict(dp="cmd", flags="CWT", priority="low")
    66124    GO_02 = dict(dp="state", flags="CWUI", priority="low")
    67125    GO_03 = dict(dp="delay", flags="CWU", priority="low")
    68126
    69     DESC = "Timer"
     127    DESC = "Timer FB"
    70128
    71129    def _init(self):
    72         """ Additionnal init of the timer
    73         """
    74130        self._timer = 0
    75131
    76132    @notify.datapoint(dp="state", condition="change")
    77133    def stateChanged(self, event):
    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
     159class 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
     169DEVICE = Timer
     170}}}
     171
     172That'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
     174Ok, now, lets check our device, using the {{{admin.py}}} script:
     175
     176{{{
     177$ ./admin.py checkdevice
     178MainThread::AdminUtility._checkRunDevice(): logger level is 'info'
     179MainThread::AdminUtility._checkRunDevice(): config path is './timer'
     180MainThread::AdminUtility._checkRunDevice(): device name is 'timer'
     181MainThread::AdminUtility._checkRunDevice(): device individual address is '1.1.1'
     182no error found
     183$
     184}}}
     185
     186Looks good. So, we can now really run the device:
     187
     188{{{
     189$ ./admin.py rundevice
     190MainThread::AdminUtility._checkRunDevice(): logger level is 'info'
     191MainThread::AdminUtility._checkRunDevice(): config path is './timer'
     192MainThread::AdminUtility._checkRunDevice(): device name is 'timer'
     193MainThread::AdminUtility._checkRunDevice(): device individual address is '1.1.1'
     194MainThread::AdminUtility._runDevice(): detach is 'False'
     195MainThread::Scheduler started
     196MainThread::Stack running
     197}}}
     198
     199Here, 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{{{
     202LinkLayer::timerfb: start timer for 10s
     203
     204Thread-15::timerfb: timer expired; switch off
     205}}}
     206
     207And the light should have been switched off after 10s.
     208
     209To 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
     213So, lets deep inside this example to explain how things work. First, we import some python objects:
     214
     215{{{
     216#!python
     217from pknyx.api import Device, FunctionalBlock
     218from pknyx.api import logger, schedule, notify
     219}}}
     220
     221'''{{{Device}}}''' and '''{{{FunctionalBlock}}}''' are classes; '''{{{logger}}}''', '''{{{schedule}}}''', '''{{{notify}}}''' are instances.
     222
     223The {{{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
     228Now, 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
     232class 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
     247The '''{{{DP_}}}''' class attributes are the Datapoint-s of our Functional Block.
     248
     249The '''{{{GO_}}}''' class attributes are the Group Object-s mapping the Datapoint-s to the bus through multicast service (Group Address).
     250
     251They 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
     253There 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
     255The '''{{{_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
     257Ok, 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):
    82263        if event['newValue'] == "On":
    83264            delay = self.dp["delay"].value
     
    88269                Logger().info("%s: switched off detected; cancel timer" % self._name)
    89270                self._timer = 0
    90 
     271}}}
     272
     273The role of the '''{{{stateChanged()}}}''' method is to start/stop the timer depending of the state.
     274
     275This method as a decorator, '''{{{@notify.datapoint()}}}'''. '''pKNyX''' does not use decorators as they usually are made for; here, the decorator registers {{{stateChanged()}}} in {{{notify}}}, which will call this method as soon as the '''{{{state}}}''' Datapoint changes because of a bus activity.
     276
     277Note how we retreive Datapoint value: our Functional Block has a '''{{{dp}}}''' dictionnary, which contains all our Datapoint-s; the keys are the names we used to describe them in the {{{DP_}}} dictionnary. To get/set the value of the Datapoint, just use the '''{{{.value}}}''' property.
     278
     279Let's now have a look at the timer treatement:
     280
     281{{{
     282#!python
    91283    @schedule.every(seconds=1)
    92284    def updateTimer(self):
    93         """ Method called every second.
    94         """
    95285        if self._timer:
    96286            self._timer -= 1
     
    98288                logger.info("%s: timer expired; switch off" % self._name)
    99289                self.dp["cmd"].value = "Off"
    100 
     290}}}
     291
     292Note how this method is periodically called, using the '''{{{schedule.every()}}}''' decorator, as for {{{notify}}}. This decorator will automatically register the method, and {{{schedule}}} will call it every second.
     293
     294There, 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_}}} object! If things do not work as expected, check that you didn't omit the {{{.value}}} property; if it is the case, you just overwrited the Datapoint itself with the value you want to assign to it!
     295
     296The last method manages the '''{{{delay}}}''' Datapoint:
     297
     298{{{
     299#!python
    101300    @notify.datapoint(dp="delay", condition="change")
    102301    def delayChanged(self, event):
    103         """ Method called when the 'delay' datapoint changes
    104         """
    105         logger.debug("TimerFB.delayChanged(): event=%s" % repr(event))
    106 
    107         # If the timer is running, we reset it to the new delay
    108302        if self._timer:
    109303            delay = self.dp["delay"].value
    110304            Logger().info("%s: delay changed; restart timer" % self._name)
    111305            self._timer = delay
    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
    326306}}}
    327307
    328308I think you got the point ;o)
    329309
    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).
     310Now we need to implement our Device:
     311
     312{{{
     313#!python
     314class 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
     324As 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
     326We 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
     328Then, 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
     330The last thing we need to write is to tell '''pKNyX''' what is our Device implementation class:
     331
     332{{{
     333#!python
     334DEVICE = Timer
     335}}}
     336
     337That'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).