Package: raspend
raspend - a lightweight web service frameworkMotivationSince I am doing a lot of home automation stuff on my Raspberry Pi and since the third Python script for that had the same structure, I decided, strictly following theDRY principle, to create an easy to use framework to simplify my life for when I start my next project on my RPi.IntroductionIt should be mentioned thatraspend, which is the abbreviation forraspberry backend, was originally intended to be a web service framework for the Raspberry Pi. But since it's written entirely in Python and has no RPi specific code or dependencies, it can be used on any platform having a Python interpreter installed.raspendcombines two core features. The first is the data processing part, where one or more threads share the samedictionarythey read from or write to. The data contained in this shared dictionary is exposed to the outside world through an HTTP interface as a JSON string. Therefore the HTTP interface provides the/dataendpoint. The second feature is providing an easy to use interface, which lets you invoke your self-defined commands via the already mentioned HTTP interface. (seeHow to use the HTTP interfacefor more details)raspendcomes with a ready-to-use application class calledRaspendApplication. It manages your threads, your commands and the HTTP interface. Pass the port number, the HTTP interface should listen on, to the constructor ofRaspendApplication. If you don't want to expose any data or invoke any commands on your system via HTTP interface, you can putRaspendApplicationinto "offline" mode by passingNoneinstead of a port number.When all initialization stuff is done (adding commands, creating threads), then you start your application by calling therunmethod ofRaspendApplication. TheRaspendApplicationclass installs signal handlers for SIGTERM and SIGINT, so you can quit your application by pressing CTRL+C or sending one of the signals via thekillcommand of your shell.Data processing withraspendWithraspend, creating threads is really simple and straight forward. All you need to do is, derive theThreadHandlerBaseclass and implement the abstract methodsprepareandinvoke. Thepreparemethod is called before the thread loop starts, whileinvokewill be called for every iteration of the thread loop. For tasks like cyclic reading temperature measurements or checking the CPU performance or something like that, you can use thecreateWorkerThreadmethod of theRaspendApplicationclass. It takes an instance of your thread handler and a timeout value in seconds as parameters. The thread will sleep fortimeoutseconds past every iteration.If you want a thread or task to do its work in a more scheduled way, such as run once a day, then you can use thecreateScheduledWorkerThreadmethod of theRaspendApplicationclass.createScheduledWorkerThread, likecreateWorkerThread, takes an instance of your thread handler. But instead taking a timeout, you have to pass a start time, a start date, the type of repetition and a repetition factor. Start time and date can beNone. If so, the current time and date will be used. IfrepetitionTypeisNone, it defaults toScheduleRepetitionType.DAILY. The repetition factor is applied to the repetition type and defaults to 1. For example, if you need to write your temperature measurements to a file every 15 minutes starting immediately, you would chooseScheduleRepetitionType.MINUTELYwith a repetition factor set to 15 and start time and date set toNone.fromraspendimportRaspendApplication,ThreadHandlerBase,ScheduleRepetitionTypeclassReadOneWireTemperature(ThreadHandlerBase):...classPublishOneWireTemperatures(ThreadHandlerBase):...classWriteOneWireTemperaturesToFile(ThreadHandlerBase):...# Create instance of the application class passing a port number for the HTTP interface.myApp=RaspendApplication(args.port)# Create worker threads for reading every temperature sensor once a minute.myApp.createWorkerThread(ReadOneWireTemperature("basement","party_room","/sys/.../w1_slave"),60)myApp.createWorkerThread(ReadOneWireTemperature("basement","heating_room","/sys/.../w1_slave"),60)myApp.createWorkerThread(ReadOneWireTemperature("basement","fitness_room","/sys/.../w1_slave"),60)myApp.createWorkerThread(ReadOneWireTemperature("ground_floor","kitchen","/sys/.../w1_slave"),60)myApp.createWorkerThread(ReadOneWireTemperature("ground_floor","living_room","/sys/.../w1_slave"),60)# Publish temperatures to MySQL database every minute.endPointURL="http://localhost/raspend_demo/api/post_data.php"myApp.createWorkerThread(PublishOneWireTemperatures(endPointURL,username,password),60)# Write temperatures to 1wire.csv every 15 minutes.myApp.createScheduledWorkerThread(WriteOneWireTemperaturesToFile("./1wire.csv"),None,None,ScheduleRepetitionType.MINUTELY,15)# Run main loop.myApp.run()Making commands invocable withraspendAs mentioned earlier,raspend'ssecond core feature is providing an easy way to let you invoke self-defined commands via its HTTP interface. Such a command must be implemented as a method of a Python class. This method then has to be passed to theaddCommandmethod ofRaspendApplication. You now can call this method with a simple HTTP GET or POST request.fromraspendimportRaspendApplicationclassDoorBell():def__init__(self,*args,**kwargs):self.doorBellState="on"defswitchDoorBell(self,onoff):iftype(onoff)==str:self.doorBellState="on"ifonoff=="on"else"off"eliftype(onoff)==int:self.doorBellState="on"ifonoff>=1else"off"else:raiseTypeError("State must be 'int' or 'string'.")returnself.doorBellStatedefgetCurrentState(self):returnself.doorBellState# Create instance of the application class passing a port number for the HTTP interface.myApp=RaspendApplication(args.port)# Create instance of the class whos methods will be made invocable via HTTP interface.theDoorBell=DoorBell()# Add the methods as a command.myApp.addCommand(theDoorBell.switchDoorBell)myApp.addCommand(theDoorBell.getCurrentState)# Run main loop.myApp.run()Please have a look at the examples included in this project. To see how I switch on/off my doorbell seedoorbell. Another good example on how to useraspendismbpv, which shows how I useraspend'sworker threads to read out current values of my solar inverters.How to use the HTTP interface?The data partAs already mentioned, all worker threads share the same dictionary they read from or write to. You can query this data by sending a HTTP GET request tohttp://ip-of-your-system:port/data. The HTTP interface then responds with the whole shared dictionary as a JSON string. It is recommended not to write too much data in this dictionary, as this could reduce performance.Let's say you are measuring the temperatures of different rooms of your house, then the shared dictionary could have the following structure:{"basement":{"party_room":17.8,"heating_room":18,"fitness_room":19.5},"ground_floor":{"kitchen":23.5,"living_room":23.6}}If you only want to know the temperatures of the ground floor you can request/data/ground_floor. Then the response would be:"ground_floor":{"kitchen":23.5,"living_room":23.6}Or if you only want to know the temperature of the fitness room in your basement you could use/data/basement/fitness_roomand the response would be:19.5The command partNow let's have a look at the command interface ofraspend. If you want to know which commands are available you can request/cmds. Then the response for the above mentioneddoorbellexample would be:{"Commands":[{"Command":{"Name":"theDoorBell.switchDoorBell","Args":{"onoff":""}}},{"Command":{"Name":"theDoorBell.getCurrentState","Args":{}}}]}As you can see in the response above, your variable names should be in a more descriptive manner, since the instance of your Python class is used instead of the class name. Among other things, this allows us to have more than one instance of the respective class.You invoke a command by sending it's call information as described in the list above via HTTP POST request.Here a JavaScript example:letpayload={Command:{Name:"theDoorBell.switchDoorBell",Args:{onoff:"off"}}};letresponse=awaitfetch("http://localhost:8080",{method:'POST',headers:{'Content-Type':'application/json; charset=utf-8'},body:JSON.stringify(payload)});if(response.status==200){letresponsePayload=awaitresponse.json();console.log(responsePayload);}The HTTP interface receives{"Command":{"Name":"theDoorBell.switchDoorBell","Args":{"onoff":"off"}}}and invokes the method. The response of this HTTP POST request will beyourJSON enhanced with the result of the method invocation:{"Command":{"Name":"theDoorBell.switchDoorBell","Args":{"onoff":"off"},"Result":"off"}}Since remain untouched, you can attach any other information with that command such as an element-id of your frontend invoking it.Starting with version 1.1.0, you can also use HTTP GET requests to invoke commands. The request has the following structure:/cmd?name=command&arg1=val1&arg2=val2...&argN=valNSo for the above mentioned exampletheDoorBell.switchDoorBell, the correct request would be:/cmd?name=theDoorBell.switchDoorBell&onoff=offThe HTTP interface invokes the command and responds with the result of the invocation as a JSON string.How to install?Make sure you have installed Python 3.5 or higher. I've tested the package on my Raspberry Pi 3 Model B+ runningRaspbian GNU/Linux 9 (stretch)with Python 3.5.3 installed.Use$ pip install raspendor if Python 3 isn't the default use$ pip3 install raspendto install the package.RemarksThere is a breaking change from version 1.4.0 to 2.0.0. The modules calledDataAcquisitionandPublishingfell victim to the DRY principle and have been replaced by the worker threads. This also means that the following methods have been removed from theRaspendApplicationclass:createDataAcquisitionThreadcreatePublishDataThreadcreateScheduledPublishDataThreadPlease usecreateWorkerThreadorcreateScheduledWorkerThreadinstead.Migration should be easy. Just replace all derivations ofDataAcquisitionHandlerandPublishDataHandlerbyThreadHandlerBase. Remove allfrom raspend.utils import dataacquisition as DataAcquisitionandfrom raspend.utils import publishing as Publishingand usefrom raspend import ThreadHandlerBaseinstead. In your implementations ofDataAcquisitionHandlerrename theacquireDatamethod toinvoke. In your implementations ofPublishDataHandlerrename thepublishDatamethod toinvokeas well.LicenseMIT. See LICENSE file.