見出し画像

外出先から自宅の玄関鍵を制御

前回、RaspberryPiとWebixライブラリを使って、スマホで玄関の鍵をコントロールする事例を紹介しましたが、前回の実装例では、自宅のWifiが届く範囲でしか玄関の鍵の状態確認や施錠・解錠ができない方式でした。やっぱり、自宅から離れた場所で、自宅の玄関の鍵状態を確認したり、施錠や解錠ができることが望ましいし、便利ですよね。今回は、外出先から操作できる環境を構築しましたので、紹介します。自宅内に設置しているRaspberryPiを外出先からコントロールするには、いくつかの方式がありますが、できるだけスマホ操作を簡単にしたいこと(URLを知っていれば、自宅の鍵の状態をすぐに確認できる環境)こともあり、VPNなどを使わないで、操作することにしました。そのためには、レンタルサーバなど、固定IPでかつ、固定のドメイン名で指定したURLでスマホからアクセスできる環境が必要となります。今回は、さくらインターネットのレンタルサーバを使って、そのサーバを踏み台のようにつかい、実現しました。
自宅のRaspberryPiから、さくらインターネットの指定URLにREST API方式で通信をして、定期的に鍵の状態をレンタルサーバに通知します。スマホは、レンタルサーバ上の鍵の状態を読み取ることで、自宅の鍵の状態を確認できます。さらに、スマホからは、鍵の操作情報をレンタルサーバにセットし、そのセットした情報を周期的に鍵の状態を書き込む通信の応答にのせることで、RaspberryPiで鍵に対する操作情報を受信し、実際の鍵操作が実現できます。レンタルサーバを経由して鍵操作情報を伝える方式のため、少し操作に時間はかかりますが(数秒)、何度も操作をしないことから実用的には問題ないと判断しています。
トライアル環境では、RaspberryPiからレンタルサーバには、3秒周期で通信(鍵情報を送信)し、レンタルサーバのファイル情報を更新します。スマホは、そのDB情報を読み出すことで、現在の鍵状態を確認できます。また、スマホからの鍵制御情報(解錠するか、施錠するか)も、レンタルサーバのDBに保存し、RaspberryPiからの鍵情報をセットするロジックの中で、鍵制御情報がDB上にあれば、その情報を応答にのせ、次の鍵情報更新処理を待って、指定した鍵制御で状態が変化したことを確認後に、スマホに応答を返します。
文章で動作を記述しただけでは、少しわかりにくいかもしれませんね。
RaspberryPiの周期処理プログラムは、Ruby言語で記述しました。レンタルサーバに対するREST操作を簡単に記述できることやJSON形式の応答を連想配列に変換するもの簡単で、鍵の制御だけPyrhonで記述しています。
rubyからPythonをコマンドベースで実行し、応答情報を変数に取り込む実装です。RaspberryPiがBootしたタイミングで処理プログラムを自動起動しています。3秒周期で動作していることを外部から確認しやすいように青LEDを点灯するソースも記述しました。
レンタルサーバ上は、PHP言語でファイル操作などを実装し、スマホ向けの画面はWebixライブラリで実装しています。尚、鍵制御のセキュリティを考慮して、制御時には、パスコードも必要とする実装とし、かつ、パスコード入力用のテンキー画面も実装しました。
また、解錠状態を把握しやすいように文字だけでなく、背景色をピンク色にして、鍵が解錠されているとには、認識しやすい画面にしています。
以下の画面は、解錠中の状態を表示しています。

パスコード入力時は、画面の下に、テンキーを表示

また、解錠または施錠動作時には、少し時間がかかるので、プログレスバーとして、くるくる回るアイコン表示にしています。

スマホ向けのソースコードです。(一部、セキュリティ面を考慮し、カットしているソースコードがあります)

<?php

    $VER_INFO ="V01L01";
    $myfilename = basename(__FILE__);   //自分自身のファイル名取得
    $userid = 'admin';
    if(isset($_GET['userid'])){
        $userid = $_GET['userid'];
    }
    $code = "";
    if(isset($_GET['code'])){
        $code = $_GET['code'];
    }
	include('../../commonlib/svr_common_lib_v2.php');	//

    $logheader = 'userid='.$userid.', '.$myfilename.':';//ログ出力時のヘッダー情報(自ファイル名,ログインIDを付与)
    error_log($logheader.' start '.$myfilename);


	$config_obj = get_config_obj();
	$define_code = $config_obj::get('app01.define_code');

	if($code != $define_code){
	  error_log($logheader.'  code miss');
	  header("HTTP/1.0 404 Not Found");
	  return;
	}
?>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
 	    <link rel="stylesheet" href="/webix_GPL_1020/css/materialdesignicons.min.css" type="text/css" charset="utf-8">
        <link rel="stylesheet" href="/webix_GPL_1020/webix.css" type="text/css"   charset="utf-8">
        <script src="/webix_GPL_1020/webix.js"></script>
        <link href="/webix_GPL_1020/skins/compact.css?<?php echo date('Ymd-H'); ?>" rel="stylesheet" type="text/css">    
        <link rel="icon" href="<?php  echo SUB_FOLDER; ?>/image/webix_64.ico">
        <script src="<?php  echo SUB_FOLDER; ?>/commonlib/moment-with-locales.js"></script>
        <title><?php echo $TITLE_INFO.' ('.$VER_INFO.')' ?></title>
    <style>
    .pink_text input{
     background: #ffc0cb;
    }
    </style>
    </head>
<body>
<script type="text/javascript" charset="utf-8">
webix.i18n.setLocale("ja-JP");
var userid = '<?php echo $userid; ?>';


webix.ui.fullScreen();


function get_key_lock_status(mess_mode){
    var send_prm = {};
    send_prm.userid = userid;
    var xhr =webix.ajax().sync().get("<?php  echo SUB_FOLDER; ?>/rest_api/HM0010/HM0011_get_key_lock_status.php",send_prm);
    var resp = xhr.responseText;
    var resp_info = JSON.parse(resp);
    var resp = resp_info.resp;
    if(resp == "ok"){
    	$$("key_status").setValue(resp_info.key_status_jp);
    	if(resp_info.key_status == "lock"){
    		$$("action_btn").setValue("解錠");
    		webix.html.removeCss($$("key_status").getNode(), "pink_text");
    	}
    	else if(resp_info.key_status == "unlock"){
    		$$("action_btn").setValue("施錠");
    		webix.html.addCss($$("key_status").getNode(), "pink_text");
    	}
    	else{
    		$$("action_btn").hide();
    	}
    	if(mess_mode){
    		webix.message({type:"success",text:"読み出しました。"});
    	}
	}
	else{
    	$$("key_status").setValue("不明");
    	webix.html.addCss($$("key_status").getNode(), "pink_text");
    	if(mess_mode){
    		webix.alert("鍵の状態確認で<br>エラーしました。");
    	}
	}
	$$("key_status").refresh();
}

//画面の初期化
webix.ui(
    {view:"form",
    id:"HM0010_form",
    name:"HM0010_form",
    padding: 10,
    rows:[
        {view:"label", template:"<span style='font-weight:bold; font-size:180%;'>鍵の状態と設定 HM0010 V1.01</span>"},
        { margin:5, 
            cols:[
              {view:"text", label:"状態",  value:"",labelWidth:100, width:200,labelAlign:"right",readonly:true,id:"key_status",name:"key_staus"},
                {view:"button",label: "再確認", id:"read_btn",name:"read_btn", width: 100
                    ,click:function(){
                    	passcode_form_win.hide();
                    	get_key_lock_status(true);
                    }
                }
            ]
        },
        {view:"label"},
        { margin:5, 
            cols:[
               {view:"text", label:"パスコード",  value:"",labelWidth:100, width:200,labelAlign:"right",id:"pass_code",name:"pass_code",readonly:true},
               {view:"button",value: "セット", id:"action_btn",name:"action_btn", width: 100, css:"webix_danger"
                    ,click:function(){
                    	passcode_form_win.hide();
                    	var pass_code = $$("pass_code").getValue();
                    	if(pass_code == ""){
                    		webix.alert("鍵操作時は、<br>パスコードを<br>指定してください。");
                    		return;
                    	}
                    	var key_mode = "lock";
                    	var key_status= $$("key_status").getValue();
                    	if(key_status == "施錠中"){
                    		key_mode = "unlock";
                    	}
                    	else if(key_status == "解錠中"){
                    		key_mode = "lock";
                    	}
                  	   	$$("action_btn").disable();
                  	   	$$("action_btn").refresh();
                  	   	
                    	$$("HM0010_form").showProgress({ hide:false});
                    	webix.delay(function(){
                           	var formData = new FormData();
                           	formData.append("userid", userid);
                           	formData.append("code",pass_code);
                           	formData.append("key_mode",key_mode);
                        
                           	var xhr =webix.ajax().sync().post("<?php  echo SUB_FOLDER; ?>/rest_api/HM0010/HM0013_key_action.php",formData);
                           	var resp =  JSON.parse(xhr.responseText);
                           	$$("HM0010_form").showProgress({hide:true});
                           	$$("action_btn").enable();
                           	$$("action_btn").refresh();

                        	if(resp.resp =="ok"){
                           		$$("key_status").setValue(resp.key_status_jp);
                           		if(resp.key_status == "lock"){
    					   			$$("action_btn").setValue("解錠");
    								webix.html.removeCss($$("key_status").getNode(), "pink_text");
                           		}
                           		else if(resp.key_status == "unlock"){
    					   			$$("action_btn").setValue("施錠");
    								webix.html.addCss($$("key_status").getNode(), "pink_text");
                           		}
                           		else{
     					   			$$("action_btn").hide();
                       	   		}
                           		webix.message({type:"success",text:"操作しました。"});
                               return;
                           }
                           else if(resp.error_code == -1){
                           		webix.alert("パスコードが<br>正しくありません。<br>code="+resp.error_code);
                               	return;
                           }
                           else{
                           	  	$$("key_status").setValue("不明");
    							webix.html.addCss($$("key_status").getNode(), "pink_text");
                           	  	webix.alert("鍵の状態変更で<br>エラーしました。<br>code="+resp.error_code);
                              	return;
                           }
                           $$("key_status").refresh();
                        },null, null, 100);
                    }
                },
            ]
        },
        {view:"label"},
        { margin:5, 
            cols:[
              {width:200},
              {view:"button",label: "閉じる", id:"close_btn",name:"close_btn", width: 100, css:"webix_primary"
                    ,click:function(){
                    	passcode_form_win.hide();
                    	location.href = 'https://www.google.com/?hl=ja';	//google画面にジャンプ
                    }
                }
            ]
        },
        ]
});

$$("pass_code").attachEvent("onItemClick", function(id, e){
    passcode_form_win.show();
});

	var passcode_form_collection = [
		{ margin:8,rows: [
			{  margin:15,cols:[
				{view:"button", value: "1", width: 40,id: "btn_1",
					click:function(){
						$$("pass_code").setValue($$("pass_code").getValue()+"1");
					}
				},
				{view:"button", value: "2", width: 40,id: "btn_2",
					click:function(){
						$$("pass_code").setValue($$("pass_code").getValue()+"2");
					}
				},
				{view:"button", value: "3", width: 40,id: "btn_3",
					click:function(){
						$$("pass_code").setValue($$("pass_code").getValue()+"3");
					}
				},
				{view:"button", value: "C", width: 40,id: "btn_clr",css:"webix_danger",
					click:function(){
						$$("pass_code").setValue("");
					}
				}
				]
			},
			{ margin:15,cols:[
				{view:"button", value: "4", width: 40,id: "btn_4",
					click:function(){
						$$("pass_code").setValue($$("pass_code").getValue()+"4");
					}
				},
				{view:"button", value: "5", width: 40,id: "btn_5",
					click:function(){
						$$("pass_code").setValue($$("pass_code").getValue()+"5");
					}
				},
				{view:"button", value: "6", width: 40,id: "btn_6",
					click:function(){
						$$("pass_code").setValue($$("pass_code").getValue()+"6");
					}
				},
				{view:"button", value: "<", width: 40,id: "btn_bk", css:"webix_primary",
					click:function(){
						var str_tmp = $$("pass_code").getValue();
						$$("pass_code").setValue(str_tmp.substr(0,str_tmp.length  -1));
					}
				}
				]
			},
			{  margin:15,cols:[
				{view:"button", value: "7", width: 40,id: "btn_7",
					click:function(){
						$$("pass_code").setValue($$("pass_code").getValue()+"7");
					}
				},
				{view:"button", value: "8", width: 40,id: "btn_8",
					click:function(){
						$$("pass_code").setValue($$("pass_code").getValue()+"8");
					}
				},
				{view:"button", value: "9", width: 40,id: "btn_9",
					click:function(){
						$$("pass_code").setValue($$("pass_code").getValue()+"9");
					}
				},
				]
			},
			{  margin:15,cols:[
				{view:"button", value: "0", width: 95,id: "btn_0",
					click:function(){
						$$("pass_code").setValue($$("pass_code").getValue()+"0");
					}
				},
				{view:"button", value: "Close", width: 95,id: "btn_ent", css:"webix_primary",
					click:function(){
						passcode_form_win.hide();
					}
				}
				]
			}
		]
		}
	];

var passcode_form_win = webix.ui({
   	view:"window", 
	id: "passcode_form_win",
	head:"パスコード", 
	left:50, top:300,
	width:260,
	height:300,
  	body:{
		rows: [
			{cols:[
				{
					view:"form",	//作成されるエレメントの種類
					id: "pass_code_entry",	//参照用のID情報
					scroll:false,	//構成画面のスクロール設定
					elements:passcode_form_collection,	//フォームの構成要素を指定
					width:260,	//フォームの横幅
				}
        	]}
		]
    }
});




var curr_key_status = "unlock";
if ( curr_key_status == "lock"){
	$$("key_status").setValue("施錠中");
    $$("action_btn").setValue("解錠");
}
else if( curr_key_status == "unlock"){
	$$("key_status").setValue("解錠中");
    $$("action_btn").setValue("施錠");
}
else{
  $$("action_btn").hide();
}
$$("pass_code").setValue("");
// プログレスバーの設定
webix.extend($$("HM0010_form"), webix.ProgressBar);



get_key_lock_status();

</script>
</body>
</html>

以下は、Rubyのソースコードです。(一部、記述を修正、カットしています)

# timer_exec.rb
#文字コードUTF-8
require 'logger'
require 'timers'
require 'date'
require "net/http"
require "json"

# DIP SW1  ON:Blue LED ON OFF:none
# DIP SW2  ON:apl exit OFF:nomal
# DIP SW3
# DIP SW4  ON:shutdown mode OFF:Normal mode

 #GPIO INPUTモードに設定
in_mode_cmd12 = `raspi-gpio set 12 ip`
in_mode_cmd25 = `raspi-gpio set 25 ip`
in_mode_cmd24 = `raspi-gpio set 24 ip`
in_mode_cmd23 = `raspi-gpio set 23 ip`
in_mode_cmd22 = `raspi-gpio set 22 ip`

 #GPIO プルアップに設定
pullup_cmd12 = `raspi-gpio set 12 pu`
pullup_cmd25 = `raspi-gpio set 25 pu`
pullup_cmd24 = `raspi-gpio set 24 pu`
pullup_cmd23 = `raspi-gpio set 23 pu`
pullup_cmd22 = `raspi-gpio set 22 pu`


outmode_cmd17 = `raspi-gpio set 17 op`
outmode_cmd18 = `raspi-gpio set 18 op`
outmode_cmd27 = `raspi-gpio set 27 op`



#読み取った結果からレベル情報を取り出して返す
#  #GPIO 25: level=1 func=INPUT
# 結果は、"0" or "1"
def get_level_info(cmd_resp)
   resp_array = cmd_resp.split
   result = resp_array[2].split("=")
   return result[1]
end


logfilename = '/home/pi/work/timer_exec_log.log'
logger = Logger.new(logfilename,5,100000)
logger.level  = Logger::INFO

logger.info('timer_exec.rb ver 1.01 start')


#LED All OFF
led_blue_off_cmd17 = `raspi-gpio set 17 dl`
led_green_off_cmd18 = `raspi-gpio set 18 dl`
led_red_off_cmd27 = `raspi-gpio set 27 dl`
sleep 0.5

read_cmd24 = `raspi-gpio get 24` 
sw2 = get_level_info(read_cmd24)
if sw2 == "0" then
    logger.info('sw2 on then this apl exit')
	led_red_on_cmd27 = `raspi-gpio set 27 dh`
	exit
end


timers = Timers::Group.new

sw5_state = 0
shutdown_state = 0
read_key_state2 = ''
curr_key_state = ''
unlock_count = 0

#周期処理
timers.every(3) do	#3sec単位に実施
  read_cmd25 = `raspi-gpio get 25` 
  sw1 = get_level_info(read_cmd25)
  read_cmd24 = `raspi-gpio get 24` 
  sw2 = get_level_info(read_cmd24)
  read_cmd23 = `raspi-gpio get 23` 
  sw3 = get_level_info(read_cmd23)
  read_cmd22 = `raspi-gpio get 22` 
  sw4 = get_level_info(read_cmd22)
  
  read_cmd12 = `raspi-gpio get 12` 
  sw5 = get_level_info(read_cmd12)

  read_key_state = `sudo python3 /home/pi/work/read_key_status.py`  #lock or unlock
  read_key_state = read_key_state.chomp

  if read_key_state!= curr_key_state then
     logger.info('key state is '+read_key_state)
     curr_key_state = read_key_state
  end

  uri = URI.parse("https://xxxx.jp/yyyyy/rest_api/HM0020/HM0022_set_key_info.php?userid=admin&code=zzzzzzzz&key_status="+curr_key_state)
  response = Net::HTTP.get_response(uri)

  if response.code == "200" then # status code
     resp_array = JSON.parse(response.body)
     if resp_array["resp"] == "ok" then
         key_action = resp_array["key_action"]
         if key_action != "" then
            logger.info('key_action='+key_action)
            #key action start
            led_grenn_on_cmd18 = `raspi-gpio set 18 dh`
            if key_action == "lock" then
               read_key_state3 = `sudo python3 /home/pi/work/home_key_action.py lock` #鍵締錠
               read_key_state3 = read_key_state2.chomp
            else
               read_key_state3 = `sudo python3 /home/pi/work/home_key_action.py unlock` #鍵解錠
               read_key_state3 = read_key_state2.chomp
            end
            curr_key_state = key_action
            led_grenn_on_cmd18 = `raspi-gpio set 18 dl`
         end
     else
         logger.info('https resp error code='+resp_array["error_code"])
     end
  else
     logger.info('https get error code='+response.code)
  end 


  if curr_key_state == "unlock" then
     unlock_count += 1
     if unlock_count > 180 && unlock_count.div(30) == 0 then
         logger.info('unlock_count is '+unlock_count.to_s)
     end
  elsif curr_key_state == "lock" then
     unlock_count = 0
  end

  if sw1 == "0" then
     #LED blue on/off
     led_blue_on_cmd17 = `raspi-gpio set 17 dh`
  end

  if sw5 == "0" && sw5_state == 0 then
     #LED grenn on/off
     led_grenn_on_cmd18 = `raspi-gpio set 18 dh`
     sw5_state = 1
     logger.info('sw5 is on key_state=' +read_key_state)
     if read_key_state == "lock" then
         read_key_state2 = `sudo python3 /home/pi/work/home_key_action.py unlock` #鍵解錠
         read_key_state2 = read_key_state2.chomp
     else
         read_key_state2 = `sudo python3 /home/pi/work/home_key_action.py lock` #鍵締錠
         read_key_state2 = read_key_state2.chomp
     end
     logger.info('sw5 is on after key_state=' +read_key_state2)
  elsif sw5 == "1" && sw5_state == 1 then
     sw5_state = 0
     logger.info('sw5 is off key_state='+read_key_state)
  end

  if sw4 == "0" && shutdown_state == 0 then
  	#shutdown command on
    logger.info('sw4 is on then shutdown')
	led_red_on_cmd27 = `raspi-gpio set 27 dh`
    resp = `sudo shutdown -h now`
    sleep 10
  end

  sleep 0.2  #0.2sec sleep

  if sw1 == "0" then
     #LED blue on/off
     led_blue_off_cmd17 = `raspi-gpio set 17 dl`
  end
  if sw5 == "0" then
     #LED grenn on/off
     led_grenn_on_cmd18 = `raspi-gpio set 18 dl`
  end

end

loop { timers.wait }

上記以外に、いくつかのREST_API用のPHPソースコードが必要です。
今回の事例は、実際に鍵操作ができる環境のため、一部のソースコードのみ紹介しています。スマホで自宅の鍵を把握できる環境を構築でき、鍵を忘れても大丈夫な環境ができました。参考にしてみてください。

この記事が気に入ったらサポートをしてみませんか?