Raspberry pi

4편 스마트 콘센트? Smart Plug! + 앱위젯

[혜안] 2017. 4. 18. 22:32
728x90

[Raspberry pi] - 1편 스마트 콘센트? Smart Plug!

[Raspberry pi] - 2편 스마트 콘센트? Smart Plug!

[Raspberry pi] - 3편 스마트 콘센트? Smart Plug!

[Raspberry pi] - 4편 스마트 콘센트? Smart Plug! + 앱위젯

[Raspberry pi] - 5편 스마트 콘센트? Smart Plug! + 터치스위치

[Raspberry pi] - 6편 스마트 콘센트? Smart Plug! + Node.js Push 서버 (개요편)

[Raspberry pi] - 7편 스마트 콘센트? Smart Plug! + Node.js Push 서버 (Firebase 등록)

[Raspberry pi] - 8편 스마트 콘센트? Smart Plug! + Node.js Push 서버 (서버편)

[Raspberry pi] - 9편 스마트 콘센트? Smart Plug! + Node.js Push 서버 (App편)




코드 몇줄짜리로 구현해 놓은 스마트콘센트(Smart Plug) 앱을 손보았다.

음성인식 앱에 위젯을 추가하여, 클릭 시마다 전원 on/off가 되고, 현재 상태를 알수 있도록 위젯 이미지가 바뀌도록 했다.


기능요약

1. 위젯을 바탕화면에 배치한다.

2. 위젯을 클릭하면 라즈베리파이(Raspberry Pi)에 전원 on/off를 요청한다.

3. 토글 방식으로 요청하고, 현재의 상태를 리턴받는다. 

4. 리턴받은 값(on/off)에 따라 이미지 버튼을 변경한다.

5. Update 주기마다 현재 상태를 요청하여 리턴받는다.

6. 리턴받은 값(on/off)에 따라 이미지 버튼을 변경한다.


구현

1. 매니패스트 수정

앱위젯은 UI가 달린 브로드캐스트 리시버이므로, 매니패스트에도 receiver로 추가된다.

여기에 받고자 하는 브로드캐스트 메시지를 정의하고, 앱 내에서 주고받으면 된다.

아래와 같이 PLUGTOG 이라는 커스텀 메시지를 정의했다.


<receiver
android:name=".MyAppWidget"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="com.viewise.home.voiceassistance.PLUGTOG" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/my_app_widget_info" />
</receiver>


2. onUpdate

onUpdate는 앱위젯이 배치되는 초기, 그리고 셋팅된 업데이트 주기마다 호출된다.

이때에 할 일은 매니패스트에 정의한 커스텀 메시지를 인텐트에 넣어서, 버튼에 등록해 놓는 것이다.

버튼을 클릭하면 커스텀 메시지가 브로드캐스팅 된다.

imgBtn은 버튼 위젯이 아니고, ImageView 이다. 


final static String ACTION_CLICK = "com.viewise.home.voiceassistance.PLUGTOG";

static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {

// Construct the RemoteViews object
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);

Intent in = new Intent(context, MyAppWidget.class);
in.setAction(ACTION_CLICK);
in.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
PendingIntent pending = PendingIntent.getBroadcast(context, appWidgetId, in, 0);
views.setOnClickPendingIntent(R.id.imgBtn, pending);

appWidgetManager.updateAppWidget(appWidgetId, views);

Log.d(TAG, "updateAppWidget.appWidgetId: " + String.valueOf(appWidgetId));
}

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
} }


3. onReceive

onReceive는 브로드캐스팅 된 메시지를 수신할 때에 호출된다.

이때에 발생한 메시지 중 UPDATE와 PLUGTOG 에 대해서만 반응하도록 했다.

UPDATE 시에는 라즈베리파이에 현재 콘센트 상태를 요청하여 받아오고, PLUGTOG 시에는 on/off 동작을 요청하고 현재 상태를 리턴받는다.

RequestPI는 스레드이고, 편의상 Http 요청을 하여 결과가 나올때까지 대기하도록 thread join을 시켰다.

Http 요청이 완료되면 isOn 이라는 전역변수에 값을 셋팅하고, 그 값을 읽어서 ImageView의 이미지소스를 on 또는 off 모양으로 바뀌도록 했다.


@Override
public void onReceive(Context context, Intent intent){

Log.d(TAG, intent.getAction());

RequestPI requestPi = null;
if(intent.getAction().equals(ACTION_CLICK)){
requestPi = new RequestPI(strPlugTog);
requestPi.start();
} else if(intent.getAction().equals(ACTION_UPDATE)) {
requestPi = new RequestPI(strPlugStat);
requestPi.start();
}

try {
if(requestPi != null)
requestPi.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

if(intent.getAction().equals(ACTION_CLICK) || intent.getAction().equals(ACTION_UPDATE)) {
Log.d(TAG, "onReceive.Icon : " + String.valueOf(isOn));

RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
if (isOn)
views.setImageViewResource(R.id.imgBtn, R.drawable.plugon);
else
views.setImageViewResource(R.id.imgBtn, R.drawable.plugoff);

AppWidgetManager manager = AppWidgetManager.getInstance(context);
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);

if(intent.getAction().equals(ACTION_UPDATE)) {//위젯 내 모든 컨트롤 업데이트
int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(context, getClass()));
for(int id:appWidgetIds) {
Log.d(TAG, "onReceive.appWidgetId : " + String.valueOf(id));
//updateAppWidget(context, manager, appWidgetId);
manager.updateAppWidget(id, views);//ImageViewResource 변경 UI반영을 위해
}
}else {
Log.d(TAG, "onReceive.appWidgetId : " + String.valueOf(appWidgetId));
//updateAppWidget(context, manager, appWidgetId);
manager.updateAppWidget(appWidgetId, views);//ImageViewResource 변경 UI반영을 위해
}
}
super.onReceive(context, intent);
}


4. RequestPI

RequestPI는 JSON 타입으로 Http 요청을 하고 결과를 리턴받는 스레드이다. 

여기서는 간단히 result:on 또는 off를 받는다.

private class RequestPI extends Thread {
private String strUrl;

public RequestPI(String value) {
this.strUrl = value;
}

public void run() {
HttpURLConnection conn = null;
try {
URL url = new URL(strUrl);
url.openConnection();
conn = (HttpURLConnection)url.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));

String strJson = in.readLine();
in.close();
Log.d(TAG, strJson);

try {
JSONObject json = new JSONArray(strJson).getJSONObject(0);
String rt = json.getString("result");
if(rt.equals("on"))
isOn = true;
else
isOn = false;

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

} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(conn != null)
conn.disconnect();
}
} };


5. 웹 서버

라즈베리파이(Raspberry Pi)에 구현된 node.js 웹서버는 그동안 여러 기능의 추가/수정이 있었다.

인터넷에서 소스를 찾아서 copy한 단순한 소스가 점점 복잡해지고 있는 관계로, 콘센트 기능 부분만 발췌했다.

PLUGTOG 발생 시 app.get('/plugtoggle', function(req, res)) 부분이 호출되고, 현재 상태를 읽어서 콘센트가 on 이면 off를, off 이면 on을 시킨다.

그리고 JSON 타입으로 result:on 또는 result:off 를 리턴한다.

UPDATE 발생 시 app.get('/plugstat', function(req, res)) 부분이 호출된다.

이후 동작은 콘센트 제어 없이 상태만 리턴한다.


app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended : false }));

app.get('/plug', function(req, res){
res.sendFile('/html/plug.html', {root : __dirname });
});

app.post('/plugdata', function(req, res){
var state = req.body.switch;
if (state == 'ON') {
plug.writeSync(0);
}
else {
plug.writeSync(1);
}
console.log(state + " " + plug.readSync());
res.sendFile('/html/plug.html', {root : __dirname });
});

app.get('/plugon', function(req, res) {
plug.writeSync(0);
res.send([{result:'ok'}]);
console.log('on ' + plug.readSync());
});

app.get('/plugoff', function(req, res) {
plug.writeSync(1);
res.send([{result:'ok'}]);
console.log('off '+ plug.readSync());
});

app.get('/plugtoggle', function(req, res) {
if(plug.readSync() == 1) {
plug.writeSync(0);
res.send([{result:'on'}]);
}
else {
plug.writeSync(1);
res.send([{result:'off'}]);
}
});

app.get('/plugstat', function(req, res) {
if(plug.readSync() == 1) {
res.send([{result:'off'}]);
}
else {
res.send([{result:'on'}]);
}
});


6. 동작영상


728x90