Funksteckdosen per iOS App steuern – Raspberry Pi

Funksteckdosen per iOS App steuern – Raspberry Pi

Bereits vor einiger Zeit haben Tutorials-Raspberrypi.de und StaticFloat zusammen ein paar Artikel geschrieben, um über eine Android App und einen Raspberry Pi Steckdosen ein und ausschalten zu können. Aufgrund von sehr vielen Anfragen in den Kommentaren haben wir uns dazu entschieden noch einmal nachzuliefern und eine iOS App zu schreiben.

Einschränkungen

Ich empfinde das als ein nettes kleines Projekt. Leider muss ich allen iPhone Besitzern, um sie etwas “abschrecken”, eine Hiobsbotschaft übermitteln. Eure erstellte iOS App wird ohne bezahlte Entwicklerlizenz seits Apple nur 2 Wochen lang lauffähig sein. Keine Angst, ihr könnt es günstig halten, das iPhone alle 2 Wochen wieder an den Mac anschließen und die iOS App per Xcode neu auf das iPhone oder iPad ziehen. Eine bezahlte Entwicklerlizenz kostet derweilen glaube ich 70$ pro Jahr. Als nächste kleine Einschränkung müsst ihr unbedingt einen Mac (oder Hackintosh) besitzen, um XCode nutzen zu können.

Ich sage das nun wirklich nicht gerne, aber es gibt leider Gottes noch ein kleineres oder größeres Problem mit der iOS App. Ich weiß nicht mal ob es möglich ist -zumindest gibt es keine App dazu und auch keine Anleitung im Netz-, aber scheinbar können wir kein SSH verwenden. Das heißt in anderen Worten, dass es uns nicht zu 100% möglich sein wird die Android App 1:1 zu übernehmen. Stattdessen werden wir uns anderweitig behelfen. Dieser Workaround ist tatsächlich sogar noch etwas schneller, als die SSH Variante. Also werde wir per iOS App die Scheckdosen noch schneller ein und ausschalten können. Übrigens: Diesen Weg könnten wir in unserer Android App auch verwenden.

Der Workaround

SSH ist zwar eine der besten Varianten auf unserem Linux PC, wie dem Raspberry Pi, Befehle auszuführen, aber weiß Gott nicht die einzige. Wir werden nun folgendes tun: Wir werden einen kleinen Webserver auf unserem Pi installieren und diesem die Rechte dazu geben ein bestimmtes Skript mit Sudo Recht auszuführen. Mittels PHP und statischer IP des Pi werden wir dann über die iOS App einen “ganz normalen” Request an unseren “Webserver” stellen und dieser wird seinerseits dann das Skript zur Schaltung der Steckdosen ausführen.

Vorerst werdet ihr einen Webserver auf eurem Raspberry Pi installieren müssen. Dazu findet ihr eine Anleitung in einem vorigen Artikel.
Dann müsst ihr den Artikel von Tutorials-Raspberrypi.de verfolgen müssen, in dem euch erklärt wird, wie ihr das entsprechende Skript installiert, die Sendemodule anschließt und die Codes zur Schaltung herausfindet. Hier gibt es den Artikel dazu.

Habt ihr diese Vorarbeiten geleistet müssen wir den Pi einmal neustarten.
Ist er wieder “oben”, dann müssen wir PHP oder eher gesagt unserem Webserver erlauben, dass er unser Skript ausführen darf. Auf Tutorials-Raspberrypi habt ihr in der Anleitung gelesen, dass das Skript zur Ausführung von Steckdosenbefehlen in “~/Funksteckdosen-RaspberryPi” liegen soll. Das bedeutet, dass euer Skript in eurem home-Ordner liegt. Das bedeutet aber wiederum, dass wenn euer User “pi” heißt, dann lautet der gesamte Ordnerpfad zu eurem Skript: “/home/pi/Funksteckdosen-RaspberryPi”.

Diesen Pfad benötigen wir jetzt. – Warum? Weil wir dem Webserver aus Sicherheitsgründen nur die Sudo-Rechte in diesem einen Ordner geben wollen.

Die Sudo-Rechte erteilen wir, indem wir in der Konsole den Befehl “sudo visudo” ausführen. Am Ende der Datei fügen wir dann ein: “www-data ALL=NOPASSWD: /home/pi/Funksteckdosen-RaspberryPi”. Natürlich mit dem angepassten Nutzernamen.

Bitte startet den Raspberry Pi noch einmal neu.

Das PHP-Script

Jetzt haben wir einen eingerichteten Pi. Was fehlt uns? – Das PHP-Script auf unserem Webserver!

Bitte geht einmal in den Ordner /var/www/html und fügt dort unter dem Namen “steckdosen.php” folgende Datei ein und beachtet den möglicherweise geänderten Ordnerpfad.

Das wars dann auch schon. War doch gar nicht so schlimm :P

Das PHP-Skript funktioniert so:
Wenn wir annehmen unser Raspberry Pi hat die IP-Adresse 192.168.0.2, dann könnten wir ganz einfach in den Browser eingeben: “http://192.168.0.2/steckdosen.php?dose=0&status=1” und wir würden den Request an unseren Pi senden, der seinerseits dann das entsprechende Skript zur Schaltung ausführt. Klappt alles, dann steht im Browser nun “Erfolg!”.
Bevor unsere iOS App das aber tun kann müssen wir als aller erstes einmal erfahren welche Steckdosen es überhaupt gibt. Dazu können wir die Internetadresse “http://192.168.0.2/steckdosen.php?getJson” aufrufen. Als Rückgabe gibt es für uns dann eben die entsprechende Liste an Steckdosen für die spätere Anzeige innerhalb der iOS App. Der Vorteil ist, dass wir nur innerhalb des Raspberry Pi neue Steckdosen definieren müssen und diese dann sofort auch in unserer App sehen und verwenden können.
In unserer iOS App werden wir auch nichts anderes tun, als eine ganz einfache Internetadresse aufrufen. Mit dem Unterschied, dass wir die Internetseite nur in unserem heimischen Netzwerk aufrufen können und dass diese Internetseite auf unserem Raspberry Pi läuft. Oh und wir zeigen die Webseite nicht an :D

Die iOS App

Wie dem auch sei. Ich habe eben etwas gelogen. Uns fehlt noch mehr, als nur das PHP-Script. Uns fehlt ja noch die iOS App.

Als aller Erstes benötigen wir ein neues Xcode Projekt. Dazu laden wir uns Xcode aus dem Mac App Store herunter und öffnen es.
Dann folgt bitte einmal dieser kleinen Bilderreihe:

Als Erstes wählen wir aus, dass wir ein neues Projekt erstellen wollen.

Anschließend geben wir an, dass wir eine iOS App erstellen möchten, dass wir lediglich eine Single View App erstellen und klicken auf “Next”.

Vergebt einen Namen für eure App und gebt in dem Feld darunter euren Namen an. Der “Organization Identifier” kann dann wieder euer Vor- und Zuname sein, welcher mit einem Punkt getrennt ist. Wählt “Swift” und nur “Use Core Data”.

Bevor es richtig los geht: Wir werden uns zu 99% in diesen beiden rot markierten Dateien aufhalten.

Vorarbeit

Wie soll es anders sein. Bevor wir anfangen können unsere App wirklich zu programmieren müssen wir innerhalb unseres Projektes noch Anpassungen vornehmen. Beispielsweise wollen wir verhindern, dass unsere App im Breitbildformat angezeigt werden kann. Warum wir das machen? – Keine Ahnung :P Ich finde es so einfach schöner und wir müssen uns nicht um andere Bildschirmansichten kümmern.

Also dann.
Öffnet bitte die Datei info.plist.

Entfernt dort die beiden unteren Einträge. Damit verhindern wir, dass die App im Breitbildformat angezeigt werden kann.

Layout

Nun wird es etwas kniffliger. Wir benötigen für die App ein Layout. Da es sich um ein dynamisches Layout handeln soll müssen wir also “erst einmal” nur dafür sorgen, dass wir für das spätere Coding eine Art Vorlage haben, die wir dann nach belieben replizieren und wiederverwerten können. Das Ganze passiert in unserem Fall zum Großteil per Drag and Drop. Ich überlege gerade ob man das am Besten per Video erklären könnte. Was meint ihr?

Naja, versuchen wir es erst einmal über die altmodische Variante. Wir nutzen einfach Text und ein paar kleinere Bilder.

Also dann. Am Ende sollte euer Layout unter dem Menüpunkt “View” in der Datei “Main.storyboard” in etwa so aussehen:

In neueren Versionen könnte zwischen “View” und “Table View” noch der Punkt “Safe Area” auftauchen. Das ist aber völlig irrelevant.

Dieses Layout erreichen wir, indem wir in dem Eingabefeld ganz unten rechts als Erstes einmal “Table View” eingeben. Dann ziehen wir per Drag and Drop das Element in die Vorschau. Ich zeige das einmal anhand des Table Views. Alle anderen Elemente können genau so dort hinein gezogen werden.

Habt ihr das “Table View” nun in euer Layout gezogen, dann wählen wir dieses in der Vorschau noch einmal aus (blauer Rand). Jetzt wird es leider etwas Tricks … deshalb meine Idee mit dem Video.

Ihr seht die beiden grünen Rechtecke auf dem folgenden Bild? – Wählt den Menüpunkt des oberen grünen Rechteckes aus und klickt dann auf alle roten Linien des unteren grünen Rechteckes, damit diese voll rot sind und durchgezogene Linien haben. In der Animation rechts daneben sollte das rote Feld auf voller Größe des Fensters bleiben, das Fenster also voll ausfüllen.

Jetzt ist unser Layout in so fern bereit, als dass es sich voll mit der Fenster-/Bildschirmgröße skalieren würde. Leider ist unser Table View noch nicht auf der vollen Breite und Höhe. Ergo klicken wir nun in das Drop-Down Feld “Arrange” und wählen einmal “Fill Container ???” für beide Variationen aus. Und schon haben wir ein Element hinzugefügt und es in der Größe angepasst.

Layout 2

Noch sind wir nicht fertig. Es fehlt noch ein wenig. Wiederholt nun den ersten Teil noch einmal und zieht einfach ein “Table View Cell” in die “Table View”. Größenanpassungen müssen wir nicht vornehmen.

 Wählt nun im rechten Menü und unter “Style” den Punkt “Subtitle” aus. Damit können wir als Überschrift den Namen der Steckdose verwenden und als Untertitel dann den Status der Steckdose.

Damit wir diese neue Zeile auch Wiederwenden können müssen wir ihr noch einen Namen geben. Wählt dazu die “Table View Cell” in der linken Leiste aus und vergebt dann den Namen “zeile”, wie hier rot markiert.

Eine einzige kleine Sache fehlt uns noch. Wir benötigen nach unserem fertigen Layout nun noch eine Verbindung von unserer Table View in unseren Code. Damit können wir dann ganz easy die Tabelle auch befüllen. Sagen wir, das ermöglicht es uns ein Layout in einer Variable zu speichern. Diese Variable können wir dann verwenden, um im Layout Änderungen vor zu nehmen. (Ok, ok, ok … Das war jetzt SEHR plump ausgedrückt).

Klickt dazu bitte als Erstes, im geöffneten Layout-Tab, die folgende Taste:

Wir haben nun links die Vorschau und rechts haben wir unseren Code. Wählt nun mit der linken Maustaste einmal die TableView aus, klickt “CTRL” und zieht bei gedrückter linker Maustaste die Verbindung in euren Code. Am Besten an die Stelle, an der jetzt “@IBOutlet weak var tableView: UITableView!” steht:

class ViewController: UIViewController {
 @IBOutlet weak var tableView: UITableView!
 
 override func viewDidLoad() {

Nun erscheint ein Pop-Up. Vergebt bitte den Namen “tableView” und klickt auf “Connect”.

Coding

Wir haben es geschafft! Yeay!
Die Vorarbeit ist voll geleistet und unser Layout steht. Fangen wir also endlich an zu programmieren! Wenn du übrigens bis hier her durchgehalten hast, dann steht dem Rest echt nichts mehr im Wege. Ich muss ja leider zugeben, dass ich Tage gebraucht habe, um in Xcode so weit zu kommen, denn ehrlich gesagt sind gute Anleitungen echt selten.

Da wir uns hier mit dem Erweitern von Tabellen beschäftigen müssen wir unsere Klasse ein wenig erweitern. Dies erreichen wir, indem wir zwischen “UIViewController” und das “{” (siehe Screenshot) folgendes einfügen:

, UITableViewDelegate, UITableViewDataSource

Xcode wird nun erst einmal einige Fehler werfen. Das kann uns aber so weit erst einmal egal sein, denn die beheben wir später. Wir fahren nun fort, indem wir uns Variablen erstellen, die uns die Steckdosen speichern lassen.

Dazu fügen wir unterhalb von unserer eben erstellten Layout Verbindung, aber oberhalb der Funktion “viewDidLoad” ein:

var names: [String] = []
var states: [String] = []
let url = "http://IP-EURES-PI/steckdosen.php"

Nun ersetzen wir den Kommentar “// Do any additional setup after loading the view, typically from a nib.” durch folgendes:

// Lade die Liste mit den Einträgen.
getSteckdosen()

Zu guter letzt benötigen wir noch den Code zur Anzeige und den Code zum Schalten der Steckdosen. Dabei führen wir beide male einfach einen Request aus, um uns von unseren Pi die Informationen zu holen oder das Schalten der Steckdosen auszuführen.

/* Lade die momentane Liste an unerledigten Einträgen herunter und erneure daraus die Tabelle. */
 func getSteckdosen(){
 
 let requestUri : String = "\(url)?getJson"
 
 // Erstelle die Url
 let request = URLRequest(url: URL(string: requestUri)!)
 
 // Erstelle einen asynchronen Task, der dann im Hintergrund die Daten lädt und bei Fertigstellung die Tabellen mit den Daten füttert.
 let task = URLSession.shared.dataTask(with: request) { data, response, error in
 
 // Prüfe auf fundamentale Netzwerkfehler
 guard let data = data, error == nil else { return }
 
 // Prüfe auf einen anderen Response Code, als 200.
 // Dieser könnte auftauchen, falls bei mit alles super ist, aber die NAS einen Fehler hat.
 if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
 print("statusCode should be 200, but is \(httpStatus.statusCode)")
 }else{
 do {
 
 if let returnData = String(data: data, encoding: .utf8) {
 print(returnData)
 } else {
 print("")
 }
 
 // Parse die Response
 let allContacts = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as! [String : NSArray]
 
 // Wenn bis hier hin kein Fhler passiert ist und die Try-Catch gegriffen hat,
 // dann lösche die alten Arrays, um sie gleich wieder neu beschreiben zu können.
 self.names = []
 self.states = []
 
 // Gehe das Ergebnis einzeln durch
 if let arrJSON = allContacts["outlets"] {
 for index in 0...arrJSON.count-1 {
 
 // Schreibe alles in die neuen leeren Arrays
 let aObject = arrJSON[index] as! [String: Any]
 self.names.append(aObject["name"] as! String)
 self.states.append(aObject["status"] as! String)
 
 }
 }
 
 }
 catch {
 print("Error")
 }
 
 // Da wir asynchron arbeiten sollte hier auf dem Mainthread noch einmal die Tabelle geupdated werden
 DispatchQueue.main.async(execute: {() -> Void in
 self.tableView.reloadData()
 self.tableView.beginUpdates()
 self.tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows!, with: .none)
 self.tableView.endUpdates()
 })
 DispatchQueue.main.async{
 
 }
 }
 
 }
 
 // Führe den eben erstellten asynchronen Task jetzt aus.
 task.resume()
 
 }
 
 func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
 return true
 }
 
 // Definiere die Länge der Liste (also die Anzahl der Einträge
 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 return self.names.count;
 }
 
 // Füge die Button hinzu, die beim Wischen nach links erscheinen
 func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
 
 let label = "Schalten"
 
 // Definiere den Button, für das Fertigstellen eines Eintrages und die dazugehörige onClick Funktion
 let toggleButton = UITableViewRowAction(style: .normal, title: label) { action, index in
 print("Es wurde \(label) geklickt, für \(self.names[indexPath.row])")
 self.toggleSteckdose(id: indexPath.row)
 }
 toggleButton.backgroundColor = UIColor.init(red: 47/255, green: 123/255, blue: 255/255, alpha: 1.0)
 
 // Gebe die Button als Liste zurück, damit der Standard diese rendern kann
 return [toggleButton]
 }
 
 // Definiere den Inhalt jeder Zeile der Tabelle
 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
 
 // Definiere das Aussehen der Zeilen
 let cell = tableView.dequeueReusableCell(withIdentifier: "zeile")
 
 // Definiere den Text des aufgewählen Zeilenstils
 cell?.textLabel?.text=self.names[indexPath.row]
 
 if(self.states[indexPath.row] == "1"){
 cell?.detailTextLabel?.text = "An"
 }else{
 cell?.detailTextLabel?.text = "Aus"
 }
 
 // Mache den text einer Zeile zu einer Multiline Zeile, damit der Text nicht abgeschnitten wird.
 cell?.textLabel?.numberOfLines = 0
 cell?.textLabel?.lineBreakMode = .byWordWrapping
 cell?.textLabel?.font = UIFont.systemFont(ofSize: UIFont.labelFontSize)

 // Gebe die Zeile an irgendwas aus dem Standard zurück, dass diese dann rendert
 return cell!
 }
 
 // Behandelt das als "Erledigt" markieren eines Eintrages und damit das Senden an den Server
 func toggleSteckdose(id: Int){
 
 // Erstelle die URL
 var request = URLRequest(url: URL(string: url)!)
 
 // Bekomme den aktuellen Status
 var neuerStatus : String = self.states[id]
 
 // Setze den neuen Status der Steckdose
 if(neuerStatus == "1"){
 neuerStatus = "0";
 }else{
 neuerStatus = "1";
 }
 
 // Codiere die Daten und füge sie zum Request hinzu, um sie Senden zu können.
 let postString = "dose=\(id)&status=\(neuerStatus)"
 request.httpBody = postString.data(using: String.Encoding.utf8)!
 
 // Erstelle einen asynchronen Task, der dann im Hintergrund den Eintrag zu verändern
 let task = URLSession.shared.dataTask(with: request) { data, response, error in
 
 // Prüfe auf fundamentale Netzwerkfehler
 guard let data = data, error == nil else { return }
 
 // Prüfe auf einen anderen Response Code, als 200.
 // Dieser könnte auftauchen, falls bei mit alles super ist, aber die NAS einen Fehler hat.
 if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
 print("statusCode should be 200, but is \(httpStatus.statusCode)")
 }else{
 
 // Das Senden war erfolgreich! Yeah!
 // Gibt jetzt eine Log Nachricht aus und erneuere die Tabelle
 let responseString = String(data: data, encoding: .utf8) ?? ""
 print("responseString = \(responseString)")
 self.getSteckdosen()
 
 }
 
 }
 
 // Führe den eben erstellten asynchronen Task jetzt aus.
 task.resume()
 }

Das war es auch schon. Ersetzt noch die Variable “url” durch die IP eures Pi und seht, wie der Strom fließt.

Marvin

Ich bin ein Mensch, der sich neben der Programmierung noch für tausend andere Dinge interessiert, die mal mehr und mal weniger verrückt sind. Vor allem aber bin ich Feuer und Flamme mit der Programmierung von eigenen kleinen Apps und Programmen, die mein Leben bereichern.

Kommentar hinzufügen

*Pflichtfeld