PyPIで提供されているnature-remoの中身を見てみた話

PyPIで提供されているnature-remoの中身を見てみた話

時代はやっぱIoT

昨今、the Internet of things(IoT)というのが流行っています。

身の回りにある物、例えばテレビ、電灯、冷蔵庫、鍵。こういうものをインターネットでつなげてスマートフォンから扱えれば便利ですよね?

それがIoTの考え方です。

日本語にすると、「物のインターネット」とかいうクソダサ用語になってしまいますが、まあそれは置いといて。

例えばテレビをスマートフォンから動かしたいとなった場合、スマートフォンを操作して、赤外線を出力する機器に命令することで、テレビを操作することができるのです! 何と便利。

これを実現してくれるのがNature Remoです。

Nature Remo

Nature Remoは赤外線を送出して、エアコンの温度やテレビのチャンネル、電灯のオンオフなどを操作できる画期的なデバイスです。

私も使ってます!(こうやって自分の環境を晒すことでクラッカーの餌食になる)

AWSを使っているので、しょっちゅう落ちますが、今や老人ホームでも使われているくらい便利なデバイスなのです。

本当はRaspberry Piで作ってしまおうと考えていたが、実はNature Remoの方が安いということがわかり諦めた。まあ、将来的にはセキュリティー面や可用性から考えて、再構築する可能性はある。

PyPI nature-remo

このNature Remo、基本的はスマートフォンなどの端末から操作するものなのですが、実はcURLでも操作できます。

それなら、Pythonでラッパーが提供されているのでは? とあるときふと思い、調べてみたら、案の定ありました。それがnature-remo(==0.3.3)です。

設計者によるqiitaPyPIがありました。

この前みたいに、調べもしないでNHK番組表APIのPythonラッパーを調べたら、もうすでにあったという問題は、今回は起きなかった。

まあ、あるならあるで、pip installするまでです。

pip install nature-remo

で、とりあえずプログラムを実行しようとしたら、アクセストークンなるものが必要らしい。

from remo import NatureRemoAPI

api = NatureRemoAPI('-0As*******************************************************************************cck4')

でも私は慎重派なので、「アクセストークンとか勝手に訳のわからないところにアップロードされていないよな?」ということを確認するために、とりあえず、import remo; remo.__path__でremoのパスを確認して中身を見てみる(実はPyPIは報告されない以上、そういった怪しいプログラムも一般人がアップロードできてしまう)

class NatureRemoAPI:
    """Client for the Nature Remo API."""

    def __init__(self, access_token: str, debug: bool = False):
        if debug:
            enable_debug_mode()
        self.access_token = access_token
        self.base_url = BASE_URL
        self.rate_limit = RateLimit()

    def __request(
        self, endpoint: str, method: HTTPMethod, data: dict = None
    ) -> requests.models.Response:
        headers = {
            "Accept": "application/json",
            "Authorization": f"Bearer {self.access_token}",
            "User-Agent": f"nature-remo/{__version__} ({__url__})",
        }
        url = f"{self.base_url}{endpoint}"

        try:
            if method == HTTPMethod.GET:
                return requests.get(url, headers=headers)
            else:
                return requests.post(url, headers=headers, data=data)
        except requests.RequestException as e:
            raise NatureRemoError(e)

見た感じ、悪意はなさそう。

とりあえず動かしてみよう、と思ってプログラムを動かしたのですが、それはほとんど、qiitaにあった通りなので割愛。

問題点

これを操作しているときに、「音量とか一気に操作したいな」と思いました(これは結局解決しないでおいた。threading.Threadを使って実現するのは読者の課題とする)

送信速度が毎回1秒くらいかかるのは、もしやtime.sleep()しているのでは? と疑いました。

実際、pyautogui.write()が所望の速度ではないのは、time.sleep(0.01)が導入されているからだし、似たようなことがあってもおかしくない。

そう思って、再びapi.pyの中身を見てみたわけです。

が、そこで見つかったのが別の問題でした。

    def send_tv_infrared_signal(self, appliance: str, button: str):
        """Send tv infrared signal.
        Args:
            appliance: Appliance ID.
            button: Button name.
        """
        endpoint = f"/1/appliances/{appliance}/tv"
        resp = self.__request(endpoint, HTTPMethod.POST, {"button": button})
        if not resp.ok:
            raise NatureRemoError(build_error_message(resp))

これ、実は良くないんですね。何が良くないかというと、requests.close()を使うべきなのに、使っていない。

セッションを閉じないとResourceWarningが発生するおそれがある。

そこで、私は中身をこんな感じに書き換えました。

    def send_tv_infrared_signal(self, appliance: str, button: str):
        """Send tv infrared signal.
        Args:
            appliance: Appliance ID.
            button: Button name.
        """
        endpoint = f"/1/appliances/{appliance}/tv"
        with self.__request(endpoint, HTTPMethod.POST, {"button": button}) as resp:
            if not resp.ok:
                raise NatureRemoError(build_error_message(resp))

まあ普通に使っている分には問題ないのですが、macの電源を消さずにスリープさせっぱなしの私にとっては、何が起きるのかわからないのは怖いので、安全策を取ったまでです。

さいごに

セッション系のプログラムを弄るときは、withを使おう。