From f8ec09040d16d92c08c05d94027a5ec84cc53984 Mon Sep 17 00:00:00 2001 From: jobe Date: Thu, 17 Oct 2019 23:43:24 +0200 Subject: [PATCH 1/4] Threaded publishing of the shared dictionary --- Examples/example4.py | 86 +++++++++++++++++++++++++++++++++++++ raspend.pyproj | 4 +- raspend/application.py | 29 +++++++++++++ raspend/utils/publishing.py | 77 +++++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 Examples/example4.py create mode 100644 raspend/utils/publishing.py diff --git a/Examples/example4.py b/Examples/example4.py new file mode 100644 index 0000000..89ef352 --- /dev/null +++ b/Examples/example4.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# raspend - Example +# +# License: MIT +# +# Copyright (c) 2019 Joerg Beckers + +import time +import random +import requests + +from raspend.application import RaspendApplication +from raspend.utils import dataacquisition as DataAcquisition +from raspend.utils import publishing as Publishing + +class DoorBell(): + def __init__(self, *args, **kwargs): + self.doorBellState = "on" + + def switchDoorBell(self, onoff): + if type(onoff) == str: + self.doorBellState = "on" if onoff == "on" else "off" + elif type(onoff) == int: + self.doorBellState = "on" if onoff >= 1 else "off" + else: + raise TypeError("State must be 'int' or 'string'.") + return self.doorBellState + + def getCurrentState(self): + return self.doorBellState + +class ReadOneWireTemperature(DataAcquisition.DataAcquisitionHandler): + def __init__(self, groupId, sensorId, oneWireSensorPath = ""): + # A groupId for grouping the temperature sensors + self.groupId = groupId + # The name or Id of your sensor under which you would read it's JSON value + self.sensorId = sensorId + # The path of your sensor within your system + self.oneWireSensorPath = oneWireSensorPath + + def prepare(self): + if self.groupId not in self.sharedDict: + self.sharedDict[self.groupId] = {} + self.sharedDict[self.groupId][self.sensorId] = 0 + return + + def acquireData(self): + # If you use 1-Wire sensors like a DS18B20 you normally would read its w1_slave file like: + # /sys/bus/w1/devices//w1_slave + temp = random.randint(18, 24) + self.sharedDict[self.groupId][self.sensorId] = temp + return + +class PublishOneWireTemperatures(Publishing.PublishDataHandler): + def __init__(self, endPointURL, userName, password): + self.endPoint = endPointURL + self.userName = userName + self.password = password + + def publishData(self): + data = json.dumps(self.sharedDict) + rq = requests.post(self.endPoint, data, auth=(self.userName, self.password)) + + +myApp = RaspendApplication(8080) + +theDoorBell = DoorBell() + +myApp.addCommand(theDoorBell.switchDoorBell) +myApp.addCommand(theDoorBell.getCurrentState) + +myApp.updateSharedDict({"Starting Time" : time.asctime()}) + +myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "party_room", "/sys/bus/w1/devices/23-000000000001/w1_slave"), 3) +myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "heating_room", "/sys/bus/w1/devices/23-000000000002/w1_slave"), 3) +myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "fitness_room", "/sys/bus/w1/devices/23-000000000003/w1_slave"), 3) +myApp.createDataAcquisitionThread(ReadOneWireTemperature("ground_floor", "kitchen", "/sys/bus/w1/devices/23-000000000004/w1_slave"), 3) +myApp.createDataAcquisitionThread(ReadOneWireTemperature("ground_floor", "living_room", "/sys/bus/w1/devices/23-000000000005/w1_slave"), 3) + +myApp.createPublishDataThread(PublishOneWireTemperatures("", "", ""), 15) + +myApp.run() + +print ("Exit") diff --git a/raspend.pyproj b/raspend.pyproj index 0e3860e..6889c7c 100644 --- a/raspend.pyproj +++ b/raspend.pyproj @@ -5,7 +5,7 @@ 2.0 {d0fd4413-717c-4da2-b189-4cc8bdecbc68} - Examples\example3.py + Examples\example4.py . . @@ -23,6 +23,7 @@ + Code @@ -30,6 +31,7 @@ Code + Code diff --git a/raspend/application.py b/raspend/application.py index ccc39a3..b54fd94 100644 --- a/raspend/application.py +++ b/raspend/application.py @@ -14,6 +14,7 @@ from .utils import serviceshutdownhandling as ServiceShutdownHandling from .utils import dataacquisition as DataAcquisition from .utils import commandmapping as CommandMapping +from .utils import publishing as Publishing class RaspendApplication(): """ This class handles the main loop for a raspend based application. @@ -26,6 +27,9 @@ def __init__(self, port, sharedDict=None): # A list holding instances of DataAcquisitionThread self.__daqThreads = list() + # A list holding instances of PublishDataThread + self.__pubThreads = list() + # The dictionary holding user's commands he wants to expose. self.__cmdMap = CommandMapping.CommandMap() @@ -58,6 +62,23 @@ def createDataAcquisitionThread(self, dataAcquisitionHandler, threadSleep=1): return len(self.__daqThreads) + def createPublishDataThread(self, publishDataHandler, threadSleep=1): + """ This method creates a new instance of 'Publishing.PublishDataThread'. + Make sure that the handler you provide is derived from 'Publishing.PublishDataHandler'! + """ + if not isinstance(publishDataHandler, Publishing.PublishDataHandler): + raise TypeError("Your 'PublishDataHandler' must be derived from 'Publishing.PublishDataHandler'!") + + publishDataHandler.setSharedDict(self.__sharedDict) + + publishDataThread = Publishing.PublishDataThread(threadSleep, + self.__shutdownFlag, + self.__dataLock, + publishDataHandler) + self.__pubThreads.append(publishDataThread) + + return len(self.__pubThreads) + def addCommand(self, callbackMethod): """ Adds a new command to the command map of your application. """ @@ -88,6 +109,9 @@ def run(self): for daqThread in self.__daqThreads: daqThread.start() + for pubThread in self.__pubThreads: + pubThread.start() + # Keep primary thread or main loop alive. while True: time.sleep(0.5) @@ -95,9 +119,14 @@ def run(self): except ServiceShutdownHandling.ServiceShutdownException: # Signal the shutdown flag, so the threads can quit their work. self.__shutdownFlag.set() + # Wait for all thread to end. + for pubThread in self.__pubThreads: + pubThread.join() + for daqThread in self.__daqThreads: daqThread.join() + httpd.join() except Exception as e: diff --git a/raspend/utils/publishing.py b/raspend/utils/publishing.py new file mode 100644 index 0000000..c448d1a --- /dev/null +++ b/raspend/utils/publishing.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Simple classes that handle threaded data publishing. +# +# License: MIT +# +# Copyright (c) 2019 Joerg Beckers + +import threading +import time + +class PublishDataHandler(): + """ Base class for a handler which published data or parts of data store in the shared dictionary. + Derive this class and override the 'publishData' - methods to publish the 'sharedDict'. + """ + + def __init__(self, sharedDict=None): + """ The contructor gets a dictionary containing any acquired data of your application. + """ + self.setSharedDict(sharedDict) + + def setSharedDict(self, sharedDict): + """ Set the shared dictionary + """ + self.sharedDict = sharedDict + + def prepare(self): + """ This method is called before the publish thread is started with this handler. + So if you need to initialize parts of the shared dictionary you should override this method. + """ + pass + + def publishData(self): + """ This method is called by a 'PublishDataThread'. Override this method publish your data in 'sharedDict'. + """ + pass + +class PublishDataThread(threading.Thread): + """ A thread class which handles cyclic data publishing. + + An instance of this class needs a lock - object for controlling access to its 'publishDataHandler', an event - object for + notifying the thread to exit and an object of a class derived from 'PublishDataHandler'. + """ + def __init__(self, threadSleep=0, shutdownFlag=None, dataLock=None, publishDataHandler=None): + """ Contructs a new instance of 'PublishDataThread'. + + Parameters: + + threadSleep - milliseconds sleep time for the thread loop. + shutdownFlag - a threading.event() object for notifying the thread to exit. + dataLock - a threading.Lock() object for controlling access to the 'dataAcquisitionHandler'. + publishDataHandler - an object of a class derived from 'PublishDataHandler'. + """ + threading.Thread.__init__(self) + + self.threadSleep = threadSleep + self.shutdownFlag = shutdownFlag + self.dataLock = dataLock + self.publishDataHandler = publishDataHandler + + def run(self): + """ The thread loop runs until 'shutdownFlag' has been signaled. Sleep for 'threadSleep' milliseconds. + """ + # Let the handler prepare itself if necessary. + self.dataLock.acquire() + self.publishDataHandler.prepare() + self.dataLock.release() + + while not self.shutdownFlag.is_set(): + # acquire lock + self.dataLock.acquire() + # call publish data handler + self.publishDataHandler.publishData() + # release lock + self.dataLock.release() + self.shutdownFlag.wait(self.threadSleep) From 6ab63a9dcb44a4018b938c07ca2c46b11e59dc6f Mon Sep 17 00:00:00 2001 From: jobe Date: Sat, 19 Oct 2019 22:01:53 +0200 Subject: [PATCH 2/4] Added exception handling to publishData Read username and password from console --- Examples/example4.py | 56 +++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/Examples/example4.py b/Examples/example4.py index 89ef352..606fda8 100644 --- a/Examples/example4.py +++ b/Examples/example4.py @@ -10,6 +10,10 @@ import time import random import requests +from requests.exceptions import HTTPError +import argparse +import getpass +import json from raspend.application import RaspendApplication from raspend.utils import dataacquisition as DataAcquisition @@ -61,26 +65,50 @@ def __init__(self, endPointURL, userName, password): def publishData(self): data = json.dumps(self.sharedDict) - rq = requests.post(self.endPoint, data, auth=(self.userName, self.password)) + try: + response = requests.post(self.endPoint, data, auth=(self.userName, self.password)) + response.raise_for_status() + except HTTPError as http_err: + print("HTTP error occurred: {}".format(http_err)) + except Exception as err: + print("Unexpected error occurred: {}".format(err)) + else: + #print("Succeeded") + print(response.text) +def main(): + + cmdLineParser = argparse.ArgumentParser(prog="example4", usage="%(prog)s [options]") + cmdLineParser.add_argument("--port", help="The port the server should listen on", type=int, required=True) + + try: + args = cmdLineParser.parse_args() + except SystemExit: + return + + username = input("Enter username: ") + password = getpass.getpass("Enter password: ") + + myApp = RaspendApplication(args.port) -myApp = RaspendApplication(8080) + theDoorBell = DoorBell() -theDoorBell = DoorBell() + myApp.addCommand(theDoorBell.switchDoorBell) + myApp.addCommand(theDoorBell.getCurrentState) -myApp.addCommand(theDoorBell.switchDoorBell) -myApp.addCommand(theDoorBell.getCurrentState) + myApp.updateSharedDict({"Starting Time" : time.asctime()}) -myApp.updateSharedDict({"Starting Time" : time.asctime()}) + myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "party_room", "/sys/bus/w1/devices/23-000000000001/w1_slave"), 3) + myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "heating_room", "/sys/bus/w1/devices/23-000000000002/w1_slave"), 3) + myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "fitness_room", "/sys/bus/w1/devices/23-000000000003/w1_slave"), 3) + myApp.createDataAcquisitionThread(ReadOneWireTemperature("ground_floor", "kitchen", "/sys/bus/w1/devices/23-000000000004/w1_slave"), 3) + myApp.createDataAcquisitionThread(ReadOneWireTemperature("ground_floor", "living_room", "/sys/bus/w1/devices/23-000000000005/w1_slave"), 3) -myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "party_room", "/sys/bus/w1/devices/23-000000000001/w1_slave"), 3) -myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "heating_room", "/sys/bus/w1/devices/23-000000000002/w1_slave"), 3) -myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "fitness_room", "/sys/bus/w1/devices/23-000000000003/w1_slave"), 3) -myApp.createDataAcquisitionThread(ReadOneWireTemperature("ground_floor", "kitchen", "/sys/bus/w1/devices/23-000000000004/w1_slave"), 3) -myApp.createDataAcquisitionThread(ReadOneWireTemperature("ground_floor", "living_room", "/sys/bus/w1/devices/23-000000000005/w1_slave"), 3) + myApp.createPublishDataThread(PublishOneWireTemperatures("http://localhost/raspend/api/post_data.php", username, password), 5) -myApp.createPublishDataThread(PublishOneWireTemperatures("", "", ""), 15) + myApp.run() -myApp.run() + print ("Exit") -print ("Exit") +if __name__ == "__main__": + main() \ No newline at end of file From 057e5eb2b2c608b98e468ca933dbe6bb71c7789a Mon Sep 17 00:00:00 2001 From: jobe Date: Sun, 20 Oct 2019 14:40:15 +0200 Subject: [PATCH 3/4] Added demo database backend Fixed typos Set thread sleep to 60 seconds, so temperatures are published to database every minute. --- Examples/example4.py | 17 ++++--- raspend/application.py | 4 +- raspend_demo/api/.vscode/launch.json | 22 +++++++++ raspend_demo/api/database.php | 40 +++++++++++++++++ raspend_demo/api/post_data.php | 43 ++++++++++++++++++ raspend_demo/raspend_demo.sql | 67 ++++++++++++++++++++++++++++ 6 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 raspend_demo/api/.vscode/launch.json create mode 100644 raspend_demo/api/database.php create mode 100644 raspend_demo/api/post_data.php create mode 100644 raspend_demo/raspend_demo.sql diff --git a/Examples/example4.py b/Examples/example4.py index 606fda8..56d73e6 100644 --- a/Examples/example4.py +++ b/Examples/example4.py @@ -63,6 +63,10 @@ def __init__(self, endPointURL, userName, password): self.userName = userName self.password = password + def prepare(self): + # Nothing to prepare so far. + pass + def publishData(self): data = json.dumps(self.sharedDict) try: @@ -73,7 +77,6 @@ def publishData(self): except Exception as err: print("Unexpected error occurred: {}".format(err)) else: - #print("Succeeded") print(response.text) def main(): @@ -98,13 +101,13 @@ def main(): myApp.updateSharedDict({"Starting Time" : time.asctime()}) - myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "party_room", "/sys/bus/w1/devices/23-000000000001/w1_slave"), 3) - myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "heating_room", "/sys/bus/w1/devices/23-000000000002/w1_slave"), 3) - myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "fitness_room", "/sys/bus/w1/devices/23-000000000003/w1_slave"), 3) - myApp.createDataAcquisitionThread(ReadOneWireTemperature("ground_floor", "kitchen", "/sys/bus/w1/devices/23-000000000004/w1_slave"), 3) - myApp.createDataAcquisitionThread(ReadOneWireTemperature("ground_floor", "living_room", "/sys/bus/w1/devices/23-000000000005/w1_slave"), 3) + myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "party_room", "/sys/bus/w1/devices/23-000000000001/w1_slave"), 60) + myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "heating_room", "/sys/bus/w1/devices/23-000000000002/w1_slave"), 60) + myApp.createDataAcquisitionThread(ReadOneWireTemperature("basement", "fitness_room", "/sys/bus/w1/devices/23-000000000003/w1_slave"), 60) + myApp.createDataAcquisitionThread(ReadOneWireTemperature("ground_floor", "kitchen", "/sys/bus/w1/devices/23-000000000004/w1_slave"), 60) + myApp.createDataAcquisitionThread(ReadOneWireTemperature("ground_floor", "living_room", "/sys/bus/w1/devices/23-000000000005/w1_slave"), 60) - myApp.createPublishDataThread(PublishOneWireTemperatures("http://localhost/raspend/api/post_data.php", username, password), 5) + myApp.createPublishDataThread(PublishOneWireTemperatures("http://localhost/raspend_demo/api/post_data.php", username, password), 60) myApp.run() diff --git a/raspend/application.py b/raspend/application.py index b54fd94..3a998f8 100644 --- a/raspend/application.py +++ b/raspend/application.py @@ -88,7 +88,7 @@ def addCommand(self, callbackMethod): def updateSharedDict(self, other): """ Updates the shared dictionary with 'other'. - Note: existing keys will be overwritten. + Note: existing keys will be overwritten! """ self.__sharedDict.update(other) return len(self.__sharedDict) @@ -120,7 +120,7 @@ def run(self): # Signal the shutdown flag, so the threads can quit their work. self.__shutdownFlag.set() - # Wait for all thread to end. + # Wait for all threads to end. for pubThread in self.__pubThreads: pubThread.join() diff --git a/raspend_demo/api/.vscode/launch.json b/raspend_demo/api/.vscode/launch.json new file mode 100644 index 0000000..8d2affc --- /dev/null +++ b/raspend_demo/api/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Verwendet IntelliSense zum Ermitteln möglicher Attribute. + // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. + // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for XDebug", + "type": "php", + "request": "launch", + "port": 9000 + }, + { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 9000 + } + ] +} \ No newline at end of file diff --git a/raspend_demo/api/database.php b/raspend_demo/api/database.php new file mode 100644 index 0000000..7d97c9f --- /dev/null +++ b/raspend_demo/api/database.php @@ -0,0 +1,40 @@ +conn = null; + + try + { + $this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name, $this->username, $this->password); + $this->conn->exec("set names utf8"); + } + catch (PDOException $exception) + { + echo "Connection error: " . $exception->getMessage(); + } + + return $this->conn; + } +} diff --git a/raspend_demo/api/post_data.php b/raspend_demo/api/post_data.php new file mode 100644 index 0000000..52acb52 --- /dev/null +++ b/raspend_demo/api/post_data.php @@ -0,0 +1,43 @@ +getConnection(); + +$insertStmt = "INSERT INTO temperatures (`basement.party_room`," . + " `basement.fitness_room`," . + " `basement.heating_room`," . + " `groundfloor.kitchen`, ". + " `groundfloor.living_room`)" . + " VALUES (?, ?, ?, ?, ?);"; + +$stmt = $db->prepare($insertStmt); +$succeeded = $stmt->execute(array($temperatures->basement->party_room, + $temperatures->basement->fitness_room, + $temperatures->basement->heating_room, + $temperatures->ground_floor->kitchen, + $temperatures->ground_floor->living_room)); + +$newID = -1; +$errMsg = ""; + +if ($succeeded) +{ + $newID = $db->lastInsertId(); +} +else +{ + $errMsg = $stmt->errorInfo()[2]; +} + +$dbResult = array("lastInsertId" => $newID, "errorMessage" => $errMsg); +$json = json_encode($dbResult); + +header('Content-Type: application/json'); +echo $json; + +exit(); +?> \ No newline at end of file diff --git a/raspend_demo/raspend_demo.sql b/raspend_demo/raspend_demo.sql new file mode 100644 index 0000000..aef7e23 --- /dev/null +++ b/raspend_demo/raspend_demo.sql @@ -0,0 +1,67 @@ +-- phpMyAdmin SQL Dump +-- version 4.8.2 +-- https://www.phpmyadmin.net/ +-- +-- Host: 127.0.0.1 +-- Erstellungszeit: 20. Okt 2019 um 14:31 +-- Server-Version: 10.1.34-MariaDB +-- PHP-Version: 7.2.8 + +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +SET AUTOCOMMIT = 0; +START TRANSACTION; +SET time_zone = "+00:00"; + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; + +-- +-- Datenbank: `raspend_demo` +-- +CREATE DATABASE IF NOT EXISTS `raspend_demo` DEFAULT CHARACTER SET utf8 COLLATE utf8_bin; +USE `raspend_demo`; + +-- -------------------------------------------------------- + +-- +-- Tabellenstruktur für Tabelle `temperatures` +-- + +DROP TABLE IF EXISTS `temperatures`; +CREATE TABLE `temperatures` ( + `id` int(11) NOT NULL, + `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `basement.party_room` float DEFAULT NULL, + `basement.fitness_room` float DEFAULT NULL, + `basement.heating_room` float DEFAULT NULL, + `groundfloor.kitchen` float DEFAULT NULL, + `groundfloor.living_room` float DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; + +-- +-- Indizes der exportierten Tabellen +-- + +-- +-- Indizes für die Tabelle `temperatures` +-- +ALTER TABLE `temperatures` + ADD PRIMARY KEY (`id`); + +-- +-- AUTO_INCREMENT für exportierte Tabellen +-- + +-- +-- AUTO_INCREMENT für Tabelle `temperatures` +-- +ALTER TABLE `temperatures` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; +COMMIT; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; From 7ce9e9106f6ddaaad0c425d4e2233cee1500f5c2 Mon Sep 17 00:00:00 2001 From: jobe Date: Sun, 20 Oct 2019 21:16:16 +0200 Subject: [PATCH 4/4] Updated README and setup.py --- README.md | 37 ++++++++++++++++++++++++++-- raspend_demo/README.md | 8 ++++++ raspend_demo/api/.vscode/launch.json | 3 --- raspend_demo/api/database.php | 6 ++--- raspend_demo/api/post_data.php | 1 + setup.py | 2 +- 6 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 raspend_demo/README.md diff --git a/README.md b/README.md index 49af995..48dfca6 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,39 @@ myApp.run() ``` +Though your acquired data is available via raspend's HTTP interface, you probably want to push this data somewhere, a database for instance. Therefore version 1.3.0 introduces the **Publishing** module. You just need to create a handler derived from the **Publishing.PublishDataHandler** class, similar to the data acquisition part, and override it's **publishData** method. Here you can publish all or parts of the data contained in the shared dictionary to wherever you want. You then pass this handler to the **createPublishDataThread** method of **RaspendApplication**. The example below posts the whole shared dictionary as a JSON string to a PHP backend, which in turn writes the data into a MySQL database (see *raspend_demo* for details). + +``` python +from raspend.application import RaspendApplication +from raspend.utils import publishing as Publishing + +class PublishOneWireTemperatures(Publishing.PublishDataHandler): + def __init__(self, endPointURL, userName, password): + self.endPoint = endPointURL + self.userName = userName + self.password = password + + def prepare(self): + # Nothing to prepare so far. + pass + + def publishData(self): + data = json.dumps(self.sharedDict) + try: + response = requests.post(self.endPoint, data, auth=(self.userName, self.password)) + response.raise_for_status() + except HTTPError as http_err: + print("HTTP error occurred: {}".format(http_err)) + except Exception as err: + print("Unexpected error occurred: {}".format(err)) + else: + print(response.text) + + +myApp.createPublishDataThread(PublishOneWireTemperatures("http://localhost/raspend_demo/api/post_data.php", username, password), 60) + +``` + The other idea is to expose different functionalities, such as switching on/off your door bell via GPIO, as a command you can send to your RPi via HTTP POST request. All you have to do is to encapsulate the functionality you want to make available to the outside world into a method of a Python class. Then instantiate your class and call the **addCommand** method of **RaspendApplication** providing the method you want to expose. Now you can execute your method using a simple HTTP POST request. ``` python @@ -87,9 +120,9 @@ myApp.run() ``` -When all initialization stuff is done (adding commands, creating data acquisition threads), then you start your application by calling the **run** method of **RaspendApplication**. The **RaspendApplication** class installs signal handlers for SIGTERM and SIGINT, so you can quit your application by pressing CTRL+C or sending one of the signals via the **kill** command of your shell. +When all initialization stuff is done (adding commands, creating threads), then you start your application by calling the **run** method of **RaspendApplication**. The **RaspendApplication** class installs signal handlers for SIGTERM and SIGINT, so you can quit your application by pressing CTRL+C or sending one of the signals via the **kill** command of your shell. -Please have a look at the examples included in this project to get a better understanding. *example1.py* and *example2.py* show how to do most of the work yourself, while *example3.py* shows you the most convenient way of using this framework. +Please have a look at the examples included in this project to get a better understanding. *example1.py* and *example2.py* show how to do most of the work yourself, while *example3.py* shows you the most convenient way of using this framework. *example4.py* is identical to *example3.py* but extended by a **PublishDataHandler**. ## How to use the HTTP interface? diff --git a/raspend_demo/README.md b/raspend_demo/README.md new file mode 100644 index 0000000..eaa1819 --- /dev/null +++ b/raspend_demo/README.md @@ -0,0 +1,8 @@ +# Purpose + +**raspend_demo** is used for testing **raspend**'s publishing module. + +# Usage + +The easiest way to use **raspend_demo** is using Visual Studio Code with the PHP Debug extension installed (that's what I did). Then you need a web server with PHP enabled (I use [XAMPP](https://www.apachefriends.org/index.html)). To use the PHP Debug extension you further need to enable XDebug. See [PHP Debug extension](https://marketplace.visualstudio.com/items?itemName=felixfbecker.php-debug) for more information and follow the installations steps. + diff --git a/raspend_demo/api/.vscode/launch.json b/raspend_demo/api/.vscode/launch.json index 8d2affc..f47e0b8 100644 --- a/raspend_demo/api/.vscode/launch.json +++ b/raspend_demo/api/.vscode/launch.json @@ -1,7 +1,4 @@ { - // Verwendet IntelliSense zum Ermitteln möglicher Attribute. - // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. - // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { diff --git a/raspend_demo/api/database.php b/raspend_demo/api/database.php index 7d97c9f..810a2ff 100644 --- a/raspend_demo/api/database.php +++ b/raspend_demo/api/database.php @@ -2,16 +2,16 @@ class Database { - // Lokale Einstellungen + // local settings private $db_name = "raspend_demo"; private $host = "localhost"; private $username = "root"; private $password = ""; - // Entfernte Einstellungen + // remote settings /* - private $db_name = ""; + private $db_name = "raspend_demo"; private $host = ""; private $username = ""; private $password = ""; diff --git a/raspend_demo/api/post_data.php b/raspend_demo/api/post_data.php index 52acb52..4957fe3 100644 --- a/raspend_demo/api/post_data.php +++ b/raspend_demo/api/post_data.php @@ -15,6 +15,7 @@ " VALUES (?, ?, ?, ?, ?);"; $stmt = $db->prepare($insertStmt); + $succeeded = $stmt->execute(array($temperatures->basement->party_room, $temperatures->basement->fitness_room, $temperatures->basement->heating_room, diff --git a/setup.py b/setup.py index 6fc2f6e..72dd077 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setuptools.setup( name="raspend", - version="1.2.3", + version="1.3.0", author="Joerg Beckers", author_email="pypi@jobe-software.de", description="A small and easy to use HTTP backend framework for the Raspberry Pi which is ideal for small to medium-sized home automation projects.",