python」カテゴリーアーカイブ

古いAndroidとラズパイで多機能モニターを作る

目的

以前作った店の呼び出しブザーは押されたらline&メール通知を飛ばすようになっている。ただし、押した側からみるとなんの反応も無いため動作しているか不安。なので、googleクラウドで合成させた音声を古いAndroidで流すようにした。(+画面でメッセージ表示)

なお、この記事を書いている段階ではエミュレータで動いたのみ。実機は古すぎて現行のAndroidStudioで素直にビルドできていない。ビルドする方法を調べてから再挑戦。

環境

呼出側

開発環境

  • OS Centos7
  • python 3.7.0
  • the Cloud Client Libraries for the Cloud Text-to-Speech API のアルファ版(ドキュメントが古いだけかも)

想定環境

  • raspberry pi typeB
  • python 3.7.0
  • the Cloud Client Libraries for the Cloud Text-to-Speech API のアルファ版(ドキュメントが古いだけかも)

触った限り、本番環境用のraspberry pi typeBで動作はするもののCloud Text-to-Speech APIの呼出が重い。10文字程度で3秒ほど。gRPCの接続/暗号化処理周りでcpuがいっぱいいっぱいになっている模様で端末能力の問題と思う。

呼び出される側(android)

開発環境

  • android studio 3.3.1
  • SHL22
  • Androidバージョン 4.2.2
  • compileSdkVersion/targetSdkVersion 28
  • minSdkVersion 16 (開発端末がこの時代の)
  • com.nanohttpd:nanohttpd-webserver:2.1.1

想定環境

  • IS11PT
  • Androidバージョン2.3.4

端末が古すぎるので開発環境を簡単に整えられなかったので、まずは新しめ(といっても8年ぐらい前)の端末で作成。

呼出側機能

概要

googleのText-to-Speech API を呼び出して、requestでAndroid内に起動させているnanohttpsにpostをするのみ。Text-to-Speech APIはほぼドキュメントどおり。requestのファイルのところはパラメータにMIMEタイプまでしていしてやらないとAndroid側で使っっているnanohttpdでファイルをよみ込めなかったので注意。

ソース

#!/usr/bin/env python3
# coding: utf-8
import requests

from google.cloud import texttospeech

def make_voice(current_time):
    _make_voice(current_time,"少々お待ちください")

def _make_voice(current_time,msg):
    file_name = 'output.mp3'
    client = texttospeech.TextToSpeechClient()
    synthesis_input = texttospeech.types.SynthesisInput(text=msg)

    voice = texttospeech.types.VoiceSelectionParams(
        language_code='ja-JP',
        ssml_gender=texttospeech.enums.SsmlVoiceGender.NEUTRAL)
    audio_config = texttospeech.types.AudioConfig(
        audio_encoding=texttospeech.enums.AudioEncoding.MP3)

    response = client.synthesize_speech(synthesis_input, voice, audio_config)

    with open('output.mp3', 'wb') as out:
        out.write(response.audio_content)

    url = 'http://127.0.0.1:8081/msg/'
    files = {'file': (file_name,open(file_name, 'rb'),'audio/mpeg')}
    payload = {"message" :  msg}
    r = requests.post(url ,files=files ,params=payload)
    print(r)
if __name__ == '__main__':
    make_voice( '2019/02/12 固定' )

呼び出される側

概要

内部で立ち上げているnanohttpdで情報(リクエスト)を受け取って、音声再生し画面にメッセージを出しておく。ちなみに、古いバージョンで動かそうとしてたので文法が古いものから新しいものまで混在しているので適宜読み替えてください。

package jp.co.epea.monitor;

import android.media.MediaPlayer;
import android.os.Handler;
import android.widget.TextView;


import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import fi.iki.elonen.NanoHTTPD;

import static fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND;
import static fi.iki.elonen.NanoHTTPD.Response.Status.OK;

public class MoniterServer extends NanoHTTPD {
    private Handler handler;
    private TextView textView;

    public MoniterServer(Handler handler, TextView textView) {
        super(8080);
        this.handler = handler;
        this.textView = textView;

    }

    @Override
    public Response serve(IHTTPSession session) {
        Method method = session.getMethod();
        String uri = session.getUri();

        System.out.println(method + " '" + uri + "' ");
        if (uri.equals("/msg/")) {
            final String msg = session.getParms().get("message");


            Map<String, String> files = new HashMap<>();
            try {
                session.parseBody(files);
            } catch (IOException ioe) {
                return new NanoHTTPD.Response(NOT_FOUND, MIME_HTML, "none");
            } catch (ResponseException re) {
                return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
            }

            MediaPlayer player = new MediaPlayer();
            try {
                player.setDataSource(files.get("file"));
                player.prepare();
                player.start();
            } catch (IOException e) {
                e.printStackTrace();
            }

            handler.post(new Runnable() {
                @Override
                public void run() {
                    textView.setText(msg);
                }
            });
            return new NanoHTTPD.Response(OK, MIME_HTML, "ok");
        } else if (uri.equals("/clear/")) {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    textView.setText("");
                }
            });
            return new NanoHTTPD.Response(OK, MIME_HTML, "ok");
        }
        return new NanoHTTPD.Response(NOT_FOUND, MIME_HTML, "none");
    }
}

いつもどおり関係ないけど

そういえば嫁のサイトをazureにも置きました

lambdaとRoute53を使ってDDNS機能を作った

概要

固定IPを振っていない店用にlambdaとRoute53を使ってDDNS機能を作った

背景

この間、店の呼び出しブザーを作ったけど死活監視をしていない。zabbixエージェントを入れようとしたけど店には固定IPが来ていないのでDDNSなり(VPNを貼るなり)しないといけない。

システム

環境

  • python3.7.0
  • raspbian9.4
  • Raspberry Pi B+
  • API Gateway
  • Lambda
  • Route53

システム概要

店のネットワーク内にあるラズパイからAPI Gateway経由でLambdaを呼出。

呼び出されたLambdaで呼び出し元(ラズパイ)のグローバルIPを取得。

今回のグローバルIPが前回のグローバルIPと違ったらLambda内でbotoを使ってRoute53のレコードを変更

lambdaのソース

ポン置きのラズパイから起動しているのでセキュリティ的に不安。なので、対象サーバとかzoneIdは引数でなくlambda側で持っている。
対象のAレコードなかったりしたら動かないけどログ見たらなんとかなるはず。

import boto3
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
        ZONE_ID = 'Route53のHosted Zone ID'
        logger.debug('call lambda')
        source_ip = event['source_ip']
        original_ip = event['original_ip']

        logger.debug( 'source_ip -> ' + source_ip)
        logger.debug( 'original_ip -> ' + original_ip)


        if original_ip == '' or original_ip != source_ip:
            logger.info('original_ip != source_ip')
            logger.info( 'source_ip -> ' + source_ip)
            logger.info( 'original_ip -> ' + original_ip)
            client = boto3.client('route53')

            try:
                response = client.list_resource_record_sets(HostedZoneId=ZONE_ID)
                target = [item for item in response['ResourceRecordSets'] if item['Name'] == 'hogehoge.epea.co.jp.' and item['Type'] == 'A'][0]
                logger.info(target)

                setting_ip = target['ResourceRecords'][0]['Value']

                if setting_ip != source_ip:
                    logger.info('modify start')
                    target['ResourceRecords'][0]['Value'] = source_ip

                    client.change_resource_record_sets(
                        HostedZoneId = ZONE_ID,
                        ChangeBatch = {
                            'Comment': '多分IPかわった',
                            'Changes': [{
                                'Action': 'UPSERT',
                                'ResourceRecordSet':target
                                }]
                            }
                    )
                    logger.info('modify finish')
            except Exception as e:
                logger.error('KOKODESUKOKODESU')
                import traceback
                traceback.print_exc()
                raise Exception("Check CloudWatch")
        return {
            'statusCode': 200,
            'body': source_ip
        }

全体の呼び出し元

Loopしながら呼び出し続けるのみ。

#!/usr/bin/env python3
# coding: utf-8
import json
import logging
import time
import os
import signal
import sys

import requests

def invoker(originalip):
    logger.debug('invocker start')
    logger.debug( 'original_ip -> ' + original_ip)

    headers = {'Content-Type' : 'application/json','x-api-key': ddns_token}
    payload = {'original_ip': original_ip}
    res = requests.post('https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/default/ddns'
        , data=json.dumps(payload)
        , headers=headers)
    if res.status_code != 200:
        print(res.text)
        print(res.status_code)
        raise Exception("TODO")

    logger.debug('res body ' + res.json()['body'])
    logger.debug('invocker finish')
    return res.json()['body']

def handler(signal, frame):
    logger.info('invocker stop')
    sys.exit(0)

signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGTERM, handler)

try:
    formatter = '%(levelname)s : %(asctime)s : %(message)s'
    logging.basicConfig(level = logging.INFO, filename = 'ddns.log', format=formatter)
except:
    print >> sys.stderr, 'error: could not open log file'
    sys.exit(1)
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

ddns_token = os.environ['DDNS_TOKEN']
original_ip = ''
logger.info('invocker start')
logger.debug('ddns_token ->[' + ddns_token + ']')
while True:
    logger.debug('in main loop')
    original_ip = invoker(original_ip)
    time.sleep(900)

権限

ラムダ作った時に作られる権限の他にRoute53のレコード参照/操作権限を付与

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "route53:ChangeResourceRecordSets",
                "route53:ListResourceRecordSets"
            ],
            "Resource": [
                "arn:aws:route53:::change/hostedzone/Route53のHosted Zone ID",
                "arn:aws:route53:::hostedzone/Route53のHosted Zone ID"
            ]
        }
    ]
}

API Gatewayの設定

リクエストのマッピング

#set ($body = $util.parseJson($input.json('$')))
{
   "original_ip" : "$body.original_ip",
   "source_ip" : "$context.identity.sourceIp"    
}

エラー時のマッピング

正規表現(でなくそのままだけど) “Check CloudWatch”でメソッドレスポンスのステータスを500に指定

systemd

特にコメントなし

[Unit]
Description=DDNS Daemon

[Service]
EnvironmentFile=/home/pi/.config/environment.d/ddns.conf
WorkingDirectory=/home/pi/develop/ddns/
ExecStart=/home/pi/develop/ddns/invoke_ddns.py
ExecStop=/bin/kill ${MAINPID}
Restart=always
Type=simple
User=pi
Group=pi

[Install]
WantedBy=multi-user.target

それはそうとボルログに店の情報のせてもらったけど今はほとんど更新されてないのね。。

Raspberry Piで作った玄関の呼び出しブザーにLine通知も付けてみた

概要

このまえqiitaに書いたメール通知ができる玄関の呼び出しブザーにLine通知も付けてみたのでそのメモ。

環境

python3.7.0
raspbian9.4
Raspberry Pi B+

プログラム

事前準備

requestsモジュールを使うのでインストール

pip install requests

ソース

ほぼこちらのとおり。他に指定できるものはドキュメント参照。

LINE_TOKENは環境変数から取得。引数のcurrent_timeは呼び出し元でフォーマット済みの文字列

(line_notify.py)

#!/usr/bin/env python3
# coding: utf-8
import os

import requests

def line_notify(current_time):
    url = 'https://notify-api.line.me/api/notify'
    token = os.environ['LINE_TOKEN']
    headers = {"Authorization" : "Bearer "+ token}

    message =  '呼出ブザーが押されました %s' % current_time
    payload = {"message" :  message}

    r = requests.post(url ,headers = headers ,params=payload)

if __name__ == '__main__':
    line_notify( '2018/11/12 固定' )

他のソース/設定ファイル

コントローラー

(control.py)

前の記事ではwait_for_edgeでボタンプッシュを検知していたが、SIGINTきても処理がとまらないとのことでadd_event_detectに変更

また、systemdに登録したのでsignalをハンドラで処理するように変更

#!/usr/bin/env python3
# coding: utf-8
from datetime import datetime
from datetime import timedelta
import logging
import signal
import sys
import time

import RPi.GPIO as GPIO

from mail import visitmail
from line_notify import line_notify

def handler(signal, frame):
    logger.info('break')
    GPIO.cleanup()
    sys.exit(0)

def callBuzzer(channel):
    try:
        # ノイズ対策 静電気等のノイズでないか0.1秒以上続いていることをチェック
        time.sleep(0.1)
        if GPIO.input(pin) == GPIO.LOW:

            # 連続クリック対応 何度もメールが飛ぶと面倒なので一定時間内のボタンプッシュは無視
            # ノイズ対策の前に置くとこの処理時間で継続時間が長くなってしまうのでこの処理順にしている
            global before_calltime
            current_time = datetime.now()
            if current_time < (before_calltime + timedelta(seconds=60)):
                logger.debug('callback yet')
                return

            logger.debug('before_calltime:%s current_time:%s'
                % (before_calltime.strftime("%Y/%m/%d %H:%M:%S"),current_time.strftime("%Y/%m/%d %H:%M:%S")))
            before_calltime = current_time

            logger.info('visit actions call')
            str_current_time = current_time.strftime("%Y/%m/%d %H:%M:%S")
            visitmail(str_current_time)
            line_notify(str_current_time)
    except Exception as e:
        logger.error('error:  %s' % e)

signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGTERM, handler)

try:
    formatter = '%(levelname)s : %(asctime)s : %(message)s'
    logging.basicConfig(level = logging.INFO, filename = 'doorphone.log', format=formatter)
except:
    print >> sys.stderr, 'error: could not open log file'
    sys.exit(1)
logger = logging.getLogger(__name__)
#logger.setLevel(logging.DEBUG)
loop_logger = logging.getLogger('loop_logger')
#loop_logger.setLevel(logging.DEBUG)

logger.info('start doorphone moniter')
before_calltime = datetime.now() - timedelta(seconds=60)

GPIO.setmode(GPIO.BCM)
pin = 25

GPIO.setup(pin, GPIO.IN,pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(pin, GPIO.FALLING, callback=callBuzzer, bouncetime=300)

while True:
    loop_logger.debug('main loop runnning')
    time.sleep(1)

メール送信処理

(mail.py)

引数かえたぐらいで特に変更なし

#!/usr/bin/env python3
# coding: utf-8
import configparser
from email.mime.text import MIMEText
from smtplib import SMTP
import os

def visitmail(current_time):
    inifile = configparser.ConfigParser()
    inifile.read('./config.ini', 'UTF-8')

    ini_host = inifile.get('server', 'host')
    ini_port = inifile.get('server', 'port')
    ini_from = inifile.get('mail', 'from')
    ini_to = inifile.get('mail', 'to')
    ini_title = inifile.get('mail', 'title')

    with SMTP(host=ini_host, port=ini_port) as smtp:
        smtp.starttls()
        smtp.login(
                user = ini_from,
                password = os.environ['MAIL_PASSWORD'],
                )
        msg = MIMEText(current_time)
        msg['Subject'] = ini_title
        msg['To'] = ini_to
        msg['From'] = ini_from

        smtp.send_message(
                from_addr = ini_from,
                to_addrs = ini_to.split(','),
                msg = msg,
                )

if __name__ == '__main__':
    visitmail( '2018/11/12 固定' )

設定ファイル

(./config.ini)


[server]
host = mail.hoge.co.jp
port = 587

[mail]
from = kitamura@hoge.co.jp
to = kitamura@hoge.co.jp,keitaiaddr@ezweb.ne.jp
title = 呼出ブザーが押されました

systemd

(/etc/systemd/system/doorphone.service)

[Unit]
Description=doorphone Daemon

[Service]
EnvironmentFile=/home/pi/.config/environment.d/doormail.conf
WorkingDirectory=/home/pi/develop/doorphone
ExecStart=/home/pi/develop/doorphone/control.py
ExecStop=/bin/kill ${MAINPID}
Restart=always
Type=simple
User=pi
Group=pi

[Install]
WantedBy=multi-user.target

そういえば嫁のサイトをfirebaseに準備しました

atom-runnerでpyenvで指定したpythonを使う

環境

  • CentOS Linux release 7.6.1810 (Core)
  • Atom 1.31.2
  • atom-runner 2.7.1

設定

Atomの設定ファイル(config.cson)にpyenvが指しているpythonのパスを指定する。ホームをチルダで指定したら動かなかったので絶対パスで指定している。

pythonのパス確認

$ which python
~/.pyenv/shims/python

~/.atom/config.csonに追記(“*”: は元からあるのでその下にrunnerからの3行を追記)

"*":
  'runner':
    'scopes':
      'python': '/home/ユーザ名/.pyenv/shims/python'
  以下、元からあった分

pyenvでCentOS7にpython3.7.0をインストール

環境

CentOS Linux release 7.5.1804 (Core)
pyenv 1.2.7

依存パッケージインストール

pyenvの公式のwikiにある推奨される依存関係をまとめてインストール。

yum install gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel

pythonインストール実施

[yoshitake@localhost ~]$ pyenv install 3.7.0
(略)
[yoshitake@localhost ~]$ pyenv versions
* system (set by /home/yoshitake/.pyenv/version)
3.7.0

切り替え

[yoshitake@localhost ~]$ pyenv global 3.7.0
[yoshitake@localhost ~]$ pyenv versions 
  system
* 3.7.0 (set by /home/yoshitake/.pyenv/version)

インストール失敗のログ

zlib本体はもとから入っていたけど、読めないと言われてインストール失敗。
zlib-develが入っていなかったからこのメッセージになった?

[yoshitake@localhost ~]$ pyenv install 3.7.0
Downloading Python-3.7.0.tar.xz...
-> https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tar.xz
Installing Python-3.7.0...

BUILD FAILED (CentOS 7.5.1804 using python-build 20180424)

Inspect or clean up the working tree at /tmp/python-build.20181029194007.1745
Results logged to /tmp/python-build.20181029194007.1745.log

Last 10 log lines:
  File "/tmp/python-build.20181029194007.1745/Python-3.7.0/Lib/ensurepip/__main__.py", line 5, in 
    sys.exit(ensurepip._main())
  File "/tmp/python-build.20181029194007.1745/Python-3.7.0/Lib/ensurepip/__init__.py", line 204, in _main
    default_pip=args.default_pip,
  File "/tmp/python-build.20181029194007.1745/Python-3.7.0/Lib/ensurepip/__init__.py", line 117, in _bootstrap
    return _run_pip(args + [p[0] for p in _PROJECTS], additional_paths)
  File "/tmp/python-build.20181029194007.1745/Python-3.7.0/Lib/ensurepip/__init__.py", line 27, in _run_pip
    import pip._internal
zipimport.ZipImportError: can't decompress data; zlib not available

zlib本体はもとから入っていた

[yoshitake@localhost ~]$ sudo yum install zlib -y
[sudo] yoshitake のパスワード:
読み込んだプラグイン:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: mirrors.cat.net
 * extras: mirrors.cat.net
 * updates: mirrors.cat.net
パッケージ zlib-1.2.7-17.el7.x86_64 はインストール済みか最新バージョンです
何もしません