Funksteckdosen per Android App steuern

Funksteckdosen per Android App steuern

Heute wollen wir uns damit beschäftigen, mittels einem Raspberry Pi und 433Mhz Sender- und EmpfängermodulFunksteckdosen per Android App steuern zu können.

Wir werden versuchen per Android App, W-Lan und SSH auf dem Raspberry Pi einen Befehl auszuführen, welcher für uns die Steuerung der Funksteckdosen übernimmt, da ein normales Smartphone nicht dazu in der Lage ist über die 433Mhz Frequenz mit einer handelsüblichen Funksteckdose kommunizieren zu können.

Wie bereits bei unserem vorigen Projekt, mit dem Bewegungsmelder per Raspberry Pi, wird dieser Artikel zusammen mit der Webseite Tutorials-RaspberryPi.de entstehen. Auf StaticFloat findet ihr wieder einmal den Code, um eine Android App zur Steuerung des Raspberry Pi zu schreiben, während Tutorials-RaspberryPi.de den Part der Programmierung des Raspberry Pi übernimmt.

Ich möchte euch darauf hinweisen, dass wir mittels SSH Schnittstelle Befehle an unseren Raspberry Pi senden. Dies bedeutet, dass wir ohne besondere Konfiguration des Routers und Raspberry Pi nicht außerhalb des W-Lan Netzwerkes eine Steckdosensteuerung vornehmen können. Wenn ihr auch von Unterwegs Steckdosen steuern möchtet, dann solltet ihr versuchen an euren Raspberry Pi eine statische lokale IP-Adresse zu vergeben, im Router eine Port-Weiterleitung einrichten und einen Dienst, wie DynDNS, in eurem Router aktivieren.

Programmierung des Raspberry Pi’s:

Wie bereits angesprochen werdet ihr hier nur den Part zur Programmierung der Android App finden.
Für eine Interaktion mit der Funksteckdose ist allerdings eine Programmierung des Raspberry Pi nötig.

Wie ihr diesen programmieren könnt, findet ihr in diesem Artikel: Raspberry Pi + Android: Funksteckdosen per App schalten

Programmierung der Android App:

Ablauf:

Vorweg würde ich gerne erläutern, wie der Programmablauf aussieht.
Wir programmieren eine Android App, die per SSH auf unseren Raspberry Pi zugreift. Über einen an den Pi gesendeten Befehl erhalten wir von diesem eine Liste an steuerbaren Steckdosen und deren Status zugesandt.

Die steuerbaren Steckdosen werden innerhalb des Raspberry Pi definiert.

Für jede steuerbare Steckdose werden wir innerhalb der App einen Button erstellen und einen momentanen Status definieren.

Klicken wir auf einen der Button innerhalb der App wird ein entsprechender Befehl an den Raspberry Pi gesendet. Dieser speichert intern den Status der Steckdose und schaltet die Steckdose demnach ein oder eben aus.

Android App erstellen:

Bevor wir beginnen können zu programmieren bedarf es einem Grundgerüst unserer App. Zum Glück erstellt Android Studio uns dieses Grundgerüst mit Hilfe eines geführten Managers.

Solltet ihr zum ersten mal eine Android App programmieren, dann folgt bitte diesen 3 Artikeln:

Habt ihr Android Studio installiert, dann erstellt eine neue App. Dieser gebt ihr einen Namen (z. B. “Steckdosensteuerung”) und einen Package Namen.
Der Package Name besteht aus einem Ländercode (z. B. de), einem Entwicklernamen und dem Appnamen. Jeweils mit einem “.” getrennt (z. B. “de.staticfloat.steckdosensteuerung”).

Als minimale Version benutzt ihr 11 und ihr wählt “Blank Activity” (ab Android Studio 2.1 heißt es “Empty Activity”) aus.
Android Studio wird nun ein erstes Grundgerüst erstellen.

Konnektivität über SSH:

Da wir für die Kommunikation zwischen Raspberry Pi und Android App eine Schnittstelle Namens SSH benötigen, werden wir diese als erstes programmieren.

Wir nutzen für die SSH Konnektivität eine vorgefertigte Bibliothek namens “JSch”.
Diese Bibliothek wird uns später viel Arbeit abnehmen.

Als erstes müssen wir JSch herunterladen.
http://sourceforge.net/projects/jsch/files/jsch.jar/0.1.53/jsch-0.1.53.jar/download

Ist dies geschehen verschieben wir die Datei in den Ordner app -> libs.

Abschließend muss noch „Sync Project with Gradle Files“ in der oberen Leiste von Android Studio gewählt werden.

Öffnet nun die Klasse “MainActivity”.
Ersetzt den Inhalt der Datei mit dem gleich folgenden Code, aber achtet drauf, dass ihr die Zeile oberste mit „package ?????;“ erhaltet!

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Toast;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Properties;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class MainActivity extends Activity {


 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);


 }



 // Kleine Zusatzfunktion, die den eigentlichen SSH Befehl asynchron im Hintergrund startet.
 public void starteSSHAbfrage(final String nutzername, final String passwort, final String ip, final int port, final String befehl){

 // Hintergrundaufgabe erstellen
 new AsyncTask() {
 @Override
 protected String doInBackground(Integer... params) {
 // String, in den die ganzen Zeilen Ausgabe geschrieben werden
 String ausgabe = "";

 try {

 // Funktion ausführen und Konsolenausgabe in "lines" speichern.
 ArrayList lines = sshBefehl(nutzername, passwort, ip, port, befehl);

 // Alle Zeilen der Konsolenausgabe in den Android Logs ausgeben.
 while(!lines.isEmpty()){
 //Ausgabe in der Konsole
 Log.e("Rückgabe", lines.get(0));

 //Hänge momentane Zeile an den String "ausgabe" an
 ausgabe += lines.get(0);

 //Zeile entfernen
 lines.remove(0);
 }

 } catch (Exception e) {
 e.printStackTrace();
 }

 return ausgabe;
 }

 protected void onPostExecute(String ausgabe) {
 // Sende die Ausgabe an die Funktion "createButton", um nach dem empfangenen Status
 // die Button zum Schalten der einzelnen Steckdosen erstellen zu können.
 createButton(ausgabe);
 }
 }.execute(1);

 }

 // Dies ist die eigentliche Funktion, die den SSH Befehl ausführt.
 public static ArrayList sshBefehl(String username, String passwort, String hostname, int port, String befehl) throws Exception {

 Log.e("SSH Befehl", "Started - Befehl: " + befehl);

 // Erstelle das benötigte Objekt, um eine SSH-Verbindung aufbauen zu können.
 JSch jsch = new JSch();

 // Bereite ein paar Variablen vor, um Ausgaben der Konsole auslesen zu können.
 byte[] buffer = new byte[1024];
 ArrayList lines = new ArrayList<>();

 // Füttere das Objekt mit allen nötigen Informationen, um eine Verbindung aufbauen zu können.
 Session session = jsch.getSession(username, hostname, port);
 session.setPassword(passwort);

 // Umgehe das Abgleichen nach dem richtigen Key. (Es wird nun eine Man-In-Middle Attacke nicht mehr abgefangen)
 Properties prop = new Properties();
 prop.put("StrictHostKeyChecking", "no");
 session.setConfig(prop);

 // Stelle eine Verbindung her.
 session.connect();

 // Erstelle ein neues Objekt, für einen neuen SSH Channel.
 ChannelExec channelssh = (ChannelExec) session.openChannel("exec");

 // Füttere den Channel mit dem Befehl und schicke den Befehl ab.
 channelssh.setCommand( befehl );
 channelssh.connect();

 // Fange an die Ausgaben der Konsole auszulesen.
 try {
 InputStream in = channelssh.getInputStream();
 String line = "";

 // Lese alle Ausgaben aus, bis der Befehl beendet wurde oder die Verbindung abbricht.
 while (true) {

 // Schreibe jede Zeile der Konsolenausgabe in unser Array.
 while (in.available() > 0) {
 int i = in.read(buffer, 0, 1024);

 // Brich die Protokollierung der Ausgabe, für diese Zeile, ab, wenn die Ausgabe leer sein sollte.
 if (i < 0) {
 break;
 }
 line = new String(buffer, 0, i);
 lines.add(line);
 }

 // Wir wurden ausgeloggt.
 if (line.contains("logout")) {
 break;
 }

 // Befehl beendet oder Verbindung abgebrochen.
 if (channelssh.isClosed()) {
 break;
 }

 // Warte einen kleinen Augenblick mit der nächsten Zeile.
 try {
 Thread.sleep(1000);
 } catch (Exception ee) {}
 }
 }catch (Exception e){}

 // Beende alle Verbindungen.
 channelssh.disconnect();
 session.disconnect();

 // Gib die Ausgabe zurück
 return lines;
 }

}

Wir haben mittels dieses Codes nun zwei Funktionen erstellt und alle später benötigten Klasse in diese Klasse importiert.
Die erste Funktion (“starteSSHAbfrage”) startet später asynchron die Funktion “sshBefehl”. Asynchron bedeutet, dass die Funktion losgelöst und parallel zur eigentlichen App abläuft. Dies müssen wir tun, weil eine Abfrage über SSH einige Sekunden andauern kann und unsere App sonst während dieser Zeit einfriert oder sogar von Android beendet wird.

Wenn wir etwas weiter unten in der Funktion “starteSSHAbfrage” nachschauen, dann werden wir eine Funktion finden, die “createButton” heißt. Wir werden also über diese Funktion die Button erstellen und die Funktion  “starteSSHAbfrage” dient nur der Abfrage der Steckdosen.

Button erstellen:

Es bedarf nun der Funktion “createButton”. Diese übernimmt, wie bereits angesprochen, das Erstellen und Anzeigen der Button, für jede Steckdose.

Wir fügen also in die Klasse “MainActivity” unter die Funktion “onCreate” ein:

// Die Datei "status.json" wurde über SSH in die App eingelesen.
// Anhand der enthaltenen Informationen werden nun die entsprechenden Button auf der Startseite erstellt.
public void createButton(String status){

 // Wähle das LinearLayout aus, um in dieses dynamisch Button für jede Steckdose erstellen zu können.
 LinearLayout hauptfeld = (LinearLayout) findViewById(R.id.hauptfeld);

 try {

 // Lese die "status.json"-Datei ordnungsgemäß ein.
 JSONObject statusObjekt = new JSONObject(status);
 JSONArray steckdosenArray = new JSONArray(statusObjekt.getString("outlets"));

 // Mache die Liste der Steckdosen so lang, wie es auch Steckdosen gibt.
 steckdosen = new Steckdose[steckdosenArray.length()];

 // Gib jedem Objekt "Steckdose" in der Liste der Steckdosen ihren Status, ihre ID und ihren Namen.
 for (int j = 0; j < steckdosenArray.length(); ++j) {
 JSONObject jsonProdukt = steckdosenArray.getJSONObject(j);
 steckdosen[j] = new Steckdose(this);
 steckdosen[j].id = j;
 steckdosen[j].name = jsonProdukt.getString("name");
 steckdosen[j].status = jsonProdukt.getInt("status");
 }

 // Gehe ein zweites mal durch alle Einträge der Steckdosenliste.
 // Dieses mal sind alle Steckdosen richtig definiert, deshalb geben wir ihnen ihren Text und
 // fügen sie in das LinearLayout "hauptfeld" hinzu.
 for(int i = 0; i < steckdosen.length; i++) {

 // Gib der Steckdose einen Namen
 steckdosen[i].setText(steckdosen[i].name + " - " + steckdosen[i].status);

 // Für die Steckdose zum LinearLayout hinzu.
 hauptfeld.addView(steckdosen[i]);

 }

 } catch (JSONException e) {
 e.printStackTrace();
 }

}

MainActivity vervollständigen:

Wir haben unsere App bereits so weit geschrieben, dass wir die Anzahl und Namen der Steckdosen vom Raspberry Pi erhalten und anzeigen könnten.
Was nun fehlt ist der gesamte Rest der Klasse “MainActivity”, um die Abfrage zu starten, Fehlerabfragen zu betreiben, Verbindungsinformationen zu erstellen und das Design zu initialisieren.

Wir fügen oberhalb der Funktion “onCreate” ein:

private SharedPreferences pref;
private SharedPreferences.Editor editor;
Steckdose[] steckdosen;
private Einstellungen einstellungen;

Unterhalb der Funktion “onCreate” fügen wir ein:

// Zeigt die Startseite an.
private void zeigeStart(){

 // Zeige die Startseite an.
 setContentView(R.layout.activity_main);

 // Verbindungsdaten laden.
 einstellungen.nutzername = pref.getString("nutzername", "");
 einstellungen.passwort = pref.getString("passwort", "");
 einstellungen.ip = pref.getString("ip", "");
 einstellungen.port = pref.getInt("port", 22);

 // Gib eine Benachrichtigung aus, falls noch keine Verbindungsdaten angegeben sind.
 benachrichtige();

 // Prüft, ob eines der Felder für die Verbindungsdaten unvollständig ist.
 if(pref.getInt("port", 0) == 0 || pref.getString("ip", "").equals("") || pref.getString("passwort", "").equals("") || pref.getString("nutzername", "").equals("")) {

 // Rufe die Einstellungsseite auf, um Verbindungsdaten angeben zu können.
 zeigeEinstellungen();

 }else{

 // Da bereits Verbindungsdaten angegeben sind kann die App nun per SSH auf den Pi zugreifen und die Datei "status.json" auslesen,
 // welche die Anzahl und Namen der Steckdosen enthält.
 starteSSHAbfrage(Einstellungen.nutzername ,Einstellungen.passwort ,Einstellungen.ip ,Einstellungen.port ,Einstellungen.befehlStatusabfrage);

 }
}

// Zeigt die Einstellungsseite an.
public void zeigeEinstellungen(){
 setContentView(R.layout.activity_einstellungen);

 // Den Textfeldern Namen geben, um den Inhalt schreiben und auslesen zu können.
 final EditText edittext_nutzername = (EditText) findViewById(R.id.edittext_nutzername);
 final EditText edittext_ip = (EditText) findViewById(R.id.edittext_ip);
 final EditText edittext_passwort = (EditText) findViewById(R.id.edittext_passwort);
 final EditText edittext_port = (EditText) findViewById(R.id.edittext_port);
 Button speichern_button = (Button) findViewById(R.id.button_speichern);

 // Fehlerabfrage
 if(edittext_ip != null && edittext_nutzername != null && edittext_passwort != null && edittext_port != null && speichern_button != null) {

 // Textfelder mit bereits bestehenden Einstellungen befüllen.
 edittext_ip.setText(pref.getString("ip", ""));
 edittext_nutzername.setText(pref.getString("nutzername", ""));
 edittext_passwort.setText(pref.getString("passwort", ""));
 edittext_port.setText(pref.getInt("port", 22) + "");

 }

 // Auf den Klick, auf den Knopf "Speichern" reagieren.
 speichern_button.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {
 // Variablen vorbereiten, um den Inhalt der Textfelder unterspeichern zu können.
 String nutzername = "";
 String passwort = "";
 String ip = "";
 String port = "";
 // Inhalt der Textfelder in Variablen schreiben.
 nutzername = edittext_nutzername.getText().toString();
 ip = edittext_ip.getText().toString();
 passwort = edittext_passwort.getText().toString();
 port = edittext_port.getText().toString();
 if(!port.equals("") && !ip.equals("") && !nutzername.equals("") && !passwort.equals("")){
 // Verbindungsdaten laden
 editor.putString("nutzername", nutzername);
 editor.putString("passwort", passwort);
 editor.putString("ip", ip);
 editor.putInt("port", Integer.parseInt(port));
 // Einstellungen speichern
 editor.commit();
 // Gehe bei erfolgreicher Speicherung zur Startseite zurück
 zeigeStart();
 }else{
 // Fehlermeldung ausgeben, falls eines der Felder frei war.
 Toast.makeText(MainActivity.this, "Es scheinen noch Felder frei zu sein.", Toast.LENGTH_SHORT).show();
 }
 }
 });

}

// Definiert, dass es ein OptionMenü gibt und wie dieses aussehen soll. (Nach Vorlage der Datei "actionbar_manu.xml")
@Override
public boolean onCreateOptionsMenu(Menu menu) {
 MenuInflater inflater = getMenuInflater();
 inflater.inflate(R.menu.actionbar_menu, menu);
 return true;
}

// Definiert, wie auf Klicks im OptionMenü reagiert werden soll.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
 switch (item.getItemId()) {
 case R.id.menu_start:
 // Dieser Teil (bis zum "return") wird ausgeführt, wenn wir auf den Menüpunkt mit der ID "menu_start" klicken.
 zeigeStart();
 return true;
 case R.id.menu_ueber:
 // Dieser Teil (bis zum "return") wird ausgeführt, wenn wir auf den Menüpunkt mit der ID "menu_ueber" klicken.
 DialogFragment newFragment = new UeberDialog();
 newFragment.show(this.getFragmentManager(), "Über");
 return true;
 case R.id.menu_einstellungen:
 // Dieser Teil (bis zum "return") wird ausgeführt, wenn wir auf den Menüpunkt mit der ID "menu_einstellungen" klicken.
 zeigeEinstellungen();
 return true;
 default:
 // Standardfunktion. Sollte bestehen bleiben.
 return super.onOptionsItemSelected(item);
 }
}

// Kleine Klasse zum Erstellen eines Dialogs.
public static class UeberDialog extends DialogFragment {
 @Override
 public Dialog onCreateDialog(Bundle savedInstanceState) {
 //Dialog erstellen
 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
 //Befülle Dialog mit dem Text "ueber_text" aus der Datei res -> strings.xml
 builder.setMessage(R.string.ueber_text)
 //Erstelle einen Button mit dem Text "ok" aus der Datei res -> strings.xml
 .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
 public void onClick(DialogInterface dialog, int id) {
 //Schließe das Dialogfeld, beim Klick auf den erstellten Button
 dismiss();
 }
 });
 //Gib das erstellte Dialogfeld an die aufrufende Klasse zurück, um es anzeigen zu können.
 return builder.create();
 }
}
 
// Zeigt eine Benachrichtigung an, falls zu dem Zeitpunkt des Starts noch keine Verbindungsdaten angegeben wurde.
public void benachrichtige(){
 // Prüft, ob eines der Felder für die Verbindungsdaten unvollständig ist.
 if(pref.getInt("port", 0) == 0 || pref.getString("ip", "").equals("") || pref.getString("passwort", "").equals("") || pref.getString("nutzername", "").equals("")) {
 Toast.makeText(MainActivity.this, "Verbindungsdaten unvollständig. Menü -> Einstellungen", Toast.LENGTH_LONG).show();
 }

}

Und ersetzen den Inhalt von der Funktion “onCreate” mit:

super.onCreate(savedInstanceState);

//shared data vorbereiten
pref = PreferenceManager.getDefaultSharedPreferences(this);
editor = pref.edit();

// Rufe die Funktion auf, die die Startseite anzeigt.
zeigeStart();

Hilfsklassen erstellen:

Wenn ihr die Klasse “MainActivity” bereits nach Vorgabe vervollständig habt, dann sollte euch auffallen, dass Android Studio viele Fehler markiert.
Dies liegt daran, dass wir Hilfsklassen und Designvorlagen im Code verwenden, die wir bisweilen noch garnicht erstellt haben.

Im Grund gibt es bei unserer App zwei Hilfsklassen.
Die erste Klasse heißt “Einstellungen” und enthält, wie der Name schon sagt ein paar Einstellungen.

Ihr erstellt diese Klasse, indem ihr in der linken Liste auf app->java->(Euer Package Name ohne Test oder ähnliches) geht und mit einem Rechtsklick „new“ und dann „Java class“ auswählt.
Gebt hier „Einstellungen“ ein.

Ersetzt den Inhalt der Datei mit dem gleich folgenden Code, aber achtet drauf, dass ihr die Zeile oberste mit „package ?????;“ erhaltet!

public class Einstellungen {

public static String nutzername;
public static String passwort;
public static String ip;
public static int port;
public static final String befehlPre = "echo ";
public static final String befehlPost = " | sudo -S ./Funksteckdosen-RaspberryPi/funk";
public static final String befehlStatusabfrage = "cat ./Funksteckdosen-RaspberryPi/status.json";

}

Die zweite Klasse hingegen lautet “Steckdose” und ist etwas umfangreicher.
Wir haben mit der Klasse “Steckdose” eigentlich eine Standardklasse von Android vorliegen. Die Klasse “Button”. Wir werden unsere Klasse “Steckdose” nun erstellen und um die Klasse “Button” erweitern. Damit haben wir die Möglichkeit alle Funktionen der Klasse “Button” zu nutzen (also einen klickbaren Button zu erstellen) und zusätzlich noch ein paar weitere Funktionen und Variablen zu definieren.

Ich möchte euch kurz erklären, warum wir dies tun. Es mag auf den ersten Blick etwas schwierig klingen, ist im Grunde aber recht einfach.
Erstens erweitern wir die Klasse “Steckdose” um die Klasse “Button”, um mittels einer Klasse benutzerdefinierte Variablen und Button erstellen zu können. Ansonsten hätten wir 2 Klassenaufrufe benötigt. Arbeit gespart, puhh.

In zweiter Instanz ist diese Erweiterung sogar zwingend nötig.
Wir möchten ja gerne auf Klicks auf die verschiedenen Button reagieren. Dazu erstellen wir einen “onClickListener”. Dieser onClickListener stellt selber eine eigene Klasse dar. Dem onClickListener zu sagen, welche Funktion er beim Klick aufrufen soll (hier das Ausführen eines SSH Befehls) ist recht simpel. Ihm allerdings zu sagen, welchen Befehl er ausführen soll ist etwas schwieriger, da jedem Button/jeder Steckdose ein anderer Befehl zugeordnet ist.
Packen wir den onClickListener nun mit in die Klasse “Steckdose”, so kann dieser onClickListener für jedes eigenständige Objekt der Klasse “Steckdose” den spezifischen Befehl sehr einfach auslesen.

Stellt euch einfach vor eine Klasse ist wie ein Lego Bauplan.
Jede Klasse (Bauplan) kann ein eigenständiges Objekt (Lego Kran) erstellen.
Jedem einzelnen Kran fest einzuprogrammieren was er kann/soll ist einfacher, als einem einzigen Kran jedes mal neu zu sagen, was er kann/soll. (Das war jetzt eine sehr grobe Ausführung.)

Ihr erstellt diese Klasse, indem ihr in der linken Liste auf app->java->(Euer Package Name ohne Test oder ähnliches) geht und mit einem Rechtsklick „new“ und dann „Java class“ auswählt.
Gebt hier „Steckdose“ ein.

Ersetzt den Inhalt der Datei mit dem gleich folgenden Code, aber achtet drauf, dass ihr die Zeile oberste mit „package ?????;“ erhaltet!

import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
import android.view.View;
import android.widget.Button;

class Steckdose extends Button {

 public int id;
 public String name;
 public int status;
 public final Steckdose dieseKlasse;

 public Steckdose(Context context) {
 super(context);

 dieseKlasse = this;

 this.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {
 Log.e("Button", "Klicked");
 if (!Einstellungen.nutzername.equals("") && !Einstellungen.passwort.equals("") && !Einstellungen.ip.equals("")) {
 Log.e("Button", "If Passed");
 starteSSHBefehl(Einstellungen.nutzername, Einstellungen.passwort, Einstellungen.ip, Einstellungen.port, getSteckdosenbefehl(), false);
 dieseKlasse.setText(dieseKlasse.name + " - " + neuerStatus());
 aenderStatus();
 }
 }
 });
 }

 public String getSteckdosenbefehl(){

 int neuerStatus = neuerStatus();

 return Einstellungen.befehlPre + Einstellungen.passwort + Einstellungen.befehlPost + " " + id + " " + neuerStatus;
 }

 private void aenderStatus(){
 status = neuerStatus();
 }

 private int neuerStatus(){
 // Erstelle einen neuen Status, abhängig von dem aktuellen Status.
 // -1 = Unbekannter Status => Schalte die Steckdose ein
 // 0 = Steckdose aus => Schalte die Steckdose ein
 // 1 = Steckdose ein => Schalte die Steckdose aus
 int neuerStatus = 0;

 if(status == -1)
 neuerStatus = 1;
 else if (status == 0)
 neuerStatus = 1;
 else if (status == 1)
 neuerStatus = 0;

 return neuerStatus;
 }

 public static void starteSSHBefehl(final String nutzername, final String passwort, final String ip, final int port, final String befehl, final boolean statusAbfrage){

 // Hintergrundaufgabe erstellen
 new AsyncTask() {
 @Override
 protected Void doInBackground(Integer... params) {
 try {
 // String, in den die ganzen Zeilen Ausgabe geschrieben werden
 String ausgabe = "";

 Log.e("Button", "SSH Started");

 // Funktion ausführen
 MainActivity.sshBefehl(nutzername, passwort, ip, port, befehl);


 } catch (Exception e) {
 e.printStackTrace();
 }
 return null;
 }
 }.execute(1);

 }

}

Design erstellen:

Ein paar Fehler unserer MainActivity Klasse haben wir nun behoben.
Es gilt nun Designs zu erstellen/zu erweitern und Texte zu definieren.

Öffnet die Datei “activity_main”, welche im Ordner res->layout liegt und ersetzt sie durch folgenden Inhalt:

<!--?xml version="1.0" encoding="utf-8"?-->
<!--
 ~ Copyright 2017 www.staticfloat.de
 ~
 ~ Licensed under the Apache License, Version 2.0 (the „License“);
 ~ you may not use this file except in compliance with the License.
 ~ You may obtain a copy of the License at
 ~
 ~ http://www.apache.org/licenses/LICENSE-2.0
 ~
 ~ Unless required by applicable law or agreed to in writing, software
 ~ distributed under the License is distributed on an „AS IS“ BASIS,
 ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->

<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingbottom="@dimen/activity_vertical_margin" android:paddingleft="@dimen/activity_horizontal_margin" android:paddingright="@dimen/activity_horizontal_margin" android:paddingtop="@dimen/activity_vertical_margin" android:id="@+id/hauptfeld" android:orientation="vertical" tools:context="de.staticfloat.funksteckdose.MainActivity">

</linearlayout>


Anschließend erstellt ihr ebenfalls im Ordner res->layout, mittels Rechtsklick->New->Layout resource file eine Datei namens “activity_einstellungen” und ersetzt den Inhalt mit:

<!--?xml version="1.0" encoding="utf-8"?-->
<!--
 ~ Copyright 2017 www.staticfloat.de
 ~
 ~ Licensed under the Apache License, Version 2.0 (the „License“);
 ~ you may not use this file except in compliance with the License.
 ~ You may obtain a copy of the License at
 ~
 ~ http://www.apache.org/licenses/LICENSE-2.0
 ~
 ~ Unless required by applicable law or agreed to in writing, software
 ~ distributed under the License is distributed on an „AS IS“ BASIS,
 ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
 <textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:textappearance="?android:attr/textAppearanceSmall" android:text="Verbindungsdaten:" android:id="@+id/textView" android:layout_margin="10dp"></textview>
 <edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/edittext_ip" android:hint="IP-Adresse:" android:layout_marginleft="10dp" android:layout_marginright="10dp"></edittext>
 <edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:inputtype="number" android:ems="10" android:id="@+id/edittext_port" android:hint="Port: (Normalerweise 22)" android:layout_marginleft="10dp" android:layout_marginright="10dp"></edittext>
 <edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/edittext_nutzername" android:hint="Nutzername:" android:layout_marginleft="10dp" android:layout_marginright="10dp"></edittext>
 <edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:inputtype="textPassword" android:ems="10" android:id="@+id/edittext_passwort" android:hint="Passwort:" android:layout_marginright="10dp" android:layout_marginleft="10dp"></edittext>
 <button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Speichern" android:id="@+id/button_speichern" android:layout_gravity="right" android:layout_marginright="16dp" android:background="@color/colorPrimary" android:textcolor="@android:color/white" android:padding="10dp"></button>
</linearlayout>

Nun öffnet ihr die Datei “strings.xml” im Ordner res->values und ersetzt den Inhalt mit:

<resources>
 <string name="app_name">Funksteckdose</string>
 <string name="ok">Ok</string>
 <string name="ueber_text">Diese App wurde nach Anleitung von www.staticFloat.de erstellt und steht unter der Apache 2.0 Lizenz.</string>

 <!-- Texte für unser Menü -->
 <string name="menu_start">Startseite</string>
 <string name="menu_ueber">Über</string>
 <string name="menu_einstellungen">Einstellungen</string>
</resources>

Zu guter letzt müssen wir noch eine Datei erstellen, die das Aussehen unseres Menüs definiert.
Vorerst müssen wir jedoch einen neuen Ordner für diese Datei erstellen.

Klickt mit rechts auf “res”, wählt New->Directory und gebt dem Ordner den Namen “menu”.
In diesen Ordner erstellt ihr eine Datei, mittels Rechtsklick->New->Menu resource file eine Datei namens “actionbar_menu” und ersetzt den Inhalt mit:

<!--?xml version="1.0" encoding="utf-8"?-->
<!--
 ~ Copyright 2017 www.staticfloat.de
 ~
 ~ Licensed under the Apache License, Version 2.0 (the „License“);
 ~ you may not use this file except in compliance with the License.
 ~ You may obtain a copy of the License at
 ~
 ~ http://www.apache.org/licenses/LICENSE-2.0
 ~
 ~ Unless required by applicable law or agreed to in writing, software
 ~ distributed under the License is distributed on an „AS IS“ BASIS,
 ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
 <!-- Menüpunkt, der angezeigt wird, wenn auf die drei Punkte in der Actionbar geklickt wird. -->
 <item android:id="@+id/menu_start" android:title="@string/menu_start" app:showasaction="never"></item>
 <item android:id="@+id/menu_einstellungen" android:title="@string/menu_einstellungen" app:showasaction="never"></item>
 <item android:id="@+id/menu_ueber" android:title="@string/menu_ueber" app:showasaction="never"></item>
</menu>

Letzter Schritt:

Nach 5 Seiten Programmierung sind wir fast so weit.
Als letzte kleine Anpassung müssen wir unserer App nur noch erlauben auf das Internet zuzugreifen. Dies ist immerhin nötig, wenn wir innerhalb des lokalen Netzwerkes und SSH auf unseren Raspberry Pi zugreifen möchten.

Wir öffnen hierzu die Datei “AndroidManifest” im Ordner “manifest” und fügen diese Zeile zwischen “manifest” und “application" ein:

<uses-permission android:name="android.permission.INTERNET"></uses-permission>

Mir ist im Laufe der Programmierung aufgefallen, dass es vorkommen kann, dass unsere Actionbar innerhalb der gestarteten App nicht angezeigt wird.
Falls das passiert, dann löscht bitte aus der selben Datei die Zeile “android:theme=”@style/AppTheme”” heraus.

Kurze Hinweise:

Noch ein paar kurze Hinweise, bevor ich euch den kompletten Source Code zu diesem Projekt anbiete und ihr die App testet.

  1. Die App kann bisher keine lokalen Hostnamen auflösen, ihr müsst also eine IP-Adresse eures Pi oder Internetadresse eingeben. (“raspberrypi” als Hostname funktioniert nicht. “beispiel.de” als Hostname würde funktionieren.)
  2. Die App funktioniert ohne Routerkonfiguration nur mit W-Lan im heimischen Netzwerk. Für einen Zugriff von außen müsst ihr euren Router mittels DynDNS auf eine statische IP-Adresse einstellen und Port-Forwardning zu eurem Pi aktivieren. Es wäre dann hilfreich und notwendig eurem Pi eine statische lokale IP-Adresse zuzuweisen. (Es gibt hier genug Anleitungen im Netz und wenn ihr nett fragt, dann wird euch auch sicherlich auf tutorials-raspberrypi.de geholfen.)
  3. Da sich die IP-Adresse eures Pi ändern kann, könnte es sein, dass ihr die IP-Adresse auch in der App wieder ändern müsst. (Eine statische IP-Adresse eures Pi’s würde hier helfen.)
  4. Die App gibt keine Fehlermeldungen aus, falls eine Verbindung mal nicht herstellbar sein sollte.
  5. Die Anzahl und Namen der Steckdosen werden nicht gespeichert, ihr müsst also bei jedem Start im lokalen Netzwerk sein oder wie in Punkt 2 vorgehen und warten, bis die Button erscheinen.
  6. In diesem Artikel bin ich davon ausgegangen, dass das Programm (wie bei Tutorials-RaspberryPi.de beschrieben) im Ordner “/Funksteckdosen-RaspberryPi” des Home Ordners liegt. Sollte sich der Pfad bei euch unterscheiden, dann müsst ihr nur den Pfad in der Klasse “Einstellungen” ändern.
  7. Hinter jedem Namen auf dem Button wird eine Zahl anzeigt. Diese bedeuten:
    -1 => Status unbekannt;
    0 => Steckdose ist momentan aus;
    1 => Steckdose ist momentan an

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