- 投稿日:2020-06-19T21:48:17+09:00
socket.ioでビデオ通話+ユーザー名表示
Zoomにあるようなビデオ映像に名前を入れて欲しい…
以前に作成したsocket.io にセッション情報を渡すとExpress+MySQLでログインするの続編です。
改めて、このプログラムを書いた経緯です。
- 自社ネットワーク内でビデオ通話をしたいという相談をうける
- hidden.inを見つける
- これは便利だと感動し、セットアップする
- ユーザー名(支店名)を表示して欲しいと相談をうける
- Node.jsを勉強する
このような経緯で今に至っております。
冒頭に紹介した記事で、MySQLにユーザー情報を保存しておき、passportモジュールを読み込み、ログイン判定をする。というところまでが出来ている想定で話を進めます。
では、実装方法です!
編集するファイルは
- index.js
- static/js/index.js
- static/css/app.css
- views/screen.ejs(前回説明不足でしたが、元からあるstatic/screen.htmlをejsファイルに変更しています。ディレクトリもviewsフォルダを作成しています。)
プログラムの流れ(イメージ)
- ログインしているユーザーがemit('join')する
- サーバーサイドでon('join')された時emit('joined')でユーザー名とIDを渡す
- サーバーサイドでemit('otherjoined')する
- 2に対してクライアントでユーザー名を保持する
- on('otherjoined')した時にmyNameSend関数を呼び出し、自分の名前とIDをemitする
- サーバーサイドでon('mynamesend')された時にemit('othernamesend')で値を送る
- クライアントサイドでon('othernamesend')を受け取った時にユーザー名を表示させる
index.js//中略 //サーバーサイド //プログラムの流れ2,3 socket.on('join', function (req) { io.to(socket.id).emit("joined", { id: socket.id,name:socket.request.session.passport.user }); socket.broadcast.to(socket.room).json.emit("otherJoined", { id: socket.id}); }ここでは、joinしたユーザーに対して、joinedを送る+ルームにいるメンバーに対して、他のユーザーがルームに入ったことを知らせています。
static/js/index.js//中略 //クライアントサイド //プログラムの流れ4,5 socket.on('joined', function (event) { joinedToRoom = true; // socket.idが入っている selfId = event.id; //usernameが入っている myName = event.name; }).on('otherJoined', function (event) { sendOffer(event.id); myNameSend(selfId,myName); }) //myNameSend関数 function myNameSend(myId,myName){ socket.emit('mynamesend',{id:myId,name:myName}); }見ての通りです
index.js//中略 //サーバーサイド //プログラムの流れ6 socket.on('mynamesend',function(id,name){ socket.broadcast.to(socket.room).emit('othernamesend',{id,name}); });myNameSend関数で送られてきた値をブロードキャストしてルームに所属するユーザーに送っています。
static/js/index.js//中略 //クライアントサイド //プログラムの流れ7 .on('othernamesend',function(event){ otherNameCreate(event); }) function otherNameCreate(otherconf){ if(otherconf.id.name != myName){ var otherId = 'area_' + otherconf.id.id; var otherArea = document.getElementById(otherId); if(!otherArea){ otherArea = document.createElement('div'); otherArea.className = 'remoteArea'; otherArea.id = otherId; document.getElementById('video_wrap').appendChild(otherArea); remoteName = document.createElement('span'); remoteName.className = 'remoteName'; remoteName.textContent = otherconf.id.name; otherArea.appendChild(remoteName); } }else{ console.log('名前が一緒'); return; } }名前表示の制限として自分と違う名前なら表示させるようにしています。
area_ + socket.idというように一意なエリアを作成し、そこに名前を表示させるHTMLを生成しているプログラムです。
この流れでログインしたユーザー名の表示が出来るようになりました!
全コードGitHubに上げています。
今後の課題
- 対応ブラウザを増やす(現在はGoogle Chromeのみ)
- FullScreenにした時にもユーザー名を表示させる、全画面時の画面編集機能を追加する
などです。
まだまだ、稚拙なコードの書き方なので精進したいと思います。間違い、改善などがありましたら、よろしくお願いします。
読んでくださりありがとうございました。
以上です
- 投稿日:2020-06-19T21:48:17+09:00
socket ioでビデオ通話+ユーザー名表示
Zoomにあるようなビデオ映像に名前を入れて欲しい…
以前に作成したsocket.io にセッション情報を渡すとExpress+MySQLでログインするの続編です。
改めて、このプログラムを書いた経緯です。
- 自社ネットワーク内でビデオ通話をしたいという相談をうける
- hidden.inを見つける
- これは便利だと感動し、セットアップする
- ユーザー名(支店名)を表示して欲しいと相談をうける
- Node.jsを勉強する
このような経緯で今に至っております。
冒頭に紹介した記事で、MySQLにユーザー情報を保存しておき、passportモジュールを読み込み、ログイン判定をする。というところまでが出来ている想定で話を進めます。
では、実装方法です!
編集するファイルは
- index.js
- static/js/index.js
- static/css/app.css
- views/screen.ejs(前回説明不足でしたが、元からあるstatic/screen.htmlをejsファイルに変更しています。ディレクトリもviewsフォルダを作成しています。)
プログラムの流れ(イメージ)
- ログインしているユーザーがemit('join')する
- サーバーサイドでon('join')された時emit('joined')でユーザー名とIDを渡す
- サーバーサイドでemit('otherjoined')する
- 2に対してクライアントでユーザー名を保持する
- on('otherjoined')した時にmyNameSend関数を呼び出し、自分の名前とIDをemitする
- サーバーサイドでon('mynamesend')された時にemit('othernamesend')で値を送る
- クライアントサイドでon('othernamesend')を受け取った時にユーザー名を表示させる
index.js//中略 //サーバーサイド //プログラムの流れ2,3 socket.on('join', function (req) { io.to(socket.id).emit("joined", { id: socket.id,name:socket.request.session.passport.user }); socket.broadcast.to(socket.room).json.emit("otherJoined", { id: socket.id}); }ここでは、joinしたユーザーに対して、joinedを送る+ルームにいるメンバーに対して、他のユーザーがルームに入ったことを知らせています。
static/js/index.js//中略 //クライアントサイド //プログラムの流れ4,5 socket.on('joined', function (event) { joinedToRoom = true; // socket.idが入っている selfId = event.id; //usernameが入っている myName = event.name; }).on('otherJoined', function (event) { sendOffer(event.id); myNameSend(selfId,myName); }) //myNameSend関数 function myNameSend(myId,myName){ socket.emit('mynamesend',{id:myId,name:myName}); }見ての通りです
index.js//中略 //サーバーサイド //プログラムの流れ6 socket.on('mynamesend',function(id,name){ socket.broadcast.to(socket.room).emit('othernamesend',{id,name}); });myNameSend関数で送られてきた値をブロードキャストしてルームに所属するユーザーに送っています。
static/js/index.js//中略 //クライアントサイド //プログラムの流れ7 .on('othernamesend',function(event){ otherNameCreate(event); }) function otherNameCreate(otherconf){ if(otherconf.id.name != myName){ var otherId = 'area_' + otherconf.id.id; var otherArea = document.getElementById(otherId); if(!otherArea){ otherArea = document.createElement('div'); otherArea.className = 'remoteArea'; otherArea.id = otherId; document.getElementById('video_wrap').appendChild(otherArea); remoteName = document.createElement('span'); remoteName.className = 'remoteName'; remoteName.textContent = otherconf.id.name; otherArea.appendChild(remoteName); } }else{ console.log('名前が一緒'); return; } }名前表示の制限として自分と違う名前なら表示させるようにしています。
area_ + socket.idというように一意なエリアを作成し、そこに名前を表示させるHTMLを生成しているプログラムです。
この流れでログインしたユーザー名の表示が出来るようになりました!
全コードGitHubに上げています。
今後の課題
- 対応ブラウザを増やす(現在はGoogle Chromeのみ)
- FullScreenにした時にもユーザー名を表示させる、全画面時の画面編集機能を追加する
などです。
まだまだ、稚拙なコードの書き方なので精進したいと思います。間違い、改善などがありましたら、よろしくお願いします。
読んでくださりありがとうございました。
以上です
- 投稿日:2020-06-19T21:31:12+09:00
コスパ最強IoT家電!TPLink製品をRaspberryPiから操作
TPLinkとは?
ルータを主力とする中国・深圳のネットワーク機器メーカーです。
近年はスマート電球、スマートプラグ等のIoT家電に力を入れており、コスパの良さからAmazonで独自の地位を築いています。今回は、APIを使用して、
・機器のON-OFF操作
・ON-OFF、電球の明るさ等の情報取得
を、PythonおよびNode.jsで実行してみましたIoT家電として思いつく用途の多くを上記でカバーできるので
応用の可能性を感じる結果となりました!必要なもの
・PC
・RaspberryPi
・TPLink製スマートプラグあるいは電球
今回は下記3製品を試しました
HS105:スマートプラグ
KL110:ホワイト電球
KL130:カラー電球①データ取得の確認
まずは、TPLinkからデータが取得できるかターミナル上でテストします。
※参考にさせて頂いた記事
https://lmjs7.net/blog/tag/tp-link/
https://qiita.com/tmisuoka0423/items/582ff0c303abe8570ee5IPを調べる
tplink-smarthome-api(参考)をインストール
sudo npm install -g tplink-smarthome-api下記コマンドで、接続しているTPLinkデバイス一覧を取得
tplink-smarthome-api searchHS105(JP) plug IOT.SMARTPLUGSWITCH 192.168.0.101 9999 B0BE76‥ スマートプラグ KL110(JP) bulb IOT.SMARTBULB 192.168.0.102 9999 98DAC4‥ ホワイト電球 KL130(JP) bulb IOT.SMARTBULB 192.168.0.103 9999 0C8063‥ カラー電球3つのデバイス全てが検出できていることが分かります
デバイス動作情報の取得確認
下記コマンドで、デバイスの設定やOnOffが取得できる
tplink-smarthome-api getSysInfo [デバイスのIPアドレス]:9999・KL130(カラー電球)の例
: ctrl_protocols: { name: 'Linkie', version: '1.0' }, ↓ここからがデバイスの設定 light_state: { on_off: 1, mode: 'normal', hue: 0, saturation: 0, color_temp: 2700, brightness: 100 }, ↑ここまでがデバイスの設定 is_dimmable: 1, is_color: 1, :on_off:0なら電源OFF、1なら電源ON
hue:色?(白色モードのとき0)
color_temp:色温度(白色モード以外のとき0)
brightness:明るさ(%単位)
と思われます・KL110(ホワイト電球)の例
: ctrl_protocols: { name: 'Linkie', version: '1.0' }, ↓ここからがデバイスの設定 light_state: { on_off: 1, mode: 'normal', hue: 0, saturation: 0, color_temp: 2700, brightness: 100 }, ↑ここまでがデバイスの設定 is_dimmable: 1, is_color: 0, :on_off:0なら電源OFF、1なら電源ON
hue:色相(白色モードのとき0)
saturation:彩度
color_temp:色温度(白色モード以外のとき0)
brightness:明るさ(%単位)
と思われます。
KL130とほぼ同じですが、カラーではないのでis_color: 0となっていると思われます。・KL105(スマートプラグ)の例
alias: '', ↓ここからがデバイスの設定 relay_state: 1, on_time: 288, active_mode: 'none', feature: 'TIM', updating: 0, icon_hash: '', rssi: -52, led_off: 0, longitude_i: 1356352, latitude_i: 348422, ↑ここまでがデバイスの設定 hwId: '047D‥',relay_state:0なら電源OFF、1なら電源ON
on_time:連続電源ON時間
rssi: WiFiの信号強度
と思われます。
経度(logitude)と緯度(latitude)も表示されていますが、実際の場所と5キロくらいずれていて謎が深まります。上記で、コマンドで欲しい情報が取得できることが確認できました!
次章以降で、プログラム(Node.js&Python)から取得・操作する方法を記載します。②Node.jsで状態取得
※「Pythonを使うからNode.jsの説明はいらん!」という方は、この章を飛ばして③に移動してください
こちらを参考に、Node.jsを
npmにパスを通す(Windowの場合)
Windowsだとnpmのグローバルインストール先にパスが通っておらず、Node.jsでモジュールが読み込めないので、下記を参考にパスを通してください
https://qiita.com/shiftsphere/items/5610f692899796b03f99npmにパスを通す(RaspberryPiの場合)
下記コマンドで、グローバルでのnpmモジュールインストール先を調べます
(なぜかWindowsのときのコマンド"npm bin -g"で見つかるフォルダとは違うようです)npm ls -g下記コマンドで.profileを編集します。
※SSH環境では.profileの代わりに、.bash_profileを編集してくださいnano /home/[ユーザ名]/.profile.profileの最後に下記の1行を追加してrebootしてください
export NODE_PATH=[上で調べたパス]/node_modules下記コマンドで指定したパスが表示されれば成功です
printenv NODE_PATHnode.jsスクリプトの作成
下記スクリプトを作成します
tplink_test.jsconst { Client } = require('tplink-smarthome-api'); const client = new Client(); client.getDevice({ host: '192.168.0.102' }).then(device => { device.getSysInfo().then(console.log); });下記コマンドでスクリプトを実行すると、①と同様に各種情報が取得できます
node tplink_test.js※上記をcsvロギングするスクリプト(③のPythonスクリプトと同機能)も作成しましたが、私のJavaScriptスキルが低くうまく動作しないときがある(非同期部分の処理順が逆転する)ので、コードはここには貼らないこととします
下記GitHubにアップロードしたので、自己責任で改造して使用していただければと思います。
(願わくば無知な私に処理順が逆転する理由もコメント…頂けると嬉しいです笑)
https://github.com/c60evaporator/TPLink_Info_Nodejs③Pythonで状態取得
私のJavaScriptスキル不足でNode.jsでのロギングが上手くいかなかったので、
気を取り直してPythonで操作・ロギングするスクリプトを作りました。PythonはNode.jsほど丁寧なドキュメントが見当たらず苦戦しましたが、こちらやこちらのコードを解読して、スクリプトを作成しました。
TPLink操作クラスの作成
上記コードを参考に、下記の4つのクラスを作成しました
TPLink_Common():プラグ、電球共通機能のクラス
TPLink_Plug():プラグ専用機能のクラス(TPLink_Common()を継承)
TPLink_Bulb():電球専用機能のクラス(TPLink_Common()を継承)
GetTPLinkData():上記クラスを利用して、データを取得するクラスtplink.pyimport socket from struct import pack import json #TPLinkデータ取得用クラス class GetTPLinkData(): #プラグデータ取得用メソッド def get_plug_data(self, ip): #プラグ操作用クラス作成 plg = TPLink_Plug(ip) #データを取得し、dictに変換 rjson = plg.info() rdict = json.loads(rjson) return rdict #電球データ取得用メソッド def get_bulb_data(self, ip): #電球操作用クラス作成 blb = TPLink_Bulb(ip) #データを取得し、dictに変換 rjson = blb.info() rdict = json.loads(rjson) return rdict #TPLink電球&プラグ共通クラス class TPLink_Common(): def __init__(self, ip, port=9999): """Default constructor """ self.__ip = ip self.__port = port def info(self): cmd = '{"system":{"get_sysinfo":{}}}' receive = self.send_command(cmd) return receive def send_command(self, cmd, timeout=10): try: sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock_tcp.settimeout(timeout) sock_tcp.connect((self.__ip, self.__port)) sock_tcp.settimeout(None) sock_tcp.send(self.encrypt(cmd)) data = sock_tcp.recv(2048) sock_tcp.close() decrypted = self.decrypt(data[4:]) print("Sent: ", cmd) print("Received: ", decrypted) return decrypted except socket.error: quit("Could not connect to host " + self.__ip + ":" + str(self.__port)) return None def encrypt(self, string): key = 171 result = pack('>I', len(string)) for i in string: a = key ^ ord(i) key = a result += bytes([a]) return result def decrypt(self, string): key = 171 result = "" for i in string: a = key ^ i key = i result += chr(a) return result #TPLinkプラグ操作用クラス class TPLink_Plug(TPLink_Common): def on(self): cmd = '{"system":{"set_relay_state":{"state":1}}}' receive = self.send_command(cmd) def off(self): cmd = '{"system":{"set_relay_state":{"state":0}}}' receive = self.send_command(cmd) def ledon(self): cmd = '{"system":{"set_led_off":{"off":0}}}' receive = self.send_command(cmd) def ledoff(self): cmd = '{"system":{"set_led_off":{"off":1}}}' receive = self.send_command(cmd) def set_countdown_on(self, delay): cmd = '{"count_down":{"add_rule":{"enable":1,"delay":' + str(delay) +',"act":1,"name":"turn on"}}}' receive = self.send_command(cmd) def set_countdown_off(self, delay): cmd = '{"count_down":{"add_rule":{"enable":1,"delay":' + str(delay) +',"act":0,"name":"turn off"}}}' receive = self.send_command(cmd) def delete_countdown_table(self): cmd = '{"count_down":{"delete_all_rules":null}}' receive = self.send_command(cmd) def energy(self): cmd = '{"emeter":{"get_realtime":{}}}' receive = self.send_command(cmd) return receive #TPLink電球操作用クラス class TPLink_Bulb(TPLink_Common): def on(self): cmd = '{"smartlife.iot.smartbulb.lightingservice":{"transition_light_state":{"on_off":1}}}' receive = self.send_command(cmd) def off(self): cmd = '{"smartlife.iot.smartbulb.lightingservice":{"transition_light_state":{"on_off":0}}}' receive = self.send_command(cmd) def transition_light_state(self, hue: int = None, saturation: int = None, brightness: int = None, color_temp: int = None, on_off: bool = None, transition_period: int = None, mode: str = None, ignore_default: bool = None): # copy all given argument name-value pairs as a dict d = {k: v for k, v in locals().items() if k is not 'self' and v is not None} r = { 'smartlife.iot.smartbulb.lightingservice': { 'transition_light_state': d } } cmd = json.dumps(r) receive = self.send_command(cmd) print(receive) def brightness(self, brightness): self.transition_light_state(brightness=brightness) def purple(self, brightness = None, transition_period = None): self.transition_light_state(hue=277, saturation=86, color_temp=0, brightness=brightness, transition_period=transition_period) def blue(self, brightness = None, transition_period = None): self.transition_light_state(hue=240, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period) def cyan(self, brightness = None, transition_period = None): self.transition_light_state(hue=180, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period) def green(self, brightness = None, transition_period = None): self.transition_light_state(hue=120, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period) def yellow(self, brightness = None, transition_period = None): self.transition_light_state(hue=60, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period) def orange(self, brightness = None, transition_period = None): self.transition_light_state(hue=39, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period) def red(self, brightness = None, transition_period = None): self.transition_light_state(hue=0, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period) def lamp_color(self, brightness = None): self.transition_light_state(color_temp=2700, brightness=brightness)TPLink操作クラスの実行方法
上記クラスは、Pythonコード上で下記のように実行できます
・電球の電源をONにしたいとき
TPLink_Bulb(電球のIPアドレス).on()・プラグの電源をOFFにしたいとき
TPLink_Plug(プラグのIPアドレス).off()・10秒後にプラグをONにしたいとき
TPLink_Plug(プラグのIPアドレス).set_countdown_on(10)・電球の明るさを10%にしたいとき
TPLink_Bulb(電球のIPアドレス).brightness(10)・電球を赤色にしたいとき(カラー電球のみ)
TPLink_Bulb(電球のIPアドレス).red()・電球のOn-Off等の情報を取得
info = GetTPLinkData().get_plug_data(プラグのIPアドレス)※上記メソッドは、取得したjson情報をdict形式に変換して出力されます。
出力される電球情報は①を参照ください④ロギング用Pythonスクリプトの作成
前章最後の方法を利用して、電球やプラグの情報をロギングするスクリプトを作成しました。
スクリプトの構造はこちらと同じなので、リンク先をご一読いただければと思います。設定ファイル
こちらの記事同様、管理をしやすくするため下記2種類の設定ファイルを作成しました
・DeviceList.csv:センサごとに必要情報を記載DeviceList.csv ApplianceName,ApplianceType,IP,Retry TPLink_KL130_ColorBulb_1,TPLink_ColorBulb,192.168.0.103,2 TPLink_KL110_WhiteBulb_1,TPLink_WhiteBulb,192.168.0.102,2 TPLink_HS105_Plug_1,TPLink_Plug,192.168.0.101,2カラムの意味は下記となります
ApplianceName:デバイス名を管理、同種類のデバイスが複数あるときの識別用
ApplianceType:デバイスの種類。
TPLink_ColorBulb:カラー電球(KL130等)
TPLink_WhiteBulb:白色電球(KL110等)
TPLink_Plug:スマートプラグ(HS105等)
IP:デバイスのIPアドレス
Retry:最大再実行回数詳細(取得失敗時の再実行回数、詳しくはこちら)・config.ini:CSVおよびログ出力ディレクトリを指定
config.ini
[Path]
CSVOutput = /share/Data/Appliance
LogOutput = /share/Log/Appliance
どちらもsambaで作成した共有フォルダ内に出力すると、RaspberryPi外からアクセスできて便利です。実際のスクリプト
appliance_data_logger.pyfrom tplink import GetTPLinkData import logging from datetime import datetime, timedelta import os import csv import configparser import pandas as pd #グローバル変数 global masterdate ######TPLinkのデータ取得###### def getdata_tplink(appliance): #データ値が得られないとき、最大appliance.Retry回スキャンを繰り返す for i in range(appliance.Retry): try: #プラグのとき if appliance.ApplianceType == 'TPLink_Plug': applianceValue = GetTPLinkData().get_plug_data(appliance.IP) #電球のとき elif appliance.ApplianceType == 'TPLink_ColorBulb' or appliance.ApplianceType == 'TPLink_WhiteBulb': applianceValue = GetTPLinkData().get_bulb_data(appliance.IP) else: applianceValue = None #エラー出たらログ出力 except: logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, appliance{appliance.ApplianceName}]') applianceValue = None continue else: break #値取得できていたら、POSTするデータをdictに格納 if applianceValue is not None: #プラグのとき if appliance.ApplianceType == 'TPLink_Plug': data = { 'ApplianceName': appliance.ApplianceName, 'Date_Master': str(masterdate), 'Date': str(datetime.today()), 'IsOn': str(applianceValue['system']['get_sysinfo']['relay_state']), 'OnTime': str(applianceValue['system']['get_sysinfo']['on_time']) } #電球のとき else: data = { 'ApplianceName': appliance.ApplianceName, 'Date_Master': str(masterdate), 'Date': str(datetime.today()), 'IsOn': str(applianceValue['system']['get_sysinfo']['light_state']['on_off']), 'Color': str(applianceValue['system']['get_sysinfo']['light_state']['hue']), 'ColorTemp': str(applianceValue['system']['get_sysinfo']['light_state']['color_temp']), 'Brightness': str(applianceValue['system']['get_sysinfo']['light_state']['brightness']) } return data #取得できていなかったら、ログ出力 else: logging.error(f'cannot get data [loop{str(appliance.Retry)}, date{str(masterdate)}, appliance{appliance.ApplianceName}]') return None ######データのCSV出力###### def output_csv(data, csvpath): appliancename = data['ApplianceName'] monthstr = masterdate.strftime('%Y%m') #出力先フォルダ名 outdir = f'{csvpath}/{appliancename}/{masterdate.year}' #出力先フォルダが存在しないとき、新規作成 os.makedirs(outdir, exist_ok=True) #出力ファイルのパス outpath = f'{outdir}/{appliancename}_{monthstr}.csv' #出力ファイル存在しないとき、新たに作成 if not os.path.exists(outpath): with open(outpath, 'w', newline="") as f: writer = csv.DictWriter(f, data.keys()) writer.writeheader() writer.writerow(data) #出力ファイル存在するとき、1行追加 else: with open(outpath, 'a', newline="") as f: writer = csv.DictWriter(f, data.keys()) writer.writerow(data) ######メイン###### if __name__ == '__main__': #開始時刻を取得 startdate = datetime.today() #開始時刻を分単位で丸める masterdate = startdate.replace(second=0, microsecond=0) if startdate.second >= 30: masterdate += timedelta(minutes=1) #設定ファイルとデバイスリスト読込 cfg = configparser.ConfigParser() cfg.read('./config.ini', encoding='utf-8') df_appliancelist = pd.read_csv('./ApplianceList.csv') #全センサ数とデータ取得成功数 appliance_num = len(df_appliancelist) success_num = 0 #ログの初期化 logname = f"/appliancelog_{str(masterdate.strftime('%y%m%d'))}.log" logging.basicConfig(filename=cfg['Path']['LogOutput'] + logname, level=logging.INFO) #取得した全データ保持用dict all_values_dict = None ######デバイスごとにデータ取得###### for appliance in df_appliancelist.itertuples(): #ApplianceTypeがTPLinkeであることを確認 if appliance.ApplianceType in ['TPLink_Plug','TPLink_ColorBulb','TPLink_WhiteBulb']: data = getdata_tplink(appliance) #上記以外 else: data = None #データが存在するとき、全データ保持用Dictに追加し、CSV出力 if data is not None: #all_values_dictがNoneのとき、新たに辞書を作成 if all_values_dict is None: all_values_dict = {data['ApplianceName']: data} #all_values_dictがNoneでないとき、既存の辞書に追加 else: all_values_dict[data['ApplianceName']] = data #CSV出力 output_csv(data, cfg['Path']['CSVOutput']) #成功数プラス success_num+=1 #処理終了をログ出力 logging.info(f'[masterdate{str(masterdate)} startdate{str(startdate)} enddate{str(datetime.today())} success{str(success_num)}/{str(appliance_num)}]')上記を実行すれば、設定ファイル"CSVOutput"で指定したフォルダに、取得データがデバイス名と日時の名称でCSV出力されます
以上で、情報取得が完了です
おわりに
RaspberrypPiで24時間稼働、かつPythonはIFTTTよりも自由度が高いので、色々なアイデアを具現化可能です
・人感センサと組み合わせて、人が入ったら電気が点くようにする
・30分以上人がいなければ、電気を消す
・人によって電球の明るさを自動で切り替える
などなどです。いくつか作りたいものがあるので、製作が完了したらまた記事にしようと思います
- 投稿日:2020-06-19T19:38:47+09:00
node.js Firebase Functionで作ったGoogle Chat BotでChatからのリクエストであるかの検証を行う
皆さんこんにちわ。Chat Bot作ってますか?まだApps Scriptで作ってますか?
このごろは外部のG SuiteテナントユーザーともやりとりできるようになったGoogle Chat。
Apps Scriptのままじゃテナントまたげません1。
やっぱりFirebase Functionsでしょう! (Cloud Functionsもほぼ同じ)…ってことでCloud Functionsをnode.jsで作り始めると、開発ドキュメントの「Verifying bot authenticity」でGoogle Chatからのリクエストかどうかを検証するためのサンプルコードが書かれていることに気づきます。
この対策をしないと、Bot Function URLが知らんやつに知られたら、Bot messageのsender情報を偽装されちゃうかもしれません。Botでユーザー情報を保持していてメッセージよって返すようにしていたら、その情報が抜かれちゃいます。そりゃまずいってことでサンプルのとおりに実装しようとしたところ…node.jsでBot Function作ってるのに、JavaとPythonのサンプルしか書かれていない!2
まじありえんと…そこから調べるのいろいろ苦労したんですがシンプルな方法で解決したので記録します。
以下ざっくりとした手順です。
- まずありがたい、
google-id-tokenを使わせてもらう。- node_modules/google-id-token/Readme.md をざっと読む。
- Readme.mdに記載のサンプルコードを、まるっとfunctionにする。
- 好みの問題もあるけど
required('request')は使いづらいので、node-fetchで代替しちゃう。- Firebaseプロジェクトに紐付くGCPのプロジェクト番号を、firebase funsionsコンフィグにセット。
- BOT URLとなるエンドポイント関数のド頭で、まるっと作ったfunctionに、req.headers.authorizationを渡して評価させてNGなら「botから呼べ!」と怒るコードを書く。
では手順の詳細。
firebase initしたプロジェクトディレクトリ内には、functionsディレクトリが作成されて、その中にindex.jsやpackage.jsonが作られてます。プロジェクトディレクトリ内はfirebaseコマンドを叩く場所。functionsディレクトリ内はFirebase Functionsのパッケージ構成する場所。…としっかり分けて考えましょう。たまに私もnpmコマンドをプロジェクトディレクトリ内で叩いて「うわーまたやっちまったー」と泣いてます。注意しましょう。
functionsディレクトリ内で、手順 1.と手順 4.をいっぺんにやっちゃいます。# npm i google-id-token # npm i node-fetchまるっとfunctionにしたものは、verifyChatIdToken.jsとでもしておきます。
google-id-tokenのReadme.mdでは、記載されているgetGoogleCerts関数でトークンの検証に使用する署名のURLが
'https://www.googleapis.com/oauth2/v1/certs'となっています。しかしGoogle Chatの開発ドキュメントのJava/Pythonのサンプルを読むと、'https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com'が使われていることがわかりますので差し替える必要があります。
これをしないとJWTのデコード自体はできるものの署名による検証結果(isAuthentic)はtrueになりません。まるっとコピーしてfunctionにしたのち、getGoogleCerts関数はgetChatCertsに名前を変更して、requestはfetchに差し替えてます。またJWTのBearerトークンからデコードした結果から、署名の検証が成功した場合のみtrueを返すようにしたものがこちら。
verifyChatIdToken.js'use strict' const functions = require('firebase-functions') const fetch = require('node-fetch') const googleIdToken = require('google-id-token') const PROJECT_NUMBER = functions.config().project.number const CHAT_SERVICE_ISS = "chat@system.gserviceaccount.com" const CHAT_SERVICE_CERTS_URI = `https://www.googleapis.com/service_accounts/v1/metadata/x509/${CHAT_SERVICE_ISS}` function getChatCerts(kid, callback) { fetch(CHAT_SERVICE_CERTS_URI) .then(res => res.json()) .then(certs => { callback(null, certs[kid]) }).catch(err => { callback(err, {}) }) } module.exports = async function verifyChatIdToken(authorization) { const BEARER_STARTS = "Bearer " if (!authorization) { console.log("authorization not found") return false } if (!authorization.startsWith(BEARER_STARTS)) { console.log(`authorization is not a ${BEARER_STARTS}idtoken`) return false } const idToken = authorization.substring(BEARER_STARTS.length) const parser = new googleIdToken({ getKeys: getChatCerts }) return new Promise((resolve) => { parser.decode(idToken, function (err, token) { let result = false if (err || !token) { console.log("error while parsing the google token: " + err) } else { console.log("parsed id_token is:\n" + JSON.stringify(token)) if (!token.isAuthentic) { console.log("failed verify by signature\n") } else { result = token.header && token.data && PROJECT_NUMBER == token.data.aud && CHAT_SERVICE_ISS == token.data.iss } } resolve(result) }) }) }それと、PROJECT_NUMBERという定数で、Bot URLとなるFirebase Functionが属するGCPプロジェクトのプロジェクト番号をFirebase Function CONFIGから参照するようにしていますので、手順 5.をやっちゃいます。GCPのプロジェクトダッシュボードでプロジェクトIDに並んでプロジェクト番号が記載されていますのでコピってきて、firebaseプロジェクトディレクトリで設定します。
# firebase functions:config:set project.number="{コピってきたプロジェクト番号}"audienceがPROJECT_NUMBER、issuerがCHAT_SERVICE_ISSと一致するかも併せてチェックして、これでChatからの要求確認が十分できました。
これであとは手順 6.のみ。ド頭でチェックしてGoogle Chatから呼ばれていない場合は怒ってあげるだけです。開発ドキュメントにあるように、User-Agentも固有の名前があるので、ついでにチェックします。
index.jsconst verifyChatIdToken = require('./verifyChatIdToken') exports.{BOT名} = functions.https.onRequest(async (req, res)=>{ if (req.method !== 'POST' || !req.body || !req.body.message || req.headers["user-agent"] !== "Google-Dynamite" || !(await verifyChatIdToken(req.headers.authorization))) { res.status(400).send('Hello! This function is meant to be used in a Google Chat Room or DM.\n') return } : // ボットの主処理 : })以上でございます。
これだけで安心・安全なボットにグレードアップしますよ! お試しください。
- 投稿日:2020-06-19T17:54:03+09:00
ESLint7系を使っていると「TypeError: createRequire is not a function」が出る
概要
ESLint 7 系とNode.js 12未満をつかっていると以下のエラーがでる場合がある
ESLint 7 がサポートしている Node.js バージョンは ^10.12.0 || >=12.0.0 となっています
↑だそうです。(コメントありがとうございました)
TypeError: createRequire is not a function Referenced from: /something/.eslintrc at Object.resolve (/something/node_modules/eslint/lib/shared/relative-module-resolver.js:28:20) at ConfigArrayFactory._loadExtendedShareableConfig (/something/node_modules/eslint/lib/cli-engine/config-array-factory.js:854:39) at ConfigArrayFactory._loadExtends (/something/node_modules/eslint/lib/cli-engine/config-array-factory.js:763:25) at ConfigArrayFactory._normalizeObjectConfigDataBody (/something/node_modules/eslint/lib/cli-engine/config-array-factory.js:702:25) at _normalizeObjectConfigDataBody.next (<anonymous>) at ConfigArrayFactory._normalizeObjectConfigData (/something/node_modules/eslint/lib/cli-engine/config-array-factory.js:647:20) at _normalizeObjectConfigData.next (<anonymous>) at ConfigArrayFactory.loadInDirectory (/something/node_modules/eslint/lib/cli-engine/config-array-factory.js:495:28) at CascadingConfigArrayFactory._loadConfigInAncestors (/something/node_modules/eslint/lib/cli-engine/cascading-config-array-factory.js:355:46) at CascadingConfigArrayFactory._loadConfigInAncestors (/something/node_modules/eslint/lib/cli-engine/cascading-config-array-factory.js:374:20)対策
その1
Node.jsを最新にする
その2
ESLintをダウングレードする
package.json (ESLintを6系にダウングレード)"eslint": "^6.8.0",
- 投稿日:2020-06-19T17:37:36+09:00
virtual背景のロジックをnpm パッケージ化した件
はじめに
先日、自分史上初めてnpmのパッケージを作成したので、記録として作業の流れを残しておくものです。
新しい情報は少ないかもしれません。
作成したパッケージは以前紹介したvirtual背景のコンポーネントです。https://qiita.com/wok/items/962929e63bc98e4033b9
もしよかったら使ってやってください。
概要
Typescriptを用いた環境でnpmパッケージを作成してpublishするまでの流れです。
前提
- npmのアカウントは作っておく。
- githubのアカウントとリポジトリは作っておく。
- 作成したgitリポジトリをクローンして、そのディレクトリに移動している状態。
作業の流れ
まずはじめに、npmの環境設定とログインをしておきましょう。
$ npm set init.author.name "xxxxxx" $ npm set init.author.email "xxxx@xxxx.co.jp" $ npm set init.author.url "https://qiita.com/wok" $ npm adduserそれでは、プロジェクトの初期化を行いpackage.jsonを作成します。
$ npm init -y次に必要なモジュールをインストールします。
ここではtypescriptを用いてパッケージを作成するので、typescript用の環境を準備します。
npx tsc --initによりtsconfig.jsonが生成されます。$ npm install -D typescript @types/node $ npx tsc --initパッケージを利用する人が型推論を使えるように、宣言ファイルを生成するようにtsconfig.jsonを変更します。
また、私はデバッグが楽になるようにsource mapも生成しておきます。これは好みです。
出力先は色んな場所で解説されているものに合わせて./distにしておきます。
moduleは用途により変える必要があるようですが、あまり理解できていません。今回はesnextにしています。
esnextにした場合は、moduleResolutionをnodeにしておかないとコンパイルが通らないようです。
(この辺がjsをいまいち好きになれないところなんだよな。そんなに覚えられないよ。)"declaration": true, "sourceMap": true, "outDir": "./dist", "module": "esnext", "moduleResolution": "node",この段階で、ソースコードを書き始めます。(実際はもともとのソースコードをコピペした。)
そしてコンパイル。エラーがなければ下記のようなファイルが出力されていると思います。$ npx tsc $ ls dist/ index.d.ts index.js index.js.map次に、package.jsonを変更してエントリポイントを設定してあげます。
今回の場合は次のような記述になります。"main": "dist/index.js",併せて、package.jsonにパッケージの情報を記載します。
nameはリポジトリ名が自動で設定されますが、すでに(他の人に)使われている名前はpublish時にエラーとなるので、かぶらないように設定します。なお、かぶったときに表示されるエラーメッセージは意味不明なものなので、ハマると辛いです。たしか、セキュリティが問題、という感じのメッセージだったと思います。"name": "local-video-effector", "author": "xxxxxx <xxxx@xxx.co.jp> (https://qiita.com/wok)", "license": "Apache-2.0",ここまでできたら、あとはpublishするまであと少し。
バージョン情報追加します。gitリポジトリと同期させておくと、問題が発生したときに解析が楽になりそうです。npm version patch git push origin <tag>最後にpublishして完了です。
npm publish成果物
https://www.npmjs.com/package/local-video-effector
お疲れ様でした。>自分
次回はここで作成したバーチャル背景のパッケージの使ったデモの開発を説明する予定です。
では、また。
- 投稿日:2020-06-19T14:20:35+09:00
Nodejs用のモデルライブラリを作ってみた【Express】
リポジトリ
https://github.com/kbc18a11/oreoreExpress
ライブラリ本体はoreoreExpress/database/AbstractModel.jsです。解説用の構成や注意点
- FW:Express
- ディレクトリ構成:WebStormの新規Expressプロジェクトの状態に
プロジェクト名/databaseとプロジェクト名/modelというディレクトリを生成- 本記事はチーム開発のメンバーのマニュアルとしても書いていますので、ライブラリには直接関係ないことも書いています。
今回利用しているtestsテーブルは以下の構成になっています
id text created_at updated_at 使い方
リポジトリのものを利用するのであれば、以下のコマンドを実行してください。
npm install mysql2
ライブラリだけを使用するのであれば、
mysql2とdate-utlisをインストールします
npm install mysql2
npm install date-utlis
そして以下のとおり、mysql2の設定ファイルを作ります。
プロジェクト名/database/mysqlConnectionconst mysql = require('mysql2'); //MySQLの接続設定 const connection = mysql.createConnection({ host: 'localhost', user: '', password: '', database: '' }).promise(); module.exports = connection;下記の通り、作りたいモデルクラスに設定します。
また以下のメソッドをオーバーライド、オーバーロードします。
abstractTABLE_NAME
abstractVALIDATIONRULES
updateメソッド
abstractTABLE_NAMEは、連携しているテーブル名のです。
メソッドabstractVALIDATIONRULESは、バリエーションルールを記述する所です。
バリエーションのルールはvalidatorjsのものになります。
https://www.npmjs.com/package/validatorjsプロジェクト名/model/Testsconst AbstractModel = require('./AbstractModel'); const connection = require('../database/mysqlConnection'); require('date-utils'); class Tests extends AbstractModel { constructor() { super(); } /** * テーブル名 * @override * @returns {string} */ static get abstractTABLE_NAME() { return 'tests'; } /** * バリデーションルール * @override * @return {Object} */ static get abstractVALIDATIONRULES() { return { get:{ rule: { id: 'required|integer' }, errorMessage: { required: '必須項目です。', integer: '数値で入力してください' } }, //POSTリクエスト用 post: { rule: { text: 'required' }, errorMessage: { required: '必須項目です。', } }, //PUTリクエスト用 put: { rule: { id: 'required|integer', text: 'required' }, errorMessage: { required: '必須項目です。', integer: '数値で入力してください' } }, //DELETEリクエスト用 delete: { rule: { id: 'required|integer' }, errorMessage: { required: '必須項目です。', integer: '数値で入力してください' } } }; } /** * UPDATE文の準備を行って、親クラスのupdate()に実行をさせる * @param {Object} insertParam */ static async update(insertParam) { //UPDATE文 const sql = `UPDATE ${this.abstractTABLE_NAME} SET text = ?,updated_at = ? WHERE id = ?`; //create_at用の日付時間取得 insertParam.updated_at = new Date().toFormat('YYYY-MM-DD HH:MI:SS'); //SQLの実行 await super.update(insertParam, sql); } } module.exports = Tests;機能
基本的にライブラリの機能を使う場合は、Expressのルーティングファイルの無名関数に
asyncを付与します。プロジェクト名/routes/ルーティングファイルrouter.get('/tests', async (req, res, next) => {そして、ルーティングファイルで以下のライブラリを取り組みます。
プロジェクト名/routes/ルーティングファイルconst express = require('express'); const router = express.Router(); const validator = require('validatorjs'); const Tests = require('../model/Tests');ライブラリでデータベースを操作するメソッドは、Promiseオブジェクトがかかわっているため、呼び出しの際には、
awaitを付与します。プロジェクト名/routes/ルーティングファイル//レコードをすべて取得 await Test.all() //引数idのカラム取得 await Test.find(id) //引数idの存在確認を行う await Test.existId(id) //引数paramの値で新規登録を行う await Test.insert(param) //引数paramの値で更新を行う await Test.update(param) //引数idのレコードを削除する await Test.delete(id)all() レコードをすべて取得
プロジェクト名/routes/test.js/** * @GET * testsのレコードをすべて取得 */ router.get('/tests', async (req, res, next) => { try { //レコードをすべて取得 const allRows = await Tests.all(); //レコードを返す return res.send(allRows); } catch (error) { //レコードの取得失敗時 console.log(error); res.status(500); return res.send({'error': 'サーバー側でエラーが発生しました'}); } });http://localhost:3000/tests に
GETでアクセスします。http[ { "id": 1, "text": "これはテストです", "created_at": "2020-06-18T16:50:45.000Z", "updated_at": null }, { "id": 2, "text": "これはテストです", "created_at": "2020-06-18T16:50:50.000Z", "updated_at": null }, { "id": 3, "text": "これはテストです", "created_at": "2020-06-18T16:50:51.000Z", "updated_at": null } ]find(id) 引数idのカラム取得
プロジェクト名/routes/test.js/** * @GET * 指定されたidのカラムを取得 */ router.get('/tests/:id', async (req, res, next) => { //バリデーションの検証を受ける値 const verificationValue = { id: req.params.id } //バリデーションの結果にエラーがあるかのチェック const validation = new validator( verificationValue, Tests.abstractVALIDATIONRULES.get.rule, Tests.abstractVALIDATIONRULES.get.errorMessage ); if (validation.fails()) { //エラーを422で返す return res.status(422).send({errors: validation.errors.all()}); } try { //レコードを取得 const row = await Tests.find(verificationValue.id); //レコードを返す return res.send(row); } catch (error) { //レコードの取得失敗時 console.log(error); res.status(500); return res.send({'error': 'サーバー側でエラーが発生しました'}); } })http://localhost:3000/(testのidを指定) に
GETでアクセスします。http[ { "id": 1, "text": "これはテストです", "created_at": "2020-06-18T16:50:45.000Z", "updated_at": null } ]また、URIのidを指定する所が以下のとおり数値以外の場合は、エラーメッセージを返します。
http://localhost:3000/aaaa77http{ "errors": { "id": [ "数値で入力してください" ] } }insert() 引数paramの値で新規登録を行う
プロジェクト名/routes/test.js/** * @POST * testsに新しいレコードを挿入 */ router.post('/test', async (req, res, next) => { //バリデーションの検証を受ける値 const verificationValue = { text: req.query.text } //バリデーションの結果にエラーがあるかのチェック const validation = new validator( verificationValue, Tests.abstractVALIDATIONRULES.post.rule, Tests.abstractVALIDATIONRULES.post.errorMessage ); if (validation.fails()) { //エラーを422で返す return res.status(422).send({errors: validation.errors.all()}); } try { //レコードの挿入開始 await Tests.insert({text: req.query.text}); return res.send({'insertResult': true}); } catch (error) { //レコードの挿入失敗時 console.log(error); return res.status(500).send({'insertResult': false}); } });http://localhost:3000/test?text=テストやりたい に
POSTでアクセスします。http[ { "id": 1, "text": "これはテストです", "created_at": "2020-06-18T16:50:45.000Z", "updated_at": null } ]リクエストのボディにtextがない場合は、エラーメッセージを返します。
http{ "errors": { "text": [ "必須項目です。" ] } }update(param) 引数paramの値で更新を行う existId(id) 引数idの存在確認を行う
プロジェクト名/routes/test.js/** * @PUT * レコードの更新 */ router.put('/test/:id', async (req, res, next) => { //バリデーションの検証を受ける値 const verificationValue = { id: req.params.id, text: req.query.text } //バリデーションの結果にエラーがあるかのチェック const validation = new validator( verificationValue, Tests.abstractVALIDATIONRULES.put.rule, Tests.abstractVALIDATIONRULES.put.errorMessage ); if (validation.fails()) { //エラーを422で返す return res.status(422).send({errors: validation.errors.all()}); } //idは存在しないか? if (!await Tests.existId(verificationValue.id)) { //エラーを422で返す return res.status(422).send({ errors: { id: ['idが存在しません'] } }); } try { //レコードの更新開始 await Tests.update(verificationValue); return res.send({'updateResult': true}); } catch (error) { //レコードの更新失敗時 console.log(error); return res.status(500).send({'updateResult': false}); } });http://localhost:3000/test?text=テストやりたい に
PUTでアクセスします。http{ "updateResult": true }URIのidを指定する所が以下のとおり数値以外やリクエストのボディにtextがない場合は、エラーメッセージを返します。
http://localhost:3000/test/aaahttp{ "errors": { "id": [ "数値で入力してください" ], "text": [ "必須項目です。" ] } }そして、
find(id)の引数のidがレコードに存在しない場合、このようなエラーを返しますhttp{ "errors": { "id": [ "idが存在しません" ] } }delete(id) 引数idのレコードを削除する
プロジェクト名/routes/test.js/** * @DELETE * レコードの削除 */ router.delete('/test/:id', async (req, res, next) => { //バリデーションの検証を受ける値 const verificationValue = { id: req.params.id, } //バリデーションの結果にエラーがあるかのチェック const validation = new validator( verificationValue, Tests.abstractVALIDATIONRULES.delete.rule, Tests.abstractVALIDATIONRULES.delete.errorMessage ); if (validation.fails()) { //エラーを422で返す return res.status(422).send({errors: validation.errors.all()}); } //idは存在しないか? if (!await Tests.existId(verificationValue.id)) { //エラーを422で返す return res.status(422).send({ errors: { id: ['idが存在しません'] } }); } try { //レコードの削除開始 await Tests.delete(verificationValue.id); return res.send({'deleteResult': true}); } catch (error) { //レコードの削除失敗時 console.log(error); return res.status(500).send({'deleteResult': false}); } })http://localhost:3000/(testのidを指定) に
DELETEでアクセスします。http[ { "id": 1, "text": "これはテストです", "created_at": "2020-06-18T16:50:45.000Z", "updated_at": null } ]URIのidを指定する所が以下のとおり数値以外の場合は、エラーメッセージを返します。
http://localhost:3000/aaaa77http{ "errors": { "id": [ "数値で入力してください" ] } }そして、
find(id)の引数のidがレコードに存在しない場合、このようなエラーを返しますhttp{ "errors": { "id": [ "idが存在しません" ] } }
- 投稿日:2020-06-19T09:17:04+09:00
JavaScriptワンライナー #1 任意長のランダムな文字列を作る
シェル芸ならぬワンライナー芸@JavaScriptで色々やってみようという試みです。
今回はテストデータを作るときに役に立つかもしれない「任意長のランダムな文字列を作る」というのをやってみます。
「ランダムな文字列を作る」というのをざっと調べるとよく見つかるコードにこんなのがあります。Math.random().toString(36).slice(-8);
- 乱数を発生させます。
- それを36進数としてパースすれば
0~9a~zで値が表現されます。- そのままでは先頭に
0.というのが付いてしまうのでお尻から使えるところを拾っています。都合の良いワンライナーですので、ここから発展させてみたいと思います。
0.を捨てるMath.random().toString(36).split('.')[1];
- ピリオドで分割します。
- 後ろを拾います。
もっと長くしたい
このままでは得られる文字列の長さに限界があるので適当に繋げてみます。
先ずは4つぐらいかな...[0,1,2,3].map(() => Math.random().toString(36).split('.')[1]).join('');
- 先程の文字列を返すアロー関数を用意します。
- 4つの要素を持つ適当な配列から
mapメソッドで文字列を配列化します。joinで連結して1つの文字列にします。任意長にしたい
この方法で任意長にするためには配列の要素数を適切に設定する必要があります。
そのためには1回で得られる文字列の長さを把握しておきたいものです。
試しにこんな事をしてみますと...for (let i = 0; i < 8; i++) console.log(Math.random().toString(36));結果が
0.tss9ygi8yk7 0.cbl7c75unf 0.oi3ntqpk9f 0.3u3sulrw5ig 0.ph9gzmq637c 0.2pn7h79139 0.eqdmmpp0kyg 0.q1zz93b8csc少なくとも10桁は使えそうです。
念の為、9桁使うという想定で進めます。(小心者)欲しい長さを変数
lenで与えたとして必要な要素数の配列を得るには...[...Array(Math.ceil(len/9))]
- 欲しい長さを一度に得られる文字数で割ります。
- 切り上げで整数化します。
- 便利なオマジナイで配列を生成します。
これを予め作っていたコードに適用してみます。
[...Array(Math.ceil(len/9))].map(() => Math.random().toString(36).split('.')[1]).join('').slice(-len);
- 必要な長さを十分に満たす文字列を作ります。
- 与えられた長さを切り出します。
長さ
0以下を指定された場合の対策このままでは長さに
0を指定されると都合が悪いので、その場合は長さ0の文字列を返すことにします。const randomString = (len) => len > 0 ? [...Array(Math.ceil(len/9))].map(() => Math.random().toString(36).split('.')[1]).join('').slice(-len) : '';結果
出来上がりました。
動かしてみましょう。const randomString = (len) => len > 0 ? [...Array(Math.ceil(len/9))].map(() => Math.random().toString(36).split('.')[1]).join('').slice(-len) : ''; for (let i = -1; i < 20; i++) console.log(randomString(i))結果は
f in bso 2qz6 6ywe9 1hdq1r a7nxjoa yaamsm56 0mu9x06ft 5vogkds05g e49k52cmq1m l9lqg4npbdag mtlygyh0dntn5 zkvbccv93eqbcg 0ywlv1k5wq9fj6q k8amki0fo5de4r4h vn05f4lic4ls3z3jd knmcn6l5nex6xflnvk c0h63fzghj9rfkq97qo狙い通りです。
めでたしめでたし



