Changes between Version 2 and Version 3 of Tutorial


Ignore:
Timestamp:
Aug 15, 2013, 10:38:20 AM (11 years ago)
Author:
Frédéric
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • Tutorial

    v2 v3  
    11[[PageOutline(2-5, Table of Contents, floated)]]
    2 
    3 '''Draft'''
    42
    53= Tutorial =
     
    2523NAME = "timer"
    2624IND_ADDR = "1.2.3"
     25LEVEL = "info"
    2726
    2827# ETS group address map
    2928GAD_MAP = {"1": dict(name="lights", desc="Lights"),
    30            "1/1": dict(name="cmds", desc="Commands"),
     29           "1/1": dict(name="lights_cmds", desc="Lights commands"),
    3130           "1/1/1": dict(name="light_test_cmd", desc="Light 'test' (cmd)"),
    32            "1/2": dict(name="states", desc="States"),
     31           "1/2": dict(name="states", desc="Lights states"),
    3332           "1/2/1": dict(name="light_test_state", desc="Ligh 'test' (state)"),
    34            "1/3": dict(name="delay", desc="Delays"),
     33           "1/3": dict(name="lights_delays", desc="Lights delays"),
    3534           "1/3/1": dict(name="light_test_delay", desc="Light 'test' (delay)"),
    3635          }
     
    3837
    3938logger = Logger("%s-%s" % (NAME, IND_ADDR))
     39logger.setLevel(LEVEL)
    4040
    4141stack = Stack(individualAddress=IND_ADDR)
     
    6565        """
    6666        self._timer = 0
    67 
    68     @schedule.every(seconds=1)
    69     def updateTimer(self):
    70         """ Method called every second.
    71         """
    72         #logger.trace("TimerFB.updateTimer()")
    73 
    74         if self._timer:
    75             self._timer -= 1
    76             if not self._timer:
    77                 logger.info("%s: timer expired; switch off" % self._name)
    78                 self.dp["cmd"].value = "Off"
    7967
    8068    @notify.datapoint(dp="state", condition="change")
     
    9381                self._timer = 0
    9482
     83    @schedule.every(seconds=1)
     84    def updateTimer(self):
     85        """ Method called every second.
     86        """
     87        if self._timer:
     88            self._timer -= 1
     89            if not self._timer:
     90                logger.info("%s: timer expired; switch off" % self._name)
     91                self.dp["cmd"].value = "Off"
     92
    9593    @notify.datapoint(dp="delay", condition="change")
    9694    def delayChanged(self, event):
     
    135133}}}
    136134
    137 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/3/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 can change the GAD to match your installation.
     135That'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/3/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.
    138136
    139137Ok, now, lets start this device (invoke the python interpreter as for any other python script):
    140138
    141139{{{
    142 
    143 }}}
    144 
    145 
    146 Lets have a closer look to this example. First, we import some python objects:
     140MainThread::Logger_.__init__(): start new logger 'timer-1.2.3'
     141MainThread::Scheduler started
     142
     143GAD                                 Datapoint                 Functional block               DPTID      Flags      Priority 
     144-----------------------------------------------------------------------------------------------------------------------------
     145 1 Lights                           
     146 ├──  1 Lights commands           
     147 │    ├──   1 Light 'test' (cmd)    cmd                       timer                          1.001      CWT        low       
     148 ├──  2 Lights States                     
     149 │    ├──   1 Ligh 'test' (state)   state                     timer                          1.001      CWUI       low       
     150 ├──  3 Lights delays             
     151 │    ├──   1 Light 'test' (delay)  delay                     timer                          7.005      CWU        low       
     152
     153Functional block               Datapoint                 DPTID      GAD                            Flags      Priority 
     154------------------------------------------------------------------------------------------------------------------------
     155timer                          delay                     7.005      1/3/1                          CWU        low       
     156timer                          state                     1.001      1/2/1                          CWUI       low       
     157timer                          cmd                       1.001      1/1/1                          CWT        low       
     158
     159Jobstore default:
     160    TimerFB.updateTimer (trigger: interval[0:00:01], next run at: 2013-08-14 20:21:46.943959)
     161
     162MainThread::Stack running
     163}}}
     164
     165Here, your device waits for bus events!
     166
     167Lets deep inside this example. First, we import some python objects:
    147168
    148169{{{
     
    155176These objects are all classes.
    156177
     178Let's define a few constants:
     179
     180{{{
     181#!python
     182NAME = "timer"
     183IND_ADDR = "1.2.3"
     184LEVEL = "info"
     185
     186# ETS group address map
     187GAD_MAP = {"1": dict(name="lights", desc="Lights"),
     188           "1/1": dict(name="lights_cmds", desc="Lights commands"),
     189           "1/1/1": dict(name="light_test_cmd", desc="Light 'test' (cmd)"),
     190           "1/2": dict(name="states", desc="States"),
     191           "1/2/1": dict(name="light_test_state", desc="Ligh 'test' (state)"),
     192           "1/3": dict(name="lights_delays", desc="Lights delays"),
     193           "1/3/1": dict(name="light_test_delay", desc="Light 'test' (delay)"),
     194          }
     195}}}
     196
    157197We then instanciante some high level objects and helpers:
    158198
     
    160200#!python
    161201logger = Logger("%s-%s" % (NAME, IND_ADDR))
     202logger.setLevel(LEVEL)
    162203
    163204stack = Stack(individualAddress=IND_ADDR)
     
    168209}}}
    169210
    170 The {{{Logger}}} object is a global logger service, which allow you to output all informations you want, at different levels. '''pKNyX''' make a big usage of that logger.
     211The {{{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).
    171212
    172213The {{{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.
    173214
    174 '''Important: to avoid internal loops, a device drops all telegrams sent by itself. So, if you want 2 virtal devices to communicate, you must ensure that they don't use the same source address.'''
    175 
    176 The {{{ETS}}} object is a tool which works more or less like the real ETS application (see below).
    177 
    178 The {{{Scheduler}}} is a helper implementating a powerfull scheduler (see below).
    179 The {{{Notifier}}} works the same way the {{{Scheduler}}} does, but does not provide the same features (see below).
    180 
    181 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:
     215'''Important: to avoid internal loops, a device drops all telegrams sent by itself. So, if you want 2 virtal 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.'''
     216
     217The {{{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.
     218
     219The {{{Scheduler}}} is a helper implementating a powerfull scheduler, based on [http://pythonhosted.org/APScheduler APScheduler] (see below).
     220The {{{Notifier}}} works the same way the {{{Scheduler}}} does, but provides bus notifications rather than time notifications (see below).
     221
     222Now, 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:
    182223
    183224{{{
     
    204245}}}
    205246
    206 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 through multicast service (Group Address). 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.
     247The {{{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.
     248
     249The {{{GO_xx}}} class attributes are the '''Group Objects''' mapping the Datapoints 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.
    207252
    208253There 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.
    209254
    210 The {{{_init()}}} method is called when the functional block is instanciated; it can be used to init some needed global vars, or make initial tasks. Here, we just create the {{{_timer}}} var, and set it to 0.
     255The {{{_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.
    211256
    212257Ok, it's time to dig in the active part of our functional block. Let's start with the {{{stateChanged()}}} method:
     
    230275}}}
    231276
    232 This method as a '''decorator'''. If you don't know what is a decorator, have a look at this [excellent documentation.
    233 
    234 
    235 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.
    236 
    237 In this method, we get the temperature and humidity values (not explained here), and give these values to the respective Datapoints.
    238 
    239 Then, we register our new Funtional Block (this will automatically instanciate it - and do other things):
    240 
    241 {{{
    242 #!python
    243 ets.register(WeatherTemperatureBlock, name="weather_temperature", desc="A simple weather block example")
    244 }}}
    245 
    246 and use the {{{ETS}}} object to weave (bind, link...) our Datapoints (their matching Group Objects, in fact) to Group Addresses:
    247 
    248 {{{
    249 #!python
    250 ets.weave(fb="weatherTempBlock", dp="temperature", gad="1/1/1")
    251 ets.weave(fb="weatherTempBlock", dp="humidity", gad="1/1/2")
    252 }}}
    253 
    254 And finally, we launch the framework main loop:
    255 
    256 {{{
    257 #!python
    258 stack.mainLoop()
    259 }}}
    260 
    261 (this call is blocking).
    262 
    263 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?
    264 
    265 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.
     277The role of this method is to start/stop the timer depending of the state.
     278
     279This 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.
     280
     281Note 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.
     282
     283Lets have a look at the timer treatement:
     284
     285{{{
     286#!python
     287    @schedule.every(seconds=1)
     288    def updateTimer(self):
     289        """ Method called every second.
     290        """
     291        if self._timer:
     292            self._timer -= 1
     293            if not self._timer:
     294                logger.info("%s: timer expired; switch off" % self._name)
     295                self.dp["cmd"].value = "Off"
     296}}}
     297
     298Note 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.
     299
     300There, 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!
     301
     302The last method just manages the ''delay'' Datapoint:
     303
     304{{{
     305#!python
     306    @notify.datapoint(dp="delay", condition="change")
     307    def delayChanged(self, event):
     308        """ Method called when the 'delay' datapoint changes
     309        """
     310        logger.debug("TimerFB.delayChanged(): event=%s" % repr(event))
     311
     312        # If the timer is running, we reset it to the new delay
     313        if self._timer:
     314            delay = self.dp["delay"].value
     315            Logger().info("%s: delay changed; restart timer" % self._name)
     316            self._timer = delay
     317}}}
     318
     319I think you got the point ;o)
     320
     321Ok, we need to write a few more things to get our device working. First, we need to register the functional bloc:
     322
     323{{{
     324#!python
     325def main():
     326
     327    # Register functional block
     328    ets.register(TimerFB, name="timer", desc="")
     329}}}
     330
     331and use the {{{ETS}}} object to weave (bind, link...) our Datapoints (their matching Group Objects, in fact) to our group addresses:
     332
     333{{{
     334#!python
     335    # Weave datapoints
     336    ets.weave(fb="timer", dp="cmd", gad="1/1/1")
     337    ets.weave(fb="timer", dp="state", gad="1/2/1")
     338    ets.weave(fb="timer", dp="delay", gad="1/3/1")
     339}}}
     340
     341We can print a summary of our mapping:
     342
     343{{{
     344#!python
     345    print
     346    ets.printGroat("gad")
     347    print
     348    ets.printGroat("go")
     349    print
     350    schedule.printJobs()
     351    print
     352}}}
     353
     354And finally, launch the framework main loop:
     355
     356{{{
     357#!python
     358    # Run the stack main loop (blocking call)
     359    stack.mainLoop()
     360}}}
     361
     362We 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.
     363
     364That'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).