Building your first module
This document will walk you through the entire process of building a custom module from the ground up. By the time you are done, your custom module will be ready to and activated on your local installation.
Time to complete this task: 15-30 minutes. Time varies depending on what extra features you add.
Lets dive right in
Modules are written in python 3 and are easy to write with the help of various helper libraries and utilities bundled with Yombo. This example creates a simple module that turns on/off a couple of items based on time of day. We will call this module mynightmode. You can name yours whatever you want.
Lets get started.
From the yombo home directory (usually found in /opt/yombo-gateway/), we will start off by duplicating the yombo-gateway/yombo/modules/empty folder. We will call ours "mynightmode", note that it should be lowercase and no spaces. The folder path will be: yombo-gateway/yombo/modules/mynightmode (or whatever you have named your module). Inside the new directory, rename "empty.py" file to match the name of your new module name, in this example we will call it "mynightmode.py". We now need to tell python some information about our new module, so edit the "__init__.py" file to be:
1 from mynightmode import MyNightMode
Currently the module is internally labeled as "empty". We will need to rename the class to "MyNightMode". Here is what the top of my module looks like:
1 """ 2 This module turns on the porch light at night, and off in the day. It also 3 turns my water fountain on at 4pm and off at 10pm. 4 5 :copyright: 2012-2018 Yombo 6 :license: GPL 7 """ 8 import time 9 10 from twisted.internet import reactor 11 12 from yombo.core.module import YomboModule 13 from yombo.core.log import getLogger 14 15 logger = getLogger("modules.mynightmode") 16 17 class MyNightMode(YomboModule): 18 """ 19 This is an empty module used to bootstrap your own module. Simply copy/paste this 20 directory to a new directy. Be sure to edit the __init__.py to match the new name. 21 """
We're off to a great start. Lets break this down:
- Anything between the triple quotes (""") is considered a multi-line comment. This is helpful for commenting your code to explain what is happening.
- We import a few python libraries from the Twisted framework as well as the Yombo framework.
- We setup a logging system so we can write information to our log.
- Finally, we create a new class called "MyNightMode" which extends an existing Yombo class.
Before moving on
The Yombo framework adds many items to your class when it starts up, mostly, it adds pointers to other system resources and information about the module itself. It also provides a system for starting and stopping the system in phases to allow all the libraries and modules to systematically startup and shutdown. While Yombo is starting up, it brings all the internal libraries online first. It then connects to Yombo servers for any updates, module listings, new devices, commands, etc. After the gateway framework is online, it starts to import and load all the modules. It starts by calling all the "_init_()" methods for all the modules. Note: Yombo uses single underscore (_) to denote it's a Yombo system called method, not to be confused with Python built-in functions having two underscores (__). Never define __init__(two underscores), that is reserved for the Yombo framework to get basic items of your module setup.
- import - The first phase is the bulk import. This loads the modules into memory and setups basic information. Behind the scenes, it sets up basic attributes for the module. After all modules are imported, then we go into the init phase.
- _init_() - This is the perfect time to define various class attributes (AKA: variables you'll use in your module). One thing to note: At this point, no processing of automation commands can take place. Your module should not make automation requests and should not expect to receive any either. At this point, you can use any framework APIs, and module APIs if they were designed to be used during this phase.
- _load_() - Now is when any files, remote connections, and opening devices (usb/serial/network), should be done. Keep in mind, that other modules may not have completed their _load_() functions yet, so you cannot send messages - however, once your module's load() function completes, it should be able to handle incoming automation requests.
- start - The _start_() function allows you to start sending any automation requests and interacting with any other modules. You can now conntrol devices, and perform any automation logic. The logic shouldn't live in start(), but it's a good time to start timers, turn on processing etc.
Let's continue with our module building. During the _init_() phase, we'll want to setup any modules variables. Lets get to it:
1 def _init_(self): 2 """ 3 Nothing to do here, the system took care of everything for us. We will pass for now. 4 Alternatively, just don't define this function. 5 """ 6 # lets check if we have a device called "porch light" 7 pass
You can see we aren't doing to much here just yet. No need to, the gateway takes care of the heavy lifting.
We don't have anything to do here, but lets define this for a more complete example.
1 def _load_(self): 2 """ 3 Nothing to do here either. 4 5 Startup phase 2 of 3. 6 """ 7 pass
Doesn't really do much yet. No fun, lets move along.
Our start method will lookup the current state of "is.light" and determine if it's light or dark. We will then run our primary function "run_my_rules()" to perform the actual logic. We break out our logic into a separate function so it can be called again when the brightness outside changes as the sun rises or sets.
1 def _start_(self): 2 """ 3 Now is a perfect time to make sure all our devices are in the start 4 state that we want them in. 5 6 Startup phase 3 of 3. 7 """ 8 # in one call, we can get if it's light or dark outside and send it to our logic method. 9 10 self.run_my_rules(self._States['is.light']) 11 12 # lets check if we should turn on or off the water fountain. We do this check 13 # incase the system may have been off at 4pm (to turn on) or 10pm (to turn off). 14 if 'water fountain' in self._Devices: # if we have a water fountain, turn it on 15 if self._Times.get_time('4pm') < time.time() < self._Times.get_time('10pm'): 16 results = self._Devices['water fountain'].command('on') 17 else: 18 results = self._Devices['water fountain'].command('off') 19 # get_time returns a tuple. The first item  is in EPOCH, the second item  is a datetime
Our core logic
Now that we are started, loaded, and ready to go, lets implement our logic. We have two methods we can implement this: listen for events from the times library, or listen for events from the states library. Both methods work the same way: implement a hook to be called when something happens. We will choose the times library for now as we only care about day and night. If we cared about other things, we would implement a hook that listens to the states_set hook, or setup both.
This function will be called whenever a time event occurs, such as sunsets, sunrises, is dark, light, etc. For now, all we care about are "now.dark" and "now.light" events.
1 def _time_event_(**kwargs): 2 """ 3 Called by the Times library whenever a time event occurs. We only care about now.dark 4 and now.light events. 5 6 :param kwargs: A dictionary, we care about 'value'. 7 """ 8 event = kwargs['value'] 9 # There are many possible events, but for now we only care if it's light or dark outside. 10 if event == 'now.light': 11 self.run_my_rules('now.light') 12 elif event == 'now.dark': 13 self.run_my_rules('now.dark')
Performs the actual logic.
1 def run_my_rules(self, brightness): 2 """ 3 Called from either _start_() or _time_event_() 4 5 This runs our automation logic based on light or dark outside. 6 7 :param bightness: Either 'now.light' or 'now.dark' outside. 8 """ 9 if (brightness == 'now.light'): 10 # Turn off the porch light, if it exists 11 if 'porch light' in self._Devices: # if we have a porch light, turn it off 12 results = self._Devices['porch light'].command('off') 13 14 # Turn on the water fountain. 15 if 'water fountain' in self._Devices: # if we have a water fountain, turn it on 16 results = self._Devices['water fountain'].command('on') 17 18 # Now, turn off the water fountain after an hour. This gets a little 19 # more advanced, so if you don't understand it, for now, it's ok. 20 21 # First, how many seconds from now? 22 # 60 seconds * 60 minutes = 3600 seconds. 23 24 # !!! CAUTION !!! Delayed messages are saved between restarts. 25 # You can remove future commands for a device with this: 26 self._Devices['water fountain'].remove_delayed() 27 28 # Now, lets set a delayed command. 29 self._Devices['water fountain'].command('off', delay=3600) 30 31 else: # if it's not light, it must be dark! 32 if 'porch light' in self._Devices: # if we have a porch light, turn it off 33 results = self._Devices['porch light'].command('on') 34 self._Devices['porch light'].remove_delayed() 35 results = self._Devices['porch light'].command('off', delay=7200)
Stopping and unloading
When the gateway is shutting down, it calls the "stop()" function of all modules, followed by "unload()". These two steps allow you to cleanly close up shop.
There is nothing here for us to do here. You don't have to define this method, it's here just for completeness. A note about this phase: After "stop" has been called, you should no longer make external calls: hooks, AMQP, etc. However, you must still be able to receive and process any incoming requests (hooks). That means, you can't quite close up shop, but you can turn hang the closed sign.
1 def _stop_(self): 2 pass
Unload is the final notification that the gateway is about to unload. The module will no longer receive any requests and should not call any hooks. Now is the time close up any network connections, close files, etc. It's time to turn out the lights and lock the door. In our demo, we don't have anything to do. This is just here for completeness.
1 def _unload_(self): 2 pass